diff --git a/.github/workflows/ci-lint.yml b/.github/workflows/ci-lint.yml index 0bc791433a..0f0c455723 100644 --- a/.github/workflows/ci-lint.yml +++ b/.github/workflows/ci-lint.yml @@ -12,4 +12,4 @@ jobs: shell: bash - name: Check workflow files run: ${{ steps.get_actionlint.outputs.executable }} -color - shell: bash \ No newline at end of file + shell: bash diff --git a/.github/workflows/light-examples-tests.yml b/.github/workflows/light-examples-tests.yml index fada8d6fca..ef1047f13e 100644 --- a/.github/workflows/light-examples-tests.yml +++ b/.github/workflows/light-examples-tests.yml @@ -4,12 +4,16 @@ on: - main paths: - "examples/**" + - "program-tests/sdk-anchor-test/**" + - "program-tests/sdk-pinocchio-test/**" - "sdk-libs/**" pull_request: branches: - "*" paths: - "examples/**" + - "program-tests/sdk-anchor-test/**" + - "program-tests/sdk-pinocchio-test/**" - "sdk-libs/**" types: - opened @@ -24,8 +28,8 @@ concurrency: cancel-in-progress: true jobs: - system-programs: - name: system-programs + examples-tests: + name: examples-tests if: github.event.pull_request.draft == false runs-on: ubuntu-latest timeout-minutes: 60 diff --git a/.github/workflows/sdk-tests.yml b/.github/workflows/sdk-tests.yml new file mode 100644 index 0000000000..852a15fee7 --- /dev/null +++ b/.github/workflows/sdk-tests.yml @@ -0,0 +1,89 @@ +on: + push: + branches: + - main + paths: + - "sdk-tests/**" + - "sdk-libs/**" + - "program-libs/**" + - ".github/workflows/sdk-tests.yml" + pull_request: + branches: + - "*" + paths: + - "sdk-tests/**" + - "sdk-libs/**" + - "program-libs/**" + - ".github/workflows/sdk-tests.yml" + types: + - opened + - synchronize + - reopened + - ready_for_review + +name: sdk-tests + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + sdk-tests: + name: sdk-tests + if: github.event.pull_request.draft == false + runs-on: warp-ubuntu-latest-x64-4x + timeout-minutes: 60 + + services: + redis: + image: redis:8.0.1 + ports: + - 6379:6379 + options: >- + --health-cmd "redis-cli ping" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + env: + REDIS_URL: redis://localhost:6379 + RUST_MIN_STACK: 8388608 + + steps: + - name: Checkout sources + uses: actions/checkout@v4 + + - name: Setup and build + uses: ./.github/actions/setup-and-build + with: + skip-components: "redis" + + - name: Build CLI + run: | + source ./scripts/devenv.sh + npx nx build @lightprotocol/zk-compression-cli + + - name: Build core programs + run: | + source ./scripts/devenv.sh + npx nx build @lightprotocol/programs + + - name: Build and test all sdk-tests programs + run: | + source ./scripts/devenv.sh + # Increase stack size for SBF compilation to avoid regex_automata stack overflow + export RUST_MIN_STACK=16777216 + # Remove -D warnings flag for SBF compilation to avoid compilation issues + export RUSTFLAGS="" + + echo "Building and testing all sdk-tests programs sequentially..." + # Build and test each program one by one to ensure .so files exist + + echo "Building and testing native-compressible" + cargo-test-sbf -p native-compressible + + echo "Building and testing anchor-compressible" + cargo-test-sbf -p anchor-compressible + + echo "Building and testing anchor-compressible-derived" + cargo-test-sbf -p anchor-compressible-derived diff --git a/AUTO_SEED_GENERATION.md b/AUTO_SEED_GENERATION.md new file mode 100644 index 0000000000..884e8bd43b --- /dev/null +++ b/AUTO_SEED_GENERATION.md @@ -0,0 +1,158 @@ +# 🎉 Automatic Seed Generation - Zero Manual Implementation + +The `add_compressible_instructions_enhanced` macro now supports **completely automatic** seed generation for both PDA accounts and CToken accounts. **NO MORE MANUAL IMPLEMENTATION NEEDED!** + +## ✅ **The New Developer Experience** + +### **Before (Manual Hell):** 100+ lines of boilerplate + +```rust +// Manual PDA seed functions +pub fn get_user_record_seeds(user: &Pubkey) -> (Vec>, Pubkey) { /* 10 lines */ } +pub fn get_game_session_seeds(session_id: u64) -> (Vec>, Pubkey) { /* 10 lines */ } +pub fn get_placeholder_record_seeds(placeholder_id: u64) -> (Vec>, Pubkey) { /* 10 lines */ } + +// Manual CToken seed function +pub fn get_ctoken_signer_seeds(user: &Pubkey, mint: &Pubkey) -> (Vec>, Pubkey) { /* 15 lines */ } + +// Manual CTokenSeedProvider trait implementation +impl ctoken_seed_system::CTokenSeedProvider for CTokenAccountVariant { /* 30+ lines */ } +``` + +### **After (Pure Magic):** 1 macro call + +```rust +#[add_compressible_instructions_enhanced( + UserRecord = ("user_record", data.owner), + GameSession = ("game_session", data.session_id.to_le_bytes()), + PlaceholderRecord = ("placeholder_record", data.placeholder_id.to_le_bytes()), + CTokenSigner = ("ctoken_signer", ctx.fee_payer, ctx.mint) +)] +#[program] +pub mod my_program { + // Your instructions - zero seed boilerplate! 🎉 +} +``` + +## 🔥 **Syntax Guide** + +### **PDA Account Seeds** + +For PDA accounts, use `data.field_name` to access account data: + +```rust +UserRecord = ("user_record", data.owner), +GameSession = ("game_session", data.session_id.to_le_bytes()), +CustomAccount = ("custom", data.custom_field, data.another_field.to_le_bytes()) +``` + +### **CToken Account Seeds** + +For CToken accounts, use `ctx.field_name` to access context: + +```rust +CTokenSigner = ("ctoken_signer", ctx.fee_payer, ctx.mint), +UserVault = ("user_vault", ctx.owner, ctx.mint), +CustomTokenAccount = ("custom_token", ctx.accounts.custom_field, ctx.mint) +``` + +### **Supported Expressions** + +The macro supports any valid Rust expression: + +```rust +// String literals +"user_record" + +// Data field access (for PDAs) +data.owner // Pubkey field +data.session_id.to_le_bytes() // u64 to bytes +data.custom_field // Any field + +// Context field access (for CTokens) +ctx.fee_payer // Standard context +ctx.mint // Standard context +ctx.owner // Standard context +ctx.accounts.user // Instruction account access + +// Complex expressions +some_id.to_be_bytes() +custom_calculation() +``` + +## 🚀 **Real World Examples** + +### **Gaming Platform** + +```rust +#[add_compressible_instructions_enhanced( + UserProfile = ("user_profile", data.owner), + GameSession = ("game_session", data.session_id.to_le_bytes()), + Achievement = ("achievement", data.player, data.achievement_id.to_le_bytes()), + GameToken = ("game_token", ctx.fee_payer, ctx.mint), + RewardVault = ("reward_vault", ctx.accounts.game_session, ctx.mint) +)] +``` + +### **DeFi Protocol** + +```rust +#[add_compressible_instructions_enhanced( + UserAccount = ("user_account", data.owner), + LendingPool = ("lending_pool", data.pool_id.to_le_bytes()), + Position = ("position", data.user, data.pool_id.to_le_bytes()), + LPToken = ("lp_token", ctx.fee_payer, ctx.mint), + RewardToken = ("reward_token", ctx.accounts.position, ctx.mint) +)] +``` + +### **NFT Marketplace** + +```rust +#[add_compressible_instructions_enhanced( + Listing = ("listing", data.seller, data.nft_mint), + Bid = ("bid", data.bidder, data.listing_id.to_le_bytes()), + Escrow = ("escrow", data.buyer, data.seller, data.nft_mint), + EscrowToken = ("escrow_token", ctx.accounts.escrow, ctx.mint) +)] +``` + +## ⚡ **Key Benefits** + +1. **🔥 Zero Boilerplate**: No manual seed functions or trait implementations +2. **🎯 Declarative**: Specify seeds directly in the macro +3. **🚀 Generic**: Works with any account structure and field types +4. **💪 Type-Safe**: Compile-time validation of seed specifications +5. **🔧 Flexible**: Support for complex expressions and field access patterns +6. **📚 Maintainable**: All seed logic centralized in one place +7. **⚡ Fast**: No runtime overhead, everything generated at compile time + +## 🎊 **Migration Guide** + +### **Step 1**: Remove manual implementations + +```rust +// DELETE THESE: +// pub fn get_user_record_seeds(...) -> (Vec>, Pubkey) { ... } +// impl CTokenSeedProvider for CTokenAccountVariant { ... } +``` + +### **Step 2**: Add seed specifications to macro + +```rust +// REPLACE THIS: +#[add_compressible_instructions_enhanced(UserRecord, GameSession)] + +// WITH THIS: +#[add_compressible_instructions_enhanced( + UserRecord = ("user_record", data.owner), + GameSession = ("game_session", data.session_id.to_le_bytes()) +)] +``` + +### **Step 3**: Enjoy zero-maintenance seed management! 🎉 + +--- + +**The manual implementation era is OVER.** 💀 +**Welcome to the age of automatic seed generation!** 🚀 diff --git a/Cargo.lock b/Cargo.lock index 184e3dd039..299fc743fc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -44,7 +44,7 @@ version = "1.1.0" dependencies = [ "account-compression", "anchor-lang", - "anchor-spl", + "anchor-spl 0.31.1 (registry+https://github.com/rust-lang/crates.io-index)", "ark-bn254 0.5.0", "ark-ff 0.5.0", "light-account-checks", @@ -256,7 +256,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", @@ -273,6 +273,70 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "anchor-compressible" +version = "0.1.0" +dependencies = [ + "anchor-lang", + "anchor-spl 0.31.1 (git+https://github.com/lightprotocol/anchor?rev=d8a2b3d9)", + "borsh 0.10.4", + "light-client", + "light-compressed-account", + "light-compressed-token-sdk", + "light-compressed-token-types", + "light-compressible-client", + "light-ctoken-types", + "light-hasher", + "light-macros", + "light-program-test", + "light-sdk", + "light-sdk-types", + "light-test-utils", + "solana-account", + "solana-instruction", + "solana-keypair", + "solana-logger", + "solana-program", + "solana-pubkey", + "solana-sdk", + "solana-signature", + "solana-signer", + "tokio", +] + +[[package]] +name = "anchor-compressible-derived" +version = "0.1.0" +dependencies = [ + "anchor-lang", + "anchor-spl 0.31.1 (git+https://github.com/lightprotocol/anchor?rev=d8a2b3d9)", + "borsh 0.10.4", + "light-client", + "light-compressed-account", + "light-compressed-token-sdk", + "light-compressed-token-types", + "light-compressible-client", + "light-ctoken-types", + "light-hasher", + "light-macros", + "light-program-test", + "light-sdk", + "light-sdk-macros", + "light-sdk-types", + "light-test-utils", + "solana-account", + "solana-instruction", + "solana-keypair", + "solana-logger", + "solana-program", + "solana-program-error", + "solana-pubkey", + "solana-sdk", + "solana-signature", + "solana-signer", + "tokio", +] + [[package]] name = "anchor-derive-accounts" version = "0.31.1" @@ -372,6 +436,22 @@ dependencies = [ "spl-token-metadata-interface", ] +[[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", + "spl-memo", + "spl-pod", + "spl-token", + "spl-token-2022 6.0.0", + "spl-token-group-interface", + "spl-token-metadata-interface", +] + [[package]] name = "anchor-syn" version = "0.31.1" @@ -1354,7 +1434,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", @@ -2103,6 +2183,21 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "835a3dc7d1ec9e75e2b5fb4ba75396837112d2060b03f7d43bc1897c7f7211da" +[[package]] +name = "fetch_accounts" +version = "0.1.0" +dependencies = [ + "base64 0.22.1", + "light-client", + "light-program-test", + "serde_json", + "solana-client", + "solana-rpc-client", + "solana-rpc-client-api", + "solana-sdk", + "tokio", +] + [[package]] name = "fiat-crypto" version = "0.2.9" @@ -2599,6 +2694,12 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + [[package]] name = "heck" version = "0.5.0" @@ -3378,6 +3479,7 @@ dependencies = [ name = "light-client" version = "0.13.1" dependencies = [ + "anchor-lang", "async-trait", "base64 0.13.1", "borsh 0.10.4", @@ -3406,6 +3508,7 @@ dependencies = [ "solana-hash", "solana-instruction", "solana-keypair", + "solana-message", "solana-program-error", "solana-pubkey", "solana-rpc-client", @@ -3483,6 +3586,7 @@ name = "light-compressed-token-sdk" version = "0.1.0" dependencies = [ "anchor-lang", + "anchor-spl 0.31.1 (git+https://github.com/lightprotocol/anchor?rev=d8a2b3d9)", "arrayvec", "borsh 0.10.4", "light-account-checks", @@ -3520,6 +3624,20 @@ dependencies = [ "thiserror 2.0.16", ] +[[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", + "thiserror 2.0.16", +] + [[package]] name = "light-concurrent-merkle-tree" version = "2.1.0" @@ -3725,6 +3843,7 @@ dependencies = [ "light-client", "light-compressed-account", "light-compressed-token", + "light-compressible-client", "light-concurrent-merkle-tree", "light-hasher", "light-indexed-array", @@ -3802,6 +3921,8 @@ name = "light-sdk" version = "0.13.0" dependencies = [ "anchor-lang", + "arrayvec", + "bincode", "borsh 0.10.4", "light-account-checks", "light-compressed-account", @@ -3812,11 +3933,16 @@ dependencies = [ "light-zero-copy", "num-bigint 0.4.6", "solana-account-info", + "solana-clock", "solana-cpi", "solana-instruction", "solana-msg", + "solana-program", "solana-program-error", "solana-pubkey", + "solana-rent", + "solana-system-interface", + "solana-sysvar", "thiserror 2.0.16", ] @@ -3825,6 +3951,7 @@ name = "light-sdk-macros" version = "0.13.0" dependencies = [ "borsh 0.10.4", + "heck 0.4.1", "light-compressed-account", "light-hasher", "light-macros", @@ -3931,7 +4058,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", @@ -4020,6 +4147,8 @@ version = "0.1.0" dependencies = [ "borsh 0.10.4", "lazy_static", + "light-hasher", + "light-sdk-macros", "light-zero-copy", "proc-macro2", "quote", @@ -4213,6 +4342,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" @@ -4231,6 +4373,26 @@ dependencies = [ "version_check", ] +[[package]] +name = "native-compressible" +version = "1.0.0" +dependencies = [ + "borsh 0.10.4", + "light-client", + "light-compressed-account", + "light-compressible-client", + "light-hasher", + "light-macros", + "light-program-test", + "light-sdk", + "light-sdk-types", + "solana-clock", + "solana-program", + "solana-sdk", + "solana-sysvar", + "tokio", +] + [[package]] name = "native-tls" version = "0.2.14" @@ -4346,6 +4508,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" @@ -5635,15 +5808,19 @@ name = "sdk-native-test" version = "1.0.0" dependencies = [ "borsh 0.10.4", + "light-client", "light-compressed-account", + "light-compressible-client", "light-hasher", "light-macros", "light-program-test", "light-sdk", "light-sdk-types", "light-zero-copy", + "solana-clock", "solana-program", "solana-sdk", + "solana-sysvar", "tokio", ] @@ -5669,7 +5846,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", @@ -6134,7 +6311,7 @@ dependencies = [ "bincode", "bytemuck", "log", - "num-derive", + "num-derive 0.4.2", "num-traits", "solana-address-lookup-table-interface", "solana-bincode", @@ -7351,7 +7528,7 @@ dependencies = [ "log", "memoffset", "num-bigint 0.4.6", - "num-derive", + "num-derive 0.4.2", "num-traits", "rand 0.8.5", "serde", @@ -7618,7 +7795,7 @@ dependencies = [ "dialoguer", "hidapi", "log", - "num-derive", + "num-derive 0.4.2", "num-traits", "parking_lot", "qstring", @@ -8585,7 +8762,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4507bb9d071fb81cfcf676f12fba3db4098f764524ef0b5567d671a81d41f3e" dependencies = [ "bincode", - "num-derive", + "num-derive 0.4.2", "num-traits", "serde", "serde_derive", @@ -8610,7 +8787,7 @@ checksum = "e0289c18977992907d361ca94c86cf45fd24cb41169fa03eb84947779e22933f" dependencies = [ "bincode", "log", - "num-derive", + "num-derive 0.4.2", "num-traits", "serde", "serde_derive", @@ -8643,7 +8820,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0a96b0ad864cc4d2156dbf0c4d7cadac4140ae13ebf7e856241500f74eca46f4" dependencies = [ "bytemuck", - "num-derive", + "num-derive 0.4.2", "num-traits", "solana-instruction", "solana-log-collector", @@ -8668,7 +8845,7 @@ dependencies = [ "js-sys", "lazy_static", "merlin", - "num-derive", + "num-derive 0.4.2", "num-traits", "rand 0.8.5", "serde", @@ -8696,7 +8873,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c540a4f7df1300dc6087f0cbb271b620dd55e131ea26075bb52ba999be3105f0" dependencies = [ "bytemuck", - "num-derive", + "num-derive 0.4.2", "num-traits", "solana-feature-set", "solana-instruction", @@ -8721,7 +8898,7 @@ dependencies = [ "itertools 0.12.1", "lazy_static", "merlin", - "num-derive", + "num-derive 0.4.2", "num-traits", "rand 0.8.5", "serde", @@ -8764,7 +8941,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", @@ -8867,7 +9044,7 @@ dependencies = [ "borsh 1.5.7", "bytemuck", "bytemuck_derive", - "num-derive", + "num-derive 0.4.2", "num-traits", "solana-decode-error", "solana-msg", @@ -8884,7 +9061,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", @@ -8910,7 +9087,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", @@ -8933,7 +9110,7 @@ checksum = "ed320a6c934128d4f7e54fe00e16b8aeaecf215799d060ae14f93378da6dc834" dependencies = [ "arrayref", "bytemuck", - "num-derive", + "num-derive 0.4.2", "num-traits", "num_enum", "solana-program", @@ -8948,7 +9125,7 @@ checksum = "5b27f7405010ef816587c944536b0eafbcc35206ab6ba0f2ca79f1d28e488f4f" dependencies = [ "arrayref", "bytemuck", - "num-derive", + "num-derive 0.4.2", "num-traits", "num_enum", "solana-program", @@ -8976,7 +9153,7 @@ checksum = "9048b26b0df0290f929ff91317c83db28b3ef99af2b3493dd35baa146774924c" dependencies = [ "arrayref", "bytemuck", - "num-derive", + "num-derive 0.4.2", "num-traits", "num_enum", "solana-program", @@ -9003,7 +9180,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", @@ -9112,7 +9289,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", @@ -9131,7 +9308,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", @@ -9153,7 +9330,7 @@ checksum = "4aa7503d52107c33c88e845e1351565050362c2314036ddf19a36cd25137c043" dependencies = [ "arrayref", "bytemuck", - "num-derive", + "num-derive 0.4.2", "num-traits", "solana-account-info", "solana-cpi", @@ -9177,7 +9354,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", @@ -9326,7 +9503,7 @@ version = "1.1.0" dependencies = [ "account-compression", "anchor-lang", - "anchor-spl", + "anchor-spl 0.31.1 (registry+https://github.com/rust-lang/crates.io-index)", "create-address-test-program", "light-account-checks", "light-batched-merkle-tree", @@ -9355,7 +9532,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", @@ -10905,6 +11082,7 @@ dependencies = [ "anyhow", "ark-bn254 0.5.0", "ark-ff 0.5.0", + "base64 0.13.1", "clap 4.5.46", "dirs", "groth16-solana", diff --git a/Cargo.toml b/Cargo.toml index 99df57b43c..620a214292 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,7 @@ members = [ "programs/registry", "anchor-programs/system", "sdk-libs/client", + "sdk-libs/compressible-client", "sdk-libs/macros", "sdk-libs/sdk", "sdk-libs/sdk-pinocchio", @@ -39,6 +40,7 @@ members = [ "program-tests/system-cpi-test", "program-tests/system-cpi-v2-test", "program-tests/system-test", + "sdk-tests/sdk-anchor-test/programs/sdk-anchor-test", "program-tests/create-address-test-program", "program-tests/utils", "program-tests/merkle-tree", @@ -51,6 +53,10 @@ members = [ "forester-utils", "forester", "sparse-merkle-tree", + "sdk-tests/anchor-compressible", + "sdk-tests/anchor-compressible-derived", + "sdk-tests/native-compressible", + "fetch-accounts", ] resolver = "2" @@ -97,6 +103,7 @@ solana-transaction = { version = "2.2" } solana-transaction-error = { version = "2.2" } solana-hash = { version = "2.2" } solana-clock = { version = "2.2" } +solana-rent = { version = "2.2" } solana-signature = { version = "2.2" } solana-commitment-config = { version = "2.2" } solana-account = { version = "2.2" } @@ -151,6 +158,9 @@ tracing-appender = "0.2.3" thiserror = "2.0" anyhow = "1.0" +# Serialization +bincode = "1.3" + ark-ff = "=0.5.0" ark-bn254 = "0.5" ark-serialize = "0.5" @@ -163,6 +173,7 @@ light-indexed-merkle-tree = { version = "2.1.0", path = "program-libs/indexed-me light-concurrent-merkle-tree = { version = "2.1.0", path = "program-libs/concurrent-merkle-tree" } light-sparse-merkle-tree = { version = "0.1.0", path = "sparse-merkle-tree" } light-client = { path = "sdk-libs/client", version = "0.13.1" } +light-compressible-client = { path = "sdk-libs/compressible-client", version = "0.13.1" } light-hasher = { path = "program-libs/hasher", version = "3.1.0" } light-macros = { path = "program-libs/macros", version = "2.1.0" } light-merkle-tree-reference = { path = "program-tests/merkle-tree", version = "2.0.0" } diff --git a/FLEXIBLE_CUSTOM_COMPRESSION_EXAMPLES.md b/FLEXIBLE_CUSTOM_COMPRESSION_EXAMPLES.md new file mode 100644 index 0000000000..0519ecba6e --- /dev/null +++ b/FLEXIBLE_CUSTOM_COMPRESSION_EXAMPLES.md @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/cli/accounts/batch_state_merkle_tree_2_2Yb3fGo2E9aWLjY8KuESaqurYpGGhEeJr7eynKrSgXwS.json b/cli/accounts/batch_state_merkle_tree_2_2Yb3fGo2E9aWLjY8KuESaqurYpGGhEeJr7eynKrSgXwS.json new file mode 100644 index 0000000000..d9d7c50e84 --- /dev/null +++ b/cli/accounts/batch_state_merkle_tree_2_2Yb3fGo2E9aWLjY8KuESaqurYpGGhEeJr7eynKrSgXwS.json @@ -0,0 +1 @@ +{"pubkey":"2Yb3fGo2E9aWLjY8KuESaqurYpGGhEeJr7eynKrSgXwS","account":{"lamports":291095040,"data":["QmF0Y2hNdGEDAAAAAAAAAA/Y1EfToz5VLJjxHxd2rjLiDsKHFAg5RA9dMMbnV0jYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABfAAAAAAAAAIgTAAAAAAAA/////////////////////wAAAAAAAAAATy/C0Fr8KxLYTClxCKFxErzKz3N965dup6b5TkvdJtsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAFAAAAAAAAAABAAAAAgAAAAAAAAAyAAAAAAAAAAoAAAAAAAAAAHECAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAAAAAAAHECAAAAAAAyAAAAAAAAAAoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAAAAAAAHECAAAAAAAyAAAAAAAAAAoAAAAAAAAAAAAAAAAAAAAyAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA5fn8H5ciLJn71SqM5QGCNQboCywgGwdP3kaAoW+6wQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAABQAAAAAAAAAFAAAAAAAAAAvaKHFjiV+QqF6bGHf9VUe1WC5kiqxGdWsjhhMlzTq2QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=","base64"],"owner":"compr6CUsB5m2jS4Y3831ztGSTnDpnKJTKS95d64XVq","executable":false,"rentEpoch":18446744073709551615,"space":41696}} \ No newline at end of file diff --git a/cli/accounts/batched_output_queue_2_12wJT3xYd46rtjeqDU6CrtT8unqLjPiheggzqhN9YsyB.json b/cli/accounts/batched_output_queue_2_12wJT3xYd46rtjeqDU6CrtT8unqLjPiheggzqhN9YsyB.json new file mode 100644 index 0000000000..0000b8d1b3 --- /dev/null +++ b/cli/accounts/batched_output_queue_2_12wJT3xYd46rtjeqDU6CrtT8unqLjPiheggzqhN9YsyB.json @@ -0,0 +1 @@ +{"pubkey":"12wJT3xYd46rtjeqDU6CrtT8unqLjPiheggzqhN9YsyB","account":{"lamports":29677440,"data":["cXVldWVhY2MP2NRH06M+VSyY8R8Xdq4y4g7ChxQIOUQPXTDG51dI2AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAXwAAAAAAAACIEwAAAAAAAP////////////////////8IUAAAAAAAAPKuWuX0POEKz8TJiMAjOgmV1yiV9Am40XHqZVvj8yn+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAAAAAAAAAIAAAAAAAAAMgAAAAAAAAAKAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMgAAAAAAAAAKAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMgAAAAAAAAAKAAAAAAAAAAAAAAAAAAAAMgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA5fn8H5ciLJn71SqM5QGCNQboCywgGwdP3kaAoW+6wQC+r4aTh/Zt5eeOfX6b7+tzLEswcugszBEGrJMWJ6HeAAAAAAAAAAAyAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=","base64"],"owner":"compr6CUsB5m2jS4Y3831ztGSTnDpnKJTKS95d64XVq","executable":false,"rentEpoch":18446744073709551615,"space":4136}} \ No newline at end of file diff --git a/cli/accounts/cpi_context_batched_2_HwtjxDvFEXiWnzeMeWkMBzpQN45A95rTJNZmz1Z3pe8R.json b/cli/accounts/cpi_context_batched_2_HwtjxDvFEXiWnzeMeWkMBzpQN45A95rTJNZmz1Z3pe8R.json new file mode 100644 index 0000000000..c226613fd6 --- /dev/null +++ b/cli/accounts/cpi_context_batched_2_HwtjxDvFEXiWnzeMeWkMBzpQN45A95rTJNZmz1Z3pe8R.json @@ -0,0 +1,14 @@ +{ + "account": { + "data": [ + "FhSV2krMgKYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABbzMBKdt4AzervcnTq70mQaynPIcOKwjsz2UC4spE/VAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + "base64" + ], + "executable": false, + "lamports": 143487360, + "owner": "SySTEM1eSU2p4BGQfQpimFEWWSC1XDFeun3Nqzz3rT7", + "rentEpoch": 18446744073709551615, + "space": 20488 + }, + "pubkey": "HwtjxDvFEXiWnzeMeWkMBzpQN45A95rTJNZmz1Z3pe8R" +} \ No newline at end of file diff --git a/cli/accounts/test_batched_cpi_context_7Hp52chxaew8bW1ApR4fck2bh6Y8qA1pu3qwH6N9zaLj.json b/cli/accounts/test_batched_cpi_context_7Hp52chxaew8bW1ApR4fck2bh6Y8qA1pu3qwH6N9zaLj.json new file mode 100644 index 0000000000..9b112e80e9 --- /dev/null +++ b/cli/accounts/test_batched_cpi_context_7Hp52chxaew8bW1ApR4fck2bh6Y8qA1pu3qwH6N9zaLj.json @@ -0,0 +1 @@ +{"account":{"data":["FhSV2krMgKYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPKuWuX0POEKz8TJiMAjOgmV1yiV9Am40XHqZVvj8yn+AAoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==","base64"],"executable":false,"lamports":143487360,"owner":"SySTEM1eSU2p4BGQfQpimFEWWSC1XDFeun3Nqzz3rT7","rentEpoch":0,"space":20488},"pubkey":"7Hp52chxaew8bW1ApR4fck2bh6Y8qA1pu3qwH6N9zaLj"} \ No newline at end of file diff --git a/cli/package.json b/cli/package.json index c962132242..4c812163a7 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "@lightprotocol/zk-compression-cli", - "version": "0.27.0", + "version": "0.27.1-alpha.1", "description": "ZK Compression: Secure Scaling on Solana", "maintainers": [ { @@ -19,6 +19,7 @@ "!bin/cargo-generate", "!/bin/**/*.vkey", "!/bin/proving-keys/*.key", + "!/bin/prover-windows-*.exe", "/bin/proving-keys/combined_26_1_1.key", "/bin/proving-keys/combined_26_1_2.key", "/bin/proving-keys/combined_26_2_1.key", @@ -37,6 +38,8 @@ "/bin/proving-keys/non-inclusion_26_2.key", "/bin/proving-keys/non-inclusion_40_1.key", "/bin/proving-keys/non-inclusion_40_2.key", + "/bin/proving-keys/non-inclusion_40_3.key", + "/bin/proving-keys/non-inclusion_40_4.key", "/dist", "/test_bin", "./config.json", diff --git a/cli/scripts/buildProver.sh b/cli/scripts/buildProver.sh index 17f0513626..394247937e 100755 --- a/cli/scripts/buildProver.sh +++ b/cli/scripts/buildProver.sh @@ -76,9 +76,11 @@ fi cd "$gnark_dir" -# Windows -build_prover windows amd64 "$out_dir"/prover-windows-x64.exe -build_prover windows arm64 "$out_dir"/prover-windows-arm64.exe +# Windows (only in development mode, not included in npm alpha releases to save space.) +if [ "$RELEASE_ONLY" = false ]; then + build_prover windows amd64 "$out_dir"/prover-windows-x64.exe + build_prover windows arm64 "$out_dir"/prover-windows-arm64.exe +fi # MacOS build_prover darwin amd64 "$out_dir"/prover-darwin-x64 diff --git a/cli/src/utils/constants.ts b/cli/src/utils/constants.ts index 7a3af7d19f..27b2e296cb 100644 --- a/cli/src/utils/constants.ts +++ b/cli/src/utils/constants.ts @@ -19,12 +19,13 @@ export const SOLANA_VALIDATOR_PROCESS_NAME = "solana-test-validator"; export const LIGHT_PROVER_PROCESS_NAME = "light-prover"; export const INDEXER_PROCESS_NAME = "photon"; -export const PHOTON_VERSION = "0.51.0"; +export const PHOTON_VERSION = "0.52.3"; // Set these to override Photon requirements with a specific git commit: export const USE_PHOTON_FROM_GIT = true; // If true, will show git install command instead of crates.io. -export const PHOTON_GIT_REPO = "https://github.com/helius-labs/photon.git"; -export const PHOTON_GIT_COMMIT = "b0ad386858384c22b4bb6a3bbbcd6a65911dac68"; // If empty, will use main branch. +export const PHOTON_GIT_REPO = "https://github.com/lightprotocol/photon.git"; +// added new v2 tree. +export const PHOTON_GIT_COMMIT = "6ba6813"; // If empty, will use main branch. export const LIGHT_PROTOCOL_PROGRAMS_DIR_ENV = "LIGHT_PROTOCOL_PROGRAMS_DIR"; export const BASE_PATH = "../../bin/"; diff --git a/cli/src/utils/process.ts b/cli/src/utils/process.ts index 6c824fce94..2eaddef190 100644 --- a/cli/src/utils/process.ts +++ b/cli/src/utils/process.ts @@ -198,6 +198,8 @@ export function spawnBinary(command: string, args: string[] = []) { const logDir = "test-ledger"; const binaryName = path.basename(command); + console.log("command", command); + console.log("args", args); const dir = path.join(__dirname, "../..", logDir); try { if (!fs.existsSync(dir)) { diff --git a/cli/src/utils/processProverServer.ts b/cli/src/utils/processProverServer.ts index af85ca26a5..4d447de8f0 100644 --- a/cli/src/utils/processProverServer.ts +++ b/cli/src/utils/processProverServer.ts @@ -147,6 +147,10 @@ export function getProverNameByArch(): string { let binaryName = `prover-${platform}-${arch}`; if (platform.toString() === "windows") { + throw new Error( + "Windows OS support is not included in this NPM release. Please reach out to team@lightprotocol.com to get the Windows binary.", + ); + //@ts-ignore binaryName += ".exe"; } return binaryName; diff --git a/cli/test_bin/lut.json b/cli/test_bin/lut.json new file mode 100644 index 0000000000..dc72f9a067 --- /dev/null +++ b/cli/test_bin/lut.json @@ -0,0 +1 @@ +{"account":{"data":["AQAAAP//////////VCmKFQAAAAAjAQFfKLpxtXVJdt0K8yLckTFEIuQhmYfmVBFu2pUHcB7pAADmyRiwvXzPkVREinq/ao85eCmh6P0Ip/DQs6q4eFL8MganVfghOQVNRCSxWvDEMM8vS3+YeTraElLUjzZmxsvOHuvEjPsyZLvDS/XFpag0/GdQAG8phtlvI1vIh1703UQLvA/Au0fKL3TEES6UqxPPo8Y05dwX6ssDzRojzX54fPuzKHUQXK6FtbREdgftv+FFJ7+0I5EcpAQjv9FSeiZ1CSw27CL1F4MA/bRKqmr/z/Ckbhy8ZBwOPtCdoTue9QgNAckoq0XTZVlaNOh3F/uwAIoZ0i8FLJ2h+YjEfFfeOguzCf2uDjrUg+kHQ7Tbp9KFUZWBkiLHcl2rauSSjP1VCKbpdecSJePoAVrHCv9ueLC92ILkkip+g4YPN4HoZU8IqnLs6kC6LnFYzDAA3P4bI+a9VH85QjlqZUSm8DujjAkVo1cjeU6Ptl0HW2tyaZw43QLllIt1sOWgQY6Al1tEBt324ddloZPZy+FGzut5rBy0he1fWzeROoz1hX7/AKkBXyi6cbV1SXbdCvMi3JExRCLkIZmH5lQRbtqVB3Ae6QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABt324e51j94YQl285GzN2rYa/E2DuQ0n/r35KNihi/wNAckoq0XTZVlaNOh3F/uwAIoZ0i8FLJ2h+YjEfFfeOguzCf2uDjrUg+kHQ7Tbp9KFUZWBkiLHcl2rauSSjP1VDQHJSTjmAlqoRvR63cIxy1VM6ev50lx9reTflsEae74Lswoa1RVJ0Mu8LffQ6X1uPzdaf1F4vE9YrAOb/3cFlg0ByVALKmF5+LZAuuTourIKXBRnlL+ueKqjmO2NT1YiC7MKL1NY6ztCiuxrOujiAu6LiQSD7ayhhKZ8zgIv+NoNAcl29p0oXdXdmJAZucjHinlDm0UdLZpmjkYlR7pjWAuzCj+l+30neNOGxmwvUYcBH5BcgmYI9bQ1G93CnQlFDQHJjHsR9+0RfSd6DupgTsrnyeMV7FkYPMUi9B72m0ELswpaaVYQ3FdNhY1zYZ6pCnZacHb3QkTqquAA790JMg0ByaKlHaua0II3lex12tecwzKR80ACCNv3pH4Uyo+nC7MKd57DjFifOFFdPZW8pdEs/3900yXEUGr46QcKMK8NAcm2aClU5SxQxOIA5pDWNyFEzGgedDouKX5Q/jSz2wuzCo8pqKAANMsHa1t957TmV6lEy1bPZfPPgZLRjHVsDQHJxMBQw9Ct1uCfVyi+5wxWd0O75gXvV0AhZ3pGjX4LswqjxSORQaRTjRJlkOMKDsJ0yZAyJlqje0FDEap/gg0BydoNu62NBXntAkJwqhoi8SO5svaEsnL1rvZDx6HSC7MKrILMRdfDTRC6TFZYXXbWukd/yxes5+TD3Rr5FDUNAcn7Dl44tSSTIed69Ah/g/iwTX7zFHBaoQ5CON87uguzCs6Ej4Y5TeYX2JLbRYORxgDKe+RqLb3KTQFD6xl9BqfVFxksXFEhjMlMPUrxf1ja7gibof1E49vZigAAAACMlyWPTiSJ8bs9ECkUjg2DC1oTmdr/EIQEjnvY2+n4WQ==","base64"],"executable":false,"lamports":9521280,"owner":"AddressLookupTab1e1111111111111111111111111","rentEpoch":18446744073709551615,"space":1240},"pubkey":"9NYFyEqPkyXUhkerbGHXUXkvb4qpzeEdHuGpgbgpH1NJ"} \ No newline at end of file diff --git a/fetch-accounts/Cargo.toml b/fetch-accounts/Cargo.toml new file mode 100644 index 0000000000..0a801ac19a --- /dev/null +++ b/fetch-accounts/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "fetch_accounts" +version = "0.1.0" +edition = "2021" + +[[bin]] +name = "fetch_test" +path = "src/main.rs" + +[[bin]] +name = "fetch_rpc" +path = "src/main_rpc.rs" + +[dependencies] +solana-sdk = "2.2" +solana-client = "2.2" +solana-rpc-client = "2.2" +solana-rpc-client-api = "2.2" +light-client = { path = "../sdk-libs/client" } +light-program-test = { path = "../sdk-libs/program-test", features = ["devenv"] } +tokio = { version = "1.45.1", features = ["rt", "macros", "rt-multi-thread"] } +base64 = "0.22" +serde_json = "1.0" diff --git a/fetch-accounts/README.md b/fetch-accounts/README.md new file mode 100644 index 0000000000..9f4e801e9c --- /dev/null +++ b/fetch-accounts/README.md @@ -0,0 +1,74 @@ +Scripts to fetch Solana accounts and save them as JSON files. Also supports LUTs. + +## Building + +```bash +cd fetch-accounts +cargo build --release +``` + +## Usage + +### 1. Test Env Fetcher (`fetch_test`) + +This script uses LightProgramTest to fetch accounts from test state trees: + +```bash +cargo run --bin fetch_test +``` + +### 2. RPC Fetcher (`fetch_rpc`) + +Fetch specific accounts from any Solana network: + +```bash +# Fetch from mainnet +NETWORK=mainnet cargo run --bin fetch_rpc ... + +# Fetch from devnet +NETWORK=devnet cargo run --bin fetch_rpc ... + +# Fetch from local validator +cargo run --bin fetch_rpc ... + +# Use custom RPC endpoint +RPC_URL=https://your-rpc.com cargo run --bin fetch_rpc ... +``` + +#### Network Options + +- `NETWORK=mainnet` - Solana Mainnet Beta +- `NETWORK=devnet` - Solana Devnet +- `NETWORK=testnet` - Solana Testnet +- `NETWORK=local` - Local validator (default) +- `RPC_URL=` - Custom RPC endpoint + +#### Regular Account Examples + +```bash +# Fetch System Program and Token Program from mainnet +NETWORK=mainnet cargo run --bin fetch_rpc \ + 11111111111111111111111111111111 \ + TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA + +# Fetch a specific account from devnet +NETWORK=devnet cargo run --bin fetch_rpc So11111111111111111111111111111111111111112 +``` + +### 3. Address Lookup Table + +Get LUTs for localnet. It sets `last_extended_slot = 0` so it works reliably with your +test-ledger. + +Upload the LUT to your test-ledger via: `--account LUT_ADDRESS_BASE58 ./dir/to/lut.json` + +```bash +# Process a lookup table from mainnet (sets last_extended_slot to 0) +IS_LUT=true NETWORK=mainnet cargo run --bin fetch_rpc + +# Process multiple lookup tables +IS_LUT=true NETWORK=mainnet cargo run --bin fetch_rpc + +# Use with custom RPC +IS_LUT=true RPC_URL=https://api.mainnet-beta.solana.com cargo run --bin fetch_rpc +``` diff --git a/fetch-accounts/src/main.rs b/fetch-accounts/src/main.rs new file mode 100644 index 0000000000..ee7be68c58 --- /dev/null +++ b/fetch-accounts/src/main.rs @@ -0,0 +1,94 @@ +use std::{fs::File, io::Write}; + +use base64::{engine::general_purpose, Engine as _}; +use light_program_test::{LightProgramTest, ProgramTestConfig, Rpc}; +use serde_json::json; +use solana_sdk::pubkey::Pubkey; + +/// Fetch the accounts for the two CPI contexts in the tree infos and write them +/// to JSON files. +#[tokio::main] +async fn main() -> Result<(), Box> { + let config = ProgramTestConfig::new_v2(false, None); + let rpc = LightProgramTest::new(config).await?; + + let tree_infos = rpc.get_state_tree_infos(); + + if tree_infos.len() < 2 { + println!("Less than 2 tree infos available"); + return Ok(()); + } + + let address_0 = tree_infos[0] + .cpi_context + .ok_or("No cpi_context for tree_info[0]")?; + let address_1 = tree_infos[1] + .cpi_context + .ok_or("No cpi_context for tree_info[1]")?; + + let account_0 = rpc + .get_account(address_0) + .await? + .ok_or("Account 0 not found")?; + let account_1 = rpc + .get_account(address_1) + .await? + .ok_or("Account 1 not found")?; + + write_account_json( + &account_0, + &address_0, + &format!("test_batched_cpi_context_{}.json", address_0), + )?; + write_account_json( + &account_1, + &address_1, + &format!("test_batched_cpi_context_{}.json", address_1), + )?; + + println!( + "Wrote account JSON to ./test_batched_cpi_context_{}.json and ./test_batched_cpi_context_{}.json", + address_0, + address_1 + ); + println!( + "Account 0: lamports={}, owner={}, executable={}, data_len={}", + account_0.lamports, + account_0.owner, + account_0.executable, + account_0.data.len() + ); + println!( + "Account 1: lamports={}, owner={}, executable={}, data_len={}", + account_1.lamports, + account_1.owner, + account_1.executable, + account_1.data.len() + ); + + Ok(()) +} + +fn write_account_json( + account: &solana_sdk::account::Account, + pubkey: &Pubkey, + filename: &str, +) -> Result<(), Box> { + let data_base64 = general_purpose::STANDARD.encode(&account.data); + let json_obj = json!({ + "pubkey": pubkey.to_string(), + "account": { + "lamports": account.lamports, + "data": [data_base64, "base64"], + "owner": account.owner.to_string(), + "executable": account.executable, + "rentEpoch": account.rent_epoch, + "space": account.data.len(), + } + }); + + let mut file = File::create(filename)?; + file.write_all(json_obj.to_string().as_bytes())?; + + Ok(()) +} diff --git a/fetch-accounts/src/main_rpc.rs b/fetch-accounts/src/main_rpc.rs new file mode 100644 index 0000000000..e4a3c690b2 --- /dev/null +++ b/fetch-accounts/src/main_rpc.rs @@ -0,0 +1,246 @@ +use std::{fs::File, io::Write, str::FromStr}; + +use base64::{engine::general_purpose, Engine as _}; +use serde_json::json; +use solana_client::rpc_client::RpcClient; +use solana_sdk::pubkey::Pubkey; + +fn main() -> Result<(), Box> { + let args: Vec = std::env::args().collect(); + + if args.len() < 2 { + print_usage(); + return Ok(()); + } + + // Set via RPC_URL env or use preset. + // env takes precedence. + let rpc_url = get_rpc_url(); + println!("Using RPC: {}", rpc_url); + + let client = RpcClient::new(rpc_url); + + let is_lut = std::env::var("IS_LUT").unwrap_or_default() == "true"; + for address_str in &args[1..] { + if is_lut { + fetch_and_process_lut(&client, address_str)?; + } else { + fetch_and_save_account(&client, address_str)?; + } + } + + println!("Processed {} accounts", args.len() - 1); + Ok(()) +} + +fn get_rpc_url() -> String { + if let Ok(custom_url) = std::env::var("RPC_URL") { + return custom_url; + } + + match std::env::var("NETWORK").as_deref() { + Ok("mainnet") => "https://api.mainnet-beta.solana.com".to_string(), + Ok("devnet") => "https://api.devnet.solana.com".to_string(), + Ok("testnet") => "https://api.testnet.solana.com".to_string(), + Ok("localnet") | Ok("local") => "http://localhost:8899".to_string(), + _ => "http://localhost:8899".to_string(), + } +} + +fn print_usage() { + println!("Account Fetcher - Fetch Solana accounts and save as JSON"); + println!(); + println!("USAGE:"); + println!(" cargo run --bin fetch_rpc ..."); + println!(); + println!("NETWORKS:"); + println!(" Set NETWORK environment variable:"); + println!(" NETWORK=mainnet - Solana Mainnet"); + println!(" NETWORK=devnet - Solana Devnet"); + println!(" NETWORK=testnet - Solana Testnet"); + println!(" NETWORK=local - Local validator (default)"); + println!(); + println!(" Or set custom RPC_URL:"); + println!(" RPC_URL=https://your-custom-rpc.com"); + println!(); + println!("LOOKUP TABLE MODE:"); + println!(" Set IS_LUT=true to decode/modify lookup tables:"); + println!(" IS_LUT=true NETWORK=mainnet cargo run --bin fetch_rpc "); + println!(); + println!("EXAMPLES:"); + println!(" # Fetch from mainnet"); + println!(" NETWORK=mainnet cargo run --bin fetch_rpc 11111111111111111111111111111111"); + println!(); + println!(" # Fetch multiple accounts from devnet"); + println!(" NETWORK=devnet cargo run --bin fetch_rpc \\"); + println!(" 11111111111111111111111111111111 \\"); + println!(" TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"); + println!(); + println!(" # Process lookup table"); + println!(" IS_LUT=true NETWORK=mainnet cargo run --bin fetch_rpc "); +} + +fn fetch_and_process_lut( + client: &RpcClient, + address_str: &str, +) -> Result<(), Box> { + let pubkey = Pubkey::from_str(address_str)?; + println!("Fetching lookup table: {}", pubkey); + + match client.get_account(&pubkey) { + Ok(account) => { + let modified_data = decode_and_modify_lut(&account.data)?; + + let filename = format!("modified_lut_{}.json", pubkey); + + let data_base64 = general_purpose::STANDARD.encode(&modified_data); + let json_obj = json!({ + "pubkey": pubkey.to_string(), + "account": { + "lamports": account.lamports, + "data": [data_base64, "base64"], + "owner": account.owner.to_string(), + "executable": account.executable, + "rentEpoch": account.rent_epoch, + "space": modified_data.len(), + } + }); + + let mut file = File::create(&filename)?; + file.write_all(json_obj.to_string().as_bytes())?; + + println!("Saved LUT {} with last_extended_slot set to 0", filename); + } + Err(e) => { + println!("Error fetching LUT {}: {}", pubkey, e); + return Err(e.into()); + } + } + + Ok(()) +} + +fn decode_and_modify_lut(data: &[u8]) -> Result, Box> { + if data.len() < 56 { + return Err("LUT data too small".into()); + } + + let mut modified_data = data.to_vec(); + + // Based on Solana's AddressLookupTable structure: + // - discriminator: u32 (4 bytes) - should be 1 for LookupTable + // - deactivation_slot: u64 (8 bytes) - at offset 4 + // - last_extended_slot: u64 (8 bytes) - at offset 12 *** THIS IS WHAT WE MODIFY *** + // - last_extended_slot_start_index: u8 (1 byte) - at offset 20 + // - authority: Option (33 bytes max) - at offset 21 + // - _padding: u16 (2 bytes) + + // CHECK: disc = 1 + let discriminator = u32::from_le_bytes([data[0], data[1], data[2], data[3]]); + if discriminator != 1 { + return Err(format!( + "Not a lookup table account (discriminator: {})", + discriminator + ) + .into()); + } + + let deactivation_slot = u64::from_le_bytes([ + data[4], data[5], data[6], data[7], data[8], data[9], data[10], data[11], + ]); + let current_last_extended_slot = u64::from_le_bytes([ + data[12], data[13], data[14], data[15], data[16], data[17], data[18], data[19], + ]); + let last_extended_slot_start_index = data[20]; + + println!(" Discriminator: {}", discriminator); + println!(" Deactivation slot: {}", deactivation_slot); + println!( + " Current last_extended_slot: {}", + current_last_extended_slot + ); + println!( + " Last extended slot start index: {}", + last_extended_slot_start_index + ); + + // Check authority (1 byte for Some/None + potentially 32 bytes for pubkey) + let has_authority = data[21] == 1; + if has_authority && data.len() >= 54 { + let authority_bytes = &data[22..54]; + let authority = Pubkey::try_from(authority_bytes)?; + println!(" Authority: {}", authority); + } else { + println!(" Authority: None"); + } + + // MUT: last_extended_slot to 0 (at offset 12, 8 bytes) + let zero_bytes = 0u64.to_le_bytes(); + modified_data[12..20].copy_from_slice(&zero_bytes); + + println!( + "🔧 Modified last_extended_slot: {} -> 0", + current_last_extended_slot + ); + + // Calculate number of addresses + let addresses_start = 56; // LOOKUP_TABLE_META_SIZE + if data.len() > addresses_start { + let addresses_data_len = data.len() - addresses_start; + let num_addresses = addresses_data_len / 32; + println!(" Number of addresses: {}", num_addresses); + } + + Ok(modified_data) +} + +fn fetch_and_save_account( + client: &RpcClient, + address_str: &str, +) -> Result<(), Box> { + let pubkey = Pubkey::from_str(address_str)?; + + match client.get_account(&pubkey) { + Ok(account) => { + let filename = format!("account_{}.json", pubkey); + write_account_json(&account, &pubkey, &filename)?; + + println!( + "Saved {} ({} bytes, {} lamports)", + filename, + account.data.len(), + account.lamports + ); + } + Err(e) => { + println!("Error fetching {}: {}", pubkey, e); + return Err(e.into()); + } + } + + Ok(()) +} + +fn write_account_json( + account: &solana_sdk::account::Account, + pubkey: &Pubkey, + filename: &str, +) -> Result<(), Box> { + let data_base64 = general_purpose::STANDARD.encode(&account.data); + let json_obj = json!({ + "pubkey": pubkey.to_string(), + "account": { + "lamports": account.lamports, + "data": [data_base64, "base64"], + "owner": account.owner.to_string(), + "executable": account.executable, + "rentEpoch": account.rent_epoch, + "space": account.data.len(), + } + }); + + let mut file = File::create(filename)?; + file.write_all(json_obj.to_string().as_bytes())?; + + Ok(()) +} diff --git a/js/compressed-token/CHANGELOG.md b/js/compressed-token/CHANGELOG.md index 5908f2f8fc..35beb46c54 100644 --- a/js/compressed-token/CHANGELOG.md +++ b/js/compressed-token/CHANGELOG.md @@ -1,9 +1,9 @@ ## [0.22.0] -- `CreateMint` action now allows passing a non-payer mint and freeze authority. -- More efficient computebudgets for actions. -- Better DX: Parameter lookup in call signatures of CompressedTokenProgram instructions -- QoL: improved typedocs. +- `CreateMint` action now allows passing a non-payer mint and freeze authority. +- More efficient computebudgets for actions. +- Better DX: Parameter lookup in call signatures of CompressedTokenProgram instructions +- QoL: improved typedocs. ## [0.21.0] @@ -58,25 +58,23 @@ const ix = await CompressedTokenProgram.decompress({ ### Overview -- new type: TokenPoolInfo -- Instruction Changes: +- new type: TokenPoolInfo +- Instruction Changes: + - `compress`, `mintTo`, `approveAndMintTo`, `compressSplTokenAccount` now require valid TokenPoolInfo + - `decompress` now requires an array of one or more TokenPoolInfos. + - `decompress`, `transfer` now do not allow state tree overrides. - - `compress`, `mintTo`, `approveAndMintTo`, `compressSplTokenAccount` now require valid TokenPoolInfo - - `decompress` now requires an array of one or more TokenPoolInfos. - - `decompress`, `transfer` now do not allow state tree overrides. +- Action Changes: + - Removed optional tokenProgramId: PublicKey + - removed optional merkleTree: PublicKey + - removed optional outputStateTree: PublicKey + - added optional stateTreeInfo: StateTreeInfo + - added optional tokenPoolInfo: TokenPoolInfo -- Action Changes: - - - Removed optional tokenProgramId: PublicKey - - removed optional merkleTree: PublicKey - - removed optional outputStateTree: PublicKey - - added optional stateTreeInfo: StateTreeInfo - - added optional tokenPoolInfo: TokenPoolInfo - -- new instructions: - - `approve`, `revoke`: delegated transfer support. - - `addTokenPools`: you can now register additional token pool pdas. Use - this if you need very high concurrency. +- new instructions: + - `approve`, `revoke`: delegated transfer support. + - `addTokenPools`: you can now register additional token pool pdas. Use + this if you need very high concurrency. ### Why the Changes are helpful @@ -96,32 +94,32 @@ accounts. ### Changed -- improved documentation and error messages. +- improved documentation and error messages. ## [0.20.4] - 2025-02-19 ### Breaking Changes -- `selectMinCompressedTokenAccountsForTransfer` and - `selectSmartCompressedTokenAccountsForTransfer` now throw an error - if not enough accounts are found. In most cases this is not a breaking - change, because a proof request would fail anyway. This just makes the error - message more informative. +- `selectMinCompressedTokenAccountsForTransfer` and + `selectSmartCompressedTokenAccountsForTransfer` now throw an error + if not enough accounts are found. In most cases this is not a breaking + change, because a proof request would fail anyway. This just makes the error + message more informative. ### Added -- `selectSmartCompressedTokenAccountsForTransfer` and - `selectSmartCompressedTokenAccountsForTransferOrPartial` +- `selectSmartCompressedTokenAccountsForTransfer` and + `selectSmartCompressedTokenAccountsForTransferOrPartial` ### Changed -- `selectMinCompressedTokenAccountsForTransfer` and - `selectMinCompressedTokenAccountsForTransferorPartial` now accept an optional - `maxInputs` parameter, defaulting to 4. +- `selectMinCompressedTokenAccountsForTransfer` and + `selectMinCompressedTokenAccountsForTransferorPartial` now accept an optional + `maxInputs` parameter, defaulting to 4. ### Security -- N/A +- N/A For previous release notes, check: https://www.zkcompression.com/release-notes/1.0.0-mainnet-beta diff --git a/js/compressed-token/README.md b/js/compressed-token/README.md index b7ae02a3d5..ebbc5e423e 100644 --- a/js/compressed-token/README.md +++ b/js/compressed-token/README.md @@ -28,8 +28,8 @@ npm install --save \ ### Documentation and examples -- [Latest Source code](https://github.com/lightprotocol/light-protocol/tree/main/js/compressed-token) -- [Creating and sending compressed tokens](https://www.zkcompression.com/developers/typescript-client#creating-minting-and-transferring-a-compressed-token) +- [Latest Source code](https://github.com/lightprotocol/light-protocol/tree/main/js/compressed-token) +- [Creating and sending compressed tokens](https://www.zkcompression.com/developers/typescript-client#creating-minting-and-transferring-a-compressed-token) ### Getting help @@ -38,9 +38,9 @@ Check out the [Light](https://discord.gg/CYvjBgzRFP) and [Helius](https://discor When asking for help, please include: -- A detailed description of what you're trying to achieve -- Source code, if possible -- The text of any errors you encountered, with stacktraces if available +- A detailed description of what you're trying to achieve +- Source code, if possible +- The text of any errors you encountered, with stacktraces if available ### Contributing diff --git a/js/compressed-token/package.json b/js/compressed-token/package.json index c142f89db7..0434f5aa43 100644 --- a/js/compressed-token/package.json +++ b/js/compressed-token/package.json @@ -1,6 +1,6 @@ { "name": "@lightprotocol/compressed-token", - "version": "0.22.0", + "version": "0.22.1-alpha.0", "description": "JS client to interact with the compressed-token program", "sideEffects": false, "main": "dist/cjs/node/index.cjs", diff --git a/js/compressed-token/rollup.config.js b/js/compressed-token/rollup.config.js index f19a4b3c29..12d2c4a462 100644 --- a/js/compressed-token/rollup.config.js +++ b/js/compressed-token/rollup.config.js @@ -20,7 +20,7 @@ const rolls = (fmt, env) => ({ external: [ '@solana/web3.js', '@solana/spl-token', - '@coral-xyz/borsh', + // '@coral-xyz/borsh', '@lightprotocol/stateless.js', ], plugins: [ diff --git a/js/compressed-token/src/compressible/derivation.ts b/js/compressed-token/src/compressible/derivation.ts new file mode 100644 index 0000000000..cd457ff500 --- /dev/null +++ b/js/compressed-token/src/compressible/derivation.ts @@ -0,0 +1,69 @@ +import { + COMPRESSED_TOKEN_PROGRAM_ID, + deriveAddressV2, + TreeInfo, +} from '@lightprotocol/stateless.js'; +import { PublicKey } from '@solana/web3.js'; + +/** + * Returns the compressed mint address as a Array (32 bytes). + */ +export function deriveCompressedMintAddress( + mintSeed: PublicKey, + addressTreeInfo: TreeInfo, +) { + // find_spl_mint_address returns [splMint, bump], we want splMint + // In JS, just use the mintSeed directly as the SPL mint address + const address = deriveAddressV2( + findMintAddress(mintSeed)[0].toBytes(), + addressTreeInfo.tree.toBytes(), + COMPRESSED_TOKEN_PROGRAM_ID.toBytes(), + ); + return Array.from(address); +} + +/// b"compressed_mint" +export const COMPRESSED_MINT_SEED = Buffer.from([ + 99, 111, 109, 112, 114, 101, 115, 115, 101, 100, 95, 109, 105, 110, 116, +]); + +/** + * Finds the SPL mint PDA for a compressed mint. + * @param mintSeed The mint seed public key. + * @returns [PDA, bump] + */ +export function findMintAddress(mintSigner: PublicKey): [PublicKey, number] { + const [address, bump] = PublicKey.findProgramAddressSync( + [COMPRESSED_MINT_SEED, mintSigner.toBuffer()], + COMPRESSED_TOKEN_PROGRAM_ID, + ); + return [address, bump]; +} + +/// Same as "getAssociatedTokenAddress" but returns the bump as well. +/// Uses compressed token program ID. +export function getAssociatedCTokenAddressAndBump( + owner: PublicKey, + mint: PublicKey, +) { + return PublicKey.findProgramAddressSync( + [ + owner.toBuffer(), + COMPRESSED_TOKEN_PROGRAM_ID.toBuffer(), + mint.toBuffer(), + ], + COMPRESSED_TOKEN_PROGRAM_ID, + ); +} + +/// Same as "getAssociatedTokenAddress" but implicitly uses compressed token program ID. +export function getAssociatedCTokenAddress(owner: PublicKey, mint: PublicKey) { + return PublicKey.findProgramAddressSync( + [ + owner.toBuffer(), + COMPRESSED_TOKEN_PROGRAM_ID.toBuffer(), + mint.toBuffer(), + ], + COMPRESSED_TOKEN_PROGRAM_ID, + )[0]; +} diff --git a/js/compressed-token/src/compressible/index.ts b/js/compressed-token/src/compressible/index.ts new file mode 100644 index 0000000000..d2225f33aa --- /dev/null +++ b/js/compressed-token/src/compressible/index.ts @@ -0,0 +1 @@ +export * from './derivation'; diff --git a/js/compressed-token/src/index.ts b/js/compressed-token/src/index.ts index 4e8896433d..9596a0ff6d 100644 --- a/js/compressed-token/src/index.ts +++ b/js/compressed-token/src/index.ts @@ -5,3 +5,4 @@ export * from './idl'; export * from './layout'; export * from './program'; export * from './types'; +export * from './compressible'; diff --git a/js/compressed-token/src/program.ts b/js/compressed-token/src/program.ts index ac19ee2c1f..9001b1cd9a 100644 --- a/js/compressed-token/src/program.ts +++ b/js/compressed-token/src/program.ts @@ -1082,60 +1082,66 @@ export class CompressedTokenProgram { recentSlot, remainingAccounts, }: CreateTokenProgramLookupTableParams) { - const [createInstruction, lookupTableAddress] = - AddressLookupTableProgram.createLookupTable({ - authority, - payer: authority, - recentSlot, - }); + // Gather all keys into a single deduped array before creating instructions + const allKeys: PublicKey[] = [ + SystemProgram.programId, + ComputeBudgetProgram.programId, + this.deriveCpiAuthorityPda, + LightSystemProgram.programId, + CompressedTokenProgram.programId, + defaultStaticAccountsStruct().registeredProgramPda, + defaultStaticAccountsStruct().noopProgram, + defaultStaticAccountsStruct().accountCompressionAuthority, + defaultStaticAccountsStruct().accountCompressionProgram, + defaultTestStateTreeAccounts().merkleTree, + defaultTestStateTreeAccounts().nullifierQueue, + defaultTestStateTreeAccounts().addressTree, + defaultTestStateTreeAccounts().addressQueue, + this.programId, + TOKEN_PROGRAM_ID, + TOKEN_2022_PROGRAM_ID, + authority, + ]; - let optionalMintKeys: PublicKey[] = []; if (mints) { - optionalMintKeys = [ + allKeys.push( ...mints, ...mints.map(mint => this.deriveTokenPoolPda(mint)), - ]; + ); } - const extendInstruction = AddressLookupTableProgram.extendLookupTable({ - payer, - authority, - lookupTable: lookupTableAddress, - addresses: [ - SystemProgram.programId, - ComputeBudgetProgram.programId, - this.deriveCpiAuthorityPda, - LightSystemProgram.programId, - CompressedTokenProgram.programId, - defaultStaticAccountsStruct().registeredProgramPda, - defaultStaticAccountsStruct().noopProgram, - defaultStaticAccountsStruct().accountCompressionAuthority, - defaultStaticAccountsStruct().accountCompressionProgram, - defaultTestStateTreeAccounts().merkleTree, - defaultTestStateTreeAccounts().nullifierQueue, - defaultTestStateTreeAccounts().addressTree, - defaultTestStateTreeAccounts().addressQueue, - this.programId, - TOKEN_PROGRAM_ID, - TOKEN_2022_PROGRAM_ID, - authority, - ...optionalMintKeys, - ], + if (remainingAccounts && remainingAccounts.length > 0) { + allKeys.push(...remainingAccounts); + } + + // Deduplicate keys + const seen = new Set(); + const dedupedKeys = allKeys.filter(key => { + const keyStr = key.toBase58(); + if (seen.has(keyStr)) return false; + seen.add(keyStr); + return true; }); - const instructions = [createInstruction, extendInstruction]; + const [createInstruction, lookupTableAddress] = + AddressLookupTableProgram.createLookupTable({ + authority, + payer: authority, + recentSlot, + }); + + const instructions = [createInstruction]; - if (remainingAccounts && remainingAccounts.length > 0) { - for (let i = 0; i < remainingAccounts.length; i += 25) { - const chunk = remainingAccounts.slice(i, i + 25); - const extendIx = AddressLookupTableProgram.extendLookupTable({ - payer, - authority, - lookupTable: lookupTableAddress, - addresses: chunk, - }); - instructions.push(extendIx); - } + // Add up to 25 keys per extend instruction + for (let i = 0; i < dedupedKeys.length; i += 25) { + const chunk = dedupedKeys.slice(i, i + 25); + const extendIx = AddressLookupTableProgram.extendLookupTable({ + payer, + authority, + lookupTable: lookupTableAddress, + addresses: chunk, + }); + instructions.push(extendIx); } return { diff --git a/js/stateless.js/CHANGELOG.md b/js/stateless.js/CHANGELOG.md index 92fa94e707..0b853bd9eb 100644 --- a/js/stateless.js/CHANGELOG.md +++ b/js/stateless.js/CHANGELOG.md @@ -36,15 +36,15 @@ migrating. ### Breaking changes -- Renamed `ActiveTreeBundle` to `StateTreeInfo` -- Updated `StateTreeInfo` internal structure: `{ tree: PublicKey, queue: PublicKey, cpiContext: PublicKey | null, treeType: TreeType }` -- Replaced `pickRandomTreeAndQueue` with `selectStateTreeInfo` -- Use `selectStateTreeInfo` for tree selection instead of `pickRandomTreeAndQueue` +- Renamed `ActiveTreeBundle` to `StateTreeInfo` +- Updated `StateTreeInfo` internal structure: `{ tree: PublicKey, queue: PublicKey, cpiContext: PublicKey | null, treeType: TreeType }` +- Replaced `pickRandomTreeAndQueue` with `selectStateTreeInfo` +- Use `selectStateTreeInfo` for tree selection instead of `pickRandomTreeAndQueue` ### Deprecations -- `rpc.getValidityProof` is now deprecated, use `rpc.getValidityProofV0` instead. -- `CompressedProof` and `CompressedProofWithContext` were renamed to `ValidityProof` and `ValidityProofWithContext` +- `rpc.getValidityProof` is now deprecated, use `rpc.getValidityProofV0` instead. +- `CompressedProof` and `CompressedProofWithContext` were renamed to `ValidityProof` and `ValidityProofWithContext` ### Migration Guide @@ -163,44 +163,43 @@ Fixed a bug where we lose precision on token amounts if compressed token account ### Breaking Changes -- ActiveTreeBundle is now a tuple of `tree`, `queue`, `cpiContext`, and `treeType`. `treeType` is a new enum ensuring forward compatibility. -- Updated LUT addresses for Mainnet and Devnet: - - stateTreeLookupTableMainnet = '7i86eQs3GSqHjN47WdWLTCGMW6gde1q96G2EVnUyK2st'; - - nullifiedStateTreeLookupTableMainnet = 'H9QD4u1fG7KmkAzn2tDXhheushxFe1EcrjGGyEFXeMqT'; - - stateTreeLookupTableDevnet = '8n8rH2bFRVA6cSGNDpgqcKHCndbFCT1bXxAQG89ejVsh'; - - nullifiedStateTreeLookupTableDevnet = '5dhaJLBjnVBQFErr8oiCJmcVsx3Zj6xDekGB2zULPsnP'; +- ActiveTreeBundle is now a tuple of `tree`, `queue`, `cpiContext`, and `treeType`. `treeType` is a new enum ensuring forward compatibility. +- Updated LUT addresses for Mainnet and Devnet: + - stateTreeLookupTableMainnet = '7i86eQs3GSqHjN47WdWLTCGMW6gde1q96G2EVnUyK2st'; + - nullifiedStateTreeLookupTableMainnet = 'H9QD4u1fG7KmkAzn2tDXhheushxFe1EcrjGGyEFXeMqT'; + - stateTreeLookupTableDevnet = '8n8rH2bFRVA6cSGNDpgqcKHCndbFCT1bXxAQG89ejVsh'; + - nullifiedStateTreeLookupTableDevnet = '5dhaJLBjnVBQFErr8oiCJmcVsx3Zj6xDekGB2zULPsnP'; ### Changed -- `createRpc` can now also be called with only the `rpcEndpoint` parameter. In - this case, `compressionApiEndpoint` and `proverEndpoint` will default to the - same value. If no parameters are provided, default localnet values are used. +- `createRpc` can now also be called with only the `rpcEndpoint` parameter. In + this case, `compressionApiEndpoint` and `proverEndpoint` will default to the + same value. If no parameters are provided, default localnet values are used. ## [0.19.0] - 2025-01-20 ### Breaking Changes -- Instruction methods (eg `LightSystemProgram.createAccount` and `CompressedTokenProgram.mintTo`) now require an explicit output state tree pubkey or input account, otherwise they will throw an error. +- Instruction methods (eg `LightSystemProgram.createAccount` and `CompressedTokenProgram.mintTo`) now require an explicit output state tree pubkey or input account, otherwise they will throw an error. ### Added -- Multiple State Tree support. Allows you to pass non-default state tree pubkeys to actions and instructions. Comes out of the box with public state trees. +- Multiple State Tree support. Allows you to pass non-default state tree pubkeys to actions and instructions. Comes out of the box with public state trees. + - `pickRandomStateTreeAndQueue` + - `getLightStateTreeInfo` - - `pickRandomStateTreeAndQueue` - - `getLightStateTreeInfo` - -- createMint allows passing of freezeAuthority in action +- createMint allows passing of freezeAuthority in action ### Changed -- `createMint`action now lets you pass tokenprogramId explicitly. is backward compatible with boolean flag for t22. +- `createMint`action now lets you pass tokenprogramId explicitly. is backward compatible with boolean flag for t22. ### Deprecated -- `rpc.getValidityProof`. Now does another rpc round trip to fetch tree info. use `rpc.getValidityProofV0` and pass tree info explicitly instead. +- `rpc.getValidityProof`. Now does another rpc round trip to fetch tree info. use `rpc.getValidityProofV0` and pass tree info explicitly instead. ### Security -- N/A +- N/A For previous release notes, check: https://www.zkcompression.com/release-notes/1.0.0-mainnet-beta diff --git a/js/stateless.js/COMPRESSIBLE_INSTRUCTION_EXAMPLE.md b/js/stateless.js/COMPRESSIBLE_INSTRUCTION_EXAMPLE.md new file mode 100644 index 0000000000..4a2aed51d7 --- /dev/null +++ b/js/stateless.js/COMPRESSIBLE_INSTRUCTION_EXAMPLE.md @@ -0,0 +1,466 @@ +# CompressibleInstruction TypeScript Implementation + +This document demonstrates the TypeScript equivalent of the Rust `CompressibleInstruction` module, now organized in a clean modular structure. + +## New Structure + +The compressible instruction functionality is now organized in `src/compressible/`: + +- **`types.ts`** - All TypeScript types and interfaces +- **`layout.ts`** - Borsh schemas and serialization functions +- **`instruction.ts`** - Standalone functions + optional class-based API +- **`index.ts`** - Clean exports and utilities + +## Usage Examples + +### Import Options + +```typescript +// Import everything from the compressible module +import { + // Action functions (high-level, recommended) + initializeCompressionConfig, + updateCompressionConfig, + compressAccount, + decompressAccountsIdempotent, + // Instruction builders (low-level) + createInitializeCompressionConfigInstruction, + createUpdateCompressionConfigInstruction, + createCompressAccountInstruction, + createDecompressAccountsIdempotentInstruction, + CompressibleInstruction, + deriveCompressionConfigAddress, + getProgramDataAccount, + checkProgramUpdateAuthority, + createCompressedAccountData, + serializeInitializeCompressionConfigData, + COMPRESSIBLE_DISCRIMINATORS, +} from '@lightprotocol/stateless.js/compressible'; + +// Or import specific items from main package +import { + initializeCompressionConfig, + createInitializeCompressionConfigInstruction, + deriveCompressionConfigAddress, + createCompressedAccountData, + COMPRESSIBLE_DISCRIMINATORS, +} from '@lightprotocol/stateless.js'; +``` + +### Initialize Compression Config (Action Function - Recommended) + +```typescript +import { initializeCompressionConfig } from '@lightprotocol/stateless.js'; +import { Rpc } from '../rpc'; // or your RPC setup + +// High-level action function handles transaction building and sending +const txSignature = await initializeCompressionConfig( + rpc, + payer, // Signer + programId, // PublicKey + authority, // Signer + compressionDelay, // number + rentRecipient, // PublicKey + addressSpace, // PublicKey[] + 0, // configBump (optional) + undefined, // custom discriminator (optional) + confirmOptions, // ConfirmOptions (optional) +); +``` + +### Initialize Compression Config (Instruction Builder) + +```typescript +import { + createCompressibleInitializeConfigInstruction, + COMPRESSIBLE_DISCRIMINATORS, +} from '@lightprotocol/stateless.js'; +import { PublicKey } from '@solana/web3.js'; + +// Using standard discriminator - standalone function (recommended) +const ix = createCompressibleInitializeConfigInstruction({ + programId, + discriminator: COMPRESSIBLE_DISCRIMINATORS.INITIALIZE_COMPRESSION_CONFIG, + payer: payer.publicKey, + authority: authority.publicKey, + compressionDelay, + rentRecipient, + addressSpace, + configBump: 0, +}); + +// Using custom discriminator - standalone function +const customDiscriminator = [1, 2, 3, 4, 5, 6, 7, 8]; +const customIx = createCompressibleInitializeConfigInstruction({ + programId, + discriminator: customDiscriminator, + payer: payer.publicKey, + authority: authority.publicKey, + compressionDelay, + rentRecipient, + addressSpace, +}); +``` + +### Initialize Compression Config (Class-based API) + +```typescript +import { + CompressibleInstruction, + COMPRESSIBLE_DISCRIMINATORS, +} from '@lightprotocol/stateless.js'; + +// Same functionality, class-based syntax +const ix = CompressibleInstruction.initializeCompressionConfig( + programId, + COMPRESSIBLE_DISCRIMINATORS.INITIALIZE_COMPRESSION_CONFIG, + payer.publicKey, + authority.publicKey, + compressionDelay, + rentRecipient, + addressSpace, + 0, // configBump +); +``` + +### Update Compression Config (Action Function - Recommended) + +```typescript +import { updateCompressionConfig } from '@lightprotocol/stateless.js'; + +// High-level action function +const txSignature = await updateCompressionConfig( + rpc, + payer, // Signer + programId, // PublicKey + authority, // Signer + newCompressionDelay, // number | null + newRentRecipient, // PublicKey | null + newAddressSpace, // PublicKey[] | null + newUpdateAuthority, // PublicKey | null + undefined, // custom discriminator (optional) + confirmOptions, // ConfirmOptions (optional) +); +``` + +### Update Compression Config (Instruction Builder) + +```typescript +import { + createUpdateCompressionConfigInstruction, + COMPRESSIBLE_DISCRIMINATORS, +} from '@lightprotocol/stateless.js'; + +// Low-level instruction builder +const updateIx = createUpdateCompressionConfigInstruction( + programId, + COMPRESSIBLE_DISCRIMINATORS.UPDATE_COMPRESSION_CONFIG, + authority.publicKey, + newCompressionDelay, + newRentRecipient, + newAddressSpace, + newUpdateAuthority, +); + +// Class-based alternative +const updateIx2 = CompressibleInstruction.updateCompressionConfig( + programId, + COMPRESSIBLE_DISCRIMINATORS.UPDATE_COMPRESSION_CONFIG, + authority.publicKey, + newCompressionDelay, + newRentRecipient, + newAddressSpace, + newUpdateAuthority, +); +``` + +### Compress Account + +```typescript +import { createCompressAccountInstruction } from '@lightprotocol/stateless.js'; + +// Standalone function (recommended) +const compressIx = createCompressAccountInstruction({ + programId, + discriminator: [1, 2, 3, 4, 5, 6, 7, 8], // custom discriminator + payer: payer.publicKey, + pdaToCompress, + rentRecipient, + compressedAccountMeta, + validityProof, + systemAccounts, +}); +``` + +### Decompress Accounts Idempotent + +```typescript +import * as borsh from '@coral-xyz/borsh'; +import { + createDecompressAccountsIdempotentInstruction, + COMPRESSIBLE_DISCRIMINATORS, +} from '@lightprotocol/stateless.js'; + +// Define your program-specific data schema +const MyDataSchema = borsh.struct([ + borsh.u64('amount'), + borsh.publicKey('mint'), + // ... other fields +]); + +type MyData = { + amount: BN; + mint: PublicKey; + // ... other fields +}; + +// Standalone function (recommended) +const decompressIx = createDecompressAccountsIdempotentInstruction({ + programId, + discriminator: COMPRESSIBLE_DISCRIMINATORS.DECOMPRESS_ACCOUNTS_IDEMPOTENT, + feePayer: feePayer.publicKey, + rentPayer: rentPayer.publicKey, + solanaAccounts, + compressedAccountsData, + bumps, + validityProof, + systemAccounts, + dataSchema: MyDataSchema, // Required for proper serialization +}); + +// Class-based alternative +const decompressIx2 = + CompressibleInstruction.decompressAccountsIdempotent( + programId, + COMPRESSIBLE_DISCRIMINATORS.DECOMPRESS_ACCOUNTS_IDEMPOTENT, + feePayer.publicKey, + rentPayer.publicKey, + solanaAccounts, + compressedAccountsData, + bumps, + validityProof, + systemAccounts, + MyDataSchema, + ); +``` + +## Helper Utilities + +### Direct Imports (Recommended) + +```typescript +import { + createCompressedAccountData, + deriveCompressionConfigAddress, + getProgramDataAccount, + checkProgramUpdateAuthority, + COMPRESSIBLE_DISCRIMINATORS, +} from '@lightprotocol/stateless.js'; + +// Create compressed account data +const compressedAccountData = createCompressedAccountData( + compressedAccount, + myDataVariant, + seeds, + outputStateTreeIndex, +); + +// Derive compression config PDA +const [configPda, bump] = deriveCompressionConfigAddress(programId, 0); + +// Get program data account for authority validation +const { programDataAddress, programDataAccountInfo } = + await getProgramDataAccount(programId, connection); + +// Check program update authority +checkProgramUpdateAuthority(programDataAccountInfo, authority); + +// Access standard discriminators +const discriminators = COMPRESSIBLE_DISCRIMINATORS; +``` + +### Class-Based API (Alternative) + +```typescript +import { CompressibleInstruction } from '@lightprotocol/stateless.js'; + +// Create compressed account data using class method +const compressedAccountData = + CompressibleInstruction.createCompressedAccountData( + compressedAccount, + myDataVariant, + seeds, + outputStateTreeIndex, + ); + +// Derive compression config PDA using class method +const [configPda, bump] = + CompressibleInstruction.deriveCompressionConfigAddress(programId, 0); + +// Get program data account using class method +const { programDataAddress, programDataAccountInfo } = + await CompressibleInstruction.getProgramDataAccount(programId, connection); + +// Check program update authority using class method +CompressibleInstruction.checkProgramUpdateAuthority( + programDataAccountInfo, + authority, +); + +// Access discriminators via class constant +const discriminators = CompressibleInstruction.DISCRIMINATORS; + +// Serialize config data using class method +const serializedData = + CompressibleInstruction.serializeInitializeCompressionConfigData( + compressionDelay, + rentRecipient, + addressSpace, + configBump, + ); +``` + +### Complete Workflow Example (Class-Based) + +```typescript +import { CompressibleInstruction } from '@lightprotocol/stateless.js'; +import { Connection, PublicKey } from '@solana/web3.js'; + +// All utilities available through one class +const programId = new PublicKey('...'); +const connection = new Connection('...'); +const authority = new PublicKey('...'); + +// Use class constants +const discriminator = + CompressibleInstruction.DISCRIMINATORS.INITIALIZE_COMPRESSION_CONFIG; + +// Use class utilities +const [configPda, bump] = + CompressibleInstruction.deriveCompressionConfigAddress(programId); +const { programDataAddress, programDataAccountInfo } = + await CompressibleInstruction.getProgramDataAccount(programId, connection); + +// Validate authority using class method +CompressibleInstruction.checkProgramUpdateAuthority( + programDataAccountInfo, + authority, +); + +// Create instruction using class method +const ix = CompressibleInstruction.initializeCompressionConfig( + programId, + discriminator, + payer.publicKey, + authority, + compressionDelay, + rentRecipient, + addressSpace, + bump, +); + +// Create compressed account data using class method +const compressedData = CompressibleInstruction.createCompressedAccountData( + compressedAccount, + myAccountData, + seeds, + outputStateTreeIndex, +); +``` + +## Type Definitions + +### Core Types + +```typescript +// Generic compressed account data for any program +type CompressedAccountData = { + meta: CompressedAccountMeta; + data: T; // Program-specific variant + seeds: Uint8Array[]; // PDA seeds without bump +}; + +// Instruction data for decompress idempotent +type DecompressMultipleAccountsIdempotentData = { + proof: ValidityProof; + compressedAccounts: CompressedAccountData[]; + bumps: number[]; + systemAccountsOffset: number; +}; + +// Update config instruction data +type UpdateCompressionConfigData = { + newCompressionDelay: number | null; + newRentRecipient: PublicKey | null; + newAddressSpace: PublicKey[] | null; + newUpdateAuthority: PublicKey | null; +}; +``` + +### Borsh Schemas + +```typescript +// Create custom schemas for your data types +export function createCompressedAccountDataSchema( + dataSchema: borsh.Layout, +): borsh.Layout>; + +export function createDecompressMultipleAccountsIdempotentDataSchema( + dataSchema: borsh.Layout, +): borsh.Layout>; +``` + +## Key Features + +1. **Clean Modular Structure**: Organized in `src/compressible/` with clear separation of concerns +2. **Dual API Design**: Both standalone functions (recommended) and class-based API +3. **Generic Type Support**: Works with any program-specific compressed account variant +4. **Custom Discriminators**: Always allows custom instruction discriminator bytes +5. **Borsh Serialization**: Uses `@coral-xyz/borsh` instead of Anchor dependency +6. **Solana SDK Patterns**: Follows patterns like `SystemProgram.transfer()` +7. **Type Safety**: Full TypeScript support with proper type checking +8. **Error Handling**: Comprehensive validation and error messages +9. **Tree Exports**: Clean imports from both main package and sub-modules + +## Comparison with Rust + +| Rust | TypeScript (Action) | TypeScript (Instruction) | TypeScript (Class) | +| ----------------------------------------------------------- | ---------------------------------------- | ---------------------------------------------------- | -------------------------------------------------------- | +| `CompressibleInstruction::initialize_compression_config()` | `initializeCompressionConfig(rpc, ...)` | `createInitializeCompressionConfigInstruction(...)` | `CompressibleInstruction.initializeCompressionConfig()` | +| `CompressibleInstruction::update_compression_config()` | `updateCompressionConfig(rpc, ...)` | `createUpdateCompressionConfigInstruction(...)` | `CompressibleInstruction.updateCompressionConfig()` | +| `CompressibleInstruction::compress_account()` | `compressAccount(rpc, ...)` | `createCompressAccountInstruction(...)` | `CompressibleInstruction.compressAccount()` | +| `CompressibleInstruction::decompress_accounts_idempotent()` | `decompressAccountsIdempotent(rpc, ...)` | `createDecompressAccountsIdempotentInstruction(...)` | `CompressibleInstruction.decompressAccountsIdempotent()` | +| `CompressedAccountData` | `CompressedAccountData` | `CompressedAccountData` | `CompressedAccountData` | +| `ValidityProof` | `ValidityProof` | `ValidityProof` | `ValidityProof` | +| `borsh::BorshSerialize` | `borsh.Layout` | `borsh.Layout` | `borsh.Layout` | + +## API Philosophy + +- **Action Functions**: Highest-level API. Handle RPC connection, transaction building, signing, and sending. Most convenient for applications. +- **Instruction Builders**: Mid-level API. Build individual `TransactionInstruction` objects. Good for custom transaction composition. +- **Utility Functions**: Helper functions for common operations like PDA derivation, account data creation, and authority validation. +- **Class-based API**: Complete alternative providing instruction builders, utilities, and constants through static methods. Familiar for teams migrating from other SDKs. + +### Recommendation + +1. **Use Action Functions** for most applications - they handle all the complexity +2. **Use Direct Utility Imports** for specific helper functions - clean and tree-shakeable +3. **Use Instruction Builders** when you need custom transaction composition or advanced control +4. **Use Class-based API** if your team prefers centralized class patterns or needs a single import + +### API Styles + +```typescript +// Direct imports (recommended for modern TS/JS) +import { + initializeCompressionConfig, + deriveCompressionConfigAddress, +} from '@lightprotocol/stateless.js'; + +// Class-based (alternative, all-in-one) +import { CompressibleInstruction } from '@lightprotocol/stateless.js'; +const config = + CompressibleInstruction.deriveCompressionConfigAddress(programId); +``` + +The TypeScript implementation provides equivalent functionality to Rust while maintaining TypeScript idioms and patterns in a clean, modular structure. diff --git a/js/stateless.js/README.md b/js/stateless.js/README.md index b25fada4d2..2765b9b65b 100644 --- a/js/stateless.js/README.md +++ b/js/stateless.js/README.md @@ -32,18 +32,18 @@ For a more detailed documentation on usage, please check [the respective section For example implementations, including web and Node, refer to the respective repositories: -- [Web application example implementation](https://github.com/Lightprotocol/example-web-client) +- [Web application example implementation](https://github.com/Lightprotocol/example-web-client) -- [Node server example implementation](https://github.com/Lightprotocol/example-nodejs-client) +- [Node server example implementation](https://github.com/Lightprotocol/example-nodejs-client) ## Troubleshooting Have a question or a problem? Feel free to ask in the [Light](https://discord.gg/CYvjBgzRFP) and [Helius](https://discord.gg/Uzzf6a7zKr) developer Discord servers. Please, include the following information: -- A detailed description or context of the issue or what you are trying to achieve. -- A code example that we can use to test and debug (if possible). Use [CodeSandbox](https://codesandbox.io/p/sandbox/vanilla-ts) or any other live environment provider. -- A description or context of any errors you are encountering with stacktraces if available. +- A detailed description or context of the issue or what you are trying to achieve. +- A code example that we can use to test and debug (if possible). Use [CodeSandbox](https://codesandbox.io/p/sandbox/vanilla-ts) or any other live environment provider. +- A description or context of any errors you are encountering with stacktraces if available. ### Source Maps @@ -57,14 +57,14 @@ We provide `index.js.map` for debugging. Exclude in production: Light and ZK Compression are open source protocols and very much welcome contributions. If you have a contribution, do not hesitate to send a PR to the respective repository or discuss in the linked developer Discord servers. -- 🐞 For bugs or feature requests, please open an - [issue](https://github.com/lightprotocol/light-protocol/issues/new). -- 🔒 For security vulnerabilities, please follow the [security policy](https://github.com/Lightprotocol/light-protocol/blob/main/SECURITY.md). +- 🐞 For bugs or feature requests, please open an + [issue](https://github.com/lightprotocol/light-protocol/issues/new). +- 🔒 For security vulnerabilities, please follow the [security policy](https://github.com/Lightprotocol/light-protocol/blob/main/SECURITY.md). ## Additional Resources -- [Light Protocol Repository](https://github.com/Lightprotocol/light-protocol) -- [ZK Compression Official Documentation](https://www.zkcompression.com/) +- [Light Protocol Repository](https://github.com/Lightprotocol/light-protocol) +- [ZK Compression Official Documentation](https://www.zkcompression.com/) ## Disclaimer diff --git a/js/stateless.js/package.json b/js/stateless.js/package.json index 411cd02345..a8bcb7e728 100644 --- a/js/stateless.js/package.json +++ b/js/stateless.js/package.json @@ -1,6 +1,6 @@ { "name": "@lightprotocol/stateless.js", - "version": "0.22.0", + "version": "0.22.1-alpha.0", "description": "JavaScript API for Light & ZK Compression", "sideEffects": false, "main": "dist/cjs/node/index.cjs", diff --git a/js/stateless.js/src/compressible/action.ts b/js/stateless.js/src/compressible/action.ts new file mode 100644 index 0000000000..554fa18758 --- /dev/null +++ b/js/stateless.js/src/compressible/action.ts @@ -0,0 +1,258 @@ +import { + ComputeBudgetProgram, + ConfirmOptions, + PublicKey, + Signer, + TransactionSignature, + AccountMeta, +} from '@solana/web3.js'; +import { sendAndConfirmTx, buildAndSignTx, dedupeSigner } from '../utils'; +import { Rpc } from '../rpc'; +import { ValidityProof } from '../state/types'; +import { CompressedAccountMeta } from '../state/compressed-account'; +import { + createInitializeCompressionConfigInstruction, + createUpdateCompressionConfigInstruction, + createCompressAccountInstruction, + createDecompressAccountsIdempotentInstruction, +} from './instruction'; +import { COMPRESSIBLE_DISCRIMINATORS, CompressedAccountData } from './types'; + +/** + * Initialize a compression config for a compressible program + * + * @param rpc RPC connection to use + * @param payer Fee payer + * @param programId Program ID for the compressible program + * @param authority Program upgrade authority + * @param compressionDelay Compression delay (in slots) + * @param rentRecipient Rent recipient public key + * @param addressSpace Array of address space public keys + * @param configBump Optional config bump (defaults to 0) + * @param discriminator Optional custom discriminator (defaults to standard) + * @param confirmOptions Options for confirming the transaction + * + * @return Signature of the confirmed transaction + */ +export async function initializeCompressionConfig( + rpc: Rpc, + payer: Signer, + programId: PublicKey, + authority: Signer, + compressionDelay: number, + rentRecipient: PublicKey, + addressSpace: PublicKey[], + configBump: number | null = null, + discriminator: + | Uint8Array + | number[] = COMPRESSIBLE_DISCRIMINATORS.INITIALIZE_COMPRESSION_CONFIG as unknown as number[], + confirmOptions?: ConfirmOptions, +): Promise { + const ix = createInitializeCompressionConfigInstruction( + programId, + discriminator, + payer.publicKey, + authority.publicKey, + compressionDelay, + rentRecipient, + addressSpace, + configBump, + ); + + const { blockhash } = await rpc.getLatestBlockhash(); + const additionalSigners = dedupeSigner(payer, [authority]); + + const tx = buildAndSignTx( + [ + ComputeBudgetProgram.setComputeUnitLimit({ + units: 200_000, + }), + ix, + ], + payer, + blockhash, + additionalSigners, + ); + + return await sendAndConfirmTx(rpc, tx, confirmOptions); +} + +/** + * Update a compression config for a compressible program + * + * @param rpc RPC connection to use + * @param payer Fee payer + * @param programId Program ID for the compressible program + * @param authority Current config authority + * @param newCompressionDelay Optional new compression delay + * @param newRentRecipient Optional new rent recipient + * @param newAddressSpace Optional new address space array + * @param newUpdateAuthority Optional new update authority + * @param discriminator Optional custom discriminator (defaults to standard) + * @param confirmOptions Options for confirming the transaction + * + * @return Signature of the confirmed transaction + */ +export async function updateCompressionConfig( + rpc: Rpc, + payer: Signer, + programId: PublicKey, + authority: Signer, + newCompressionDelay: number | null = null, + newRentRecipient: PublicKey | null = null, + newAddressSpace: PublicKey[] | null = null, + newUpdateAuthority: PublicKey | null = null, + discriminator: + | Uint8Array + | number[] = COMPRESSIBLE_DISCRIMINATORS.UPDATE_COMPRESSION_CONFIG as unknown as number[], + confirmOptions?: ConfirmOptions, +): Promise { + const ix = createUpdateCompressionConfigInstruction( + programId, + discriminator, + authority.publicKey, + newCompressionDelay, + newRentRecipient, + newAddressSpace, + newUpdateAuthority, + ); + + const { blockhash } = await rpc.getLatestBlockhash(); + const additionalSigners = dedupeSigner(payer, [authority]); + + const tx = buildAndSignTx( + [ + ComputeBudgetProgram.setComputeUnitLimit({ + units: 150_000, + }), + ix, + ], + payer, + blockhash, + additionalSigners, + ); + + return await sendAndConfirmTx(rpc, tx, confirmOptions); +} + +/** + * Compress a generic compressible account + * + * @param rpc RPC connection to use + * @param payer Fee payer and signer + * @param programId Program ID for the compressible program + * @param pdaToCompress PDA to compress + * @param rentRecipient Rent recipient public key + * @param compressedAccountMeta Compressed account metadata + * @param validityProof Validity proof for compression + * @param systemAccounts Additional system accounts (trees, queues, etc.) + * @param discriminator Custom instruction discriminator (8 bytes) + * @param confirmOptions Options for confirming the transaction + * + * @return Signature of the confirmed transaction + */ +export async function compressAccount( + rpc: Rpc, + payer: Signer, + programId: PublicKey, + pdaToCompress: PublicKey, + rentRecipient: PublicKey, + compressedAccountMeta: CompressedAccountMeta, + validityProof: ValidityProof, + systemAccounts: AccountMeta[], + discriminator: Uint8Array | number[], + confirmOptions?: ConfirmOptions, +): Promise { + const ix = createCompressAccountInstruction( + programId, + discriminator, + payer.publicKey, + pdaToCompress, + rentRecipient, + compressedAccountMeta, + validityProof, + systemAccounts, + ); + + const { blockhash } = await rpc.getLatestBlockhash(); + + const tx = buildAndSignTx( + [ + ComputeBudgetProgram.setComputeUnitLimit({ + units: 300_000, + }), + ix, + ], + payer, + blockhash, + ); + + return await sendAndConfirmTx(rpc, tx, confirmOptions); +} + +/** + * Decompress one or more compressed accounts idempotently + * + * @param rpc RPC connection to use + * @param payer Fee payer + * @param programId Program ID for the compressible program + * @param feePayer Fee payer (can be same as payer) + * @param rentPayer Rent payer + * @param solanaAccounts Array of PDA accounts to decompress + * @param compressedAccountsData Array of compressed account data + * @param bumps Array of PDA bumps + * @param validityProof Validity proof for decompression + * @param systemAccounts Additional system accounts (trees, queues, etc.) + * @param dataSchema Borsh schema for account data serialization + * @param discriminator Optional custom discriminator (defaults to standard) + * @param confirmOptions Options for confirming the transaction + * + * @return Signature of the confirmed transaction + */ +export async function decompressAccountsIdempotent( + rpc: Rpc, + payer: Signer, + programId: PublicKey, + feePayer: Signer, + rentPayer: Signer, + solanaAccounts: PublicKey[], + compressedAccountsData: CompressedAccountData[], + bumps: number[], + validityProof: ValidityProof, + systemAccounts: AccountMeta[], + dataSchema: any, // borsh.Layout + discriminator: + | Uint8Array + | number[] = COMPRESSIBLE_DISCRIMINATORS.DECOMPRESS_ACCOUNTS_IDEMPOTENT as unknown as number[], + confirmOptions?: ConfirmOptions, +): Promise { + const ix = createDecompressAccountsIdempotentInstruction( + programId, + discriminator, + feePayer.publicKey, + rentPayer.publicKey, + solanaAccounts, + compressedAccountsData, + bumps, + validityProof, + systemAccounts, + dataSchema, + ); + + const { blockhash } = await rpc.getLatestBlockhash(); + const additionalSigners = dedupeSigner(payer, [feePayer, rentPayer]); + + const tx = buildAndSignTx( + [ + ComputeBudgetProgram.setComputeUnitLimit({ + units: 400_000 + compressedAccountsData.length * 50_000, + }), + ix, + ], + payer, + blockhash, + additionalSigners, + ); + + return await sendAndConfirmTx(rpc, tx, confirmOptions); +} diff --git a/js/stateless.js/src/compressible/index.ts b/js/stateless.js/src/compressible/index.ts new file mode 100644 index 0000000000..b081876373 --- /dev/null +++ b/js/stateless.js/src/compressible/index.ts @@ -0,0 +1,85 @@ +export { + COMPRESSIBLE_DISCRIMINATORS, + DecompressMultipleAccountsIdempotentData, + UpdateCompressionConfigData, + GenericCompressAccountInstruction, +} from './types'; + +export { + UpdateCompressionConfigSchema, + ValidityProofSchema, + PackedStateTreeInfoSchema, + CompressedAccountMetaSchema, + GenericCompressAccountInstructionSchema, + createCompressedAccountDataSchema, + createDecompressMultipleAccountsIdempotentDataSchema, + serializeInstructionData, +} from './layout'; + +export { + createInitializeCompressionConfigInstruction, + createUpdateCompressionConfigInstruction, + createCompressAccountInstruction, + createDecompressAccountsIdempotentInstruction, + CompressibleInstruction, +} from './instruction'; + +export { + initializeCompressionConfig, + updateCompressionConfig, + compressAccount, + decompressAccountsIdempotent, +} from './action'; + +export { + deriveCompressionConfigAddress, + getProgramDataAccount, + checkProgramUpdateAuthority, +} from './utils'; + +export { serializeInitializeCompressionConfigData } from './layout'; + +import { CompressedAccount } from '../state/compressed-account'; +import { + PackedStateTreeInfo, + CompressedAccountMeta, +} from '../state/compressed-account'; +import { CompressedAccountData } from './types'; + +/** + * Convert a compressed account to the format expected by instruction builders + */ +export function createCompressedAccountData( + compressedAccount: CompressedAccount, + data: T, + seeds: Uint8Array[], + outputStateTreeIndex: number, +): CompressedAccountData { + // Note: This is a simplified version. The full implementation would need + // to handle proper tree info packing from ValidityProofWithContext + const treeInfo: PackedStateTreeInfo = { + rootIndex: 0, // Should be derived from ValidityProofWithContext + proveByIndex: compressedAccount.proveByIndex, + merkleTreePubkeyIndex: 0, // Should be derived from remaining accounts + queuePubkeyIndex: 0, // Should be derived from remaining accounts + leafIndex: compressedAccount.leafIndex, + }; + + const meta: CompressedAccountMeta = { + treeInfo, + address: compressedAccount.address + ? Array.from(compressedAccount.address) + : null, + lamports: compressedAccount.lamports, + outputStateTreeIndex, + }; + + return { + meta, + data, + seeds, + }; +} + +// Re-export for easy access following Solana SDK patterns +export { CompressibleInstruction as compressibleInstruction } from './instruction'; diff --git a/js/stateless.js/src/compressible/instruction.ts b/js/stateless.js/src/compressible/instruction.ts new file mode 100644 index 0000000000..fdcfdb8f95 --- /dev/null +++ b/js/stateless.js/src/compressible/instruction.ts @@ -0,0 +1,527 @@ +import { + PublicKey, + TransactionInstruction, + SystemProgram, + AccountMeta, +} from '@solana/web3.js'; +import { + CompressionConfigIxData, + UpdateCompressionConfigData, + GenericCompressAccountInstruction, + DecompressMultipleAccountsIdempotentData, +} from './types'; +import { + InitializeCompressionConfigSchema, + UpdateCompressionConfigSchema, + GenericCompressAccountInstructionSchema, + createDecompressMultipleAccountsIdempotentDataSchema, + serializeInstructionData, +} from './layout'; +import { + deriveCompressionConfigAddress, + getProgramDataAccount, + checkProgramUpdateAuthority, +} from './utils'; +import { serializeInitializeCompressionConfigData } from './layout'; +import { COMPRESSIBLE_DISCRIMINATORS, CompressedAccountData } from './types'; +import { CompressedAccount } from '../state/compressed-account'; +import { + PackedStateTreeInfo, + CompressedAccountMeta, +} from '../state/compressed-account'; + +/** + * Create an instruction to initialize a compression config. + * + * @param programId Program ID for the compressible program + * @param discriminator Instruction discriminator (8 bytes) + * @param payer Fee payer + * @param authority Program upgrade authority + * @param compressionDelay Compression delay (in slots) + * @param rentRecipient Rent recipient public key + * @param addressSpace Array of address space public keys + * @param configBump Optional config bump (defaults to 0) + * @returns TransactionInstruction + */ +export function createInitializeCompressionConfigInstruction( + programId: PublicKey, + discriminator: Uint8Array | number[], + payer: PublicKey, + authority: PublicKey, + compressionDelay: number, + rentRecipient: PublicKey, + addressSpace: PublicKey[], + configBump: number | null = null, +): TransactionInstruction { + const actualConfigBump = configBump ?? 0; + const [configPda] = deriveCompressionConfigAddress( + programId, + actualConfigBump, + ); + + // Get program data account for BPF Loader Upgradeable + const bpfLoaderUpgradeableId = new PublicKey( + 'BPFLoaderUpgradeab1e11111111111111111111111', + ); + const [programDataPda] = PublicKey.findProgramAddressSync( + [programId.toBuffer()], + bpfLoaderUpgradeableId, + ); + + const accounts = [ + { pubkey: payer, isSigner: true, isWritable: true }, // payer + { pubkey: configPda, isSigner: false, isWritable: true }, // config + { pubkey: programDataPda, isSigner: false, isWritable: false }, // program_data + { pubkey: authority, isSigner: true, isWritable: false }, // authority + { + pubkey: SystemProgram.programId, + isSigner: false, + isWritable: false, + }, // system_program + ]; + + const instructionData: CompressionConfigIxData = { + compressionDelay, + rentRecipient, + addressSpace, + configBump: actualConfigBump, + }; + + const data = serializeInstructionData( + InitializeCompressionConfigSchema, + instructionData, + discriminator, + ); + + return new TransactionInstruction({ + programId, + keys: accounts, + data, + }); +} + +/** + * Create an instruction to update a compression config. + * + * @param programId Program ID for the compressible program + * @param discriminator Instruction discriminator (8 bytes) + * @param authority Current config authority + * @param newCompressionDelay Optional new compression delay + * @param newRentRecipient Optional new rent recipient + * @param newAddressSpace Optional new address space array + * @param newUpdateAuthority Optional new update authority + * @returns TransactionInstruction + */ +export function createUpdateCompressionConfigInstruction( + programId: PublicKey, + discriminator: Uint8Array | number[], + authority: PublicKey, + newCompressionDelay: number | null = null, + newRentRecipient: PublicKey | null = null, + newAddressSpace: PublicKey[] | null = null, + newUpdateAuthority: PublicKey | null = null, +): TransactionInstruction { + const [configPda] = deriveCompressionConfigAddress(programId, 0); + + const accounts = [ + { pubkey: configPda, isSigner: false, isWritable: true }, // config + { pubkey: authority, isSigner: true, isWritable: false }, // authority + ]; + + const instructionData: UpdateCompressionConfigData = { + newCompressionDelay, + newRentRecipient, + newAddressSpace, + newUpdateAuthority, + }; + + const data = serializeInstructionData( + UpdateCompressionConfigSchema, + instructionData, + discriminator, + ); + + return new TransactionInstruction({ + programId, + keys: accounts, + data, + }); +} + +/** + * Create an instruction to compress a generic compressible account. + * + * @param programId Program ID for the compressible program + * @param discriminator Instruction discriminator (8 bytes) + * @param payer Fee payer + * @param pdaToCompress PDA to compress + * @param rentRecipient Rent recipient public key + * @param compressedAccountMeta Compressed account metadata + * @param validityProof Validity proof for compression + * @param systemAccounts Additional system accounts (optional) + * @returns TransactionInstruction + */ +export function createCompressAccountInstruction( + programId: PublicKey, + discriminator: Uint8Array | number[], + payer: PublicKey, + pdaToCompress: PublicKey, + rentRecipient: PublicKey, + compressedAccountMeta: import('../state/compressed-account').CompressedAccountMeta, + validityProof: import('../state/types').ValidityProof, + systemAccounts: AccountMeta[] = [], +): TransactionInstruction { + const [configPda] = deriveCompressionConfigAddress(programId, 0); + + // Create the instruction account metas + const accounts = [ + { pubkey: payer, isSigner: true, isWritable: true }, // user (signer) + { pubkey: pdaToCompress, isSigner: false, isWritable: true }, // pda_to_compress (writable) + { pubkey: configPda, isSigner: false, isWritable: false }, // config + { pubkey: rentRecipient, isSigner: false, isWritable: true }, // rent_recipient (writable) + ...systemAccounts, // Additional system accounts (trees, queues, etc.) + ]; + + const instructionData: GenericCompressAccountInstruction = { + proof: validityProof, + compressedAccountMeta, + }; + + const data = serializeInstructionData( + GenericCompressAccountInstructionSchema, + instructionData, + discriminator, + ); + + return new TransactionInstruction({ + programId, + keys: accounts, + data, + }); +} + +/** + * Create an instruction to decompress one or more compressed accounts idempotently. + * + * @param programId Program ID for the compressible program + * @param discriminator Instruction discriminator (8 bytes) + * @param feePayer Fee payer + * @param rentPayer Rent payer + * @param solanaAccounts Array of PDA accounts to decompress + * @param compressedAccountsData Array of compressed account data + * @param bumps Array of PDA bumps + * @param validityProof Validity proof for decompression + * @param systemAccounts Additional system accounts (optional) + * @param coder Borsh schema for account data + * @returns TransactionInstruction + */ +export function createDecompressAccountsIdempotentInstruction( + programId: PublicKey, + discriminator: Uint8Array | number[], + feePayer: PublicKey, + rentPayer: PublicKey, + solanaAccounts: PublicKey[], + compressedAccountsData: import('./types').CompressedAccountData[], + bumps: number[], + validityProof: import('../state/types').ValidityProof, + systemAccounts: AccountMeta[] = [], + coder: (data: any) => Buffer, +): TransactionInstruction { + // Validation + if (solanaAccounts.length !== compressedAccountsData.length) { + throw new Error( + 'PDA accounts and compressed accounts must have the same length', + ); + } + if (solanaAccounts.length !== bumps.length) { + throw new Error('PDA accounts and bumps must have the same length'); + } + + const [configPda] = deriveCompressionConfigAddress(programId, 0); + + // Build instruction accounts + const accounts: AccountMeta[] = [ + { pubkey: feePayer, isSigner: true, isWritable: true }, // fee_payer + { pubkey: rentPayer, isSigner: true, isWritable: true }, // rent_payer + { pubkey: configPda, isSigner: false, isWritable: false }, // config + ...systemAccounts, // Light Protocol system accounts (trees, queues, etc.) + ]; + + // Build instruction data + const instructionData: DecompressMultipleAccountsIdempotentData = { + proof: validityProof, + compressedAccounts: compressedAccountsData, + bumps, + systemAccountsOffset: solanaAccounts.length, + }; + + const data = coder(instructionData); + + return new TransactionInstruction({ + programId, + keys: accounts, + data, + }); +} + +/** + * Instruction builders for compressible accounts, following Solana SDK patterns. + */ +export class CompressibleInstruction { + /** + * Create an instruction to initialize a compression config. + * + * @param programId Program ID for the compressible program + * @param discriminator Instruction discriminator (8 bytes) + * @param payer Fee payer + * @param authority Program upgrade authority + * @param compressionDelay Compression delay (in slots) + * @param rentRecipient Rent recipient public key + * @param addressSpace Array of address space public keys + * @param configBump Optional config bump (defaults to 0) + * @returns TransactionInstruction + */ + static initializeCompressionConfig( + programId: PublicKey, + discriminator: Uint8Array | number[], + payer: PublicKey, + authority: PublicKey, + compressionDelay: number, + rentRecipient: PublicKey, + addressSpace: PublicKey[], + configBump: number | null = null, + ): TransactionInstruction { + return createInitializeCompressionConfigInstruction( + programId, + discriminator, + payer, + authority, + compressionDelay, + rentRecipient, + addressSpace, + configBump, + ); + } + + /** + * Create an instruction to update a compression config. + * + * @param programId Program ID for the compressible program + * @param discriminator Instruction discriminator (8 bytes) + * @param authority Current config authority + * @param newCompressionDelay Optional new compression delay + * @param newRentRecipient Optional new rent recipient + * @param newAddressSpace Optional new address space array + * @param newUpdateAuthority Optional new update authority + * @returns TransactionInstruction + */ + static updateCompressionConfig( + programId: PublicKey, + discriminator: Uint8Array | number[], + authority: PublicKey, + newCompressionDelay: number | null = null, + newRentRecipient: PublicKey | null = null, + newAddressSpace: PublicKey[] | null = null, + newUpdateAuthority: PublicKey | null = null, + ): TransactionInstruction { + return createUpdateCompressionConfigInstruction( + programId, + discriminator, + authority, + newCompressionDelay, + newRentRecipient, + newAddressSpace, + newUpdateAuthority, + ); + } + + /** + * Create an instruction to compress a generic compressible account. + * + * @param programId Program ID for the compressible program + * @param discriminator Instruction discriminator (8 bytes) + * @param payer Fee payer + * @param pdaToCompress PDA to compress + * @param rentRecipient Rent recipient public key + * @param compressedAccountMeta Compressed account metadata + * @param validityProof Validity proof for compression + * @param systemAccounts Additional system accounts (optional) + * @returns TransactionInstruction + */ + static compressAccount( + programId: PublicKey, + discriminator: Uint8Array | number[], + payer: PublicKey, + pdaToCompress: PublicKey, + rentRecipient: PublicKey, + compressedAccountMeta: import('../state/compressed-account').CompressedAccountMeta, + validityProof: import('../state/types').ValidityProof, + systemAccounts: AccountMeta[] = [], + ): TransactionInstruction { + return createCompressAccountInstruction( + programId, + discriminator, + payer, + pdaToCompress, + rentRecipient, + compressedAccountMeta, + validityProof, + systemAccounts, + ); + } + + /** + * Create an instruction to decompress one or more compressed accounts idempotently. + * + * @param programId Program ID for the compressible program + * @param discriminator Instruction discriminator (8 bytes) + * @param feePayer Fee payer + * @param rentPayer Rent payer + * @param solanaAccounts Array of PDA accounts to decompress + * @param compressedAccountsData Array of compressed account data + * @param bumps Array of PDA bumps + * @param validityProof Validity proof for decompression + * @param systemAccounts Additional system accounts (optional) + * @param dataSchema Borsh schema for account data + * @returns TransactionInstruction + */ + static decompressAccountsIdempotent( + programId: PublicKey, + discriminator: Uint8Array | number[], + feePayer: PublicKey, + rentPayer: PublicKey, + solanaAccounts: PublicKey[], + compressedAccountsData: import('./types').CompressedAccountData[], + bumps: number[], + validityProof: import('../state/types').ValidityProof, + systemAccounts: AccountMeta[] = [], + dataSchema?: any, + ): TransactionInstruction { + return createDecompressAccountsIdempotentInstruction( + programId, + discriminator, + feePayer, + rentPayer, + solanaAccounts, + compressedAccountsData, + bumps, + validityProof, + systemAccounts, + dataSchema, + ); + } + + /** + * Standard instruction discriminators for compressible instructions + */ + static readonly DISCRIMINATORS = COMPRESSIBLE_DISCRIMINATORS; + + /** + * Derive the compression config PDA address + * + * @param programId Program ID for the compressible program + * @param configIndex Config index (defaults to 0) + * @returns [PDA address, bump seed] + */ + static deriveCompressionConfigAddress( + programId: PublicKey, + configIndex: number = 0, + ): [PublicKey, number] { + return deriveCompressionConfigAddress(programId, configIndex); + } + + /** + * Get the program data account address and its raw data for a given program + * + * @param programId Program ID + * @param connection Solana connection + * @returns Program data address and account info + */ + static async getProgramDataAccount( + programId: PublicKey, + connection: import('@solana/web3.js').Connection, + ): Promise<{ + programDataAddress: PublicKey; + programDataAccountInfo: import('@solana/web3.js').AccountInfo; + }> { + return await getProgramDataAccount(programId, connection); + } + + /** + * Check that the provided authority matches the program's upgrade authority + * + * @param programDataAccountInfo Program data account info + * @param providedAuthority Authority to validate + * @throws Error if authority doesn't match + */ + static checkProgramUpdateAuthority( + programDataAccountInfo: import('@solana/web3.js').AccountInfo, + providedAuthority: PublicKey, + ): void { + checkProgramUpdateAuthority(programDataAccountInfo, providedAuthority); + } + + /** + * Serialize instruction data for initializeCompressionConfig using Borsh + * + * @param compressionDelay Compression delay (in slots) + * @param rentRecipient Rent recipient public key + * @param addressSpace Array of address space public keys + * @param configBump Optional config bump + * @returns Serialized instruction data with discriminator + */ + static serializeInitializeCompressionConfigData( + compressionDelay: number, + rentRecipient: PublicKey, + addressSpace: PublicKey[], + configBump: number | null, + ): Buffer { + return serializeInitializeCompressionConfigData( + compressionDelay, + rentRecipient, + addressSpace, + configBump, + ); + } + + /** + * Convert a compressed account to the format expected by instruction builders + * + * @param compressedAccount Compressed account from state + * @param data Program-specific account data + * @param seeds PDA seeds (without bump) + * @param outputStateTreeIndex Output state tree index + * @returns Compressed account data for instructions + */ + static createCompressedAccountData( + compressedAccount: CompressedAccount, + data: T, + seeds: Uint8Array[], + outputStateTreeIndex: number, + ): CompressedAccountData { + // Note: This is a simplified version. The full implementation would need + // to handle proper tree info packing from ValidityProofWithContext + const treeInfo: PackedStateTreeInfo = { + rootIndex: 0, // Should be derived from ValidityProofWithContext + proveByIndex: compressedAccount.proveByIndex, + merkleTreePubkeyIndex: 0, // Should be derived from remaining accounts + queuePubkeyIndex: 0, // Should be derived from remaining accounts + leafIndex: compressedAccount.leafIndex, + }; + + const meta: CompressedAccountMeta = { + treeInfo, + address: compressedAccount.address + ? Array.from(compressedAccount.address) + : null, + lamports: compressedAccount.lamports, + outputStateTreeIndex, + }; + + return { + meta, + data, + seeds, + }; + } +} diff --git a/js/stateless.js/src/compressible/layout.ts b/js/stateless.js/src/compressible/layout.ts new file mode 100644 index 0000000000..9126d1e170 --- /dev/null +++ b/js/stateless.js/src/compressible/layout.ts @@ -0,0 +1,155 @@ +import * as borsh from '@coral-xyz/borsh'; +import { ValidityProof } from '../state/types'; +import { + PackedStateTreeInfo, + CompressedAccountMeta, +} from '../state/compressed-account'; +import { + CompressionConfigIxData, + UpdateCompressionConfigData, + GenericCompressAccountInstruction, + CompressedAccountData, + DecompressMultipleAccountsIdempotentData, +} from './types'; + +/** + * Borsh schema for initializeCompressionConfig instruction data + * Note: This is also available from '@lightprotocol/stateless.js' main exports + */ +export const InitializeCompressionConfigSchema: borsh.Layout = + borsh.struct([ + borsh.u32('compressionDelay'), + borsh.publicKey('rentRecipient'), + borsh.vec(borsh.publicKey(), 'addressSpace'), + borsh.option(borsh.u8(), 'configBump'), + ]); + +/** + * Borsh schema for updateCompressionConfig instruction data + */ +export const UpdateCompressionConfigSchema: borsh.Layout = + borsh.struct([ + borsh.option(borsh.u32(), 'newCompressionDelay'), + borsh.option(borsh.publicKey(), 'newRentRecipient'), + borsh.option(borsh.vec(borsh.publicKey()), 'newAddressSpace'), + borsh.option(borsh.publicKey(), 'newUpdateAuthority'), + ]); + +/** + * Borsh schema for ValidityProof + */ +export const ValidityProofSchema: borsh.Layout = borsh.struct([ + borsh.array(borsh.u8(), 32, 'a'), + borsh.array(borsh.u8(), 64, 'b'), + borsh.array(borsh.u8(), 32, 'c'), +]); + +/** + * Borsh schema for PackedStateTreeInfo + */ +export const PackedStateTreeInfoSchema: borsh.Layout = + borsh.struct([ + borsh.u16('rootIndex'), + borsh.bool('proveByIndex'), + borsh.u8('merkleTreePubkeyIndex'), + borsh.u8('queuePubkeyIndex'), + borsh.u32('leafIndex'), + ]); + +/** + * Borsh schema for CompressedAccountMeta + */ +export const CompressedAccountMetaSchema: borsh.Layout = + borsh.struct([ + PackedStateTreeInfoSchema.replicate('treeInfo'), + borsh.option(borsh.array(borsh.u8(), 32), 'address'), + borsh.option(borsh.u64(), 'lamports'), + borsh.u8('outputStateTreeIndex'), + ]); + +/** + * Borsh schema for GenericCompressAccountInstruction + */ +export const GenericCompressAccountInstructionSchema: borsh.Layout = + borsh.struct([ + ValidityProofSchema.replicate('proof'), + CompressedAccountMetaSchema.replicate('compressedAccountMeta'), + ]); + +/** + * Helper function to create borsh schema for CompressedAccountData + * This is generic to work with any data type T + */ +export function createCompressedAccountDataSchema( + dataSchema: borsh.Layout, +): borsh.Layout> { + return borsh.struct([ + CompressedAccountMetaSchema.replicate('meta'), + dataSchema.replicate('data'), + borsh.vec(borsh.vec(borsh.u8()), 'seeds'), + ]); +} + +/** + * Helper function to create borsh schema for DecompressMultipleAccountsIdempotentData + * This is generic to work with any data type T + */ +export function createDecompressMultipleAccountsIdempotentDataSchema( + dataSchema: borsh.Layout, +): borsh.Layout> { + return borsh.struct([ + ValidityProofSchema.replicate('proof'), + borsh.vec( + createCompressedAccountDataSchema(dataSchema), + 'compressedAccounts', + ), + borsh.vec(borsh.u8(), 'bumps'), + borsh.u8('systemAccountsOffset'), + ]); +} + +/** + * Serialize instruction data with custom discriminator + */ +export function serializeInstructionData( + schema: borsh.Layout, + data: T, + discriminator: Uint8Array | number[], +): Buffer { + const buffer = Buffer.alloc(2000); + const len = schema.encode(data, buffer); + const serializedData = Buffer.from(new Uint8Array(buffer.slice(0, len))); + + return Buffer.concat([Buffer.from(discriminator), serializedData]); +} + +/** + * Serialize instruction data for initializeCompressionConfig using Borsh + */ +export function serializeInitializeCompressionConfigData( + compressionDelay: number, + rentRecipient: import('@solana/web3.js').PublicKey, + addressSpace: import('@solana/web3.js').PublicKey[], + configBump: number | null, +): Buffer { + const discriminator = Buffer.from([133, 228, 12, 169, 56, 76, 222, 61]); + + const instructionData: CompressionConfigIxData = { + compressionDelay, + rentRecipient, + addressSpace, + configBump, + }; + + const buffer = Buffer.alloc(1000); + const len = InitializeCompressionConfigSchema.encode( + instructionData, + buffer, + ); + const dataBuffer = Buffer.from(new Uint8Array(buffer.slice(0, len))); + + return Buffer.concat([ + new Uint8Array(discriminator), + new Uint8Array(dataBuffer), + ]); +} diff --git a/js/stateless.js/src/compressible/types.ts b/js/stateless.js/src/compressible/types.ts new file mode 100644 index 0000000000..3235fbc13b --- /dev/null +++ b/js/stateless.js/src/compressible/types.ts @@ -0,0 +1,125 @@ +import { PublicKey, AccountMeta } from '@solana/web3.js'; +import BN from 'bn.js'; +import { ValidityProof } from '../state/types'; +import { CompressedAccountMeta } from '../state/compressed-account'; + +/** + * Standard instruction discriminators for compressible instructions + * These match the Rust implementation discriminators + */ +export const COMPRESSIBLE_DISCRIMINATORS = { + INITIALIZE_COMPRESSION_CONFIG: [133, 228, 12, 169, 56, 76, 222, 61], + UPDATE_COMPRESSION_CONFIG: [135, 215, 243, 81, 163, 146, 33, 70], + DECOMPRESS_ACCOUNTS_IDEMPOTENT: [114, 67, 61, 123, 234, 31, 1, 112], +} as const; + +/** + * Generic compressed account data structure for decompress operations + * This is generic over the account variant type, allowing programs to use their specific enums + */ +export type CompressedAccountData = { + /** The compressed account metadata containing tree info, address, and output index */ + meta: CompressedAccountMeta; + /** Program-specific account variant enum */ + data: T; + /** PDA seeds (without bump) used to derive the PDA address */ + seeds: Uint8Array[]; +}; + +/** + * Instruction data structure for decompress_accounts_idempotent + * This matches the exact format expected by Anchor programs + */ +export type DecompressMultipleAccountsIdempotentData = { + proof: ValidityProof; + compressedAccounts: CompressedAccountData[]; + bumps: number[]; + systemAccountsOffset: number; +}; + +/** + * Instruction data for update compression config + */ +export type UpdateCompressionConfigData = { + newCompressionDelay: number | null; + newRentRecipient: PublicKey | null; + newAddressSpace: PublicKey[] | null; + newUpdateAuthority: PublicKey | null; +}; + +/** + * Generic instruction data for compress account + * This matches the expected format for compress account instructions + */ +export type GenericCompressAccountInstruction = { + proof: ValidityProof; + compressedAccountMeta: CompressedAccountMeta; +}; + +/** + * Existing CompressionConfigIxData type (re-exported for compatibility) + */ +export type CompressionConfigIxData = { + compressionDelay: number; + rentRecipient: PublicKey; + addressSpace: PublicKey[]; + configBump: number | null; +}; + +/** + * Common instruction builder parameters + */ +export type InstructionBuilderParams = { + programId: PublicKey; + discriminator: Uint8Array | number[]; +}; + +/** + * Initialize compression config instruction parameters + */ +export type InitializeCompressionConfigParams = InstructionBuilderParams & { + payer: PublicKey; + authority: PublicKey; + compressionDelay: number; + rentRecipient: PublicKey; + addressSpace: PublicKey[]; + configBump?: number | null; +}; + +/** + * Update compression config instruction parameters + */ +export type UpdateCompressionConfigParams = InstructionBuilderParams & { + authority: PublicKey; + newCompressionDelay?: number | null; + newRentRecipient?: PublicKey | null; + newAddressSpace?: PublicKey[] | null; + newUpdateAuthority?: PublicKey | null; +}; + +/** + * Compress account instruction parameters + */ +export type CompressAccountParams = InstructionBuilderParams & { + payer: PublicKey; + pdaToCompress: PublicKey; + rentRecipient: PublicKey; + compressedAccountMeta: CompressedAccountMeta; + validityProof: ValidityProof; + systemAccounts?: AccountMeta[]; +}; + +/** + * Decompress accounts idempotent instruction parameters + */ +export type DecompressAccountsIdempotentParams = + InstructionBuilderParams & { + feePayer: PublicKey; + rentPayer: PublicKey; + solanaAccounts: PublicKey[]; + compressedAccountsData: CompressedAccountData[]; + bumps: number[]; + validityProof: ValidityProof; + systemAccounts?: AccountMeta[]; + dataSchema?: any; // borsh.Layout - keeping it flexible + }; diff --git a/js/stateless.js/src/compressible/utils.ts b/js/stateless.js/src/compressible/utils.ts new file mode 100644 index 0000000000..3bb828b5fc --- /dev/null +++ b/js/stateless.js/src/compressible/utils.ts @@ -0,0 +1,65 @@ +import { Connection, PublicKey, AccountInfo } from '@solana/web3.js'; + +/** + * Derive the compression config PDA address + */ +export function deriveCompressionConfigAddress( + programId: PublicKey, + configIndex: number = 0, +): [PublicKey, number] { + const [configAddress, configBump] = PublicKey.findProgramAddressSync( + [Buffer.from('compressible_config'), Buffer.from([configIndex])], + programId, + ); + return [configAddress, configBump]; +} + +/** + * Get the program data account address and its raw data for a given program. + */ +export async function getProgramDataAccount( + programId: PublicKey, + connection: Connection, +): Promise<{ + programDataAddress: PublicKey; + programDataAccountInfo: AccountInfo; +}> { + const programAccount = await connection.getAccountInfo(programId); + if (!programAccount) { + throw new Error('Program account does not exist'); + } + const programDataAddress = new PublicKey(programAccount.data.slice(4, 36)); + const programDataAccountInfo = + await connection.getAccountInfo(programDataAddress); + if (!programDataAccountInfo) { + throw new Error('Program data account does not exist'); + } + return { programDataAddress, programDataAccountInfo }; +} + +/** + * Check that the provided authority matches the program's upgrade authority. + */ +export function checkProgramUpdateAuthority( + programDataAccountInfo: AccountInfo, + providedAuthority: PublicKey, +): void { + // Check discriminator (should be 3 for ProgramData) + const discriminator = programDataAccountInfo.data.readUInt32LE(0); + if (discriminator !== 3) { + throw new Error('Invalid program data discriminator'); + } + // Check if authority exists + const hasAuthority = programDataAccountInfo.data[12] === 1; + if (!hasAuthority) { + throw new Error('Program has no upgrade authority'); + } + // Extract upgrade authority (bytes 13-44) + const authorityBytes = programDataAccountInfo.data.slice(13, 45); + const upgradeAuthority = new PublicKey(authorityBytes); + if (!upgradeAuthority.equals(providedAuthority)) { + throw new Error( + `Provided authority ${providedAuthority.toBase58()} does not match program's upgrade authority ${upgradeAuthority.toBase58()}`, + ); + } +} diff --git a/js/stateless.js/src/constants.ts b/js/stateless.js/src/constants.ts index b34cfaaff5..af603f3c7d 100644 --- a/js/stateless.js/src/constants.ts +++ b/js/stateless.js/src/constants.ts @@ -167,24 +167,37 @@ export const localTestActiveStateTreeInfos = (): TreeInfo[] => { { tree: new PublicKey(batchMerkleTree), queue: new PublicKey(batchQueue), - cpiContext: PublicKey.default, + cpiContext: new PublicKey(batchCpiContext), treeType: TreeType.StateV2, nextTreeInfo: null, }, ].filter(info => - featureFlags.isV2() ? true : info.treeType === TreeType.StateV1, + featureFlags.isV2() + ? info.treeType === TreeType.StateV2 + : info.treeType === TreeType.StateV1, ); }; export const getDefaultAddressTreeInfo = () => { - return { - tree: new PublicKey(addressTree), - queue: new PublicKey(addressQueue), - cpiContext: null, - treeType: TreeType.AddressV1, - nextTreeInfo: null, - }; + if (featureFlags.isV2()) { + return { + tree: addressTreeV2, + queue: addressTreeV2, // v2 has queue in same account as tree. + cpiContext: null, + treeType: TreeType.AddressV2, + nextTreeInfo: null, + }; + } else { + return { + tree: new PublicKey(addressTree), + queue: new PublicKey(addressQueue), + cpiContext: null, + treeType: TreeType.AddressV1, + nextTreeInfo: null, + }; + } }; + /** * @deprecated use {@link rpc.getStateTreeInfos} and {@link selectStateTreeInfo} instead. * for address trees, use {@link getDefaultAddressTreeInfo} instead. @@ -232,6 +245,11 @@ export const merkletreePubkey = 'smt1NamzXdq4AMqS2fS2F1i5KTYPZRhoHgWx38d8WsT'; export const addressTree = 'amt1Ayt45jfbdw5YSo7iz6WZxUmnZsQTYXy82hVwyC2'; export const addressQueue = 'aq1S9z4reTSQAdgWHGD2zDaS39sjGrAxbR31vxJ2F4F'; +// V2 tree is in same account as queue. +export const addressTreeV2 = new PublicKey( + 'EzKE84aVTkCUhDHLELqyJaq1Y7UVVmqxXqZjVHwHY3rK', +); + export const merkleTree2Pubkey = 'smt2rJAFdyJJupwMKAqTNAJwvjhmiZ4JYGZmbVRw1Ho'; export const nullifierQueue2Pubkey = 'nfq2hgS7NYemXsFaFUCe3EMXSDSfnZnAe27jC6aPP1X'; @@ -240,7 +258,8 @@ export const cpiContext2Pubkey = 'cpi2cdhkH5roePvcudTgUL8ppEBfTay1desGh8G8QxK'; // V2 testing. export const batchMerkleTree = 'HLKs5NJ8FXkJg8BrzJt56adFYYuwg5etzDtBbQYTsixu'; // v2 merkle tree (includes nullifier queue) export const batchQueue = '6L7SzhYB3anwEQ9cphpJ1U7Scwj57bx2xueReg7R9cKU'; // v2 output queue - +export const batchCpiContext = '7Hp52chxaew8bW1ApR4fck2bh6Y8qA1pu3qwH6N9zaLj'; +// export const batchCpiContext = 'HwtjxDvFEXiWnzeMeWkMBzpQN45A95rTJNZmz1Z3pe8R'; export const confirmConfig: ConfirmOptions = { commitment: 'confirmed', preflightCommitment: 'confirmed', diff --git a/js/stateless.js/src/index.ts b/js/stateless.js/src/index.ts index 847d0127f9..dfcf04a98d 100644 --- a/js/stateless.js/src/index.ts +++ b/js/stateless.js/src/index.ts @@ -7,3 +7,4 @@ export * from './constants'; export * from './errors'; export * from './rpc-interface'; export * from './rpc'; +export * from './compressible'; diff --git a/js/stateless.js/src/programs/system/pack.ts b/js/stateless.js/src/programs/system/pack.ts index de88c30e33..c9bdb1aaf8 100644 --- a/js/stateless.js/src/programs/system/pack.ts +++ b/js/stateless.js/src/programs/system/pack.ts @@ -1,4 +1,5 @@ import { AccountMeta, PublicKey } from '@solana/web3.js'; +import BN from 'bn.js'; import { AccountProofInput, CompressedAccountLegacy, @@ -7,13 +8,16 @@ import { PackedCompressedAccountWithMerkleContext, TreeInfo, TreeType, + ValidityProof, } from '../../state'; +import { ValidityProofWithContext } from '../../rpc-interface'; import { CompressedAccountWithMerkleContextLegacy, PackedAddressTreeInfo, PackedStateTreeInfo, } from '../../state/compressed-account'; import { featureFlags } from '../../constants'; +import { PackedAccounts, PackedAccountsSmall } from '../../utils'; /** * @internal Finds the index of a PublicKey in an array, or adds it if not @@ -72,18 +76,10 @@ export function toAccountMetas(remainingAccounts: PublicKey[]): AccountMeta[] { ); } -export interface PackedStateTreeInfos { - packedTreeInfos: PackedStateTreeInfo[]; - outputTreeIndex: number; -} - -export interface PackedTreeInfos { - stateTrees?: PackedStateTreeInfos; - addressTrees: PackedAddressTreeInfo[]; -} - const INVALID_TREE_INDEX = -1; + /** + * @deprecated Use {@link packTreeInfos} instead. * Packs TreeInfos. Replaces PublicKey with index pointer to remaining accounts. * * Only use for MUT, CLOSE, NEW_ADDRESSES. For INIT, pass @@ -99,7 +95,7 @@ const INVALID_TREE_INDEX = -1; * @returns Remaining accounts, packed state and address tree infos, state tree * output index and address tree infos. */ -export function packTreeInfos( +export function packTreeInfosWithPubkeys( remainingAccounts: PublicKey[], accountProofInputs: AccountProofInput[], newAddressProofInputs: NewAddressProofInput[], @@ -113,7 +109,7 @@ export function packTreeInfos( // Early exit. if (accountProofInputs.length === 0 && newAddressProofInputs.length === 0) { return { - stateTrees: undefined, + stateTrees: null, addressTrees: addressTreeInfos, }; } @@ -181,7 +177,7 @@ export function packTreeInfos( packedTreeInfos: stateTreeInfos, outputTreeIndex, } - : undefined, + : null, addressTrees: addressTreeInfos, }; } @@ -307,3 +303,259 @@ export function packCompressedAccounts( remainingAccounts: _remainingAccounts, }; } + +/** + * Root index for state tree proofs. + */ +export type RootIndex = { + proofByIndex: boolean; + rootIndex: number; +}; + +/** + * Creates a RootIndex for proving by merkle proof. + */ +export function createRootIndex(rootIndex: number): RootIndex { + return { + proofByIndex: false, + rootIndex, + }; +} + +/** + * Creates a RootIndex for proving by leaf index. + */ +export function createRootIndexByIndex(): RootIndex { + return { + proofByIndex: true, + rootIndex: 0, + }; +} + +/** + * Account proof inputs for state tree accounts. + */ +export type AccountProofInputs = { + hash: Uint8Array; + root: Uint8Array; + rootIndex: RootIndex; + leafIndex: number; + treeInfo: TreeInfo; +}; + +/** + * Address proof inputs for address tree accounts. + */ +export type AddressProofInputs = { + address: Uint8Array; + root: Uint8Array; + rootIndex: number; + treeInfo: TreeInfo; +}; + +/** + * Validity proof with context structure that matches Rust implementation. + */ +export type ValidityProofWithContextV2 = { + proof: ValidityProof | null; + accounts: AccountProofInputs[]; + addresses: AddressProofInputs[]; +}; + +/** + * Packed state tree infos. + */ +export type PackedStateTreeInfos = { + packedTreeInfos: PackedStateTreeInfo[]; + outputTreeIndex: number; +}; + +/** + * Packed tree infos containing both state and address trees. + */ +export type PackedTreeInfos = { + stateTrees: PackedStateTreeInfos | null; + addressTrees: PackedAddressTreeInfo[]; +}; + +/** + * Packs the output tree index based on tree type. + * For StateV1, returns the index of the tree account. + * For StateV2, returns the index of the queue account. + */ +function packOutputTreeIndex( + treeInfo: TreeInfo, + packedAccounts: PackedAccounts | PackedAccountsSmall, +): number { + switch (treeInfo.treeType) { + case TreeType.StateV1: + return packedAccounts.insertOrGet(treeInfo.tree); + case TreeType.StateV2: + return packedAccounts.insertOrGet(treeInfo.queue); + default: + throw new Error('Invalid tree type for packing output tree index'); + } +} + +/** + * Converts ValidityProofWithContext to ValidityProofWithContextV2 format. + * Infers the split between state and address accounts based on tree types. + */ +function convertValidityProofToV2( + validityProof: ValidityProofWithContext, +): ValidityProofWithContextV2 { + const accounts: AccountProofInputs[] = []; + const addresses: AddressProofInputs[] = []; + + for (let i = 0; i < validityProof.treeInfos.length; i++) { + const treeInfo = validityProof.treeInfos[i]; + + if ( + treeInfo.treeType === TreeType.StateV1 || + treeInfo.treeType === TreeType.StateV2 + ) { + // State tree account + accounts.push({ + hash: new Uint8Array(validityProof.leaves[i].toArray('le', 32)), + root: new Uint8Array(validityProof.roots[i].toArray('le', 32)), + rootIndex: { + proofByIndex: validityProof.proveByIndices[i], + rootIndex: validityProof.rootIndices[i], + }, + leafIndex: validityProof.leafIndices[i], + treeInfo, + }); + } else { + // Address tree account + addresses.push({ + address: new Uint8Array( + validityProof.leaves[i].toArray('le', 32), + ), + root: new Uint8Array(validityProof.roots[i].toArray('le', 32)), + rootIndex: validityProof.rootIndices[i], + treeInfo, + }); + } + } + + return { + proof: validityProof.compressedProof, + accounts, + addresses, + }; +} + +/** + * Packs tree infos from ValidityProofWithContext into packed format. This is a + * TypeScript equivalent of the Rust pack_tree_infos method. + * + * @param validityProof - The validity proof with context (flat format) + * @param packedAccounts - The packed accounts manager (supports both PackedAccounts and PackedAccountsSmall) + * @returns Packed tree infos + */ +export function packTreeInfos( + validityProof: ValidityProofWithContext, + packedAccounts: PackedAccounts | PackedAccountsSmall, +): PackedTreeInfos; + +/** + * Packs tree infos from ValidityProofWithContextV2 into packed format. This is + * a TypeScript equivalent of the Rust pack_tree_infos method. + * + * @param validityProof - The validity proof with context (structured format) + * @param packedAccounts - The packed accounts manager (supports both PackedAccounts and PackedAccountsSmall) + * @returns Packed tree infos + */ +export function packTreeInfos( + validityProof: ValidityProofWithContextV2, + packedAccounts: PackedAccounts | PackedAccountsSmall, +): PackedTreeInfos; + +export function packTreeInfos( + validityProof: ValidityProofWithContext | ValidityProofWithContextV2, + packedAccounts: PackedAccounts | PackedAccountsSmall, +): PackedTreeInfos { + // Convert flat format to structured format if needed + const structuredProof = + 'accounts' in validityProof + ? (validityProof as ValidityProofWithContextV2) + : convertValidityProofToV2( + validityProof as ValidityProofWithContext, + ); + const packedTreeInfos: PackedStateTreeInfo[] = []; + const addressTrees: PackedAddressTreeInfo[] = []; + let outputTreeIndex: number | null = null; + + // Process state tree accounts + for (const account of structuredProof.accounts) { + // Pack TreeInfo + const merkleTreePubkeyIndex = packedAccounts.insertOrGet( + account.treeInfo.tree, + ); + const queuePubkeyIndex = packedAccounts.insertOrGet( + account.treeInfo.queue, + ); + + const treeInfoPacked: PackedStateTreeInfo = { + rootIndex: account.rootIndex.rootIndex, + merkleTreePubkeyIndex, + queuePubkeyIndex, + leafIndex: account.leafIndex, + proveByIndex: account.rootIndex.proofByIndex, + }; + packedTreeInfos.push(treeInfoPacked); + + // Determine output tree index + // If a next Merkle tree exists, the Merkle tree is full -> use the next Merkle tree for new state. + // Else use the current Merkle tree for new state. + if (account.treeInfo.nextTreeInfo) { + // SAFETY: account will always have a state Merkle tree context. + // packOutputTreeIndex only throws on an invalid address Merkle tree context. + const index = packOutputTreeIndex( + account.treeInfo.nextTreeInfo, + packedAccounts, + ); + if (outputTreeIndex === null) { + outputTreeIndex = index; + } + } else { + // SAFETY: account will always have a state Merkle tree context. + // packOutputTreeIndex only throws on an invalid address Merkle tree context. + const index = packOutputTreeIndex(account.treeInfo, packedAccounts); + if (outputTreeIndex === null) { + outputTreeIndex = index; + } + } + } + + // Process address tree accounts + for (const address of structuredProof.addresses) { + // Pack AddressTreeInfo + const addressMerkleTreePubkeyIndex = packedAccounts.insertOrGet( + address.treeInfo.tree, + ); + const addressQueuePubkeyIndex = packedAccounts.insertOrGet( + address.treeInfo.queue, + ); + + addressTrees.push({ + addressMerkleTreePubkeyIndex, + addressQueuePubkeyIndex, + rootIndex: address.rootIndex, + }); + } + + // Create final packed tree infos + const stateTrees = + packedTreeInfos.length === 0 + ? null + : { + packedTreeInfos, + outputTreeIndex: outputTreeIndex!, + }; + + return { + stateTrees, + addressTrees, + }; +} diff --git a/js/stateless.js/src/rpc-interface.ts b/js/stateless.js/src/rpc-interface.ts index 89376fa00f..aac2259df9 100644 --- a/js/stateless.js/src/rpc-interface.ts +++ b/js/stateless.js/src/rpc-interface.ts @@ -305,6 +305,15 @@ const Base64EncodedCompressedAccountDataResult = coerce( string(), value => (value === '' ? null : value), ); + +/** + * + * @internal + * Discriminator as base64 encoded string (8 bytes) + */ +const Base64EncodedDiscriminatorResult = coerce(string(), string(), value => + value === '' ? null : value, +); /** * @internal */ diff --git a/js/stateless.js/src/rpc.ts b/js/stateless.js/src/rpc.ts index d0b08b26c6..a282419364 100644 --- a/js/stateless.js/src/rpc.ts +++ b/js/stateless.js/src/rpc.ts @@ -1,4 +1,5 @@ import { + AccountInfo, Connection, ConnectionConfig, PublicKey, @@ -65,6 +66,7 @@ import { TreeType, AddressTreeInfo, CompressedAccount, + MerkleContext, } from './state'; import { array, create, nullable } from 'superstruct'; import { @@ -89,7 +91,7 @@ import { getTreeInfoByPubkey, } from './utils/get-state-tree-infos'; import { TreeInfo } from './state/types'; -import { validateNumbersForProof } from './utils'; +import { deriveAddressV2, validateNumbersForProof } from './utils'; /** @internal */ export function parseAccountData({ @@ -101,10 +103,12 @@ export function parseAccountData({ data: string; dataHash: BN; }) { + const discriminatorBytes = Buffer.from(discriminator.toArray('le', 8)); + return { - discriminator: discriminator.toArray('le', 8), + discriminator: Array.from(discriminatorBytes), data: Buffer.from(data, 'base64'), - dataHash: dataHash.toArray('le', 32), + dataHash: dataHash.toArray('be', 32), }; } @@ -1938,4 +1942,76 @@ export class Rpc extends Connection implements CompressionApiInterface { }; } } + + /** + * Get account info from either compressed or onchain storage. + * @param address The account address to fetch. + * @param programId The owner program ID. + * @param addressTreeInfo The address tree info used to store the account. + * @param rpc The RPC client to use. + * + * @returns Account info with compression info, or null if account + * doesn't exist. + */ + async getCompressibleAccountInfo( + address: PublicKey, + programId: PublicKey, + addressTreeInfo: TreeInfo, + rpc: Rpc, + ): Promise<{ + accountInfo: AccountInfo; + merkleContext?: MerkleContext; + } | null> { + const cAddress = deriveAddressV2( + address.toBytes(), + addressTreeInfo.tree.toBytes(), + programId.toBytes(), + ); + + // Execute both calls in parallel + const [onchainResult, compressedResult] = await Promise.allSettled([ + rpc.getAccountInfo(address), + rpc.getCompressedAccount(bn(Array.from(cAddress))), + ]); + + const onchainAccount = + onchainResult.status === 'fulfilled' ? onchainResult.value : null; + const compressedAccount = + compressedResult.status === 'fulfilled' + ? compressedResult.value + : null; + + if (onchainAccount) { + return { accountInfo: onchainAccount, merkleContext: undefined }; + } + + // is compressed. + if ( + compressedAccount && + compressedAccount.data && + compressedAccount.data.data.length > 0 + ) { + const accountInfo: AccountInfo = { + executable: false, + owner: compressedAccount.owner, + lamports: compressedAccount.lamports.toNumber(), + data: Buffer.concat([ + Buffer.from(compressedAccount.data!.discriminator), + compressedAccount.data!.data, + ]), + }; + return { + accountInfo, + merkleContext: { + treeInfo: addressTreeInfo, + hash: compressedAccount.hash, + leafIndex: compressedAccount.leafIndex, + proveByIndex: compressedAccount.proveByIndex, + }, + }; + } + + // account does not exist. + return null; + } } diff --git a/js/stateless.js/src/test-helpers/test-rpc/test-rpc.ts b/js/stateless.js/src/test-helpers/test-rpc/test-rpc.ts index a523002893..825df193a2 100644 --- a/js/stateless.js/src/test-helpers/test-rpc/test-rpc.ts +++ b/js/stateless.js/src/test-helpers/test-rpc/test-rpc.ts @@ -1,4 +1,9 @@ -import { Connection, ConnectionConfig, PublicKey } from '@solana/web3.js'; +import { + AccountInfo, + Connection, + ConnectionConfig, + PublicKey, +} from '@solana/web3.js'; import BN from 'bn.js'; import { getCompressedAccountByHashTest, @@ -39,6 +44,7 @@ import { import { BN254, CompressedAccountWithMerkleContext, + MerkleContext, MerkleContextWithMerkleProof, PublicTransactionEvent, TreeType, @@ -951,4 +957,18 @@ export class TestRpc extends Connection implements CompressionApiInterface { newAddresses.map(address => address.address), ); } + + async getCompressibleAccountInfo( + address: PublicKey, + programId: PublicKey, + addressTreeInfo: TreeInfo, + rpc: TestRpc, + ): Promise<{ + accountInfo: AccountInfo; + merkleContext?: MerkleContext; + } | null> { + throw new Error( + 'getCompressibleAccountInfo not implemented in test-rpc', + ); + } } diff --git a/js/stateless.js/src/utils/address.ts b/js/stateless.js/src/utils/address.ts index 7d4a6ce074..2432cad789 100644 --- a/js/stateless.js/src/utils/address.ts +++ b/js/stateless.js/src/utils/address.ts @@ -2,6 +2,47 @@ import { PublicKey } from '@solana/web3.js'; import { hashToBn254FieldSizeBe, hashvToBn254FieldSizeBe } from './conversion'; import { defaultTestStateTreeAccounts } from '../constants'; import { getIndexOrAdd } from '../programs/system/pack'; +import { keccak_256 } from '@noble/hashes/sha3'; + +/** + * Derive an address for a compressed account from a seed and an address Merkle + * tree public key. + * + * @param seed 32 bytes seed to derive the address from + * @param addressMerkleTreePubkey Address Merkle tree public key as bytes. + * @param programIdBytes Program ID bytes. + * @returns Derived address as bytes + */ +export function deriveAddressV2( + seed: Uint8Array, + addressMerkleTreePubkey: Uint8Array, + programIdBytes: Uint8Array, +): Uint8Array { + const slices = [seed, addressMerkleTreePubkey, programIdBytes]; + + return hashVWithBumpSeed(slices); +} + +export function hashVWithBumpSeed(bytes: Uint8Array[]): Uint8Array { + const HASH_TO_FIELD_SIZE_SEED = 255; // u8::MAX + + const hasher = keccak_256.create(); + + // Hash all input bytes + for (const input of bytes) { + hasher.update(input); + } + + // Add the bump seed (just like Rust version) + hasher.update(new Uint8Array([HASH_TO_FIELD_SIZE_SEED])); + + const hash = hasher.digest(); + + // Truncate to BN254 field size (just like Rust version) + hash[0] = 0; + + return hash; +} export function deriveAddressSeed( seeds: Uint8Array[], @@ -13,6 +54,8 @@ export function deriveAddressSeed( } /** + * @deprecated Use {@link deriveAddressV2} instead, unless you're using v1. + * * Derive an address for a compressed account from a seed and an address Merkle * tree public key. * diff --git a/js/stateless.js/src/utils/conversion.ts b/js/stateless.js/src/utils/conversion.ts index 718ed43a61..86ebf6b880 100644 --- a/js/stateless.js/src/utils/conversion.ts +++ b/js/stateless.js/src/utils/conversion.ts @@ -79,6 +79,7 @@ export function hashToBn254FieldSizeBe(bytes: Buffer): [Buffer, number] | null { } /** + * TODO: make consistent with latest rust. (use u8::max bumpseed) * Hash the provided `bytes` with Keccak256 and ensure that the result fits in * the BN254 prime field by truncating the resulting hash to 31 bytes. * diff --git a/js/stateless.js/src/utils/index.ts b/js/stateless.js/src/utils/index.ts index 1135d41f81..d079b7e786 100644 --- a/js/stateless.js/src/utils/index.ts +++ b/js/stateless.js/src/utils/index.ts @@ -10,3 +10,4 @@ export * from './sleep'; export * from './validation'; export * from './state-tree-lookup-table'; export * from './get-state-tree-infos'; +export * from './packed-accounts'; diff --git a/js/stateless.js/src/utils/packed-accounts.ts b/js/stateless.js/src/utils/packed-accounts.ts new file mode 100644 index 0000000000..13aee187cc --- /dev/null +++ b/js/stateless.js/src/utils/packed-accounts.ts @@ -0,0 +1,503 @@ +import { defaultStaticAccountsStruct } from '../constants'; +import { LightSystemProgram } from '../programs/system'; +import { AccountMeta, PublicKey, SystemProgram } from '@solana/web3.js'; + +/** + * This file provides two variants of packed accounts for Light Protocol: + * + * 1. PackedAccounts - Matches CpiAccounts (11 system accounts) + * - Includes: LightSystemProgram, Authority, RegisteredProgramPda, NoopProgram, + * AccountCompressionAuthority, AccountCompressionProgram, InvokingProgram, + * [Optional: SolPoolPda, DecompressionRecipient], SystemProgram, [Optional: CpiContext] + * + * 2. PackedAccountsSmall - Matches CpiAccountsSmall (9 system accounts max) + * - Includes: LightSystemProgram, Authority, RegisteredProgramPda, + * AccountCompressionAuthority, AccountCompressionProgram, SystemProgram, + * [Optional: SolPoolPda, DecompressionRecipient, CpiContext] + * - Excludes: NoopProgram and InvokingProgram for a more compact structure + */ + +/** + * Create a PackedAccounts instance to pack the light protocol system accounts + * for your custom program instruction. Typically, you will append them to the + * end of your instruction's accounts / remainingAccounts. + * + * This matches the full CpiAccounts structure with 11 system accounts including + * NoopProgram and InvokingProgram. For a more compact version, use PackedAccountsSmall. + * + * @example + * ```ts + * const packedAccounts = PackedAccounts.newWithSystemAccounts(config); + * + * const instruction = new TransactionInstruction({ + * keys: [...yourInstructionAccounts, ...packedAccounts.toAccountMetas()], + * programId: selfProgram, + * data: data, + * }); + * ``` + */ +export class PackedAccounts { + private preAccounts: AccountMeta[] = []; + private systemAccounts: AccountMeta[] = []; + private nextIndex: number = 0; + private map: Map = new Map(); + + static newWithSystemAccounts( + config: SystemAccountMetaConfig, + ): PackedAccounts { + const instance = new PackedAccounts(); + instance.addSystemAccounts(config); + return instance; + } + + addPreAccountsSigner(pubkey: PublicKey): void { + this.preAccounts.push({ pubkey, isSigner: true, isWritable: false }); + } + + addPreAccountsSignerMut(pubkey: PublicKey): void { + this.preAccounts.push({ pubkey, isSigner: true, isWritable: true }); + } + + addPreAccountsMeta(accountMeta: AccountMeta): void { + this.preAccounts.push(accountMeta); + } + + addSystemAccounts(config: SystemAccountMetaConfig): void { + this.systemAccounts.push(...getLightSystemAccountMetas(config)); + } + + insertOrGet(pubkey: PublicKey): number { + return this.insertOrGetConfig(pubkey, false, true); + } + + insertOrGetReadOnly(pubkey: PublicKey): number { + return this.insertOrGetConfig(pubkey, false, false); + } + + insertOrGetConfig( + pubkey: PublicKey, + isSigner: boolean, + isWritable: boolean, + ): number { + const key = pubkey.toString(); + const entry = this.map.get(key); + if (entry) { + return entry[0]; + } + const index = this.nextIndex++; + const meta: AccountMeta = { pubkey, isSigner, isWritable }; + this.map.set(key, [index, meta]); + return index; + } + + private hashSetAccountsToMetas(): AccountMeta[] { + const entries = Array.from(this.map.entries()); + entries.sort((a, b) => a[1][0] - b[1][0]); + return entries.map(([, [, meta]]) => meta); + } + + private getOffsets(): [number, number] { + const systemStart = this.preAccounts.length; + const packedStart = systemStart + this.systemAccounts.length; + return [systemStart, packedStart]; + } + + toAccountMetas(): { + remainingAccounts: AccountMeta[]; + systemStart: number; + packedStart: number; + } { + const packed = this.hashSetAccountsToMetas(); + const [systemStart, packedStart] = this.getOffsets(); + return { + remainingAccounts: [ + ...this.preAccounts, + ...this.systemAccounts, + ...packed, + ], + systemStart, + packedStart, + }; + } +} + +/** + * Creates a PackedAccounts instance with system accounts for the specified + * program. This is a convenience wrapper around SystemAccountMetaConfig.new() + * and PackedAccounts.newWithSystemAccounts(). + * + * @param programId - The program ID that will be using these system accounts + * @returns A new PackedAccounts instance with system accounts configured + * + * @example + * ```ts + * const packedAccounts = createPackedAccounts(myProgram.programId); + * + * const instruction = new TransactionInstruction({ + * keys: [...yourInstructionAccounts, ...packedAccounts.toAccountMetas().remainingAccounts], + * programId: myProgram.programId, + * data: instructionData, + * }); + * ``` + */ +export function createPackedAccounts(programId: PublicKey): PackedAccounts { + const systemAccountConfig = SystemAccountMetaConfig.new(programId); + return PackedAccounts.newWithSystemAccounts(systemAccountConfig); +} + +/** + * Creates a PackedAccounts instance with system accounts and CPI context for the specified program. + * This is a convenience wrapper that includes CPI context configuration. + * + * @param programId - The program ID that will be using these system accounts + * @param cpiContext - The CPI context account public key + * @returns A new PackedAccounts instance with system accounts and CPI context configured + * + * @example + * ```ts + * const packedAccounts = createPackedAccountsWithCpiContext( + * myProgram.programId, + * cpiContextAccount + * ); + * ``` + */ +export function createPackedAccountsWithCpiContext( + programId: PublicKey, + cpiContext: PublicKey, +): PackedAccounts { + const systemAccountConfig = SystemAccountMetaConfig.newWithCpiContext( + programId, + cpiContext, + ); + return PackedAccounts.newWithSystemAccounts(systemAccountConfig); +} + +export class SystemAccountMetaConfig { + selfProgram: PublicKey; + cpiContext?: PublicKey; + solCompressionRecipient?: PublicKey; + solPoolPda?: PublicKey; + + private constructor( + selfProgram: PublicKey, + cpiContext?: PublicKey, + solCompressionRecipient?: PublicKey, + solPoolPda?: PublicKey, + ) { + this.selfProgram = selfProgram; + this.cpiContext = cpiContext; + this.solCompressionRecipient = solCompressionRecipient; + this.solPoolPda = solPoolPda; + } + + static new(selfProgram: PublicKey): SystemAccountMetaConfig { + return new SystemAccountMetaConfig(selfProgram); + } + + static newWithCpiContext( + selfProgram: PublicKey, + cpiContext: PublicKey, + ): SystemAccountMetaConfig { + return new SystemAccountMetaConfig(selfProgram, cpiContext); + } +} + +/** + * Get the light protocol system accounts for your custom program instruction. + * Use via `link PackedAccounts.addSystemAccounts(config)`. + */ +export function getLightSystemAccountMetas( + config: SystemAccountMetaConfig, +): AccountMeta[] { + const signerSeed = new TextEncoder().encode('cpi_authority'); + const cpiSigner = PublicKey.findProgramAddressSync( + [signerSeed], + config.selfProgram, + )[0]; + const defaults = SystemAccountPubkeys.default(); + const metas: AccountMeta[] = [ + { + pubkey: defaults.lightSystemProgram, + isSigner: false, + isWritable: false, + }, + { pubkey: cpiSigner, isSigner: false, isWritable: false }, + { + pubkey: defaults.registeredProgramPda, + isSigner: false, + isWritable: false, + }, + { pubkey: defaults.noopProgram, isSigner: false, isWritable: false }, + { + pubkey: defaults.accountCompressionAuthority, + isSigner: false, + isWritable: false, + }, + { + pubkey: defaults.accountCompressionProgram, + isSigner: false, + isWritable: false, + }, + { pubkey: config.selfProgram, isSigner: false, isWritable: false }, + ]; + if (config.solPoolPda) { + metas.push({ + pubkey: config.solPoolPda, + isSigner: false, + isWritable: true, + }); + } + if (config.solCompressionRecipient) { + metas.push({ + pubkey: config.solCompressionRecipient, + isSigner: false, + isWritable: true, + }); + } + metas.push({ + pubkey: defaults.systemProgram, + isSigner: false, + isWritable: false, + }); + if (config.cpiContext) { + metas.push({ + pubkey: config.cpiContext, + isSigner: false, + isWritable: true, + }); + } + return metas; +} + +/** + * PackedAccountsSmall matches the CpiAccountsSmall structure with simplified account ordering. + * This is a more compact version that excludes NoopProgram and InvokingProgram. + */ +export class PackedAccountsSmall { + private preAccounts: AccountMeta[] = []; + private systemAccounts: AccountMeta[] = []; + private nextIndex: number = 0; + private map: Map = new Map(); + + static newWithSystemAccounts( + config: SystemAccountMetaConfig, + ): PackedAccountsSmall { + const instance = new PackedAccountsSmall(); + instance.addSystemAccounts(config); + return instance; + } + + /** + * Returns the internal map of pubkey to [index, AccountMeta]. + * For debugging purposes only. + */ + getNamedMetas(): Map { + return this.map; + } + + addPreAccountsSigner(pubkey: PublicKey): void { + this.preAccounts.push({ pubkey, isSigner: true, isWritable: false }); + } + + addPreAccountsSignerMut(pubkey: PublicKey): void { + this.preAccounts.push({ pubkey, isSigner: true, isWritable: true }); + } + + addPreAccountsMeta(accountMeta: AccountMeta): void { + this.preAccounts.push(accountMeta); + } + + addSystemAccounts(config: SystemAccountMetaConfig): void { + this.systemAccounts.push(...getLightSystemAccountMetasSmall(config)); + } + + insertOrGet(pubkey: PublicKey): number { + return this.insertOrGetConfig(pubkey, false, true); + } + + insertOrGetReadOnly(pubkey: PublicKey): number { + return this.insertOrGetConfig(pubkey, false, false); + } + + insertOrGetConfig( + pubkey: PublicKey, + isSigner: boolean, + isWritable: boolean, + ): number { + const key = pubkey.toString(); + const entry = this.map.get(key); + if (entry) { + return entry[0]; + } + const index = this.nextIndex++; + const meta: AccountMeta = { pubkey, isSigner, isWritable }; + this.map.set(key, [index, meta]); + return index; + } + + private hashSetAccountsToMetas(): AccountMeta[] { + const entries = Array.from(this.map.entries()); + entries.sort((a, b) => a[1][0] - b[1][0]); + return entries.map(([, [, meta]]) => meta); + } + + private getOffsets(): [number, number] { + const systemStart = this.preAccounts.length; + const packedStart = systemStart + this.systemAccounts.length; + return [systemStart, packedStart]; + } + + toAccountMetas(): { + remainingAccounts: AccountMeta[]; + systemStart: number; + packedStart: number; + } { + const packed = this.hashSetAccountsToMetas(); + const [systemStart, packedStart] = this.getOffsets(); + return { + remainingAccounts: [ + ...this.preAccounts, + ...this.systemAccounts, + ...packed, + ], + systemStart, + packedStart, + }; + } +} + +/** + * Get the light protocol system accounts for the small variant. + * This matches CpiAccountsSmall ordering: removes NoopProgram and InvokingProgram. + */ +export function getLightSystemAccountMetasSmall( + config: SystemAccountMetaConfig, +): AccountMeta[] { + const signerSeed = new TextEncoder().encode('cpi_authority'); + const cpiSigner = PublicKey.findProgramAddressSync( + [signerSeed], + config.selfProgram, + )[0]; + const defaults = SystemAccountPubkeys.default(); + + // Small variant ordering: LightSystemProgram, Authority, RegisteredProgramPda, + // AccountCompressionAuthority, AccountCompressionProgram, SystemProgram, + // [Optional: SolPoolPda, DecompressionRecipient, CpiContext] + const metas: AccountMeta[] = [ + { + pubkey: defaults.lightSystemProgram, + isSigner: false, + isWritable: false, + }, + { pubkey: cpiSigner, isSigner: false, isWritable: false }, + { + pubkey: defaults.registeredProgramPda, + isSigner: false, + isWritable: false, + }, + { + pubkey: defaults.accountCompressionAuthority, + isSigner: false, + isWritable: false, + }, + { + pubkey: defaults.accountCompressionProgram, + isSigner: false, + isWritable: false, + }, + { + pubkey: defaults.systemProgram, + isSigner: false, + isWritable: false, + }, + ]; + + // Optional accounts in order + if (config.solPoolPda) { + metas.push({ + pubkey: config.solPoolPda, + isSigner: false, + isWritable: true, + }); + } + if (config.solCompressionRecipient) { + metas.push({ + pubkey: config.solCompressionRecipient, + isSigner: false, + isWritable: true, + }); + } + if (config.cpiContext) { + metas.push({ + pubkey: config.cpiContext, + isSigner: false, + isWritable: true, + }); + } + return metas; +} + +/** + * Creates a PackedAccountsSmall instance with system accounts for the specified program. + * This uses the simplified account ordering that matches CpiAccountsSmall. + */ +export function createPackedAccountsSmall( + programId: PublicKey, +): PackedAccountsSmall { + const systemAccountConfig = SystemAccountMetaConfig.new(programId); + return PackedAccountsSmall.newWithSystemAccounts(systemAccountConfig); +} + +/** + * Creates a PackedAccountsSmall instance with system accounts and CPI context. + */ +export function createPackedAccountsSmallWithCpiContext( + programId: PublicKey, + cpiContext: PublicKey, +): PackedAccountsSmall { + const systemAccountConfig = SystemAccountMetaConfig.newWithCpiContext( + programId, + cpiContext, + ); + return PackedAccountsSmall.newWithSystemAccounts(systemAccountConfig); +} + +export class SystemAccountPubkeys { + lightSystemProgram: PublicKey; + systemProgram: PublicKey; + accountCompressionProgram: PublicKey; + accountCompressionAuthority: PublicKey; + registeredProgramPda: PublicKey; + noopProgram: PublicKey; + solPoolPda: PublicKey; + + private constructor( + lightSystemProgram: PublicKey, + systemProgram: PublicKey, + accountCompressionProgram: PublicKey, + accountCompressionAuthority: PublicKey, + registeredProgramPda: PublicKey, + noopProgram: PublicKey, + solPoolPda: PublicKey, + ) { + this.lightSystemProgram = lightSystemProgram; + this.systemProgram = systemProgram; + this.accountCompressionProgram = accountCompressionProgram; + this.accountCompressionAuthority = accountCompressionAuthority; + this.registeredProgramPda = registeredProgramPda; + this.noopProgram = noopProgram; + this.solPoolPda = solPoolPda; + } + + static default(): SystemAccountPubkeys { + return new SystemAccountPubkeys( + LightSystemProgram.programId, + SystemProgram.programId, + defaultStaticAccountsStruct().accountCompressionProgram, + defaultStaticAccountsStruct().accountCompressionAuthority, + defaultStaticAccountsStruct().registeredProgramPda, + defaultStaticAccountsStruct().noopProgram, + PublicKey.default, + ); + } +} diff --git a/js/stateless.js/src/utils/validation.ts b/js/stateless.js/src/utils/validation.ts index 39ea74e319..018dc5a0f4 100644 --- a/js/stateless.js/src/utils/validation.ts +++ b/js/stateless.js/src/utils/validation.ts @@ -4,6 +4,7 @@ import { CompressedAccountWithMerkleContext, bn, } from '../state'; +import { featureFlags } from '../constants'; export const validateSufficientBalance = (balance: BN) => { if (balance.lt(bn(0))) { @@ -38,7 +39,15 @@ export const validateNumbersForProof = ( `Invalid number of compressed accounts for proof: ${hashesLength}. Allowed numbers: ${[1, 2, 3, 4].join(', ')}`, ); } - validateNumbers(hashesLength, [1, 2, 3, 4], 'compressed accounts'); + if (!featureFlags.isV2()) { + validateNumbers(hashesLength, [1, 2, 3, 4], 'compressed accounts'); + } else { + validateNumbers( + hashesLength, + [1, 2, 3, 4, 8], + 'compressed accounts', + ); + } validateNumbersForNonInclusionProof(newAddressesLength); } else { if (hashesLength > 0) { @@ -51,14 +60,22 @@ export const validateNumbersForProof = ( /// Ensure that the amount if compressed accounts is allowed. export const validateNumbersForInclusionProof = (hashesLength: number) => { - validateNumbers(hashesLength, [1, 2, 3, 4, 8], 'compressed accounts'); + if (!featureFlags.isV2()) { + validateNumbers(hashesLength, [1, 2, 3, 4], 'compressed accounts'); + } else { + validateNumbers(hashesLength, [1, 2, 3, 4, 8], 'compressed accounts'); + } }; /// Ensure that the amount if new addresses is allowed. export const validateNumbersForNonInclusionProof = ( newAddressesLength: number, ) => { - validateNumbers(newAddressesLength, [1, 2], 'new addresses'); + if (!featureFlags.isV2()) { + validateNumbers(newAddressesLength, [1, 2], 'new addresses'); + } else { + validateNumbers(newAddressesLength, [1, 2, 3, 4], 'new addresses'); + } }; /// V1 circuit safeguards. diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f0344f6ecc..43ec0b9ac3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -443,7 +443,11 @@ importers: program-tests: {} - program-tests/sdk-anchor-test: + programs: {} + + sdk-tests: {} + + sdk-tests/sdk-anchor-test: dependencies: '@coral-xyz/anchor': specifier: ^0.29.0 @@ -462,7 +466,7 @@ importers: specifier: ^10.0.10 version: 10.0.10 chai: - specifier: ^5.2.1 + specifier: ^5.2.0 version: 5.2.1 mocha: specifier: ^11.7.1 @@ -477,8 +481,6 @@ importers: specifier: ^5.9.2 version: 5.9.2 - programs: {} - tsconfig: {} packages: @@ -3506,10 +3508,6 @@ packages: resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} engines: {node: '>= 0.4'} - foreground-child@3.1.1: - resolution: {integrity: sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==} - engines: {node: '>=14'} - foreground-child@3.3.1: resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} engines: {node: '>=14'} @@ -4345,10 +4343,6 @@ packages: resolution: {integrity: sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - lru-cache@10.0.1: - resolution: {integrity: sha512-IJ4uwUTi2qCccrioU6g9g/5rvvVl13bsdczUUcqbciD9iLr095yj8DQKdObriEvuNSx325N1rV1O0sJFszx75g==} - engines: {node: 14 || >=16.14} - lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} @@ -4520,8 +4514,8 @@ packages: nanoassert@2.0.0: resolution: {integrity: sha512-7vO7n28+aYO4J+8w96AzhmU8G+Y/xpPDJz/se19ICsqj/momRbb9mh9ZUtkoJ5X3nTnPdhEJyc0qnM6yAsHBaA==} - nanoid@3.3.8: - resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==} + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true @@ -9630,7 +9624,7 @@ snapshots: debug: 4.4.1(supports-color@8.1.1) enhanced-resolve: 5.17.1 eslint: 8.57.0 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.9.2))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.9.2))(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.9.2))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.13.1(eslint@8.57.0)(typescript@5.9.2))(eslint@8.57.0) fast-glob: 3.3.2 get-tsconfig: 4.7.2 @@ -9652,7 +9646,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.9.2))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.9.2))(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0): + eslint-module-utils@2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.9.2))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0): dependencies: debug: 3.2.7 optionalDependencies: @@ -9921,7 +9915,7 @@ snapshots: fancy-test@2.0.42: dependencies: - '@types/chai': 4.3.20 + '@types/chai': 5.2.2 '@types/lodash': 4.14.199 '@types/node': 24.0.15 '@types/sinon': 10.0.16 @@ -10065,11 +10059,6 @@ snapshots: dependencies: is-callable: 1.2.7 - foreground-child@3.1.1: - dependencies: - cross-spawn: 7.0.6 - signal-exit: 4.1.0 - foreground-child@3.3.1: dependencies: cross-spawn: 7.0.6 @@ -10231,11 +10220,11 @@ snapshots: glob@11.0.0: dependencies: - foreground-child: 3.1.1 + foreground-child: 3.3.1 jackspeak: 4.0.1 minimatch: 10.0.3 minipass: 7.1.2 - package-json-from-dist: 1.0.0 + package-json-from-dist: 1.0.1 path-scurry: 2.0.0 glob@7.2.3: @@ -10419,7 +10408,7 @@ snapshots: hosted-git-info@7.0.1: dependencies: - lru-cache: 10.0.1 + lru-cache: 10.4.3 hosted-git-info@8.1.0: dependencies: @@ -10961,8 +10950,6 @@ snapshots: lowercase-keys@3.0.0: {} - lru-cache@10.0.1: {} - lru-cache@10.4.3: {} lru-cache@11.0.1: {} @@ -11137,7 +11124,7 @@ snapshots: nanoassert@2.0.0: {} - nanoid@3.3.8: {} + nanoid@3.3.11: {} natural-compare-lite@1.4.0: {} @@ -11582,7 +11569,7 @@ snapshots: postcss@8.5.1: dependencies: - nanoid: 3.3.8 + nanoid: 3.3.11 picocolors: 1.1.1 source-map-js: 1.2.1 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 974314d2d4..ad881a78f6 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -10,3 +10,4 @@ packages: - "examples/**" - "forester/**" - "program-tests/**" + - "sdk-tests/**" diff --git a/program-libs/account-checks/Cargo.toml b/program-libs/account-checks/Cargo.toml index 3f77092681..d0d766181c 100644 --- a/program-libs/account-checks/Cargo.toml +++ b/program-libs/account-checks/Cargo.toml @@ -7,7 +7,7 @@ license = "Apache-2.0" edition = "2021" [features] -default = [] +default = ["solana"] solana = [ "solana-program-error", "solana-sysvar", diff --git a/program-libs/account-checks/src/checks.rs b/program-libs/account-checks/src/checks.rs index fdbc043afa..96ef682b43 100644 --- a/program-libs/account-checks/src/checks.rs +++ b/program-libs/account-checks/src/checks.rs @@ -1,3 +1,6 @@ +use solana_msg::msg; +use solana_pubkey::Pubkey; + use crate::{ discriminator::{Discriminator, DISCRIMINATOR_LEN}, error::AccountError, @@ -130,6 +133,12 @@ pub fn check_owner( account_info: &A, ) -> Result<(), AccountError> { if !account_info.is_owned_by(owner) { + msg!("account_info.pubkey(): {:?}", account_info.pubkey()); + msg!( + "account_info.key(): {:?}", + Pubkey::new_from_array(account_info.key()) + ); + msg!("owner: {}", Pubkey::new_from_array(*owner)); return Err(AccountError::AccountOwnedByWrongProgram); } Ok(()) diff --git a/program-libs/compressed-account/src/address.rs b/program-libs/compressed-account/src/address.rs index 1e3f633ee0..8b1ff9fd94 100644 --- a/program-libs/compressed-account/src/address.rs +++ b/program-libs/compressed-account/src/address.rs @@ -40,6 +40,19 @@ pub fn derive_address( hashv_to_bn254_field_size_be_const_array::<4>(&slices).unwrap() } +/// Convenience function for calling derive_address with 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(), + &address_tree_pubkey.to_bytes(), + &program_id.to_bytes(), + ) +} + pub fn add_and_get_remaining_account_indices( pubkeys: &[Pubkey], remaining_accounts: &mut HashMap, diff --git a/program-libs/compressed-account/src/instruction_data/compressed_proof.rs b/program-libs/compressed-account/src/instruction_data/compressed_proof.rs index a2cde10a10..e8d1e577ca 100644 --- a/program-libs/compressed-account/src/instruction_data/compressed_proof.rs +++ b/program-libs/compressed-account/src/instruction_data/compressed_proof.rs @@ -80,3 +80,83 @@ impl Into> for ValidityProof { self.0 } } + +// Borsh compatible validity proof implementation. Use this in your anchor +// program unless you have zero-copy instruction data. Convert to zero-copy via +// `let proof = compression_params.proof.into();`. +// +// TODO: make the zerocopy implementation compatible with borsh serde via +// Anchor. +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)] + pub struct ValidityProof(pub Option); + + impl ValidityProof { + pub fn new(proof: Option) -> Self { + Self(proof) + } + } + + impl From for CompressedProof { + fn from(proof: super::CompressedProof) -> Self { + Self { + a: proof.a, + b: proof.b, + c: proof.c, + } + } + } + + impl From for super::CompressedProof { + fn from(proof: CompressedProof) -> Self { + Self { + a: proof.a, + b: proof.b, + c: proof.c, + } + } + } + + impl From for ValidityProof { + fn from(proof: super::ValidityProof) -> Self { + Self(proof.0.map(|p| p.into())) + } + } + + impl From for super::ValidityProof { + fn from(proof: ValidityProof) -> Self { + Self(proof.0.map(|p| p.into())) + } + } + + impl From for ValidityProof { + fn from(proof: CompressedProof) -> Self { + Self(Some(proof)) + } + } + + impl From> for ValidityProof { + fn from(proof: Option) -> Self { + Self(proof) + } + } +} diff --git a/program-libs/compressed-account/src/pubkey.rs b/program-libs/compressed-account/src/pubkey.rs index 9325f96988..821ef878f6 100644 --- a/program-libs/compressed-account/src/pubkey.rs +++ b/program-libs/compressed-account/src/pubkey.rs @@ -173,6 +173,22 @@ impl From for anchor_lang::prelude::Pubkey { } } +#[cfg(feature = "solana")] +#[cfg(not(feature = "anchor"))] +impl From for Pubkey { + fn from(pubkey: solana_pubkey::Pubkey) -> Self { + Self::new_from_array(pubkey.to_bytes()) + } +} + +#[cfg(feature = "solana")] +#[cfg(not(feature = "anchor"))] +impl From<&solana_pubkey::Pubkey> for Pubkey { + fn from(pubkey: &solana_pubkey::Pubkey) -> Self { + Self::new_from_array(pubkey.to_bytes()) + } +} + #[cfg(feature = "anchor")] impl From<&Pubkey> for anchor_lang::prelude::Pubkey { fn from(pubkey: &Pubkey) -> Self { diff --git a/program-libs/ctoken-types/src/instructions/create_compressed_mint.rs b/program-libs/ctoken-types/src/instructions/create_compressed_mint.rs new file mode 100644 index 0000000000..79b7121be8 --- /dev/null +++ b/program-libs/ctoken-types/src/instructions/create_compressed_mint.rs @@ -0,0 +1,190 @@ +use light_compressed_account::{ + instruction_data::{ + compressed_proof::CompressedProof, zero_copy_set::CompressedCpiContextTrait, + }, + Pubkey, +}; +use light_zero_copy::{ZeroCopy, ZeroCopyMut}; + +use crate::{ + instructions::extensions::ExtensionInstructionData, + state::{CompressedMint, ExtensionStruct}, + AnchorDeserialize, AnchorSerialize, CTokenError, +}; + +#[repr(C)] +#[derive(Debug, Clone, AnchorSerialize, AnchorDeserialize, ZeroCopy)] +pub struct CreateCompressedMintInstructionData { + pub decimals: u8, + pub mint_authority: Pubkey, + pub mint_bump: u8, + pub address_merkle_tree_root_index: u16, + // compressed address TODO: make a type CompressedAddress (not straight forward because of AnchorSerialize) + pub mint_address: [u8; 32], + pub version: u8, + pub freeze_authority: Option, + pub extensions: Option>, + pub cpi_context: Option, + /// To create the compressed mint account address a proof is always required. + /// Set none if used with cpi context, the proof is required with the executing cpi. + pub proof: Option, +} + +#[repr(C)] +#[derive(Debug, Clone, AnchorSerialize, AnchorDeserialize, ZeroCopy)] +pub struct CompressedMintWithContext { + pub leaf_index: u32, + pub prove_by_index: bool, + pub root_index: u16, + pub address: [u8; 32], + pub mint: CompressedMintInstructionData, +} + +impl CompressedMintWithContext { + pub fn new( + compressed_address: [u8; 32], + root_index: u16, + decimals: u8, + mint_authority: Option, + freeze_authority: Option, + spl_mint: Pubkey, + ) -> Self { + Self { + leaf_index: 0, + prove_by_index: false, + root_index, + address: compressed_address, + mint: CompressedMintInstructionData { + version: 0, + spl_mint, + supply: 0, // TODO: dynamic? + decimals, + is_decompressed: false, + mint_authority, + freeze_authority, + extensions: None, + }, + } + } + + pub fn new_with_extensions( + compressed_address: [u8; 32], + root_index: u16, + decimals: u8, + mint_authority: Option, + freeze_authority: Option, + spl_mint: Pubkey, + extensions: Option>, + ) -> Self { + Self { + leaf_index: 0, + prove_by_index: false, + root_index, + address: compressed_address, + mint: CompressedMintInstructionData { + version: 0, + spl_mint, + supply: 0, + decimals, + is_decompressed: false, + mint_authority, + freeze_authority, + extensions, + }, + } + } +} + +#[repr(C)] +#[derive(Debug, PartialEq, Eq, Clone, AnchorSerialize, AnchorDeserialize, ZeroCopy)] +pub struct CompressedMintInstructionData { + /// Version for upgradability + pub version: u8, + /// Pda with seed address of compressed mint + pub spl_mint: Pubkey, + /// Total supply of tokens. + pub supply: u64, + /// Number of base 10 digits to the right of the decimal place. + pub decimals: u8, + /// Extension, necessary for mint to. + pub is_decompressed: bool, + /// Optional authority used to mint new tokens. The mint authority may only + /// be provided during mint creation. If no mint authority is present + /// then the mint has a fixed supply and no further tokens may be + /// minted. + pub mint_authority: Option, + /// Optional authority to freeze token accounts. + pub freeze_authority: Option, + pub extensions: Option>, +} + +impl TryFrom for CompressedMintInstructionData { + type Error = CTokenError; + + fn try_from(mint: CompressedMint) -> Result { + let extensions = match mint.extensions { + Some(exts) => { + let converted_exts: Result, Self::Error> = exts + .into_iter() + .map(|ext| match ext { + /* ExtensionStruct::MetadataPointer(metadata_pointer) => { + Ok(ExtensionInstructionData::MetadataPointer( + crate::instructions::extensions::metadata_pointer::InitMetadataPointer { + authority: metadata_pointer.authority, + metadata_address: metadata_pointer.metadata_address, + }, + )) + }*/ + ExtensionStruct::TokenMetadata(token_metadata) => { + Ok(ExtensionInstructionData::TokenMetadata( + crate::instructions::extensions::token_metadata::TokenMetadataInstructionData { + update_authority: token_metadata.update_authority, + metadata: token_metadata.metadata, + additional_metadata: Some(token_metadata.additional_metadata), + version: token_metadata.version, + }, + )) + } + _ => { + Err(CTokenError::UnsupportedExtension) + } + }) + .collect(); + Some(converted_exts?) + } + None => None, + }; + + Ok(Self { + version: mint.version, + spl_mint: mint.spl_mint, + supply: mint.supply, + decimals: mint.decimals, + mint_authority: mint.mint_authority, + is_decompressed: mint.is_decompressed, + freeze_authority: mint.freeze_authority, + extensions, + }) + } +} +#[repr(C)] +#[derive( + Debug, Clone, PartialEq, Eq, AnchorSerialize, AnchorDeserialize, ZeroCopy, ZeroCopyMut, +)] +pub struct CpiContext { + pub set_context: bool, + pub first_set_context: bool, + pub address_tree_index: u8, + pub out_queue_index: u8, + pub cpi_context_pubkey: Pubkey, +} + +impl CompressedCpiContextTrait for ZCpiContext<'_> { + fn first_set_context(&self) -> u8 { + self.first_set_context() as u8 + } + + fn set_context(&self) -> u8 { + self.set_context() as u8 + } +} diff --git a/program-libs/ctoken-types/src/instructions/create_spl_mint.rs b/program-libs/ctoken-types/src/instructions/create_spl_mint.rs new file mode 100644 index 0000000000..d5b4c1b39b --- /dev/null +++ b/program-libs/ctoken-types/src/instructions/create_spl_mint.rs @@ -0,0 +1,17 @@ +use light_compressed_account::instruction_data::compressed_proof::CompressedProof; +use light_zero_copy::ZeroCopy; + +use crate::{ + instructions::create_compressed_mint::CompressedMintWithContext, AnchorDeserialize, + AnchorSerialize, +}; + +#[repr(C)] +#[derive(ZeroCopy, AnchorDeserialize, AnchorSerialize, Clone, Debug)] +pub struct CreateSplMintInstructionData { + pub mint_bump: u8, + pub mint_authority_is_none: bool, // if mint authority is None anyone can create the spl mint. + pub cpi_context: bool, // Can only execute since mutates solana account state. + pub mint: CompressedMintWithContext, + pub proof: Option, +} diff --git a/program-libs/ctoken-types/src/instructions/extensions/metadata_pointer.rs b/program-libs/ctoken-types/src/instructions/extensions/metadata_pointer.rs new file mode 100644 index 0000000000..cc56e0b46d --- /dev/null +++ b/program-libs/ctoken-types/src/instructions/extensions/metadata_pointer.rs @@ -0,0 +1,109 @@ +use light_compressed_account::Pubkey; +use light_hasher::{ + hash_to_field_size::hashv_to_bn254_field_size_be_const_array, DataHasher, Hasher, HasherError, +}; +use light_zero_copy::{ZeroCopy, ZeroCopyMut}; + +use crate::{ + context::HashCache, state::ExtensionType, AnchorDeserialize, AnchorSerialize, CTokenError, +}; + +/// Metadata pointer extension data for compressed mints. +#[derive( + Debug, Clone, PartialEq, Eq, AnchorSerialize, ZeroCopy, AnchorDeserialize, ZeroCopyMut, +)] +pub struct MetadataPointer { + /// Authority that can set the metadata address + pub authority: Option, + /// (Compressed) address that holds the metadata (in token 22) + pub metadata_address: Option, +} + +impl DataHasher for MetadataPointer { + fn hash(&self) -> Result<[u8; 32], HasherError> { + let mut discriminator = [0u8; 32]; + discriminator[31] = ExtensionType::MetadataPointer as u8; + let hashed_metadata_address = if let Some(metadata_address) = self.metadata_address { + hashv_to_bn254_field_size_be_const_array::<2>(&[metadata_address.as_ref()])? + } else { + [0u8; 32] + }; + let hashed_authority = if let Some(authority) = self.authority { + hashv_to_bn254_field_size_be_const_array::<2>(&[authority.as_ref()])? + } else { + [0u8; 32] + }; + H::hashv(&[ + discriminator.as_slice(), + hashed_metadata_address.as_slice(), + hashed_authority.as_slice(), + ]) + } +} + +/// Instruction data for initializing metadata pointer +#[derive(Debug, Clone, PartialEq, Eq, AnchorSerialize, AnchorDeserialize, ZeroCopy)] +pub struct InitMetadataPointer { + /// The authority that can set the metadata address + pub authority: Option, + /// The account address that holds the metadata + pub metadata_address: Option, +} + +impl InitMetadataPointer { + pub fn hash_metadata_pointer( + &self, + context: &mut HashCache, + ) -> Result<[u8; 32], CTokenError> { + let mut discriminator = [0u8; 32]; + discriminator[31] = ExtensionType::MetadataPointer as u8; + + let hashed_metadata_address = if let Some(metadata_address) = self.metadata_address { + context.get_or_hash_pubkey(&metadata_address.into()) + } else { + [0u8; 32] + }; + + let hashed_authority = if let Some(authority) = self.authority { + context.get_or_hash_pubkey(&authority.into()) + } else { + [0u8; 32] + }; + + H::hashv(&[ + discriminator.as_slice(), + hashed_metadata_address.as_slice(), + hashed_authority.as_slice(), + ]) + .map_err(CTokenError::from) + } +} + +impl ZInitMetadataPointer<'_> { + pub fn hash_metadata_pointer( + &self, + context: &mut HashCache, + ) -> Result<[u8; 32], CTokenError> { + let mut discriminator = [0u8; 32]; + discriminator[31] = ExtensionType::MetadataPointer as u8; + + let hashed_metadata_address = if let Some(metadata_address) = self.metadata_address { + context.get_or_hash_pubkey(&(*metadata_address).into()) + } else { + [0u8; 32] + }; + + let hashed_authority = if let Some(authority) = self.authority { + context.get_or_hash_pubkey(&(*authority).into()) + } else { + [0u8; 32] + }; + + H::hashv(&[ + discriminator.as_slice(), + hashed_metadata_address.as_slice(), + hashed_authority.as_slice(), + ]) + .map_err(CTokenError::from) + } +} diff --git a/program-libs/ctoken-types/src/instructions/mint_action/cpi_context.rs b/program-libs/ctoken-types/src/instructions/mint_action/cpi_context.rs index 0424364e0b..b3d3ba1eea 100644 --- a/program-libs/ctoken-types/src/instructions/mint_action/cpi_context.rs +++ b/program-libs/ctoken-types/src/instructions/mint_action/cpi_context.rs @@ -34,3 +34,22 @@ impl CompressedCpiContextTrait for ZCpiContext<'_> { } } } + +impl CpiContext { + /// Specific helper for creating a cmint as last use of cpi context. + pub fn last_cpi_create_mint( + address_tree_index: u8, + output_state_queue_index: u8, + mint_account_index: u8, + ) -> Self { + Self { + set_context: false, + first_set_context: false, + in_tree_index: address_tree_index, + in_queue_index: 0, // unused + out_queue_index: output_state_queue_index, + token_out_queue_index: output_state_queue_index, + assigned_account_index: mint_account_index, + } + } +} diff --git a/program-libs/ctoken-types/src/instructions/mint_action/instruction_data.rs b/program-libs/ctoken-types/src/instructions/mint_action/instruction_data.rs index 79eb62ddf8..d1075cfe93 100644 --- a/program-libs/ctoken-types/src/instructions/mint_action/instruction_data.rs +++ b/program-libs/ctoken-types/src/instructions/mint_action/instruction_data.rs @@ -1,4 +1,4 @@ -use light_compressed_account::instruction_data::compressed_proof::CompressedProof; +use light_compressed_account::{instruction_data::compressed_proof::CompressedProof, Pubkey}; use light_zero_copy::ZeroCopy; use super::{ @@ -49,13 +49,13 @@ pub struct MintActionCompressedInstructionData { /// If proof by index not used. pub root_index: u16, pub compressed_address: [u8; 32], + /// If some -> no input because we create mint + pub mint: CompressedMintInstructionData, pub token_pool_bump: u8, pub token_pool_index: u8, pub actions: Vec, pub proof: Option, pub cpi_context: Option, - /// If some -> no input because we create mint - pub mint: CompressedMintInstructionData, } #[repr(C)] @@ -68,6 +68,64 @@ pub struct CompressedMintWithContext { pub mint: CompressedMintInstructionData, } +impl CompressedMintWithContext { + pub fn new( + compressed_address: [u8; 32], + root_index: u16, + decimals: u8, + mint_authority: Option, + freeze_authority: Option, + spl_mint: Pubkey, + ) -> Self { + Self { + leaf_index: 0, + prove_by_index: false, + root_index, + address: compressed_address, + mint: CompressedMintInstructionData { + base: BaseCompressedMint { + version: 0, + spl_mint, + supply: 0, // TODO: dynamic? + decimals, + is_decompressed: false, + mint_authority, + freeze_authority, + }, + extensions: None, + }, + } + } + + pub fn new_with_extensions( + compressed_address: [u8; 32], + root_index: u16, + decimals: u8, + mint_authority: Option, + freeze_authority: Option, + spl_mint: Pubkey, + extensions: Option>, + ) -> Self { + Self { + leaf_index: 0, + prove_by_index: false, + root_index, + address: compressed_address, + mint: CompressedMintInstructionData { + base: BaseCompressedMint { + version: 0, + spl_mint, + supply: 0, // TODO: dynamic? + decimals, + is_decompressed: false, + mint_authority, + freeze_authority, + }, + extensions, + }, + } + } +} #[repr(C)] #[derive(Debug, PartialEq, Eq, Clone, AnchorSerialize, AnchorDeserialize, ZeroCopy)] pub struct CompressedMintInstructionData { diff --git a/program-libs/ctoken-types/src/instructions/mint_actions.rs b/program-libs/ctoken-types/src/instructions/mint_actions.rs new file mode 100644 index 0000000000..65ea87bb5b --- /dev/null +++ b/program-libs/ctoken-types/src/instructions/mint_actions.rs @@ -0,0 +1,133 @@ +use light_compressed_account::{ + instruction_data::{ + compressed_proof::CompressedProof, zero_copy_set::CompressedCpiContextTrait, + }, + Pubkey, +}; +use light_zero_copy::{ZeroCopy, ZeroCopyMut}; + +use crate::{ + instructions::{ + create_compressed_mint::CompressedMintInstructionData, mint_to_compressed::MintToAction, + }, + AnchorDeserialize, AnchorSerialize, +}; + +#[repr(C)] +#[derive(Debug, Clone, AnchorSerialize, AnchorDeserialize, ZeroCopy)] +pub struct UpdateAuthority { + pub new_authority: Option, // None = revoke authority, Some(key) = set new authority +} + +#[repr(C)] +#[derive(Debug, Clone, AnchorSerialize, AnchorDeserialize, ZeroCopy)] +pub struct CreateSplMintAction { + pub mint_bump: u8, +} + +#[repr(C)] +#[derive(Debug, Clone, AnchorSerialize, AnchorDeserialize, ZeroCopy)] +pub struct DecompressedRecipient { + pub account_index: u8, // Index into remaining accounts for the recipient token account + pub amount: u64, +} + +#[repr(C)] +#[derive(Debug, Clone, AnchorSerialize, AnchorDeserialize, ZeroCopy)] +pub struct MintToDecompressedAction { + pub recipient: DecompressedRecipient, +} + +#[repr(C)] +#[derive(Debug, Clone, AnchorSerialize, AnchorDeserialize, ZeroCopy)] +pub struct UpdateMetadataFieldAction { + pub extension_index: u8, // Index of the TokenMetadata extension in the extensions array + pub field_type: u8, // 0=Name, 1=Symbol, 2=Uri, 3=Custom key + pub key: Vec, // Empty for Name/Symbol/Uri, key string for custom fields + pub value: Vec, // UTF-8 encoded value +} + +#[repr(C)] +#[derive(Debug, Clone, AnchorSerialize, AnchorDeserialize, ZeroCopy)] +pub struct UpdateMetadataAuthorityAction { + pub extension_index: u8, // Index of the TokenMetadata extension in the extensions array + pub new_authority: Pubkey, // Use zero bytes to set to None +} + +#[repr(C)] +#[derive(Debug, Clone, AnchorSerialize, AnchorDeserialize, ZeroCopy)] +pub struct RemoveMetadataKeyAction { + pub extension_index: u8, // Index of the TokenMetadata extension in the extensions array + pub key: Vec, // UTF-8 encoded key to remove + pub idempotent: u8, // 0=false, 1=true - don't error if key doesn't exist +} + +#[repr(C)] +#[derive(Debug, Clone, AnchorSerialize, AnchorDeserialize, ZeroCopy)] +pub enum Action { + /// Mint compressed tokens to compressed accounts. + MintTo(MintToAction), + /// Update mint authority of a compressed mint account. + UpdateMintAuthority(UpdateAuthority), + /// Update freeze authority of a compressed mint account. + UpdateFreezeAuthority(UpdateAuthority), + /// Create an spl mint for a cmint. + /// - existing supply is minted to a token pool account. + /// - mint and freeze authority are a ctoken pda. + /// - is an spl-token-2022 mint account. + CreateSplMint(CreateSplMintAction), + /// Mint ctokens from a cmint to a ctoken solana account + /// (tokens are not compressed but not spl tokens). + MintToDecompressed(MintToDecompressedAction), + UpdateMetadataField(UpdateMetadataFieldAction), + UpdateMetadataAuthority(UpdateMetadataAuthorityAction), + RemoveMetadataKey(RemoveMetadataKeyAction), +} + +#[repr(C)] +#[derive(Debug, Clone, AnchorSerialize, AnchorDeserialize, ZeroCopy)] +pub struct MintActionCompressedInstructionData { + pub create_mint: bool, + /// Only used if create mint + pub mint_bump: u8, + /// Only set if mint already exists + pub leaf_index: u32, + /// Only set if mint already exists + pub prove_by_index: bool, + /// If create mint, root index of address proof + /// If mint already exists, root index of validity proof + /// If proof by index not used. + pub root_index: u16, + pub compressed_address: [u8; 32], + /// If some -> no input because we create mint + pub mint: CompressedMintInstructionData, + pub token_pool_bump: u8, + pub token_pool_index: u8, + pub actions: Vec, + pub proof: Option, + pub cpi_context: Option, +} + +#[repr(C)] +#[derive(Debug, Clone, AnchorSerialize, AnchorDeserialize, ZeroCopy, ZeroCopyMut)] +pub struct CpiContext { + pub set_context: bool, + pub first_set_context: bool, + // Used as address tree index if create mint + pub in_tree_index: u8, + pub in_queue_index: u8, + pub out_queue_index: u8, + pub token_out_queue_index: u8, + // Index of the compressed account that should receive the new address (0 = mint, 1+ = token accounts) + pub assigned_account_index: u8, +} +impl CompressedCpiContextTrait for ZCpiContext<'_> { + fn first_set_context(&self) -> u8 { + self.first_set_context() as u8 + } + + fn set_context(&self) -> u8 { + self.set_context() as u8 + } +} + diff --git a/program-libs/ctoken-types/src/instructions/mint_to_compressed.rs b/program-libs/ctoken-types/src/instructions/mint_to_compressed.rs new file mode 100644 index 0000000000..646b4bd44c --- /dev/null +++ b/program-libs/ctoken-types/src/instructions/mint_to_compressed.rs @@ -0,0 +1,68 @@ +use light_compressed_account::{ + instruction_data::{ + compressed_proof::CompressedProof, zero_copy_set::CompressedCpiContextTrait, + }, + Pubkey, +}; +use light_zero_copy::{ZeroCopy, ZeroCopyMut}; + +use crate::{ + instructions::create_compressed_mint::CompressedMintWithContext, AnchorDeserialize, + AnchorSerialize, +}; + +/* TODO: double check that it is only used in tests + * #[derive(Debug, Clone, AnchorSerialize, AnchorDeserialize, ZeroCopy)] +pub struct CompressedMintInputs { + pub leaf_index: u32, + pub prove_by_index: bool, + pub root_index: u16, + pub address: [u8; 32], + pub compressed_mint_input: CompressedMint, //TODO: move supply and authority last so that we can send only the hash chain. +}*/ + +#[repr(C)] +#[derive(Debug, Clone, AnchorSerialize, AnchorDeserialize, ZeroCopy)] +pub struct Recipient { + pub recipient: Pubkey, + pub amount: u64, +} + +#[repr(C)] +#[derive(Debug, Clone, AnchorSerialize, AnchorDeserialize, ZeroCopy)] +pub struct MintToAction { + pub token_account_version: u8, + pub lamports: Option, + pub recipients: Vec, +} + +#[repr(C)] +#[derive(Debug, Clone, AnchorSerialize, AnchorDeserialize, ZeroCopy)] +pub struct MintToCompressedInstructionData { + pub token_account_version: u8, + pub compressed_mint_inputs: CompressedMintWithContext, + pub proof: Option, + pub lamports: Option, + pub recipients: Vec, + pub cpi_context: Option, +} + +#[repr(C)] +#[derive(Debug, Clone, AnchorSerialize, AnchorDeserialize, ZeroCopy, ZeroCopyMut)] +pub struct CpiContext { + pub set_context: bool, + pub first_set_context: bool, + pub in_tree_index: u8, + pub in_queue_index: u8, + pub out_queue_index: u8, + pub token_out_queue_index: u8, +} +impl CompressedCpiContextTrait for ZCpiContext<'_> { + fn first_set_context(&self) -> u8 { + self.first_set_context() as u8 + } + + fn set_context(&self) -> u8 { + self.set_context() as u8 + } +} diff --git a/program-libs/ctoken-types/src/instructions/transfer2.rs b/program-libs/ctoken-types/src/instructions/transfer2.rs index 3196328e77..5000cd5a2a 100644 --- a/program-libs/ctoken-types/src/instructions/transfer2.rs +++ b/program-libs/ctoken-types/src/instructions/transfer2.rs @@ -9,6 +9,7 @@ use zerocopy::Ref; use crate::{AnchorDeserialize, AnchorSerialize, CTokenError}; +// 22 bytes #[repr(C)] #[derive( Debug, @@ -28,7 +29,7 @@ pub struct MultiInputTokenDataWithContext { pub delegate: u8, pub mint: u8, pub version: u8, - pub merkle_context: PackedMerkleContext, + pub merkle_context: PackedMerkleContext, // 7 pub root_index: u16, } diff --git a/program-libs/ctoken-types/src/instructions/update_compressed_mint.rs b/program-libs/ctoken-types/src/instructions/update_compressed_mint.rs new file mode 100644 index 0000000000..86427351ca --- /dev/null +++ b/program-libs/ctoken-types/src/instructions/update_compressed_mint.rs @@ -0,0 +1,70 @@ +use light_compressed_account::{ + instruction_data::{ + compressed_proof::CompressedProof, zero_copy_set::CompressedCpiContextTrait, + }, + Pubkey, +}; +use light_zero_copy::{ZeroCopy, ZeroCopyMut}; + +use crate::{ + instructions::create_compressed_mint::CompressedMintWithContext, AnchorDeserialize, + AnchorSerialize, CTokenError, +}; + +/// Authority types for compressed mint updates, following SPL Token-2022 pattern +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, AnchorSerialize, AnchorDeserialize)] +pub enum CompressedMintAuthorityType { + /// Authority to mint new tokens + MintTokens = 0, + /// Authority to freeze token accounts + FreezeAccount = 1, +} + +impl TryFrom for CompressedMintAuthorityType { + type Error = CTokenError; + + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(CompressedMintAuthorityType::MintTokens), + 1 => Ok(CompressedMintAuthorityType::FreezeAccount), + _ => Err(CTokenError::InvalidAuthorityType), + } + } +} + +impl From for u8 { + fn from(authority_type: CompressedMintAuthorityType) -> u8 { + authority_type as u8 + } +} + +#[repr(C)] +#[derive(Debug, Clone, AnchorSerialize, AnchorDeserialize, ZeroCopy)] +pub struct UpdateCompressedMintInstructionData { + pub authority_type: u8, // CompressedMintAuthorityType as u8 + pub compressed_mint_inputs: CompressedMintWithContext, + pub new_authority: Option, // None = revoke authority, Some(key) = set new authority + pub proof: Option, + pub cpi_context: Option, +} + +#[repr(C)] +#[derive(Debug, Clone, AnchorSerialize, AnchorDeserialize, ZeroCopy, ZeroCopyMut)] +pub struct UpdateMintCpiContext { + pub set_context: bool, + pub first_set_context: bool, + pub in_tree_index: u8, + pub in_queue_index: u8, + pub out_queue_index: u8, +} + +impl CompressedCpiContextTrait for ZUpdateMintCpiContext<'_> { + fn first_set_context(&self) -> u8 { + self.first_set_context() as u8 + } + + fn set_context(&self) -> u8 { + self.set_context() as u8 + } +} diff --git a/program-libs/ctoken-types/src/instructions/update_metadata.rs b/program-libs/ctoken-types/src/instructions/update_metadata.rs new file mode 100644 index 0000000000..8b29cc24d4 --- /dev/null +++ b/program-libs/ctoken-types/src/instructions/update_metadata.rs @@ -0,0 +1,111 @@ +use light_compressed_account::{instruction_data::compressed_proof::CompressedProof, Pubkey}; +use light_zero_copy::{traits::ZeroCopyAt, ZeroCopy}; + +use crate::{ + instructions::{ + create_compressed_mint::{CompressedMintWithContext, ZCompressedMintWithContext}, + update_compressed_mint::UpdateMintCpiContext, + }, + AnchorDeserialize, AnchorSerialize, +}; + +/// Authority types for compressed mint updates, following SPL Token-2022 pattern +#[repr(u8)] +#[derive(Debug, Clone, PartialEq, Eq, AnchorSerialize, AnchorDeserialize)] +pub enum MetadataUpdate { + UpdateAuthority(UpdateAuthority), + UpdateKey(UpdateKey), + RemoveKey(RemoveKey), +} + +#[repr(u8)] +#[derive(Debug, Clone, PartialEq)] +pub enum ZMetadataUpdate<'a> { + UpdateAuthority(ZUpdateAuthority<'a>), + UpdateKey(ZUpdateKey<'a>), + RemoveKey(ZRemoveKey<'a>), +} + +#[repr(C)] +#[derive(Debug, Clone, PartialEq, Eq, AnchorSerialize, AnchorDeserialize, ZeroCopy)] +pub struct UpdateKey { + pub extension_index: u8, + pub key_index: u8, + pub key: Vec, + pub value: Vec, +} + +#[repr(C)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, AnchorSerialize, AnchorDeserialize, ZeroCopy)] +pub struct RemoveKey { + pub extension_index: u8, + pub key_index: u8, +} + +#[repr(C)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, AnchorSerialize, AnchorDeserialize, ZeroCopy)] +pub struct UpdateAuthority { + pub extension_index: u8, + pub new_authority: Pubkey, +} + +#[derive(Debug, Clone, AnchorSerialize, AnchorDeserialize)] +pub struct UpdateMetadataInstructionData { + pub mint: CompressedMintWithContext, + pub updates: Vec, + pub proof: Option, + pub cpi_context: Option, +} + +pub struct ZUpdateMetadataInstructionData<'a> { + pub mint: ZCompressedMintWithContext<'a>, + pub updates: Vec>, + pub proof: as ZeroCopyAt<'a>>::ZeroCopyAt, + pub cpi_context: as ZeroCopyAt<'a>>::ZeroCopyAt, +} + +impl<'a> ZeroCopyAt<'a> for UpdateMetadataInstructionData { + type ZeroCopyAt = ZUpdateMetadataInstructionData<'a>; + fn zero_copy_at( + bytes: &'a [u8], + ) -> Result<(Self::ZeroCopyAt, &'a [u8]), light_zero_copy::errors::ZeroCopyError> { + let (mint, bytes) = CompressedMintWithContext::zero_copy_at(bytes)?; + let (updates, bytes) = Vec::::zero_copy_at(bytes)?; + let (proof, bytes) = as ZeroCopyAt<'a>>::zero_copy_at(bytes)?; + let (cpi_context, bytes) = + as ZeroCopyAt<'a>>::zero_copy_at(bytes)?; + Ok(( + ZUpdateMetadataInstructionData { + mint, + updates, + proof, + cpi_context, + }, + bytes, + )) + } +} + +impl<'a> ZeroCopyAt<'a> for MetadataUpdate { + type ZeroCopyAt = ZMetadataUpdate<'a>; + fn zero_copy_at( + bytes: &'a [u8], + ) -> Result<(Self::ZeroCopyAt, &'a [u8]), light_zero_copy::errors::ZeroCopyError> { + let (enum_bytes, bytes) = bytes.split_at(1); + match enum_bytes[0] { + 0 => { + let (authority, bytes) = UpdateAuthority::zero_copy_at(bytes)?; + Ok((ZMetadataUpdate::UpdateAuthority(authority), bytes)) + } + 1 => { + let (update_key, bytes) = UpdateKey::zero_copy_at(bytes)?; + Ok((ZMetadataUpdate::UpdateKey(update_key), bytes)) + } + 2 => { + let (remove_key, bytes) = RemoveKey::zero_copy_at(bytes)?; + Ok((ZMetadataUpdate::RemoveKey(remove_key), bytes)) + } + _ => Err(light_zero_copy::errors::ZeroCopyError::InvalidEnumValue), + } + } +} diff --git a/program-libs/ctoken-types/src/state/extensions/compressible.rs b/program-libs/ctoken-types/src/state/extensions/compressible.rs index 3f5dd43d74..8a93587ab9 100644 --- a/program-libs/ctoken-types/src/state/extensions/compressible.rs +++ b/program-libs/ctoken-types/src/state/extensions/compressible.rs @@ -44,7 +44,7 @@ impl CompressibleExtension { self.slots_until_compression } - pub fn set_last_written_slot(&mut self, slot: u64) { + pub fn bump_last_written_slot(&mut self, slot: u64) { self.last_written_slot = slot; } } diff --git a/program-libs/hasher/src/keccak.rs b/program-libs/hasher/src/keccak.rs index 81d81d810c..ab1c666ee8 100644 --- a/program-libs/hasher/src/keccak.rs +++ b/program-libs/hasher/src/keccak.rs @@ -9,6 +9,8 @@ use crate::{ pub struct Keccak; impl Hasher for Keccak { + const ID: u8 = 2; + fn hash(val: &[u8]) -> Result { Self::hashv(&[val]) } diff --git a/program-libs/hasher/src/lib.rs b/program-libs/hasher/src/lib.rs index 9f4e4758c0..83a0875ae9 100644 --- a/program-libs/hasher/src/lib.rs +++ b/program-libs/hasher/src/lib.rs @@ -24,6 +24,7 @@ pub const HASH_BYTES: usize = 32; pub type Hash = [u8; HASH_BYTES]; pub trait Hasher { + const ID: u8; fn hash(val: &[u8]) -> Result; fn hashv(vals: &[&[u8]]) -> Result; fn zero_bytes() -> ZeroBytes; diff --git a/program-libs/hasher/src/poseidon.rs b/program-libs/hasher/src/poseidon.rs index 0cd6c670da..b13d4a6a83 100644 --- a/program-libs/hasher/src/poseidon.rs +++ b/program-libs/hasher/src/poseidon.rs @@ -78,6 +78,8 @@ impl From for u64 { pub struct Poseidon; impl Hasher for Poseidon { + const ID: u8 = 0; + fn hash(val: &[u8]) -> Result { Self::hashv(&[val]) } diff --git a/program-libs/hasher/src/sha256.rs b/program-libs/hasher/src/sha256.rs index 907d1f65ab..7a0c92506a 100644 --- a/program-libs/hasher/src/sha256.rs +++ b/program-libs/hasher/src/sha256.rs @@ -9,6 +9,7 @@ use crate::{ pub struct Sha256; impl Hasher for Sha256 { + const ID: u8 = 1; fn hash(val: &[u8]) -> Result { Self::hashv(&[val]) } @@ -54,6 +55,7 @@ impl Hasher for Sha256 { pub struct Sha256BE; impl Hasher for Sha256BE { + const ID: u8 = 2; fn hash(val: &[u8]) -> Result { let mut hash = Self::hashv(&[val])?; hash[0] = 0; diff --git a/program-libs/zero-copy-derive/Cargo.toml b/program-libs/zero-copy-derive/Cargo.toml index 1cdc8254e8..2ac89effe2 100644 --- a/program-libs/zero-copy-derive/Cargo.toml +++ b/program-libs/zero-copy-derive/Cargo.toml @@ -24,3 +24,5 @@ rand = "0.8" borsh = { workspace = true } light-zero-copy = { workspace = true, features = ["std", "derive"] } zerocopy = { workspace = true, features = ["derive"] } +light-sdk-macros = { workspace = true } +light-hasher = { workspace = true, features = ["zero-copy"] } diff --git a/program-libs/zero-copy-derive/src/shared/z_struct.rs b/program-libs/zero-copy-derive/src/shared/z_struct.rs index a15fce4580..0f6f9bfd71 100644 --- a/program-libs/zero-copy-derive/src/shared/z_struct.rs +++ b/program-libs/zero-copy-derive/src/shared/z_struct.rs @@ -320,6 +320,12 @@ fn generate_struct_fields_with_zerocopy_types<'a, const MUT: bool>( pub #field_name: <#field_type as #trait_name<'a>>::#associated_type_ident } } + // FieldType::Bool(field_name) => { + // quote! { + // #(#attributes)* + // pub #field_name: >::Output + // } + // } FieldType::Copy(field_name, field_type) => { let zerocopy_type = utils::convert_to_zerocopy_type(field_type); quote! { diff --git a/program-libs/zero-copy-derive/tests/action_enum_test.rs b/program-libs/zero-copy-derive/tests/action_enum_test.rs new file mode 100644 index 0000000000..396c7df497 --- /dev/null +++ b/program-libs/zero-copy-derive/tests/action_enum_test.rs @@ -0,0 +1,74 @@ +use light_zero_copy_derive::ZeroCopy; + +// Test struct for the MintTo action +#[derive(Debug, Clone, PartialEq, ZeroCopy)] +pub struct MintToAction { + pub amount: u64, + pub recipient: Vec, +} + +// Test enum similar to your Action example +#[derive(Debug, Clone, ZeroCopy)] +pub enum Action { + MintTo(MintToAction), + Update, + CreateSplMint, + UpdateMetadata, +} + +#[cfg(test)] +mod tests { + use light_zero_copy::traits::ZeroCopyAt; + + use super::*; + + #[test] + fn test_action_enum_unit_variants() { + // Test Update variant (discriminant 1) + let data = [1u8]; + let (result, remaining) = Action::zero_copy_at(&data).unwrap(); + // We can't pattern match without importing the generated type, + // but we can verify it doesn't panic and processes correctly + println!("Successfully deserialized Update variant"); + assert_eq!(remaining.len(), 0); + } + + #[test] + fn test_action_enum_data_variant() { + // Test MintTo variant (discriminant 0) + let mut data = vec![0u8]; // discriminant 0 for MintTo + + // Add MintToAction serialized data + // amount: 1000 + data.extend_from_slice(&1000u64.to_le_bytes()); + + // recipient: "alice" (5 bytes length + "alice") + data.extend_from_slice(&5u32.to_le_bytes()); + data.extend_from_slice(b"alice"); + + let (result, remaining) = Action::zero_copy_at(&data).unwrap(); + // We can't easily pattern match without the generated type imported, + // but we can verify it processes without errors + println!("Successfully deserialized MintTo variant"); + assert_eq!(remaining.len(), 0); + } + + #[test] + fn test_action_enum_all_unit_variants() { + // Test all unit variants + let variants = [ + (1u8, "Update"), + (2u8, "CreateSplMint"), + (3u8, "UpdateMetadata"), + ]; + + for (discriminant, name) in variants { + let data = [discriminant]; + let result = Action::zero_copy_at(&data); + assert!(result.is_ok(), "Failed to deserialize {} variant", name); + let (_, remaining) = result.unwrap(); + assert_eq!(remaining.len(), 0); + println!("Successfully deserialized {} variant", name); + } + } +} diff --git a/program-libs/zero-copy-derive/tests/comprehensive_enum_example.rs b/program-libs/zero-copy-derive/tests/comprehensive_enum_example.rs new file mode 100644 index 0000000000..514372bbc8 --- /dev/null +++ b/program-libs/zero-copy-derive/tests/comprehensive_enum_example.rs @@ -0,0 +1,156 @@ +/*! +This file demonstrates the complete enum support for the ZeroCopy derive macro. + +## What gets generated: + +For this enum: +```rust +#[derive(ZeroCopy)] +pub enum Action { + MintTo(MintToAction), + Update, + CreateSplMint, + UpdateMetadata, +} +``` + +The macro generates: +```rust +#[derive(Debug, Clone, PartialEq)] +pub enum ZAction<'a> { + MintTo(ZMintToAction<'a>), // Concrete type for pattern matching + Update, + CreateSplMint, + UpdateMetadata, +} + +impl<'a> Deserialize<'a> for Action { + type Output = ZAction<'a>; + fn zero_copy_at(data: &'a [u8]) -> Result<(Self::Output, &'a [u8]), ZeroCopyError> { + match data[0] { + 0 => { + let (value, bytes) = MintToAction::zero_copy_at(&data[1..])?; + Ok((ZAction::MintTo(value), bytes)) + } + 1 => Ok((ZAction::Update, &data[1..])), + 2 => Ok((ZAction::CreateSplMint, &data[1..])), + 3 => Ok((ZAction::UpdateMetadata, &data[1..])), + _ => Err(ZeroCopyError::InvalidConversion), + } + } +} +``` + +## Usage: + +```rust +for action in parsed_instruction_data.actions.iter() { + match action { + ZAction::MintTo(mint_action) => { + // Access mint_action.amount, mint_action.recipient, etc. + } + ZAction::Update => { + // Handle update + } + ZAction::CreateSplMint => { + // Handle SPL mint creation + } + ZAction::UpdateMetadata => { + // Handle metadata update + } + } +} +``` +*/ + +use light_zero_copy_derive::ZeroCopy; + +#[derive(Debug, Clone, PartialEq, ZeroCopy)] +pub struct MintToAction { + pub amount: u64, + pub recipient: Vec, +} + +#[derive(Debug, Clone, ZeroCopy)] +pub enum Action { + MintTo(MintToAction), + Update, + CreateSplMint, + UpdateMetadata, +} + +#[cfg(test)] +mod tests { + use light_zero_copy::traits::ZeroCopyAt; + + use super::*; + + #[test] + fn test_generated_enum_structure() { + // The macro should generate ZAction<'a> with concrete variants + + // Test unit variants + for (discriminant, expected_name) in [ + (1u8, "Update"), + (2u8, "CreateSplMint"), + (3u8, "UpdateMetadata"), + ] { + let data = [discriminant]; + let (result, remaining) = Action::zero_copy_at(&data).unwrap(); + assert_eq!(remaining.len(), 0); + println!("✓ {}: {:?}", expected_name, result); + } + // Test data variant + let mut data = vec![0u8]; // MintTo discriminant + data.extend_from_slice(&42u64.to_le_bytes()); // amount + data.extend_from_slice(&4u32.to_le_bytes()); // recipient length + data.extend_from_slice(b"test"); // recipient data + let (result, remaining) = Action::zero_copy_at(&data).unwrap(); + assert_eq!(remaining.len(), 0); + println!("✓ MintTo: {:?}", result); + } + #[test] + fn test_pattern_matching_example() { + // This demonstrates the exact usage pattern the user wants + let mut actions_data = Vec::new(); + // Create some test actions + // Action 1: MintTo + actions_data.push({ + let mut data = vec![0u8]; // MintTo discriminant + data.extend_from_slice(&1000u64.to_le_bytes()); + data.extend_from_slice(&5u32.to_le_bytes()); + data.extend_from_slice(b"alice"); + data + }); + + // Action 2: Update + actions_data.push(vec![1u8]); + + // Action 3: CreateSplMint + actions_data.push(vec![2u8]); + + // Process each action (simulating the user's use case) + for (i, action_data) in actions_data.iter().enumerate() { + let (action, _) = Action::zero_copy_at(action_data).unwrap(); + + // This is what the user wants to be able to write: + println!("Processing action {}: {:?}", i, action); + + // In the user's real code, this would be: + // match action { + // ZAction::MintTo(mint_action) => { + // println!("Minting {} tokens to {:?}", mint_action.amount, mint_action.recipient); + // } + // ZAction::Update => { + // println!("Performing update"); + // } + // ZAction::CreateSplMint => { + // println!("Creating SPL mint"); + // } + // ZAction::UpdateMetadata => { + // println!("Updating metadata"); + // } + // } + } + } +} diff --git a/program-libs/zero-copy-derive/tests/cross_crate_copy.rs b/program-libs/zero-copy-derive/tests/cross_crate_copy.rs new file mode 100644 index 0000000000..363fed3140 --- /dev/null +++ b/program-libs/zero-copy-derive/tests/cross_crate_copy.rs @@ -0,0 +1,299 @@ +#![cfg(feature = "mut")] +//! Test cross-crate Copy identification functionality +//! +//! This test validates that the zero-copy derive macro correctly identifies +//! which types implement Copy, both for built-in types and user-defined types. + +use borsh::{BorshDeserialize, BorshSerialize}; +use light_zero_copy_derive::{ZeroCopy, ZeroCopyEq, ZeroCopyMut}; + +// Test struct with primitive Copy types that should be in meta fields +#[repr(C)] +#[derive(Debug, PartialEq, BorshSerialize, BorshDeserialize, ZeroCopy)] +pub struct PrimitiveCopyStruct { + pub a: u8, + pub b: u16, + pub c: u32, + pub d: u64, + pub e: bool, + pub f: Vec, // Split point - this and following fields go to struct_fields + pub g: u32, // Should be in struct_fields due to field ordering rules +} + +// Test struct with primitive Copy types that should be in meta fields +#[repr(C)] +#[derive(Debug, PartialEq, BorshSerialize, BorshDeserialize, ZeroCopy, ZeroCopyEq, ZeroCopyMut)] +pub struct PrimitiveCopyStruct2 { + pub f: Vec, // Split point - this and following fields go to struct_fields + pub a: u8, + pub b: u16, + pub c: u32, + pub d: u64, + pub e: bool, + pub g: u32, +} + +// Test struct with arrays that use u8 (which supports Unaligned) +#[repr(C)] +#[derive(Debug, PartialEq, BorshSerialize, BorshDeserialize, ZeroCopy)] +pub struct ArrayCopyStruct { + pub fixed_u8: [u8; 4], + pub another_u8: [u8; 8], + pub data: Vec, // Split point + pub more_data: [u8; 3], // Should be in struct_fields due to field ordering +} + +// Test struct with Vec of primitive Copy types +#[repr(C)] +#[derive(Debug, PartialEq, BorshSerialize, BorshDeserialize, ZeroCopy)] +pub struct VecPrimitiveStruct { + pub header: u32, + pub data: Vec, // Vec - special case + pub numbers: Vec, // Vec of Copy type + pub footer: u64, +} + +#[cfg(test)] +mod tests { + use light_zero_copy::borsh::Deserialize; + + use super::*; + + #[test] + fn test_primitive_copy_field_splitting() { + // This test validates that primitive Copy types are correctly + // identified and placed in meta_fields until we hit a Vec + + let data = PrimitiveCopyStruct { + a: 1, + b: 2, + c: 3, + d: 4, + e: true, + f: vec![5, 6, 7], + g: 8, + }; + + let serialized = borsh::to_vec(&data).unwrap(); + let (deserialized, _) = PrimitiveCopyStruct::zero_copy_at(&serialized).unwrap(); + + // Verify we can access meta fields (should be zero-copy references) + assert_eq!(deserialized.a, 1); + assert_eq!(deserialized.b.get(), 2); // U16 type, use .get() + assert_eq!(deserialized.c.get(), 3); // U32 type, use .get() + assert_eq!(deserialized.d.get(), 4); // U64 type, use .get() + assert!(deserialized.e()); // bool accessor method + + // Verify we can access struct fields + assert_eq!(deserialized.f, &[5, 6, 7]); + assert_eq!(deserialized.g.get(), 8); // U32 type in struct fields + } + + #[test] + fn test_array_copy_field_splitting() { + // Arrays should be treated as Copy types + let data = ArrayCopyStruct { + fixed_u8: [1, 2, 3, 4], + another_u8: [10, 20, 30, 40, 50, 60, 70, 80], + data: vec![5, 6], + more_data: [30, 40, 50], + }; + + let serialized = borsh::to_vec(&data).unwrap(); + let (deserialized, _) = ArrayCopyStruct::zero_copy_at(&serialized).unwrap(); + + // Arrays should be accessible (in meta_fields before Vec split) + assert_eq!(deserialized.fixed_u8.as_ref(), &[1, 2, 3, 4]); + assert_eq!( + deserialized.another_u8.as_ref(), + &[10, 20, 30, 40, 50, 60, 70, 80] + ); + + // After Vec split + assert_eq!(deserialized.data, &[5, 6]); + assert_eq!(deserialized.more_data.as_ref(), &[30, 40, 50]); + } + + #[test] + fn test_vec_primitive_types() { + // Test Vec with various primitive Copy element types + let data = VecPrimitiveStruct { + header: 1, + data: vec![10, 20, 30], + numbers: vec![100, 200, 300], + footer: 999, + }; + + let serialized = borsh::to_vec(&data).unwrap(); + let (deserialized, _) = VecPrimitiveStruct::zero_copy_at(&serialized).unwrap(); + + assert_eq!(deserialized.header.get(), 1); + + // Vec is special case - stored as slice + assert_eq!(deserialized.data, &[10, 20, 30]); + + // Vec should use ZeroCopySliceBorsh + assert_eq!(deserialized.numbers.len(), 3); + assert_eq!(deserialized.numbers[0].get(), 100); + assert_eq!(deserialized.numbers[1].get(), 200); + assert_eq!(deserialized.numbers[2].get(), 300); + + assert_eq!(deserialized.footer.get(), 999); + } + + #[test] + fn test_all_derives_with_vec_first() { + // This test validates PrimitiveCopyStruct2 which has Vec as the first field + // This means NO meta fields (all fields go to struct_fields due to field ordering) + // Also tests all derive macros: ZeroCopy, ZeroCopyEq, ZeroCopyMut + + use light_zero_copy::{borsh_mut::DeserializeMut, init_mut::ZeroCopyNew}; + + let data = PrimitiveCopyStruct2 { + f: vec![1, 2, 3], // Vec first - causes all fields to be in struct_fields + a: 10, + b: 20, + c: 30, + d: 40, + e: true, + g: 50, + }; + + // Test ZeroCopy (immutable) + let serialized = borsh::to_vec(&data).unwrap(); + let (deserialized, _) = PrimitiveCopyStruct2::zero_copy_at(&serialized).unwrap(); + + // Since Vec is first, ALL fields should be in struct_fields (no meta fields) + assert_eq!(deserialized.f, &[1, 2, 3]); + assert_eq!(deserialized.a, 10); // u8 direct access + assert_eq!(deserialized.b.get(), 20); // U16 via .get() + assert_eq!(deserialized.c.get(), 30); // U32 via .get() + assert_eq!(deserialized.d.get(), 40); // U64 via .get() + assert!(deserialized.e()); // bool accessor method + assert_eq!(deserialized.g.get(), 50); // U32 via .get() + + // Test ZeroCopyEq (PartialEq implementation) + let original = PrimitiveCopyStruct2 { + f: vec![1, 2, 3], + a: 10, + b: 20, + c: 30, + d: 40, + e: true, + g: 50, + }; + + // Should be equal to original + assert_eq!(deserialized, original); + + // Test inequality + let different = PrimitiveCopyStruct2 { + f: vec![1, 2, 3], + a: 11, + b: 20, + c: 30, + d: 40, + e: true, + g: 50, // Different 'a' + }; + assert_ne!(deserialized, different); + + // Test ZeroCopyMut (mutable zero-copy) + #[cfg(feature = "mut")] + { + let mut serialized_mut = borsh::to_vec(&data).unwrap(); + let (deserialized_mut, _) = + PrimitiveCopyStruct2::zero_copy_at_mut(&mut serialized_mut).unwrap(); + + // Test mutable access + assert_eq!(deserialized_mut.f, &[1, 2, 3]); + assert_eq!(*deserialized_mut.a, 10); // Mutable u8 field + assert_eq!(deserialized_mut.b.get(), 20); + let (deserialized_mut, _) = + PrimitiveCopyStruct2::zero_copy_at(&serialized_mut).unwrap(); + + // Test From implementation (ZeroCopyEq generates this for immutable version) + let converted: PrimitiveCopyStruct2 = deserialized_mut.into(); + assert_eq!(converted.a, 10); + assert_eq!(converted.b, 20); + assert_eq!(converted.c, 30); + assert_eq!(converted.d, 40); + assert!(converted.e); + assert_eq!(converted.f, vec![1, 2, 3]); + assert_eq!(converted.g, 50); + } + + // Test ZeroCopyNew (configuration-based initialization) + let config = super::PrimitiveCopyStruct2Config { + f: 3, // Vec length + // Other fields don't need config (they're primitives) + }; + + // Calculate required buffer size + let buffer_size = PrimitiveCopyStruct2::byte_len(&config); + let mut buffer = vec![0u8; buffer_size]; + + // Initialize the zero-copy struct + let (mut initialized, _) = + PrimitiveCopyStruct2::new_zero_copy(&mut buffer, config).unwrap(); + + // Verify we can access the initialized fields + assert_eq!(initialized.f.len(), 3); // Vec should have correct length + + // Set some values in the Vec + initialized.f[0] = 100; + initialized.f[1] = 101; + initialized.f[2] = 102; + *initialized.a = 200; + + // Verify the values were set correctly + assert_eq!(initialized.f, &[100, 101, 102]); + assert_eq!(*initialized.a, 200); + + println!("All derive macros (ZeroCopy, ZeroCopyEq, ZeroCopyMut) work correctly with Vec-first struct!"); + } + + #[test] + fn test_copy_identification_compilation() { + // The primary test is that our macro successfully processes all struct definitions + // above without panicking or generating invalid code. The fact that compilation + // succeeds demonstrates that our Copy identification logic works correctly. + + // Test basic functionality to ensure the generated code is sound + let primitive_data = PrimitiveCopyStruct { + a: 1, + b: 2, + c: 3, + d: 4, + e: true, + f: vec![1, 2], + g: 5, + }; + + let array_data = ArrayCopyStruct { + fixed_u8: [1, 2, 3, 4], + another_u8: [5, 6, 7, 8, 9, 10, 11, 12], + data: vec![13, 14], + more_data: [15, 16, 17], + }; + + let vec_data = VecPrimitiveStruct { + header: 42, + data: vec![1, 2, 3], + numbers: vec![10, 20], + footer: 99, + }; + + // Serialize and deserialize to verify the generated code works + let serialized = borsh::to_vec(&primitive_data).unwrap(); + let (_, _) = PrimitiveCopyStruct::zero_copy_at(&serialized).unwrap(); + + let serialized = borsh::to_vec(&array_data).unwrap(); + let (_, _) = ArrayCopyStruct::zero_copy_at(&serialized).unwrap(); + + let serialized = borsh::to_vec(&vec_data).unwrap(); + let (_, _) = VecPrimitiveStruct::zero_copy_at(&serialized).unwrap(); + + println!("Cross-crate Copy identification test passed - all structs compiled and work correctly!"); + } +} diff --git a/program-libs/zero-copy-derive/tests/enum_test.rs b/program-libs/zero-copy-derive/tests/enum_test.rs new file mode 100644 index 0000000000..5d9cdf36e3 --- /dev/null +++ b/program-libs/zero-copy-derive/tests/enum_test.rs @@ -0,0 +1,100 @@ +use light_zero_copy_derive::ZeroCopy; + +// Test struct that will be used in enum variants +#[derive(Debug, Clone, PartialEq, ZeroCopy)] +pub struct TokenMetadataInstructionData { + pub name: Vec, + pub symbol: Vec, + pub uri: Vec, +} + +// Test enum using the ExtensionInstructionData example from the user +#[derive(Debug, Clone, PartialEq, ZeroCopy)] +pub enum ExtensionInstructionData { + Placeholder0, + Placeholder1, + Placeholder2, + Placeholder3, + Placeholder4, + Placeholder5, + Placeholder6, + Placeholder7, + Placeholder8, + Placeholder9, + Placeholder10, + Placeholder11, + Placeholder12, + Placeholder13, + Placeholder14, + Placeholder15, + Placeholder16, + Placeholder17, + Placeholder18, // MetadataPointer(InitMetadataPointer), + TokenMetadata(TokenMetadataInstructionData), +} + +#[cfg(test)] +mod tests { + use light_zero_copy::traits::ZeroCopyAt; + + use super::*; + + #[test] + fn test_enum_unit_variant_deserialization() { + // Test unit variant (Placeholder0 has discriminant 0) + let data = [0u8]; // discriminant 0 for Placeholder0 + let (result, remaining) = ExtensionInstructionData::zero_copy_at(&data).unwrap(); + match result { + ref variant => { + // For unit variants, we can't easily pattern match without knowing the exact type + // In a real test, you'd check the discriminant or use other means + println!("Got variant: {:?}", variant); + } + } + assert_eq!(remaining.len(), 0); + } + + #[test] + fn test_enum_data_variant_deserialization() { + // Test data variant (TokenMetadata has discriminant 19) + let mut data = vec![19u8]; // discriminant 19 for TokenMetadata + // Add TokenMetadataInstructionData serialized data + // For this test, we'll create simple serialized data for the struct + // name: "test" (4 bytes length + "test") + data.extend_from_slice(&4u32.to_le_bytes()); + data.extend_from_slice(b"test"); + + // symbol: "TST" (3 bytes length + "TST") + data.extend_from_slice(&3u32.to_le_bytes()); + data.extend_from_slice(b"TST"); + + // uri: "http://test.com" (15 bytes length + "http://test.com") + data.extend_from_slice(&15u32.to_le_bytes()); + data.extend_from_slice(b"http://test.com"); + + let (result, remaining) = ExtensionInstructionData::zero_copy_at(&data).unwrap(); + + // For this test, just verify we get a result without panicking + // In practice, you'd have more specific assertions based on your actual types + println!("Got result: {:?}", result); + + assert_eq!(remaining.len(), 0); + } + + #[test] + fn test_enum_invalid_discriminant() { + // Test with invalid discriminant (255) + let data = [255u8]; + let result = ExtensionInstructionData::zero_copy_at(&data); + assert!(result.is_err()); + } + + #[test] + fn test_enum_empty_data() { + // Test with empty data + let data = []; + let result = ExtensionInstructionData::zero_copy_at(&data); + + assert!(result.is_err()); + } +} diff --git a/program-libs/zero-copy-derive/tests/generated_code_demo.rs b/program-libs/zero-copy-derive/tests/generated_code_demo.rs new file mode 100644 index 0000000000..a54006b8ce --- /dev/null +++ b/program-libs/zero-copy-derive/tests/generated_code_demo.rs @@ -0,0 +1,130 @@ +/*! +This test demonstrates what code gets generated by the enum ZeroCopy derive. + +For this input: +```rust +#[derive(ZeroCopy)] +pub enum Action { + MintTo(MintToAction), + Update, +} +``` + +The macro generates: +```rust +// Type alias for pattern matching +pub type MintToType<'a> = >::Output; + +#[derive(Debug, Clone, PartialEq)] +pub enum ZAction<'a> { + MintTo(MintToType<'a>), // Uses the type alias - no import needed! + Update, +} +``` + +This solves both problems: +1. ✅ No import issues - uses qualified Deserialize::Output internally +2. ✅ Pattern matching works - concrete types via type aliases +*/ + +use light_zero_copy_derive::ZeroCopy; + +#[derive(Debug, Clone, PartialEq, ZeroCopy)] +pub struct MintToAction { + pub amount: u64, + pub recipient: Vec, +} + +#[derive(Debug, Clone, ZeroCopy)] +pub enum Action { + MintTo(MintToAction), + Update, + CreateSplMint, +} + +#[cfg(test)] +mod tests { + use light_zero_copy::traits::ZeroCopyAt; + + use super::*; + + #[test] + fn test_generated_type_aliases_work() { + // The macro should generate: + // - pub type MintToType<'a> = >::Output; + // - enum ZAction<'a> { MintTo(MintToType<'a>), Update, CreateSplMint } + // Test that we can deserialize without import issues + let mut data = vec![0u8]; // MintTo discriminant + data.extend_from_slice(&999u64.to_le_bytes()); + data.extend_from_slice(&4u32.to_le_bytes()); + data.extend_from_slice(b"user"); + + let (result, remaining) = Action::zero_copy_at(&data).unwrap(); + assert_eq!(remaining.len(), 0); + + // The key insight: this should work without any imports because + // the type alias MintToType<'a> resolves to the Deserialize::Output internally + println!( + "✅ Successfully deserialized with type aliases: {:?}", + result + ); + } + + #[test] + fn test_pattern_matching_should_work() { + // Test unit variant + let data = [1u8]; // Update discriminant + let (result, _) = Action::zero_copy_at(&data).unwrap(); + + // This demonstrates the usage pattern: + println!("Got action variant: {:?}", result); + + // In the user's code, this should work: + // match result { + // ZAction::MintTo(mint_action) => { + // // mint_action has type MintToType<'_> + // // which is actually ZMintToAction<'_> + // } + // ZAction::Update => { /* handle */ } + // ZAction::CreateSplMint => { /* handle */ } + // } + } +} + +/* +The generated code structure should be: + +```rust +// Generated type aliases +pub type MintToType<'a> = >::Output; + +// Generated enum +#[derive(Debug, Clone, PartialEq)] +pub enum ZAction<'a> { + MintTo(MintToType<'a>), + Update, + CreateSplMint, +} + +// Generated Deserialize impl +impl<'a> light_zero_copy::borsh::Deserialize<'a> for Action { + type Output = ZAction<'a>; + fn zero_copy_at(data: &'a [u8]) -> Result<(Self::Output, &'a [u8]), ZeroCopyError> { + match data[0] { + 0 => { + let (value, bytes) = MintToAction::zero_copy_at(&data[1..])?; + Ok((ZAction::MintTo(value), bytes)) + } + 1 => Ok((ZAction::Update, &data[1..])), + 2 => Ok((ZAction::CreateSplMint, &data[1..])), + _ => Err(ZeroCopyError::InvalidConversion), + } + } +} +``` + +This approach: +- ✅ Avoids import issues (uses qualified syntax in type alias) +- ✅ Enables pattern matching (concrete types via aliases) +- ✅ Maintains type safety (proper Deserialize trait usage) +*/ diff --git a/program-libs/zero-copy-derive/tests/pattern_match_test.rs b/program-libs/zero-copy-derive/tests/pattern_match_test.rs new file mode 100644 index 0000000000..c68ac9b0c5 --- /dev/null +++ b/program-libs/zero-copy-derive/tests/pattern_match_test.rs @@ -0,0 +1,98 @@ +use light_zero_copy_derive::ZeroCopy; + +// Test struct for the MintTo action +#[repr(C)] +#[derive(Debug, Clone, PartialEq, ZeroCopy)] +pub struct MintToAction { + pub amount: u64, + pub recipient: Vec, +} + +// Test enum similar to your Action example +#[repr(C)] +#[derive(Debug, Clone, ZeroCopy)] +pub enum Action { + MintTo(MintToAction), + Update, + CreateSplMint, + UpdateMetadata, +} + +#[cfg(test)] +mod tests { + use light_zero_copy::traits::ZeroCopyAt; + + use super::*; + + #[test] + fn test_pattern_matching_works() { + use light_zero_copy::traits::ZeroCopyAt; + // Test MintTo variant (discriminant 0) + let mut data = vec![0u8]; // discriminant 0 for MintTo + + // Add MintToAction serialized data + // amount: 1000 + data.extend_from_slice(&1000u64.to_le_bytes()); + + // recipient: "alice" (5 bytes length + "alice") + data.extend_from_slice(&5u32.to_le_bytes()); + data.extend_from_slice(b"alice"); + + let (result, _remaining) = Action::zero_copy_at(&data).unwrap(); + // This is the key test - we should be able to pattern match! + // The generated type should be ZAction<'_> with variants like ZAction::MintTo(ZMintToAction<'_>) + match result { + // This pattern should work with the concrete Z-types + action_variant => { + // We can't easily test the exact pattern match without importing the generated type + // but we can verify the structure exists and is Debug printable + println!("Pattern match successful: {:?}", action_variant); + // In real usage, this would be: + // ZAction::MintTo(mint_action) => { + // // use mint_action.amount, mint_action.recipient, etc. + // } + // ZAction::Update => { /* handle update */ } + // etc. + } + } + } + + #[test] + fn test_unit_variant_pattern_matching() { + use light_zero_copy::traits::ZeroCopyAt; + // Test Update variant (discriminant 1) + let data = [1u8]; + let (result, _remaining) = Action::zero_copy_at(&data).unwrap(); + + // This should also support pattern matching + match result { + action_variant => { + println!( + "Unit variant pattern match successful: {:?}", + action_variant + ); + // In real usage: ZAction::Update => { /* handle */ } + } + } + } +} + +// This shows what the user's code should look like: +// +// for action in parsed_instruction_data.actions.iter() { +// match action { +// ZAction::MintTo(mint_action) => { +// // Access mint_action.amount, mint_action.recipient, etc. +// println!("Minting {} tokens to {:?}", mint_action.amount, mint_action.recipient); +// } +// ZAction::Update => { +// println!("Performing update"); +// } +// ZAction::CreateSplMint => { +// println!("Creating SPL mint"); +// } +// ZAction::UpdateMetadata => { +// println!("Updating metadata"); +// } +// } +// } diff --git a/program-libs/zero-copy-derive/tests/ui/pass/02_single_u8_field.rs b/program-libs/zero-copy-derive/tests/ui/pass/02_single_u8_field.rs index c9cb260c06..8597614026 100644 --- a/program-libs/zero-copy-derive/tests/ui/pass/02_single_u8_field.rs +++ b/program-libs/zero-copy-derive/tests/ui/pass/02_single_u8_field.rs @@ -15,13 +15,13 @@ fn main() { let ref_struct = SingleU8 { value: 42 }; let bytes = ref_struct.try_to_vec().unwrap(); - let (struct_copy, remaining) = SingleU8::zero_copy_at(&bytes).unwrap(); + let (struct_copy, _remaining) = SingleU8::zero_copy_at(&bytes).unwrap(); assert_eq!(struct_copy, ref_struct); - assert!(remaining.is_empty()); + assert!(_remaining.is_empty()); let mut bytes_mut = bytes.clone(); - let (_struct_copy_mut, remaining) = SingleU8::zero_copy_at_mut(&mut bytes_mut).unwrap(); - assert!(remaining.is_empty()); + let (_struct_copy_mut, _remaining) = SingleU8::zero_copy_at_mut(&mut bytes_mut).unwrap(); + assert!(_remaining.is_empty()); // assert byte len let config = (); let byte_len = SingleU8::byte_len(&config).unwrap(); diff --git a/program-libs/zero-copy/src/errors.rs b/program-libs/zero-copy/src/errors.rs index e0de888992..9614358936 100644 --- a/program-libs/zero-copy/src/errors.rs +++ b/program-libs/zero-copy/src/errors.rs @@ -20,6 +20,8 @@ pub enum ZeroCopyError { InvalidEnumValue, InsufficientCapacity, PlatformSizeOverflow, + // #[error("InvalidEnumValue")] + // InvalidEnumValue, } impl fmt::Display for ZeroCopyError { diff --git a/program-tests/compressed-token-test/tests/account.rs b/program-tests/compressed-token-test/tests/account.rs index cd124fd074..d86556b69c 100644 --- a/program-tests/compressed-token-test/tests/account.rs +++ b/program-tests/compressed-token-test/tests/account.rs @@ -1,9 +1,13 @@ // #![cfg(feature = "test-sbf")] use anchor_spl::token_2022::spl_token_2022; -use light_compressed_token_sdk::instructions::{ - close::close_account, create_associated_token_account::derive_ctoken_ata, - create_associated_token_account_idempotent, create_token_account, +use light_compressed_token_sdk::{ + compressible::{initialize_compressible_token_account, InitializeCompressibleTokenAccount}, + instructions::{ + close::close_account, create_associated_token_account::derive_ctoken_ata, + create_associated_token_account_idempotent, create_token_account, + }, + SPL_TOKEN_PROGRAM_ID, }; use light_ctoken_types::COMPRESSIBLE_TOKEN_ACCOUNT_SIZE; use light_program_test::{program_test::TestRpc, LightProgramTest, ProgramTestConfig}; @@ -203,16 +207,14 @@ async fn test_compressible_account_with_rent_authority_lifecycle() -> Result<(), // Initialize compressible token account let create_token_account_ix = - light_compressed_token_sdk::instructions::create_compressible_token_account( - light_compressed_token_sdk::instructions::CreateCompressibleTokenAccount { - account_pubkey: token_account_pubkey, - mint_pubkey: context.mint_pubkey, - owner_pubkey: context.owner_keypair.pubkey(), - rent_authority: rent_authority_pubkey, - rent_recipient: rent_recipient_pubkey, - slots_until_compression: 0, - }, - ) + initialize_compressible_token_account(InitializeCompressibleTokenAccount { + account_pubkey: token_account_pubkey, + mint_pubkey: context.mint_pubkey, + owner_pubkey: context.owner_keypair.pubkey(), + rent_authority: rent_authority_pubkey, + rent_recipient: rent_recipient_pubkey, + slots_until_compression: 0, + }) .map_err(|e| { RpcError::AssertRpcError(format!( "Failed to create compressible token account instruction: {}", @@ -512,16 +514,14 @@ async fn test_compress_and_close_with_rent_authority() -> Result<(), RpcError> { ); let create_token_account_ix = - light_compressed_token_sdk::instructions::create_compressible_token_account( - light_compressed_token_sdk::instructions::CreateCompressibleTokenAccount { - account_pubkey: token_account_pubkey, - mint_pubkey, - owner_pubkey: context.owner_keypair.pubkey(), - rent_authority: rent_authority_keypair.pubkey(), - rent_recipient: rent_recipient_pubkey, - slots_until_compression: 0, - }, - ) + initialize_compressible_token_account(InitializeCompressibleTokenAccount { + account_pubkey: token_account_pubkey, + mint_pubkey, + owner_pubkey: context.owner_keypair.pubkey(), + rent_authority: rent_authority_keypair.pubkey(), + rent_recipient: rent_recipient_pubkey, + slots_until_compression: 0, + }) .map_err(|e| RpcError::AssertRpcError(format!("Failed to create instruction: {}", e)))?; context @@ -790,6 +790,7 @@ async fn test_spl_to_ctoken_transfer() -> Result<(), RpcError> { &recipient, mint, &payer, + SPL_TOKEN_PROGRAM_ID.into(), ) .await?; diff --git a/program-tests/package.json b/program-tests/package.json index cfb09042fb..71a9760235 100644 --- a/program-tests/package.json +++ b/program-tests/package.json @@ -4,7 +4,17 @@ "license": "Apache-2.0", "description": "Test programs for Light Protocol uses test-sbf to build because build-sbf -- -p creates an infinite loop.", "scripts": { - "build": "cargo test-sbf -p create-address-test-program" + "build": "cargo test-sbf -p create-address-test-program", + "test": "RUSTFLAGS=\"-D warnings\" && pnpm test-account-compression && pnpm test-system && pnpm test-registry && pnpm test-compressed-token && pnpm test-system-cpi && pnpm test-system-cpi-v2 && pnpm test-e2e && pnpm test-sdk-anchor && pnpm test-sdk-pinocchio", + "test-account-compression": "cargo test-sbf -p account-compression-test", + "test-system": "cargo test-sbf -p system-test", + "test-registry": "cargo test-sbf -p registry-test", + "test-compressed-token": "cargo test-sbf -p compressed-token-test", + "test-system-cpi": "cargo test-sbf -p system-cpi-test", + "test-system-cpi-v2": "cargo test-sbf -p system-cpi-v2-test", + "test-e2e": "cargo test-sbf -p e2e-test", + "test-sdk-anchor": "cargo test-sbf -p sdk-anchor-test", + "test-sdk-pinocchio": "cargo test-sbf -p sdk-pinocchio-test" }, "nx": { "targets": { diff --git a/program-tests/sdk-anchor-test/package.json b/program-tests/sdk-anchor-test/package.json deleted file mode 100644 index f6ef6ebfb9..0000000000 --- a/program-tests/sdk-anchor-test/package.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "scripts": { - "test": "cargo test-sbf -p sdk-native-test" - }, - "dependencies": { - "@coral-xyz/anchor": "^0.29.0" - }, - "devDependencies": { - "@lightprotocol/zk-compression-cli": "workspace:*", - "chai": "^5.2.1", - "mocha": "^11.7.1", - "ts-mocha": "^11.1.0", - "@types/bn.js": "^5.2.0", - "@types/chai": "^5.2.2", - "@types/mocha": "^10.0.10", - "typescript": "^5.9.2", - "prettier": "^3.6.2" - } -} diff --git a/program-tests/system-cpi-test/tests/test_program_owned_trees.rs b/program-tests/system-cpi-test/tests/test_program_owned_trees.rs index 1fdf5636d0..1c61376140 100644 --- a/program-tests/system-cpi-test/tests/test_program_owned_trees.rs +++ b/program-tests/system-cpi-test/tests/test_program_owned_trees.rs @@ -126,7 +126,7 @@ async fn test_program_owned_merkle_tree() { assert_ne!(post_merkle_tree.root(), pre_merkle_tree.root()); assert_eq!( post_merkle_tree.root(), - test_indexer.state_merkle_trees[2].merkle_tree.root() + test_indexer.state_merkle_trees[3].merkle_tree.root() ); let invalid_program_owned_merkle_tree_keypair = Keypair::new(); diff --git a/program-tests/utils/src/test_keypairs.rs b/program-tests/utils/src/test_keypairs.rs index 27312ac4d8..14ad5df98b 100644 --- a/program-tests/utils/src/test_keypairs.rs +++ b/program-tests/utils/src/test_keypairs.rs @@ -64,10 +64,15 @@ pub fn from_target_folder() -> TestKeypairs { nullifier_queue_2: Keypair::new(), cpi_context_2: Keypair::new(), group_pda_seed: Keypair::new(), + batched_state_merkle_tree_2: Keypair::from_bytes(&BATCHED_STATE_MERKLE_TREE_TEST_KEYPAIR_2) + .unwrap(), + batched_output_queue_2: Keypair::from_bytes(&BATCHED_OUTPUT_QUEUE_TEST_KEYPAIR_2).unwrap(), + batched_cpi_context_2: Keypair::from_bytes(&BATCHED_CPI_CONTEXT_TEST_KEYPAIR_2).unwrap(), } } pub fn for_regenerate_accounts() -> TestKeypairs { + // Note: this requries your machine to have the light-keypairs dir with the correct keypairs. let prefix = String::from("../../../light-keypairs/"); let state_merkle_tree = read_keypair_file(format!( "{}smt1NamzXdq4AMqS2fS2F1i5KTYPZRhoHgWx38d8WsT.json", @@ -144,5 +149,9 @@ pub fn for_regenerate_accounts() -> TestKeypairs { nullifier_queue_2, cpi_context_2, group_pda_seed: Keypair::new(), + batched_state_merkle_tree_2: Keypair::from_bytes(&BATCHED_STATE_MERKLE_TREE_TEST_KEYPAIR_2) + .unwrap(), + batched_output_queue_2: Keypair::from_bytes(&BATCHED_OUTPUT_QUEUE_TEST_KEYPAIR_2).unwrap(), + batched_cpi_context_2: Keypair::from_bytes(&BATCHED_CPI_CONTEXT_TEST_KEYPAIR_2).unwrap(), } } diff --git a/programs/account-compression/src/processor/insert_addresses.rs b/programs/account-compression/src/processor/insert_addresses.rs index 3b98e20f9b..9fbdea8eb9 100644 --- a/programs/account-compression/src/processor/insert_addresses.rs +++ b/programs/account-compression/src/processor/insert_addresses.rs @@ -40,6 +40,7 @@ pub fn insert_addresses( for &(tree_index, queue_index) in &visited { let queue_account = &mut accounts[queue_index as usize]; + // msg!(&format!("queue_index: {:?}", queue_index)); match queue_account { AcpAccount::BatchedAddressTree(address_tree) => { inserted_addresses += diff --git a/programs/compressed-token/program/src/constants.rs b/programs/compressed-token/program/src/constants.rs index ed4ebf4714..d2e656e115 100644 --- a/programs/compressed-token/program/src/constants.rs +++ b/programs/compressed-token/program/src/constants.rs @@ -6,4 +6,4 @@ pub const BUMP_CPI_AUTHORITY: u8 = 254; // SPL token pool constants pub const POOL_SEED: &[u8] = b"pool"; -pub const NUM_MAX_POOL_ACCOUNTS: u8 = 5; \ No newline at end of file +pub const NUM_MAX_POOL_ACCOUNTS: u8 = 5; diff --git a/programs/compressed-token/program/src/extensions/metadata_pointer.rs b/programs/compressed-token/program/src/extensions/metadata_pointer.rs new file mode 100644 index 0000000000..18ae1c2c6a --- /dev/null +++ b/programs/compressed-token/program/src/extensions/metadata_pointer.rs @@ -0,0 +1,63 @@ +use anchor_lang::prelude::ProgramError; +use light_compressed_account::instruction_data::data::ZOutputCompressedAccountWithPackedContextMut; +use light_ctoken_types::instructions::extensions::metadata_pointer::{ + MetadataPointer, MetadataPointerConfig, ZInitMetadataPointer, +}; +use light_hasher::DataHasher; +use light_zero_copy::ZeroCopyNew; + +pub fn create_output_metadata_pointer<'a>( + metadata_pointer_data: &ZInitMetadataPointer<'a>, + output_compressed_account: &mut ZOutputCompressedAccountWithPackedContextMut<'a>, + start_offset: usize, +) -> Result<([u8; 32], usize), ProgramError> { + if metadata_pointer_data.authority.is_none() && metadata_pointer_data.metadata_address.is_none() + { + return Err(anchor_lang::prelude::ProgramError::InvalidInstructionData); + } + + let cpi_data = output_compressed_account + .compressed_account + .data + .as_mut() + .ok_or(ProgramError::InvalidInstructionData)?; + + let config = MetadataPointerConfig { + authority: (metadata_pointer_data.authority.is_some(), ()), + metadata_address: (metadata_pointer_data.metadata_address.is_some(), ()), + }; + let byte_len = MetadataPointer::byte_len(&config); + let end_offset = start_offset + byte_len; + + println!("MetadataPointer::new_zero_copy - start_offset: {}, end_offset: {}, total_data_len: {}, slice_len: {}", + start_offset, end_offset, cpi_data.data.len(), end_offset - start_offset); + println!( + "Data slice at offset: {:?}", + &cpi_data.data[start_offset..std::cmp::min(start_offset + 32, cpi_data.data.len())] + ); + let (metadata_pointer, _) = + MetadataPointer::new_zero_copy(&mut cpi_data.data[start_offset..end_offset], config)?; + if let Some(mut authority) = metadata_pointer.authority { + *authority = *metadata_pointer_data + .authority + .ok_or(ProgramError::InvalidInstructionData)?; + } + if let Some(mut metadata_address) = metadata_pointer.metadata_address { + *metadata_address = *metadata_pointer_data + .metadata_address + .ok_or(ProgramError::InvalidInstructionData)?; + } + + // Create the actual MetadataPointer struct for hashing + let metadata_pointer_for_hash = MetadataPointer { + authority: metadata_pointer_data.authority.map(|a| *a), + metadata_address: metadata_pointer_data.metadata_address.map(|a| *a), + }; + + let hash = metadata_pointer_for_hash + .hash::() + .map_err(|_| ProgramError::InvalidAccountData)?; + + Ok((hash, end_offset)) +} +// TODO: add update diff --git a/programs/compressed-token/program/src/extensions/token_metadata_ui.rs b/programs/compressed-token/program/src/extensions/token_metadata_ui.rs new file mode 100644 index 0000000000..51e717d3c7 --- /dev/null +++ b/programs/compressed-token/program/src/extensions/token_metadata_ui.rs @@ -0,0 +1,41 @@ +use borsh::{BorshDeserialize, BorshSerialize}; +use light_sdk::LightHasher; +use solana_pubkey::Pubkey; + +// TODO: add borsh compat test TokenMetadataUi TokenMetadata +/// Ui Token metadata with Strings instead of bytes. +#[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)] +pub struct TokenMetadataUi { + // TODO: decide whether to move down for more efficient zero copy. Or impl manual zero copy. + /// The authority that can sign to update the metadata + pub update_authority: Option, + // TODO: decide whether to keep this. + /// The associated mint, used to counter spoofing to be sure that metadata + /// belongs to a particular mint + pub mint: Pubkey, + pub metadata: MetadataUi, + /// Any additional metadata about the token as key-value pairs. The program + /// must avoid storing the same key twice. + pub additional_metadata: Vec, + // TODO: decide whether to do this on this or MintAccount level + /// 0: Poseidon, 1: Sha256, 2: Keccak256, 3: Sha256Flat + pub version: u8, +} + +#[derive(Debug, LightHasher, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)] +pub struct MetadataUi { + /// The longer name of the token + pub name: String, + /// The shortened symbol for the token + pub symbol: String, + /// The URI pointing to richer metadata + pub uri: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)] +pub struct AdditionalMetadataUi { + /// The key of the metadata + pub key: String, + /// The value of the metadata + pub value: String, +} diff --git a/programs/compressed-token/program/src/mint_action/create_mint.rs b/programs/compressed-token/program/src/mint_action/create_mint.rs new file mode 100644 index 0000000000..e897027fbe --- /dev/null +++ b/programs/compressed-token/program/src/mint_action/create_mint.rs @@ -0,0 +1,83 @@ +use anchor_compressed_token::ErrorCode; +use anchor_lang::prelude::ProgramError; +use light_compressed_account::{ + instruction_data::with_readonly::ZInstructionDataInvokeCpiWithReadOnlyMut, Pubkey, +}; +use light_ctoken_types::{ + instructions::mint_actions::ZMintActionCompressedInstructionData, CTokenError, + COMPRESSED_MINT_SEED, +}; +use spl_pod::solana_msg::msg; + +use crate::mint_action::accounts::MintActionAccounts; + +// TODO: unit test. +/// Processes the create mint action by validating parameters and setting up the new address. +/// Note, the compressed output account creation is unified with other actions in a different function. +pub fn process_create_mint_action( + parsed_instruction_data: &ZMintActionCompressedInstructionData<'_>, + validated_accounts: &MintActionAccounts, + cpi_instruction_struct: &mut ZInstructionDataInvokeCpiWithReadOnlyMut<'_>, + address_merkle_tree_account_index: u8, +) -> Result<(), ProgramError> { + // 1. Create spl mint PDA using provided bump + // - The compressed address is derived from the spl_mint_pda. + // - The spl mint pda is used as mint in compressed token accounts. + // Note: we cant use pinocchio_pubkey::derive_address because don't use the mint_pda in this ix. + // The pda would be unvalidated and an invalid bump could be used. + let mint_signer = validated_accounts + .mint_signer + .ok_or(CTokenError::ExpectedMintSignerAccount) + .map_err(|_| ErrorCode::MintActionMissingExecutingAccounts)?; + let spl_mint_pda: Pubkey = solana_pubkey::Pubkey::create_program_address( + &[ + COMPRESSED_MINT_SEED, + mint_signer.key().as_slice(), + &[parsed_instruction_data.mint_bump], + ], + &crate::ID, + )? + .into(); + + if spl_mint_pda.to_bytes() != parsed_instruction_data.mint.spl_mint.to_bytes() { + msg!("Invalid mint PDA derivation"); + return Err(ErrorCode::MintActionInvalidMintPda.into()); + } + // 2. Create NewAddressParams + cpi_instruction_struct.new_address_params[0].set( + spl_mint_pda.to_bytes(), + parsed_instruction_data.root_index, + Some( + parsed_instruction_data + .cpi_context + .as_ref() + .map(|ctx| ctx.assigned_account_index) + .unwrap_or_default(), + ), + address_merkle_tree_account_index, + ); + // Validate mint parameters + if u64::from(parsed_instruction_data.mint.supply) != 0 { + msg!("Initial supply must be 0 for new mint creation"); + return Err(ErrorCode::MintActionInvalidInitialSupply.into()); + } + + // Validate version is supported + if parsed_instruction_data.mint.version > 1 { + msg!("Unsupported mint version"); + return Err(ErrorCode::MintActionUnsupportedVersion.into()); + } + + // Validate is_decompressed is false for new mint creation + if parsed_instruction_data.mint.is_decompressed() { + msg!("New mint must start as compressed (is_decompressed=false)"); + return Err(ErrorCode::MintActionInvalidCompressionState.into()); + } + // Unchecked mint instruction data + // 1. decimals + // 2. mint authority + // 3. freeze_authority + // 4. extensions are checked when created. + + Ok(()) +} diff --git a/programs/compressed-token/program/src/mint_action/create_spl_mint/create_mint_account.rs b/programs/compressed-token/program/src/mint_action/create_spl_mint/create_mint_account.rs new file mode 100644 index 0000000000..cb06972fa3 --- /dev/null +++ b/programs/compressed-token/program/src/mint_action/create_spl_mint/create_mint_account.rs @@ -0,0 +1,85 @@ +use anchor_lang::solana_program::program_error::ProgramError; +use light_ctoken_types::COMPRESSED_MINT_SEED; + +use crate::LIGHT_CPI_SIGNER; + +/// Creates the mint account manually as a PDA derived from our program but owned by the token program +pub fn create_mint_account( + executing_accounts: &crate::mint_action::accounts::ExecutingAccounts<'_>, + program_id: &pinocchio::pubkey::Pubkey, + mint_bump: u8, + mint_signer: &pinocchio::account_info::AccountInfo, +) -> Result<(), ProgramError> { + let mint_account_size = light_ctoken_types::MINT_ACCOUNT_SIZE as usize; + let mint_account = executing_accounts + .mint + .ok_or(ProgramError::InvalidAccountData)?; + let token_program = executing_accounts + .token_program + .ok_or(ProgramError::InvalidAccountData)?; + + // Verify the provided mint account matches the expected PDA + let seeds = &[COMPRESSED_MINT_SEED, mint_signer.key().as_ref()]; + crate::shared::verify_pda(mint_account.key(), seeds, mint_bump, program_id)?; + + // Create account using shared function + let config = crate::shared::CreatePdaAccountConfig { + seeds, + bump: mint_bump, + account_size: mint_account_size, + owner_program_id: token_program.key(), // Owned by token program + derivation_program_id: program_id, + }; + + crate::shared::create_pda_account( + executing_accounts.system.fee_payer, + mint_account, + executing_accounts.system.system_program, + config, + ) +} + +/// Initializes the mint account using Token-2022's initialize_mint2 instruction +pub fn initialize_mint_account_for_action( + executing_accounts: &crate::mint_action::accounts::ExecutingAccounts<'_>, + mint_data: &light_ctoken_types::instructions::create_compressed_mint::ZCompressedMintInstructionData<'_>, +) -> Result<(), ProgramError> { + let mint_account = executing_accounts + .mint + .ok_or(ProgramError::InvalidAccountData)?; + let token_program = executing_accounts + .token_program + .ok_or(ProgramError::InvalidAccountData)?; + + let spl_ix = spl_token_2022::instruction::initialize_mint2( + &solana_pubkey::Pubkey::new_from_array(*token_program.key()), + &solana_pubkey::Pubkey::new_from_array(*mint_account.key()), + // cpi_signer is spl mint authority for compressed mints. + // So that the program can ensure cmint and spl mint supply is consistent. + &solana_pubkey::Pubkey::new_from_array(LIGHT_CPI_SIGNER.cpi_signer), + // Control that the token pool cannot be frozen. + Some(&solana_pubkey::Pubkey::new_from_array( + LIGHT_CPI_SIGNER.cpi_signer, + )), + mint_data.decimals, + )?; + + let initialize_mint_ix = pinocchio::instruction::Instruction { + program_id: token_program.key(), + accounts: &[pinocchio::instruction::AccountMeta::new( + mint_account.key(), + true, + false, + )], + data: &spl_ix.data, + }; + + match pinocchio::program::invoke(&initialize_mint_ix, &[mint_account]) { + Ok(()) => {} + Err(e) => { + return Err(ProgramError::Custom(u64::from(e) as u32)); + } + } + + Ok(()) +} diff --git a/programs/compressed-token/program/src/mint_action/create_spl_mint/create_token_pool.rs b/programs/compressed-token/program/src/mint_action/create_spl_mint/create_token_pool.rs new file mode 100644 index 0000000000..12a4889eb0 --- /dev/null +++ b/programs/compressed-token/program/src/mint_action/create_spl_mint/create_token_pool.rs @@ -0,0 +1,93 @@ +use anchor_lang::solana_program::program_error::ProgramError; +use pinocchio::instruction::AccountMeta; + +use crate::constants::POOL_SEED; + +/// Creates the token pool account manually as a PDA derived from our program but owned by the token program +pub fn create_token_pool_account_manual( + executing_accounts: &crate::mint_action::accounts::ExecutingAccounts<'_>, + program_id: &pinocchio::pubkey::Pubkey, +) -> Result<(), ProgramError> { + let token_account_size = light_ctoken_types::BASE_TOKEN_ACCOUNT_SIZE as usize; + + // Get required accounts + let mint_account = executing_accounts + .mint + .ok_or(ProgramError::InvalidAccountData)?; + let token_pool_pda = executing_accounts + .token_pool_pda + .ok_or(ProgramError::InvalidAccountData)?; + let token_program = executing_accounts + .token_program + .ok_or(ProgramError::InvalidAccountData)?; + + // Find the bump for verification + let mint_key = mint_account.key(); + let program_id_pubkey = solana_pubkey::Pubkey::new_from_array(*program_id); + let (expected_token_pool, bump) = solana_pubkey::Pubkey::find_program_address( + &[POOL_SEED, mint_key.as_ref()], + &program_id_pubkey, + ); + + // Verify the provided token pool account matches the expected PDA + if token_pool_pda.key() != &expected_token_pool.to_bytes() { + return Err(ProgramError::InvalidAccountData); + } + + // Create account using shared function + let seeds = &[POOL_SEED, mint_key.as_ref()]; + let config = crate::shared::CreatePdaAccountConfig { + seeds, + bump, + account_size: token_account_size, + owner_program_id: token_program.key(), // Owned by token program + derivation_program_id: program_id, + }; + + crate::shared::create_pda_account( + executing_accounts.system.fee_payer, + token_pool_pda, + executing_accounts.system.system_program, + config, + ) +} + +/// Initializes the token pool account (assumes account already exists) +pub fn initialize_token_pool_account_for_action( + executing_accounts: &crate::mint_action::accounts::ExecutingAccounts<'_>, +) -> Result<(), ProgramError> { + let mint_account = executing_accounts + .mint + .ok_or(ProgramError::InvalidAccountData)?; + let token_pool_pda = executing_accounts + .token_pool_pda + .ok_or(ProgramError::InvalidAccountData)?; + let token_program = executing_accounts + .token_program + .ok_or(ProgramError::InvalidAccountData)?; + + let initialize_account_ix = pinocchio::instruction::Instruction { + program_id: token_program.key(), + accounts: &[ + AccountMeta::new(token_pool_pda.key(), true, false), // writable=true for initialization + AccountMeta::readonly(mint_account.key()), + ], + data: &spl_token_2022::instruction::initialize_account3( + &solana_pubkey::Pubkey::new_from_array(*token_program.key()), + &solana_pubkey::Pubkey::new_from_array(*token_pool_pda.key()), + &solana_pubkey::Pubkey::new_from_array(*mint_account.key()), + &solana_pubkey::Pubkey::new_from_array( + *executing_accounts.system.cpi_authority_pda.key(), + ), + )? + .data, + }; + + match pinocchio::program::invoke(&initialize_account_ix, &[token_pool_pda, mint_account]) { + Ok(()) => {} + Err(e) => { + return Err(ProgramError::Custom(u64::from(e) as u32)); + } + } + Ok(()) +} diff --git a/programs/compressed-token/program/src/mint_action/create_spl_mint/mod.rs b/programs/compressed-token/program/src/mint_action/create_spl_mint/mod.rs new file mode 100644 index 0000000000..572475feb6 --- /dev/null +++ b/programs/compressed-token/program/src/mint_action/create_spl_mint/mod.rs @@ -0,0 +1,7 @@ +mod create_mint_account; +mod create_token_pool; +mod process; + +pub use create_mint_account::*; +pub use create_token_pool::*; +pub use process::*; diff --git a/programs/compressed-token/program/src/mint_action/create_spl_mint/process.rs b/programs/compressed-token/program/src/mint_action/create_spl_mint/process.rs new file mode 100644 index 0000000000..fb20a7918b --- /dev/null +++ b/programs/compressed-token/program/src/mint_action/create_spl_mint/process.rs @@ -0,0 +1,78 @@ +use anchor_compressed_token::ErrorCode; +use anchor_lang::solana_program::program_error::ProgramError; +use light_ctoken_types::CTokenError; + +use super::{ + create_mint_account, create_token_pool_account_manual, initialize_mint_account_for_action, + initialize_token_pool_account_for_action, +}; +use crate::mint_action::accounts::MintActionAccounts; + +/// Helper function for processing CreateSplMint action +pub fn process_create_spl_mint_action( + create_spl_action: &light_ctoken_types::instructions::mint_actions::ZCreateSplMintAction<'_>, + validated_accounts: &MintActionAccounts, + mint_data: &light_ctoken_types::instructions::create_compressed_mint::ZCompressedMintInstructionData<'_>, +) -> Result<(), ProgramError> { + let executing_accounts = validated_accounts + .executing + .as_ref() + .ok_or(ErrorCode::MintActionMissingExecutingAccounts)?; + + // Check mint authority if it exists + if let Some(ix_data_mint_authority) = mint_data.mint_authority { + if *validated_accounts.authority.key() != ix_data_mint_authority.to_bytes() { + return Err(ErrorCode::MintActionInvalidMintAuthority.into()); + } + } + + // Verify mint PDA matches the spl_mint field in compressed mint inputs + let expected_mint: [u8; 32] = mint_data.spl_mint.to_bytes(); + if executing_accounts + .mint + .ok_or(ErrorCode::MintActionMissingMintAccount)? + .key() + != &expected_mint + { + return Err(ErrorCode::MintActionInvalidMintPda.into()); + } + + // 1. Create the mint account manually (PDA derived from our program, owned by token program) + let mint_signer = validated_accounts + .mint_signer + .ok_or(CTokenError::ExpectedMintSignerAccount)?; + create_mint_account( + executing_accounts, + &crate::LIGHT_CPI_SIGNER.program_id, + create_spl_action.mint_bump, + mint_signer, + )?; + + // 2. Initialize the mint account using Token-2022's initialize_mint2 instruction + initialize_mint_account_for_action(executing_accounts, mint_data)?; + + // 3. Create the token pool account manually (PDA derived from our program, owned by token program) + create_token_pool_account_manual(executing_accounts, &crate::LIGHT_CPI_SIGNER.program_id)?; + + // 4. Initialize the token pool account + initialize_token_pool_account_for_action(executing_accounts)?; + + // 5. Mint the existing supply to the token pool if there's any supply + if mint_data.supply > 0 { + crate::shared::mint_to_token_pool( + executing_accounts + .mint + .ok_or(ErrorCode::MintActionMissingMintAccount)?, + executing_accounts + .token_pool_pda + .ok_or(ErrorCode::MintActionMissingTokenPoolAccount)?, + executing_accounts + .token_program + .ok_or(ErrorCode::MintActionMissingTokenProgram)?, + executing_accounts.system.cpi_authority_pda, + mint_data.supply.into(), + )?; + } + + Ok(()) +} diff --git a/programs/compressed-token/program/src/mint_action/mint_to.rs b/programs/compressed-token/program/src/mint_action/mint_to.rs new file mode 100644 index 0000000000..f897d86fce --- /dev/null +++ b/programs/compressed-token/program/src/mint_action/mint_to.rs @@ -0,0 +1,151 @@ +use anchor_compressed_token::ErrorCode; +use anchor_lang::solana_program::program_error::ProgramError; +use light_compressed_account::Pubkey; +use light_ctoken_types::{ + hash_cache::HashCache, instructions::mint_to_compressed::ZMintToAction, + state::ZCompressedMintMut, +}; +use light_sdk_pinocchio::ZOutputCompressedAccountWithPackedContextMut; + +use crate::{ + mint_action::accounts::{AccountsConfig, MintActionAccounts}, + shared::{mint_to_token_pool, token_output::set_output_compressed_account}, +}; + +#[inline(always)] +pub fn mint_authority_check( + compressed_mint: &ZCompressedMintMut<'_>, + validated_accounts: &MintActionAccounts, + instruction_fallback: Option, +) -> Result<(), ErrorCode> { + // Get current authority (from field or instruction fallback) + let mint_authority = compressed_mint + .mint_authority + .as_ref() + .map(|a| **a) + .or(instruction_fallback) + .ok_or(ErrorCode::InvalidAuthorityMint)?; + + if *validated_accounts.authority.key() != mint_authority.to_bytes() { + use anchor_lang::prelude::msg; + msg!( + "authority.key() {:?} != mint {:?}", + solana_pubkey::Pubkey::new_from_array(*validated_accounts.authority.key()), + solana_pubkey::Pubkey::new_from_array(mint_authority.to_bytes()) + ); + Err(ErrorCode::InvalidAuthorityMint) + } else { + Ok(()) + } +} + +/// Processes a mint-to action by validating authority, calculating amounts, and creating compressed token accounts. +/// +/// ## Process Steps +/// 1. **Authority Validation**: Verify signer matches current mint authority from compressed mint state +/// 2. **Amount Calculation**: Sum recipient amounts with overflow protection +/// 3. **Lamports Calculation**: Calculate total lamports for compressed accounts (if specified) +/// 4. **Supply Update**: Calculate new total supply with overflow protection +/// 5. **SPL Mint Synchronization**: For decompressed mints, validate accounts and mint equivalent tokens to token pool via CPI +/// 6. **Compressed Account Creation**: Create new compressed token account for each recipient +/// +/// ## Decompressed Mint Handling +/// Decompressed mint means that an spl mint exists for this compressed mint. +/// When `accounts_config.is_decompressed` is true, the function maintains consistency between the compressed +/// token supply and the underlying SPL mint supply by minting equivalent tokens to a program-controlled +/// token pool account via CPI to SPL Token 2022. +#[allow(clippy::too_many_arguments)] +pub fn process_mint_to_action( + action: &ZMintToAction, + compressed_mint: &ZCompressedMintMut<'_>, + validated_accounts: &MintActionAccounts, + accounts_config: &AccountsConfig, + cpi_instruction_struct: &mut [ZOutputCompressedAccountWithPackedContextMut<'_>], + hash_cache: &mut HashCache, + mint: Pubkey, + out_token_queue_index: u8, + instruction_mint_authority: Option, +) -> Result { + mint_authority_check( + compressed_mint, + validated_accounts, + instruction_mint_authority, + )?; + + let mut sum_amounts: u64 = 0; + for recipient in &action.recipients { + sum_amounts = sum_amounts + .checked_add(u64::from(recipient.amount)) + .ok_or(ErrorCode::MintActionAmountTooLarge)?; + } + + let updated_supply = sum_amounts + .checked_add(compressed_mint.supply.into()) + .ok_or(ErrorCode::MintActionAmountTooLarge)?; + + if let Some(system_accounts) = validated_accounts.executing.as_ref() { + // If mint is decompressed, mint tokens to the token pool to maintain SPL mint supply consistency + if accounts_config.is_decompressed { + let mint_account = system_accounts + .mint + .ok_or(ErrorCode::MintActionMissingMintAccount)?; + + let token_pool_account = system_accounts + .token_pool_pda + .ok_or(ErrorCode::MintActionMissingTokenPoolAccount)?; + let token_program = system_accounts + .token_program + .ok_or(ErrorCode::MintActionMissingTokenProgram)?; + mint_to_token_pool( + mint_account, + token_pool_account, + token_program, + validated_accounts.cpi_authority()?, + sum_amounts, + )?; + } + } + // Create output token accounts + create_output_compressed_token_accounts( + action, + cpi_instruction_struct, + hash_cache, + mint, + out_token_queue_index, + )?; + Ok(updated_supply) +} + +fn create_output_compressed_token_accounts( + parsed_instruction_data: &ZMintToAction<'_>, + output_compressed_accounts: &mut [ZOutputCompressedAccountWithPackedContextMut<'_>], + hash_cache: &mut HashCache, + mint: Pubkey, + queue_pubkey_index: u8, +) -> Result<(), ProgramError> { + let hashed_mint = hash_cache.get_or_hash_mint(&mint.to_bytes())?; + + let lamports = parsed_instruction_data + .lamports + .map(|lamports| u64::from(*lamports)); + for (recipient, output_account) in parsed_instruction_data + .recipients + .iter() + .zip(output_compressed_accounts.iter_mut()) + { + let output_delegate = None; + set_output_compressed_account::( + output_account, + hash_cache, + recipient.recipient, + output_delegate, + recipient.amount, + lamports, + mint, + &hashed_mint, + queue_pubkey_index, + parsed_instruction_data.token_account_version, + )?; + } + Ok(()) +} diff --git a/programs/compressed-token/program/src/mint_action/mint_to_decompressed.rs b/programs/compressed-token/program/src/mint_action/mint_to_decompressed.rs new file mode 100644 index 0000000000..eeba1c0251 --- /dev/null +++ b/programs/compressed-token/program/src/mint_action/mint_to_decompressed.rs @@ -0,0 +1,100 @@ +use anchor_compressed_token::ErrorCode; +use anchor_lang::solana_program::program_error::ProgramError; +use light_account_checks::packed_accounts::ProgramPackedAccounts; +use light_compressed_account::Pubkey; +use light_ctoken_types::{ + instructions::{mint_actions::ZMintToDecompressedAction, transfer2::CompressionMode}, + state::ZCompressedMintMut, +}; +use pinocchio::account_info::AccountInfo; +use spl_pod::solana_msg::msg; + +use crate::{ + mint_action::{ + accounts::{AccountsConfig, MintActionAccounts}, + mint_to::mint_authority_check, + }, + shared::mint_to_token_pool, + transfer2::native_compression::native_compression, +}; + +#[allow(clippy::too_many_arguments)] +pub fn process_mint_to_decompressed_action( + action: &ZMintToDecompressedAction, + current_supply: u64, + compressed_mint: &ZCompressedMintMut<'_>, + validated_accounts: &MintActionAccounts, + accounts_config: &AccountsConfig, + packed_accounts: &ProgramPackedAccounts<'_, AccountInfo>, + mint: Pubkey, + instruction_mint_authority: Option, +) -> Result { + mint_authority_check( + compressed_mint, + validated_accounts, + instruction_mint_authority, + )?; + + let amount = u64::from(action.recipient.amount); + let updated_supply = current_supply + .checked_add(amount) + .ok_or(ErrorCode::MintActionAmountTooLarge)?; + + handle_decompressed_mint_to_token_pool(validated_accounts, accounts_config, amount, mint)?; + + // Get the recipient token account from packed accounts using the index + let token_account_info = packed_accounts.get_u8( + action.recipient.account_index, + "decompressed mint to recipient", + )?; + + // Authority check now performed above - safe to proceed with decompression + native_compression( + None, // No authority needed for decompression + amount, + mint.into(), + token_account_info, + CompressionMode::Decompress, + )?; + Ok(updated_supply) +} + +fn handle_decompressed_mint_to_token_pool( + validated_accounts: &MintActionAccounts, + accounts_config: &crate::mint_action::accounts::AccountsConfig, + amount: u64, + mint: Pubkey, +) -> Result<(), ProgramError> { + if let Some(system_accounts) = validated_accounts.executing.as_ref() { + // If mint is decompressed, mint tokens to the token pool to maintain SPL mint supply consistency + if accounts_config.is_decompressed { + let mint_account = system_accounts + .mint + .ok_or(ErrorCode::MintActionMissingMintAccount)?; + if mint.to_bytes() != *mint_account.key() { + msg!("Mint account mismatch"); + return Err(ErrorCode::MintAccountMismatch.into()); + } + // TODO: check derivation. with bump. + let token_pool_account = system_accounts + .token_pool_pda + .ok_or(ErrorCode::MintActionMissingTokenPoolAccount)?; + let token_program = system_accounts + .token_program + .ok_or(ErrorCode::MintActionMissingTokenProgram)?; + + msg!( + "Minting {} tokens to token pool for decompressed action", + amount + ); + mint_to_token_pool( + mint_account, + token_pool_account, + token_program, + validated_accounts.cpi_authority()?, + amount, + )?; + } + } + Ok(()) +} diff --git a/programs/compressed-token/program/src/mint_action/update_authority.rs b/programs/compressed-token/program/src/mint_action/update_authority.rs new file mode 100644 index 0000000000..9a2eb0d166 --- /dev/null +++ b/programs/compressed-token/program/src/mint_action/update_authority.rs @@ -0,0 +1,53 @@ +use anchor_compressed_token::ErrorCode; +use anchor_lang::solana_program::program_error::ProgramError; +use light_compressed_account::Pubkey; +use light_ctoken_types::instructions::mint_actions::ZUpdateAuthority; +use light_zero_copy::traits::ZeroCopyAtMut; +use spl_pod::solana_msg::msg; + +/// Validates signer authority and updates the authority field in one operation +pub fn validate_and_update_authority( + authority_field: &mut as ZeroCopyAtMut<'_>>::ZeroCopyAtMut, + instruction_fallback: Option, + update_action: &ZUpdateAuthority<'_>, + signer: &pinocchio::pubkey::Pubkey, + authority_name: &str, +) -> Result<(), ProgramError> { + // Get current authority (from field or instruction fallback) + let current_authority = authority_field + .as_ref() + .map(|a| **a) + .or(instruction_fallback) + .ok_or(ProgramError::InvalidArgument)?; + + // Validate signer matches current authority + if *signer != current_authority.to_bytes() { + msg!( + "Invalid authority: signer does not match current {}", + authority_name + ); + return Err(ProgramError::InvalidArgument); + } + + // Apply update based on allocation and requested change + let new_authority = update_action.new_authority.as_ref().map(|auth| **auth); + match (authority_field.as_mut(), new_authority) { + // Set new authority value in allocated field + (Some(field_ref), Some(new_auth)) => **field_ref = new_auth, + // Inconsistent state: allocated Some but trying to revoke + // This indicates allocation logic bug - revoke should allocate None + (Some(_), None) => { + msg!("Zero copy field is some but should be None"); + return Err(ErrorCode::MintActionUnsupportedOperation.into()); + } + // Invalid operation: cannot set authority when not allocated + (None, Some(_)) => { + msg!("Cannot set {} when none was allocated", authority_name); + return Err(ErrorCode::MintActionUnsupportedOperation.into()); + } + // Already revoked - no operation needed + (None, None) => {} + } + + Ok(()) +} diff --git a/programs/compressed-token/program/src/mint_action/update_metadata.rs b/programs/compressed-token/program/src/mint_action/update_metadata.rs new file mode 100644 index 0000000000..8946ad43ee --- /dev/null +++ b/programs/compressed-token/program/src/mint_action/update_metadata.rs @@ -0,0 +1,348 @@ +use anchor_compressed_token::ErrorCode; +use anchor_lang::prelude::ProgramError; +use light_compressed_account::Pubkey; +use light_ctoken_types::{ + instructions::mint_actions::{ + ZRemoveMetadataKeyAction, ZUpdateMetadataAuthorityAction, ZUpdateMetadataFieldAction, + }, + state::{ZCompressedMintMut, ZExtensionStructMut}, +}; +use light_zero_copy::traits::{ZeroCopyAt, ZeroCopyAtMut}; +use spl_pod::solana_msg::msg; + +/// Simple authority check helper - validates that authority is Some (signer was validated) +fn check_validated_metadata_authority( + validated_metadata_authority: &Option, + authority: & as ZeroCopyAtMut<'_>>::ZeroCopyAtMut, + operation_name: &str, +) -> Result<(), ProgramError> { + if let Some(validated_metadata_authority) = validated_metadata_authority { + msg!("authority {:?} ", authority); + let authority = authority.as_ref().ok_or(ProgramError::from( + ErrorCode::MintActionInvalidMintAuthority, + ))?; + + if *validated_metadata_authority != **authority { + msg!( + "validated_metadata_authority {:?} authority {:?}", + validated_metadata_authority, + **authority + ); + return Err(ErrorCode::MintActionInvalidMintAuthority.into()); + } + } else { + msg!( + "Metadata authority validation failed for {}: no valid metadata authority", + operation_name + ); + return Err(ErrorCode::MintActionInvalidMintAuthority.into()); + } + msg!( + "Metadata authority validation passed for {}", + operation_name + ); + Ok(()) +} + +/// Copies metadata value with length validation to prevent buffer overflow +pub fn safe_copy_metadata_value( + dest: &mut [u8], + src: &[u8], + field_name: &str, +) -> Result<(), ProgramError> { + // Validate source length fits in destination buffer + if src.len() > dest.len() { + msg!( + "Metadata {} value too large: {} bytes, maximum allowed: {} bytes", + field_name, + src.len(), + dest.len() + ); + return Err(ErrorCode::MintActionUnsupportedOperation.into()); + } + + // Safe and efficient copy - clear entire buffer for security + dest.fill(0); + dest[..src.len()].copy_from_slice(src); + Ok(()) +} + +/// Process update metadata field action - modifies the instruction data extensions directly +pub fn process_update_metadata_field_action( + action: &ZUpdateMetadataFieldAction, + compressed_mint: &mut ZCompressedMintMut<'_>, + validated_metadata_authority: &Option, +) -> Result<(), ProgramError> { + msg!("update_metadata_field_action: ENTRY"); + msg!( + "extension_index={}, field_type={}", + action.extension_index, + action.field_type + ); + let extensions = compressed_mint.extensions.as_mut().ok_or_else(|| { + msg!("No extensions found - cannot update metadata"); + ErrorCode::MintActionMissingMetadataExtension + })?; + msg!("Found {} extensions", extensions.len()); + + // Validate extension index bounds + let extension_index = action.extension_index as usize; + if extension_index >= extensions.len() { + msg!( + "Extension index {} out of bounds, available extensions: {}", + extension_index, + extensions.len() + ); + return Err(ErrorCode::MintActionInvalidExtensionIndex.into()); + } + msg!("Extension index {} is valid", extension_index); + + // Get the metadata extension + msg!("About to match on extension type"); + match &mut extensions.as_mut_slice()[extension_index] { + ZExtensionStructMut::TokenMetadata(ref mut metadata) => { + msg!("Matched TokenMetadata extension"); + // Simple authority check: validated_metadata_authority must be Some + check_validated_metadata_authority( + validated_metadata_authority, + &metadata.update_authority, + "metadata field update", + )?; + + // Update metadata fields with length validation + msg!("About to process field type {}", action.field_type); + match action.field_type { + 0 => { + msg!( + "Processing name field update, buffer len: {}, value len: {}", + metadata.metadata.name.len(), + action.value.len() + ); + // Update name + safe_copy_metadata_value(metadata.metadata.name, action.value, "name")?; + msg!("Updated metadata name"); + } + 1 => { + // Update symbol + safe_copy_metadata_value(metadata.metadata.symbol, action.value, "symbol")?; + msg!("Updated metadata symbol"); + } + 2 => { + // Update uri + safe_copy_metadata_value(metadata.metadata.uri, action.value, "uri")?; + msg!("Updated metadata uri"); + } + _ => { + // Find existing key or add new one + // Validate additional_metadata is not empty before processing + if metadata.additional_metadata.is_empty() { + msg!("No additional metadata fields available for custom key updates"); + return Err(ErrorCode::MintActionUnsupportedOperation.into()); + } + let mut found = false; + for metadata_pair in metadata.additional_metadata.iter_mut() { + if metadata_pair.key == action.key { + safe_copy_metadata_value( + metadata_pair.value, + action.value, + "custom field", + )?; + found = true; + break; + } + } + if !found { + msg!("Adding new custom key-value pair not supported in zero-copy mode"); + return Err(ErrorCode::MintActionUnsupportedOperation.into()); + } + + let key_str = String::from_utf8_lossy(action.key); + msg!("Updated metadata custom key: {}", key_str); + } + } + } + _ => { + msg!( + "Extension at index {} is not a TokenMetadata extension", + extension_index + ); + return Err(ErrorCode::MintActionInvalidExtensionType.into()); + } + } + + msg!("Successfully updated metadata field"); + + // Invariant check: Verify metadata state is valid after update + validate_metadata_invariants(compressed_mint, "field update")?; + Ok(()) +} + +/// Validates metadata invariants to ensure consistent state +fn validate_metadata_invariants( + compressed_mint: &ZCompressedMintMut<'_>, + operation: &str, +) -> Result<(), ProgramError> { + if let Some(extensions) = compressed_mint.extensions.as_ref() { + // Ensure we have at least one extension if extensions exist + if extensions.is_empty() { + msg!( + "Invalid state after {}: extensions array exists but is empty", + operation + ); + return Err(ErrorCode::MintActionInvalidExtensionType.into()); + } + } + Ok(()) +} + +/// Updates metadata authority field when allocation and action match +fn update_metadata_authority_field( + metadata_authority: &mut as ZeroCopyAtMut<'_>>::ZeroCopyAtMut, + new_authority: Option, +) -> Result<(), ProgramError> { + match (metadata_authority.as_mut(), new_authority) { + (Some(field_ref), Some(new_auth)) => { + // Update existing authority to new value + **field_ref = new_auth; + msg!("Authority updated successfully"); + } + (None, None) => { + // Authority was correctly revoked during allocation - nothing to do + msg!("Authority successfully revoked"); + } + (Some(_), None) => { + // This should never happen with correct allocation logic + msg!("Internal error: authority field allocated but should be revoked"); + return Err(ErrorCode::MintActionUnsupportedOperation.into()); + } + (None, Some(_)) => { + // This should never happen with correct allocation logic + msg!("Internal error: no authority field allocated but trying to set authority"); + return Err(ErrorCode::MintActionUnsupportedOperation.into()); + } + } + Ok(()) +} + +/// Process update metadata authority action +pub fn process_update_metadata_authority_action( + action: &ZUpdateMetadataAuthorityAction, + compressed_mint: &mut ZCompressedMintMut<'_>, + instruction_data_mint_authority: & as ZeroCopyAt< + '_, + >>::ZeroCopyAt, + validated_metadata_authority: &mut Option, +) -> Result<(), ProgramError> { + let extensions = compressed_mint.extensions.as_mut().ok_or_else(|| { + msg!("No extensions found - cannot update metadata authority"); + ErrorCode::MintActionMissingMetadataExtension + })?; + + let extension_index = action.extension_index as usize; + if extension_index >= extensions.len() { + msg!("Extension index {} out of bounds", extension_index); + return Err(ErrorCode::MintActionInvalidExtensionIndex.into()); + } + + // Get the metadata extension and update the authority + match &mut extensions.as_mut_slice()[extension_index] { + ZExtensionStructMut::TokenMetadata(ref mut metadata) => { + let new_authority = if action.new_authority.to_bytes() == [0u8; 32] { + None + } else { + Some(action.new_authority) + }; + + if metadata.update_authority.is_none() { + let instruction_data_mint_authority = instruction_data_mint_authority + .ok_or(ErrorCode::MintActionInvalidMintAuthority)?; + msg!( + "instruction_data_mint_authority {:?}", + solana_pubkey::Pubkey::new_from_array( + instruction_data_mint_authority.to_bytes() + ) + ); + { + let validated_metadata_authority = validated_metadata_authority + .as_ref() + .ok_or(ErrorCode::MintActionInvalidMintAuthority)?; + msg!( + "validated_metadata_authority {:?}", + solana_pubkey::Pubkey::new_from_array( + validated_metadata_authority.to_bytes() + ) + ); + if *instruction_data_mint_authority != *validated_metadata_authority { + msg!( + "Metadata authority validation failed for metadata authority update: no valid metadata authority" + ); + return Err(ErrorCode::MintActionInvalidMintAuthority.into()); + } + } + } else { + msg!("here4"); + // Simple authority check: validated_metadata_authority must be Some to perform authority operations + check_validated_metadata_authority( + validated_metadata_authority, + &metadata.update_authority, + "metadata authority update", + )?; + + update_metadata_authority_field(&mut metadata.update_authority, new_authority)?; + } // Update the validated authority state for future actions + *validated_metadata_authority = new_authority; + } + _ => { + msg!( + "Extension at index {} is not a TokenMetadata extension", + extension_index + ); + return Err(ErrorCode::MintActionInvalidExtensionType.into()); + } + } + + // Invariant check: Verify metadata state is valid after authority update + validate_metadata_invariants(compressed_mint, "authority update")?; + Ok(()) +} + +/// Only checks authority, the key is removed during data allocation. +pub fn process_remove_metadata_key_action( + action: &ZRemoveMetadataKeyAction, + compressed_mint: &ZCompressedMintMut<'_>, + validated_metadata_authority: &Option, +) -> Result<(), ProgramError> { + let extensions = compressed_mint.extensions.as_ref().ok_or_else(|| { + msg!("No extensions found - cannot update metadata authority"); + ErrorCode::MintActionMissingMetadataExtension + })?; + + let extension_index = action.extension_index as usize; + if extension_index >= extensions.len() { + msg!("Extension index {} out of bounds", extension_index); + return Err(ErrorCode::MintActionInvalidExtensionIndex.into()); + } + + // Verify extension exists and is TokenMetadata + match &extensions.as_slice()[extension_index] { + ZExtensionStructMut::TokenMetadata(metadata) => { + msg!("TokenMetadata extension validated for key removal"); + check_validated_metadata_authority( + validated_metadata_authority, + &metadata.update_authority, + "metadata key removal", + )?; + } + _ => { + msg!( + "Extension at index {} is not a TokenMetadata extension", + extension_index + ); + return Err(ErrorCode::MintActionInvalidExtensionType.into()); + } + } + + // Invariant check: Verify metadata state is valid after key removal + validate_metadata_invariants(compressed_mint, "key removal")?; + Ok(()) +} diff --git a/programs/compressed-token/program/src/shared/create_pda_account.rs b/programs/compressed-token/program/src/shared/create_pda_account.rs index 7d6eadc351..5b8142615c 100644 --- a/programs/compressed-token/program/src/shared/create_pda_account.rs +++ b/programs/compressed-token/program/src/shared/create_pda_account.rs @@ -44,12 +44,10 @@ pub fn create_pda_account( let bump_bytes = [config.bump]; let mut seed_vec: ArrayVec = ArrayVec::new(); - for &seed in config.seeds { seed_vec.push(Seed::from(seed)); } seed_vec.push(Seed::from(bump_bytes.as_ref())); - let signer = Signer::from(seed_vec.as_slice()); let create_account_ix = system_instruction::create_account( &solana_pubkey::Pubkey::new_from_array(*fee_payer.key()), diff --git a/programs/system/Cargo.toml b/programs/system/Cargo.toml index aa0cef37fa..8362f9d598 100644 --- a/programs/system/Cargo.toml +++ b/programs/system/Cargo.toml @@ -54,6 +54,7 @@ pinocchio-pubkey = { workspace = true } solana-msg = { workspace = true } light-profiler = { workspace = true } light-heap = { workspace = true, optional = true } + [dev-dependencies] rand = { workspace = true } light-compressed-account = { workspace = true, features = [ diff --git a/programs/system/src/cpi_context/account.rs b/programs/system/src/cpi_context/account.rs index 99c69eb4a9..5d4656fa0e 100644 --- a/programs/system/src/cpi_context/account.rs +++ b/programs/system/src/cpi_context/account.rs @@ -127,7 +127,7 @@ impl OutputAccount<'_> for CpiContextOutAccount { } fn has_data(&self) -> bool { - self.discriminator != [0; 8] + self.discriminator != [0; 8] || self.data_hash != [0; 32] } fn skip(&self) -> bool { diff --git a/programs/system/src/cpi_context/process_cpi_context.rs b/programs/system/src/cpi_context/process_cpi_context.rs index aea9f0f624..ed5b2d45c3 100644 --- a/programs/system/src/cpi_context/process_cpi_context.rs +++ b/programs/system/src/cpi_context/process_cpi_context.rs @@ -81,6 +81,7 @@ pub fn process_cpi_context<'a, 'info, T: InstructionData<'a>>( return Err(SystemProgramError::CpiContextEmpty.into()); } if (*cpi_context_account.fee_payer).to_bytes() != fee_payer { + msg!("fee payer mismatch"); msg!(format!(" {:?} != {:?}", fee_payer, cpi_context_account.fee_payer).as_str()); return Err(SystemProgramError::CpiContextFeePayerMismatch.into()); } @@ -90,6 +91,7 @@ pub fn process_cpi_context<'a, 'info, T: InstructionData<'a>>( return Ok(Some((1, instruction_data))); } } + msg!("cpi context is none"); Ok(Some((0, instruction_data))) } @@ -161,7 +163,7 @@ pub fn copy_cpi_context_outputs( compressed_account: CompressedAccountConfig { address: (output_account.address().is_some(), ()), data: ( - !output_data.is_empty(), + output_account.has_data(), CompressedAccountDataConfig { data: output_data.len() as u32, }, diff --git a/programs/system/src/lib.rs b/programs/system/src/lib.rs index 8a9c10d5e8..53c2fd19c1 100644 --- a/programs/system/src/lib.rs +++ b/programs/system/src/lib.rs @@ -94,9 +94,7 @@ pub fn invoke<'a, 'b, 'c: 'info, 'info>( accounts: &[AccountInfo], instruction_data: &[u8], ) -> Result<()> { - // remove vec prefix let instruction_data = &instruction_data[4..]; - let (inputs, _) = ZInstructionDataInvoke::zero_copy_at(instruction_data)?; let (ctx, remaining_accounts) = InvokeInstruction::from_account_infos(accounts)?; @@ -119,11 +117,8 @@ pub fn invoke_cpi<'a, 'b, 'c: 'info, 'info>( accounts: &[AccountInfo], instruction_data: &[u8], ) -> Result<()> { - // remove vec prefix let instruction_data = &instruction_data[4..]; - let (inputs, _) = ZInstructionDataInvokeCpi::zero_copy_at(instruction_data)?; - let (ctx, remaining_accounts) = InvokeCpiInstruction::from_account_infos(accounts)?; process_invoke_cpi::( @@ -196,7 +191,9 @@ fn shared_invoke_cpi<'a, 'info, T: InstructionData<'a>>( ctx, inputs, remaining_accounts, - ) + )?; + + Ok(()) } } } diff --git a/programs/system/src/processor/create_address_cpi_data.rs b/programs/system/src/processor/create_address_cpi_data.rs index 0d8d18b638..508a086500 100644 --- a/programs/system/src/processor/create_address_cpi_data.rs +++ b/programs/system/src/processor/create_address_cpi_data.rs @@ -100,6 +100,8 @@ pub fn derive_new_addresses<'info, 'a, 'b: 'a, const ADDRESS_ASSIGNMENT: bool>( } cpi_ix_data.addresses[i].address = address; + // msg!("setting rollover fee"); + context.set_rollover_fee(new_address_params.address_queue_index(), rollover_fee); } cpi_ix_data.num_address_queues = accounts diff --git a/programs/system/src/processor/process.rs b/programs/system/src/processor/process.rs index de66c60b07..245844a7d9 100644 --- a/programs/system/src/processor/process.rs +++ b/programs/system/src/processor/process.rs @@ -103,6 +103,17 @@ pub fn process< let cpi_outputs_data_len = inputs.get_cpi_context_outputs_end_offset() - inputs.get_cpi_context_outputs_start_offset(); + // msg!(&format!("cpi_outputs_data_len {:?}", cpi_outputs_data_len)); + // msg!(&format!( + // "cpi_context_inputs_len {:?}", + // cpi_context_inputs_len + // )); + // msg!(&format!("num_new_addresses {:?}", num_new_addresses)); + // msg!(&format!("num_input_accounts {:?}", num_input_accounts)); + // msg!(&format!( + // "num_output_compressed_accounts {:?}", + // num_output_compressed_accounts + // )); // 1. Allocate cpi data and initialize context let (mut context, mut cpi_ix_bytes) = create_cpi_data_and_context( ctx, @@ -116,7 +127,9 @@ pub fn process< )?; // 2. Deserialize and check all Merkle tree and queue accounts. + // msg!("trying from account infos"); let mut accounts = try_from_account_infos(remaining_accounts, &mut context)?; + // msg!("done from account infos"); // 3. Deserialize cpi instruction data as zero copy to fill it. let (mut cpi_ix_data, bytes) = InsertIntoQueuesInstructionDataMut::new_at( &mut cpi_ix_bytes[12..], // 8 bytes instruction discriminator + 4 bytes vector length @@ -151,6 +164,7 @@ pub fn process< context.addresses.push(account.address()); }); + // msg!("trying derive new addresses"); // 7. Derive new addresses from seed and invoking program if num_new_addresses != 0 { derive_new_addresses::( @@ -171,6 +185,7 @@ pub fn process< return Err(SystemProgramError::InvalidAddress.into()); } } + // msg!("done deriving new addresses"); // 7. Verify read only address non-inclusion in bloom filters verify_read_only_address_queue_non_inclusion( diff --git a/programs/system/src/processor/verify_proof.rs b/programs/system/src/processor/verify_proof.rs index c10e3121c2..b41fb91aa1 100644 --- a/programs/system/src/processor/verify_proof.rs +++ b/programs/system/src/processor/verify_proof.rs @@ -120,6 +120,32 @@ fn read_root( roots: &mut Vec<[u8; 32]>, ) -> Result { let height; + + // let account_type = match &merkle_tree_account { + // AcpAccount::Authority(_) => "Authority", + // AcpAccount::RegisteredProgramPda(_) => "RegisteredProgramPda", + // AcpAccount::SystemProgram(_) => "SystemProgram", + // AcpAccount::OutputQueue(_) => "OutputQueue", + // AcpAccount::BatchedStateTree(_) => "BatchedStateTree", + // AcpAccount::BatchedAddressTree(_) => "BatchedAddressTree", + // AcpAccount::StateTree(_) => "StateTree", + // AcpAccount::AddressTree(_) => "AddressTree", + // AcpAccount::AddressQueue(_, _) => "AddressQueue", + // AcpAccount::V1Queue(_) => "V1Queue", + // AcpAccount::Unknown() => "Unknown", + // }; + // // msg!(&format!("merkle_tree_account type: {}", account_type)); + // let pubkey = match &merkle_tree_account { + // AcpAccount::AddressTree((pubkey, _)) => pubkey, + // AcpAccount::BatchedAddressTree(tree) => tree.pubkey(), + // _ => { + // msg!("fu"); + // return Err(SystemProgramError::AddressMerkleTreeAccountDiscriminatorMismatch.into()); + // } + // }; + + // msg!(&format!("root_index:{:?} pubkey: {:?}", root_index, pubkey)); + match merkle_tree_account { AcpAccount::AddressTree((_, merkle_tree)) => { if IS_READ_ONLY { @@ -151,6 +177,7 @@ fn read_root( return if IS_STATE { Err(SystemProgramError::StateMerkleTreeAccountDiscriminatorMismatch) } else { + msg!("is_state: false"); Err(SystemProgramError::AddressMerkleTreeAccountDiscriminatorMismatch) } } diff --git a/programs/system/tests/invoke_cpi_instruction_small.rs b/programs/system/tests/invoke_cpi_instruction_small.rs index 231242faee..360d305fa1 100644 --- a/programs/system/tests/invoke_cpi_instruction_small.rs +++ b/programs/system/tests/invoke_cpi_instruction_small.rs @@ -326,7 +326,6 @@ fn test_decompression_recipient_and_cpi_context_validation() { let account_compression_program = get_account_compression_program_account_info(); let system_program = get_system_program_account_info(); - let account_info_array = [ fee_payer.clone(), authority.clone(), @@ -388,7 +387,6 @@ fn failing_from_account_infos_small() { let account_compression_program = get_account_compression_program_account_info(); let system_program = get_system_program_account_info(); - // Base array for tests let account_info_array = [ fee_payer.clone(), diff --git a/prover/server/prover/proving_keys_utils.go b/prover/server/prover/proving_keys_utils.go index a90850967e..49b25ab2f4 100644 --- a/prover/server/prover/proving_keys_utils.go +++ b/prover/server/prover/proving_keys_utils.go @@ -157,6 +157,7 @@ func GetKeys(keysDir string, runMode RunMode, circuits []string) []string { keysDir + "non-inclusion_26_2.key", keysDir + "non-inclusion_40_1.key", keysDir + "non-inclusion_40_2.key", + keysDir + "non-inclusion_40_3.key", } var appendKeys []string = []string{ diff --git a/scripts/format.sh b/scripts/format.sh index c6637070f2..4bdf64748a 100755 --- a/scripts/format.sh +++ b/scripts/format.sh @@ -10,9 +10,12 @@ cargo clippy \ --workspace \ --no-deps \ --all-features \ - --exclude name-service \ --exclude photon-api \ --exclude name-service \ + --exclude anchor-compressible-derived \ + --exclude native-compressible \ + --exclude sdk-native-test \ + --exclude fetch-accounts \ -- -A clippy::result_large_err \ -A clippy::empty-docs \ -A clippy::to-string-trait-impl \ @@ -29,4 +32,4 @@ cargo test-sbf -p compressed-token-test --no-run cargo test-sbf -p sdk-native-test --no-run cargo test-sbf -p sdk-anchor-test --no-run cargo test-sbf -p client-test --no-run -cargo test-sbf -p sdk-pinocchio-test --no-run +cargo test-sbf -p sdk-pinocchio-test --no-run \ No newline at end of file diff --git a/scripts/install.sh b/scripts/install.sh index 4780e7cb28..a9c9f08f62 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -13,7 +13,7 @@ VERSIONS=( "solana:2.2.15" "anchor:anchor-v0.29.0" "jq:jq-1.8.0" - "photon:0.51.0" + "photon:0.52.3" "redis:8.0.1" ) @@ -210,7 +210,8 @@ install_photon() { if [ "$photon_installed" = false ] || [ "$photon_correct_version" = false ]; then echo "Installing Photon indexer (version $expected_version)..." # Use git commit for now as specified in constants.ts - cargo install --git https://github.com/helius-labs/photon.git --rev b0ad386858384c22b4bb6a3bbbcd6a65911dac68 --locked --force + # cargo install --git https://github.com/lightprotocol/photon.git --rev b739156 --locked --force + cargo install --git https://github.com/lightprotocol/photon.git --rev 6ba6813 --locked --force log "photon" else echo "Photon already installed with correct version, skipping..." diff --git a/sdk-libs/client/Cargo.toml b/sdk-libs/client/Cargo.toml index 2a7ef31287..f44d44c1a1 100644 --- a/sdk-libs/client/Cargo.toml +++ b/sdk-libs/client/Cargo.toml @@ -36,6 +36,8 @@ solana-address-lookup-table-interface = { version = "2.2.1", features = [ "bytemuck", "bincode", ] } +solana-message = "2.2" +anchor-lang = { workspace = true, features = ["idl-build"], optional = true } # Light Protocol dependencies light-merkle-tree-metadata = { workspace = true, features = ["solana"] } @@ -64,5 +66,7 @@ tracing = { workspace = true } lazy_static = { workspace = true } rand = { workspace = true } + + # Tests are in program-tests/client-test/tests/light-client.rs # [dev-dependencies] diff --git a/sdk-libs/client/src/constants.rs b/sdk-libs/client/src/constants.rs index 9c6e41699e..3d8fdf0d43 100644 --- a/sdk-libs/client/src/constants.rs +++ b/sdk-libs/client/src/constants.rs @@ -9,3 +9,27 @@ pub const STATE_TREE_LOOKUP_TABLE_DEVNET: Pubkey = pubkey!("8n8rH2bFRVA6cSGNDpgqcKHCndbFCT1bXxAQG89ejVsh"); 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 LOOKUP_TABLE_ADDRESS: Pubkey = pubkey!("9NYFyEqPkyXUhkerbGHXUXkvb4qpzeEdHuGpgbgpH1NJ"); diff --git a/sdk-libs/client/src/indexer/tree_info.rs b/sdk-libs/client/src/indexer/tree_info.rs index a4a0a29cdc..a11eedf3a4 100644 --- a/sdk-libs/client/src/indexer/tree_info.rs +++ b/sdk-libs/client/src/indexer/tree_info.rs @@ -259,28 +259,44 @@ lazy_static! { ); } + + // v2 tree 1 m.insert( "6L7SzhYB3anwEQ9cphpJ1U7Scwj57bx2xueReg7R9cKU".to_string(), TreeInfo { tree: pubkey!("HLKs5NJ8FXkJg8BrzJt56adFYYuwg5etzDtBbQYTsixu"), queue: pubkey!("6L7SzhYB3anwEQ9cphpJ1U7Scwj57bx2xueReg7R9cKU"), - cpi_context: None, + cpi_context: Some(pubkey!("7Hp52chxaew8bW1ApR4fck2bh6Y8qA1pu3qwH6N9zaLj")), tree_type: TreeType::StateV2, next_tree_info: None, }, ); + // v2 queue 1 m.insert( "HLKs5NJ8FXkJg8BrzJt56adFYYuwg5etzDtBbQYTsixu".to_string(), TreeInfo { tree: pubkey!("HLKs5NJ8FXkJg8BrzJt56adFYYuwg5etzDtBbQYTsixu"), queue: pubkey!("6L7SzhYB3anwEQ9cphpJ1U7Scwj57bx2xueReg7R9cKU"), - cpi_context: None, + cpi_context: Some(pubkey!("7Hp52chxaew8bW1ApR4fck2bh6Y8qA1pu3qwH6N9zaLj")), tree_type: TreeType::StateV2, next_tree_info: None, }, ); + // v2 cpi context 1 + m.insert( + "7Hp52chxaew8bW1ApR4fck2bh6Y8qA1pu3qwH6N9zaLj".to_string(), + TreeInfo { + tree: pubkey!("HLKs5NJ8FXkJg8BrzJt56adFYYuwg5etzDtBbQYTsixu"), + queue: pubkey!("6L7SzhYB3anwEQ9cphpJ1U7Scwj57bx2xueReg7R9cKU"), + cpi_context: Some(pubkey!("7Hp52chxaew8bW1ApR4fck2bh6Y8qA1pu3qwH6N9zaLj")), + tree_type: TreeType::StateV2, + next_tree_info: None, + }, + ); + + // address v2 tree m.insert( "EzKE84aVTkCUhDHLELqyJaq1Y7UVVmqxXqZjVHwHY3rK".to_string(), TreeInfo { @@ -292,6 +308,42 @@ lazy_static! { }, ); + // v2 queue 2 + m.insert( + "12wJT3xYd46rtjeqDU6CrtT8unqLjPiheggzqhN9YsyB".to_string(), + TreeInfo { + tree: pubkey!("2Yb3fGo2E9aWLjY8KuESaqurYpGGhEeJr7eynKrSgXwS"), + queue: pubkey!("12wJT3xYd46rtjeqDU6CrtT8unqLjPiheggzqhN9YsyB"), + cpi_context: Some(pubkey!("HwtjxDvFEXiWnzeMeWkMBzpQN45A95rTJNZmz1Z3pe8R")), + tree_type: TreeType::StateV2, + next_tree_info: None, + }, + ); + + // v2 tree 2 + m.insert( + "2Yb3fGo2E9aWLjY8KuESaqurYpGGhEeJr7eynKrSgXwS".to_string(), + TreeInfo { + tree: pubkey!("2Yb3fGo2E9aWLjY8KuESaqurYpGGhEeJr7eynKrSgXwS"), + queue: pubkey!("12wJT3xYd46rtjeqDU6CrtT8unqLjPiheggzqhN9YsyB"), + cpi_context: Some(pubkey!("HwtjxDvFEXiWnzeMeWkMBzpQN45A95rTJNZmz1Z3pe8R")), + tree_type: TreeType::StateV2, + next_tree_info: None, + }, + ); + + // v2 cpi context + m.insert( + "HwtjxDvFEXiWnzeMeWkMBzpQN45A95rTJNZmz1Z3pe8R".to_string(), + TreeInfo { + tree: pubkey!("2Yb3fGo2E9aWLjY8KuESaqurYpGGhEeJr7eynKrSgXwS"), + queue: pubkey!("12wJT3xYd46rtjeqDU6CrtT8unqLjPiheggzqhN9YsyB"), + cpi_context: Some(pubkey!("HwtjxDvFEXiWnzeMeWkMBzpQN45A95rTJNZmz1Z3pe8R")), + tree_type: TreeType::StateV2, + next_tree_info: None, + }, + ); + m }; } diff --git a/sdk-libs/client/src/indexer/types.rs b/sdk-libs/client/src/indexer/types.rs index 98c1a4d125..0766f3c3f9 100644 --- a/sdk-libs/client/src/indexer/types.rs +++ b/sdk-libs/client/src/indexer/types.rs @@ -519,10 +519,18 @@ impl TryFrom for CompressedAccount { let hash = account .hash() .map_err(|_| IndexerError::InvalidResponseData)?; - // Breaks light-program-test - // let tree_info = QUEUE_TREE_MAPPING - // .get(&account.merkle_context.merkle_tree_pubkey.to_string()) - // .ok_or(IndexerError::InvalidResponseData)?; + + let tree_pubkey = + Pubkey::new_from_array(account.merkle_context.merkle_tree_pubkey.to_bytes()); + let tree_info = QUEUE_TREE_MAPPING + .get(&tree_pubkey.to_string()) + .ok_or_else(|| { + println!( + "ERROR: No tree_info found for tree pubkey: {:?}", + tree_pubkey.to_string() + ); + IndexerError::InvalidResponseData + })?; Ok(CompressedAccount { address: account.compressed_account.address, @@ -531,10 +539,10 @@ impl TryFrom for CompressedAccount { lamports: account.compressed_account.lamports, leaf_index: account.merkle_context.leaf_index, tree_info: TreeInfo { - tree: Pubkey::new_from_array(account.merkle_context.merkle_tree_pubkey.to_bytes()), + tree: tree_pubkey, queue: Pubkey::new_from_array(account.merkle_context.queue_pubkey.to_bytes()), tree_type: account.merkle_context.tree_type, - cpi_context: None, + cpi_context: tree_info.cpi_context, next_tree_info: None, }, owner: Pubkey::new_from_array(account.compressed_account.owner.to_bytes()), @@ -581,6 +589,18 @@ impl TryFrom<&photon_api::models::AccountV2> for CompressedAccount { Ok::, IndexerError>(None) }?; + let tree_pubkey = + Pubkey::new_from_array(decode_base58_to_fixed_array(&account.merkle_context.tree)?); + let tree_info = QUEUE_TREE_MAPPING + .get(&tree_pubkey.to_string()) + .ok_or_else(|| { + println!( + "ERROR: No tree_info found for tree pubkey: {}", + account.merkle_context.tree + ); + IndexerError::InvalidResponseData + })?; + let owner = Pubkey::new_from_array(decode_base58_to_fixed_array(&account.owner)?); let address = account .address @@ -590,14 +610,12 @@ impl TryFrom<&photon_api::models::AccountV2> for CompressedAccount { let hash = decode_base58_to_fixed_array(&account.hash)?; let tree_info = TreeInfo { - tree: Pubkey::new_from_array(decode_base58_to_fixed_array( - &account.merkle_context.tree, - )?), + tree: tree_pubkey, queue: Pubkey::new_from_array(decode_base58_to_fixed_array( &account.merkle_context.queue, )?), tree_type: TreeType::from(account.merkle_context.tree_type as u64), - cpi_context: decode_base58_option_to_pubkey(&account.merkle_context.cpi_context)?, + cpi_context: tree_info.cpi_context, next_tree_info: account .merkle_context .next_tree_context diff --git a/sdk-libs/client/src/lib.rs b/sdk-libs/client/src/lib.rs index a5159c310d..095cf2a8e7 100644 --- a/sdk-libs/client/src/lib.rs +++ b/sdk-libs/client/src/lib.rs @@ -81,6 +81,7 @@ pub mod fee; pub mod indexer; pub mod local_test_validator; pub mod rpc; +pub mod utils; /// Reexport for ProverConfig and other types. pub use light_prover_client; diff --git a/sdk-libs/client/src/rpc/client.rs b/sdk-libs/client/src/rpc/client.rs index af3fcb1641..25d2de1692 100644 --- a/sdk-libs/client/src/rpc/client.rs +++ b/sdk-libs/client/src/rpc/client.rs @@ -691,13 +691,22 @@ impl Rpc for LightClient { use crate::indexer::TreeInfo; #[cfg(feature = "v2")] - let default_trees = vec![TreeInfo { - tree: pubkey!("HLKs5NJ8FXkJg8BrzJt56adFYYuwg5etzDtBbQYTsixu"), - queue: pubkey!("6L7SzhYB3anwEQ9cphpJ1U7Scwj57bx2xueReg7R9cKU"), - cpi_context: Some(pubkey!("7Hp52chxaew8bW1ApR4fck2bh6Y8qA1pu3qwH6N9zaLj")), - next_tree_info: None, - tree_type: TreeType::StateV2, - }]; + let default_trees = vec![ + TreeInfo { + tree: pubkey!("HLKs5NJ8FXkJg8BrzJt56adFYYuwg5etzDtBbQYTsixu"), + queue: pubkey!("6L7SzhYB3anwEQ9cphpJ1U7Scwj57bx2xueReg7R9cKU"), + cpi_context: Some(pubkey!("7Hp52chxaew8bW1ApR4fck2bh6Y8qA1pu3qwH6N9zaLj")), + next_tree_info: None, + tree_type: TreeType::StateV2, + }, + TreeInfo { + tree: pubkey!("2Yb3fGo2E9aWLjY8KuESaqurYpGGhEeJr7eynKrSgXwS"), + queue: pubkey!("12wJT3xYd46rtjeqDU6CrtT8unqLjPiheggzqhN9YsyB"), + cpi_context: Some(pubkey!("HwtjxDvFEXiWnzeMeWkMBzpQN45A95rTJNZmz1Z3pe8R")), + next_tree_info: None, + tree_type: TreeType::StateV2, + }, + ]; #[cfg(not(feature = "v2"))] let default_trees = vec![TreeInfo { diff --git a/sdk-libs/client/src/rpc/lookup_table.rs b/sdk-libs/client/src/rpc/lookup_table.rs new file mode 100644 index 0000000000..e1adfe9872 --- /dev/null +++ b/sdk-libs/client/src/rpc/lookup_table.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 7912056568..19a2e67f40 100644 --- a/sdk-libs/client/src/rpc/mod.rs +++ b/sdk-libs/client/src/rpc/mod.rs @@ -1,6 +1,7 @@ pub mod client; pub mod errors; pub mod indexer; +pub mod lookup_table; pub mod merkle_tree; mod rpc_trait; pub mod state; @@ -9,3 +10,4 @@ pub use client::{LightClient, RetryConfig}; pub use errors::RpcError; pub use rpc_trait::{LightClientConfig, Rpc}; pub mod get_light_state_tree_infos; +pub use lookup_table::load_lookup_table; diff --git a/sdk-libs/client/src/rpc/rpc_trait.rs b/sdk-libs/client/src/rpc/rpc_trait.rs index 0869d4740e..e78ae84c67 100644 --- a/sdk-libs/client/src/rpc/rpc_trait.rs +++ b/sdk-libs/client/src/rpc/rpc_trait.rs @@ -59,7 +59,7 @@ impl LightClientConfig { commitment_config: Some(CommitmentConfig::confirmed()), photon_url: Some("http://127.0.0.1:8784".to_string()), api_key: None, - fetch_active_tree: false, + fetch_active_tree: true, } } diff --git a/sdk-libs/compressed-token-sdk/Cargo.toml b/sdk-libs/compressed-token-sdk/Cargo.toml index a3be15dc86..132dc9d4c2 100644 --- a/sdk-libs/compressed-token-sdk/Cargo.toml +++ b/sdk-libs/compressed-token-sdk/Cargo.toml @@ -4,8 +4,8 @@ version = "0.1.0" edition = { workspace = true } [features] - -anchor = ["anchor-lang", "light-compressed-token-types/anchor"] +default = ["anchor"] +anchor = ["anchor-lang", "light-compressed-token-types/anchor", "light-ctoken-types/anchor", "light-sdk/anchor"] profile-program = [ "light-profiler/profile-program", "light-compressed-account/profile-program", @@ -24,26 +24,30 @@ light-compressed-account = { workspace = true } light-ctoken-types = { workspace = true } light-sdk = { workspace = true } light-macros = { workspace = true } -thiserror = { workspace = true } +light-account-checks = { workspace = true, features = ["solana"] } +light-sdk-types = { workspace = true } +light-zero-copy = { workspace = true } +light-profiler = { workspace = true } + + # Serialization borsh = { workspace = true } -solana-msg = { workspace = true } # Solana dependencies +solana-msg = { workspace = true } solana-pubkey = { workspace = true, features = ["sha2", "curve25519"] } solana-instruction = { workspace = true } solana-account-info = { workspace = true } solana-cpi = { workspace = true } solana-program-error = { workspace = true } -arrayvec = { workspace = true } spl-token-2022 = { workspace = true } spl-pod = { workspace = true } -light-account-checks = { workspace = true, features = ["solana"] } -light-sdk-types = { workspace = true } -light-zero-copy = { workspace = true } -light-profiler = { workspace = true } + +arrayvec = { workspace = true } +thiserror = { workspace = true } # Optional Anchor dependency anchor-lang = { workspace = true, optional = true } +anchor-spl = { version = "=0.31.1", git = "https://github.com/lightprotocol/anchor", rev = "d8a2b3d9", features = ["memo", "metadata", "idl-build"] } [dev-dependencies] light-account-checks = { workspace = true, features = ["test-only", "solana"] } diff --git a/sdk-libs/compressed-token-sdk/src/account2.rs b/sdk-libs/compressed-token-sdk/src/account2.rs index 5acccad17c..9753b322dd 100644 --- a/sdk-libs/compressed-token-sdk/src/account2.rs +++ b/sdk-libs/compressed-token-sdk/src/account2.rs @@ -20,11 +20,11 @@ use crate::{ #[derive(Debug, PartialEq, Clone)] pub struct CTokenAccount2 { - inputs: Vec, - pub(crate) output: MultiTokenTransferOutputData, - compression: Option, - delegate_is_set: bool, - pub(crate) method_used: bool, + pub inputs: Vec, + pub output: MultiTokenTransferOutputData, + pub compression: Option, + pub delegate_is_set: bool, + pub method_used: bool, } impl CTokenAccount2 { @@ -427,31 +427,29 @@ impl Deref for CTokenAccount2 { #[allow(clippy::too_many_arguments)] #[profile] pub fn create_spl_to_ctoken_transfer_instruction( - source_spl_token_account: Pubkey, - to: Pubkey, - amount: u64, + payer: Pubkey, authority: Pubkey, + source_spl_token_account: Pubkey, + destination_ctoken_account: Pubkey, mint: Pubkey, - _payer: Pubkey, - token_pool_pda: Pubkey, - token_pool_pda_bump: u8, + spl_token_program: Pubkey, + compressed_token_pool_pda: Pubkey, + compressed_token_pool_pda_bump: u8, + amount: u64, ) -> Result { let packed_accounts = vec![ // Mint (index 0) AccountMeta::new_readonly(mint, false), // Destination token account (index 1) - AccountMeta::new(to, false), + AccountMeta::new(destination_ctoken_account, 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, - ), + AccountMeta::new(compressed_token_pool_pda, false), + // SPL Token program (or T22) (index 5) - needed for CPI + AccountMeta::new_readonly(spl_token_program, false), ]; let wrap_spl_to_ctoken_account = CTokenAccount2 { @@ -460,11 +458,11 @@ pub fn create_spl_to_ctoken_transfer_instruction( compression: Some(Compression::compress_spl( amount, 0, // mint - 3, // source or recpient + 3, // source 2, // authority 4, // 0, - token_pool_pda_bump, + compressed_token_pool_pda_bump, )), delegate_is_set: false, method_used: true, @@ -482,7 +480,10 @@ pub fn create_spl_to_ctoken_transfer_instruction( let inputs = Transfer2Inputs { validity_proof: ValidityProof::default(), transfer_config: Transfer2Config::default().filter_zero_amount_outputs(), - meta_config: Transfer2AccountsMetaConfig::new_decompressed_accounts_only(packed_accounts), + 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], @@ -495,31 +496,23 @@ pub fn create_spl_to_ctoken_transfer_instruction( #[allow(clippy::too_many_arguments)] #[profile] pub fn create_ctoken_to_spl_transfer_instruction( + payer: Pubkey, + authority: Pubkey, 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, + compressed_token_pool_pda: Pubkey, + compressed_token_pool_pda_bump: u8, + amount: u64, ) -> 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, - ), + AccountMeta::new_readonly(mint, false), // Mint (index 0) + AccountMeta::new(source_ctoken_account, false), // Source ctoken account (index 1) - writable + AccountMeta::new(destination_spl_token_account, false), // Destination SPL token account (index 2) - writable + AccountMeta::new_readonly(authority, true), // Authority (index 3) - signer + AccountMeta::new(compressed_token_pool_pda, false), // Token pool PDA (index 4) - writable + AccountMeta::new_readonly(spl_token_program, false), // SPL Token program (index 5) - needed for CPI ]; // First operation: compress from ctoken account to pool using compress_spl @@ -545,7 +538,7 @@ pub fn create_ctoken_to_spl_transfer_instruction( 2, // destination SPL token account index 4, // pool_account_index 0, // pool_index (TODO: make dynamic) - token_pool_pda_bump, + compressed_token_pool_pda_bump, )), delegate_is_set: false, method_used: true, @@ -555,7 +548,10 @@ pub fn create_ctoken_to_spl_transfer_instruction( let inputs = Transfer2Inputs { validity_proof: ValidityProof::default(), transfer_config: Transfer2Config::default().filter_zero_amount_outputs(), - meta_config: Transfer2AccountsMetaConfig::new_decompressed_accounts_only(packed_accounts), + 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], diff --git a/sdk-libs/compressed-token-sdk/src/compressible.rs b/sdk-libs/compressed-token-sdk/src/compressible.rs new file mode 100644 index 0000000000..fe4f42f906 --- /dev/null +++ b/sdk-libs/compressed-token-sdk/src/compressible.rs @@ -0,0 +1,429 @@ +#[cfg(feature = "anchor")] +use anchor_lang::{ + prelude::{InterfaceAccount, Signer}, + ToAccountInfo, +}; +use light_account_checks::AccountInfoTrait; +use light_ctoken_types::{ + instructions::transfer2::{ + Compression, CompressionMode, MultiInputTokenDataWithContext, MultiTokenTransferOutputData, + }, + COMPRESSED_TOKEN_PROGRAM_ID, COMPRESSIBLE_TOKEN_ACCOUNT_SIZE, +}; +use light_sdk::{ + compressible::{create_or_allocate_account, CompressibleConfig}, + constants::CPI_AUTHORITY_PDA_SEED, + cpi::{CpiAccountsSmall, CpiSigner}, + instruction::borsh_compat::ValidityProof, + AnchorDeserialize, AnchorSerialize, +}; +use solana_account_info::AccountInfo; +use solana_cpi::{invoke, invoke_signed}; +use solana_instruction::{AccountMeta, Instruction}; +use solana_pubkey::Pubkey; + +use crate::{ + account2::CTokenAccount2, + error::Result, + instructions::transfer2::{ + account_metas::Transfer2AccountsMetaConfig, create_transfer2_instruction, Transfer2Config, + Transfer2Inputs, + }, +}; + +/// Same as SPL-token discriminator +pub const CLOSE_TOKEN_ACCOUNT_DISCRIMINATOR: u8 = 9; + +#[derive(AnchorSerialize, AnchorDeserialize, Debug, Clone)] +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, + } +} + +/// Structure to hold token account data for batch compression +#[cfg(feature = "anchor")] +pub struct TokenAccountToCompress<'info> { + pub token_account: InterfaceAccount<'info, anchor_spl::token_interface::TokenAccount>, + pub signer_seeds: Vec>, +} + +fn add_or_get_index(vec: &mut Vec, item: T) -> u8 { + if let Some(idx) = vec.iter().position(|x| x == &item) { + idx as u8 + } else { + vec.push(item); + (vec.len() - 1) as u8 + } +} + +/// Input parameters for creating a token account with compressible extension +#[derive(Debug, Clone)] +pub struct InitializeCompressibleTokenAccount { + /// The account to be created + pub account_pubkey: Pubkey, + /// The mint for the token account + pub mint_pubkey: Pubkey, + /// The owner of the token account + pub owner_pubkey: Pubkey, + /// The authority that can close this account (in addition to owner) + pub rent_authority: Pubkey, + /// The recipient of lamports when the account is closed by rent authority + pub rent_recipient: Pubkey, + /// Number of slots that must pass before compression is allowed + pub slots_until_compression: u64, +} + +pub fn initialize_compressible_token_account( + inputs: InitializeCompressibleTokenAccount, +) -> Result { + // Format: [18, owner_pubkey_32_bytes, 0] + // Create compressible extension data manually + // Layout: [slots_until_compression: u64, rent_authority: 32 bytes, rent_recipient: 32 bytes] + let mut data = Vec::with_capacity(1 + 32 + 1 + 8 + 32 + 32); + data.push(18u8); // InitializeAccount3 opcode + data.extend_from_slice(&inputs.owner_pubkey.to_bytes()); + data.push(1); // Some option byte extension + data.extend_from_slice(&inputs.slots_until_compression.to_le_bytes()); + data.extend_from_slice(&inputs.rent_authority.to_bytes()); + data.extend_from_slice(&inputs.rent_recipient.to_bytes()); + + Ok(Instruction { + program_id: Pubkey::from(light_ctoken_types::COMPRESSED_TOKEN_PROGRAM_ID), + accounts: vec![ + solana_instruction::AccountMeta::new(inputs.account_pubkey, false), + solana_instruction::AccountMeta::new_readonly(inputs.mint_pubkey, false), + ], + data, + }) +} + +#[allow(clippy::too_many_arguments)] +#[cfg(feature = "anchor")] +pub fn create_compressible_token_account<'a>( + authority: &AccountInfo<'a>, + payer: &AccountInfo<'a>, + token_account: &AccountInfo<'a>, + mint_account: &AccountInfo<'a>, + system_program: &AccountInfo<'a>, + token_program: &AccountInfo<'a>, + signer_seeds: &[&[u8]], + rent_authority: &AccountInfo<'a>, + rent_recipient: &AccountInfo<'a>, + slots_until_compression: u64, +) -> std::result::Result<(), solana_program_error::ProgramError> { + use anchor_lang::ToAccountInfo; + use solana_cpi::invoke; + + let space = COMPRESSIBLE_TOKEN_ACCOUNT_SIZE as usize; + + create_or_allocate_account( + token_program.key, + payer.to_account_info(), + system_program.to_account_info(), + token_account.to_account_info(), + signer_seeds, + space, + )?; + + let init_ix = initialize_compressible_token_account(InitializeCompressibleTokenAccount { + account_pubkey: *token_account.key, + mint_pubkey: *mint_account.key, + owner_pubkey: *authority.key, + rent_authority: *rent_authority.key, + rent_recipient: *rent_recipient.key, + slots_until_compression, + })?; + + invoke( + &init_ix, + &[ + token_account.to_account_info(), + mint_account.to_account_info(), + authority.to_account_info(), + rent_authority.to_account_info(), + rent_recipient.to_account_info(), + ], + )?; + + Ok(()) +} + +/// CPI function to close a compressed token account +/// +/// # Arguments +/// * `token_account` - The token account to close (must have 0 balance) +/// * `destination` - The account to receive the lamports +/// * `authority` - The owner of the token account (must sign) +/// * `signer_seeds` - Optional signer seeds if calling from a PDA +pub fn close_compressed_token_account<'info>( + token_account: AccountInfo<'info>, + destination: AccountInfo<'info>, + authority: AccountInfo<'info>, + signer_seeds: Option<&[&[&[u8]]]>, +) -> std::result::Result<(), solana_program_error::ProgramError> { + let instruction_data = vec![CLOSE_TOKEN_ACCOUNT_DISCRIMINATOR]; + + let account_metas = vec![ + AccountMeta::new(token_account.pubkey(), false), // token_account (mutable) + AccountMeta::new(destination.pubkey(), false), // destination (mutable) + AccountMeta::new_readonly(authority.pubkey(), true), // authority (signer) + ]; + + let instruction = Instruction { + program_id: COMPRESSED_TOKEN_PROGRAM_ID.into(), + accounts: account_metas, + data: instruction_data, + }; + + let account_infos = vec![ + token_account.to_account_info(), + destination.to_account_info(), + authority.to_account_info(), + ]; + + if let Some(seeds) = signer_seeds { + invoke_signed(&instruction, &account_infos, seeds)?; + } else { + invoke(&instruction, &account_infos)?; + } + + Ok(()) +} + +/// Generic function to process token account compression +/// +/// This function handles the compression of a native SPL token account into a compressed token account. +/// It accepts signer seeds as a parameter, making it reusable for different PDA derivation schemes. +/// +/// # Arguments +/// * `fee_payer` - The account paying for transaction fees +/// * `authority` - The authority account (owner or delegate of the token account) +/// * `compressed_token_cpi_authority` - The CPI authority for the compressed token program +/// * `compressed_token_program` - The compressed token program +/// * `token_account` - The SPL token account to compress +/// * `config_account` - The compression configuration account +/// * `rent_recipient` - The account that will receive the reclaimed rent +/// * `remaining_accounts` - Additional accounts needed for the Light Protocol CPI +/// * `token_signer_seeds` - The signer seeds for the token account PDA (without bump) +/// +/// # Returns +/// * `Result<()>` - Success or error +#[allow(clippy::too_many_arguments)] +#[cfg(feature = "anchor")] +pub fn compress_and_close_token_account<'info>( + program_id: Pubkey, + fee_payer: &Signer<'info>, + token_account: InterfaceAccount<'info, anchor_spl::token_interface::TokenAccount>, + authority: &AccountInfo<'info>, + compressed_token_cpi_authority: &AccountInfo<'info>, + compressed_token_program: &AccountInfo<'info>, + config_account: &AccountInfo<'info>, + rent_recipient: &AccountInfo<'info>, + remaining_accounts: &[AccountInfo<'info>], + cpi_signer: CpiSigner, + token_signer_seeds: Vec>, +) -> std::result::Result<(), solana_program_error::ProgramError> { + compress_and_close_token_accounts( + program_id, + fee_payer, + authority, + compressed_token_cpi_authority, + compressed_token_program, + config_account, + rent_recipient, + remaining_accounts, + vec![TokenAccountToCompress { + token_account, + signer_seeds: token_signer_seeds, + }], + cpi_signer, + ) +} + +/// Compress and close multiple compressible token accounts in a single +/// instruction. +/// +/// All token accounts must be owned by the same program authority. +/// +/// # Arguments +/// * `program_id` - The program ID that owns the token accounts +/// * `fee_payer` - The account paying for transaction fees +/// * `authority` - The authority account (must be the same for all token +/// accounts) +/// * `compressed_token_cpi_authority` - The CPI authority for the compressed +/// token program +/// * `compressed_token_program` - The compressed token program +/// * `config_account` - The compression configuration account +/// * `rent_recipient` - The account that will receive the reclaimed rent +/// * `remaining_accounts` - Additional accounts needed for the Light Protocol +/// CPI +/// * `token_accounts_to_compress` - Vector of token accounts with their +/// respective signer seeds +/// * `cpi_signer` - The CPI signer for the program +/// +/// # Returns +/// * `Result<()>` - Success or error +#[allow(clippy::too_many_arguments)] +#[cfg(feature = "anchor")] +pub fn compress_and_close_token_accounts<'info>( + program_id: Pubkey, + fee_payer: &Signer<'info>, + authority: &AccountInfo<'info>, + compressed_token_cpi_authority: &AccountInfo<'info>, + compressed_token_program: &AccountInfo<'info>, + config_account: &AccountInfo<'info>, + rent_recipient: &AccountInfo<'info>, + remaining_accounts: &[AccountInfo<'info>], + token_accounts_to_compress: Vec>, + cpi_signer: CpiSigner, +) -> std::result::Result<(), solana_program_error::ProgramError> { + if token_accounts_to_compress.is_empty() { + return Ok(()); + } + + // TODO: consider removing this check. + let config = CompressibleConfig::load_checked(config_account, &program_id)?; + + // Verify rent recipient matches config + if rent_recipient.pubkey() != config.rent_recipient { + return Err(solana_program_error::ProgramError::InvalidAccountData); + } + + let cpi_accounts = CpiAccountsSmall::new(authority, remaining_accounts, cpi_signer); + + let mut account_metas: Vec = Vec::new(); + + // Fee payer (index 0) + account_metas.push(account_meta_from_account_info(&fee_payer.to_account_info())); + + // Pack token accounts + let mut ctoken_accounts = Vec::with_capacity(token_accounts_to_compress.len()); + for token_data in &token_accounts_to_compress { + let token_account = token_data.token_account.clone(); + + let seeds: Vec<&[u8]> = token_data + .signer_seeds + .iter() + .map(|s| s.as_slice()) + .collect(); + + let expected_token_account = Pubkey::create_program_address(&seeds, &program_id) + .map_err(|_| solana_program_error::ProgramError::InvalidSeeds)?; + + if token_account.to_account_info().key != &expected_token_account { + return Err(solana_program_error::ProgramError::InvalidAccountData); + } + + // MERKLE TREE OUTPUT QUEUE + let output_queue_index = add_or_get_index( + &mut account_metas, + AccountMeta { + pubkey: cpi_accounts.tree_accounts().unwrap()[0].pubkey(), + is_writable: true, + is_signer: false, + }, + ); + // TOKEN ACCOUNT + let token_account_index = add_or_get_index( + &mut account_metas, + account_meta_from_account_info(&token_account.to_account_info()), + ); + + // MINT + let mint_index = add_or_get_index( + &mut account_metas, + AccountMeta { + pubkey: token_account.mint, + is_writable: false, + is_signer: false, + }, + ); + + // AUTHORITY + let authority_index = add_or_get_index( + &mut account_metas, + AccountMeta { + pubkey: cpi_accounts.authority().unwrap().pubkey(), + is_writable: false, + is_signer: true, + }, + ); + + // Create the compressed token account structure + let ctoken_account = CTokenAccount2 { + inputs: vec![], + output: MultiTokenTransferOutputData { + owner: token_account_index, + amount: token_account.amount, + merkle_tree: output_queue_index, + mint: mint_index, + version: 2, + delegate: 0, + has_delegate: false, + }, + compression: Some(Compression { + amount: token_account.amount, + mode: CompressionMode::Compress, + mint: mint_index, // Index of mint + source_or_recipient: token_account_index, // Index of token account + authority: authority_index, // Index of authority + pool_account_index: 0, // unused + pool_index: 0, // unused + bump: 0, // unused + }), + delegate_is_set: false, + method_used: false, + }; + + ctoken_accounts.push(ctoken_account); + } + + let ctoken_ix = create_transfer2_instruction(Transfer2Inputs { + validity_proof: ValidityProof::default().into(), + transfer_config: Transfer2Config::new().filter_zero_amount_outputs(), + meta_config: Transfer2AccountsMetaConfig::new(fee_payer.pubkey(), account_metas), + in_lamports: None, + out_lamports: None, + token_accounts: ctoken_accounts, + }) + .map_err(solana_program_error::ProgramError::from)?; + + // Account Infos + let mut all_account_infos = vec![ + fee_payer.to_account_info(), + compressed_token_cpi_authority.to_account_info(), + compressed_token_program.to_account_info(), + config_account.to_account_info(), + ]; + all_account_infos.extend(cpi_accounts.to_account_infos()); + + // authority + let authority_seeds = &[CPI_AUTHORITY_PDA_SEED, &[cpi_signer.bump]]; + + invoke_signed( + &ctoken_ix, + all_account_infos.as_slice(), + &[authority_seeds.as_slice()], + )?; + + // Clean up token accounts + for token_data in token_accounts_to_compress { + close_compressed_token_account( + token_data.token_account.to_account_info(), + rent_recipient.to_account_info(), + cpi_accounts.authority().unwrap().to_account_info(), + Some(&[authority_seeds]), + )?; + } + + Ok(()) +} 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 3c26db0efd..e1d7ff151f 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 @@ -3,47 +3,6 @@ use solana_pubkey::Pubkey; use crate::error::Result; -/// Input parameters for creating a token account with compressible extension -#[derive(Debug, Clone)] -pub struct CreateCompressibleTokenAccount { - /// The account to be created - pub account_pubkey: Pubkey, - /// The mint for the token account - pub mint_pubkey: Pubkey, - /// The owner of the token account - pub owner_pubkey: Pubkey, - /// The authority that can close this account (in addition to owner) - pub rent_authority: Pubkey, - /// The recipient of lamports when the account is closed by rent authority - pub rent_recipient: Pubkey, - /// Number of slots that must pass before compression is allowed - pub slots_until_compression: u64, -} - -pub fn create_compressible_token_account( - inputs: CreateCompressibleTokenAccount, -) -> Result { - // Format: [18, owner_pubkey_32_bytes, 0] - // Create compressible extension data manually - // Layout: [slots_until_compression: u64, rent_authority: 32 bytes, rent_recipient: 32 bytes] - let mut data = Vec::with_capacity(1 + 32 + 1 + 8 + 32 + 32); - data.push(18u8); // InitializeAccount3 opcode - data.extend_from_slice(&inputs.owner_pubkey.to_bytes()); - data.push(1); // Some option byte extension - data.extend_from_slice(&inputs.slots_until_compression.to_le_bytes()); - data.extend_from_slice(&inputs.rent_authority.to_bytes()); - data.extend_from_slice(&inputs.rent_recipient.to_bytes()); - - Ok(Instruction { - program_id: Pubkey::from(light_ctoken_types::COMPRESSED_TOKEN_PROGRAM_ID), - accounts: vec![ - solana_instruction::AccountMeta::new(inputs.account_pubkey, false), - solana_instruction::AccountMeta::new_readonly(inputs.mint_pubkey, false), - ], - data, - }) -} - pub fn create_token_account( account_pubkey: Pubkey, mint_pubkey: Pubkey, 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 f2223df73c..90f87de5c3 100644 --- a/sdk-libs/compressed-token-sdk/src/instructions/decompress_full.rs +++ b/sdk-libs/compressed-token-sdk/src/instructions/decompress_full.rs @@ -5,8 +5,11 @@ use light_ctoken_types::instructions::transfer2::MultiInputTokenDataWithContext; use light_profiler::profile; use light_sdk::{ error::LightSdkError, - instruction::{AccountMetasVec, PackedAccounts, PackedStateTreeInfo, SystemAccountMetaConfig}, - token::TokenData, + instruction::{ + account_meta::CompressedAccountMetaNoLamportsNoAddress, AccountMetasVec, PackedAccounts, + PackedStateTreeInfo, SystemAccountMetaConfig, + }, + token::{InputTokenDataCompressible, TokenData}, }; use solana_account_info::AccountInfo; use solana_instruction::{AccountMeta, Instruction}; @@ -33,6 +36,43 @@ pub struct DecompressFullIndices { pub destination_index: u8, // Destination ctoken Solana account (must exist) } +impl + From<( + InputTokenDataCompressible, + CompressedAccountMetaNoLamportsNoAddress, + u8, + )> for DecompressFullIndices +{ + fn from( + (token_data, meta, destination_index): ( + InputTokenDataCompressible, + CompressedAccountMetaNoLamportsNoAddress, + u8, + ), + ) -> Self { + let source = MultiInputTokenDataWithContext { + owner: token_data.owner, + amount: token_data.amount, + has_delegate: token_data.has_delegate, + delegate: token_data.delegate, + mint: token_data.mint, + version: token_data.version, + merkle_context: PackedMerkleContext { + merkle_tree_pubkey_index: meta.tree_info.merkle_tree_pubkey_index, + queue_pubkey_index: meta.tree_info.queue_pubkey_index, + leaf_index: meta.tree_info.leaf_index, + prove_by_index: meta.tree_info.prove_by_index, + }, + root_index: meta.tree_info.root_index, + }; + + DecompressFullIndices { + source, + destination_index, + } + } +} + /// Decompress full balance from compressed token accounts with pre-computed indices /// /// # Arguments @@ -55,7 +95,6 @@ pub fn decompress_full_ctoken_accounts_with_indices<'info>( if indices.is_empty() { return Err(TokenSdkError::InvalidAccountData); } - // Process each set of indices let mut token_accounts = Vec::with_capacity(indices.len()); @@ -73,11 +112,18 @@ pub fn decompress_full_ctoken_accounts_with_indices<'info>( } // Convert packed_accounts to AccountMetas + // + // TODO: we may have to add conditional delegate signers for delegate + // support via CPI. + let signer_indices: Vec = indices + .iter() + .map(|idx| idx.source.owner) // owner is always a signer + .collect(); 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_indices.contains(&(i as u8)), is_writable: info.is_writable, }); } diff --git a/sdk-libs/compressed-token-sdk/src/instructions/decompressed_transfer.rs b/sdk-libs/compressed-token-sdk/src/instructions/decompressed_transfer.rs new file mode 100644 index 0000000000..3746230bcb --- /dev/null +++ b/sdk-libs/compressed-token-sdk/src/instructions/decompressed_transfer.rs @@ -0,0 +1,72 @@ +use light_ctoken_types::COMPRESSED_TOKEN_PROGRAM_ID; +use solana_account_info::AccountInfo; +use solana_cpi::{invoke, invoke_signed}; +use solana_instruction::{AccountMeta, Instruction}; +use solana_program_error::ProgramError; +use solana_pubkey::Pubkey; + +/// Create a decompressed token transfer instruction. +/// This creates an instruction that uses discriminator 3 (DecompressedTransfer) to perform +/// SPL token transfers on decompressed compressed token accounts. +/// +/// # Arguments +/// * `source` - Source token account +/// * `destination` - Destination token account +/// * `amount` - Amount to transfer +/// * `authority` - Authority pubkey +/// +/// # Returns +/// `Instruction` +pub fn create_decompressed_token_transfer_instruction( + source: Pubkey, + destination: Pubkey, + amount: u64, + authority: Pubkey, +) -> Instruction { + Instruction { + program_id: Pubkey::from(COMPRESSED_TOKEN_PROGRAM_ID), + accounts: vec![ + AccountMeta::new(source, false), + AccountMeta::new(destination, false), + AccountMeta::new_readonly(authority, true), + ], + data: { + let mut data = vec![3u8]; // DecompressedTransfer discriminator + data.push(3u8); // SPL Transfer discriminator + data.extend_from_slice(&amount.to_le_bytes()); + data + }, + } +} + +/// Transfer decompressed ctokens +pub fn transfer<'info>( + from: &AccountInfo<'info>, + to: &AccountInfo<'info>, + authority: &AccountInfo<'info>, + amount: u64, +) -> Result<(), ProgramError> { + let ix = + create_decompressed_token_transfer_instruction(*from.key, *to.key, amount, *authority.key); + + // Return Result directly, as is best practice for CPI helpers in native Solana programs. + invoke(&ix, &[from.clone(), to.clone(), authority.clone()]) +} + +/// Transfer decompressed ctokens with signer seeds +pub fn transfer_signed<'info>( + from: &AccountInfo<'info>, + to: &AccountInfo<'info>, + authority: &AccountInfo<'info>, + amount: u64, + signer_seeds: &[&[&[u8]]], +) -> Result<(), ProgramError> { + let ix = + create_decompressed_token_transfer_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/mint_action/instruction.rs b/sdk-libs/compressed-token-sdk/src/instructions/mint_action/instruction.rs index d16c969b79..3b15560b36 100644 --- a/sdk-libs/compressed-token-sdk/src/instructions/mint_action/instruction.rs +++ b/sdk-libs/compressed-token-sdk/src/instructions/mint_action/instruction.rs @@ -8,7 +8,6 @@ use light_ctoken_types::{ }, }; use solana_instruction::Instruction; -use solana_msg::msg; use solana_pubkey::Pubkey; use crate::{ @@ -41,6 +40,38 @@ pub struct MintActionInputs { pub token_pool: Option, } +impl MintActionInputs { + /// Creates a new MintActionInputs for creating a compressed mint + #[allow(clippy::too_many_arguments)] + pub fn new_for_create_mint( + compressed_mint_inputs: CompressedMintWithContext, + actions: Vec, + output_queue: Pubkey, + address_tree_pubkey: Pubkey, + mint_seed: Pubkey, + mint_bump: Option, + authority: Pubkey, + payer: Pubkey, + proof: Option, + ) -> Self { + Self { + compressed_mint_inputs, + mint_seed, + create_mint: true, // Always true for create mint + mint_bump, + authority, + payer, + proof, + actions, + address_tree_pubkey, + input_queue: None, // No input queue when creating new mint + output_queue, + tokens_out_queue: Some(output_queue), // Default to None, can be set separately if needed + token_pool: None, // Default to None, can be set separately if needed + } + } +} + /// High-level action types for the mint action instruction #[derive(Debug, Clone, AnchorDeserialize, AnchorSerialize)] pub enum MintActionType { @@ -255,7 +286,7 @@ pub fn create_mint_action_cpi( // Get account metas (before moving compressed_mint_inputs) let accounts = get_mint_action_instruction_account_metas(meta_config, &input.compressed_mint_inputs); - msg!("account metas {:?}", accounts); + let instruction_data = MintActionCompressedInstructionData { create_mint, mint_bump, diff --git a/sdk-libs/compressed-token-sdk/src/instructions/mod.rs b/sdk-libs/compressed-token-sdk/src/instructions/mod.rs index 876025f5d7..23bd473a85 100644 --- a/sdk-libs/compressed-token-sdk/src/instructions/mod.rs +++ b/sdk-libs/compressed-token-sdk/src/instructions/mod.rs @@ -8,6 +8,7 @@ mod create_spl_mint; pub mod create_token_account; pub mod ctoken_accounts; pub mod decompress_full; +pub mod decompressed_transfer; pub mod mint_action; pub mod mint_to_compressed; pub mod transfer; @@ -30,11 +31,12 @@ pub use compress_and_close::{ 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, -}; +pub use create_token_account::create_token_account; pub use ctoken_accounts::*; pub use decompress_full::{decompress_full_ctoken_accounts_with_indices, DecompressFullIndices}; +pub use decompressed_transfer::{ + create_decompressed_token_transfer_instruction, transfer, transfer_signed, +}; pub use mint_action::{ create_mint_action, create_mint_action_cpi, get_mint_action_instruction_account_metas, get_mint_action_instruction_account_metas_cpi_write, mint_action_cpi_write, MintActionInputs, diff --git a/sdk-libs/compressed-token-sdk/src/instructions/transfer/account_infos.rs b/sdk-libs/compressed-token-sdk/src/instructions/transfer/account_infos.rs index c67187d332..4878e90e1e 100644 --- a/sdk-libs/compressed-token-sdk/src/instructions/transfer/account_infos.rs +++ b/sdk-libs/compressed-token-sdk/src/instructions/transfer/account_infos.rs @@ -65,6 +65,7 @@ impl<'info, const N: usize> TransferAccountInfos<'_, 'info, N> { let account_infos = self.into_account_infos(); for (account_meta, account_info) in ix.accounts.iter().zip(account_infos.iter()) { if account_meta.pubkey != *account_info.key { + msg!("account info and meta don't match."); msg!("account meta {:?}", account_meta); msg!("account info {:?}", account_info); diff --git a/sdk-libs/compressed-token-sdk/src/instructions/transfer2/account_metas.rs b/sdk-libs/compressed-token-sdk/src/instructions/transfer2/account_metas.rs index 1cbc7fb0c5..44d441cefd 100644 --- a/sdk-libs/compressed-token-sdk/src/instructions/transfer2/account_metas.rs +++ b/sdk-libs/compressed-token-sdk/src/instructions/transfer2/account_metas.rs @@ -29,10 +29,27 @@ impl Transfer2AccountsMetaConfig { packed_accounts: Some(packed_accounts), } } - - pub fn new_decompressed_accounts_only(packed_accounts: Vec) -> Self { + pub fn new_with_cpi_context( + fee_payer: Pubkey, + packed_accounts: Vec, + cpi_context: Pubkey, + ) -> Self { Self { - fee_payer: None, + fee_payer: Some(fee_payer), + decompressed_accounts_only: false, + sol_pool_pda: None, + sol_decompression_recipient: None, + cpi_context: Some(cpi_context), + with_sol_pool: false, + packed_accounts: Some(packed_accounts), + } + } + pub fn new_decompressed_accounts_only( + _fee_payer: Pubkey, + packed_accounts: Vec, + ) -> Self { + Self { + fee_payer: None, // TODO: make it some once we add fee_per_write! sol_pool_pda: None, sol_decompression_recipient: None, cpi_context: None, @@ -48,6 +65,7 @@ pub fn get_transfer2_instruction_account_metas( config: Transfer2AccountsMetaConfig, ) -> Vec { let default_pubkeys = CTokenDefaultAccounts::default(); + let packed_accounts_len = if let Some(packed_accounts) = config.packed_accounts.as_ref() { packed_accounts.len() } else { @@ -104,6 +122,7 @@ pub fn get_transfer2_instruction_account_metas( false, )); } + // always add packed accounts if let Some(packed_accounts) = config.packed_accounts.as_ref() { for account in packed_accounts { metas.push(account.clone()); diff --git a/sdk-libs/compressed-token-sdk/src/instructions/transfer2/decompressed_transfer.rs b/sdk-libs/compressed-token-sdk/src/instructions/transfer2/decompressed_transfer.rs new file mode 100644 index 0000000000..c2b4ddddfc --- /dev/null +++ b/sdk-libs/compressed-token-sdk/src/instructions/transfer2/decompressed_transfer.rs @@ -0,0 +1,195 @@ +use solana_account_info::AccountInfo; +use solana_cpi::{invoke, invoke_signed}; +use solana_program_error::ProgramError; + +use crate::{ + account2::{ + create_ctoken_to_spl_transfer_instruction, create_spl_to_ctoken_transfer_instruction, + }, + error::TokenSdkError, +}; + +/// Transfer SPL tokens to compressed tokens +/// +/// This function creates the instruction and immediately invokes it. +/// Similar to SPL Token's transfer wrapper functions. +#[allow(clippy::too_many_arguments)] +pub fn transfer_spl_to_ctoken<'info>( + payer: AccountInfo<'info>, + authority: AccountInfo<'info>, + source_spl_token_account: AccountInfo<'info>, + destination_ctoken_account: AccountInfo<'info>, + mint: AccountInfo<'info>, + spl_token_program: AccountInfo<'info>, + compressed_token_pool_pda: AccountInfo<'info>, + compressed_token_pool_pda_bump: u8, + compressed_token_program_authority: AccountInfo<'info>, + amount: u64, +) -> Result<(), ProgramError> { + let instruction = create_spl_to_ctoken_transfer_instruction( + *payer.key, + *authority.key, + *source_spl_token_account.key, + *destination_ctoken_account.key, + *mint.key, + *spl_token_program.key, + *compressed_token_pool_pda.key, + compressed_token_pool_pda_bump, + amount, + ) + .map_err(|_| ProgramError::InvalidInstructionData)?; + + // let mut account_infos = remaining_accounts.to_vec(); + let account_infos = vec![ + authority.clone(), + compressed_token_program_authority, + mint, // Index 0: Mint + destination_ctoken_account, // Index 1: Destination owner + authority, // Index 2: Authority (signer) + source_spl_token_account, // Index 3: Source SPL token account + compressed_token_pool_pda, // Index 4: Token pool PDA + spl_token_program, // Index 5: SPL Token program + ]; + + invoke(&instruction, &account_infos)?; + Ok(()) +} + +// TODO: must test this. +/// Transfer SPL tokens to compressed tokens via CPI signer. +/// +/// This function creates the instruction and invokes it with the provided +/// signer seeds. +#[allow(clippy::too_many_arguments)] +pub fn transfer_spl_to_ctoken_signed<'info>( + payer: AccountInfo<'info>, + authority: AccountInfo<'info>, + source_spl_token_account: AccountInfo<'info>, + destination_ctoken_account: AccountInfo<'info>, + mint: AccountInfo<'info>, + spl_token_program: AccountInfo<'info>, + compressed_token_pool_pda: AccountInfo<'info>, + compressed_token_pool_pda_bump: u8, + compressed_token_program_authority: AccountInfo<'info>, + amount: u64, + signer_seeds: &[&[&[u8]]], +) -> Result<(), ProgramError> { + let instruction = create_spl_to_ctoken_transfer_instruction( + *payer.key, + *authority.key, + *source_spl_token_account.key, + *destination_ctoken_account.key, + *mint.key, + *spl_token_program.key, + *compressed_token_pool_pda.key, + compressed_token_pool_pda_bump, + amount, + ) + .map_err(|_| TokenSdkError::MethodUsed)?; + + let account_infos = vec![ + payer.clone(), + compressed_token_program_authority, + mint, // Index 0: Mint + destination_ctoken_account, // Index 1: Destination owner + authority, // Index 2: Authority (signer) + source_spl_token_account, // Index 3: Source SPL token account + compressed_token_pool_pda, // Index 4: Token pool PDA + spl_token_program, // Index 5: SPL Token program + ]; + + invoke_signed(&instruction, &account_infos, signer_seeds) + .map_err(|_| TokenSdkError::MethodUsed)?; + Ok(()) +} + +// TODO: TEST. +/// Transfer compressed tokens to SPL tokens +/// +/// This function creates the instruction and invokes it. +#[allow(clippy::too_many_arguments)] +pub fn transfer_ctoken_to_spl<'info>( + payer: AccountInfo<'info>, + authority: AccountInfo<'info>, + source_ctoken_account: AccountInfo<'info>, + destination_spl_token_account: AccountInfo<'info>, + mint: AccountInfo<'info>, + spl_token_program: AccountInfo<'info>, + compressed_token_pool_pda: AccountInfo<'info>, + compressed_token_pool_pda_bump: u8, + compressed_token_program_authority: AccountInfo<'info>, + amount: u64, +) -> Result<(), ProgramError> { + let instruction = create_ctoken_to_spl_transfer_instruction( + *payer.key, + *authority.key, + *source_ctoken_account.key, + *destination_spl_token_account.key, + *mint.key, + *spl_token_program.key, + *compressed_token_pool_pda.key, + compressed_token_pool_pda_bump, + amount, + ) + .map_err(|_| ProgramError::InvalidInstructionData)?; + + let account_infos = vec![ + authority.clone(), + compressed_token_program_authority, + mint, // Index 0: Mint + destination_spl_token_account, // Index 1: Destination owner + authority, // Index 2: Authority (signer) + source_ctoken_account, // Index 3: Source SPL token account + compressed_token_pool_pda, // Index 4: Token pool PDA + spl_token_program, // Index 5: SPL Token program + ]; + + invoke(&instruction, &account_infos)?; + Ok(()) +} + +/// Transfer compressed tokens to SPL tokens via CPI signer. +/// +/// This function creates the instruction and invokes it with the provided +/// signer seeds. +#[allow(clippy::too_many_arguments)] +pub fn transfer_ctoken_to_spl_signed<'info>( + payer: AccountInfo<'info>, + authority: AccountInfo<'info>, + source_ctoken_account: AccountInfo<'info>, + destination_spl_token_account: AccountInfo<'info>, + mint: AccountInfo<'info>, + spl_token_program: AccountInfo<'info>, + compressed_token_pool_pda: AccountInfo<'info>, + compressed_token_pool_pda_bump: u8, + compressed_token_program_authority: AccountInfo<'info>, + amount: u64, + signer_seeds: &[&[&[u8]]], +) -> Result<(), ProgramError> { + let instruction = create_ctoken_to_spl_transfer_instruction( + *payer.key, + *authority.key, + *source_ctoken_account.key, + *destination_spl_token_account.key, + *mint.key, + *spl_token_program.key, + *compressed_token_pool_pda.key, + compressed_token_pool_pda_bump, + amount, + ) + .map_err(|_| ProgramError::InvalidInstructionData)?; + + let account_infos = vec![ + payer.clone(), + compressed_token_program_authority, + mint, // Index 0: Mint + destination_spl_token_account, // Index 1: Destination owner + authority, // Index 2: Authority (signer) + source_ctoken_account, // Index 3: Source SPL token account + compressed_token_pool_pda, // Index 4: Token pool PDA + spl_token_program, // Index 5: SPL Token program + ]; + + invoke_signed(&instruction, &account_infos, signer_seeds)?; + Ok(()) +} diff --git a/sdk-libs/compressed-token-sdk/src/instructions/transfer2/mod.rs b/sdk-libs/compressed-token-sdk/src/instructions/transfer2/mod.rs index f51f2f2ad4..78e2ed2321 100644 --- a/sdk-libs/compressed-token-sdk/src/instructions/transfer2/mod.rs +++ b/sdk-libs/compressed-token-sdk/src/instructions/transfer2/mod.rs @@ -1,6 +1,8 @@ pub mod account_metas; pub mod cpi_helpers; +pub mod decompressed_transfer; pub mod instruction; pub use cpi_helpers::*; +pub use decompressed_transfer::*; pub use instruction::*; 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..dad0285273 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 @@ -20,7 +20,6 @@ pub fn get_update_compressed_mint_instruction_account_metas( config: UpdateCompressedMintMetaConfig, ) -> Vec { let default_pubkeys = CTokenDefaultAccounts::default(); - let mut metas = Vec::new(); // First two accounts are static non-CPI accounts as expected by CPI_ACCOUNTS_OFFSET = 2 diff --git a/sdk-libs/compressed-token-sdk/src/lib.rs b/sdk-libs/compressed-token-sdk/src/lib.rs index 45ac38becd..6f336f0898 100644 --- a/sdk-libs/compressed-token-sdk/src/lib.rs +++ b/sdk-libs/compressed-token-sdk/src/lib.rs @@ -6,9 +6,12 @@ pub mod token_metadata_ui; pub mod token_pool; pub mod utils; +pub mod compressible; + // Conditional anchor re-exports #[cfg(feature = "anchor")] use anchor_lang::{AnchorDeserialize, AnchorSerialize}; #[cfg(not(feature = "anchor"))] use borsh::{BorshDeserialize as AnchorDeserialize, BorshSerialize as AnchorSerialize}; +pub use compressible::*; pub use light_compressed_token_types::*; diff --git a/sdk-libs/compressed-token-types/src/token_data.rs b/sdk-libs/compressed-token-types/src/token_data.rs index b126d6582f..988f1f77bc 100644 --- a/sdk-libs/compressed-token-types/src/token_data.rs +++ b/sdk-libs/compressed-token-types/src/token_data.rs @@ -1,13 +1,13 @@ -use borsh::{BorshDeserialize, BorshSerialize}; +use crate::{AnchorDeserialize, AnchorSerialize}; -#[derive(Clone, Copy, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, AnchorSerialize, AnchorDeserialize)] #[repr(u8)] pub enum AccountState { Initialized, Frozen, } -#[derive(Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize, Clone)] +#[derive(Debug, PartialEq, Eq, AnchorSerialize, AnchorDeserialize, Clone)] pub struct TokenData { /// The mint associated with this account pub mint: [u8; 32], diff --git a/sdk-libs/compressible-client/Cargo.toml b/sdk-libs/compressible-client/Cargo.toml new file mode 100644 index 0000000000..9f3dbb31cb --- /dev/null +++ b/sdk-libs/compressible-client/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "light-compressible-client" +version = "0.13.1" +edition = "2021" +license = "Apache-2.0" +repository = "https://github.com/lightprotocol/light-protocol" +description = "Client instruction builders for Light Protocol compressible accounts" + +[features] +anchor = ["anchor-lang", "light-sdk/anchor"] + +[dependencies] +# Solana dependencies +solana-instruction = { workspace = true } +solana-pubkey = { workspace = true } +solana-account = { workspace = true } + +# Light Protocol dependencies +light-client = { workspace = true, features = ["v2"] } +light-sdk = { workspace = true, features = ["v2"] } + +# Conditional dependencies +anchor-lang = { workspace = true, features = ["idl-build"], optional = true } +borsh = { workspace = true } + +# External dependencies +thiserror = { workspace = true } diff --git a/sdk-libs/compressible-client/examples/enhanced_usage.rs b/sdk-libs/compressible-client/examples/enhanced_usage.rs new file mode 100644 index 0000000000..a1d3c47cd5 --- /dev/null +++ b/sdk-libs/compressible-client/examples/enhanced_usage.rs @@ -0,0 +1,138 @@ +/// Example demonstrating the enhanced client helper with additional accounts +/// +/// This shows how to use the new decompress_accounts_idempotent function +/// for programs that need additional accounts for seed derivation + +use light_compressible_client::CompressibleInstruction; +use light_client::indexer::{CompressedAccount, ValidityProofWithContext, TreeInfo}; +use solana_pubkey::Pubkey; + +// Example: Using the enhanced client helper for a program like Raydium +// that needs additional accounts (amm_config, token_mints) for seed derivation + +async fn decompress_raydium_accounts_example( + program_id: &Pubkey, + fee_payer: &Pubkey, + rent_payer: &Pubkey, + + // PDA accounts to decompress into + pool_state_pda: &Pubkey, + observation_state_pda: &Pubkey, + + // Additional accounts required for seed derivation + amm_config: &Pubkey, + token_0_mint: &Pubkey, + token_1_mint: &Pubkey, + + // Compressed account data + compressed_pool_state: CompressedAccount, + compressed_observation_state: CompressedAccount, + pool_state_data: PoolStateVariant, + observation_state_data: ObservationStateVariant, + + // Proof data + validity_proof_with_context: ValidityProofWithContext, + output_state_tree_info: TreeInfo, +) -> Result> { + + // 🎉 Use the enhanced client helper with additional accounts + let instruction = CompressibleInstruction::decompress_accounts_idempotent( + program_id, + &CompressibleInstruction::DECOMPRESS_ACCOUNTS_IDEMPOTENT_DISCRIMINATOR, + fee_payer, + rent_payer, + + // PDAs to decompress into (same order as compressed_accounts) + &[*pool_state_pda, *observation_state_pda], + + // Compressed accounts with their data + &[ + (compressed_pool_state, pool_state_data), + (compressed_observation_state, observation_state_data), + ], + + // 🎉 Additional accounts needed for seed derivation + // These will be added to the DecompressAccountsIdempotent struct automatically + &[*amm_config, *token_0_mint, *token_1_mint], + + validity_proof_with_context, + output_state_tree_info, + )?; + + Ok(instruction) +} + +// For programs that don't need additional accounts (like anchor-compressible-derived) +async fn decompress_simple_accounts_example( + program_id: &Pubkey, + fee_payer: &Pubkey, + rent_payer: &Pubkey, + user_record_pda: &Pubkey, + compressed_user_record: CompressedAccount, + user_record_data: UserRecordVariant, + validity_proof_with_context: ValidityProofWithContext, + output_state_tree_info: TreeInfo, +) -> Result> { + + // 🎉 Use the simple version for backward compatibility + let instruction = CompressibleInstruction::decompress_accounts_idempotent_simple( + program_id, + &CompressibleInstruction::DECOMPRESS_ACCOUNTS_IDEMPOTENT_DISCRIMINATOR, + fee_payer, + rent_payer, + &[*user_record_pda], + &[(compressed_user_record, user_record_data)], + validity_proof_with_context, + output_state_tree_info, + )?; + + Ok(instruction) +} + +// Placeholder types for the example +#[derive(Clone, Debug)] +pub struct PoolStateVariant; + +#[derive(Clone, Debug)] +pub struct ObservationStateVariant; + +#[derive(Clone, Debug)] +pub struct UserRecordVariant; + +// Implement Pack trait for the example types +impl light_sdk::compressible::Pack for PoolStateVariant { + type Packed = Self; + fn pack(&self, _remaining_accounts: &mut light_sdk::instruction::PackedAccounts) -> Self::Packed { + self.clone() + } +} + +impl light_sdk::compressible::Pack for ObservationStateVariant { + type Packed = Self; + fn pack(&self, _remaining_accounts: &mut light_sdk::instruction::PackedAccounts) -> Self::Packed { + self.clone() + } +} + +impl light_sdk::compressible::Pack for UserRecordVariant { + type Packed = Self; + fn pack(&self, _remaining_accounts: &mut light_sdk::instruction::PackedAccounts) -> Self::Packed { + self.clone() + } +} + +/// Summary of the enhanced client helper: +/// +/// 1. **decompress_accounts_idempotent()** - Enhanced version with additional_accounts parameter +/// - Use for programs with complex seed derivation (like Raydium) +/// - Pass additional accounts needed for seed derivation +/// +/// 2. **decompress_accounts_idempotent_simple()** - Backward-compatible version +/// - Use for programs with simple seed derivation (like anchor-compressible-derived) +/// - No additional accounts needed +/// +/// 3. **Automatic account struct generation** - The macro now auto-generates +/// DecompressAccountsIdempotent with the required additional accounts +/// +/// 4. **Abstracts complexity** - Client developers don't need to worry about +/// packing, account ordering, or instruction data serialization diff --git a/sdk-libs/compressible-client/examples/pack_trait_usage.rs b/sdk-libs/compressible-client/examples/pack_trait_usage.rs new file mode 100644 index 0000000000..373f44e212 --- /dev/null +++ b/sdk-libs/compressible-client/examples/pack_trait_usage.rs @@ -0,0 +1,106 @@ +// Example showing how to implement the Pack trait for custom types + +use anchor_lang::{AnchorDeserialize, AnchorSerialize}; +use light_compressible_client::{Pack, PackedAccounts}; +use solana_pubkey::Pubkey; + +// Original data structure with Pubkeys +#[derive(Debug, Clone, AnchorSerialize, AnchorDeserialize)] +pub struct UserRecord { + pub owner: Pubkey, // 32 bytes + pub delegate: Pubkey, // 32 bytes + pub name: String, // Variable + pub score: u64, // 8 bytes +} + +// Packed version with u8 indices instead of Pubkeys +#[derive(Debug, Clone, AnchorSerialize, AnchorDeserialize)] +pub struct PackedUserRecord { + pub owner: u8, // 1 byte (index into remaining_accounts) + pub delegate: u8, // 1 byte (index into remaining_accounts) + pub name: String, // Stays as-is + pub score: u64, // Stays as-is +} + +// Implement Pack trait for UserRecord +impl Pack for UserRecord { + type Packed = PackedUserRecord; + + fn pack(&self, remaining_accounts: &mut PackedAccounts) -> Self::Packed { + PackedUserRecord { + owner: remaining_accounts.insert_or_get(self.owner), + delegate: remaining_accounts.insert_or_get(self.delegate), + name: self.name.clone(), + score: self.score, + } + } +} + +// Example with variant wrapper (for token accounts) +#[derive(Debug, Clone, Copy, AnchorSerialize, AnchorDeserialize)] +pub enum AccountVariant { + Standard = 0, + Premium = 1, +} + +// Wrapper that combines variant with data +#[derive(Debug, Clone, AnchorSerialize, AnchorDeserialize)] +pub struct AccountWithVariant { + pub variant: AccountVariant, + pub data: UserRecord, +} + +// Packed version +#[derive(Debug, Clone, AnchorSerialize, AnchorDeserialize)] +pub struct PackedAccountWithVariant { + pub variant: AccountVariant, // Variant stays unchanged + pub data: PackedUserRecord, // Data gets packed +} + +// Pack implementation for the wrapper +impl Pack for AccountWithVariant { + type Packed = PackedAccountWithVariant; + + fn pack(&self, remaining_accounts: &mut PackedAccounts) -> Self::Packed { + PackedAccountWithVariant { + variant: self.variant, // Variant is copied as-is + data: self.data.pack(remaining_accounts), // Data is packed + } + } +} + +// For simple types without Pubkeys, you can use identity packing +#[derive(Debug, Clone, AnchorSerialize, AnchorDeserialize)] +pub struct SimpleData { + pub counter: u64, + pub active: bool, +} + +// Identity pack - returns self +impl Pack for SimpleData { + type Packed = Self; + + fn pack(&self, _remaining_accounts: &mut PackedAccounts) -> Self::Packed { + self.clone() + } +} + +fn main() { + // Example usage + let mut remaining_accounts = PackedAccounts::default(); + + let user_record = UserRecord { + owner: Pubkey::new_unique(), + delegate: Pubkey::new_unique(), + name: "Alice".to_string(), + score: 100, + }; + + // Pack the user record + let packed = user_record.pack(&mut remaining_accounts); + println!("Packed: {:?}", packed); + + // The remaining_accounts now contains the Pubkeys + let (account_metas, _, _) = remaining_accounts.to_account_metas(); + println!("Account metas: {} accounts", account_metas.len()); +} diff --git a/sdk-libs/compressible-client/src/account_fetcher.rs b/sdk-libs/compressible-client/src/account_fetcher.rs new file mode 100644 index 0000000000..cda4243a17 --- /dev/null +++ b/sdk-libs/compressible-client/src/account_fetcher.rs @@ -0,0 +1,233 @@ +//! Utilities for fetching compressible accounts from either compressed or on-chain storage. +//! +//! This module provides functionality to transparently fetch accounts regardless of +//! whether they are compressed or on-chain. + +use light_client::{ + indexer::{Indexer, TreeInfo}, + rpc::{Rpc, RpcError}, +}; +use light_sdk::address::v1::derive_address; +use solana_pubkey::Pubkey; +use thiserror::Error; + +#[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, + + #[error("Deserialization error: {0}")] + Deserialization(String), + + #[cfg(feature = "anchor")] + #[error("Anchor deserialization error: {0}")] + AnchorDeserialization(#[from] anchor_lang::error::Error), + + #[error("Borsh deserialization error: {0}")] + BorshDeserialization(#[from] std::io::Error), +} + +/// Fetch account from either compressed or on-chain storage. +/// +/// This function first checks if the account exists on-chain. If not found, +/// it derives the compressed address and fetches from compressed storage. +/// +/// # Arguments +/// +/// * `address` - The account address (PDA or regular address) +/// * `program_id` - The program that owns the account +/// * `address_tree_info` - The address tree information for compressed accounts +/// * `rpc` - An RPC client implementing both `Rpc` and `Indexer` traits +/// +/// # Returns +/// +/// Returns the account data as bytes, including the discriminator if present. +/// +/// # Example +/// +/// ```no_run +/// use light_compressible_client::account_fetcher::get_compressible_account_data; +/// use light_client::{ +/// indexer::TreeInfo, +/// rpc::{LightClient, LightClientConfig, Rpc}, +/// }; +/// use solana_pubkey::Pubkey; +/// +/// #[tokio::main] +/// async fn main() -> Result<(), Box> { +/// let mut rpc = LightClient::new(LightClientConfig::local()).await?; +/// +/// let address = Pubkey::new_unique(); +/// let program_id = Pubkey::new_unique(); +/// let address_tree_info = rpc.get_address_tree_v1(); +/// +/// let account_data = get_compressible_account_data( +/// &address, +/// &program_id, +/// &address_tree_info, +/// &mut rpc, +/// ).await?; +/// +/// Ok(()) +/// } +/// ``` +pub async fn get_compressible_account_data( + address: &Pubkey, + program_id: &Pubkey, + address_tree_info: &TreeInfo, + rpc: &mut R, +) -> Result, CompressibleAccountError> +where + R: Rpc + Indexer, +{ + // First check if account exists on-chain + if let Ok(Some(onchain_account)) = rpc.get_account(*address).await { + return Ok(onchain_account.data); + } + + // If not on-chain, check compressed storage + // Derive the compressed address using the account address as seed + let (compressed_address, _) = + derive_address(&[&address.to_bytes()], &address_tree_info.tree, program_id); + + let compressed_account = rpc + .get_compressed_account(compressed_address, None) + .await? + .value; + + let account_data = compressed_account + .data + .as_ref() + .ok_or(CompressibleAccountError::NoData)?; + + // Combine discriminator and data + let mut data_slice = + Vec::with_capacity(account_data.discriminator.len() + account_data.data.len()); + data_slice.extend_from_slice(&account_data.discriminator); + data_slice.extend_from_slice(&account_data.data); + + Ok(data_slice) +} + +#[cfg(feature = "anchor")] +/// Fetch and deserialize a compressible account using Anchor. +/// +/// This function combines fetching from either compressed or on-chain storage +/// with Anchor deserialization. +/// +/// # Arguments +/// +/// * `address` - The account address (PDA or regular address) +/// * `program_id` - The program that owns the account +/// * `address_tree_info` - The address tree information for compressed accounts +/// * `rpc` - An RPC client implementing both `Rpc` and `Indexer` traits +/// +/// # Type Parameters +/// +/// * `T` - The account type implementing `AccountDeserialize` +/// +/// # Example +/// +/// ```no_run +/// use light_compressible_client::account_fetcher::get_compressible_account; +/// use light_client::{ +/// indexer::TreeInfo, +/// rpc::{LightClient, LightClientConfig, Rpc}, +/// }; +/// use solana_pubkey::Pubkey; +/// use anchor_lang::AccountDeserialize; +/// +/// #[derive(AccountDeserialize)] +/// struct MyAccount { +/// pub data: u64, +/// } +/// +/// #[tokio::main] +/// async fn main() -> Result<(), Box> { +/// let mut rpc = LightClient::new(LightClientConfig::local()).await?; +/// +/// let address = Pubkey::new_unique(); +/// let program_id = Pubkey::new_unique(); +/// let address_tree_info = rpc.get_address_tree_v1(); +/// +/// let account: MyAccount = get_compressible_account( +/// &address, +/// &program_id, +/// &address_tree_info, +/// &mut rpc, +/// ).await?; +/// +/// Ok(()) +/// } +/// ``` +pub async fn get_compressible_account( + address: &Pubkey, + program_id: &Pubkey, + address_tree_info: &TreeInfo, + rpc: &mut R, +) -> Result +where + T: anchor_lang::AccountDeserialize, + R: Rpc + Indexer, +{ + let data = get_compressible_account_data(address, program_id, address_tree_info, rpc).await?; + + T::try_deserialize(&mut data.as_slice()) + .map_err(CompressibleAccountError::AnchorDeserialization) +} + +#[cfg(feature = "anchor")] +/// Deserialize an on-chain account using Anchor. +/// +/// This is a utility function that deserializes an already fetched account. +pub fn deserialize_anchor_account( + account: &solana_account::Account, +) -> Result +where + T: anchor_lang::AccountDeserialize, +{ + T::try_deserialize(&mut &account.data[..]) + .map_err(CompressibleAccountError::AnchorDeserialization) +} + +// pub async fn get_compressible_account( +// address: &Pubkey, +// program_id: &Pubkey, +// address_tree_info: &TreeInfo, +// light_client: &mut LightClient, +// rpc_client: &RpcClient, +// ) -> Result { +// // First check if account exists onchain +// if let Ok(onchain_account) = rpc_client.get_account(address) { +// return deserialize_anchor_account(&onchain_account); +// } + +// // If not onchain, check compressed storage +// let compressed_address = derive_address( +// &address.to_bytes(), +// &address_tree_info.tree.to_bytes(), +// &program_id.to_bytes(), +// ); + +// let compressed_account = light_client +// .get_compressed_account(compressed_address, None) +// .await? +// .value; + +// let account_data = compressed_account +// .data +// .as_ref() +// .ok_or_else(|| anyhow::anyhow!("Compressed account has no data"))?; + +// let mut data_slice = Vec::with_capacity(8 + account_data.data.len()); +// data_slice.extend_from_slice(&account_data.discriminator); +// data_slice.extend_from_slice(&account_data.data); + +// T::try_deserialize(&mut data_slice.as_slice()).map_err(Into::into) +// } diff --git a/sdk-libs/compressible-client/src/lib.rs b/sdk-libs/compressible-client/src/lib.rs new file mode 100644 index 0000000000..5c732f0d06 --- /dev/null +++ b/sdk-libs/compressible-client/src/lib.rs @@ -0,0 +1,493 @@ +pub mod account_fetcher; +#[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::{COMPRESSED_TOKEN_PROGRAM_CPI_AUTHORITY, COMPRESSED_TOKEN_PROGRAM_ID}, + instruction::{ + account_meta::CompressedAccountMetaNoLamportsNoAddress, PackedAccounts, + SystemAccountMetaConfig, ValidityProof, + }, +}; +use solana_account::Account; +use solana_instruction::{AccountMeta, Instruction}; +use solana_pubkey::Pubkey; + +/// Generic instruction data for initialize config +/// Note: Real programs should use their specific instruction format +#[derive(AnchorSerialize, AnchorDeserialize)] +pub struct InitializeCompressionConfigData { + pub compression_delay: u32, + pub rent_recipient: Pubkey, + pub address_space: Vec, + pub config_bump: u8, +} + +/// Generic instruction data for update config +/// Note: Real programs should use their specific instruction format +#[derive(AnchorSerialize, AnchorDeserialize)] +pub struct UpdateCompressionConfigData { + pub new_compression_delay: Option, + pub new_rent_recipient: Option, + pub new_address_space: Option>, + pub new_update_authority: Option, +} + +/// Instruction data structure for decompress_accounts_idempotent +/// This matches the exact format expected by Anchor programs +/// T is the packed type (result of calling .pack() on the original type) +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)] +pub struct DecompressMultipleAccountsIdempotentData { + pub proof: ValidityProof, + pub compressed_accounts: Vec>, + pub system_accounts_offset: u8, +} + +/// Instruction data structure for compress_accounts_idempotent +/// This matches the exact format expected by Anchor programs +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)] +pub struct CompressAccountsIdempotentData { + pub proof: ValidityProof, + pub compressed_accounts: Vec, + pub signer_seeds: Vec>>, + pub system_accounts_offset: u8, +} + +/// Instruction builders for compressible accounts, following Solana SDK patterns +/// These are generic builders that work with any program implementing the compressible pattern +pub struct CompressibleInstruction; + +impl CompressibleInstruction { + pub const INITIALIZE_COMPRESSION_CONFIG_DISCRIMINATOR: [u8; 8] = + [133, 228, 12, 169, 56, 76, 222, 61]; + pub const UPDATE_COMPRESSION_CONFIG_DISCRIMINATOR: [u8; 8] = + [135, 215, 243, 81, 163, 146, 33, 70]; + /// Hardcoded discriminator for the standardized decompress_accounts_idempotent instruction + /// This is calculated as SHA256("global:decompress_accounts_idempotent")[..8] (Anchor format) + pub const DECOMPRESS_ACCOUNTS_IDEMPOTENT_DISCRIMINATOR: [u8; 8] = + [114, 67, 61, 123, 234, 31, 1, 112]; + /// Hardcoded discriminator for compress_token_account_ctoken_signer instruction + /// This is calculated as SHA256("global:compress_token_account_ctoken_signer")[..8] (Anchor format) + pub const COMPRESS_TOKEN_ACCOUNT_CTOKEN_SIGNER_DISCRIMINATOR: [u8; 8] = + [243, 154, 172, 243, 44, 214, 139, 73]; + /// Hardcoded discriminator for the standardized compress_accounts_idempotent instruction + /// This is calculated as SHA256("global:compress_accounts_idempotent")[..8] (Anchor format) + pub const COMPRESS_ACCOUNTS_IDEMPOTENT_DISCRIMINATOR: [u8; 8] = + [89, 130, 165, 88, 12, 207, 178, 185]; + + /// Creates an initialize_compression_config instruction + /// + /// Following Solana SDK patterns like system_instruction::transfer() + /// Returns Instruction directly - errors surface at execution time + /// + /// # Arguments + /// * `program_id` - The program ID + /// * `discriminator` - The instruction discriminator bytes (flexible length) + /// * `payer` - The payer account + /// * `authority` - The authority account + /// * `compression_delay` - The compression delay + /// * `rent_recipient` - The rent recipient + /// * `address_space` - The address space + /// * `config_bump` - The config bump + #[allow(clippy::too_many_arguments)] + pub fn initialize_compression_config( + program_id: &Pubkey, + discriminator: &[u8], + payer: &Pubkey, + authority: &Pubkey, + compression_delay: u32, + rent_recipient: Pubkey, + address_space: Vec, + config_bump: Option, + ) -> Instruction { + let config_bump = config_bump.unwrap_or(0); + let (config_pda, _) = CompressibleConfig::derive_pda(program_id, config_bump); + + // Get program data account for BPF Loader Upgradeable + let bpf_loader_upgradeable_id = + solana_pubkey::pubkey!("BPFLoaderUpgradeab1e11111111111111111111111"); + let (program_data_pda, _) = + Pubkey::find_program_address(&[program_id.as_ref()], &bpf_loader_upgradeable_id); + + let system_program_id = solana_pubkey::pubkey!("11111111111111111111111111111111"); + let accounts = vec![ + AccountMeta::new(*payer, true), // payer + AccountMeta::new(config_pda, false), // config + AccountMeta::new_readonly(program_data_pda, false), // program_data + AccountMeta::new_readonly(*authority, true), // authority + AccountMeta::new_readonly(system_program_id, false), // system_program + ]; + + let instruction_data = InitializeCompressionConfigData { + compression_delay, + rent_recipient, + address_space, + config_bump, + }; + + // Prepend discriminator to serialized data, following Solana SDK pattern + let serialized_data = instruction_data + .try_to_vec() + .expect("Failed to serialize instruction data"); + + let mut data = Vec::new(); + data.extend_from_slice(discriminator); + data.extend_from_slice(&serialized_data); + + Instruction { + program_id: *program_id, + accounts, + data, + } + } + + /// Creates an update config instruction + /// + /// Following Solana SDK patterns - returns Instruction directly + /// + /// # Arguments + /// * `program_id` - The program ID + /// * `discriminator` - The instruction discriminator bytes (flexible length) + /// * `authority` - The authority account + /// * `new_compression_delay` - Optional new compression delay + /// * `new_rent_recipient` - Optional new rent recipient + /// * `new_address_space` - Optional new address space + /// * `new_update_authority` - Optional new update authority + pub fn update_compression_config( + program_id: &Pubkey, + discriminator: &[u8], + authority: &Pubkey, + new_compression_delay: Option, + new_rent_recipient: Option, + new_address_space: Option>, + new_update_authority: Option, + ) -> Instruction { + let (config_pda, _) = CompressibleConfig::derive_pda(program_id, 0); + + let accounts = vec![ + AccountMeta::new(config_pda, false), // config + AccountMeta::new_readonly(*authority, true), // authority + ]; + + let instruction_data = UpdateCompressionConfigData { + new_compression_delay, + new_rent_recipient, + new_address_space, + new_update_authority, + }; + + // Prepend discriminator to serialized data, following Solana SDK pattern + let serialized_data = instruction_data + .try_to_vec() + .expect("Failed to serialize instruction data"); + let mut data = Vec::with_capacity(discriminator.len() + serialized_data.len()); + data.extend_from_slice(discriminator); + data.extend_from_slice(&serialized_data); + + Instruction { + program_id: *program_id, + accounts, + data, + } + } + + /// Build a `decompress_accounts_idempotent` instruction for any program's compressed account variant. + /// + /// # Arguments + /// * `program_id` - Target program + /// * `discriminator` - The instruction discriminator bytes (flexible length) + /// * `fee_payer` - Fee payer signer + /// * `rent_payer` - Rent payer signer + /// * `solana_accounts` - PDAs to decompress into + /// * `compressed_accounts` - Compressed accounts with their data (which implements Pack trait) + /// * `additional_accounts` - Additional accounts required for seed derivation (e.g., amm_config, token_mints) + /// * `validity_proof_with_context` - Validity proof with context + /// * `output_state_tree_info` - Output state tree info + /// + /// Returns `Ok(Instruction)` or error. + #[allow(clippy::too_many_arguments)] + pub fn decompress_accounts_idempotent( + program_id: &Pubkey, + discriminator: &[u8], + fee_payer: &Pubkey, + rent_payer: &Pubkey, + solana_accounts: &[Pubkey], + compressed_accounts: &[(CompressedAccount, T)], + additional_accounts: &[Pubkey], + validity_proof_with_context: ValidityProofWithContext, + output_state_tree_info: TreeInfo, + ) -> Result> + where + T: Pack + Clone + std::fmt::Debug, + { + let mut remaining_accounts = PackedAccounts::default(); + + // check if pdas/tokens + let mut has_tokens = false; + let mut has_pdas = false; + for (compressed_account, _) in compressed_accounts.iter() { + if compressed_account.owner == COMPRESSED_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 solana_accounts.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_small(system_config)?; + } else { + let system_config = SystemAccountMetaConfig::new(*program_id); + remaining_accounts.add_system_accounts_small(system_config)?; + } + + // pack output queue + let output_state_tree_index = + remaining_accounts.insert_or_get(output_state_tree_info.queue); + + // pack all tree infos + let packed_tree_infos = + validity_proof_with_context.pack_tree_infos(&mut remaining_accounts); + + let config_pda = CompressibleConfig::derive_pda(program_id, 0).0; + + // Required instruction accounts. + let mut accounts = vec![ + AccountMeta::new(*fee_payer, true), // fee_payer + AccountMeta::new(*rent_payer, true), // rent_payer + AccountMeta::new_readonly(config_pda, false), // config + AccountMeta::new_readonly(COMPRESSED_TOKEN_PROGRAM_ID.into(), false), + AccountMeta::new_readonly(COMPRESSED_TOKEN_PROGRAM_CPI_AUTHORITY.into(), false), + ]; + + // Add the dynamic accounts required for seed derivation + for account in additional_accounts { + accounts.push(AccountMeta::new_readonly(*account, false)); + } + // Pack all account data using the Pack trait. This converts types with + // Pubkeys to their packed versions with u8 indices. PDAs must implement + // pack trait. Tokens have a standard implementation. + let typed_compressed_accounts: Vec> = compressed_accounts + .iter() + .map(|(compressed_account, data)| { + let queue_index = + remaining_accounts.insert_or_get(compressed_account.tree_info.queue); + // Create compressed_account_meta + let compressed_meta = CompressedAccountMetaNoLamportsNoAddress { + tree_info: packed_tree_infos + .state_trees + .as_ref() + .unwrap() + .packed_tree_infos + .iter() + .find(|pti| { + pti.queue_pubkey_index == queue_index + && pti.leaf_index == compressed_account.leaf_index + }) + .copied() + .ok_or( + "Matching PackedStateTreeInfo (queue_pubkey_index + leaf_index) not found", + )?, + output_state_tree_index, + }; + // Pack data. Is standardized for TokenData and user-implemented for other types. + let packed_data = data.pack(&mut remaining_accounts); + Ok(CompressedAccountData { + meta: compressed_meta, + data: packed_data, + }) + }) + .collect::, Box>>()?; + + // add all packed systemaccounts to anchor metas. + let (system_accounts, system_accounts_offset, _) = remaining_accounts.to_account_metas(); + accounts.extend(system_accounts); + + // Onchain Accounts must be the last accounts. + for account in solana_accounts { + accounts.push(AccountMeta::new(*account, false)); + } + + let instruction_data = DecompressMultipleAccountsIdempotentData { + proof: validity_proof_with_context.proof, + compressed_accounts: typed_compressed_accounts, + system_accounts_offset: system_accounts_offset as u8, + }; + + // Serialize instruction data with discriminator + let serialized_data = instruction_data.try_to_vec()?; + let mut data = Vec::new(); + data.extend_from_slice(discriminator); + data.extend_from_slice(&serialized_data); + + Ok(Instruction { + program_id: *program_id, + accounts, + data, + }) + } + + /// Build a `compress_accounts_idempotent` instruction for compressing multiple accounts (PDAs and token accounts). + /// + /// # Arguments + /// * `program_id` - Target program + /// * `discriminator` - The instruction discriminator bytes (flexible length) + /// * `fee_payer` - Fee payer signer + /// * `rent_recipient` - Rent recipient account + /// * `accounts_to_compress` - Accounts to compress (PDAs and token accounts) + /// * `compressed_account_metas` - Metadata for where to store compressed data + /// * `signer_seeds` - Signer seeds for each account (empty vec if no seeds needed) + /// * `validity_proof_with_context` - Validity proof with context + /// * `output_state_tree_info` - Output state tree info + /// + /// Returns `Ok(Instruction)` or error. + #[allow(clippy::too_many_arguments)] + pub fn compress_accounts_idempotent( + program_id: &Pubkey, + discriminator: &[u8], + fee_payer: &Pubkey, + rent_authority: &Pubkey, + rent_recipient: &Pubkey, + accounts_pubkeys: &[Pubkey], + accounts_to_compress: &[Account], + signer_seeds: Vec>>, + validity_proof_with_context: ValidityProofWithContext, + output_state_tree_info: TreeInfo, + ) -> Result> { + if accounts_pubkeys.len() != accounts_to_compress.len() { + return Err("Accounts pubkeys length must match accounts length".into()); + } + // Sanity checks. + if !signer_seeds.is_empty() && signer_seeds.len() != accounts_to_compress.len() { + return Err("Signer seeds length must match accounts length or be empty".into()); + } + + // Sanity check for better error messages. + for (i, account) in accounts_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_small(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 config_pda = CompressibleConfig::derive_pda(program_id, 0).0; + + // Required instruction accounts. + let mut accounts = vec![ + AccountMeta::new(*fee_payer, true), // fee_payer + AccountMeta::new_readonly(config_pda, false), // config + AccountMeta::new(*rent_recipient, false), // rent_recipient + AccountMeta::new(*rent_authority, true), // rent_authority + AccountMeta::new_readonly(COMPRESSED_TOKEN_PROGRAM_ID.into(), false), // compressed_token_program + AccountMeta::new_readonly(COMPRESSED_TOKEN_PROGRAM_CPI_AUTHORITY.into(), false), // compressed_token_cpi_authority + ]; + + for account in accounts_to_compress.iter() { + if account.owner == COMPRESSED_TOKEN_PROGRAM_ID.into() { + let mint = Pubkey::new_from_array(account.data[0..32].try_into().unwrap()); + remaining_accounts.insert_or_get_read_only(mint); + } + } + + let (system_accounts, system_accounts_offset, _) = remaining_accounts.to_account_metas(); + accounts.extend(system_accounts); + + // Accounts to compress must be at the end. + for account in accounts_pubkeys { + accounts.push(AccountMeta::new(*account, false)); + } + + let instruction_data = CompressAccountsIdempotentData { + proof: validity_proof_with_context.proof, + compressed_accounts: compressed_account_metas_no_lamports_no_address, + signer_seeds, + system_accounts_offset: system_accounts_offset as u8, + }; + + let serialized_data = instruction_data.try_to_vec()?; + let mut data = Vec::new(); + data.extend_from_slice(discriminator); + data.extend_from_slice(&serialized_data); + + Ok(Instruction { + program_id: *program_id, + accounts, + data, + }) + } +} + +/// Generic instruction data for decompress multiple PDAs +// Re-export for easy access following Solana SDK patterns +pub use CompressibleInstruction as compressible_instruction; diff --git a/sdk-libs/compressible-client/tests/pack_trait_test.rs b/sdk-libs/compressible-client/tests/pack_trait_test.rs new file mode 100644 index 0000000000..fff1d18763 --- /dev/null +++ b/sdk-libs/compressible-client/tests/pack_trait_test.rs @@ -0,0 +1,119 @@ +#[cfg(test)] +mod tests { + use light_compressible_client::{ + Pack, PackedCompressibleTokenDataWithVariant, TokenDataWithVariant, + }; + use light_sdk::{instruction::PackedAccounts, token::TokenData}; + use solana_pubkey::Pubkey; + + #[test] + fn test_token_data_packing() { + let mut remaining_accounts = PackedAccounts::default(); + + let owner = Pubkey::new_unique(); + let mint = Pubkey::new_unique(); + let delegate = Pubkey::new_unique(); + + let token_data = TokenData { + owner, + mint, + amount: 1000, + delegate: Some(delegate), + state: Default::default(), + tlv: None, + }; + + // Pack the token data + let packed = token_data.pack(&mut remaining_accounts); + + // Verify the packed data + assert_eq!(packed.owner, 0); // First pubkey gets index 0 + assert_eq!(packed.mint, 1); // Second pubkey gets index 1 + assert_eq!(packed.delegate, 2); // Third pubkey gets index 2 + assert_eq!(packed.amount, 1000); + assert!(packed.has_delegate); + assert_eq!(packed.version, 2); + + // Verify remaining_accounts contains the pubkeys + let pubkeys = remaining_accounts.packed_pubkeys(); + assert_eq!(pubkeys[0], owner); + assert_eq!(pubkeys[1], mint); + assert_eq!(pubkeys[2], delegate); + } + + #[test] + fn test_token_data_with_variant_packing() { + use anchor_lang::{AnchorDeserialize, AnchorSerialize}; + + #[derive(Debug, Clone, Copy, AnchorSerialize, AnchorDeserialize)] + enum MyVariant { + TypeA = 0, + TypeB = 1, + } + + let mut remaining_accounts = PackedAccounts::default(); + + let token_with_variant = TokenDataWithVariant { + variant: MyVariant::TypeA, + token_data: TokenData { + owner: Pubkey::new_unique(), + mint: Pubkey::new_unique(), + amount: 500, + delegate: None, + state: Default::default(), + tlv: None, + }, + }; + + // Pack the wrapper + let packed: PackedCompressibleTokenDataWithVariant = + token_with_variant.pack(&mut remaining_accounts); + + // Verify variant is unchanged + assert!(matches!(packed.variant, MyVariant::TypeA)); + + // Verify token data is packed + assert_eq!(packed.token_data.owner, 0); + assert_eq!(packed.token_data.mint, 1); + assert_eq!(packed.token_data.amount, 500); + assert!(!packed.token_data.has_delegate); + } + + #[test] + fn test_deduplication_in_packing() { + let mut remaining_accounts = PackedAccounts::default(); + + let shared_owner = Pubkey::new_unique(); + let shared_mint = Pubkey::new_unique(); + + let token1 = TokenData { + owner: shared_owner, + mint: shared_mint, + amount: 100, + delegate: None, + state: Default::default(), + tlv: None, + }; + + let token2 = TokenData { + owner: shared_owner, // Same owner + mint: shared_mint, // Same mint + amount: 200, + delegate: None, + state: Default::default(), + tlv: None, + }; + + // Pack both tokens + let packed1 = token1.pack(&mut remaining_accounts); + let packed2 = token2.pack(&mut remaining_accounts); + + // Both should reference the same indices + assert_eq!(packed1.owner, packed2.owner); + assert_eq!(packed1.mint, packed2.mint); + + // Only 2 unique pubkeys should be stored + let pubkeys = remaining_accounts.packed_pubkeys(); + assert_eq!(pubkeys.len(), 2); + } +} diff --git a/sdk-libs/macros/CHANGELOG.md b/sdk-libs/macros/CHANGELOG.md new file mode 100644 index 0000000000..e6f0223b7b --- /dev/null +++ b/sdk-libs/macros/CHANGELOG.md @@ -0,0 +1,106 @@ +# Changelog + +## [Unreleased] + +### Changed + +- **BREAKING**: `add_compressible_instructions` macro no longer generates `create_*` instructions: + - Removed automatic generation of `create_user_record`, `create_game_session`, etc. + - Developers must implement their own create instructions with custom initialization logic + - This change recognizes that create instructions typically need custom business logic +- Updated `add_compressible_instructions` macro to align with new SDK patterns: + - Now generates `create_compression_config` and `update_compression_config` instructions + - Uses `HasCompressionInfo` trait instead of deprecated `CompressionTiming` + - `compress_*` instructions validate against config rent recipient + - `decompress_multiple_pdas` now accepts seeds in `CompressedAccountData` + - All generated instructions follow the pattern used in `anchor-compressible` + - Automatically uses Anchor's `INIT_SPACE` for account size calculation (no manual SIZE needed) + +### Added + +- **MAJOR**: Enhanced external file module support: + - Comprehensive pattern matching for common AMM/DEX structures (PoolState, Vault, Position, etc.) + - Explicit seed specification syntax: `#[add_compressible_instructions(PoolState@[POOL_SEED.as_bytes(), amm_config.key().as_ref()])]` + - Improved import detection for `pub use` statements and CamelCase account structs + - Intelligent seed inference for 7+ common DeFi patterns (pools, vaults, positions, configs, etc.) + - Enhanced error messages with debugging info and actionable solutions + - Support for complex multi-file project structures like Raydium CP-Swap +- Config management support in generated code: + - `CreateCompressibleConfig` accounts struct + - `UpdateCompressibleConfig` accounts struct + - Automatic config validation in create/compress instructions +- `CompressedAccountData` now includes `seeds` field for flexible PDA derivation +- Generated error codes for config validation +- `CompressionInfo` now implements `anchor_lang::Space` trait for automatic size calculation + +### Fixed + +- External file module parsing that previously threw "External file modules require explicit seed definitions" +- Import resolution for `pub use` statements across multiple files +- Pattern detection for account structs with various naming conventions + +### Removed + +- Deprecated `CompressionTiming` trait support +- Hardcoded constants (RENT_RECIPIENT, ADDRESS_SPACE, COMPRESSION_DELAY) +- Manual SIZE constant requirement - now uses Anchor's built-in space calculation + +## Migration Guide + +1. **Implement your own create instructions** (macro no longer generates them): + + ```rust + #[derive(Accounts)] + pub struct CreateUserRecord<'info> { + #[account(mut)] + pub user: Signer<'info>, + #[account( + init, + payer = user, + space = 8 + UserRecord::INIT_SPACE, + seeds = [b"user_record", user.key().as_ref()], + bump, + )] + pub user_record: Account<'info, UserRecord>, + pub system_program: Program<'info, System>, + } + + pub fn create_user_record(ctx: Context, name: String) -> Result<()> { + let user_record = &mut ctx.accounts.user_record; + user_record.compression_info = CompressionInfo::new_decompressed()?; + user_record.owner = ctx.accounts.user.key(); + user_record.name = name; + user_record.score = 0; + Ok(()) + } + ``` + +2. Update account structs to use `CompressionInfo` field and derive `InitSpace`: + + ```rust + #[derive(Debug, LightHasher, LightDiscriminator, Default, InitSpace)] + #[account] + pub struct UserRecord { + #[skip] + pub compression_info: CompressionInfo, + #[hash] + pub owner: Pubkey, + #[max_len(32)] // Required for String fields + pub name: String, + pub score: u64, + } + ``` + +3. Implement `HasCompressionInfo` trait instead of `CompressionTiming` + +4. Create config after program deployment: + + ```typescript + await program.methods + .createCompressibleConfig(compressionDelay, rentRecipient, addressSpace) + .rpc(); + ``` + +5. Update client code to use new instruction names: + - `create_record` → `create_user_record` (based on struct name) + - Pass entire struct data instead of individual fields diff --git a/sdk-libs/macros/Cargo.toml b/sdk-libs/macros/Cargo.toml index 791a4e9787..caea3eaed2 100644 --- a/sdk-libs/macros/Cargo.toml +++ b/sdk-libs/macros/Cargo.toml @@ -6,12 +6,16 @@ repository = "https://github.com/Lightprotocol/light-protocol" license = "Apache-2.0" edition = "2021" +[features] +default = [] +anchor-discriminator-compat = [] + [dependencies] proc-macro2 = { workspace = true } quote = { workspace = true } syn = { workspace = true } solana-pubkey = { workspace = true, features = ["curve25519", "sha2"] } - +heck = "0.4.1" light-hasher = { workspace = true } light-poseidon = { workspace = true } diff --git a/sdk-libs/macros/MODULAR_MACROS_USAGE.md b/sdk-libs/macros/MODULAR_MACROS_USAGE.md new file mode 100644 index 0000000000..6b5d8d6177 --- /dev/null +++ b/sdk-libs/macros/MODULAR_MACROS_USAGE.md @@ -0,0 +1,181 @@ +# Modular Compressible Account Macros + +This document demonstrates how to use the new modular macros to replace manual trait implementations for compressible accounts. + +## Available Macros + +### 1. `#[derive(Compressible)]` + +**Generates:** `HasCompressionInfo`, `Size`, and `CompressAs` trait implementations. + +**Replaces:** All manual trait implementations for individual account types. + +**Usage:** + +```rust +use light_sdk_macros::Compressible; +use light_sdk::compressible::CompressionInfo; + +// Basic usage - keeps all fields as-is during compression +#[derive(Compressible)] +pub struct UserRecord { + #[skip] + pub compression_info: Option, + #[hash] + pub owner: Pubkey, + pub name: String, + pub score: u64, +} + +// Custom compression - reset specific fields +#[derive(Compressible)] +#[compress_as(start_time = 0, end_time = None, score = 0)] +pub struct GameSession { + #[skip] + pub compression_info: Option, + pub session_id: u64, // KEPT + pub player: Pubkey, // KEPT + pub game_type: String, // KEPT + pub start_time: u64, // RESET to 0 + pub end_time: Option, // RESET to None + pub score: u64, // RESET to 0 +} +``` + +### 2. `#[derive(CompressiblePack)]` + +**Generates:** `Pack` and `Unpack` trait implementations, plus `PackedXxx` struct for types with Pubkeys. + +**Replaces:** All manual `Pack`/`Unpack` implementations and `PackedXxx` struct definitions. + +**Usage:** + +```rust +use light_sdk_macros::CompressiblePack; + +#[derive(CompressiblePack)] +pub struct UserRecord { + pub compression_info: Option, + pub owner: Pubkey, // Will be packed as u8 index + pub name: String, // Kept as-is + pub score: u64, // Kept as-is +} +// Automatically generates PackedUserRecord struct + all Pack/Unpack impls +``` + +### 3. `compressed_account_variant!` macro + +**Generates:** `CompressedAccountVariant` enum + all trait implementations + `CompressedAccountData` struct. + +**Replaces:** Entire enum definition and all its trait implementations. + +**Usage:** + +```rust +use light_sdk_macros::compressed_account_variant; + +// Generate the unified enum for all account types +compressed_account_variant!(UserRecord, GameSession, PlaceholderRecord); +``` + +## Complete Example: Replacing Manual Implementation + +### Before (Manual Implementation): + +```rust +// Manual trait implementations for each account type +impl HasCompressionInfo for UserRecord { /* 20+ lines */ } +impl Size for UserRecord { /* 3 lines */ } +impl CompressAs for UserRecord { /* 10+ lines */ } +impl Pack for UserRecord { /* 10+ lines */ } +impl Unpack for UserRecord { /* 5+ lines */ } + +// Manual PackedUserRecord struct +#[derive(Debug, Clone, AnchorSerialize, AnchorDeserialize)] +pub struct PackedUserRecord { /* fields */ } +impl Pack for PackedUserRecord { /* 5+ lines */ } +impl Unpack for PackedUserRecord { /* 10+ lines */ } + +// Repeat for GameSession, PlaceholderRecord... + +// Manual CompressedAccountVariant enum + all traits (100+ lines) +#[derive(Clone, Debug, AnchorSerialize, AnchorDeserialize)] +pub enum CompressedAccountVariant { /* variants */ } +impl Default for CompressedAccountVariant { /* match arms */ } +impl DataHasher for CompressedAccountVariant { /* match arms */ } +// ... 5 more trait implementations + +// Manual CompressedAccountData struct +#[derive(Clone, Debug, AnchorDeserialize, AnchorSerialize)] +pub struct CompressedAccountData { /* fields */ } +``` + +### After (Using Modular Macros): + +```rust +use light_sdk_macros::{Compressible, CompressiblePack, compressed_account_variant}; + +// Account definitions with automatic trait generation +#[derive(Compressible, CompressiblePack)] +pub struct UserRecord { + #[skip] + pub compression_info: Option, + #[hash] + pub owner: Pubkey, + pub name: String, + pub score: u64, +} + +#[derive(Compressible, CompressiblePack)] +#[compress_as(start_time = 0, end_time = None, score = 0)] +pub struct GameSession { + #[skip] + pub compression_info: Option, + pub session_id: u64, + #[hash] + pub player: Pubkey, + pub game_type: String, + pub start_time: u64, + pub end_time: Option, + pub score: u64, +} + +#[derive(Compressible, CompressiblePack)] +pub struct PlaceholderRecord { + #[skip] + pub compression_info: Option, + #[hash] + pub owner: Pubkey, + pub name: String, + pub placeholder_id: u64, +} + +// Generate the unified enum and data structures +compressed_account_variant!(UserRecord, GameSession, PlaceholderRecord); +``` + +## Code Reduction + +- **Manual implementation**: ~500+ lines of boilerplate trait implementations +- **Macro implementation**: ~15 lines with derive attributes + 1 macro call +- **Reduction**: ~97% less boilerplate code +- **Maintainability**: Single source of truth for trait implementations +- **Consistency**: Guaranteed identical behavior across all account types + +## Benefits + +1. **Drop-in Replacement**: Each macro replaces specific manual code sections +2. **Modular**: Can use macros independently (e.g., just `#[derive(Compressible)]`) +3. **Configurable**: Custom compression behavior via `compress_as` attribute +4. **Type Safety**: Compile-time validation of all trait implementations +5. **Future-Proof**: Centralized logic that's easy to update + +## Migration Guide + +1. **Replace individual trait impls**: Add `#[derive(Compressible)]` to account structs +2. **Replace Pack/Unpack impls**: Add `#[derive(CompressiblePack)]` to account structs +3. **Replace enum + traits**: Replace entire enum with `compressed_account_variant!` macro call +4. **Remove manual code**: Delete all manual trait implementations and structs +5. **Test**: Verify identical behavior with existing tests + +The macros generate identical code to the manual implementation, ensuring 100% compatibility. diff --git a/sdk-libs/macros/src/account_seeds.rs b/sdk-libs/macros/src/account_seeds.rs new file mode 100644 index 0000000000..aa95787038 --- /dev/null +++ b/sdk-libs/macros/src/account_seeds.rs @@ -0,0 +1,158 @@ +use proc_macro2::TokenStream; +use quote::{format_ident, quote}; +use syn::{ + parse::{Parse, ParseStream}, + punctuated::Punctuated, + Attribute, Expr, ItemStruct, Meta, Result, Token, +}; + +/// Parse account structs and generate seed functions based on their Anchor seeds attributes +struct AccountStructList { + structs: Punctuated, +} + +impl Parse for AccountStructList { + fn parse(input: ParseStream) -> Result { + Ok(AccountStructList { + structs: Punctuated::parse_terminated(input)?, + }) + } +} + +/// Generates seed getter functions by analyzing Anchor account structs +/// +/// This macro scans account structs for `#[account(seeds = [...], ...)]` attributes +/// and generates corresponding seed getter functions. +/// +/// Usage: +/// ```rust +/// generate_seed_functions! { +/// #[derive(Accounts)] +/// pub struct CreateRecord<'info> { +/// #[account( +/// init, +/// seeds = [b"user_record", user.key().as_ref()], +/// bump, +/// )] +/// pub user_record: Account<'info, UserRecord>, +/// pub user: Signer<'info>, +/// } +/// +/// #[derive(Accounts)] +/// #[instruction(session_id: u64)] +/// pub struct CreateGameSession<'info> { +/// #[account( +/// init, +/// seeds = [b"game_session", session_id.to_le_bytes().as_ref()], +/// bump, +/// )] +/// pub game_session: Account<'info, GameSession>, +/// pub player: Signer<'info>, +/// } +/// } +/// ``` +/// +/// This generates: +/// - `get_user_record_seeds(user: &Pubkey) -> (Vec>, Pubkey)` +/// - `get_game_session_seeds(session_id: u64) -> (Vec>, Pubkey)` +pub fn generate_seed_functions(input: TokenStream) -> Result { + let account_structs = syn::parse2::(input)?; + + let mut generated_functions = Vec::new(); + + for account_struct in &account_structs.structs { + if let Some(function) = analyze_account_struct(account_struct)? { + generated_functions.push(function); + } + } + + let expanded = quote! { + #(#generated_functions)* + }; + + Ok(expanded) +} + +fn analyze_account_struct(account_struct: &ItemStruct) -> Result> { + // Look for fields with #[account(...)] attributes that have seeds + for field in &account_struct.fields { + if let Some(account_attr) = find_account_attribute(&field.attrs) { + if let Some(seeds_info) = extract_seeds_from_account_attr(account_attr)? { + let field_name = field.ident.as_ref().unwrap(); + let function_name = format_ident!("get_{}_seeds", field_name); + + let (parameters, seed_expressions) = analyze_seeds_expressions(&seeds_info)?; + + let function = quote! { + /// Auto-generated seed function from Anchor account struct + pub fn #function_name(#(#parameters),*) -> (Vec>, anchor_lang::prelude::Pubkey) { + let seeds = [#(#seed_expressions),*]; + let (pda, bump) = anchor_lang::prelude::Pubkey::find_program_address(&seeds, &crate::ID); + let bump_slice = vec![bump]; + let seeds_vec = vec![ + #( + (#seed_expressions).to_vec(), + )* + bump_slice, + ]; + (seeds_vec, pda) + } + }; + + return Ok(Some(function)); + } + } + } + + Ok(None) +} + +fn find_account_attribute(attrs: &[Attribute]) -> Option<&Attribute> { + attrs.iter().find(|attr| attr.path().is_ident("account")) +} + +fn extract_seeds_from_account_attr(attr: &Attribute) -> Result>> { + // For now, return None to skip seed extraction - this is complex to parse correctly + // The Anchor macro parsing is quite involved and would need more sophisticated handling + Ok(None) +} + +fn analyze_seeds_expressions( + seed_expressions: &[Expr], +) -> Result<(Vec, Vec)> { + let mut parameters = Vec::new(); + let mut processed_seeds = Vec::new(); + + for expr in seed_expressions { + match expr { + // Handle byte string literals like b"user_record" + Expr::Lit(lit_expr) => { + processed_seeds.push(quote! { #expr }); + } + // Handle method calls like user.key().as_ref() + Expr::MethodCall(method_call) => { + // Extract the base identifier (e.g., "user" from "user.key().as_ref()") + if let Expr::Path(path_expr) = &*method_call.receiver { + if let Some(ident) = path_expr.path.get_ident() { + parameters.push(quote! { #ident: &anchor_lang::prelude::Pubkey }); + processed_seeds.push(quote! { #ident.as_ref() }); + } + } else if let Expr::MethodCall(inner_call) = &*method_call.receiver { + // Handle nested calls like session_id.to_le_bytes().as_ref() + if let Expr::Path(path_expr) = &*inner_call.receiver { + if let Some(ident) = path_expr.path.get_ident() { + parameters.push(quote! { #ident: u64 }); + processed_seeds.push(quote! { #ident.to_le_bytes().as_ref() }); + } + } + } + } + // Handle other expressions as-is + _ => { + processed_seeds.push(quote! { #expr }); + } + } + } + + Ok((parameters, processed_seeds)) +} diff --git a/sdk-libs/macros/src/compress_as.rs b/sdk-libs/macros/src/compress_as.rs new file mode 100644 index 0000000000..2e0e82e5f9 --- /dev/null +++ b/sdk-libs/macros/src/compress_as.rs @@ -0,0 +1,206 @@ +use proc_macro2::TokenStream; +use quote::quote; +use syn::{ + parse::{Parse, ParseStream}, + punctuated::Punctuated, + Expr, Ident, ItemStruct, Result, Token, +}; + +/// Parse the compress_as attribute content +struct CompressAsFields { + fields: Punctuated, +} + +struct CompressAsField { + name: Ident, + value: Expr, +} + +impl Parse for CompressAsField { + fn parse(input: ParseStream) -> Result { + let name: Ident = input.parse()?; + input.parse::()?; + let value: Expr = input.parse()?; + Ok(CompressAsField { name, value }) + } +} + +impl Parse for CompressAsFields { + fn parse(input: ParseStream) -> Result { + Ok(CompressAsFields { + fields: Punctuated::parse_terminated(input)?, + }) + } +} + +/// Generates CompressAs trait implementation for a struct with optional compress_as attribute +pub fn derive_compress_as(input: ItemStruct) -> Result { + let struct_name = &input.ident; + + // Find the compress_as attribute (optional) + let compress_as_attr = input + .attrs + .iter() + .find(|attr| attr.path().is_ident("compress_as")); + + // Parse the attribute content if it exists + let compress_as_fields = if let Some(attr) = compress_as_attr { + Some(attr.parse_args::()?) + } else { + None + }; + + // Get all struct fields + let struct_fields = match &input.fields { + syn::Fields::Named(fields) => &fields.named, + _ => { + return Err(syn::Error::new_spanned( + &input, + "CompressAs derive only supports structs with named fields", + )); + } + }; + + // Create field assignments for the compress_as method + let field_assignments = struct_fields.iter().map(|field| { + let field_name = field.ident.as_ref().unwrap(); + + // ALWAYS set compression_info to None - this is required for compressed storage + if field_name == "compression_info" { + return quote! { #field_name: None }; + } + + // Check if this field is overridden in the compress_as attribute + let override_field = compress_as_fields + .as_ref() + .and_then(|fields| fields.fields.iter().find(|f| f.name == *field_name)); + + if let Some(override_field) = override_field { + let override_value = &override_field.value; + quote! { #field_name: #override_value } + } else { + // Keep the original value - determine how to clone/copy based on field type + let field_type = &field.ty; + if is_copy_type(field_type) { + quote! { #field_name: self.#field_name } + } else { + quote! { #field_name: self.#field_name.clone() } + } + } + }); + + // Determine if we need custom compression (any fields specified in compress_as attribute) + let has_custom_fields = compress_as_fields.is_some(); + + let compress_as_impl = if has_custom_fields { + // Custom compression - return Cow::Owned with modified fields + quote! { + fn compress_as(&self) -> std::borrow::Cow<'_, Self::Output> { + std::borrow::Cow::Owned(Self { + #(#field_assignments,)* + }) + } + } + } else { + // Simple case - return Cow::Owned with compression_info = None + // We can't return Cow::Borrowed because compression_info must be None + quote! { + fn compress_as(&self) -> std::borrow::Cow<'_, Self::Output> { + std::borrow::Cow::Owned(Self { + #(#field_assignments,)* + }) + } + } + }; + + // Generate HasCompressionInfo implementation (automatically included with Compressible) + let has_compression_info_impl = quote! { + impl light_sdk::compressible::HasCompressionInfo for #struct_name { + fn compression_info(&self) -> &light_sdk::compressible::CompressionInfo { + self.compression_info + .as_ref() + .expect("CompressionInfo must be Some on-chain") + } + + fn compression_info_mut(&mut self) -> &mut light_sdk::compressible::CompressionInfo { + self.compression_info + .as_mut() + .expect("CompressionInfo must be Some on-chain") + } + + fn compression_info_mut_opt(&mut self) -> &mut Option { + &mut self.compression_info + } + + fn set_compression_info_none(&mut self) { + self.compression_info = None; + } + } + }; + + let expanded = quote! { + impl light_sdk::compressible::CompressAs for #struct_name { + type Output = Self; + + #compress_as_impl + } + + impl light_sdk::Size for #struct_name { + fn size(&self) -> usize { + Self::LIGHT_DISCRIMINATOR.len() + Self::INIT_SPACE + } + } + + // Automatically derive HasCompressionInfo when using Compressible + #has_compression_info_impl + }; + + Ok(expanded) +} + +/// Determines if a type is likely to be Copy (simple heuristic) +fn is_copy_type(ty: &syn::Type) -> bool { + match ty { + syn::Type::Path(type_path) => { + if let Some(segment) = type_path.path.segments.last() { + let type_name = segment.ident.to_string(); + matches!( + type_name.as_str(), + "u8" | "u16" + | "u32" + | "u64" + | "u128" + | "usize" + | "i8" + | "i16" + | "i32" + | "i64" + | "i128" + | "isize" + | "f32" + | "f64" + | "bool" + | "char" + | "Pubkey" + ) || (type_name == "Option" && has_copy_inner_type(&segment.arguments)) + } else { + false + } + } + _ => false, + } +} + +/// Check if Option where T is Copy +fn has_copy_inner_type(args: &syn::PathArguments) -> bool { + match args { + syn::PathArguments::AngleBracketed(args) => args.args.iter().any(|arg| { + if let syn::GenericArgument::Type(ty) = arg { + is_copy_type(ty) + } else { + false + } + }), + _ => false, + } +} diff --git a/sdk-libs/macros/src/compressible.rs b/sdk-libs/macros/src/compressible.rs new file mode 100644 index 0000000000..9d182c283b --- /dev/null +++ b/sdk-libs/macros/src/compressible.rs @@ -0,0 +1,90 @@ +use heck::ToSnakeCase; +use proc_macro2::TokenStream; +use quote::{format_ident, quote}; +use syn::{ + parse::{Parse, ParseStream}, + punctuated::Punctuated, + Ident, Item, ItemEnum, ItemFn, ItemMod, ItemStruct, Result, Token, +}; + +/// Parse a comma-separated list of identifiers +#[derive(Clone)] +enum CompressibleType { + Regular(Ident), +} + +struct CompressibleTypeList { + types: Punctuated, +} + +impl Parse for CompressibleType { + fn parse(input: ParseStream) -> Result { + let ident: Ident = input.parse()?; + Ok(CompressibleType::Regular(ident)) + } +} + +impl Parse for CompressibleTypeList { + fn parse(input: ParseStream) -> Result { + Ok(CompressibleTypeList { + types: Punctuated::parse_terminated(input)?, + }) + } +} + +/// Generates HasCompressionInfo trait implementation for a struct with compression_info field +pub fn derive_has_compression_info(input: syn::ItemStruct) -> Result { + let struct_name = input.ident.clone(); + + // Find the compression_info field + let compression_info_field = match &input.fields { + syn::Fields::Named(fields) => fields.named.iter().find(|field| { + field + .ident + .as_ref() + .map(|ident| ident == "compression_info") + .unwrap_or(false) + }), + _ => { + return Err(syn::Error::new_spanned( + &struct_name, + "HasCompressionInfo can only be derived for structs with named fields", + )) + } + }; + + let _compression_info_field = compression_info_field.ok_or_else(|| { + syn::Error::new_spanned( + &struct_name, + "HasCompressionInfo requires a field named 'compression_info' of type Option" + ) + })?; + + // Validate that the field is Option. For now, we'll assume + // it's correct and let the compiler catch type errors + let has_compression_info_impl = quote! { + impl light_sdk::compressible::HasCompressionInfo for #struct_name { + fn compression_info(&self) -> &light_sdk::compressible::CompressionInfo { + self.compression_info + .as_ref() + .expect("CompressionInfo must be Some on-chain") + } + + fn compression_info_mut(&mut self) -> &mut light_sdk::compressible::CompressionInfo { + self.compression_info + .as_mut() + .expect("CompressionInfo must be Some on-chain") + } + + fn compression_info_mut_opt(&mut self) -> &mut Option { + &mut self.compression_info + } + + fn set_compression_info_none(&mut self) { + self.compression_info = None; + } + } + }; + + Ok(has_compression_info_impl) +} diff --git a/sdk-libs/macros/src/compressible_derive.rs b/sdk-libs/macros/src/compressible_derive.rs new file mode 100644 index 0000000000..e6aa5dde93 --- /dev/null +++ b/sdk-libs/macros/src/compressible_derive.rs @@ -0,0 +1,255 @@ +use proc_macro2::TokenStream; +use quote::quote; +use syn::{ + parse::{Parse, ParseStream}, + punctuated::Punctuated, + Data, DeriveInput, Expr, Fields, Ident, Result, Token, +}; + +/// Parse the compress_as attribute content +struct CompressAsFields { + fields: Punctuated, +} + +struct CompressAsField { + name: Ident, + value: Expr, +} + +impl Parse for CompressAsField { + fn parse(input: ParseStream) -> Result { + let name: Ident = input.parse()?; + input.parse::()?; + let value: Expr = input.parse()?; + Ok(CompressAsField { name, value }) + } +} + +impl Parse for CompressAsFields { + fn parse(input: ParseStream) -> Result { + Ok(CompressAsFields { + fields: Punctuated::parse_terminated(input)?, + }) + } +} + +/// Generates HasCompressionInfo, Size, and CompressAs trait implementations for compressible account types +/// +/// Supports optional compress_as attribute for custom compression behavior: +/// #[derive(Compressible)] +/// #[compress_as(start_time = 0, end_time = None)] +/// pub struct GameSession { ... } +/// +/// Usage: #[derive(Compressible)] +pub fn derive_compressible(input: DeriveInput) -> Result { + let struct_name = &input.ident; + + // Validate struct has compression_info field + let fields = match &input.data { + Data::Struct(data) => match &data.fields { + Fields::Named(fields) => &fields.named, + _ => { + return Err(syn::Error::new_spanned( + &input, + "Compressible only supports structs with named fields", + )); + } + }, + _ => { + return Err(syn::Error::new_spanned( + &input, + "Compressible only supports structs", + )); + } + }; + + // Find the compression_info field + let compression_info_field = fields.iter().find(|field| { + field + .ident + .as_ref() + .map(|ident| ident == "compression_info") + .unwrap_or(false) + }); + + if compression_info_field.is_none() { + return Err(syn::Error::new_spanned( + &struct_name, + "Compressible requires a field named 'compression_info' of type Option" + )); + } + + // Parse the compress_as attribute (optional) + let compress_as_attr = input + .attrs + .iter() + .find(|attr| attr.path().is_ident("compress_as")); + + let compress_as_fields = if let Some(attr) = compress_as_attr { + Some(attr.parse_args::()?) + } else { + None + }; + + // Generate HasCompressionInfo implementation + let has_compression_info_impl = quote! { + impl light_sdk::compressible::HasCompressionInfo for #struct_name { + fn compression_info(&self) -> &light_sdk::compressible::CompressionInfo { + self.compression_info + .as_ref() + .expect("CompressionInfo must be Some on-chain") + } + + fn compression_info_mut(&mut self) -> &mut light_sdk::compressible::CompressionInfo { + self.compression_info + .as_mut() + .expect("CompressionInfo must be Some on-chain") + } + + fn compression_info_mut_opt(&mut self) -> &mut Option { + &mut self.compression_info + } + + fn set_compression_info_none(&mut self) { + self.compression_info = None; + } + } + }; + + // Generate Size implementation + let size_impl = quote! { + impl light_sdk::account::Size for #struct_name { + fn size(&self) -> usize { + Self::LIGHT_DISCRIMINATOR.len() + Self::INIT_SPACE + } + } + }; + + // Generate CompressAs implementation + let field_assignments = fields.iter().map(|field| { + let field_name = field.ident.as_ref().unwrap(); + + // ALWAYS set compression_info to None - this is required for compressed storage + if field_name == "compression_info" { + return quote! { #field_name: None }; + } + + // Check if this field is overridden in the compress_as attribute + let override_field = compress_as_fields + .as_ref() + .and_then(|fields| fields.fields.iter().find(|f| f.name == *field_name)); + + if let Some(override_field) = override_field { + let override_value = &override_field.value; + quote! { #field_name: #override_value } + } else { + // Keep the original value - determine how to clone/copy based on field type + let field_type = &field.ty; + if is_copy_type(field_type) { + quote! { #field_name: self.#field_name } + } else { + quote! { #field_name: self.#field_name.clone() } + } + } + }); + + let compress_as_impl = quote! { + impl light_sdk::compressible::CompressAs for #struct_name { + type Output = Self; + + fn compress_as(&self) -> std::borrow::Cow<'_, Self::Output> { + std::borrow::Cow::Owned(Self { + #(#field_assignments,)* + }) + } + } + }; + + let expanded = quote! { + #has_compression_info_impl + #size_impl + #compress_as_impl + }; + + Ok(expanded) +} + +/// Determines if a type is likely to be Copy (simple heuristic) +fn is_copy_type(ty: &syn::Type) -> bool { + match ty { + syn::Type::Path(type_path) => { + if let Some(segment) = type_path.path.segments.last() { + let type_name = segment.ident.to_string(); + matches!( + type_name.as_str(), + "u8" | "u16" + | "u32" + | "u64" + | "u128" + | "usize" + | "i8" + | "i16" + | "i32" + | "i64" + | "i128" + | "isize" + | "f32" + | "f64" + | "bool" + | "char" + | "Pubkey" + ) || (type_name == "Option" && has_copy_inner_type(&segment.arguments)) + } else { + false + } + } + _ => false, + } +} + +/// Check if Option where T is Copy +fn has_copy_inner_type(args: &syn::PathArguments) -> bool { + match args { + syn::PathArguments::AngleBracketed(args) => args.args.iter().any(|arg| { + if let syn::GenericArgument::Type(ty) = arg { + is_copy_type(ty) + } else { + false + } + }), + _ => false, + } +} + +#[allow(dead_code)] +fn generate_identity_pack_unpack(struct_name: &syn::Ident) -> Result { + let pack_impl = quote! { + impl light_sdk::compressible::Pack for #struct_name { + type Packed = Self; + + fn pack(&self, _remaining_accounts: &mut light_sdk::instruction::PackedAccounts) -> Self::Packed { + self.clone() + } + } + }; + + let unpack_impl = quote! { + impl light_sdk::compressible::Unpack for #struct_name { + type Unpacked = Self; + + fn unpack( + &self, + _remaining_accounts: &[solana_account_info::AccountInfo], + ) -> std::result::Result { + Ok(self.clone()) + } + } + }; + + let expanded = quote! { + #pack_impl + #unpack_impl + }; + + Ok(expanded) +} diff --git a/sdk-libs/macros/src/compressible_instructions.rs b/sdk-libs/macros/src/compressible_instructions.rs new file mode 100644 index 0000000000..80daabc762 --- /dev/null +++ b/sdk-libs/macros/src/compressible_instructions.rs @@ -0,0 +1,1770 @@ +use proc_macro2::TokenStream; +use quote::{format_ident, quote}; +use syn::{ + parse::{Parse, ParseStream}, + punctuated::Punctuated, + spanned::Spanned, + Expr, Ident, Item, ItemFn, ItemStruct, ItemMod, LitStr, Result, Token, +}; + +/// Parse seed specification for a token account variant +struct TokenSeedSpec { + variant: Ident, + _eq: Token![=], + is_token: Option, // Optional explicit token flag + seeds: Punctuated, +} + +impl Parse for TokenSeedSpec { + fn parse(input: ParseStream) -> Result { + let variant = input.parse()?; + let _eq = input.parse()?; + + let content; + syn::parenthesized!(content in input); + + // Check if first element is an explicit token flag + let (is_token, seeds) = if content.peek(Ident) { + let first_ident: Ident = content.parse()?; + + match first_ident.to_string().as_str() { + "is_token" | "true" => { + // Explicit token flag + let _comma: Token![,] = content.parse()?; + let seeds = Punctuated::parse_terminated(&content)?; + (Some(true), seeds) + } + "is_pda" | "false" => { + // Explicit PDA flag + let _comma: Token![,] = content.parse()?; + let seeds = Punctuated::parse_terminated(&content)?; + (Some(false), seeds) + } + _ => { + // Not a flag, treat as first seed element + let mut seeds = Punctuated::new(); + seeds.push(SeedElement::Expression(syn::Expr::Path(syn::ExprPath { + attrs: vec![], + qself: None, + path: syn::Path::from(first_ident), + }))); + + if content.peek(Token![,]) { + let _comma: Token![,] = content.parse()?; + let rest: Punctuated = Punctuated::parse_terminated(&content)?; + seeds.extend(rest); + } + + (None, seeds) + } + } + } else { + // No identifier first, parse all as seeds + let seeds = Punctuated::parse_terminated(&content)?; + (None, seeds) + }; + + Ok(TokenSeedSpec { + variant, + _eq, + is_token, + seeds, + }) + } +} + +enum SeedElement { + /// String literal like "user_record" + Literal(LitStr), + /// Any expression: data.owner, ctx.fee_payer, data.session_id.to_le_bytes(), etc. + Expression(Expr), +} + +impl Parse for SeedElement { + fn parse(input: ParseStream) -> Result { + if input.peek(LitStr) { + Ok(SeedElement::Literal(input.parse()?)) + } else { + // Parse everything else as an expression + // This will handle ctx.fee_payer, data.session_id.to_le_bytes(), etc. + Ok(SeedElement::Expression(input.parse()?)) + } + } +} + +/// Parse instruction data field specification: field_name = Type +struct InstructionDataSpec { + field_name: Ident, + field_type: syn::Type, +} + +impl Parse for InstructionDataSpec { + fn parse(input: ParseStream) -> Result { + // Parse: field_name = Type (e.g., session_id = u64) + let field_name: Ident = input.parse()?; + let _eq: Token![=] = input.parse()?; + let field_type: syn::Type = input.parse()?; + + Ok(InstructionDataSpec { + field_name, + field_type, + }) + } +} + +/// Parse enhanced macro arguments with mixed account types, PDA seeds, token seeds, and instruction data +struct EnhancedMacroArgs { + account_types: Vec, + pda_seeds: Vec, + token_seeds: Vec, + instruction_data: Vec, +} + +impl Parse for EnhancedMacroArgs { + fn parse(input: ParseStream) -> Result { + let mut account_types = Vec::new(); + let mut pda_seeds = Vec::new(); + let mut token_seeds = Vec::new(); + let mut instruction_data = Vec::new(); + + while !input.is_empty() { + let ident: Ident = input.parse()?; + + if input.peek(Token![=]) { + let _eq: Token![=] = input.parse()?; + + if input.peek(syn::token::Paren) { + // This is a seed specification (either PDA or CToken) + let content; + syn::parenthesized!(content in input); + + // Check for explicit token flag as first element + let (is_token_explicit, seeds) = if content.peek(Ident) { + let first_ident: Ident = content.parse()?; + + if first_ident == "is_token" { + let _comma: Token![,] = content.parse()?; + let seeds = Punctuated::parse_terminated(&content)?; + (Some(true), seeds) + } else if first_ident == "is_pda" { + let _comma: Token![,] = content.parse()?; + let seeds = Punctuated::parse_terminated(&content)?; + (Some(false), seeds) + } else { + // Not a flag, treat as first seed element + let mut seeds = Punctuated::new(); + seeds.push(SeedElement::Expression(syn::Expr::Path(syn::ExprPath { + attrs: vec![], + qself: None, + path: syn::Path::from(first_ident), + }))); + + if content.peek(Token![,]) { + let _comma: Token![,] = content.parse()?; + let rest: Punctuated = Punctuated::parse_terminated(&content)?; + seeds.extend(rest); + } + + (None, seeds) + } + } else { + // No identifier first, parse all as seeds + let seeds = Punctuated::parse_terminated(&content)?; + (None, seeds) + }; + + let seed_spec = TokenSeedSpec { + variant: ident.clone(), + _eq: Token![=]([proc_macro2::Span::call_site()]), + is_token: is_token_explicit, + seeds, + }; + + let is_token_account = is_token_explicit.unwrap_or_else(|| { + // Default to PDA if no explicit flag provided + false + }); + + if is_token_account { + token_seeds.push(seed_spec); + } else { + // This is a PDA seed specification + pda_seeds.push(seed_spec); + account_types.push(ident); + } + } else { + // This is an instruction data type specification: field_name = Type + let field_type: syn::Type = input.parse()?; + instruction_data.push(InstructionDataSpec { + field_name: ident, + field_type, + }); + } + } else { + // This is a regular account type without seed specification + account_types.push(ident); + } + + if input.peek(Token![,]) { + let _comma: Token![,] = input.parse()?; + } else { + break; + } + } + + Ok(EnhancedMacroArgs { + account_types, + pda_seeds, + token_seeds, + instruction_data, + }) + } +} + +// Legacy parsing removed - only declarative syntax supported now! 🎉 + +/// Enhanced version of add_compressible_instructions that generates both compress and decompress instructions +/// +/// Now supports automatic CToken seed derivation: +/// - Specify token seeds directly in the macro +/// - Eliminates need for manual CTokenSeedProvider implementation +/// - Completely automatic seed generation +/// +/// Usage: +/// ```rust +/// #[add_compressible_instructions( +/// MyAccount = ("my_account", data.field), +/// AnotherAccount = ("another", data.id.to_le_bytes()), +/// MyToken = (is_token, "my_token", ctx.fee_payer, ctx.mint), +/// field = Pubkey, +/// id = u64 +/// )] +/// #[program] +/// pub mod my_program { +/// // Your other instructions... +/// } +/// ``` +/// +/// ## Explicit Token/PDA Flags: +/// - Use `is_token` as first element for token accounts (REQUIRED for tokens!) +/// - Use `is_pda` as first element for PDA accounts (optional, defaults to PDA) +/// - NO naming convention fallbacks - be explicit! +pub fn add_compressible_instructions( + args: TokenStream, + mut module: ItemMod, +) -> Result { + // Parse with enhanced format - no legacy fallback! + let enhanced_args = syn::parse2::(args)?; + let account_types = enhanced_args.account_types; + let pda_seeds = Some(enhanced_args.pda_seeds); + let token_seeds = Some(enhanced_args.token_seeds); + let instruction_data = enhanced_args.instruction_data; + + if module.content.is_none() { + return Err(syn::Error::new_spanned(&module, "Module must have a body")); + } + + if account_types.is_empty() { + return Err(syn::Error::new_spanned(&module, "At least one account type must be specified")); + } + + let content = module.content.as_mut().unwrap(); + + // Generate the CTokenAccountVariant enum automatically from token_seeds + let ctoken_enum = if let Some(ref token_seed_specs) = token_seeds { + if !token_seed_specs.is_empty() { + generate_ctoken_account_variant_enum(token_seed_specs)? + } else { + quote! { + // No CToken variants - generate empty enum for compatibility + #[derive(AnchorSerialize, AnchorDeserialize, Debug, Clone, Copy)] + #[repr(u8)] + pub enum CTokenAccountVariant {} + } + } + } else { + quote! { + // No CToken variants - generate empty enum for compatibility + #[derive(AnchorSerialize, AnchorDeserialize, Debug, Clone, Copy)] + #[repr(u8)] + pub enum CTokenAccountVariant {} + } + }; + + // Generate the compressed_account_variant enum automatically + let mut account_types_stream = TokenStream::new(); + for (i, account_type) in account_types.iter().enumerate() { + if i > 0 { + account_types_stream.extend(quote! { , }); + } + account_types_stream.extend(quote! { #account_type }); + } + let enum_and_traits = crate::variant_enum::compressed_account_variant(account_types_stream)?; + + // Extract required accounts from seed expressions + let required_accounts = extract_required_accounts_from_seeds(&pda_seeds, &token_seeds)?; + + // Generate the DecompressAccountsIdempotent accounts struct with required accounts + let decompress_accounts = generate_decompress_accounts_struct(&required_accounts)?; + + // Generate match arms for decompress instruction using the account types + let decompress_match_arms: Result> = account_types.iter().map(|name| { + let name_str = name.to_string(); + + // Generate seed derivation from PDA seed specification - NO FALLBACKS! + let seed_call = if let Some(ref pda_seed_specs) = pda_seeds { + if let Some(spec) = pda_seed_specs.iter().find(|s| s.variant.to_string() == name_str) { + // Generate dynamic seed derivation from the specification + generate_pda_seed_derivation(spec, &instruction_data)? + } else { + return Err(syn::Error::new_spanned( + name, + format!("No seed specification provided for account type '{}'. All accounts must have seed specifications.", name_str) + )) + } + } else { + return Err(syn::Error::new_spanned( + name, + "No seed specifications provided. Use the new syntax: AccountType = (\"seed\", data.field)" + )) + }; + + Ok(quote! { + CompressedAccountVariant::#name(data) => { + let (seeds_vec, _) = #seed_call; + + let compressed_infos = light_sdk::compressible::prepare_account_for_decompression_idempotent::<#name>( + &crate::ID, + data, + light_sdk::compressible::into_compressed_meta_with_address( + &compressed_data.meta, + &solana_accounts[i], + address_space, + &crate::ID, + ), + &solana_accounts[i], + &ctx.accounts.rent_payer, + &cpi_accounts, + seeds_vec + .iter() + .map(|v| v.as_slice()) + .collect::>() + .as_slice(), + )?; + compressed_pda_infos.extend(compressed_infos); + } + }) + }).collect(); + let decompress_match_arms = decompress_match_arms?; + + // Generate unreachable match arms for Packed variants (PDA types are unpacked, not packed) + let packed_unreachable_arms = account_types.iter().map(|name| { + let packed_name = format_ident!("Packed{}", name); + quote! { + CompressedAccountVariant::#packed_name(_) => { + unreachable!(); + } + } + }); + + // Generate trait-based system for TRULY generic CToken variant handling + let ctoken_trait_system: syn::ItemMod = syn::parse_quote! { + /// Trait-based system for generic CToken variant seed handling + /// Users implement this trait for their CTokenAccountVariant enum + pub mod ctoken_seed_system { + use super::*; + + /// Context struct providing access to ALL instruction accounts + /// This gives users access to any account in the instruction context + pub struct CTokenSeedContext<'a, 'info> { + pub accounts: &'a DecompressAccountsIdempotent<'info>, + pub remaining_accounts: &'a [anchor_lang::prelude::AccountInfo<'info>], + pub fee_payer: &'a Pubkey, + pub owner: &'a Pubkey, + pub mint: &'a Pubkey, + // Users can access any account via ctx.accounts.field_name + } + + /// Trait that CToken variants implement to provide seed derivation + /// Completely extensible - users can implement ANY seed logic with access to ALL accounts + pub trait CTokenSeedProvider { + fn get_seeds<'a, 'info>(&self, ctx: &CTokenSeedContext<'a, 'info>) -> (Vec>, Pubkey); + } + } + }; + + // Generate the decompress instruction + let decompress_instruction: ItemFn = syn::parse_quote! { + /// Auto-generated decompress_accounts_idempotent instruction + pub fn decompress_accounts_idempotent<'info>( + ctx: Context<'_, '_, '_, 'info, DecompressAccountsIdempotent<'info>>, + proof: light_sdk::instruction::ValidityProof, + compressed_accounts: Vec, + system_accounts_offset: u8, + ) -> Result<()> { + let compression_config = light_sdk::compressible::CompressibleConfig::load_checked( + &ctx.accounts.config, + &crate::ID, + )?; + let address_space = compression_config.address_space[0]; + + let (mut has_tokens, mut has_pdas) = (false, false); + for c in &compressed_accounts { + match c.data { + CompressedAccountVariant::CompressibleTokenAccountPacked(_) => { + has_tokens = true; + } + _ => has_pdas = true, + } + if has_tokens && has_pdas { + break; + } + } + + let cpi_accounts = if has_tokens && has_pdas { + light_sdk_types::CpiAccountsSmall::new_with_config( + ctx.accounts.fee_payer.as_ref(), + &ctx.remaining_accounts[system_accounts_offset as usize..], + light_sdk_types::CpiAccountsConfig::new_with_cpi_context(LIGHT_CPI_SIGNER), + ) + } else { + light_sdk_types::CpiAccountsSmall::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 mut compressed_token_accounts = Vec::new(); + let mut compressed_pda_infos = Vec::new(); + + for (i, compressed_data) in compressed_accounts.clone().into_iter().enumerate() { + let unpacked_data = compressed_data + .data + .unpack(cpi_accounts.post_system_accounts().unwrap())?; + + match unpacked_data { + #(#decompress_match_arms)* + #(#packed_unreachable_arms)* + CompressedAccountVariant::CompressibleTokenAccountPacked(data) => { + compressed_token_accounts.push((data, compressed_data.meta)); + } + CompressedAccountVariant::CompressibleTokenData(_) => { + unreachable!(); + } + } + } + + let has_pdas = !compressed_pda_infos.is_empty(); + let has_tokens = !compressed_token_accounts.is_empty(); + + if !has_pdas && !has_tokens { + msg!("All accounts already initialized."); + return Ok(()); + } + + let fee_payer = ctx.accounts.fee_payer.as_ref(); + let authority = cpi_accounts.authority().unwrap(); + let cpi_context = cpi_accounts.cpi_context().unwrap(); + + if has_pdas && has_tokens { + let system_cpi_accounts = light_sdk_types::cpi_context_write::CpiContextWriteAccounts { + fee_payer, + authority, + cpi_context, + cpi_signer: LIGHT_CPI_SIGNER, + }; + + let cpi_inputs = light_sdk::cpi::CpiInputs::new_first_cpi( + compressed_pda_infos, + Vec::new(), + ); + cpi_inputs.invoke_light_system_program_cpi_context(system_cpi_accounts)?; + } else if has_pdas { + let cpi_inputs = light_sdk::cpi::CpiInputs::new(proof, compressed_pda_infos); + cpi_inputs.invoke_light_system_program_small(cpi_accounts.clone())?; + } + + // Handle token account decompression + let mut token_decompress_indices = Vec::new(); + let mut token_signers_seeds = Vec::new(); + let packed_accounts = cpi_accounts.post_system_accounts().unwrap(); + + for (token_data, meta) in compressed_token_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(); + + // ✅ TRULY GENERIC CToken variant handling using trait dispatch + // Users get access to ALL instruction accounts via ctx.accounts + // NO NEED TO MODIFY THE MACRO - completely extensible by users + use crate::ctoken_seed_system::{CTokenSeedProvider, CTokenSeedContext}; + + let seed_context = CTokenSeedContext { + accounts: &ctx.accounts, + remaining_accounts: ctx.remaining_accounts, + fee_payer: &fee_payer.key(), + owner: &owner_info.key(), + mint: &mint_info.key(), + }; + + let ctoken_signer_seeds = token_data.variant.get_seeds(&seed_context).0; + + light_compressed_token_sdk::create_compressible_token_account( + authority, + fee_payer, + &owner_info, + &mint_info, + cpi_accounts.system_program().unwrap(), + ctx.accounts.compressed_token_program.as_ref().unwrap(), + &ctoken_signer_seeds + .iter() + .map(|s| s.as_slice()) + .collect::>(), + fee_payer, // rent_auth + fee_payer, // rent_recipient + 0, // slots_until_compression + )?; + + let decompress_index = light_compressed_token_sdk::instructions::DecompressFullIndices::from((token_data.token_data, meta, owner_index)); + + token_decompress_indices.push(decompress_index); + token_signers_seeds.extend(ctoken_signer_seeds); + } + + if has_tokens { + 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 = vec![fee_payer.to_account_info()]; + all_account_infos.extend( + ctx.accounts + .compressed_token_cpi_authority + .to_account_infos(), + ); + all_account_infos.extend(ctx.accounts.compressed_token_program.to_account_infos()); + all_account_infos.extend(ctx.accounts.rent_payer.to_account_infos()); + all_account_infos.extend(ctx.accounts.config.to_account_infos()); + all_account_infos.extend(cpi_accounts.to_account_infos()); + + let seed_refs = token_signers_seeds + .iter() + .map(|s| s.as_slice()) + .collect::>(); + anchor_lang::solana_program::program::invoke_signed( + &ctoken_ix, + all_account_infos.as_slice(), + &[seed_refs.as_slice()], + )?; + } + Ok(()) + } + }; + + // Generate the CompressAccountsIdempotent accounts struct + let compress_accounts: syn::ItemStruct = syn::parse_quote! { + #[derive(Accounts)] + pub struct CompressAccountsIdempotent<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + /// The global config account + /// CHECK: Config is validated by the SDK's load_checked method + pub config: AccountInfo<'info>, + /// Rent recipient - must match config + /// CHECK: Rent recipient is validated against the config + #[account(mut)] + pub rent_recipient: AccountInfo<'info>, + + /// CHECK: compression_authority must be the rent_authority defined when creating the token account. + #[account(mut)] + pub token_compression_authority: AccountInfo<'info>, + + // Optional token-specific accounts (only needed when compressing token accounts) + /// Compressed token program + /// CHECK: Program ID validated to be cTokenmWW8bLPjZEBAUgYy3zKxQZW6VKi7bqNFEVv3m + pub compressed_token_program: Option>, + + /// CPI authority PDA of the compressed token program + /// CHECK: PDA derivation validated with seeds ["cpi_authority"] and bump 254 + pub compressed_token_cpi_authority: Option>, + } + }; + + // Generate compress match arms for each account type with dedicated vectors + let compress_match_arms = account_types.iter().map(|name| { + quote! { + d if d == #name::discriminator() => { + let mut anchor_account = anchor_lang::prelude::Account::<#name>::try_from(account_info)?; + + let compressed_info = light_sdk::compressible::compress_account::prepare_account_for_compression::<#name>( + &crate::ID, + &mut anchor_account, + &meta, + &cpi_accounts, + &compression_config.compression_delay, + &compression_config.address_space, + )?; + + // Store in type-specific vector for proper closing + #name.push(anchor_account); + compressed_pda_infos.push(compressed_info); + } + } + }); + + // Generate the compress instruction + let compress_instruction: syn::ItemFn = syn::parse_quote! { + /// Auto-generated compress_accounts_idempotent instruction + pub fn compress_accounts_idempotent<'info>( + ctx: Context<'_, '_, 'info, 'info, CompressAccountsIdempotent<'info>>, + proof: light_sdk::instruction::ValidityProof, + compressed_accounts: Vec, + signer_seeds: Vec>>, + system_accounts_offset: u8, + ) -> Result<()> { + let compression_config = light_sdk::compressible::CompressibleConfig::load_checked( + &ctx.accounts.config, + &crate::ID, + )?; + if ctx.accounts.rent_recipient.key() != compression_config.rent_recipient { + return err!(ErrorCode::InvalidRentRecipient); + } + + let cpi_accounts = light_sdk_types::CpiAccountsSmall::new( + ctx.accounts.fee_payer.as_ref(), + &ctx.remaining_accounts[system_accounts_offset as usize..], + LIGHT_CPI_SIGNER, + ); + + // We use signer_seeds because compressed_accounts can be != accounts to compress + let pda_accounts_start = ctx.remaining_accounts.len() - signer_seeds.len(); + let solana_accounts = &ctx.remaining_accounts[pda_accounts_start..]; + + // Initialize collections for different account types + let mut token_accounts_to_compress = Vec::new(); + let mut compressed_pda_infos = Vec::new(); + // Create dedicated vectors for each account type for proper closing + #(let mut #account_types = 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 == &light_ctoken_types::COMPRESSED_TOKEN_PROGRAM_ID.into() { + if let Ok(token_account) = anchor_lang::prelude::InterfaceAccount::::try_from(account_info) { + let account_signer_seeds = signer_seeds[i].clone(); + token_accounts_to_compress.push( + light_compressed_token_sdk::TokenAccountToCompress { + token_account, + signer_seeds: account_signer_seeds, + }, + ); + } + } else if account_info.owner == &crate::ID { + let data = account_info.try_borrow_data()?; + let discriminator = &data[0..8]; + let meta = compressed_accounts[i]; + + // Generic PDA account handling + match discriminator { + #(#compress_match_arms)* + _ => { + panic!("Trying to compress with invalid account discriminator"); + } + } + } + } + + let has_pdas = !compressed_pda_infos.is_empty(); + let has_tokens = !token_accounts_to_compress.is_empty(); + + // 1. Compress and close token accounts in one CPI (no proof) + if has_tokens { + light_compressed_token_sdk::compress_and_close_token_accounts( + crate::ID, + &ctx.accounts.fee_payer, + cpi_accounts.authority().unwrap(), + ctx.accounts.compressed_token_cpi_authority.as_ref().unwrap(), + ctx.accounts.compressed_token_program.as_ref().unwrap(), + &ctx.accounts.config, + &ctx.accounts.rent_recipient, + ctx.remaining_accounts, + token_accounts_to_compress, + LIGHT_CPI_SIGNER, + )?; + } + + // 2. Compress and close PDAs in another CPI (with proof) + if has_pdas { + let cpi_inputs = light_sdk::cpi::CpiInputs::new(proof, compressed_pda_infos); + cpi_inputs.invoke_light_system_program_small(cpi_accounts)?; + } + + // Close all PDA accounts using Anchor's proper close method + #( + for anchor_account in #account_types.iter() { + anchor_account.close(ctx.accounts.rent_recipient.clone())?; + } + )* + + Ok(()) + } + }; + + // Generate compression config instructions (same as old add_compressible_instructions macro) + let init_config_accounts: syn::ItemStruct = syn::parse_quote! { + #[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>, + } + }; + + let update_config_accounts: syn::ItemStruct = syn::parse_quote! { + #[derive(Accounts)] + pub struct UpdateCompressionConfig<'info> { + /// CHECK: config account is validated by the SDK + #[account(mut)] + pub config: AccountInfo<'info>, + /// CHECK: authority must be the current update authority + pub authority: Signer<'info>, + } + }; + + let init_config_instruction: syn::ItemFn = syn::parse_quote! { + /// Initialize compression config for the program + pub fn initialize_compression_config<'info>( + ctx: Context<'_, '_, '_, 'info, InitializeCompressionConfig<'info>>, + compression_delay: u32, + rent_recipient: Pubkey, + address_space: Vec, + ) -> Result<()> { + light_sdk::compressible::process_initialize_compression_config_checked( + &ctx.accounts.config.to_account_info(), + &ctx.accounts.authority.to_account_info(), + &ctx.accounts.program_data.to_account_info(), + &rent_recipient, + address_space, + compression_delay, + 0, // one global config for now, so bump is 0. + &ctx.accounts.payer.to_account_info(), + &ctx.accounts.system_program.to_account_info(), + &crate::ID, + ).map_err(|e| anchor_lang::error::Error::from(e)) + } + }; + + let update_config_instruction: syn::ItemFn = syn::parse_quote! { + /// Update compression config for the program + pub fn update_compression_config<'info>( + ctx: Context<'_, '_, '_, 'info, UpdateCompressionConfig<'info>>, + new_compression_delay: Option, + new_rent_recipient: Option, + new_address_space: Option>, + new_update_authority: Option, + ) -> Result<()> { + light_sdk::compressible::process_update_compression_config( + ctx.accounts.config.as_ref(), + ctx.accounts.authority.as_ref(), + new_update_authority.as_ref(), + new_rent_recipient.as_ref(), + new_address_space, + new_compression_delay, + &crate::ID, + ).map_err(|e| anchor_lang::error::Error::from(e)) + } + }; + + // Add all generated items to the module + content.1.push(Item::Struct(decompress_accounts)); + content.1.push(Item::Fn(decompress_instruction)); + content.1.push(Item::Struct(compress_accounts)); + content.1.push(Item::Fn(compress_instruction)); + content.1.push(Item::Struct(init_config_accounts)); + content.1.push(Item::Struct(update_config_accounts)); + content.1.push(Item::Fn(init_config_instruction)); + content.1.push(Item::Fn(update_config_instruction)); + + // Generate automatic CTokenSeedProvider implementation + let ctoken_implementation = if let Some(ref seeds) = token_seeds { + if !seeds.is_empty() { + generate_ctoken_seed_provider_implementation(seeds)? + } else { + quote! { + // No CToken variants specified - implementation not needed + } + } + } else { + quote! { + // No CToken variants specified - implementation not needed + } + }; + + // Generate public client-side seed functions for external consumption + let client_seed_functions = generate_client_seed_functions(&account_types, &pda_seeds, &token_seeds, &instruction_data)?; + + Ok(quote! { + // Auto-generated CTokenAccountVariant enum + #ctoken_enum + + // Auto-generated CompressedAccountVariant enum and traits + #enum_and_traits + + // Auto-generated public seed functions for client consumption + #client_seed_functions + + // Generate the trait system OUTSIDE the module so users can implement it + #ctoken_trait_system + + // Auto-generated CTokenSeedProvider implementation + #ctoken_implementation + + // Suppress snake_case warnings for account type names in macro usage + #[allow(non_snake_case)] + #module + }) +} + +/// Generate CTokenAccountVariant enum automatically from token seed specifications +fn generate_ctoken_account_variant_enum(token_seeds: &[TokenSeedSpec]) -> Result { + let variants = token_seeds.iter().enumerate().map(|(index, spec)| { + let variant_name = &spec.variant; + let index_u8 = index as u8; + quote! { + #variant_name = #index_u8, + } + }); + + Ok(quote! { + /// Auto-generated CTokenAccountVariant enum from token seed specifications + #[derive(AnchorSerialize, AnchorDeserialize, Debug, Clone, Copy)] + #[repr(u8)] + pub enum CTokenAccountVariant { + #(#variants)* + } + }) +} + +/// Generate CTokenSeedProvider implementation from token seed specifications +fn generate_ctoken_seed_provider_implementation( + token_seeds: &[TokenSeedSpec], +) -> Result { + let mut match_arms = Vec::new(); + + for spec in token_seeds { + let variant_name = &spec.variant; + + // Generate bindings for any expressions that need them + let mut bindings = Vec::new(); + let mut seed_refs = Vec::new(); + + for (i, seed) in spec.seeds.iter().enumerate() { + match seed { + SeedElement::Literal(lit) => { + let value = lit.value(); + seed_refs.push(quote! { #value.as_bytes() }); + } + SeedElement::Expression(expr) => { + // For CToken seeds, we need to handle account references specially + // ctx.accounts.mint -> ctx.accounts.mint.key().as_ref() + let mut handled = false; + + match expr { + syn::Expr::Field(field_expr) => { + // Check if this is ctx.accounts.field_name + if let syn::Member::Named(field_name) = &field_expr.member { + if let syn::Expr::Field(nested_field) = &*field_expr.base { + if let syn::Member::Named(base_name) = &nested_field.member { + if base_name == "accounts" { + if let syn::Expr::Path(path) = &*nested_field.base { + if let Some(segment) = path.path.segments.first() { + if segment.ident == "ctx" { + // This is ctx.accounts.field_name + // In CTokenSeedContext, accounts are accessed via ctx.accounts.field_name + let binding_name = syn::Ident::new(&format!("seed_{}", i), expr.span()); + bindings.push(quote! { + let #binding_name = ctx.accounts.#field_name.key().to_bytes(); + }); + seed_refs.push(quote! { &#binding_name }); + handled = true; + } + } + } + } + } + } else if let syn::Expr::Path(path) = &*field_expr.base { + if let Some(segment) = path.path.segments.first() { + if segment.ident == "ctx" { + // This is ctx.field_name + let field_str = field_name.to_string(); + + // Check if it's a standard CTokenSeedContext field + if field_str == "fee_payer" || field_str == "owner" || field_str == "mint" { + // Standard field - use directly from ctx + let binding_name = syn::Ident::new(&format!("seed_{}", i), expr.span()); + bindings.push(quote! { + let #binding_name = ctx.#field_name.to_bytes(); + }); + seed_refs.push(quote! { &#binding_name }); + } else { + // Custom field - access via ctx.accounts + let binding_name = syn::Ident::new(&format!("seed_{}", i), expr.span()); + bindings.push(quote! { + let #binding_name = ctx.accounts.#field_name.key().to_bytes(); + }); + seed_refs.push(quote! { &#binding_name }); + } + handled = true; + } + } + } + } + } + _ => {} + } + + if !handled { + // Not a ctx.accounts reference, use as-is + seed_refs.push(quote! { (#expr).as_ref() }); + } + } + } + } + + let match_arm = quote! { + CTokenAccountVariant::#variant_name => { + #(#bindings)* + let seeds: &[&[u8]] = &[#(#seed_refs),*]; + let (pda, bump) = anchor_lang::prelude::Pubkey::find_program_address(seeds, &crate::ID); + let seeds_vec = seeds.iter().map(|s| s.to_vec()).collect::>(); + let mut seeds_vec = seeds_vec; + seeds_vec.push(vec![bump]); + (seeds_vec, pda) + } + }; + match_arms.push(match_arm); + } + + Ok(quote! { + /// Auto-generated CTokenSeedProvider implementation + impl ctoken_seed_system::CTokenSeedProvider for CTokenAccountVariant { + fn get_seeds<'a, 'info>( + &self, + ctx: &ctoken_seed_system::CTokenSeedContext<'a, 'info>, + ) -> (Vec>, anchor_lang::prelude::Pubkey) { + match self { + #(#match_arms)* + _ => { + unreachable!("CToken variant not configured with seeds") + } + } + } + } + }) +} + +/// Generate seed expressions from SeedElement specifications +fn generate_seed_expressions( + seeds: &Punctuated, +) -> Result> { + let mut expressions = Vec::new(); + + for seed in seeds { + let expr = match seed { + SeedElement::Literal(lit) => { + let value = lit.value(); + quote! { #value.as_bytes() } + } + SeedElement::Expression(expr) => { + // Handle ctx.accounts.field_name specially + match expr { + syn::Expr::Field(field_expr) => { + if let syn::Member::Named(field_name) = &field_expr.member { + if let syn::Expr::Field(nested_field) = &*field_expr.base { + if let syn::Member::Named(base_name) = &nested_field.member { + if base_name == "accounts" { + if let syn::Expr::Path(path) = &*nested_field.base { + if let Some(segment) = path.path.segments.first() { + if segment.ident == "ctx" { + // This is ctx.accounts.field_name - convert to key + quote! { ctx.accounts.#field_name.key().as_ref() } + } else { + quote! { (#expr).as_ref() } + } + } else { + quote! { (#expr).as_ref() } + } + } else { + quote! { (#expr).as_ref() } + } + } else { + quote! { (#expr).as_ref() } + } + } else { + quote! { (#expr).as_ref() } + } + } else if let syn::Expr::Path(path) = &*field_expr.base { + if let Some(segment) = path.path.segments.first() { + if segment.ident == "ctx" { + // This is ctx.field_name - convert to key + quote! { ctx.accounts.#field_name.key().as_ref() } + } else { + quote! { (#expr).as_ref() } + } + } else { + quote! { (#expr).as_ref() } + } + } else { + quote! { (#expr).as_ref() } + } + } else { + quote! { (#expr).as_ref() } + } + } + syn::Expr::Path(path_expr) => { + if let Some(ident) = path_expr.path.get_ident() { + // This is a direct account reference - convert to key + quote! { ctx.accounts.#ident.key().as_ref() } + } else { + quote! { (#expr).as_ref() } + } + } + _ => { + quote! { (#expr).as_ref() } + } + } + } + }; + expressions.push(expr); + } + + Ok(expressions) +} + +/// Generate seed expressions with proper type handling +fn generate_seed_expressions_with_types( + seeds: &Punctuated, + instruction_data: &[InstructionDataSpec], +) -> Result> { + let mut expressions = Vec::new(); + + for seed in seeds { + let expr = match seed { + SeedElement::Literal(lit) => { + let value = lit.value(); + quote! { #value.as_bytes() } + } + SeedElement::Expression(expr) => { + match expr { + syn::Expr::Field(field_expr) => { + // Handle ctx.accounts.field_name, ctx.field_name, data.field + if let syn::Member::Named(field_name) = &field_expr.member { + match &*field_expr.base { + syn::Expr::Field(nested_field) => { + // Handle ctx.accounts.field_name + if let syn::Member::Named(base_name) = &nested_field.member { + if base_name == "accounts" { + if let syn::Expr::Path(path) = &*nested_field.base { + if let Some(segment) = path.path.segments.first() { + if segment.ident == "ctx" { + // This is ctx.accounts.field_name + quote! { ctx.accounts.#field_name.key().as_ref() } + } else { + quote! { (#expr).as_ref() } + } + } else { + quote! { (#expr).as_ref() } + } + } else { + quote! { (#expr).as_ref() } + } + } else { + quote! { (#expr).as_ref() } + } + } else { + quote! { (#expr).as_ref() } + } + } + syn::Expr::Path(path) => { + if let Some(segment) = path.path.segments.first() { + if segment.ident == "ctx" { + // This is ctx.field_name + quote! { ctx.accounts.#field_name.key().as_ref() } + } else if segment.ident == "data" { + // This is data.field - check type from instruction_data + if let Some(data_spec) = instruction_data.iter().find(|d| d.field_name == *field_name) { + if is_pubkey_type(&data_spec.field_type) { + quote! { data.#field_name.as_ref() } + } else { + // Numeric type needs to_le_bytes + quote! { data.#field_name.to_le_bytes().as_ref() } + } + } else { + // Default to as_ref if type not found + quote! { data.#field_name.as_ref() } + } + } else { + // Other + quote! { (#expr).as_ref() } + } + } else { + quote! { (#expr).as_ref() } + } + } + _ => { + quote! { (#expr).as_ref() } + } + } + } else { + quote! { (#expr).as_ref() } + } + } + syn::Expr::Path(path_expr) => { + // Handle direct account references + if let Some(ident) = path_expr.path.get_ident() { + // This is a direct account reference + quote! { ctx.accounts.#ident.key().as_ref() } + } else { + quote! { (#expr).as_ref() } + } + } + _ => { + quote! { (#expr).as_ref() } + } + } + } + }; + expressions.push(expr); + } + + Ok(expressions) +} + +/// Generate PDA seed derivation from specification +fn generate_pda_seed_derivation(spec: &TokenSeedSpec, _instruction_data: &[InstructionDataSpec]) -> Result { + // First, generate bindings for any expressions that need them + let mut bindings = Vec::new(); + let mut seed_refs = Vec::new(); + + for (i, seed) in spec.seeds.iter().enumerate() { + match seed { + SeedElement::Literal(lit) => { + let value = lit.value(); + seed_refs.push(quote! { #value.as_bytes() }); + } + SeedElement::Expression(expr) => { + // We need to handle different types of expressions differently + let mut handled = false; + + // Check for expressions that need special handling + match expr { + syn::Expr::MethodCall(mc) if mc.method == "to_le_bytes" => { + // This creates a temporary array, needs binding + let binding_name = syn::Ident::new(&format!("seed_binding_{}", i), expr.span()); + bindings.push(quote! { + let #binding_name = #expr; + }); + seed_refs.push(quote! { #binding_name.as_ref() }); + handled = true; + } + syn::Expr::Field(field_expr) => { + // Check if this is ctx.accounts.field_name + if let syn::Member::Named(field_name) = &field_expr.member { + if let syn::Expr::Field(nested_field) = &*field_expr.base { + if let syn::Member::Named(base_name) = &nested_field.member { + if base_name == "accounts" { + if let syn::Expr::Path(path) = &*nested_field.base { + if let Some(segment) = path.path.segments.first() { + if segment.ident == "ctx" { + // This is ctx.accounts.field_name - create binding for the key + let binding_name = syn::Ident::new(&format!("seed_binding_{}", i), expr.span()); + bindings.push(quote! { + let #binding_name = ctx.accounts.#field_name.key().to_bytes(); + }); + seed_refs.push(quote! { &#binding_name }); + handled = true; + } + } + } + } + } + } else if let syn::Expr::Path(path) = &*field_expr.base { + if let Some(segment) = path.path.segments.first() { + if segment.ident == "ctx" { + // This is ctx.field_name - create binding + let binding_name = syn::Ident::new(&format!("seed_binding_{}", i), expr.span()); + bindings.push(quote! { + let #binding_name = ctx.accounts.#field_name.key().to_bytes(); + }); + seed_refs.push(quote! { &#binding_name }); + handled = true; + } else if segment.ident == "data" { + // This is data.field - might need to_le_bytes + // Just use the expression as-is, will be handled by generate_seed_expressions + seed_refs.push(quote! { (#expr).as_ref() }); + handled = true; + } + } + } + } + } + _ => {} + } + + if !handled { + // Other expressions - use as-is + seed_refs.push(quote! { (#expr).as_ref() }); + } + } + } + } + + // Generate indices for accessing seeds array + let indices: Vec = (0..seed_refs.len()).collect(); + + Ok(quote! { + { + #(#bindings)* + let seeds: &[&[u8]] = &[ + #(#seed_refs,)* + ]; + let (pda, bump) = anchor_lang::prelude::Pubkey::find_program_address(seeds, &crate::ID); + let seeds_vec: Vec> = vec![ + #( + seeds[#indices].to_vec(), + )* + vec![bump], + ]; + (seeds_vec, pda) + } + }) +} + +/// Generate temporary bindings and references for seeds to avoid lifetime issues +fn generate_seed_bindings( + seeds: &Punctuated, +) -> Result<(Vec, Vec)> { + let mut temp_bindings = Vec::new(); + let mut seed_refs = Vec::new(); + + for (i, seed) in seeds.iter().enumerate() { + let temp_var = syn::Ident::new(&format!("seed_{}", i), proc_macro2::Span::call_site()); + + match seed { + SeedElement::Literal(lit) => { + let value = lit.value(); + temp_bindings.push(quote! { + let #temp_var = #value.as_bytes(); + }); + seed_refs.push(quote! { #temp_var }); + } + SeedElement::Expression(expr) => { + match expr { + syn::Expr::Field(field_expr) => { + // Handle ctx.accounts.field_name, ctx.field_name, data.field + if let syn::Member::Named(field_name) = &field_expr.member { + match &*field_expr.base { + syn::Expr::Field(nested_field) => { + // Handle ctx.accounts.field_name + if let syn::Member::Named(base_name) = &nested_field.member { + if base_name == "accounts" { + if let syn::Expr::Path(path) = &*nested_field.base { + if let Some(segment) = path.path.segments.first() { + if segment.ident == "ctx" { + // This is ctx.accounts.field_name + temp_bindings.push(quote! { + let #temp_var = ctx.accounts.#field_name.key().to_bytes(); + }); + seed_refs.push(quote! { #temp_var.as_ref() }); + } else { + temp_bindings.push(quote! { + let #temp_var = (#expr).as_ref(); + }); + seed_refs.push(quote! { #temp_var }); + } + } else { + temp_bindings.push(quote! { + let #temp_var = (#expr).as_ref(); + }); + seed_refs.push(quote! { #temp_var }); + } + } else { + temp_bindings.push(quote! { + let #temp_var = (#expr).as_ref(); + }); + seed_refs.push(quote! { #temp_var }); + } + } else { + temp_bindings.push(quote! { + let #temp_var = (#expr).as_ref(); + }); + seed_refs.push(quote! { #temp_var }); + } + } else { + temp_bindings.push(quote! { + let #temp_var = (#expr).as_ref(); + }); + seed_refs.push(quote! { #temp_var }); + } + } + syn::Expr::Path(path) => { + if let Some(segment) = path.path.segments.first() { + if segment.ident == "ctx" { + // This is ctx.field_name + temp_bindings.push(quote! { + let #temp_var = ctx.accounts.#field_name.key().to_bytes(); + }); + seed_refs.push(quote! { #temp_var.as_ref() }); + } else if segment.ident == "data" { + // This is data.field - use as-ref for Pubkey, to_le_bytes for numbers + temp_bindings.push(quote! { + let #temp_var = (#expr).as_ref(); + }); + seed_refs.push(quote! { #temp_var }); + } else { + // Other expressions + temp_bindings.push(quote! { + let #temp_var = (#expr).as_ref(); + }); + seed_refs.push(quote! { #temp_var }); + } + } else { + temp_bindings.push(quote! { + let #temp_var = (#expr).as_ref(); + }); + seed_refs.push(quote! { #temp_var }); + } + } + _ => { + temp_bindings.push(quote! { + let #temp_var = (#expr).as_ref(); + }); + seed_refs.push(quote! { #temp_var }); + } + } + } else { + temp_bindings.push(quote! { + let #temp_var = (#expr).as_ref(); + }); + seed_refs.push(quote! { #temp_var }); + } + } + syn::Expr::Path(path_expr) => { + // Handle direct account references + if let Some(ident) = path_expr.path.get_ident() { + temp_bindings.push(quote! { + let #temp_var = ctx.accounts.#ident.key().to_bytes(); + }); + seed_refs.push(quote! { #temp_var.as_ref() }); + } else { + temp_bindings.push(quote! { + let #temp_var = (#expr).as_ref(); + }); + seed_refs.push(quote! { #temp_var }); + } + } + _ => { + temp_bindings.push(quote! { + let #temp_var = (#expr).as_ref(); + }); + seed_refs.push(quote! { #temp_var }); + } + } + } + } + } + + Ok((temp_bindings, seed_refs)) +} + +/// Generate public client-side seed functions for external consumption +fn generate_client_seed_functions( + _account_types: &[Ident], + pda_seeds: &Option>, + token_seeds: &Option>, + instruction_data: &[InstructionDataSpec], +) -> Result { + let mut functions = Vec::new(); + + // Generate PDA seed functions - FULLY GENERIC based on seed specifications + if let Some(pda_seed_specs) = pda_seeds { + for spec in pda_seed_specs { + let variant_name = &spec.variant; + let function_name = format_ident!("get_{}_seeds", variant_name.to_string().to_lowercase()); + + // Extract parameters and expressions from the seed specification + let (parameters, seed_expressions) = analyze_seed_spec_for_client(spec, instruction_data)?; + + let function = quote! { + /// Auto-generated client-side seed function + pub fn #function_name(#(#parameters),*) -> (Vec>, anchor_lang::prelude::Pubkey) { + let seed_values: Vec> = vec![ + #( + (#seed_expressions).to_vec(), + )* + ]; + let seed_slices: Vec<&[u8]> = seed_values.iter().map(|v| v.as_slice()).collect(); + let (pda, bump) = anchor_lang::prelude::Pubkey::find_program_address(&seed_slices, &crate::ID); + let mut seeds_vec = seed_values; + seeds_vec.push(vec![bump]); + (seeds_vec, pda) + } + }; + functions.push(function); + } + } + + // Generate CToken seed functions - FULLY GENERIC based on seed specifications + if let Some(token_seed_specs) = token_seeds { + for spec in token_seed_specs { + let variant_name = &spec.variant; + let function_name = format_ident!("get_{}_seeds", variant_name.to_string().to_lowercase()); + + // Extract parameters and expressions from the seed specification + let (parameters, seed_expressions) = analyze_seed_spec_for_client(spec, instruction_data)?; + + let function = quote! { + /// Auto-generated client-side CToken seed function + pub fn #function_name(#(#parameters),*) -> (Vec>, anchor_lang::prelude::Pubkey) { + let seed_values: Vec> = vec![ + #( + (#seed_expressions).to_vec(), + )* + ]; + let seed_slices: Vec<&[u8]> = seed_values.iter().map(|v| v.as_slice()).collect(); + let (pda, bump) = anchor_lang::prelude::Pubkey::find_program_address(&seed_slices, &crate::ID); + let mut seeds_vec = seed_values; + seeds_vec.push(vec![bump]); + (seeds_vec, pda) + } + }; + functions.push(function); + } + } + + Ok(quote! { + #(#functions)* + }) +} + +/// Analyze seed specification and generate parameters + expressions for client functions +fn analyze_seed_spec_for_client( + spec: &TokenSeedSpec, + instruction_data: &[InstructionDataSpec] +) -> Result<(Vec, Vec)> { + let mut parameters = Vec::new(); + let mut expressions = Vec::new(); + + for seed in &spec.seeds { + match seed { + SeedElement::Literal(lit) => { + // String literals don't need parameters + let value = lit.value(); + expressions.push(quote! { #value.as_bytes() }); + } + SeedElement::Expression(expr) => { + // Analyze the expression to extract parameter and generate client expression + match expr { + syn::Expr::Field(field_expr) => { + // Handle data.field or ctx.field + if let syn::Member::Named(field_name) = &field_expr.member { + if let syn::Expr::Path(path) = &*field_expr.base { + if let Some(segment) = path.path.segments.first() { + if segment.ident == "data" { + // This is a data field - look up the type from instruction_data + if let Some(data_spec) = instruction_data.iter().find(|d| d.field_name == *field_name) { + let param_type = &data_spec.field_type; + // Use references for Pubkey, direct values for numeric types + let param_with_ref = if is_pubkey_type(param_type) { + quote! { #field_name: &#param_type } + } else { + quote! { #field_name: #param_type } + }; + parameters.push(param_with_ref); + expressions.push(quote! { #field_name.as_ref() }); + } else { + return Err(syn::Error::new_spanned( + field_name, + format!("data.{} used in seeds but no type specified. Add: {} = Pubkey (or u8, u16, u64)", field_name, field_name) + )); + } + } else { + // ctx.field - determine type by field name + let param_type = if field_name.to_string().contains("owner") || + field_name.to_string().contains("fee_payer") || + field_name.to_string().contains("mint") { + quote! { &anchor_lang::prelude::Pubkey } + } else { + quote! { &anchor_lang::prelude::Pubkey } // Default to Pubkey + }; + parameters.push(quote! { #field_name: #param_type }); + expressions.push(quote! { #field_name.as_ref() }); + } + } + } + } + } + syn::Expr::Path(path_expr) => { + // Handle direct account field references like: amm_config, token_0_mint, pool_state + if let Some(ident) = path_expr.path.get_ident() { + // This is an account field reference - assume it's a Pubkey for client functions + parameters.push(quote! { #ident: &anchor_lang::prelude::Pubkey }); + expressions.push(quote! { #ident.as_ref() }); + } + } + syn::Expr::MethodCall(method_call) => { + // Handle method calls like amm_config.key().as_ref(), data.session_id.to_le_bytes(), etc. + if let syn::Expr::Field(field_expr) = &*method_call.receiver { + if let syn::Member::Named(field_name) = &field_expr.member { + if let syn::Expr::Path(path) = &*field_expr.base { + if let Some(segment) = path.path.segments.first() { + if segment.ident == "data" { + // This is a data field - look up the type from instruction_data + if let Some(data_spec) = instruction_data.iter().find(|d| d.field_name == *field_name) { + let param_type = &data_spec.field_type; + // Use references for Pubkey, direct values for numeric types + let param_with_ref = if is_pubkey_type(param_type) { + quote! { #field_name: &#param_type } + } else { + quote! { #field_name: #param_type } + }; + parameters.push(param_with_ref); + + // Generate expression for client function + let method_name = &method_call.method; + expressions.push(quote! { #field_name.#method_name().as_ref() }); + } else { + return Err(syn::Error::new_spanned( + field_name, + format!("data.{} used in seeds but no type specified. Add: {} = Pubkey (or u8, u16, u64)", field_name, field_name) + )); + } + } + } + } + } + } else if let syn::Expr::Path(path_expr) = &*method_call.receiver { + // Handle direct account method calls like amm_config.key().as_ref() + if let Some(ident) = path_expr.path.get_ident() { + // This is an account field reference - assume it's a Pubkey for client functions + parameters.push(quote! { #ident: &anchor_lang::prelude::Pubkey }); + expressions.push(quote! { #ident.as_ref() }); + } + } + } + _ => { + // For other expressions, try to use as-is + expressions.push(quote! { (#expr).as_ref() }); + } + } + } + } + } + + Ok((parameters, expressions)) +} + +/// Check if a type is Pubkey-like +fn is_pubkey_type(ty: &syn::Type) -> bool { + if let syn::Type::Path(type_path) = ty { + if let Some(segment) = type_path.path.segments.last() { + let type_name = segment.ident.to_string(); + type_name == "Pubkey" || type_name.contains("Pubkey") + } else { + false + } + } else { + false + } +} + +/// Extract required account names from seed expressions +fn extract_required_accounts_from_seeds( + pda_seeds: &Option>, + token_seeds: &Option>, +) -> Result> { + let mut required_accounts = std::collections::HashSet::new(); + + // Extract from PDA seeds + if let Some(pda_seed_specs) = pda_seeds { + for spec in pda_seed_specs { + extract_accounts_from_seed_spec(spec, &mut required_accounts)?; + } + } + + // Extract from token seeds + if let Some(token_seed_specs) = token_seeds { + for spec in token_seed_specs { + extract_accounts_from_seed_spec(spec, &mut required_accounts)?; + } + } + + Ok(required_accounts.into_iter().collect()) +} + +/// Extract account names from a single seed specification +/// Extract account name from an expression, handling method chains +/// Simply looks for ctx.accounts.FIELD_NAME pattern and extracts FIELD_NAME +fn extract_account_from_expr( + expr: &syn::Expr, + required_accounts: &mut std::collections::HashSet, +) { + match expr { + syn::Expr::MethodCall(method_call) => { + // For method calls, check the receiver + // e.g., ctx.accounts.mint.key().as_ref() -> check ctx.accounts.mint.key() + extract_account_from_expr(&*method_call.receiver, required_accounts); + } + syn::Expr::Field(field_expr) => { + // Check if this is ctx.accounts.FIELD_NAME + if let syn::Member::Named(field_name) = &field_expr.member { + if let syn::Expr::Field(nested_field) = &*field_expr.base { + if let syn::Member::Named(base_name) = &nested_field.member { + if base_name == "accounts" { + if let syn::Expr::Path(path) = &*nested_field.base { + if let Some(segment) = path.path.segments.first() { + if segment.ident == "ctx" { + // Found ctx.accounts.FIELD_NAME - extract FIELD_NAME + required_accounts.insert(field_name.to_string()); + return; // Found it, no need to recurse further + } + } + } + } + } + } else if let syn::Expr::Path(path) = &*field_expr.base { + if let Some(segment) = path.path.segments.first() { + if segment.ident == "ctx" && field_name != "accounts" { + // Found ctx.FIELD_NAME (shorthand) - treat as account + required_accounts.insert(field_name.to_string()); + return; + } + } + } + } + } + syn::Expr::Path(path_expr) => { + // Handle direct account references (just an identifier) + if let Some(ident) = path_expr.path.get_ident() { + let name = ident.to_string(); + // Skip "ctx" and "data" as they're not accounts + if name != "ctx" && name != "data" { + required_accounts.insert(name); + } + } + } + _ => {} + } +} + +fn extract_accounts_from_seed_spec( + spec: &TokenSeedSpec, + required_accounts: &mut std::collections::HashSet, +) -> Result<()> { + for seed in &spec.seeds { + match seed { + SeedElement::Literal(_) => { + // String literals don't require accounts + } + SeedElement::Expression(expr) => { + match expr { + syn::Expr::MethodCall(method_call) => { + // Recursively find the base account through method call chains + // e.g., ctx.accounts.mint.key().as_ref() -> extract "mint" + extract_account_from_expr(&*method_call.receiver, required_accounts); + } + syn::Expr::Path(_) | syn::Expr::Field(_) => { + // Use the helper function for all expressions + extract_account_from_expr(expr, required_accounts); + } + _ => { + // Other expressions - try to extract identifiers + } + } + } + } + } + Ok(()) +} + +/// Generate DecompressAccountsIdempotent struct with required accounts +fn generate_decompress_accounts_struct(required_accounts: &[String]) -> Result { + let mut account_fields = vec![ + // Standard fields + quote! { + #[account(mut)] + pub fee_payer: Signer<'info> + }, + quote! { + /// UNCHECKED: Anyone can pay to init. + #[account(mut)] + pub rent_payer: Signer<'info> + }, + quote! { + /// The global config account + /// CHECK: load_checked. + pub config: AccountInfo<'info> + }, + quote! { + /// Compressed token program + /// CHECK: Program ID validated to be cTokenmWW8bLPjZEBAUgYy3zKxQZW6VKi7bqNFEVv3m + pub compressed_token_program: Option> + }, + quote! { + /// CPI authority PDA of the compressed token program + /// CHECK: PDA derivation validated with seeds ["cpi_authority"] and bump 254 + pub compressed_token_cpi_authority: Option> + }, + ]; + + // Add required accounts as unchecked accounts (skip standard fields) + let standard_fields = ["fee_payer", "rent_payer", "config", "compressed_token_program", "compressed_token_cpi_authority"]; + + for account_name in required_accounts { + if !standard_fields.contains(&account_name.as_str()) { + let account_ident = syn::Ident::new(account_name, proc_macro2::Span::call_site()); + account_fields.push(quote! { + /// CHECK: Required for seed derivation - validated by program logic + pub #account_ident: UncheckedAccount<'info> + }); + } + } + + let struct_def = quote! { + #[derive(Accounts)] + pub struct DecompressAccountsIdempotent<'info> { + #(#account_fields,)* + } + }; + + Ok(syn::parse2(struct_def)?) +} + +// Client seed function generation complete! 🎉 + +// No more hardcoded fallbacks! Everything is now auto-generated! 🎉 diff --git a/sdk-libs/macros/src/cpi_signer.rs b/sdk-libs/macros/src/cpi_signer.rs index d27403df1d..87747e20b4 100644 --- a/sdk-libs/macros/src/cpi_signer.rs +++ b/sdk-libs/macros/src/cpi_signer.rs @@ -2,6 +2,8 @@ use proc_macro::TokenStream; use quote::quote; use syn::{parse_macro_input, LitStr}; +// TODO: review where needed. +#[allow(dead_code)] pub fn derive_light_cpi_signer_pda(input: TokenStream) -> TokenStream { // Parse the input - just a program ID string literal let program_id_lit = parse_macro_input!(input as LitStr); diff --git a/sdk-libs/macros/src/derive_seeds.rs b/sdk-libs/macros/src/derive_seeds.rs new file mode 100644 index 0000000000..73f84672e6 --- /dev/null +++ b/sdk-libs/macros/src/derive_seeds.rs @@ -0,0 +1,205 @@ +use proc_macro2::TokenStream; +use quote::{format_ident, quote}; +use syn::{ + parse::{Parse, ParseStream}, + punctuated::Punctuated, + Data, DeriveInput, Expr, Fields, Ident, LitStr, Result, Token, +}; + +/// Parse the seeds attribute content +struct SeedsAttribute { + seeds: Punctuated, +} + +enum SeedElement { + Literal(LitStr), + Field(Ident), + Expression(Expr), +} + +impl Parse for SeedElement { + fn parse(input: ParseStream) -> Result { + if input.peek(LitStr) { + Ok(SeedElement::Literal(input.parse()?)) + } else if input.peek(Ident) { + Ok(SeedElement::Field(input.parse()?)) + } else { + Ok(SeedElement::Expression(input.parse()?)) + } + } +} + +impl Parse for SeedsAttribute { + fn parse(input: ParseStream) -> Result { + Ok(SeedsAttribute { + seeds: Punctuated::parse_terminated(input)?, + }) + } +} + +/// Generates seed getter functions for PDA and token accounts +/// +/// Usage: +/// ```rust +/// #[derive(DeriveSeeds)] +/// #[seeds("user_record", owner)] +/// pub struct UserRecord { +/// pub owner: Pubkey, +/// // ... +/// } +/// +/// #[derive(DeriveSeeds)] +/// #[seeds("ctoken_signer", user, mint)] +/// #[token_account] +/// pub struct CTokenSigner { +/// pub user: Pubkey, +/// pub mint: Pubkey, +/// } +/// ``` +/// +/// This generates: +/// - `get_user_record_seeds(owner: &Pubkey) -> (Vec>, Pubkey)` +/// - `get_c_token_signer_seeds(user: &Pubkey, mint: &Pubkey) -> (Vec>, Pubkey)` +pub fn derive_seeds(input: DeriveInput) -> Result { + let struct_name = &input.ident; + + // Find the seeds attribute + let seeds_attr = input + .attrs + .iter() + .find(|attr| attr.path().is_ident("seeds")) + .ok_or_else(|| { + syn::Error::new_spanned( + &struct_name, + "DeriveSeeds requires a #[seeds(...)] attribute", + ) + })?; + + let seeds_content = seeds_attr.parse_args::()?; + + // Check if this is a token account + let is_token_account = input + .attrs + .iter() + .any(|attr| attr.path().is_ident("token_account")); + + // Get struct fields to determine parameters + let fields = match &input.data { + Data::Struct(data) => match &data.fields { + Fields::Named(fields) => &fields.named, + _ => { + return Err(syn::Error::new_spanned( + &input, + "DeriveSeeds only supports structs with named fields", + )); + } + }, + _ => { + return Err(syn::Error::new_spanned( + &input, + "DeriveSeeds only supports structs", + )); + } + }; + + // Generate function name + let fn_name = format_ident!( + "get_{}_seeds", + struct_name + .to_string() + .to_lowercase() + .replace("record", "_record") + .replace("session", "_session") + ); + + // Extract parameters from seeds that reference fields + let mut parameters = Vec::new(); + let mut seed_expressions = Vec::new(); + + for seed in &seeds_content.seeds { + match seed { + SeedElement::Literal(lit) => { + let lit_value = lit.value(); + seed_expressions.push(quote! { #lit_value.as_bytes() }); + } + SeedElement::Field(field_name) => { + // Find the field type + let field = fields + .iter() + .find(|f| f.ident.as_ref().map(|id| id == field_name).unwrap_or(false)) + .ok_or_else(|| { + syn::Error::new_spanned( + field_name, + format!("Field '{}' not found in struct", field_name), + ) + })?; + + let field_type = &field.ty; + parameters.push(quote! { #field_name: &#field_type }); + + // Handle different field types for seed generation + if is_pubkey_type(field_type) { + seed_expressions.push(quote! { #field_name.as_ref() }); + } else if is_u64_type(field_type) { + seed_expressions.push(quote! { #field_name.to_le_bytes().as_ref() }); + } else { + return Err(syn::Error::new_spanned( + field_type, + format!( + "Unsupported field type for seeds: {}", + quote! { #field_type } + ), + )); + } + } + SeedElement::Expression(expr) => { + seed_expressions.push(quote! { #expr }); + } + } + } + + // Generate the function - simplified approach matching the original manual implementation + let function_impl = quote! { + /// Auto-generated seed function for PDA account + pub fn #fn_name(#(#parameters),*) -> (Vec>, anchor_lang::prelude::Pubkey) { + let seeds = [#(#seed_expressions),*]; + let (pda, bump) = anchor_lang::prelude::Pubkey::find_program_address(&seeds, &crate::ID); + let bump_slice = vec![bump]; + let seeds_vec = vec![ + #( + (#seed_expressions).to_vec(), + )* + bump_slice, + ]; + (seeds_vec, pda) + } + }; + + Ok(function_impl) +} + +/// Check if a type is Pubkey +fn is_pubkey_type(ty: &syn::Type) -> bool { + if let syn::Type::Path(type_path) = ty { + if let Some(segment) = type_path.path.segments.last() { + segment.ident == "Pubkey" + } else { + false + } + } else { + false + } +} + +/// Check if a type is u64 +fn is_u64_type(ty: &syn::Type) -> bool { + if let syn::Type::Path(type_path) = ty { + if let Some(segment) = type_path.path.segments.last() { + segment.ident == "u64" + } else { + false + } + } else { + false + } +} diff --git a/sdk-libs/macros/src/discriminator.rs b/sdk-libs/macros/src/discriminator.rs index 1d289db888..be711224c0 100644 --- a/sdk-libs/macros/src/discriminator.rs +++ b/sdk-libs/macros/src/discriminator.rs @@ -4,14 +4,34 @@ use quote::quote; use syn::{ItemStruct, Result}; pub(crate) fn discriminator(input: ItemStruct) -> Result { + discriminator_with_hasher(input, false) +} + +pub(crate) fn discriminator_sha(input: ItemStruct) -> Result { + discriminator_with_hasher(input, true) +} + +fn discriminator_with_hasher(input: ItemStruct, is_sha: bool) -> Result { let account_name = &input.ident; let (impl_gen, type_gen, where_clause) = input.generics.split_for_impl(); let mut discriminator = [0u8; 8]; - discriminator.copy_from_slice(&Sha256::hash(account_name.to_string().as_bytes()).unwrap()[..8]); + + // When anchor-discriminator-compat feature is enabled, use "account:" prefix like Anchor does + #[cfg(feature = "anchor-discriminator-compat")] + let hash_input = format!("account:{}", account_name); + + #[cfg(not(feature = "anchor-discriminator-compat"))] + let hash_input = account_name.to_string(); + + discriminator.copy_from_slice(&Sha256::hash(hash_input.as_bytes()).unwrap()[..8]); let discriminator: proc_macro2::TokenStream = format!("{discriminator:?}").parse().unwrap(); + // For SHA256 variant, we could add specific logic here if needed + // Currently both variants work the same way since discriminator is just based on struct name + let _variant_marker = if is_sha { "sha256" } else { "poseidon" }; + Ok(quote! { impl #impl_gen LightDiscriminator for #account_name #type_gen #where_clause { const LIGHT_DISCRIMINATOR: [u8; 8] = #discriminator; @@ -44,7 +64,55 @@ mod tests { let output = discriminator(input).unwrap(); let output = output.to_string(); + assert!(output.contains("impl LightDiscriminator for MyAccount")); + + // The discriminator value will be different based on whether anchor-discriminator-compat is enabled + #[cfg(feature = "anchor-discriminator-compat")] + assert!(output.contains("account:MyAccount")); // This won't be visible in output, but logic uses it + + #[cfg(not(feature = "anchor-discriminator-compat"))] + assert!(output.contains("[181 , 255 , 112 , 42 , 17 , 188 , 66 , 199]")); + } + + #[test] + fn test_discriminator_sha() { + let input: ItemStruct = parse_quote! { + struct MyAccount { + a: u32, + b: i32, + c: u64, + d: i64, + } + }; + + let output = discriminator_sha(input).unwrap(); + let output = output.to_string(); + assert!(output.contains("impl LightDiscriminator for MyAccount")); assert!(output.contains("[181 , 255 , 112 , 42 , 17 , 188 , 66 , 199]")); } + + #[test] + fn test_discriminator_sha_large_struct() { + // Test that SHA256 discriminator can handle large structs (that would fail with regular hasher) + let input: ItemStruct = parse_quote! { + struct LargeAccount { + pub field1: u64, pub field2: u64, pub field3: u64, pub field4: u64, + pub field5: u64, pub field6: u64, pub field7: u64, pub field8: u64, + pub field9: u64, pub field10: u64, pub field11: u64, pub field12: u64, + pub field13: u64, pub field14: u64, pub field15: u64, + pub owner: solana_program::pubkey::Pubkey, + pub authority: solana_program::pubkey::Pubkey, + } + }; + + let result = discriminator_sha(input); + assert!( + result.is_ok(), + "SHA256 discriminator should handle large structs" + ); + + let output = result.unwrap().to_string(); + assert!(output.contains("impl LightDiscriminator for LargeAccount")); + } } diff --git a/sdk-libs/macros/src/hasher/data_hasher.rs b/sdk-libs/macros/src/hasher/data_hasher.rs index 2486fdd4b7..7d27bdc619 100644 --- a/sdk-libs/macros/src/hasher/data_hasher.rs +++ b/sdk-libs/macros/src/hasher/data_hasher.rs @@ -37,7 +37,14 @@ pub(crate) fn generate_data_hasher_impl( slices[num_flattned_fields] = element.as_slice(); } - H::hashv(slices.as_slice()) + let mut result = H::hashv(slices.as_slice())?; + + // Apply field size truncation for non-Poseidon hashers + if H::ID != 0 { + result[0] = 0; + } + + Ok(result) } } } @@ -59,10 +66,50 @@ pub(crate) fn generate_data_hasher_impl( println!("DataHasher::hash inputs {:?}", debug_prints); } } - H::hashv(&[ + let mut result = H::hashv(&[ #(#data_hasher_assignments.as_slice(),)* - ]) + ])?; + + // Apply field size truncation for non-Poseidon hashers + if H::ID != 0 { + result[0] = 0; + } + + Ok(result) + } + } + } + }; + + Ok(hasher_impl) +} + +/// SHA256-specific DataHasher implementation that serializes the whole struct +pub(crate) fn generate_data_hasher_impl_sha( + struct_name: &syn::Ident, + generics: &syn::Generics, +) -> Result { + let (impl_gen, type_gen, where_clause) = generics.split_for_impl(); + + let hasher_impl = quote! { + impl #impl_gen ::light_hasher::DataHasher for #struct_name #type_gen #where_clause { + fn hash(&self) -> ::std::result::Result<[u8; 32], ::light_hasher::HasherError> + where + H: ::light_hasher::Hasher + { + use ::light_hasher::Hasher; + use borsh::BorshSerialize; + + // For SHA256, we serialize the whole struct and hash it in one go + let serialized = self.try_to_vec().map_err(|_| ::light_hasher::HasherError::BorshError)?; + let mut result = H::hash(&serialized)?; + + // Truncate field size for non-Poseidon hashers + if H::ID != 0 { + result[0] = 0; } + + Ok(result) } } }; diff --git a/sdk-libs/macros/src/hasher/input_validator.rs b/sdk-libs/macros/src/hasher/input_validator.rs index af57976b8d..0b2800e15a 100644 --- a/sdk-libs/macros/src/hasher/input_validator.rs +++ b/sdk-libs/macros/src/hasher/input_validator.rs @@ -60,6 +60,36 @@ pub(crate) fn validate_input(input: &ItemStruct) -> Result<()> { Ok(()) } +/// SHA256-specific validation - much more relaxed constraints +pub(crate) fn validate_input_sha(input: &ItemStruct) -> Result<()> { + // Check that we have a struct with named fields + match &input.fields { + Fields::Named(_) => (), + _ => { + return Err(Error::new_spanned( + input, + "Only structs with named fields are supported", + )) + } + }; + + // For SHA256, we don't limit field count or require specific attributes + // Just ensure flatten is not used (not implemented for SHA256 path) + let flatten_field_exists = input + .fields + .iter() + .any(|field| get_field_attribute(field) == FieldAttribute::Flatten); + + if flatten_field_exists { + return Err(Error::new_spanned( + input, + "Flatten attribute is not supported in SHA256 hasher.", + )); + } + + Ok(()) +} + /// Gets the primary attribute for a field (only one attribute can be active) pub(crate) fn get_field_attribute(field: &Field) -> FieldAttribute { if field.attrs.iter().any(|attr| attr.path().is_ident("hash")) { diff --git a/sdk-libs/macros/src/hasher/light_hasher.rs b/sdk-libs/macros/src/hasher/light_hasher.rs index 911cc35f73..fbb9da4271 100644 --- a/sdk-libs/macros/src/hasher/light_hasher.rs +++ b/sdk-libs/macros/src/hasher/light_hasher.rs @@ -3,10 +3,10 @@ use quote::quote; use syn::{Fields, ItemStruct, Result}; use crate::hasher::{ - data_hasher::generate_data_hasher_impl, + data_hasher::{generate_data_hasher_impl, generate_data_hasher_impl_sha}, field_processor::{process_field, FieldProcessingContext}, - input_validator::{get_field_attribute, validate_input, FieldAttribute}, - to_byte_array::generate_to_byte_array_impl, + input_validator::{get_field_attribute, validate_input, validate_input_sha, FieldAttribute}, + to_byte_array::{generate_to_byte_array_impl_sha, generate_to_byte_array_impl_with_hasher}, }; /// - ToByteArray: @@ -49,6 +49,33 @@ use crate::hasher::{ /// - Enums, References, SmartPointers: /// - Not supported pub(crate) fn derive_light_hasher(input: ItemStruct) -> Result { + derive_light_hasher_with_hasher(input, "e!(::light_hasher::Poseidon)) +} + +pub(crate) fn derive_light_hasher_sha(input: ItemStruct) -> Result { + // Use SHA256-specific validation (no field count limits) + validate_input_sha(&input)?; + + let generics = input.generics.clone(); + + let fields = match &input.fields { + Fields::Named(fields) => fields.clone(), + _ => unreachable!("Validation should have caught this"), + }; + + let field_count = fields.named.len(); + + let to_byte_array_impl = generate_to_byte_array_impl_sha(&input.ident, &generics, field_count)?; + let data_hasher_impl = generate_data_hasher_impl_sha(&input.ident, &generics)?; + + Ok(quote! { + #to_byte_array_impl + + #data_hasher_impl + }) +} + +fn derive_light_hasher_with_hasher(input: ItemStruct, hasher: &TokenStream) -> Result { // Validate the input structure validate_input(&input)?; @@ -74,8 +101,13 @@ pub(crate) fn derive_light_hasher(input: ItemStruct) -> Result { process_field(field, i, &mut context); }); - let to_byte_array_impl = - generate_to_byte_array_impl(&input.ident, &generics, field_count, &context)?; + let to_byte_array_impl = generate_to_byte_array_impl_with_hasher( + &input.ident, + &generics, + field_count, + &context, + hasher, + )?; let data_hasher_impl = generate_data_hasher_impl(&input.ident, &generics, &context)?; @@ -244,7 +276,7 @@ impl ::light_hasher::DataHasher for TruncateOptionStruct { #[cfg(debug_assertions)] { if std::env::var("RUST_BACKTRACE").is_ok() { - let debug_prints: Vec<[u8; 32]> = vec![ + let debug_prints: Vec<[u8;32]> = vec![ if let Some(a) = & self.a { let result = a.hash_to_field_size() ?; if result == [0u8; 32] { return Err(::light_hasher::errors::HasherError::OptionHashToFieldSizeZero); } @@ -405,4 +437,277 @@ impl ::light_hasher::DataHasher for OuterStruct { }; assert!(derive_light_hasher(input).is_ok()); } + + #[test] + fn test_sha256_large_struct_with_pubkeys() { + // Test that SHA256 can handle large structs with Pubkeys that would fail with Poseidon + // This struct has 15 fields including Pubkeys without #[hash] attribute + let input: ItemStruct = parse_quote! { + struct LargeAccountSha { + pub field1: u64, + pub field2: u64, + pub field3: u64, + pub field4: u64, + pub field5: u64, + pub field6: u64, + pub field7: u64, + pub field8: u64, + pub field9: u64, + pub field10: u64, + pub field11: u64, + pub field12: u64, + pub field13: u64, + // Pubkeys without #[hash] attribute - this would fail with Poseidon + pub owner: solana_program::pubkey::Pubkey, + pub authority: solana_program::pubkey::Pubkey, + } + }; + + // SHA256 should handle this fine + let sha_result = derive_light_hasher_sha(input.clone()); + assert!( + sha_result.is_ok(), + "SHA256 should handle large structs with Pubkeys" + ); + + // Regular Poseidon hasher should fail due to field count (>12) and Pubkey without #[hash] + let poseidon_result = derive_light_hasher(input); + assert!( + poseidon_result.is_err(), + "Poseidon should fail with >12 fields and unhashed Pubkeys" + ); + } + + #[test] + fn test_sha256_vs_poseidon_hashing_behavior() { + // Test a struct that both can handle to show the difference in hashing approach + let input: ItemStruct = parse_quote! { + struct TestAccount { + pub data: [u8; 31], + pub counter: u64, + } + }; + + // Both should succeed + let sha_result = derive_light_hasher_sha(input.clone()); + assert!(sha_result.is_ok()); + + let poseidon_result = derive_light_hasher(input); + assert!(poseidon_result.is_ok()); + + // Verify SHA256 implementation serializes whole struct + let sha_output = sha_result.unwrap(); + let sha_code = sha_output.to_string(); + + // SHA256 should use try_to_vec() for whole struct serialization (account for spaces) + assert!( + sha_code.contains("try_to_vec") && sha_code.contains("BorshSerialize"), + "SHA256 should serialize whole struct using try_to_vec. Actual code: {}", + sha_code + ); + assert!( + sha_code.contains("result [0] = 0") || sha_code.contains("result[0] = 0"), + "SHA256 should truncate first byte. Actual code: {}", + sha_code + ); + + // Poseidon should use field-by-field hashing + let poseidon_output = poseidon_result.unwrap(); + let poseidon_code = poseidon_output.to_string(); + + assert!( + poseidon_code.contains("to_byte_array") && poseidon_code.contains("as_slice"), + "Poseidon should use field-by-field hashing with to_byte_array. Actual code: {}", + poseidon_code + ); + } + + #[test] + fn test_sha256_no_field_limit() { + // Test that SHA256 doesn't enforce the 12-field limit + let input: ItemStruct = parse_quote! { + struct ManyFieldsStruct { + pub f1: u32, pub f2: u32, pub f3: u32, pub f4: u32, + pub f5: u32, pub f6: u32, pub f7: u32, pub f8: u32, + pub f9: u32, pub f10: u32, pub f11: u32, pub f12: u32, + pub f13: u32, pub f14: u32, pub f15: u32, pub f16: u32, + pub f17: u32, pub f18: u32, pub f19: u32, pub f20: u32, + } + }; + + // SHA256 should handle 20 fields without issue + let result = derive_light_hasher_sha(input); + assert!(result.is_ok(), "SHA256 should handle any number of fields"); + } + + #[test] + fn test_sha256_flatten_not_supported() { + // Test that SHA256 rejects flatten attribute (not implemented) + let input: ItemStruct = parse_quote! { + struct FlattenStruct { + #[flatten] + pub inner: InnerStruct, + pub data: u64, + } + }; + + let result = derive_light_hasher_sha(input); + assert!(result.is_err(), "SHA256 should reject flatten attribute"); + + let error_msg = result.unwrap_err().to_string(); + assert!( + error_msg.contains("not supported in SHA256"), + "Should mention SHA256 limitation" + ); + } + + #[test] + fn test_sha256_with_discriminator_integration() { + // Test that shows LightHasherSha works with LightDiscriminatorSha for large structs + // This would be impossible with regular Poseidon-based macros + let input: ItemStruct = parse_quote! { + struct LargeIntegratedAccount { + pub field1: u64, pub field2: u64, pub field3: u64, pub field4: u64, + pub field5: u64, pub field6: u64, pub field7: u64, pub field8: u64, + pub field9: u64, pub field10: u64, pub field11: u64, pub field12: u64, + pub field13: u64, pub field14: u64, pub field15: u64, pub field16: u64, + pub field17: u64, pub field18: u64, pub field19: u64, pub field20: u64, + // Pubkeys without #[hash] attribute + pub owner: solana_program::pubkey::Pubkey, + pub authority: solana_program::pubkey::Pubkey, + pub delegate: solana_program::pubkey::Pubkey, + } + }; + + // Both SHA256 hasher and discriminator should work + let sha_hasher_result = derive_light_hasher_sha(input.clone()); + assert!( + sha_hasher_result.is_ok(), + "SHA256 hasher should work with large structs" + ); + + let sha_discriminator_result = crate::discriminator::discriminator_sha(input.clone()); + assert!( + sha_discriminator_result.is_ok(), + "SHA256 discriminator should work with large structs" + ); + + // Regular Poseidon variants should fail + let poseidon_hasher_result = derive_light_hasher(input); + assert!( + poseidon_hasher_result.is_err(), + "Poseidon hasher should fail with large structs" + ); + + // Verify the generated code contains expected patterns + let sha_hasher_code = sha_hasher_result.unwrap().to_string(); + assert!( + sha_hasher_code.contains("try_to_vec"), + "Should use serialization approach" + ); + assert!( + sha_hasher_code.contains("BorshSerialize"), + "Should use Borsh serialization" + ); + + let sha_discriminator_code = sha_discriminator_result.unwrap().to_string(); + assert!( + sha_discriminator_code.contains("LightDiscriminator"), + "Should implement LightDiscriminator" + ); + assert!( + sha_discriminator_code.contains("LIGHT_DISCRIMINATOR"), + "Should provide discriminator constant" + ); + } + + #[test] + fn test_complete_sha256_ecosystem_practical_example() { + // Demonstrates a real-world scenario where SHA256 variants are essential + // This struct would be impossible with Poseidon due to: + // 1. >12 fields (23+ fields) + // 2. Multiple Pubkeys without #[hash] attribute + // 3. Large data structures + let input: ItemStruct = parse_quote! { + pub struct ComplexGameState { + // Game metadata (13 fields) + pub game_id: u64, + pub round: u32, + pub turn: u8, + pub phase: u8, + pub start_time: i64, + pub end_time: i64, + pub max_players: u8, + pub current_players: u8, + pub entry_fee: u64, + pub prize_pool: u64, + pub game_mode: u32, + pub difficulty: u8, + pub status: u8, + + // Player information (6 Pubkey fields - would require #[hash] with Poseidon) + pub creator: solana_program::pubkey::Pubkey, + pub winner: solana_program::pubkey::Pubkey, + pub current_player: solana_program::pubkey::Pubkey, + pub authority: solana_program::pubkey::Pubkey, + pub treasury: solana_program::pubkey::Pubkey, + pub program_id: solana_program::pubkey::Pubkey, + + // Game state data (4+ more fields) + pub board_state: [u8; 64], // Large array + pub player_scores: [u32; 8], // Array of scores + pub moves_history: [u16; 32], // Move history + pub special_flags: u32, + + // This gives us 23+ fields total - way beyond Poseidon's 12-field limit + } + }; + + // SHA256 variants should handle this complex struct effortlessly + let sha_hasher_result = derive_light_hasher_sha(input.clone()); + assert!( + sha_hasher_result.is_ok(), + "SHA256 hasher must handle complex real-world structs" + ); + + let sha_discriminator_result = crate::discriminator::discriminator_sha(input.clone()); + assert!( + sha_discriminator_result.is_ok(), + "SHA256 discriminator must handle complex real-world structs" + ); + + // Poseidon would fail with this struct + let poseidon_result = derive_light_hasher(input); + assert!( + poseidon_result.is_err(), + "Poseidon cannot handle structs with >12 fields and unhashed Pubkeys" + ); + + // Verify SHA256 generates efficient serialization-based code + let hasher_code = sha_hasher_result.unwrap().to_string(); + assert!( + hasher_code.contains("try_to_vec"), + "Should serialize entire struct efficiently" + ); + assert!( + hasher_code.contains("BorshSerialize"), + "Should use Borsh for serialization" + ); + assert!( + hasher_code.contains("result [0] = 0") || hasher_code.contains("result[0] = 0"), + "Should apply field size truncation. Actual code: {}", + hasher_code + ); + + // Verify discriminator works correctly + let discriminator_code = sha_discriminator_result.unwrap().to_string(); + assert!( + discriminator_code.contains("ComplexGameState"), + "Should target correct struct" + ); + assert!( + discriminator_code.contains("LIGHT_DISCRIMINATOR"), + "Should provide discriminator constant" + ); + } } diff --git a/sdk-libs/macros/src/hasher/mod.rs b/sdk-libs/macros/src/hasher/mod.rs index 5c81807edf..c2ebd8034e 100644 --- a/sdk-libs/macros/src/hasher/mod.rs +++ b/sdk-libs/macros/src/hasher/mod.rs @@ -4,4 +4,4 @@ mod input_validator; mod light_hasher; mod to_byte_array; -pub(crate) use light_hasher::derive_light_hasher; +pub(crate) use light_hasher::{derive_light_hasher, derive_light_hasher_sha}; diff --git a/sdk-libs/macros/src/hasher/to_byte_array.rs b/sdk-libs/macros/src/hasher/to_byte_array.rs index 27d49ae232..9cec46c117 100644 --- a/sdk-libs/macros/src/hasher/to_byte_array.rs +++ b/sdk-libs/macros/src/hasher/to_byte_array.rs @@ -4,11 +4,12 @@ use syn::Result; use crate::hasher::field_processor::FieldProcessingContext; -pub(crate) fn generate_to_byte_array_impl( +pub(crate) fn generate_to_byte_array_impl_with_hasher( struct_name: &syn::Ident, generics: &syn::Generics, field_count: usize, context: &FieldProcessingContext, + hasher: &TokenStream, ) -> Result { let (impl_gen, type_gen, where_clause) = generics.split_for_impl(); @@ -20,34 +21,70 @@ pub(crate) fn generate_to_byte_array_impl( Some(s) => s, None => &alt_res, }; - let field_assignment: TokenStream = syn::parse_str(str)?; - - // Create a token stream with the field_assignment and the import code - let mut hash_imports = proc_macro2::TokenStream::new(); - for code in &context.hash_to_field_size_code { - hash_imports.extend(code.clone()); - } + let content: TokenStream = str.parse().expect("Invalid generated code"); Ok(quote! { impl #impl_gen ::light_hasher::to_byte_array::ToByteArray for #struct_name #type_gen #where_clause { - const NUM_FIELDS: usize = #field_count; + const NUM_FIELDS: usize = 1; fn to_byte_array(&self) -> ::std::result::Result<[u8; 32], ::light_hasher::HasherError> { - #hash_imports - #field_assignment + use ::light_hasher::to_byte_array::ToByteArray; + use ::light_hasher::hash_to_field_size::HashToFieldSize; + #content } } }) } else { + let data_hasher_assignments = &context.data_hasher_assignments; Ok(quote! { impl #impl_gen ::light_hasher::to_byte_array::ToByteArray for #struct_name #type_gen #where_clause { const NUM_FIELDS: usize = #field_count; fn to_byte_array(&self) -> ::std::result::Result<[u8; 32], ::light_hasher::HasherError> { - ::light_hasher::DataHasher::hash::<::light_hasher::Poseidon>(self) - } + use ::light_hasher::to_byte_array::ToByteArray; + use ::light_hasher::hash_to_field_size::HashToFieldSize; + use ::light_hasher::Hasher; + let mut result = #hasher::hashv(&[ + #(#data_hasher_assignments.as_slice(),)* + ])?; + + // Truncate field size for non-Poseidon hashers + if #hasher::ID != 0 { + result[0] = 0; + } + Ok(result) + } } }) } } + +/// SHA256-specific ToByteArray implementation that serializes the whole struct +pub(crate) fn generate_to_byte_array_impl_sha( + struct_name: &syn::Ident, + generics: &syn::Generics, + field_count: usize, +) -> Result { + let (impl_gen, type_gen, where_clause) = generics.split_for_impl(); + + Ok(quote! { + impl #impl_gen ::light_hasher::to_byte_array::ToByteArray for #struct_name #type_gen #where_clause { + const NUM_FIELDS: usize = #field_count; + + fn to_byte_array(&self) -> ::std::result::Result<[u8; 32], ::light_hasher::HasherError> { + use borsh::BorshSerialize; + use ::light_hasher::Hasher; + + // For SHA256, we can serialize the whole struct and hash it in one go + let serialized = self.try_to_vec().map_err(|_| ::light_hasher::HasherError::BorshError)?; + let mut result = ::light_hasher::Sha256::hash(&serialized)?; + + // Truncate field size for non-Poseidon hashers + result[0] = 0; + + Ok(result) + } + } + }) +} diff --git a/sdk-libs/macros/src/lib.rs b/sdk-libs/macros/src/lib.rs index 324660c861..32ddae3fe1 100644 --- a/sdk-libs/macros/src/lib.rs +++ b/sdk-libs/macros/src/lib.rs @@ -1,17 +1,32 @@ extern crate proc_macro; use accounts::{process_light_accounts, process_light_system_accounts}; -use hasher::derive_light_hasher; +use discriminator::{discriminator, discriminator_sha}; +use hasher::{derive_light_hasher, derive_light_hasher_sha}; use proc_macro::TokenStream; -use syn::{parse_macro_input, DeriveInput, ItemMod, ItemStruct}; +use syn::{parse_macro_input, DeriveInput, ItemStruct}; use traits::process_light_traits; mod account; +mod account_seeds; mod accounts; +mod compress_as; +mod compressible; +mod compressible_derive; +mod compressible_instructions; mod cpi_signer; +// Legacy CToken and instruction generator modules removed - functionality integrated into compressible_instructions +mod derive_seeds; mod discriminator; mod hasher; +// Legacy instruction generators removed - functionality integrated into compressible_instructions +mod native_compressible; +mod pack_unpack; mod program; mod traits; +mod variant_enum; + +#[cfg(test)] +mod test_modular_macros; /// Adds required fields to your anchor instruction for applying a zk-compressed /// state transition. @@ -135,7 +150,35 @@ pub fn light_traits_derive(input: TokenStream) -> TokenStream { #[proc_macro_derive(LightDiscriminator)] pub fn light_discriminator(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as ItemStruct); - discriminator::discriminator(input) + discriminator(input) + .unwrap_or_else(|err| err.to_compile_error()) + .into() +} + +/// SHA256 variant of the LightDiscriminator derive macro. +/// +/// This derive macro provides the same discriminator functionality as LightDiscriminator +/// but is designed to be used with SHA256-based hashing for consistency. +/// +/// ## Example +/// +/// ```ignore +/// use light_sdk::sha::{LightHasher, LightDiscriminator}; +/// +/// #[derive(LightHasher, LightDiscriminator)] +/// pub struct LargeGameState { +/// pub field1: u64, pub field2: u64, pub field3: u64, pub field4: u64, +/// pub field5: u64, pub field6: u64, pub field7: u64, pub field8: u64, +/// pub field9: u64, pub field10: u64, pub field11: u64, pub field12: u64, +/// pub field13: u64, pub field14: u64, pub field15: u64, +/// pub owner: Pubkey, +/// pub authority: Pubkey, +/// } +/// ``` +#[proc_macro_derive(LightDiscriminatorSha)] +pub fn light_discriminator_sha(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as ItemStruct); + discriminator_sha(input) .unwrap_or_else(|err| err.to_compile_error()) .into() } @@ -152,178 +195,482 @@ pub fn light_discriminator(input: TokenStream) -> TokenStream { /// `AsByteVec` trait. The trait is implemented by default for the most of /// standard Rust types (primitives, `String`, arrays and options carrying the /// former). If there is a field of a type not implementing the trait, there -/// are two options: +/// will be a compilation error. /// -/// 1. The most recommended one - annotating that type with the `light_hasher` -/// macro as well. -/// 2. Manually implementing the `AsByteVec` trait. +/// ## Example /// -/// # Attributes +/// ```ignore +/// use light_sdk::LightHasher; +/// use solana_pubkey::Pubkey; /// -/// - `skip` - skips the given field, it doesn't get included neither in -/// `AsByteVec` nor `DataHasher` implementation. -/// - `hash` - makes sure that the byte value does not exceed the BN254 -/// prime field modulus, by hashing it (with Keccak) and truncating it to 31 -/// bytes. It's generally a good idea to use it on any field which is -/// expected to output more than 31 bytes. +/// #[derive(LightHasher)] +/// pub struct UserRecord { +/// pub owner: Pubkey, +/// pub name: String, +/// pub score: u64, +/// } +/// ``` /// -/// # Examples +/// ## Hash attribute /// -/// Compressed account with only primitive types as fields: +/// Fields marked with `#[hash]` will be hashed to field size (31 bytes) before +/// being included in the main hash calculation. This is useful for fields that +/// exceed the field size limit (like Pubkeys which are 32 bytes). /// /// ```ignore /// #[derive(LightHasher)] -/// pub struct MyCompressedAccount { -/// a: i64, -/// b: Option, +/// pub struct GameState { +/// #[hash] +/// pub player: Pubkey, // Will be hashed to 31 bytes +/// pub level: u32, /// } /// ``` +#[proc_macro_derive(LightHasher, attributes(hash, skip))] +pub fn light_hasher(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as ItemStruct); + + derive_light_hasher(input) + .unwrap_or_else(|err| err.to_compile_error()) + .into() +} + +/// SHA256 variant of the LightHasher derive macro. +/// +/// This derive macro automatically implements the `DataHasher` and `ToByteArray` traits +/// for structs, using SHA256 as the hashing algorithm instead of Poseidon. /// -/// Compressed account with fields which might exceed the BN254 prime field: +/// ## Example /// /// ```ignore +/// use light_sdk::sha::LightHasher; +/// /// #[derive(LightHasher)] -/// pub struct MyCompressedAccount { -/// a: i64 -/// b: Option, -/// #[hash] -/// c: [u8; 32], +/// pub struct GameState { /// #[hash] -/// d: String, +/// pub player: Pubkey, // Will be hashed to 31 bytes +/// pub level: u32, /// } /// ``` +#[proc_macro_derive(LightHasherSha, attributes(hash, skip))] +pub fn light_hasher_sha(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as ItemStruct); + + derive_light_hasher_sha(input) + .unwrap_or_else(|err| err.to_compile_error()) + .into() +} + +/// Alias of `LightHasher`. +#[proc_macro_derive(DataHasher, attributes(skip, hash))] +pub fn data_hasher(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as ItemStruct); + + derive_light_hasher_sha(input) + .unwrap_or_else(|err| err.to_compile_error()) + .into() +} + +/// Automatically implements the HasCompressionInfo trait for structs that have a +/// `compression_info: Option` field. /// -/// Compressed account with fields we want to skip: +/// This derive macro generates the required trait methods for managing compression +/// information in compressible account structs. +/// +/// ## Example /// /// ```ignore -/// #[derive(LightHasher)] -/// pub struct MyCompressedAccount { -/// a: i64 -/// b: Option, +/// use light_sdk::compressible::{CompressionInfo, HasCompressionInfo}; +/// +/// #[derive(HasCompressionInfo)] +/// pub struct UserRecord { /// #[skip] -/// c: [u8; 32], +/// pub compression_info: Option, +/// pub owner: Pubkey, +/// pub name: String, +/// pub score: u64, /// } /// ``` /// -/// Compressed account with a nested struct: +/// ## Requirements /// -/// ```ignore -/// #[derive(LightHasher)] -/// pub struct MyCompressedAccount { -/// a: i64 -/// b: Option, -/// c: MyStruct, -/// } +/// The struct must have exactly one field named `compression_info` of type +/// `Option`. The field should be marked with `#[skip]` to +/// exclude it from hashing. +#[proc_macro_derive(HasCompressionInfo)] +pub fn has_compression_info(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as ItemStruct); + + compressible::derive_has_compression_info(input) + .unwrap_or_else(|err| err.to_compile_error()) + .into() +} + +/// Legacy CompressAs trait implementation (use Compressible instead). /// -/// #[derive(LightHasher)] -/// pub struct MyStruct { -/// a: i32 -/// b: u32, -/// } -/// ``` +/// This derive macro allows you to specify which fields should be reset/overridden +/// during compression while keeping other fields as-is. Only the specified fields +/// are modified; all others retain their current values. /// -/// Compressed account with a type with a custom `AsByteVec` implementation: +/// ## Example /// /// ```ignore -/// #[derive(LightHasher)] -/// pub struct MyCompressedAccount { -/// a: i64 -/// b: Option, -/// c: RData, +/// use light_sdk::compressible::{CompressAs, CompressionInfo, HasCompressionInfo}; +/// use light_sdk_macros::CompressAs; +/// +/// #[derive(CompressAs)] +/// #[compress_as( +/// start_time = 0, +/// end_time = None, +/// score = 0 +/// )] +/// pub struct GameSession { +/// #[skip] +/// pub compression_info: Option, +/// pub session_id: u64, +/// pub player: Pubkey, +/// pub game_type: String, +/// pub start_time: u64, +/// pub end_time: Option, +/// pub score: u64, /// } +/// ``` /// -/// pub enum RData { -/// A(Ipv4Addr), -/// AAAA(Ipv6Addr), -/// CName(String), -/// } +/// ## Note /// -/// impl AsByteVec for RData { -/// fn as_byte_vec(&self) -> Vec> { -/// match self { -/// Self::A(ipv4_addr) => vec![ipv4_addr.octets().to_vec()], -/// Self::AAAA(ipv6_addr) => vec![ipv6_addr.octets().to_vec()], -/// Self::CName(cname) => cname.as_byte_vec(), -/// } -/// } +/// Use the new `Compressible` derive instead - it includes this functionality plus more. +#[proc_macro_derive(CompressAs, attributes(compress_as))] +pub fn compress_as_derive(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as ItemStruct); + + compress_as::derive_compress_as(input) + .unwrap_or_else(|err| err.to_compile_error()) + .into() +} + +/// Adds compressible account support with automatic seed generation. +/// +/// This macro generates everything needed for compressible accounts: +/// - CompressedAccountVariant enum with all trait implementations +/// - Compress and decompress instructions with auto-generated seed derivation +/// - CTokenSeedProvider implementation for token accounts +/// - All required account structs and functions +/// +/// ## Usage +/// ``` +/// #[add_compressible_instructions( +/// UserRecord = ("user_record", data.owner), +/// GameSession = ("game_session", data.session_id.to_le_bytes()), +/// CTokenSigner = (is_token, "ctoken_signer", ctx.fee_payer, ctx.mint) +/// )] +/// #[program] +/// pub mod my_program { +/// // Your regular instructions here - everything else is auto-generated! +/// // CTokenAccountVariant enum is automatically generated with: +/// // - CTokenSigner = 0 /// } /// ``` -#[proc_macro_derive(LightHasher, attributes(skip, hash))] -pub fn light_hasher(input: TokenStream) -> TokenStream { - let input = parse_macro_input!(input as ItemStruct); - derive_light_hasher(input) +#[proc_macro_attribute] +pub fn add_compressible_instructions(args: TokenStream, input: TokenStream) -> TokenStream { + let module = syn::parse_macro_input!(input as syn::ItemMod); + compressible_instructions::add_compressible_instructions(args.into(), module) .unwrap_or_else(|err| err.to_compile_error()) .into() } -/// Alias of `LightHasher`. -#[proc_macro_derive(DataHasher, attributes(skip, hash))] -pub fn data_hasher(input: TokenStream) -> TokenStream { - let input = parse_macro_input!(input as ItemStruct); - derive_light_hasher(input) +/// Adds native compressible instructions for the specified account types +/// +/// This macro generates thin wrapper processor functions that you dispatch manually. +/// +/// ## Usage +/// ``` +/// #[add_native_compressible_instructions(MyPdaAccount, AnotherAccount)] +/// pub mod compression {} +/// ``` +/// +/// This generates: +/// - Unified data structures (CompressedAccountVariant enum, etc.) +/// - Instruction data structs (CreateCompressionConfigData, etc.) +/// - Processor functions (create_compression_config, compress_my_pda_account, etc.) +/// +/// You then dispatch these in your process_instruction function. +#[proc_macro_attribute] +pub fn add_native_compressible_instructions(args: TokenStream, input: TokenStream) -> TokenStream { + let input = syn::parse_macro_input!(input as syn::ItemMod); + + native_compressible::add_native_compressible_instructions(args.into(), input) .unwrap_or_else(|err| err.to_compile_error()) .into() } #[proc_macro_attribute] -pub fn light_account(_: TokenStream, input: TokenStream) -> TokenStream { +pub fn account(_: TokenStream, input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as ItemStruct); + account::account(input) .unwrap_or_else(|err| err.to_compile_error()) .into() } -#[proc_macro_attribute] -pub fn light_program(_: TokenStream, input: TokenStream) -> TokenStream { - let input = parse_macro_input!(input as ItemMod); - program::program(input) +/// Automatically implements all required traits for compressible accounts. +/// +/// This derive macro generates HasCompressionInfo, Size, and CompressAs trait implementations. +/// It supports optional compress_as attribute for custom compression behavior. +/// +/// ## Example - Basic Usage +/// +/// ```ignore +/// use light_sdk_macros::Compressible; +/// use light_sdk::compressible::CompressionInfo; +/// +/// #[derive(Compressible)] +/// pub struct UserRecord { +/// #[skip] +/// pub compression_info: Option, +/// pub owner: Pubkey, +/// pub name: String, +/// pub score: u64, +/// } +/// ``` +/// +/// ## Example - Custom Compression +/// +/// ```ignore +/// #[derive(Compressible)] +/// #[compress_as(start_time = 0, end_time = None, score = 0)] +/// pub struct GameSession { +/// #[skip] +/// pub compression_info: Option, +/// pub session_id: u64, // KEPT +/// pub player: Pubkey, // KEPT +/// pub game_type: String, // KEPT +/// pub start_time: u64, // RESET to 0 +/// pub end_time: Option, // RESET to None +/// pub score: u64, // RESET to 0 +/// } +/// ``` +#[proc_macro_derive(Compressible, attributes(compress_as, light_seeds))] +pub fn compressible_derive(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + + compressible_derive::derive_compressible(input) .unwrap_or_else(|err| err.to_compile_error()) .into() } -/// Derives a Light Protocol CPI signer address at compile time +/// Automatically implements Pack and Unpack traits for compressible accounts. /// -/// This macro computes the CPI signer PDA using the "cpi_authority" seed -/// for the given program ID at compile time. +/// For types with Pubkey fields, generates a PackedXxx struct and proper packing. +/// For types without Pubkeys, generates identity Pack/Unpack implementations. /// -/// ## Usage +/// ## Example +/// +/// ```ignore +/// use light_sdk_macros::CompressiblePack; +/// +/// #[derive(CompressiblePack)] +/// pub struct UserRecord { +/// pub compression_info: Option, +/// pub owner: Pubkey, // Will be packed as u8 index +/// pub name: String, // Kept as-is +/// pub score: u64, // Kept as-is +/// } +/// // This generates PackedUserRecord struct + Pack/Unpack implementations +/// ``` +#[proc_macro_derive(CompressiblePack)] +pub fn compressible_pack(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + + pack_unpack::derive_compressible_pack(input) + .unwrap_or_else(|err| err.to_compile_error()) + .into() +} + +// DEPRECATED: compressed_account_variant macro is now integrated into add_compressible_instructions +// Use add_compressible_instructions instead for complete automation + +/// Generates complete compressible instructions with auto-generated seed derivation. +/// +/// This is a drop-in replacement for manual decompress_accounts_idempotent and +/// compress_accounts_idempotent instructions. It reads #[light_seeds(...)] attributes +/// from account types and generates complete instructions with inline seed derivation. +/// +/// ## Example +/// +/// Add #[light_seeds(...)] to your account types: +/// ```ignore +/// #[derive(Compressible, CompressiblePack)] +/// #[light_seeds(b"user_record", owner.as_ref())] +/// pub struct UserRecord { +/// pub owner: Pubkey, +/// // ... +/// } +/// +/// #[derive(Compressible, CompressiblePack)] +/// #[light_seeds(b"game_session", session_id.to_le_bytes().as_ref())] +/// pub struct GameSession { +/// pub session_id: u64, +/// // ... +/// } +/// ``` /// +/// Then generate complete instructions: +/// ```ignore +/// compressed_account_variant_with_instructions!(UserRecord, GameSession, PlaceholderRecord); /// ``` -/// use light_sdk_macros::derive_light_cpi_signer_pda; -/// // Derive CPI signer for your program -/// const CPI_SIGNER_DATA: ([u8; 32], u8) = derive_light_cpi_signer_pda!("SySTEM1eSU2p4BGQfQpimFEWWSC1XDFeun3Nqzz3rT7"); -/// const CPI_SIGNER: [u8; 32] = CPI_SIGNER_DATA.0; -/// const CPI_SIGNER_BUMP: u8 = CPI_SIGNER_DATA.1; +/// +/// This generates: +/// - CompressedAccountVariant enum + all trait implementations +/// - Complete decompress_accounts_idempotent instruction with auto-generated seed derivation +/// - Complete compress_accounts_idempotent instruction with auto-generated seed derivation +/// - CompressedAccountData struct +/// +/// The generated instructions automatically handle seed derivation for each account type +/// without requiring manual seed function calls. +// DEPRECATED: compressed_account_variant_with_instructions macro is now integrated into add_compressible_instructions +// Use add_compressible_instructions instead for complete automation with declarative seed syntax + +/// Generates seed getter functions by analyzing Anchor account structs. +/// +/// This macro scans account structs for `#[account(seeds = [...], ...)]` attributes +/// and generates corresponding public seed getter functions that can be used by +/// both the program and external clients. +/// +/// ## Example +/// +/// ```ignore +/// use light_sdk_macros::generate_seed_functions; +/// +/// generate_seed_functions! { +/// #[derive(Accounts)] +/// pub struct CreateRecord<'info> { +/// #[account( +/// init, +/// seeds = [b"user_record", user.key().as_ref()], +/// bump, +/// )] +/// pub user_record: Account<'info, UserRecord>, +/// pub user: Signer<'info>, +/// } +/// +/// #[derive(Accounts)] +/// #[instruction(session_id: u64)] +/// pub struct CreateGameSession<'info> { +/// #[account( +/// init, +/// seeds = [b"game_session", session_id.to_le_bytes().as_ref()], +/// bump, +/// )] +/// pub game_session: Account<'info, GameSession>, +/// pub player: Signer<'info>, +/// } +/// } /// ``` /// -/// This macro computes the PDA during compile time and returns a tuple of ([u8; 32], bump). +/// This generates: +/// - `get_user_record_seeds(user: &Pubkey) -> (Vec>, Pubkey)` +/// - `get_game_session_seeds(session_id: u64) -> (Vec>, Pubkey)` +/// +/// The functions extract parameters from the seeds expressions and create +/// public functions that match the exact same seed derivation logic. #[proc_macro] -pub fn derive_light_cpi_signer_pda(input: TokenStream) -> TokenStream { - cpi_signer::derive_light_cpi_signer_pda(input) +pub fn generate_seed_functions(input: TokenStream) -> TokenStream { + account_seeds::generate_seed_functions(input.into()) + .unwrap_or_else(|err| err.to_compile_error()) + .into() } -/// Derives a complete Light Protocol CPI configuration at compile time +// Legacy add_compressible_instructions_enhanced macro removed - now just use add_compressible_instructions! + +// DEPRECATED: ctoken_seeds macro is now integrated into add_compressible_instructions +// Use add_compressible_instructions with CToken seed specifications instead + +/// Automatically generates seed getter functions for PDA and token accounts. /// -/// This macro computes the program ID, CPI signer PDA, and bump seed -/// for the given program ID at compile time. +/// This derive macro generates public functions that can be used by both the program +/// and external clients to get PDA seeds and addresses. /// -/// ## Usage +/// ## Example - PDA Account /// +/// ```ignore +/// use light_sdk_macros::DeriveSeeds; +/// +/// #[derive(DeriveSeeds)] +/// #[seeds("user_record", owner)] +/// pub struct UserRecord { +/// pub owner: Pubkey, +/// pub name: String, +/// pub score: u64, +/// } +/// // Generates: get_user_record_seeds(owner: &Pubkey) -> (Vec>, Pubkey) /// ``` -/// use light_sdk_macros::derive_light_cpi_signer; -/// use light_sdk_types::CpiSigner; -/// // Derive complete CPI signer for your program -/// const LIGHT_CPI_SIGNER: CpiSigner = derive_light_cpi_signer!("SySTEM1eSU2p4BGQfQpimFEWWSC1XDFeun3Nqzz3rT7"); -/// -/// // Access individual fields: -/// const PROGRAM_ID: [u8; 32] = LIGHT_CPI_SIGNER.program_id; -/// const CPI_SIGNER: [u8; 32] = LIGHT_CPI_SIGNER.cpi_signer; -/// const BUMP: u8 = LIGHT_CPI_SIGNER.bump; +/// +/// ## Example - Token Account +/// +/// ```ignore +/// #[derive(DeriveSeeds)] +/// #[seeds("ctoken_signer", user, mint)] +/// #[token_account] +/// pub struct CTokenSigner { +/// pub user: Pubkey, +/// pub mint: Pubkey, +/// } +/// // Generates: get_c_token_signer_seeds(user: &Pubkey, mint: &Pubkey) -> (Vec>, Pubkey) /// ``` /// -/// This macro computes all values during compile time and returns a CpiSigner struct -/// containing the program ID, CPI signer address, and bump seed. +/// ## Supported Seed Types +/// +/// - String literals: `"user_record"` -> `b"user_record".as_ref()` +/// - Pubkey fields: `owner` -> `owner.as_ref()` +/// - u64 fields: `session_id` -> `session_id.to_le_bytes().as_ref()` +/// - Custom expressions: `custom_expr` -> `custom_expr` +#[proc_macro_derive(DeriveSeeds, attributes(seeds, token_account))] +pub fn derive_seeds(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + + derive_seeds::derive_seeds(input) + .unwrap_or_else(|err| err.to_compile_error()) + .into() +} + +// DEPRECATED: DeriveCTokenSeeds is now integrated into add_compressible_instructions +// Use add_compressible_instructions with CToken seed specifications instead + +/// Derive the CPI signer from the program ID. The program ID must be a string +/// literal. +/// +/// ## Example +/// +/// ```ignore +/// use light_sdk::derive_light_cpi_signer; +/// +/// pub const LIGHT_CPI_SIGNER: CpiSigner = +/// derive_light_cpi_signer!("8Ld9pGkCNfU6A7KdKe1YrTNYJWKMCFqVHqmUvjNmER7B"); +/// ``` #[proc_macro] pub fn derive_light_cpi_signer(input: TokenStream) -> TokenStream { cpi_signer::derive_light_cpi_signer(input) } + +/// Generates a Light program for the given module. +/// +/// ## Example +/// +/// ```ignore +/// use light_sdk::light_program; +/// +/// #[light_program] +/// pub mod my_program { +/// pub fn my_instruction(ctx: Context) -> Result<()> { +/// // Your instruction logic here +/// Ok(()) +/// } +/// } +/// ``` +#[proc_macro_attribute] +pub fn light_program(_: TokenStream, input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as syn::ItemMod); + + program::program(input) + .unwrap_or_else(|err| err.to_compile_error()) + .into() +} diff --git a/sdk-libs/macros/src/native_compressible.rs b/sdk-libs/macros/src/native_compressible.rs new file mode 100644 index 0000000000..fd02104c27 --- /dev/null +++ b/sdk-libs/macros/src/native_compressible.rs @@ -0,0 +1,524 @@ +use heck::ToSnakeCase; +use proc_macro2::TokenStream; +use quote::{format_ident, quote}; +use syn::{ + parse::{Parse, ParseStream}, + punctuated::Punctuated, + Ident, Item, ItemMod, Result, Token, +}; + +/// Parse a comma-separated list of identifiers +struct IdentList { + idents: Punctuated, +} + +impl Parse for IdentList { + fn parse(input: ParseStream) -> Result { + if input.is_empty() { + return Err(syn::Error::new( + input.span(), + "Expected at least one account type", + )); + } + + // Try to parse as a simple identifier first + if input.peek(Ident) && !input.peek2(Token![,]) { + // Single identifier case + let ident: Ident = input.parse()?; + let mut idents = Punctuated::new(); + idents.push(ident); + return Ok(IdentList { idents }); + } + + // Otherwise parse as comma-separated list + Ok(IdentList { + idents: Punctuated::parse_terminated(input)?, + }) + } +} + +/// Generate compress instructions for the specified account types (Native Solana version) +pub(crate) fn add_native_compressible_instructions( + args: TokenStream, + mut module: ItemMod, +) -> Result { + // Try to parse the arguments + let ident_list = match syn::parse2::(args) { + Ok(list) => list, + Err(e) => { + return Err(syn::Error::new( + e.span(), + format!("Failed to parse arguments: {}", e), + )); + } + }; + + // Check if module has content + if module.content.is_none() { + return Err(syn::Error::new_spanned(&module, "Module must have a body")); + } + + // Get the module content + let content = module.content.as_mut().unwrap(); + + // Collect all struct names + let struct_names: Vec<_> = ident_list.idents.iter().collect(); + + // Add necessary imports at the beginning + let imports: Item = syn::parse_quote! { + use super::*; + }; + content.1.insert(0, imports); + + // Add borsh imports + let borsh_imports: Item = syn::parse_quote! { + use borsh::{BorshDeserialize, BorshSerialize}; + }; + content.1.insert(1, borsh_imports); + + // Generate unified data structures + let unified_structures = generate_unified_structures(&struct_names); + for item in unified_structures { + content.1.push(item); + } + + // Generate instruction data structures + let instruction_data_structs = generate_instruction_data_structs(&struct_names); + for item in instruction_data_structs { + content.1.push(item); + } + + // Generate thin wrapper processor functions + let processor_functions = generate_thin_processors(&struct_names); + for item in processor_functions { + content.1.push(item); + } + + Ok(quote! { + #module + }) +} + +fn generate_unified_structures(struct_names: &[&Ident]) -> Vec { + let mut items = Vec::new(); + + // Generate the CompressedAccountVariant enum + let enum_variants = struct_names.iter().map(|name| { + quote! { + #name(#name) + } + }); + + let compressed_variant_enum: Item = syn::parse_quote! { + #[derive(Clone, Debug, borsh::BorshSerialize, borsh::BorshDeserialize)] + pub enum CompressedAccountVariant { + #(#enum_variants),* + } + }; + items.push(compressed_variant_enum); + + // Generate Default implementation + if let Some(first_struct) = struct_names.first() { + let default_impl: Item = syn::parse_quote! { + impl Default for CompressedAccountVariant { + fn default() -> Self { + CompressedAccountVariant::#first_struct(Default::default()) + } + } + }; + items.push(default_impl); + } + + // Generate DataHasher implementation with correct signature + let hash_match_arms = struct_names.iter().map(|name| { + quote! { + CompressedAccountVariant::#name(data) => data.hash::() + } + }); + + let data_hasher_impl: Item = syn::parse_quote! { + impl light_hasher::DataHasher for CompressedAccountVariant { + fn hash(&self) -> Result<[u8; 32], light_hasher::errors::HasherError> { + match self { + #(#hash_match_arms),* + } + } + } + }; + items.push(data_hasher_impl); + + // Generate LightDiscriminator implementation with correct constants and method signature + let light_discriminator_impl: Item = syn::parse_quote! { + impl light_sdk::LightDiscriminator for CompressedAccountVariant { + const LIGHT_DISCRIMINATOR: [u8; 8] = [0; 8]; // Default discriminator for enum + const LIGHT_DISCRIMINATOR_SLICE: &'static [u8] = &Self::LIGHT_DISCRIMINATOR; + + fn discriminator() -> [u8; 8] { + Self::LIGHT_DISCRIMINATOR + } + } + }; + items.push(light_discriminator_impl); + + // Generate HasCompressionInfo implementation with correct method signatures + let compression_info_match_arms = struct_names.iter().map(|name| { + quote! { + CompressedAccountVariant::#name(data) => data.compression_info() + } + }); + + let compression_info_mut_match_arms = struct_names.iter().map(|name| { + quote! { + CompressedAccountVariant::#name(data) => data.compression_info_mut() + } + }); + + let has_compression_info_impl: Item = syn::parse_quote! { + impl light_sdk::compressible::HasCompressionInfo for CompressedAccountVariant { + fn compression_info(&self) -> &light_sdk::compressible::CompressionInfo { + match self { + #(#compression_info_match_arms),* + } + } + + fn compression_info_mut(&mut self) -> &mut light_sdk::compressible::CompressionInfo { + match self { + #(#compression_info_mut_match_arms),* + } + } + } + }; + items.push(has_compression_info_impl); + + // Generate CompressedAccountData struct + let compressed_account_data: Item = syn::parse_quote! { + #[derive(Clone, Debug, borsh::BorshSerialize, borsh::BorshDeserialize)] + pub struct CompressedAccountData { + pub meta: light_sdk_types::instruction::account_meta::CompressedAccountMeta, + pub data: CompressedAccountVariant, + pub seeds: Vec>, // Seeds for PDA derivation (without bump) + } + }; + items.push(compressed_account_data); + + items +} + +fn generate_instruction_data_structs(struct_names: &[&Ident]) -> Vec { + let mut items = Vec::new(); + + // Create config instruction data + let create_config: Item = syn::parse_quote! { + #[derive(Clone, Debug, BorshSerialize, BorshDeserialize)] + pub struct CreateCompressionConfigData { + pub compression_delay: u32, + pub rent_recipient: solana_program::pubkey::Pubkey, + pub address_space: Vec, + } + }; + items.push(create_config); + + // Update config instruction data + let update_config: Item = syn::parse_quote! { + #[derive(Clone, Debug, BorshSerialize, BorshDeserialize)] + pub struct UpdateCompressionConfigData { + pub new_compression_delay: Option, + pub new_rent_recipient: Option, + pub new_address_space: Option>, + pub new_update_authority: Option, + } + }; + items.push(update_config); + + // Decompress multiple PDAs instruction data + let decompress_multiple: Item = syn::parse_quote! { + #[derive(Clone, Debug, BorshSerialize, BorshDeserialize)] + pub struct DecompressMultiplePdasData { + pub proof: light_sdk::instruction::ValidityProof, + pub compressed_accounts: Vec, + pub bumps: Vec, + pub system_accounts_offset: u8, + } + }; + items.push(decompress_multiple); + + // Generate compress instruction data for each struct + for struct_name in struct_names { + let compress_data_name = format_ident!("Compress{}Data", struct_name); + let compress_data: Item = syn::parse_quote! { + #[derive(Clone, Debug, BorshSerialize, BorshDeserialize)] + pub struct #compress_data_name { + pub proof: light_sdk::instruction::ValidityProof, + pub compressed_account_meta: light_sdk_types::instruction::account_meta::CompressedAccountMeta, + } + }; + items.push(compress_data); + } + + items +} + +fn generate_thin_processors(struct_names: &[&Ident]) -> Vec { + let mut functions = Vec::new(); + + // Create config processor + let create_config_fn: Item = syn::parse_quote! { + /// Creates a compression config for this program + /// + /// Accounts expected: + /// 0. `[writable, signer]` Payer account + /// 1. `[writable]` Config PDA (seeds: [b"compressible_config"]) + /// 2. `[]` Program data account + /// 3. `[signer]` Program upgrade authority + /// 4. `[]` System program + pub fn create_compression_config( + accounts: &[solana_program::account_info::AccountInfo], + compression_delay: u32, + rent_recipient: solana_program::pubkey::Pubkey, + address_space: Vec, + ) -> solana_program::entrypoint::ProgramResult { + if accounts.len() < 5 { + return Err(solana_program::program_error::ProgramError::NotEnoughAccountKeys); + } + + let payer = &accounts[0]; + let config_account = &accounts[1]; + let program_data = &accounts[2]; + let authority = &accounts[3]; + let system_program = &accounts[4]; + + light_sdk::compressible::create_compression_config_checked( + config_account, + authority, + program_data, + &rent_recipient, + address_space, + compression_delay, + payer, + system_program, + &crate::ID, + ) + .map_err(|e| solana_program::program_error::ProgramError::from(e))?; + + Ok(()) + } + }; + functions.push(create_config_fn); + + // Update config processor + let update_config_fn: Item = syn::parse_quote! { + /// Updates the compression config + /// + /// Accounts expected: + /// 0. `[writable]` Config PDA (seeds: [b"compressible_config"]) + /// 1. `[signer]` Update authority (must match config) + pub fn update_compression_config( + accounts: &[solana_program::account_info::AccountInfo], + new_compression_delay: Option, + new_rent_recipient: Option, + new_address_space: Option>, + new_update_authority: Option, + ) -> solana_program::entrypoint::ProgramResult { + if accounts.len() < 2 { + return Err(solana_program::program_error::ProgramError::NotEnoughAccountKeys); + } + + let config_account = &accounts[0]; + let authority = &accounts[1]; + + light_sdk::compressible::update_compression_config( + config_account, + authority, + new_update_authority.as_ref(), + new_rent_recipient.as_ref(), + new_address_space, + new_compression_delay, + &crate::ID, + ) + .map_err(|e| solana_program::program_error::ProgramError::from(e))?; + + Ok(()) + } + }; + functions.push(update_config_fn); + + // Decompress multiple PDAs processor + let variant_match_arms = struct_names.iter().map(|name| { + quote! { + CompressedAccountVariant::#name(data) => { + CompressedAccountVariant::#name(data) + } + } + }); + + let decompress_fn: Item = syn::parse_quote! { + /// Decompresses multiple compressed PDAs in a single transaction + /// + /// Accounts expected: + /// 0. `[writable, signer]` Fee payer + /// 1. `[writable, signer]` Rent payer + /// 2. `[]` System program + /// 3..N. `[writable]` PDA accounts to decompress into + /// N+1... `[]` Light Protocol system accounts + pub fn decompress_multiple_pdas( + accounts: &[solana_program::account_info::AccountInfo], + proof: light_sdk::instruction::ValidityProof, + compressed_accounts: Vec, + bumps: Vec, + system_accounts_offset: u8, + ) -> solana_program::entrypoint::ProgramResult { + if accounts.len() < 3 { + return Err(solana_program::program_error::ProgramError::NotEnoughAccountKeys); + } + + let fee_payer = &accounts[0]; + let rent_payer = &accounts[1]; + + // Get PDA accounts from remaining accounts + let pda_accounts_end = system_accounts_offset as usize; + let solana_accounts = &accounts[3..3 + pda_accounts_end]; + let system_accounts = &accounts[3 + pda_accounts_end..]; + + // Validate we have matching number of PDAs, compressed accounts, and bumps + if solana_accounts.len() != compressed_accounts.len() + || solana_accounts.len() != bumps.len() { + return Err(solana_program::program_error::ProgramError::InvalidAccountData); + } + + let cpi_accounts = light_sdk::cpi::CpiAccounts::new( + fee_payer, + system_accounts, + crate::LIGHT_CPI_SIGNER, + ); + + // Convert to unified enum accounts + let mut light_accounts = Vec::new(); + let mut pda_account_refs = Vec::new(); + let mut signer_seeds_storage = Vec::new(); + + for (i, (compressed_data, bump)) in compressed_accounts.into_iter() + .zip(bumps.iter()).enumerate() { + + // Convert to unified enum type + let unified_account = match compressed_data.data { + #(#variant_match_arms)* + }; + + let light_account = light_sdk::account::sha::LightAccount::<'_, CompressedAccountVariant>::new_mut( + &crate::ID, + &compressed_data.meta, + unified_account.clone(), + ) + .map_err(|e| solana_program::program_error::ProgramError::from(e))?; + + // Build signer seeds based on account type + let seeds = match &unified_account { + #( + CompressedAccountVariant::#struct_names(_) => { + // Get the seeds from the instruction data and append bump + let mut seeds = compressed_data.seeds.clone(); + seeds.push(vec![*bump]); + seeds + } + ),* + }; + + signer_seeds_storage.push(seeds); + light_accounts.push(light_account); + pda_account_refs.push(&solana_accounts[i]); + } + + // Convert to the format needed by the SDK + let signer_seeds_refs: Vec> = signer_seeds_storage + .iter() + .map(|seeds| seeds.iter().map(|s| s.as_slice()).collect()) + .collect(); + let signer_seeds_slices: Vec<&[&[u8]]> = signer_seeds_refs + .iter() + .map(|seeds| seeds.as_slice()) + .collect(); + + // Single CPI call with unified enum type + light_sdk::compressible::decompress_multiple_idempotent::( + &pda_account_refs, + light_accounts, + &signer_seeds_slices, + proof, + cpi_accounts, + &crate::ID, + rent_payer, + ) + .map_err(|e| solana_program::program_error::ProgramError::from(e))?; + + Ok(()) + } + }; + functions.push(decompress_fn); + + // Generate compress processors for each account type + for struct_name in struct_names { + let compress_fn_name = + format_ident!("compress_{}", struct_name.to_string().to_snake_case()); + + let compress_processor: Item = syn::parse_quote! { + /// Compresses a #struct_name PDA + /// + /// Accounts expected: + /// 0. `[signer]` Authority + /// 1. `[writable]` PDA account to compress + /// 2. `[]` System program + /// 3. `[]` Config PDA + /// 4. `[]` Rent recipient (must match config) + /// 5... `[]` Light Protocol system accounts + pub fn #compress_fn_name( + accounts: &[solana_program::account_info::AccountInfo], + proof: light_sdk::instruction::ValidityProof, + compressed_account_meta: light_sdk_types::instruction::account_meta::CompressedAccountMeta, + ) -> solana_program::entrypoint::ProgramResult { + if accounts.len() < 6 { + return Err(solana_program::program_error::ProgramError::NotEnoughAccountKeys); + } + + let authority = &accounts[0]; + let solana_account = &accounts[1]; + let _system_program = &accounts[2]; + let config_account = &accounts[3]; + let rent_recipient = &accounts[4]; + let system_accounts = &accounts[5..]; + + // Load config from AccountInfo + let config = light_sdk::compressible::CompressibleConfig::load_checked( + config_account, + &crate::ID + ).map_err(|_| solana_program::program_error::ProgramError::InvalidAccountData)?; + + // Verify rent recipient matches config + if rent_recipient.key != &config.rent_recipient { + return Err(solana_program::program_error::ProgramError::InvalidAccountData); + } + + let cpi_accounts = light_sdk::cpi::CpiAccounts::new( + authority, + system_accounts, + crate::LIGHT_CPI_SIGNER, + ); + + light_sdk::compressible::compress_account::<#struct_name>( + solana_account, + &compressed_account_meta, + proof, + cpi_accounts, + &crate::ID, + rent_recipient, + &config.compression_delay, + ) + .map_err(|e| solana_program::program_error::ProgramError::from(e))?; + + Ok(()) + } + }; + functions.push(compress_processor); + } + + functions +} diff --git a/sdk-libs/macros/src/pack_unpack.rs b/sdk-libs/macros/src/pack_unpack.rs new file mode 100644 index 0000000000..a715bfc162 --- /dev/null +++ b/sdk-libs/macros/src/pack_unpack.rs @@ -0,0 +1,270 @@ +use proc_macro2::TokenStream; +use quote::{format_ident, quote}; +use syn::{Data, DeriveInput, Fields, Result, Type}; + +/// Generates Pack and Unpack trait implementations for compressible account types +/// +/// For types with Pubkey fields, this also generates a PackedXxx struct where Pubkeys become u8 indices. +/// For types without Pubkeys, generates identity Pack/Unpack implementations. +/// +/// Usage: #[derive(CompressiblePack)] +pub fn derive_compressible_pack(input: DeriveInput) -> Result { + let struct_name = &input.ident; + let packed_struct_name = format_ident!("Packed{}", struct_name); + + let fields = match &input.data { + Data::Struct(data) => match &data.fields { + Fields::Named(fields) => &fields.named, + _ => { + return Err(syn::Error::new_spanned( + &input, + "CompressiblePack only supports structs with named fields", + )); + } + }, + _ => { + return Err(syn::Error::new_spanned( + &input, + "CompressiblePack only supports structs", + )); + } + }; + + // Check if this struct has any Pubkey fields that need packing + let has_pubkey_fields = fields.iter().any(|field| { + if let Type::Path(type_path) = &field.ty { + if let Some(segment) = type_path.path.segments.last() { + segment.ident == "Pubkey" + } else { + false + } + } else { + false + } + }); + + if has_pubkey_fields { + // Generate PackedXxx struct and Pack/Unpack implementations for types with Pubkeys + generate_with_packed_struct(struct_name, &packed_struct_name, fields) + } else { + // Generate identity Pack/Unpack implementations for types without Pubkeys + generate_identity_pack_unpack(struct_name) + } +} + +fn generate_with_packed_struct( + struct_name: &syn::Ident, + packed_struct_name: &syn::Ident, + fields: &syn::punctuated::Punctuated, +) -> Result { + // Generate fields for the packed struct + let packed_fields = fields.iter().map(|field| { + let field_name = field.ident.as_ref().unwrap(); + let field_type = &field.ty; + + // Convert Pubkey fields to u8, keep others as-is + let packed_type = if is_pubkey_type(field_type) { + quote! { u8 } + } else { + quote! { #field_type } + }; + + quote! { pub #field_name: #packed_type } + }); + + // Generate the packed struct + let packed_struct = quote! { + #[derive(Debug, Clone, light_sdk::AnchorSerialize, light_sdk::AnchorDeserialize)] + pub struct #packed_struct_name { + #(#packed_fields,)* + } + }; + + // Generate Pack implementation for original struct + let pack_field_assignments = fields.iter().map(|field| { + let field_name = field.ident.as_ref().unwrap(); + let field_type = &field.ty; + + if field_name.to_string() == "compression_info" { + quote! { #field_name: None } + } else if is_pubkey_type(field_type) { + quote! { #field_name: remaining_accounts.insert_or_get(self.#field_name) } + } else if is_copy_type(field_type) { + quote! { #field_name: self.#field_name } + } else { + quote! { #field_name: self.#field_name.clone() } + } + }); + + let pack_impl = quote! { + impl light_sdk::compressible::Pack for #struct_name { + type Packed = #packed_struct_name; + + fn pack(&self, remaining_accounts: &mut light_sdk::instruction::PackedAccounts) -> Self::Packed { + #packed_struct_name { + #(#pack_field_assignments,)* + } + } + } + }; + + // Generate Unpack implementation for original struct (identity) + let unpack_impl_original = quote! { + impl light_sdk::compressible::Unpack for #struct_name { + type Unpacked = Self; + + fn unpack( + &self, + _remaining_accounts: &[anchor_lang::prelude::AccountInfo], + ) -> std::result::Result { + Ok(self.clone()) + } + } + }; + + // Generate Pack implementation for packed struct (identity) + let pack_impl_packed = quote! { + impl light_sdk::compressible::Pack for #packed_struct_name { + type Packed = Self; + + fn pack(&self, _remaining_accounts: &mut light_sdk::instruction::PackedAccounts) -> Self::Packed { + self.clone() + } + } + }; + + // Generate Unpack implementation for packed struct + let unpack_field_assignments = fields.iter().map(|field| { + let field_name = field.ident.as_ref().unwrap(); + let field_type = &field.ty; + + if field_name.to_string() == "compression_info" { + quote! { #field_name: None } + } else if is_pubkey_type(field_type) { + quote! { + #field_name: *remaining_accounts[self.#field_name as usize].key + } + } else if is_copy_type(field_type) { + quote! { #field_name: self.#field_name } + } else { + quote! { #field_name: self.#field_name.clone() } + } + }); + + let unpack_impl_packed = quote! { + impl light_sdk::compressible::Unpack for #packed_struct_name { + type Unpacked = #struct_name; + + fn unpack( + &self, + remaining_accounts: &[anchor_lang::prelude::AccountInfo], + ) -> std::result::Result { + Ok(#struct_name { + #(#unpack_field_assignments,)* + }) + } + } + }; + + let expanded = quote! { + #packed_struct + #pack_impl + #unpack_impl_original + #pack_impl_packed + #unpack_impl_packed + }; + + Ok(expanded) +} + +fn generate_identity_pack_unpack(struct_name: &syn::Ident) -> Result { + let pack_impl = quote! { + impl light_sdk::compressible::Pack for #struct_name { + type Packed = Self; + + fn pack(&self, _remaining_accounts: &mut light_sdk::instruction::PackedAccounts) -> Self::Packed { + self.clone() + } + } + }; + + let unpack_impl = quote! { + impl light_sdk::compressible::Unpack for #struct_name { + type Unpacked = Self; + + fn unpack( + &self, + _remaining_accounts: &[anchor_lang::prelude::AccountInfo], + ) -> std::result::Result { + Ok(self.clone()) + } + } + }; + + let expanded = quote! { + #pack_impl + #unpack_impl + }; + + Ok(expanded) +} + +/// Check if a type is Pubkey +fn is_pubkey_type(ty: &Type) -> bool { + if let Type::Path(type_path) = ty { + if let Some(segment) = type_path.path.segments.last() { + segment.ident == "Pubkey" + } else { + false + } + } else { + false + } +} + +/// Determines if a type is likely to be Copy (simple heuristic) +fn is_copy_type(ty: &Type) -> bool { + match ty { + Type::Path(type_path) => { + if let Some(segment) = type_path.path.segments.last() { + let type_name = segment.ident.to_string(); + matches!( + type_name.as_str(), + "u8" | "u16" + | "u32" + | "u64" + | "u128" + | "usize" + | "i8" + | "i16" + | "i32" + | "i64" + | "i128" + | "isize" + | "f32" + | "f64" + | "bool" + | "char" + | "Pubkey" + ) || (type_name == "Option" && has_copy_inner_type(&segment.arguments)) + } else { + false + } + } + _ => false, + } +} + +/// Check if Option where T is Copy +fn has_copy_inner_type(args: &syn::PathArguments) -> bool { + match args { + syn::PathArguments::AngleBracketed(args) => args.args.iter().any(|arg| { + if let syn::GenericArgument::Type(ty) = arg { + is_copy_type(ty) + } else { + false + } + }), + _ => false, + } +} diff --git a/sdk-libs/macros/src/test_modular_macros.rs b/sdk-libs/macros/src/test_modular_macros.rs new file mode 100644 index 0000000000..6fa3df9c63 --- /dev/null +++ b/sdk-libs/macros/src/test_modular_macros.rs @@ -0,0 +1,164 @@ +#[cfg(test)] +mod test { + use quote::quote; + use syn::parse_quote; + + #[test] + fn test_compressible_derive() { + let input: syn::DeriveInput = parse_quote! { + pub struct UserRecord { + pub compression_info: Option, + pub owner: Pubkey, + pub name: String, + pub score: u64, + } + }; + + let result = crate::compressible_derive::derive_compressible(input); + assert!(result.is_ok(), "Compressible derive should succeed"); + + let output = result.unwrap(); + let output_str = output.to_string(); + + println!("Generated output:\n{}", output_str); + + // Check that all required trait implementations are generated + assert!(output_str.contains("impl light_sdk :: compressible :: HasCompressionInfo")); + assert!(output_str.contains("impl light_sdk :: account :: Size")); + assert!(output_str.contains("impl light_sdk :: compressible :: CompressAs")); + assert!(output_str.contains("compression_info : None")); + } + + #[test] + fn test_compressible_pack_derive() { + let input: syn::DeriveInput = parse_quote! { + pub struct UserRecord { + pub compression_info: Option, + pub owner: Pubkey, + pub name: String, + pub score: u64, + } + }; + + let result = crate::pack_unpack::derive_compressible_pack(input); + assert!(result.is_ok(), "CompressiblePack derive should succeed"); + + let output = result.unwrap(); + let output_str = output.to_string(); + + println!("Pack derive output:\n{}", output_str); + + // Check that PackedUserRecord struct is generated + assert!(output_str.contains("pub struct PackedUserRecord")); + assert!(output_str.contains("pub owner : u8")); // Pubkey packed as u8 + assert!(output_str.contains("pub name : String")); // String kept as-is + + // Check that Pack/Unpack implementations are generated + assert!(output_str.contains("impl light_sdk :: compressible :: Pack for UserRecord")); + assert!(output_str.contains("impl light_sdk :: compressible :: Unpack for UserRecord")); + assert!(output_str.contains("impl light_sdk :: compressible :: Pack for PackedUserRecord")); + assert!( + output_str.contains("impl light_sdk :: compressible :: Unpack for PackedUserRecord") + ); + } + + #[test] + fn test_compressed_account_variant_macro() { + let input = quote! { UserRecord, GameSession }; + + let result = crate::variant_enum::compressed_account_variant(input); + assert!( + result.is_ok(), + "compressed_account_variant macro should succeed" + ); + + let output = result.unwrap(); + let output_str = output.to_string(); + + println!("Variant enum output:\n{}", output_str); + + // Check that enum is generated with all variants + assert!(output_str.contains("pub enum CompressedAccountVariant")); + assert!(output_str.contains("UserRecord (UserRecord)")); + assert!(output_str.contains("PackedUserRecord (PackedUserRecord)")); + assert!(output_str.contains("GameSession (GameSession)")); + assert!(output_str.contains("CompressibleTokenAccountPacked")); + assert!(output_str.contains("CompressibleTokenData")); + + // Check that all trait implementations are generated + assert!(output_str.contains("impl Default for CompressedAccountVariant")); + assert!(output_str.contains("impl light_hasher :: DataHasher for CompressedAccountVariant")); + assert!(output_str + .contains("impl light_sdk :: LightDiscriminator for CompressedAccountVariant")); + assert!(output_str.contains( + "impl light_sdk :: compressible :: HasCompressionInfo for CompressedAccountVariant" + )); + assert!( + output_str.contains("impl light_sdk :: account :: Size for CompressedAccountVariant") + ); + assert!(output_str + .contains("impl light_sdk :: compressible :: Pack for CompressedAccountVariant")); + assert!(output_str + .contains("impl light_sdk :: compressible :: Unpack for CompressedAccountVariant")); + + // Check that CompressedAccountData struct is generated + assert!(output_str.contains("pub struct CompressedAccountData")); + } + + #[test] + fn test_custom_compression_with_compress_as_attribute() { + let input: syn::DeriveInput = parse_quote! { + #[compress_as(start_time = 0, score = 100)] + pub struct GameSession { + pub compression_info: Option, + pub session_id: u64, + pub player: Pubkey, + pub start_time: u64, + pub score: u64, + } + }; + + let result = crate::compressible_derive::derive_compressible(input); + assert!( + result.is_ok(), + "Compressible derive with compress_as should succeed" + ); + + let output = result.unwrap(); + let output_str = output.to_string(); + + println!("Custom compression output:\n{}", output_str); + + // Check that custom field values are used + assert!(output_str.contains("start_time : 0")); + assert!(output_str.contains("score : 100")); + // Check that non-overridden fields use original values + assert!(output_str.contains("session_id : self . session_id")); + assert!(output_str.contains("player : self . player")); + } + + #[test] + fn test_derive_seeds_macro() { + let input: syn::DeriveInput = parse_quote! { + #[seeds("user_record", owner)] + pub struct UserRecord { + pub owner: Pubkey, + pub name: String, + pub score: u64, + } + }; + + let result = crate::derive_seeds::derive_seeds(input); + assert!(result.is_ok(), "DeriveSeeds should succeed"); + + let output = result.unwrap(); + let output_str = output.to_string(); + + println!("DeriveSeeds output:\n{}", output_str); + + // Check that function is generated + assert!(output_str.contains("pub fn get_user_record_seeds")); + assert!(output_str.contains("owner : & Pubkey")); + assert!(output_str.contains("find_program_address")); + } +} diff --git a/sdk-libs/macros/src/variant_enum.rs b/sdk-libs/macros/src/variant_enum.rs new file mode 100644 index 0000000000..926188ea6b --- /dev/null +++ b/sdk-libs/macros/src/variant_enum.rs @@ -0,0 +1,263 @@ +use proc_macro2::TokenStream; +use quote::quote; +use syn::{ + parse::{Parse, ParseStream}, + punctuated::Punctuated, + Ident, Result, Token, +}; + +/// Parse a comma-separated list of account type identifiers +struct AccountTypeList { + types: Punctuated, +} + +impl Parse for AccountTypeList { + fn parse(input: ParseStream) -> Result { + Ok(AccountTypeList { + types: Punctuated::parse_terminated(input)?, + }) + } +} + +/// Generates CompressedAccountVariant enum and CompressedAccountData struct with all trait implementations +/// +/// Usage: compressed_account_variant!(UserRecord, GameSession, PlaceholderRecord); +/// +/// This generates: +/// - CompressedAccountVariant enum with variants for each type + token variants +/// - All required trait implementations: Default, DataHasher, LightDiscriminator, HasCompressionInfo, Size, Pack, Unpack +/// - CompressedAccountData struct for instruction data +pub fn compressed_account_variant(input: TokenStream) -> Result { + let type_list = syn::parse2::(input)?; + let account_types: Vec<&Ident> = type_list.types.iter().collect(); + + if account_types.is_empty() { + return Err(syn::Error::new_spanned( + &type_list.types, + "At least one account type must be specified", + )); + } + + // Generate enum variants for account types + let account_variants = account_types.iter().map(|name| { + let packed_name = quote::format_ident!("Packed{}", name); + quote! { + #name(#name), + #packed_name(#packed_name), + } + }); + + // Generate the CompressedAccountVariant enum with token variants + let enum_def = quote! { + #[derive(Clone, Debug, light_sdk::AnchorSerialize, light_sdk::AnchorDeserialize)] + pub enum CompressedAccountVariant { + #(#account_variants)* + // Token account variants (always included) + CompressibleTokenAccountPacked(light_sdk::token::PackedCompressibleTokenDataWithVariant), + CompressibleTokenData(light_sdk::token::CompressibleTokenDataWithVariant), + } + }; + + // Generate Default implementation + let first_type = account_types[0]; + let default_impl = quote! { + impl Default for CompressedAccountVariant { + fn default() -> Self { + Self::#first_type(#first_type::default()) + } + } + }; + + // Generate DataHasher implementation + let hash_match_arms = account_types.iter().map(|name| { + let packed_name = quote::format_ident!("Packed{}", name); + quote! { + CompressedAccountVariant::#name(data) => <#name as light_hasher::DataHasher>::hash::(data), + CompressedAccountVariant::#packed_name(_) => unreachable!(), + } + }); + + let data_hasher_impl = quote! { + impl light_hasher::DataHasher for CompressedAccountVariant { + fn hash(&self) -> std::result::Result<[u8; 32], light_hasher::HasherError> { + match self { + #(#hash_match_arms)* + Self::CompressibleTokenAccountPacked(_) => unreachable!(), + Self::CompressibleTokenData(_) => unreachable!(), + } + } + } + }; + + // Generate LightDiscriminator implementation + let light_discriminator_impl = quote! { + impl light_sdk::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; + } + }; + + // Generate HasCompressionInfo implementation + let compression_info_match_arms = account_types.iter().map(|name| { + let packed_name = quote::format_ident!("Packed{}", name); + quote! { + CompressedAccountVariant::#name(data) => <#name as light_sdk::compressible::HasCompressionInfo>::compression_info(data), + CompressedAccountVariant::#packed_name(_) => unreachable!(), + } + }); + + let compression_info_mut_match_arms = account_types.iter().map(|name| { + let packed_name = quote::format_ident!("Packed{}", name); + quote! { + CompressedAccountVariant::#name(data) => <#name as light_sdk::compressible::HasCompressionInfo>::compression_info_mut(data), + CompressedAccountVariant::#packed_name(_) => unreachable!(), + } + }); + + let compression_info_mut_opt_match_arms = account_types.iter().map(|name| { + let packed_name = quote::format_ident!("Packed{}", name); + quote! { + CompressedAccountVariant::#name(data) => <#name as light_sdk::compressible::HasCompressionInfo>::compression_info_mut_opt(data), + CompressedAccountVariant::#packed_name(_) => unreachable!(), + } + }); + + let set_compression_info_none_match_arms = account_types.iter().map(|name| { + let packed_name = quote::format_ident!("Packed{}", name); + quote! { + CompressedAccountVariant::#name(data) => <#name as light_sdk::compressible::HasCompressionInfo>::set_compression_info_none(data), + CompressedAccountVariant::#packed_name(_) => unreachable!(), + } + }); + + let has_compression_info_impl = quote! { + impl light_sdk::compressible::HasCompressionInfo for CompressedAccountVariant { + fn compression_info(&self) -> &light_sdk::compressible::CompressionInfo { + match self { + #(#compression_info_match_arms)* + Self::CompressibleTokenAccountPacked(_) => unreachable!(), + Self::CompressibleTokenData(_) => unreachable!(), + } + } + + fn compression_info_mut(&mut self) -> &mut light_sdk::compressible::CompressionInfo { + match self { + #(#compression_info_mut_match_arms)* + Self::CompressibleTokenAccountPacked(_) => unreachable!(), + Self::CompressibleTokenData(_) => unreachable!(), + } + } + + fn compression_info_mut_opt(&mut self) -> &mut Option { + match self { + #(#compression_info_mut_opt_match_arms)* + Self::CompressibleTokenAccountPacked(_) => unreachable!(), + Self::CompressibleTokenData(_) => unreachable!(), + } + } + + fn set_compression_info_none(&mut self) { + match self { + #(#set_compression_info_none_match_arms)* + Self::CompressibleTokenAccountPacked(_) => unreachable!(), + Self::CompressibleTokenData(_) => unreachable!(), + } + } + } + }; + + // Generate Size implementation + let size_match_arms = account_types.iter().map(|name| { + let packed_name = quote::format_ident!("Packed{}", name); + quote! { + CompressedAccountVariant::#name(data) => <#name as light_sdk::account::Size>::size(data), + CompressedAccountVariant::#packed_name(_) => unreachable!(), + } + }); + + let size_impl = quote! { + impl light_sdk::account::Size for CompressedAccountVariant { + fn size(&self) -> usize { + match self { + #(#size_match_arms)* + Self::CompressibleTokenAccountPacked(_) => unreachable!(), + Self::CompressibleTokenData(_) => unreachable!(), + } + } + } + }; + + // Generate Pack implementation + let pack_match_arms = account_types.iter().map(|name| { + let packed_name = quote::format_ident!("Packed{}", name); + quote! { + CompressedAccountVariant::#packed_name(_) => unreachable!(), + CompressedAccountVariant::#name(data) => CompressedAccountVariant::#packed_name(<#name as light_sdk::compressible::Pack>::pack(data, remaining_accounts)), + } + }); + + let pack_impl = quote! { + impl light_sdk::compressible::Pack for CompressedAccountVariant { + type Packed = Self; + + fn pack(&self, remaining_accounts: &mut light_sdk::instruction::PackedAccounts) -> Self::Packed { + match self { + #(#pack_match_arms)* + Self::CompressibleTokenAccountPacked(_) => unreachable!(), + Self::CompressibleTokenData(data) => { + Self::CompressibleTokenAccountPacked(data.pack(remaining_accounts)) + } + } + } + } + }; + + // Generate Unpack implementation + let unpack_match_arms = account_types.iter().map(|name| { + let packed_name = quote::format_ident!("Packed{}", name); + quote! { + CompressedAccountVariant::#packed_name(data) => Ok(CompressedAccountVariant::#name(<#packed_name as light_sdk::compressible::Unpack>::unpack(data, remaining_accounts)?)), + CompressedAccountVariant::#name(_) => unreachable!(), + } + }); + + let unpack_impl = quote! { + impl light_sdk::compressible::Unpack for CompressedAccountVariant { + type Unpacked = Self; + + fn unpack( + &self, + remaining_accounts: &[anchor_lang::prelude::AccountInfo], + ) -> std::result::Result { + match self { + #(#unpack_match_arms)* + Self::CompressibleTokenAccountPacked(_data) => Ok(self.clone()), // as-is + Self::CompressibleTokenData(_data) => unreachable!(), // as-is + } + } + } + }; + + // Generate CompressedAccountData struct + let compressed_account_data_struct = quote! { + #[derive(Clone, Debug, light_sdk::AnchorDeserialize, light_sdk::AnchorSerialize)] + pub struct CompressedAccountData { + pub meta: light_sdk::instruction::account_meta::CompressedAccountMetaNoLamportsNoAddress, + pub data: CompressedAccountVariant, + } + }; + + let expanded = quote! { + #enum_def + #default_impl + #data_hasher_impl + #light_discriminator_impl + #has_compression_info_impl + #size_impl + #pack_impl + #unpack_impl + #compressed_account_data_struct + }; + + Ok(expanded) +} diff --git a/sdk-libs/photon-api/src/lib.rs b/sdk-libs/photon-api/src/lib.rs index 92760eb50e..5bcbbc3dd2 100644 --- a/sdk-libs/photon-api/src/lib.rs +++ b/sdk-libs/photon-api/src/lib.rs @@ -10,3 +10,4 @@ extern crate url; pub mod apis; pub mod models; +pub mod string_u64; diff --git a/sdk-libs/photon-api/src/models/_get_batch_address_update_info_post_200_response_result.rs b/sdk-libs/photon-api/src/models/_get_batch_address_update_info_post_200_response_result.rs index 2d30e4e946..4ee5644af9 100644 --- a/sdk-libs/photon-api/src/models/_get_batch_address_update_info_post_200_response_result.rs +++ b/sdk-libs/photon-api/src/models/_get_batch_address_update_info_post_200_response_result.rs @@ -18,7 +18,11 @@ pub struct GetBatchAddressUpdateInfoPost200ResponseResult { pub context: Box, #[serde(rename = "nonInclusionProofs")] pub non_inclusion_proofs: Vec, - #[serde(rename = "startIndex")] + #[serde( + rename = "startIndex", + deserialize_with = "crate::string_u64::direct::deserialize", + serialize_with = "crate::string_u64::direct::serialize" + )] pub start_index: u64, #[serde(rename = "subtrees")] pub subtrees: Vec>, diff --git a/sdk-libs/photon-api/src/models/_get_compressed_account_balance_post_200_response_result.rs b/sdk-libs/photon-api/src/models/_get_compressed_account_balance_post_200_response_result.rs index 05856cb36c..160e1503d2 100644 --- a/sdk-libs/photon-api/src/models/_get_compressed_account_balance_post_200_response_result.rs +++ b/sdk-libs/photon-api/src/models/_get_compressed_account_balance_post_200_response_result.rs @@ -14,7 +14,11 @@ use crate::models; pub struct GetCompressedAccountBalancePost200ResponseResult { #[serde(rename = "context")] pub context: Box, - #[serde(rename = "value")] + #[serde( + rename = "value", + deserialize_with = "crate::string_u64::direct::deserialize", + serialize_with = "crate::string_u64::direct::serialize" + )] pub value: u64, } diff --git a/sdk-libs/photon-api/src/models/account.rs b/sdk-libs/photon-api/src/models/account.rs index c35b077ae7..5cbdd3db8b 100644 --- a/sdk-libs/photon-api/src/models/account.rs +++ b/sdk-libs/photon-api/src/models/account.rs @@ -20,16 +20,34 @@ pub struct Account { /// A 32-byte hash represented as a base58 string. #[serde(rename = "hash")] pub hash: String, - #[serde(rename = "lamports")] + #[serde( + rename = "lamports", + deserialize_with = "crate::string_u64::direct::deserialize", + serialize_with = "crate::string_u64::direct::serialize" + )] pub lamports: u64, - #[serde(rename = "leafIndex")] + #[serde( + rename = "leafIndex", + deserialize_with = "crate::string_u64::u32_direct::deserialize", + serialize_with = "crate::string_u64::u32_direct::serialize" + )] pub leaf_index: u32, /// A Solana public key represented as a base58 string. #[serde(rename = "owner")] pub owner: String, - #[serde(rename = "seq", skip_serializing_if = "Option::is_none")] + #[serde( + rename = "seq", + skip_serializing_if = "Option::is_none", + deserialize_with = "crate::string_u64::option_direct::deserialize", + serialize_with = "crate::string_u64::option_direct::serialize", + default + )] pub seq: Option, - #[serde(rename = "slotCreated")] + #[serde( + rename = "slotCreated", + deserialize_with = "crate::string_u64::direct::deserialize", + serialize_with = "crate::string_u64::direct::serialize" + )] pub slot_created: u64, /// A Solana public key represented as a base58 string. #[serde(rename = "tree")] diff --git a/sdk-libs/photon-api/src/models/account_context.rs b/sdk-libs/photon-api/src/models/account_context.rs index 971342d90e..50fc60f0c2 100644 --- a/sdk-libs/photon-api/src/models/account_context.rs +++ b/sdk-libs/photon-api/src/models/account_context.rs @@ -30,7 +30,11 @@ pub struct AccountContext { pub queue: String, #[serde(rename = "spent")] pub spent: bool, - #[serde(rename = "treeType")] + #[serde( + rename = "treeType", + deserialize_with = "crate::string_u64::u16_direct::deserialize", + serialize_with = "crate::string_u64::u16_direct::serialize" + )] pub tree_type: u16, /// A 32-byte hash represented as a base58 string. #[serde(rename = "txHash", skip_serializing_if = "Option::is_none")] diff --git a/sdk-libs/photon-api/src/models/account_data.rs b/sdk-libs/photon-api/src/models/account_data.rs index c6c683dfcd..15b24fd74c 100644 --- a/sdk-libs/photon-api/src/models/account_data.rs +++ b/sdk-libs/photon-api/src/models/account_data.rs @@ -18,7 +18,11 @@ pub struct AccountData { /// A 32-byte hash represented as a base58 string. #[serde(rename = "dataHash")] pub data_hash: String, - #[serde(rename = "discriminator")] + #[serde( + rename = "discriminator", + deserialize_with = "crate::string_u64::direct::deserialize", + serialize_with = "crate::string_u64::direct::serialize" + )] pub discriminator: u64, } diff --git a/sdk-libs/photon-api/src/models/account_proof_inputs.rs b/sdk-libs/photon-api/src/models/account_proof_inputs.rs index 1950ea015d..d230527fc7 100644 --- a/sdk-libs/photon-api/src/models/account_proof_inputs.rs +++ b/sdk-libs/photon-api/src/models/account_proof_inputs.rs @@ -14,7 +14,11 @@ use crate::models; pub struct AccountProofInputs { #[serde(rename = "hash")] pub hash: String, - #[serde(rename = "leafIndex")] + #[serde( + rename = "leafIndex", + deserialize_with = "crate::string_u64::direct::deserialize", + serialize_with = "crate::string_u64::direct::serialize" + )] pub leaf_index: u64, #[serde(rename = "merkleContext")] pub merkle_context: Box, diff --git a/sdk-libs/photon-api/src/models/account_v2.rs b/sdk-libs/photon-api/src/models/account_v2.rs index 31159f8c67..c67b70e0f5 100644 --- a/sdk-libs/photon-api/src/models/account_v2.rs +++ b/sdk-libs/photon-api/src/models/account_v2.rs @@ -20,9 +20,17 @@ pub struct AccountV2 { /// A 32-byte hash represented as a base58 string. #[serde(rename = "hash")] pub hash: String, - #[serde(rename = "lamports")] + #[serde( + rename = "lamports", + deserialize_with = "crate::string_u64::direct::deserialize", + serialize_with = "crate::string_u64::direct::serialize" + )] pub lamports: u64, - #[serde(rename = "leafIndex")] + #[serde( + rename = "leafIndex", + deserialize_with = "crate::string_u64::u32_direct::deserialize", + serialize_with = "crate::string_u64::u32_direct::serialize" + )] pub leaf_index: u32, #[serde(rename = "merkleContext")] pub merkle_context: Box, @@ -31,9 +39,19 @@ pub struct AccountV2 { pub owner: String, #[serde(rename = "proveByIndex")] pub prove_by_index: bool, - #[serde(rename = "seq", skip_serializing_if = "Option::is_none")] + #[serde( + rename = "seq", + skip_serializing_if = "Option::is_none", + deserialize_with = "crate::string_u64::option_direct::deserialize", + serialize_with = "crate::string_u64::option_direct::serialize", + default + )] pub seq: Option, - #[serde(rename = "slotCreated")] + #[serde( + rename = "slotCreated", + deserialize_with = "crate::string_u64::direct::deserialize", + serialize_with = "crate::string_u64::direct::serialize" + )] pub slot_created: u64, } diff --git a/sdk-libs/photon-api/src/models/address_proof_inputs.rs b/sdk-libs/photon-api/src/models/address_proof_inputs.rs index eaf6f424aa..c0558e14f5 100644 --- a/sdk-libs/photon-api/src/models/address_proof_inputs.rs +++ b/sdk-libs/photon-api/src/models/address_proof_inputs.rs @@ -18,7 +18,11 @@ pub struct AddressProofInputs { pub merkle_context: Box, #[serde(rename = "root")] pub root: String, - #[serde(rename = "rootIndex")] + #[serde( + rename = "rootIndex", + deserialize_with = "crate::string_u64::u16_direct::deserialize", + serialize_with = "crate::string_u64::u16_direct::serialize" + )] pub root_index: u16, } diff --git a/sdk-libs/photon-api/src/models/address_queue_index.rs b/sdk-libs/photon-api/src/models/address_queue_index.rs index 3ce38a203d..b6f18faf2f 100644 --- a/sdk-libs/photon-api/src/models/address_queue_index.rs +++ b/sdk-libs/photon-api/src/models/address_queue_index.rs @@ -15,7 +15,11 @@ pub struct AddressQueueIndex { /// A Solana public key represented as a base58 string. #[serde(rename = "address")] pub address: String, - #[serde(rename = "queueIndex")] + #[serde( + rename = "queueIndex", + deserialize_with = "crate::string_u64::direct::deserialize", + serialize_with = "crate::string_u64::direct::serialize" + )] pub queue_index: u64, } diff --git a/sdk-libs/photon-api/src/models/context.rs b/sdk-libs/photon-api/src/models/context.rs index 3df4b4952e..4cfc2c4806 100644 --- a/sdk-libs/photon-api/src/models/context.rs +++ b/sdk-libs/photon-api/src/models/context.rs @@ -12,7 +12,11 @@ use crate::models; #[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] pub struct Context { - #[serde(rename = "slot")] + #[serde( + rename = "slot", + deserialize_with = "crate::string_u64::direct::deserialize", + serialize_with = "crate::string_u64::direct::serialize" + )] pub slot: u64, } diff --git a/sdk-libs/photon-api/src/models/get_compressed_account_proof_response_value.rs b/sdk-libs/photon-api/src/models/get_compressed_account_proof_response_value.rs index 40d32d17b4..2ea7cdfe39 100644 --- a/sdk-libs/photon-api/src/models/get_compressed_account_proof_response_value.rs +++ b/sdk-libs/photon-api/src/models/get_compressed_account_proof_response_value.rs @@ -15,7 +15,11 @@ pub struct GetCompressedAccountProofResponseValue { /// A 32-byte hash represented as a base58 string. #[serde(rename = "hash")] pub hash: String, - #[serde(rename = "leafIndex")] + #[serde( + rename = "leafIndex", + deserialize_with = "crate::string_u64::direct::deserialize", + serialize_with = "crate::string_u64::direct::serialize" + )] pub leaf_index: u64, /// A Solana public key represented as a base58 string. #[serde(rename = "merkleTree")] @@ -25,7 +29,11 @@ pub struct GetCompressedAccountProofResponseValue { /// A 32-byte hash represented as a base58 string. #[serde(rename = "root")] pub root: String, - #[serde(rename = "rootSeq")] + #[serde( + rename = "rootSeq", + deserialize_with = "crate::string_u64::direct::deserialize", + serialize_with = "crate::string_u64::direct::serialize" + )] pub root_seq: u64, } diff --git a/sdk-libs/photon-api/src/models/get_compressed_account_proof_response_value_v2.rs b/sdk-libs/photon-api/src/models/get_compressed_account_proof_response_value_v2.rs index 92aff2070c..e3eecc2725 100644 --- a/sdk-libs/photon-api/src/models/get_compressed_account_proof_response_value_v2.rs +++ b/sdk-libs/photon-api/src/models/get_compressed_account_proof_response_value_v2.rs @@ -15,7 +15,11 @@ pub struct GetCompressedAccountProofResponseValueV2 { /// A 32-byte hash represented as a base58 string. #[serde(rename = "hash")] pub hash: String, - #[serde(rename = "leafIndex")] + #[serde( + rename = "leafIndex", + deserialize_with = "crate::string_u64::u32_direct::deserialize", + serialize_with = "crate::string_u64::u32_direct::serialize" + )] pub leaf_index: u32, #[serde(rename = "proof")] pub proof: Vec, @@ -24,7 +28,11 @@ pub struct GetCompressedAccountProofResponseValueV2 { /// A 32-byte hash represented as a base58 string. #[serde(rename = "root")] pub root: String, - #[serde(rename = "rootSeq")] + #[serde( + rename = "rootSeq", + deserialize_with = "crate::string_u64::direct::deserialize", + serialize_with = "crate::string_u64::direct::serialize" + )] pub root_seq: u64, #[serde(rename = "treeContext")] pub tree_context: Box, diff --git a/sdk-libs/photon-api/src/models/get_queue_elements_response_value.rs b/sdk-libs/photon-api/src/models/get_queue_elements_response_value.rs index 175579c0d2..1877868edf 100644 --- a/sdk-libs/photon-api/src/models/get_queue_elements_response_value.rs +++ b/sdk-libs/photon-api/src/models/get_queue_elements_response_value.rs @@ -18,14 +18,22 @@ pub struct GetQueueElementsResponseValue { /// A 32-byte hash represented as a base58 string. #[serde(rename = "leaf")] pub leaf: String, - #[serde(rename = "leafIndex")] + #[serde( + rename = "leafIndex", + deserialize_with = "crate::string_u64::direct::deserialize", + serialize_with = "crate::string_u64::direct::serialize" + )] pub leaf_index: u64, #[serde(rename = "proof")] pub proof: Vec, /// A 32-byte hash represented as a base58 string. #[serde(rename = "root")] pub root: String, - #[serde(rename = "rootSeq")] + #[serde( + rename = "rootSeq", + deserialize_with = "crate::string_u64::direct::deserialize", + serialize_with = "crate::string_u64::direct::serialize" + )] pub root_seq: u64, /// A 32-byte hash represented as a base58 string. #[serde(rename = "tree")] diff --git a/sdk-libs/photon-api/src/models/merkle_context_v2.rs b/sdk-libs/photon-api/src/models/merkle_context_v2.rs index aed6cc38b6..67783feb80 100644 --- a/sdk-libs/photon-api/src/models/merkle_context_v2.rs +++ b/sdk-libs/photon-api/src/models/merkle_context_v2.rs @@ -23,7 +23,11 @@ pub struct MerkleContextV2 { /// A Solana public key represented as a base58 string. #[serde(rename = "tree")] pub tree: String, - #[serde(rename = "treeType")] + #[serde( + rename = "treeType", + deserialize_with = "crate::string_u64::u16_direct::deserialize", + serialize_with = "crate::string_u64::u16_direct::serialize" + )] pub tree_type: u16, } diff --git a/sdk-libs/photon-api/src/models/merkle_context_with_new_address_proof.rs b/sdk-libs/photon-api/src/models/merkle_context_with_new_address_proof.rs index 9fdc7faa2f..1f2e2ae4ae 100644 --- a/sdk-libs/photon-api/src/models/merkle_context_with_new_address_proof.rs +++ b/sdk-libs/photon-api/src/models/merkle_context_with_new_address_proof.rs @@ -18,7 +18,11 @@ pub struct MerkleContextWithNewAddressProof { /// A Solana public key represented as a base58 string. #[serde(rename = "higherRangeAddress")] pub higher_range_address: String, - #[serde(rename = "lowElementLeafIndex")] + #[serde( + rename = "lowElementLeafIndex", + deserialize_with = "crate::string_u64::direct::deserialize", + serialize_with = "crate::string_u64::direct::serialize" + )] pub low_element_leaf_index: u64, /// A Solana public key represented as a base58 string. #[serde(rename = "lowerRangeAddress")] @@ -26,14 +30,22 @@ pub struct MerkleContextWithNewAddressProof { /// A Solana public key represented as a base58 string. #[serde(rename = "merkleTree")] pub merkle_tree: String, - #[serde(rename = "nextIndex")] + #[serde( + rename = "nextIndex", + deserialize_with = "crate::string_u64::direct::deserialize", + serialize_with = "crate::string_u64::direct::serialize" + )] pub next_index: u64, #[serde(rename = "proof")] pub proof: Vec, /// A 32-byte hash represented as a base58 string. #[serde(rename = "root")] pub root: String, - #[serde(rename = "rootSeq")] + #[serde( + rename = "rootSeq", + deserialize_with = "crate::string_u64::direct::deserialize", + serialize_with = "crate::string_u64::direct::serialize" + )] pub root_seq: u64, } diff --git a/sdk-libs/photon-api/src/models/owner_balance.rs b/sdk-libs/photon-api/src/models/owner_balance.rs index 71658fedef..2320aa6f7b 100644 --- a/sdk-libs/photon-api/src/models/owner_balance.rs +++ b/sdk-libs/photon-api/src/models/owner_balance.rs @@ -12,7 +12,11 @@ use crate::models; #[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] pub struct OwnerBalance { - #[serde(rename = "balance")] + #[serde( + rename = "balance", + deserialize_with = "crate::string_u64::direct::deserialize", + serialize_with = "crate::string_u64::direct::serialize" + )] pub balance: u64, /// A Solana public key represented as a base58 string. #[serde(rename = "owner")] diff --git a/sdk-libs/photon-api/src/models/root_index.rs b/sdk-libs/photon-api/src/models/root_index.rs index ff87169a88..f94b436e9c 100644 --- a/sdk-libs/photon-api/src/models/root_index.rs +++ b/sdk-libs/photon-api/src/models/root_index.rs @@ -14,7 +14,11 @@ use crate::models; pub struct RootIndex { #[serde(rename = "proveByIndex")] pub prove_by_index: bool, - #[serde(rename = "rootIndex")] + #[serde( + rename = "rootIndex", + deserialize_with = "crate::string_u64::u16_direct::deserialize", + serialize_with = "crate::string_u64::u16_direct::serialize" + )] pub root_index: u16, } diff --git a/sdk-libs/photon-api/src/models/signature_info.rs b/sdk-libs/photon-api/src/models/signature_info.rs index caaf338c67..60fc3a1c0a 100644 --- a/sdk-libs/photon-api/src/models/signature_info.rs +++ b/sdk-libs/photon-api/src/models/signature_info.rs @@ -13,12 +13,20 @@ use crate::models; #[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] pub struct SignatureInfo { /// An Unix timestamp (seconds) - #[serde(rename = "blockTime")] + #[serde( + rename = "blockTime", + deserialize_with = "crate::string_u64::direct::deserialize", + serialize_with = "crate::string_u64::direct::serialize" + )] pub block_time: u64, /// A Solana transaction signature. #[serde(rename = "signature")] pub signature: String, - #[serde(rename = "slot")] + #[serde( + rename = "slot", + deserialize_with = "crate::string_u64::direct::deserialize", + serialize_with = "crate::string_u64::direct::serialize" + )] pub slot: u64, } diff --git a/sdk-libs/photon-api/src/models/token_account_balance.rs b/sdk-libs/photon-api/src/models/token_account_balance.rs index 17aa2e4925..c6b90722d7 100644 --- a/sdk-libs/photon-api/src/models/token_account_balance.rs +++ b/sdk-libs/photon-api/src/models/token_account_balance.rs @@ -12,7 +12,11 @@ use crate::models; #[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] pub struct TokenAccountBalance { - #[serde(rename = "amount")] + #[serde( + rename = "amount", + deserialize_with = "crate::string_u64::direct::deserialize", + serialize_with = "crate::string_u64::direct::serialize" + )] pub amount: u64, } diff --git a/sdk-libs/photon-api/src/models/token_balance.rs b/sdk-libs/photon-api/src/models/token_balance.rs index 6f02c6b1fa..cd5bb97f57 100644 --- a/sdk-libs/photon-api/src/models/token_balance.rs +++ b/sdk-libs/photon-api/src/models/token_balance.rs @@ -12,7 +12,11 @@ use crate::models; #[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] pub struct TokenBalance { - #[serde(rename = "balance")] + #[serde( + rename = "balance", + deserialize_with = "crate::string_u64::direct::deserialize", + serialize_with = "crate::string_u64::direct::serialize" + )] pub balance: u64, /// A Solana public key represented as a base58 string. #[serde(rename = "mint")] diff --git a/sdk-libs/photon-api/src/models/token_data.rs b/sdk-libs/photon-api/src/models/token_data.rs index 2cc5f1d795..e16993f4c8 100644 --- a/sdk-libs/photon-api/src/models/token_data.rs +++ b/sdk-libs/photon-api/src/models/token_data.rs @@ -12,7 +12,11 @@ use crate::models; #[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] pub struct TokenData { - #[serde(rename = "amount")] + #[serde( + rename = "amount", + deserialize_with = "crate::string_u64::direct::deserialize", + serialize_with = "crate::string_u64::direct::serialize" + )] pub amount: u64, /// A Solana public key represented as a base58 string. #[serde(rename = "delegate", skip_serializing_if = "Option::is_none")] diff --git a/sdk-libs/photon-api/src/models/tree_context_info.rs b/sdk-libs/photon-api/src/models/tree_context_info.rs index 90e6b04434..a203e3c85a 100644 --- a/sdk-libs/photon-api/src/models/tree_context_info.rs +++ b/sdk-libs/photon-api/src/models/tree_context_info.rs @@ -21,7 +21,11 @@ pub struct TreeContextInfo { /// A Solana public key represented as a base58 string. #[serde(rename = "tree")] pub tree: String, - #[serde(rename = "treeType")] + #[serde( + rename = "treeType", + deserialize_with = "crate::string_u64::u16_direct::deserialize", + serialize_with = "crate::string_u64::u16_direct::serialize" + )] pub tree_type: u16, } diff --git a/sdk-libs/photon-api/src/string_u64.rs b/sdk-libs/photon-api/src/string_u64.rs new file mode 100644 index 0000000000..db5590bef1 --- /dev/null +++ b/sdk-libs/photon-api/src/string_u64.rs @@ -0,0 +1,214 @@ +use std::fmt; + +use serde::{Deserialize, Deserializer, Serialize, Serializer}; + +/// A wrapper type that can deserialize from either a u64 or a string +#[derive(Clone, Debug, PartialEq, Default)] +pub struct StringU64(pub u64); + +impl StringU64 { + pub fn new(value: u64) -> Self { + StringU64(value) + } + + pub fn value(&self) -> u64 { + self.0 + } +} + +impl fmt::Display for StringU64 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +impl From for StringU64 { + fn from(value: u64) -> Self { + StringU64(value) + } +} + +impl From for u64 { + fn from(value: StringU64) -> Self { + value.0 + } +} + +impl<'de> Deserialize<'de> for StringU64 { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + #[derive(Deserialize)] + #[serde(untagged)] + enum StringOrU64 { + String(String), + U64(u64), + } + + match StringOrU64::deserialize(deserializer)? { + StringOrU64::String(s) => s + .parse::() + .map(StringU64) + .map_err(serde::de::Error::custom), + StringOrU64::U64(n) => Ok(StringU64(n)), + } + } +} + +impl Serialize for StringU64 { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + // Always serialize as string to match what the server expects + serializer.serialize_str(&self.0.to_string()) + } +} + +/// Helper module for optional StringU64 fields +pub mod option { + use serde::{Deserialize, Deserializer, Serialize, Serializer}; + + use super::StringU64; + + pub fn serialize(value: &Option, serializer: S) -> Result + where + S: Serializer, + { + match value { + Some(v) => v.serialize(serializer), + None => serializer.serialize_none(), + } + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'de>, + { + Option::::deserialize(deserializer) + } +} + +/// Direct deserialization helpers for u64 fields +pub mod direct { + use serde::{Deserialize, Deserializer, Serialize, Serializer}; + + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + #[derive(Deserialize)] + #[serde(untagged)] + enum StringOrU64 { + String(String), + U64(u64), + } + + match StringOrU64::deserialize(deserializer)? { + StringOrU64::String(s) => s.parse::().map_err(serde::de::Error::custom), + StringOrU64::U64(n) => Ok(n), + } + } + + pub fn serialize(value: &u64, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&value.to_string()) + } +} + +/// Direct deserialization helpers for optional u64 fields +pub mod option_direct { + use serde::{Deserialize, Deserializer, Serialize, Serializer}; + + pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'de>, + { + #[derive(Deserialize)] + #[serde(untagged)] + enum StringOrU64 { + String(String), + U64(u64), + } + + let opt = Option::::deserialize(deserializer)?; + match opt { + Some(StringOrU64::String(s)) => { + s.parse::().map(Some).map_err(serde::de::Error::custom) + } + Some(StringOrU64::U64(n)) => Ok(Some(n)), + None => Ok(None), + } + } + + pub fn serialize(value: &Option, serializer: S) -> Result + where + S: Serializer, + { + match value { + Some(v) => serializer.serialize_str(&v.to_string()), + None => serializer.serialize_none(), + } + } +} + +/// Direct deserialization helpers for u32 fields +pub mod u32_direct { + use serde::{Deserialize, Deserializer, Serialize, Serializer}; + + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + #[derive(Deserialize)] + #[serde(untagged)] + enum StringOrU32 { + String(String), + U32(u32), + } + + match StringOrU32::deserialize(deserializer)? { + StringOrU32::String(s) => s.parse::().map_err(serde::de::Error::custom), + StringOrU32::U32(n) => Ok(n), + } + } + + pub fn serialize(value: &u32, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&value.to_string()) + } +} + +/// Direct deserialization helpers for u16 fields +pub mod u16_direct { + use serde::{Deserialize, Deserializer, Serialize, Serializer}; + + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + #[derive(Deserialize)] + #[serde(untagged)] + enum StringOrU16 { + String(String), + U16(u16), + } + + match StringOrU16::deserialize(deserializer)? { + StringOrU16::String(s) => s.parse::().map_err(serde::de::Error::custom), + StringOrU16::U16(n) => Ok(n), + } + } + + pub fn serialize(value: &u16, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&value.to_string()) + } +} diff --git a/sdk-libs/photon-api/tests/string_u64_test.rs b/sdk-libs/photon-api/tests/string_u64_test.rs new file mode 100644 index 0000000000..b9bdfbafc6 --- /dev/null +++ b/sdk-libs/photon-api/tests/string_u64_test.rs @@ -0,0 +1,103 @@ +#[cfg(test)] +mod tests { + use serde_derive::{Deserialize, Serialize}; + use serde_json; + + #[derive(Debug, Serialize, Deserialize, PartialEq)] + struct TestStruct { + #[serde( + deserialize_with = "photon_api::string_u64::direct::deserialize", + serialize_with = "photon_api::string_u64::direct::serialize" + )] + amount: u64, + #[serde( + deserialize_with = "photon_api::string_u64::u32_direct::deserialize", + serialize_with = "photon_api::string_u64::u32_direct::serialize" + )] + index: u32, + #[serde( + deserialize_with = "photon_api::string_u64::u16_direct::deserialize", + serialize_with = "photon_api::string_u64::u16_direct::serialize" + )] + root_index: u16, + } + + #[test] + fn test_deserialize_from_string() { + // Test deserializing from string values (new Photon API format) + let json_str = r#"{"amount":"5106734359795461623","index":"42","root_index":"7"}"#; + let result: TestStruct = serde_json::from_str(json_str).unwrap(); + + assert_eq!(result.amount, 5106734359795461623); + assert_eq!(result.index, 42); + assert_eq!(result.root_index, 7); + } + + #[test] + fn test_deserialize_from_number() { + // Test deserializing from numeric values (backward compatibility) + let json_str = r#"{"amount":5106734359795461623,"index":42,"root_index":7}"#; + let result: TestStruct = serde_json::from_str(json_str).unwrap(); + + assert_eq!(result.amount, 5106734359795461623); + assert_eq!(result.index, 42); + assert_eq!(result.root_index, 7); + } + + #[test] + fn test_serialize_to_string() { + // Test that serialization produces strings + let test_struct = TestStruct { + amount: 5106734359795461623, + index: 42, + root_index: 7, + }; + + let json = serde_json::to_string(&test_struct).unwrap(); + assert!(json.contains(r#""amount":"5106734359795461623""#)); + assert!(json.contains(r#""index":"42""#)); + assert!(json.contains(r#""root_index":"7""#)); + } + + #[test] + fn test_roundtrip() { + let original = TestStruct { + amount: u64::MAX, + index: u32::MAX, + root_index: u16::MAX, + }; + + let json = serde_json::to_string(&original).unwrap(); + let deserialized: TestStruct = serde_json::from_str(&json).unwrap(); + + assert_eq!(original, deserialized); + } + + #[test] + fn test_optional_fields() { + #[derive(Debug, Serialize, Deserialize, PartialEq)] + struct OptionalStruct { + #[serde( + deserialize_with = "photon_api::string_u64::option_direct::deserialize", + serialize_with = "photon_api::string_u64::option_direct::serialize", + default + )] + seq: Option, + } + + // Test with Some value as string + let json_str = r#"{"seq":"123456789"}"#; + let result: OptionalStruct = serde_json::from_str(json_str).unwrap(); + assert_eq!(result.seq, Some(123456789)); + + // Test with None + let json_str = r#"{}"#; + let result: OptionalStruct = serde_json::from_str(json_str).unwrap(); + assert_eq!(result.seq, None); + + // Test with null + let json_str = r#"{"seq":null}"#; + let result: OptionalStruct = serde_json::from_str(json_str).unwrap(); + assert_eq!(result.seq, None); + } +} diff --git a/sdk-libs/program-test/Cargo.toml b/sdk-libs/program-test/Cargo.toml index 8ee936eddf..359aff612b 100644 --- a/sdk-libs/program-test/Cargo.toml +++ b/sdk-libs/program-test/Cargo.toml @@ -20,6 +20,7 @@ light-concurrent-merkle-tree = { workspace = true } light-hasher = { workspace = true } light-compressed-account = { workspace = true, features = ["anchor"] } light-batched-merkle-tree = { workspace = true, features = ["test-only"] } +light-compressible-client = { workspace = true, features = ["anchor"] } # unreleased light-client = { workspace = true, features = ["program-test"] } diff --git a/sdk-libs/program-test/src/accounts/initialize.rs b/sdk-libs/program-test/src/accounts/initialize.rs index 7781a87af9..431fcc9358 100644 --- a/sdk-libs/program-test/src/accounts/initialize.rs +++ b/sdk-libs/program-test/src/accounts/initialize.rs @@ -177,6 +177,18 @@ pub async fn initialize_accounts( *v2_state_tree_config, ) .await?; + + // Initialize the second v2 state tree + create_batched_state_merkle_tree( + &keypairs.governance_authority, + true, + context, + &keypairs.batched_state_merkle_tree_2, + &keypairs.batched_output_queue_2, + &keypairs.batched_cpi_context_2, + *v2_state_tree_config, + ) + .await?; } #[cfg(feature = "v2")] if let Some(params) = _v2_address_tree_config { @@ -211,11 +223,18 @@ pub async fn initialize_accounts( merkle_tree: keypairs.address_merkle_tree.pubkey(), queue: keypairs.address_merkle_tree_queue.pubkey(), }], - v2_state_trees: vec![StateMerkleTreeAccountsV2 { - merkle_tree: keypairs.batched_state_merkle_tree.pubkey(), - output_queue: keypairs.batched_output_queue.pubkey(), - cpi_context: keypairs.batched_cpi_context.pubkey(), - }], + v2_state_trees: vec![ + StateMerkleTreeAccountsV2 { + merkle_tree: keypairs.batched_state_merkle_tree.pubkey(), + output_queue: keypairs.batched_output_queue.pubkey(), + cpi_context: keypairs.batched_cpi_context.pubkey(), + }, + StateMerkleTreeAccountsV2 { + merkle_tree: keypairs.batched_state_merkle_tree_2.pubkey(), + output_queue: keypairs.batched_output_queue_2.pubkey(), + cpi_context: keypairs.batched_cpi_context_2.pubkey(), + }, + ], v2_address_trees: vec![keypairs.batch_address_merkle_tree.pubkey()], }) } diff --git a/sdk-libs/program-test/src/accounts/test_accounts.rs b/sdk-libs/program-test/src/accounts/test_accounts.rs index ea4284c30d..f6f1516647 100644 --- a/sdk-libs/program-test/src/accounts/test_accounts.rs +++ b/sdk-libs/program-test/src/accounts/test_accounts.rs @@ -80,11 +80,18 @@ impl TestAccounts { }], v2_address_trees: vec![pubkey!("EzKE84aVTkCUhDHLELqyJaq1Y7UVVmqxXqZjVHwHY3rK")], - v2_state_trees: vec![StateMerkleTreeAccountsV2 { - merkle_tree: pubkey!("HLKs5NJ8FXkJg8BrzJt56adFYYuwg5etzDtBbQYTsixu"), - output_queue: pubkey!("6L7SzhYB3anwEQ9cphpJ1U7Scwj57bx2xueReg7R9cKU"), - cpi_context: pubkey!("7Hp52chxaew8bW1ApR4fck2bh6Y8qA1pu3qwH6N9zaLj"), - }], + v2_state_trees: vec![ + StateMerkleTreeAccountsV2 { + merkle_tree: pubkey!("HLKs5NJ8FXkJg8BrzJt56adFYYuwg5etzDtBbQYTsixu"), + output_queue: pubkey!("6L7SzhYB3anwEQ9cphpJ1U7Scwj57bx2xueReg7R9cKU"), + cpi_context: pubkey!("7Hp52chxaew8bW1ApR4fck2bh6Y8qA1pu3qwH6N9zaLj"), + }, + StateMerkleTreeAccountsV2 { + merkle_tree: pubkey!("2Yb3fGo2E9aWLjY8KuESaqurYpGGhEeJr7eynKrSgXwS"), + output_queue: pubkey!("12wJT3xYd46rtjeqDU6CrtT8unqLjPiheggzqhN9YsyB"), + cpi_context: pubkey!("HwtjxDvFEXiWnzeMeWkMBzpQN45A95rTJNZmz1Z3pe8R"), // TODO: replace. + }, + ], } } @@ -127,17 +134,30 @@ impl TestAccounts { merkle_tree: pubkey!("amt1Ayt45jfbdw5YSo7iz6WZxUmnZsQTYXy82hVwyC2"), queue: pubkey!("aq1S9z4reTSQAdgWHGD2zDaS39sjGrAxbR31vxJ2F4F"), }], - v2_state_trees: vec![StateMerkleTreeAccountsV2 { - merkle_tree: Keypair::from_bytes(&BATCHED_STATE_MERKLE_TREE_TEST_KEYPAIR) - .unwrap() - .pubkey(), - output_queue: Keypair::from_bytes(&BATCHED_OUTPUT_QUEUE_TEST_KEYPAIR) - .unwrap() - .pubkey(), - cpi_context: Keypair::from_bytes(&BATCHED_CPI_CONTEXT_TEST_KEYPAIR) - .unwrap() - .pubkey(), - }], + v2_state_trees: vec![ + StateMerkleTreeAccountsV2 { + merkle_tree: Keypair::from_bytes(&BATCHED_STATE_MERKLE_TREE_TEST_KEYPAIR) + .unwrap() + .pubkey(), + output_queue: Keypair::from_bytes(&BATCHED_OUTPUT_QUEUE_TEST_KEYPAIR) + .unwrap() + .pubkey(), + cpi_context: Keypair::from_bytes(&BATCHED_CPI_CONTEXT_TEST_KEYPAIR) + .unwrap() + .pubkey(), + }, + StateMerkleTreeAccountsV2 { + merkle_tree: Keypair::from_bytes(&BATCHED_STATE_MERKLE_TREE_TEST_KEYPAIR_2) + .unwrap() + .pubkey(), + output_queue: Keypair::from_bytes(&BATCHED_OUTPUT_QUEUE_TEST_KEYPAIR_2) + .unwrap() + .pubkey(), + cpi_context: Keypair::from_bytes(&BATCHED_CPI_CONTEXT_TEST_KEYPAIR_2) + .unwrap() + .pubkey(), + }, + ], v2_address_trees: vec![ Keypair::from_bytes(&BATCHED_ADDRESS_MERKLE_TREE_TEST_KEYPAIR) .unwrap() diff --git a/sdk-libs/program-test/src/accounts/test_keypairs.rs b/sdk-libs/program-test/src/accounts/test_keypairs.rs index 0a0a59aeec..2cae5319fd 100644 --- a/sdk-libs/program-test/src/accounts/test_keypairs.rs +++ b/sdk-libs/program-test/src/accounts/test_keypairs.rs @@ -14,6 +14,9 @@ pub struct TestKeypairs { pub batched_state_merkle_tree: Keypair, pub batched_output_queue: Keypair, pub batched_cpi_context: Keypair, + pub batched_state_merkle_tree_2: Keypair, + pub batched_output_queue_2: Keypair, + pub batched_cpi_context_2: Keypair, pub batch_address_merkle_tree: Keypair, pub state_merkle_tree_2: Keypair, pub nullifier_queue_2: Keypair, @@ -38,6 +41,14 @@ impl TestKeypairs { .unwrap(), batched_output_queue: Keypair::from_bytes(&BATCHED_OUTPUT_QUEUE_TEST_KEYPAIR).unwrap(), batched_cpi_context: Keypair::from_bytes(&BATCHED_CPI_CONTEXT_TEST_KEYPAIR).unwrap(), + batched_state_merkle_tree_2: Keypair::from_bytes( + &BATCHED_STATE_MERKLE_TREE_TEST_KEYPAIR_2, + ) + .unwrap(), + batched_output_queue_2: Keypair::from_bytes(&BATCHED_OUTPUT_QUEUE_TEST_KEYPAIR_2) + .unwrap(), + batched_cpi_context_2: Keypair::from_bytes(&BATCHED_CPI_CONTEXT_TEST_KEYPAIR_2) + .unwrap(), batch_address_merkle_tree: Keypair::from_bytes( &BATCHED_ADDRESS_MERKLE_TREE_TEST_KEYPAIR, ) @@ -152,3 +163,27 @@ pub const BATCHED_ADDRESS_MERKLE_TREE_TEST_KEYPAIR: [u8; 64] = [ 28, 24, 35, 87, 72, 11, 158, 224, 210, 70, 207, 214, 165, 6, 152, 46, 60, 129, 118, 32, 27, 128, 68, 73, 71, 250, 6, 83, 176, 199, 153, 140, 237, 11, 55, 237, 3, 179, 242, 138, 37, 12, ]; + +// 2Yb3fGo2E9aWLjY8KuESaqurYpGGhEeJr7eynKrSgXwS +pub const BATCHED_STATE_MERKLE_TREE_TEST_KEYPAIR_2: [u8; 64] = [ + 90, 177, 184, 7, 31, 2, 75, 156, 206, 95, 137, 254, 248, 143, 80, 51, 244, 47, 172, 66, 49, 28, + 209, 135, 246, 185, 1, 215, 203, 206, 45, 205, 22, 243, 48, 18, 157, 183, 128, 51, 122, 187, + 220, 157, 58, 187, 210, 100, 26, 202, 115, 200, 112, 226, 176, 142, 204, 246, 80, 46, 44, 164, + 79, 213, +]; + +// 12wJT3xYd46rtjeqDU6CrtT8unqLjPiheggzqhN9YsyB +pub const BATCHED_OUTPUT_QUEUE_TEST_KEYPAIR_2: [u8; 64] = [ + 22, 251, 188, 220, 48, 112, 152, 88, 12, 111, 253, 20, 152, 160, 181, 28, 52, 135, 176, 56, 37, + 253, 214, 155, 207, 174, 40, 34, 120, 168, 220, 48, 0, 126, 250, 157, 250, 233, 33, 126, 217, + 161, 223, 128, 212, 172, 27, 168, 153, 70, 78, 223, 110, 234, 56, 119, 236, 165, 128, 65, 219, + 103, 124, 58, +]; + +// HwtjxDvFEXiWnzeMeWkMBzpQN45A95rTJNZmz1Z3pe8R +pub const BATCHED_CPI_CONTEXT_TEST_KEYPAIR_2: [u8; 64] = [ + 192, 190, 219, 50, 49, 251, 81, 115, 108, 69, 25, 24, 64, 192, 70, 119, 227, 163, 244, 162, + 151, 22, 202, 75, 143, 238, 60, 231, 45, 143, 70, 166, 251, 202, 219, 148, 255, 199, 4, 181, 2, + 206, 241, 189, 231, 73, 214, 93, 163, 87, 254, 68, 179, 132, 226, 66, 188, 189, 86, 84, 143, + 190, 33, 218, +]; diff --git a/sdk-libs/program-test/src/indexer/test_indexer.rs b/sdk-libs/program-test/src/indexer/test_indexer.rs index 910e39a1b1..f36ce8665a 100644 --- a/sdk-libs/program-test/src/indexer/test_indexer.rs +++ b/sdk-libs/program-test/src/indexer/test_indexer.rs @@ -86,8 +86,9 @@ use crate::accounts::{ use crate::{ accounts::{ address_tree::create_address_merkle_tree_and_queue_account, - state_tree::create_state_merkle_tree_and_queue_account, test_accounts::TestAccounts, - test_keypairs::BATCHED_OUTPUT_QUEUE_TEST_KEYPAIR, + state_tree::create_state_merkle_tree_and_queue_account, + test_accounts::TestAccounts, + test_keypairs::{BATCHED_OUTPUT_QUEUE_TEST_KEYPAIR, BATCHED_OUTPUT_QUEUE_TEST_KEYPAIR_2}, }, indexer::TestIndexerExtensions, }; @@ -193,12 +194,10 @@ impl Indexer for TestIndexer { let account = self .compressed_accounts .iter() - .find(|acc| acc.compressed_account.address == Some(address)); + .find(|acc| acc.compressed_account.address == Some(address)) + .ok_or(IndexerError::AccountNotFound)?; - let account_data = account - .ok_or(IndexerError::AccountNotFound)? - .clone() - .try_into()?; + let account_data: CompressedAccount = account.clone().try_into()?; Ok(Response { context: Context { @@ -453,9 +452,10 @@ impl Indexer for TestIndexer { let account = self.get_compressed_account_by_hash(*hash, None).await?; state_merkle_tree_pubkeys.push(account.value.tree_info.tree); } - let mut proof_inputs = vec![]; + let mut proof_inputs = vec![]; let mut indices_to_remove = Vec::new(); + // for all accounts in batched trees, check whether values are in tree or queue let compressed_accounts = if !hashes.is_empty() && !state_merkle_tree_pubkeys.is_empty() { @@ -474,6 +474,7 @@ impl Indexer for TestIndexer { .output_queue_elements .iter() .find(|(hash, _)| hash == compressed_account); + if let Some((_, index)) = queue_element { if accounts.output_queue_batch_size.is_some() && accounts.leaf_index_in_queue_range(*index as usize)? @@ -551,7 +552,7 @@ impl Indexer for TestIndexer { #[cfg(debug_assertions)] { if std::env::var("RUST_BACKTRACE").is_ok() { - println!("get_validit_proof: rpc_result {:?}", rpc_result); + println!("get_validity_proof: rpc_result {:?}", rpc_result); } } @@ -1287,9 +1288,12 @@ impl TestIndexer { for state_merkle_tree_account in state_merkle_tree_accounts.iter() { let test_batched_output_queue = Keypair::from_bytes(&BATCHED_OUTPUT_QUEUE_TEST_KEYPAIR).unwrap(); + let test_batched_output_queue_2 = + Keypair::from_bytes(&BATCHED_OUTPUT_QUEUE_TEST_KEYPAIR_2).unwrap(); let (tree_type, merkle_tree, output_queue_batch_size) = if state_merkle_tree_account .nullifier_queue == test_batched_output_queue.pubkey() + || state_merkle_tree_account.nullifier_queue == test_batched_output_queue_2.pubkey() { let merkle_tree = Box::new(MerkleTree::::new_with_history( DEFAULT_BATCH_STATE_TREE_HEIGHT as usize, @@ -2260,28 +2264,47 @@ impl TestIndexer { .body(json_payload.clone()) .send() .await; - if let Ok(response_result) = response_result { - if response_result.status().is_success() { - let body = response_result.text().await.unwrap(); - let proof_json = deserialize_gnark_proof_json(&body).unwrap(); - let (proof_a, proof_b, proof_c) = proof_from_json_struct(proof_json); - let (proof_a, proof_b, proof_c) = - compress_proof(&proof_a, &proof_b, &proof_c); - return Ok(ValidityProofWithContext { - accounts: account_proof_inputs, - addresses: address_proof_inputs, - proof: CompressedProof { - a: proof_a, - b: proof_b, - c: proof_c, - } - .into(), - }); + + match response_result { + Ok(resp) => { + let status = resp.status(); + if status.is_success() { + let body = resp.text().await.unwrap(); + let proof_json = deserialize_gnark_proof_json(&body).unwrap(); + let (proof_a, proof_b, proof_c) = proof_from_json_struct(proof_json); + let (proof_a, proof_b, proof_c) = + compress_proof(&proof_a, &proof_b, &proof_c); + return Ok(ValidityProofWithContext { + accounts: account_proof_inputs, + addresses: address_proof_inputs, + proof: CompressedProof { + a: proof_a, + b: proof_b, + c: proof_c, + } + .into(), + }); + } + + // Non-success HTTP response. Read body for diagnostics and decide whether to retry. + let body = resp.text().await.unwrap_or_default(); + // Fail fast on 4xx (client errors are usually non-retryable: bad params or missing circuit) + if status.is_client_error() { + return Err(IndexerError::CustomError(format!( + "Prover client error {}: {}", + status, body + ))); + } + // Otherwise, treat as transient and backoff + println!("Prover non-success {}: {}", status, body); + retries -= 1; + tokio::time::sleep(Duration::from_secs(5)).await; + } + Err(err) => { + println!("Request error: {:?}", err); + retries -= 1; + tokio::time::sleep(Duration::from_secs(5)).await; } - } else { - println!("Error: {:#?}", response_result); - tokio::time::sleep(Duration::from_secs(5)).await; - retries -= 1; } } Err(IndexerError::CustomError( diff --git a/sdk-libs/program-test/src/lib.rs b/sdk-libs/program-test/src/lib.rs index c36f8a52d3..ef5cc8ddcc 100644 --- a/sdk-libs/program-test/src/lib.rs +++ b/sdk-libs/program-test/src/lib.rs @@ -122,4 +122,7 @@ pub use light_client::{ indexer::{AddressWithTree, Indexer}, rpc::{Rpc, RpcError}, }; -pub use program_test::{config::ProgramTestConfig, LightProgramTest}; +pub use program_test::{ + config::ProgramTestConfig, initialize_compression_config, setup_mock_program_data, + update_compression_config, LightProgramTest, +}; diff --git a/sdk-libs/program-test/src/program_test/compressible_setup.rs b/sdk-libs/program-test/src/program_test/compressible_setup.rs new file mode 100644 index 0000000000..37b1493dab --- /dev/null +++ b/sdk-libs/program-test/src/program_test/compressible_setup.rs @@ -0,0 +1,159 @@ +//! Test helpers for compressible account operations +//! +//! This module provides common functionality for testing compressible accounts, +//! including mock program data setup and configuration management. + +use light_client::rpc::{Rpc, RpcError}; +use light_compressible_client::CompressibleInstruction; +use solana_sdk::{ + bpf_loader_upgradeable, + pubkey::Pubkey, + signature::{Keypair, Signer}, +}; + +use crate::program_test::TestRpc; + +/// Create mock program data account for testing +/// +/// This creates a minimal program data account structure that mimics +/// what the BPF loader would create for deployed programs. +pub fn create_mock_program_data(authority: Pubkey) -> Vec { + let mut data = vec![0u8; 1024]; + data[0..4].copy_from_slice(&3u32.to_le_bytes()); // Program data discriminator + data[4..12].copy_from_slice(&0u64.to_le_bytes()); // Slot + data[12] = 1; // Option Some(authority) + data[13..45].copy_from_slice(authority.as_ref()); // Authority pubkey + data +} + +/// Setup mock program data account for testing +/// +/// For testing without ledger, LiteSVM does not create program data accounts, +/// so we need to create them manually. This is required for programs that +/// check their upgrade authority. +/// +/// # Arguments +/// * `rpc` - The test RPC client +/// * `payer` - The payer keypair (used as authority) +/// * `program_id` - The program ID to create data account for +/// +/// # Returns +/// The pubkey of the created program data account +pub fn setup_mock_program_data( + rpc: &mut T, + payer: &Keypair, + program_id: &Pubkey, +) -> Pubkey { + let (program_data_pda, _) = + Pubkey::find_program_address(&[program_id.as_ref()], &bpf_loader_upgradeable::ID); + let mock_data = create_mock_program_data(payer.pubkey()); + let mock_account = solana_sdk::account::Account { + lamports: 1_000_000, + data: mock_data, + owner: bpf_loader_upgradeable::ID, + executable: false, + rent_epoch: 0, + }; + rpc.set_account(program_data_pda, mock_account); + program_data_pda +} + +/// Initialize compression config for a program +/// +/// This is a high-level helper that handles the complete flow of initializing +/// a compression configuration for a program, including proper signer management. +/// +/// # Arguments +/// * `rpc` - The test RPC client +/// * `payer` - The transaction fee payer +/// * `program_id` - The program to initialize config for +/// * `authority` - The config authority (can be same as payer) +/// * `compression_delay` - Number of slots to wait before compression +/// * `rent_recipient` - Where to send rent from compressed accounts +/// * `address_space` - List of address trees for this program +/// +/// # Returns +/// Transaction signature on success +#[allow(clippy::too_many_arguments)] +pub async fn initialize_compression_config( + rpc: &mut T, + payer: &Keypair, + program_id: &Pubkey, + authority: &Keypair, + compression_delay: u32, + rent_recipient: Pubkey, + address_space: Vec, + discriminator: &[u8], + config_bump: Option, +) -> Result { + if address_space.is_empty() { + return Err(RpcError::CustomError( + "At least one address space must be provided".to_string(), + )); + } + + // Use the mid-level instruction builder + let instruction = CompressibleInstruction::initialize_compression_config( + program_id, + discriminator, + &payer.pubkey(), + &authority.pubkey(), + compression_delay, + rent_recipient, + address_space, + config_bump, + ); + + let signers = if payer.pubkey() == authority.pubkey() { + vec![payer] + } else { + vec![payer, authority] + }; + + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &signers) + .await +} + +/// Update compression config for a program +/// +/// This is a high-level helper for updating an existing compression configuration. +/// All parameters except the required ones are optional - pass None to keep existing values. +/// +/// # Arguments +/// * `rpc` - The test RPC client +/// * `payer` - The transaction fee payer +/// * `program_id` - The program to update config for +/// * `authority` - The current config authority +/// * `new_compression_delay` - New compression delay (optional) +/// * `new_rent_recipient` - New rent recipient (optional) +/// * `new_address_space` - New address space list (optional) +/// * `new_update_authority` - New authority (optional) +/// +/// # Returns +/// Transaction signature on success +#[allow(clippy::too_many_arguments)] +pub async fn update_compression_config( + rpc: &mut T, + payer: &Keypair, + program_id: &Pubkey, + authority: &Keypair, + new_compression_delay: Option, + new_rent_recipient: Option, + new_address_space: Option>, + new_update_authority: Option, + discriminator: &[u8], +) -> Result { + // Use the mid-level instruction builder + let instruction = CompressibleInstruction::update_compression_config( + program_id, + discriminator, + &authority.pubkey(), + new_compression_delay, + new_rent_recipient, + new_address_space, + new_update_authority, + ); + + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer, authority]) + .await +} diff --git a/sdk-libs/program-test/src/program_test/mod.rs b/sdk-libs/program-test/src/program_test/mod.rs index c9eee711e3..fe14c39909 100644 --- a/sdk-libs/program-test/src/program_test/mod.rs +++ b/sdk-libs/program-test/src/program_test/mod.rs @@ -1,3 +1,4 @@ +pub mod compressible_setup; pub mod config; #[cfg(feature = "devenv")] pub mod extensions; @@ -7,4 +8,5 @@ pub mod test_rpc; pub use light_program_test::LightProgramTest; pub mod indexer; +pub use compressible_setup::*; pub use test_rpc::TestRpc; diff --git a/sdk-libs/program-test/src/utils/mod.rs b/sdk-libs/program-test/src/utils/mod.rs index e1b9d7be63..768d68ac5c 100644 --- a/sdk-libs/program-test/src/utils/mod.rs +++ b/sdk-libs/program-test/src/utils/mod.rs @@ -3,4 +3,5 @@ pub mod create_account; pub mod find_light_bin; pub mod register_test_forester; pub mod setup_light_programs; +pub mod simulation; pub mod tree_accounts; 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..78987c6c18 --- /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( + &[instruction.clone()], + 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-pinocchio/src/cpi/accounts_small.rs b/sdk-libs/sdk-pinocchio/src/cpi/accounts_small.rs index bd3d3821b1..e2d3324ab9 100644 --- a/sdk-libs/sdk-pinocchio/src/cpi/accounts_small.rs +++ b/sdk-libs/sdk-pinocchio/src/cpi/accounts_small.rs @@ -1,3 +1,5 @@ +#![allow(clippy::all)] // TODO: Remove. + use light_sdk_types::{ CpiAccountsSmall as GenericCpiAccountsSmall, ACCOUNT_COMPRESSION_AUTHORITY_PDA, ACCOUNT_COMPRESSION_PROGRAM_ID, REGISTERED_PROGRAM_PDA, SMALL_SYSTEM_ACCOUNTS_LEN, diff --git a/sdk-libs/sdk-types/src/constants.rs b/sdk-libs/sdk-types/src/constants.rs index d46c44ff67..497913f254 100644 --- a/sdk-libs/sdk-types/src/constants.rs +++ b/sdk-libs/sdk-types/src/constants.rs @@ -14,6 +14,12 @@ pub const ACCOUNT_COMPRESSION_AUTHORITY_PDA: [u8; 32] = /// ID of the light-compressed-token program. pub const C_TOKEN_PROGRAM_ID: [u8; 32] = pubkey_array!("cTokenmWW8bLPjZEBAUgYy3zKxQZW6VKi7bqNFEVv3m"); +pub const COMPRESSED_TOKEN_PROGRAM_ID: [u8; 32] = + pubkey_array!("cTokenmWW8bLPjZEBAUgYy3zKxQZW6VKi7bqNFEVv3m"); + +/// ID of the compressed token program CPI authority PDA. +pub const COMPRESSED_TOKEN_PROGRAM_CPI_AUTHORITY: [u8; 32] = + pubkey_array!("GXtd2izAiMJPwMEjfgTRH3d7k9mjn4Jq3JrWFv9gySYy"); /// Seed of the CPI authority. pub const CPI_AUTHORITY_PDA_SEED: &[u8] = b"cpi_authority"; @@ -38,3 +44,8 @@ pub const CPI_CONTEXT_ACCOUNT_DISCRIMINATOR_V1: [u8; 8] = [22, 20, 149, 218, 74, pub const CPI_CONTEXT_ACCOUNT_DISCRIMINATOR: [u8; 8] = [34, 184, 183, 14, 100, 80, 183, 124]; pub const SOL_POOL_PDA: [u8; 32] = pubkey_array!("CHK57ywWSDncAoRu1F8QgwYJeXuAJyyBYT4LixLXvMZ1"); + +// For input accounts with empty data. +pub const DEFAULT_DATA_HASH: [u8; 32] = [ + 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, +]; diff --git a/sdk-libs/sdk-types/src/cpi_accounts_small.rs b/sdk-libs/sdk-types/src/cpi_accounts_small.rs index 6f090ea4d8..07c245777e 100644 --- a/sdk-libs/sdk-types/src/cpi_accounts_small.rs +++ b/sdk-libs/sdk-types/src/cpi_accounts_small.rs @@ -7,15 +7,15 @@ use crate::{ #[repr(usize)] pub enum CompressionCpiAccountIndexSmall { - LightSystemProgram, - Authority, // index 0 - Cpi authority of the custom program, used to invoke the light system program. - RegisteredProgramPda, // index 1 - registered_program_pda - AccountCompressionAuthority, // index 2 - account_compression_authority - AccountCompressionProgram, // index 3 - account_compression_program - SystemProgram, // index 4 - system_program - SolPoolPda, // index 5 - Optional - DecompressionRecipient, // index 6 - Optional - CpiContext, // index 7 - Optional + LightSystemProgram, // index 0 - hardcoded in cpi hence no getter. + Authority, // index 1 - Cpi authority of the custom program, used to invoke the light system program. + RegisteredProgramPda, // index 2 - registered_program_pda + AccountCompressionAuthority, // index 3 - account_compression_authority + AccountCompressionProgram, // index 4 - account_compression_program + SystemProgram, // index 5 - system_program + SolPoolPda, // index 6 - Optional + DecompressionRecipient, // index 7 - Optional + CpiContext, // index 8 - Optional } pub const PROGRAM_ACCOUNTS_LEN: usize = 0; // No program accounts in CPI @@ -37,7 +37,8 @@ impl<'a, T: AccountInfoTrait + Clone> CpiAccountsSmall<'a, T> { config: CpiAccountsConfig::new(cpi_signer), } } - + #[inline(never)] + #[cold] pub fn new_with_config(fee_payer: &'a T, accounts: &'a [T], config: CpiAccountsConfig) -> Self { Self { fee_payer, @@ -160,6 +161,17 @@ impl<'a, T: AccountInfoTrait + Clone> CpiAccountsSmall<'a, T> { )) } + /// Returns accounts after the system accounts; instruction-specific + /// remaining_accounts start at this offset. + pub fn post_system_accounts(&self) -> Result<&'a [T]> { + let system_offset = self.system_accounts_end_offset(); + self.accounts + .get(system_offset..) + .ok_or(LightSdkTypesError::CpiAccountsIndexOutOfBounds( + system_offset, + )) + } + pub fn get_tree_account_info(&self, tree_index: usize) -> Result<&'a T> { let tree_accounts = self.tree_accounts()?; tree_accounts @@ -169,6 +181,16 @@ impl<'a, T: AccountInfoTrait + Clone> CpiAccountsSmall<'a, T> { )) } + // TODO: unify with get_tree_account_info + pub fn get_tree_address(&self, tree_index: u8) -> Result<&'a T> { + let tree_accounts = self.tree_accounts()?; + tree_accounts.get(tree_index as usize).ok_or( + LightSdkTypesError::CpiAccountsIndexOutOfBounds( + self.system_accounts_end_offset() + tree_index as usize, + ), + ) + } + /// Create a vector of account info references pub fn to_account_infos(&self) -> Vec { let mut account_infos = Vec::with_capacity(1 + self.accounts.len()); diff --git a/sdk-libs/sdk-types/src/instruction/tree_info.rs b/sdk-libs/sdk-types/src/instruction/tree_info.rs index 8f0f481507..39dab2190c 100644 --- a/sdk-libs/sdk-types/src/instruction/tree_info.rs +++ b/sdk-libs/sdk-types/src/instruction/tree_info.rs @@ -1,6 +1,10 @@ use light_account_checks::AccountInfoTrait; +#[cfg(feature = "v2")] +use light_compressed_account::instruction_data::data::NewAddressParamsAssignedPacked; use light_compressed_account::instruction_data::data::NewAddressParamsPacked; +#[cfg(feature = "v2")] +use crate::CpiAccountsSmall; use crate::{AnchorDeserialize, AnchorSerialize, CpiAccounts}; #[derive(Debug, Clone, Copy, AnchorDeserialize, AnchorSerialize, PartialEq, Default)] @@ -29,6 +33,23 @@ impl PackedAddressTreeInfo { } } + #[cfg(feature = "v2")] + pub fn into_new_address_params_assigned_packed( + self, + seed: [u8; 32], + assigned_to_account: bool, + assigned_account_index: Option, + ) -> NewAddressParamsAssignedPacked { + NewAddressParamsAssignedPacked { + address_merkle_tree_account_index: self.address_merkle_tree_pubkey_index, + address_queue_account_index: self.address_queue_pubkey_index, + address_merkle_tree_root_index: self.root_index, + seed, + assigned_to_account, + assigned_account_index: assigned_account_index.unwrap_or_default(), + } + } + pub fn get_tree_pubkey( &self, cpi_accounts: &CpiAccounts<'_, T>, @@ -37,4 +58,14 @@ impl PackedAddressTreeInfo { cpi_accounts.get_tree_account_info(self.address_merkle_tree_pubkey_index as usize)?; Ok(account.pubkey()) } + + #[cfg(feature = "v2")] + pub fn get_tree_pubkey_small( + &self, + cpi_accounts: &CpiAccountsSmall<'_, T>, + ) -> Result { + let account = + cpi_accounts.get_tree_account_info(self.address_merkle_tree_pubkey_index as usize)?; + Ok(account.pubkey()) + } } diff --git a/sdk-libs/sdk/Cargo.toml b/sdk-libs/sdk/Cargo.toml index efc616be08..faf603856f 100644 --- a/sdk-libs/sdk/Cargo.toml +++ b/sdk-libs/sdk/Cargo.toml @@ -18,6 +18,7 @@ anchor = [ "light-compressed-account/anchor", "light-sdk-types/anchor", ] +anchor-discriminator-compat = ["light-sdk-macros/anchor-discriminator-compat"] v2 = ["light-sdk-types/v2"] @@ -28,6 +29,13 @@ solana-msg = { workspace = true } solana-cpi = { workspace = true } solana-program-error = { workspace = true } solana-instruction = { workspace = true } +solana-system-interface = { workspace = true } +solana-clock = { workspace = true } +solana-sysvar = { workspace = true } +solana-rent = { workspace = true } +# TODO: find a way to not depend on solana-program +solana-program = { workspace = true } +bincode = { workspace = true } anchor-lang = { workspace = true, optional = true } num-bigint = { workspace = true } @@ -35,11 +43,12 @@ num-bigint = { workspace = true } # only needed with solana-program borsh = { workspace = true, optional = true } thiserror = { workspace = true } +arrayvec = { workspace = true } light-sdk-macros = { workspace = true } light-sdk-types = { workspace = true } light-macros = { workspace = true } -light-compressed-account = { workspace = true } +light-compressed-account = { workspace = true, features = ["solana"] } light-hasher = { workspace = true } light-account-checks = { workspace = true, features = ["solana"] } light-zero-copy = { workspace = true } diff --git a/sdk-libs/sdk/src/account.rs b/sdk-libs/sdk/src/account.rs index 8206696040..9cd82cf6e1 100644 --- a/sdk-libs/sdk/src/account.rs +++ b/sdk-libs/sdk/src/account.rs @@ -65,33 +65,54 @@ //! ``` // TODO: add example for manual hashing -use std::ops::{Deref, DerefMut}; +use std::{ + marker::PhantomData, + ops::{Deref, DerefMut}, +}; use light_compressed_account::{ compressed_account::PackedMerkleContext, instruction_data::with_account_info::{CompressedAccountInfo, InAccountInfo, OutAccountInfo}, }; -use light_sdk_types::instruction::account_meta::CompressedAccountMetaTrait; +use light_sdk_types::{instruction::account_meta::CompressedAccountMetaTrait, DEFAULT_DATA_HASH}; use solana_pubkey::Pubkey; use crate::{ error::LightSdkError, - light_hasher::{DataHasher, Poseidon}, + light_hasher::{DataHasher, Hasher, Poseidon, Sha256}, AnchorDeserialize, AnchorSerialize, LightDiscriminator, }; +pub trait Size { + fn size(&self) -> usize; +} + +pub type LightAccount<'a, A> = LightAccountInner<'a, Poseidon, A>; + +pub mod sha { + use super::*; + /// LightAccount variant that uses SHA256 hashing + pub type LightAccount<'a, A> = super::LightAccountInner<'a, Sha256, A>; +} + #[derive(Debug, PartialEq)] -pub struct LightAccount< +pub struct LightAccountInner< 'a, + H: Hasher, A: AnchorSerialize + AnchorDeserialize + LightDiscriminator + DataHasher + Default, > { owner: &'a Pubkey, pub account: A, account_info: CompressedAccountInfo, + should_remove_data: bool, + _hasher: PhantomData, } -impl<'a, A: AnchorSerialize + AnchorDeserialize + LightDiscriminator + DataHasher + Default> - LightAccount<'a, A> +impl< + 'a, + H: Hasher, + A: AnchorSerialize + AnchorDeserialize + LightDiscriminator + DataHasher + Default, + > LightAccountInner<'a, H, A> { pub fn new_init( owner: &'a Pubkey, @@ -111,6 +132,8 @@ impl<'a, A: AnchorSerialize + AnchorDeserialize + LightDiscriminator + DataHashe input: None, output: Some(output_account_info), }, + should_remove_data: false, + _hasher: PhantomData, } } @@ -120,7 +143,7 @@ impl<'a, A: AnchorSerialize + AnchorDeserialize + LightDiscriminator + DataHashe input_account: A, ) -> Result { let input_account_info = { - let input_data_hash = input_account.hash::()?; + let input_data_hash = input_account.hash::()?; let tree_info = input_account_meta.get_tree_info(); InAccountInfo { data_hash: input_data_hash, @@ -155,6 +178,57 @@ impl<'a, A: AnchorSerialize + AnchorDeserialize + LightDiscriminator + DataHashe input: Some(input_account_info), output: Some(output_account_info), }, + should_remove_data: false, + _hasher: PhantomData, + }) + } + + /// Create a new LightAccount for compression from an empty compressed + /// account. This is used when compressing a PDA - we know the compressed + /// account exists but is empty (data: [], data_hash: [0, 1, 1, 1, 1, 1, 1, + /// 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + /// 1]). + pub fn new_mut_without_data( + owner: &'a Pubkey, + input_account_meta: &impl CompressedAccountMetaTrait, + ) -> Result { + let input_account_info = { + let tree_info = input_account_meta.get_tree_info(); + InAccountInfo { + data_hash: DEFAULT_DATA_HASH, // TODO: review security. + lamports: input_account_meta.get_lamports().unwrap_or_default(), + merkle_context: PackedMerkleContext { + merkle_tree_pubkey_index: tree_info.merkle_tree_pubkey_index, + queue_pubkey_index: tree_info.queue_pubkey_index, + leaf_index: tree_info.leaf_index, + prove_by_index: tree_info.prove_by_index, + }, + root_index: input_account_meta.get_root_index().unwrap_or_default(), + discriminator: A::LIGHT_DISCRIMINATOR, + } + }; + let output_account_info = { + let output_merkle_tree_index = input_account_meta + .get_output_state_tree_index() + .ok_or(LightSdkError::OutputStateTreeIndexIsNone)?; + OutAccountInfo { + lamports: input_account_meta.get_lamports().unwrap_or_default(), + output_merkle_tree_index, + discriminator: A::LIGHT_DISCRIMINATOR, + ..Default::default() + } + }; + + Ok(Self { + owner, + account: A::default(), // Start with default, will be filled with PDA data + account_info: CompressedAccountInfo { + address: input_account_meta.get_address(), + input: Some(input_account_info), + output: Some(output_account_info), + }, + should_remove_data: false, + _hasher: PhantomData, }) } @@ -164,7 +238,7 @@ impl<'a, A: AnchorSerialize + AnchorDeserialize + LightDiscriminator + DataHashe input_account: A, ) -> Result { let input_account_info = { - let input_data_hash = input_account.hash::()?; + let input_data_hash = input_account.hash::()?; let tree_info = input_account_meta.get_tree_info(); InAccountInfo { data_hash: input_data_hash, @@ -179,6 +253,7 @@ impl<'a, A: AnchorSerialize + AnchorDeserialize + LightDiscriminator + DataHashe discriminator: A::LIGHT_DISCRIMINATOR, } }; + Ok(Self { owner, account: input_account, @@ -187,6 +262,8 @@ impl<'a, A: AnchorSerialize + AnchorDeserialize + LightDiscriminator + DataHashe input: Some(input_account_info), output: None, }, + should_remove_data: false, + _hasher: PhantomData, }) } @@ -230,6 +307,20 @@ impl<'a, A: AnchorSerialize + AnchorDeserialize + LightDiscriminator + DataHashe &self.account_info.output } + /// Get the byte size of the account type. + pub fn size(&self) -> Result + where + A: Size, + { + Ok(self.account.size()) + } + + /// Remove the data from this account by setting it to default. + /// This is used when decompressing to ensure the compressed account is properly zeroed. + pub fn remove_data(&mut self) { + self.should_remove_data = true; + } + /// 1. Serializes the account data and sets the output data hash. /// 2. Returns CompressedAccountInfo. /// @@ -237,18 +328,28 @@ impl<'a, A: AnchorSerialize + AnchorDeserialize + LightDiscriminator + DataHashe /// that should only be called once per instruction. pub fn to_account_info(mut self) -> Result { if let Some(output) = self.account_info.output.as_mut() { - output.data_hash = self.account.hash::()?; - output.data = self - .account - .try_to_vec() - .map_err(|_| LightSdkError::Borsh)?; + if self.should_remove_data { + // TODO: review security. + output.data_hash = DEFAULT_DATA_HASH; + } else { + output.data_hash = self.account.hash::()?; + if H::ID != 0 { + output.data_hash[0] = 0; + } + output.data = self + .account + .try_to_vec() + .map_err(|_| LightSdkError::Borsh)?; + } } Ok(self.account_info) } } -impl Deref - for LightAccount<'_, A> +impl< + H: Hasher, + A: AnchorSerialize + AnchorDeserialize + LightDiscriminator + DataHasher + Default, + > Deref for LightAccountInner<'_, H, A> { type Target = A; @@ -257,8 +358,10 @@ impl DerefMut - for LightAccount<'_, A> +impl< + H: Hasher, + A: AnchorSerialize + AnchorDeserialize + LightDiscriminator + DataHasher + Default, + > DerefMut for LightAccountInner<'_, H, A> { fn deref_mut(&mut self) -> &mut ::Target { &mut self.account diff --git a/sdk-libs/sdk/src/compressible/allocate.rs b/sdk-libs/sdk/src/compressible/allocate.rs new file mode 100644 index 0000000000..f320fbb308 --- /dev/null +++ b/sdk-libs/sdk/src/compressible/allocate.rs @@ -0,0 +1,79 @@ +#[cfg(feature = "anchor")] +use anchor_lang::{system_program::CreateAccount, Result}; +#[cfg(feature = "anchor")] +use solana_account_info::AccountInfo; +#[cfg(feature = "anchor")] +use solana_pubkey::Pubkey; +#[cfg(feature = "anchor")] +use solana_rent::Rent; +#[cfg(feature = "anchor")] +use solana_sysvar::Sysvar; + +#[cfg(feature = "anchor")] +pub fn create_or_allocate_account<'a>( + program_id: &Pubkey, + payer: AccountInfo<'a>, + system_program: AccountInfo<'a>, + target_account: AccountInfo<'a>, + signer_seed: &[&[u8]], + space: usize, +) -> Result<()> { + let rent = Rent::get()?; + let current_lamports = target_account.lamports(); + + if current_lamports == 0 { + use anchor_lang::{prelude::CpiContext, system_program::create_account}; + + let lamports = rent.minimum_balance(space); + let cpi_accounts = CreateAccount { + from: payer, + to: target_account.clone(), + }; + let cpi_context = CpiContext::new(system_program.clone(), cpi_accounts); + create_account( + cpi_context.with_signer(&[signer_seed]), + lamports, + u64::try_from(space).unwrap(), + program_id, + )?; + } else { + use anchor_lang::{ + prelude::CpiContext, + system_program::{allocate, assign, Allocate, Assign}, + }; + + let required_lamports = rent + .minimum_balance(space) + .max(1) + .saturating_sub(current_lamports); + if required_lamports > 0 { + use anchor_lang::{ + prelude::CpiContext, + system_program::{transfer, Transfer}, + ToAccountInfo, + }; + + let cpi_accounts = Transfer { + from: payer.to_account_info(), + to: target_account.clone(), + }; + let cpi_context = CpiContext::new(system_program.clone(), cpi_accounts); + transfer(cpi_context, required_lamports)?; + } + let cpi_accounts = Allocate { + account_to_allocate: target_account.clone(), + }; + let cpi_context = CpiContext::new(system_program.clone(), cpi_accounts); + allocate( + cpi_context.with_signer(&[signer_seed]), + u64::try_from(space).unwrap(), + )?; + + let cpi_accounts = Assign { + account_to_assign: target_account.clone(), + }; + let cpi_context = CpiContext::new(system_program.clone(), cpi_accounts); + assign(cpi_context.with_signer(&[signer_seed]), program_id)?; + } + 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..55c768d3dd --- /dev/null +++ b/sdk-libs/sdk/src/compressible/compress_account.rs @@ -0,0 +1,253 @@ +#[cfg(feature = "anchor")] +use anchor_lang::{prelude::Account, AccountDeserialize, AccountSerialize}; +#[cfg(feature = "anchor")] +use light_compressed_account::instruction_data::with_account_info::CompressedAccountInfo; +use light_hasher::DataHasher; +#[cfg(feature = "anchor")] +use light_sdk_types::instruction::account_meta::CompressedAccountMetaNoLamportsNoAddress; +use solana_account_info::AccountInfo; +use solana_clock::Clock; +use solana_msg::msg; +#[cfg(feature = "anchor")] +use solana_pubkey::Pubkey; +use solana_sysvar::Sysvar; + +#[cfg(feature = "anchor")] +use crate::compressible::compression_info::CompressAs; +use crate::{ + account::sha::LightAccount, + compressible::{compress_account_on_init_native::close, compression_info::HasCompressionInfo}, + cpi::{CpiAccountsSmall, CpiInputs}, + error::LightSdkError, + instruction::{account_meta::CompressedAccountMeta, ValidityProof}, + AnchorDeserialize, AnchorSerialize, LightDiscriminator, +}; + +/// Helper function to compress a PDA and reclaim rent. +/// +/// This function uses the CompressAs trait to determine what data should be +/// stored in the compressed state. For simple cases where you want to store the +/// exact same data, implement CompressAs with `type Output = Self` and return +/// `self.clone()`. For custom compression, you can specify different field +/// values or even a different type entirely. +/// +/// This requires the compressed PDA that is tied to the onchain PDA to already +/// exist, and the account type must implement CompressAs. +/// +/// +/// 1. updates the empty compressed PDA with data from CompressAs::compress_as() +/// 2. transfers PDA lamports to rent_recipient +/// 1. closes onchain PDA +/// +/// +/// # Arguments +/// * `solana_account` - The PDA account to compress (will be closed) +/// * `compressed_account_meta` - Metadata for the compressed account (must be +/// empty but have an address) +/// * `proof` - Validity proof +/// * `cpi_accounts` - Accounts needed for CPI +/// * `rent_recipient` - The account to receive the PDA's rent +/// * `compression_delay` - The number of slots to wait before compression is +/// allowed +#[cfg(feature = "anchor")] +pub fn compress_account<'info, A>( + solana_account: &mut Account<'info, A>, + compressed_account_meta: &CompressedAccountMeta, + proof: ValidityProof, + cpi_accounts: CpiAccountsSmall<'_, 'info>, + _rent_recipient: &AccountInfo<'info>, + compression_delay: &u32, +) -> Result<(), crate::ProgramError> +where + A: DataHasher + + LightDiscriminator + + AnchorSerialize + + AnchorDeserialize + + AccountSerialize + + AccountDeserialize + + Default + + Clone + + HasCompressionInfo + + CompressAs, + A::Output: DataHasher + + LightDiscriminator + + AnchorSerialize + + AnchorDeserialize + + HasCompressionInfo + + Default, +{ + let current_slot = Clock::get()?.slot; + + let last_written_slot = solana_account.compression_info().last_written_slot(); + + if current_slot < last_written_slot + *compression_delay as u64 { + msg!( + "compress_account failed: Cannot compress yet. {} slots remaining", + (last_written_slot + *compression_delay as u64).saturating_sub(current_slot) + ); + return Err(LightSdkError::ConstraintViolation.into()); + } + // ensure re-init attack is not possible + solana_account.compression_info_mut().set_compressed(); + + let owner_program_id = cpi_accounts.self_program_id(); + let mut compressed_account = LightAccount::<'_, A::Output>::new_mut_without_data( + &owner_program_id, + compressed_account_meta, + )?; + + let compressed_data = match solana_account.compress_as() { + std::borrow::Cow::Borrowed(data) => data.clone(), + std::borrow::Cow::Owned(data) => data, + }; + compressed_account.account = compressed_data; + + let cpi_inputs = CpiInputs::new(proof, vec![compressed_account.to_account_info()?]); + + // invoke light system program to update compressed account + cpi_inputs.invoke_light_system_program_small(cpi_accounts)?; + + Ok(()) +} + +#[cfg(feature = "anchor")] +pub fn prepare_account_for_compression<'info, A>( + program_id: &Pubkey, + account: &mut Account<'info, A>, + compressed_account_meta: &CompressedAccountMetaNoLamportsNoAddress, + cpi_accounts: &CpiAccountsSmall<'_, 'info>, + compression_delay: &u32, + address_space: &[Pubkey], +) -> Result +where + A: DataHasher + + LightDiscriminator + + AnchorSerialize + + AnchorDeserialize + + AccountSerialize + + AccountDeserialize + + Default + + Clone + + HasCompressionInfo + + CompressAs, + A::Output: DataHasher + + LightDiscriminator + + AnchorSerialize + + AnchorDeserialize + + HasCompressionInfo + + Default, +{ + use anchor_lang::Key; + use light_compressed_account::address::derive_compressed_address; + + let derived_c_pda = derive_compressed_address( + &account.key().into(), + &address_space[0].into(), + &program_id.into(), + ); + + 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.compression_info().last_written_slot(); + + if current_slot < last_written_slot + *compression_delay as u64 { + msg!( + "compress_account failed: Cannot compress yet. {} slots remaining", + (last_written_slot + *compression_delay as u64).saturating_sub(current_slot) + ); + return Err(LightSdkError::ConstraintViolation.into()); + } + // ensure re-init attack is not possible + account.compression_info_mut().set_compressed(); + + let owner_program_id = cpi_accounts.self_program_id(); + let mut compressed_account = + LightAccount::<'_, A::Output>::new_mut_without_data(&owner_program_id, &meta_with_address)?; + + let compressed_data = match account.compress_as() { + std::borrow::Cow::Borrowed(data) => data.clone(), + std::borrow::Cow::Owned(data) => data, + }; + compressed_account.account = compressed_data; + + Ok(compressed_account.to_account_info()?) +} + +/// Native Solana variant of compress_account that works with AccountInfo and pre-deserialized data. +/// +/// Helper function to compress a PDA and reclaim rent. +/// +/// 1. updates the empty compressed PDA with onchain PDA data +/// 2. transfers PDA lamports to rent_recipient +/// 3. closes onchain PDA +/// +/// This requires the compressed PDA that is tied to the onchain PDA to already +/// exist. +/// +/// # Arguments +/// * `pda_account_info` - The PDA AccountInfo to compress (will be closed) +/// * `pda_account_data` - The pre-deserialized PDA account data +/// * `compressed_account_meta` - Metadata for the compressed account (must be +/// empty but have an address) +/// * `proof` - Validity proof +/// * `cpi_accounts` - Accounts needed for CPI +/// * `owner_program` - The program that will own the compressed account +/// * `rent_recipient` - The account to receive the PDA's rent +/// * `compression_delay` - The number of slots to wait before compression is +/// allowed +pub fn compress_pda_native<'info, A>( + pda_account_info: &mut AccountInfo<'info>, + pda_account_data: &mut A, + compressed_account_meta: &CompressedAccountMeta, + proof: ValidityProof, + cpi_accounts: CpiAccountsSmall<'_, 'info>, + rent_recipient: &AccountInfo<'info>, + compression_delay: &u32, +) -> Result<(), crate::ProgramError> +where + A: DataHasher + + LightDiscriminator + + AnchorSerialize + + AnchorDeserialize + + Default + + Clone + + HasCompressionInfo, +{ + let current_slot = Clock::get()?.slot; + + let last_written_slot = pda_account_data.compression_info().last_written_slot(); + + if current_slot < last_written_slot + *compression_delay as u64 { + msg!( + "compress_pda_native failed: Cannot compress yet. {} slots remaining", + (last_written_slot + *compression_delay as u64).saturating_sub(current_slot) + ); + return Err(LightSdkError::ConstraintViolation.into()); + } + // ensure re-init attack is not possible + pda_account_data.compression_info_mut().set_compressed(); + + // Create the compressed account with the PDA data + let owner_program_id = cpi_accounts.self_program_id(); + let mut compressed_account = + LightAccount::<'_, A>::new_mut_without_data(&owner_program_id, compressed_account_meta)?; + + let mut compressed_data = pda_account_data.clone(); + compressed_data.set_compression_info_none(); + compressed_account.account = compressed_data; + + // Create CPI inputs + let cpi_inputs = CpiInputs::new(proof, vec![compressed_account.to_account_info()?]); + + // Invoke light system program to create the compressed account + cpi_inputs.invoke_light_system_program_small(cpi_accounts)?; + // Close PDA account manually + close(pda_account_info, rent_recipient.clone())?; + Ok(()) +} 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..9788427ecb --- /dev/null +++ b/sdk-libs/sdk/src/compressible/compress_account_on_init.rs @@ -0,0 +1,343 @@ +#![allow(clippy::all)] // TODO: Remove. +#[cfg(feature = "anchor")] +use anchor_lang::Key; +#[allow(unused_imports)] // TODO: Remove. +#[cfg(feature = "anchor")] +use anchor_lang::{ + AccountsClose, + {prelude::Account, AccountDeserialize, AccountSerialize}, +}; +#[cfg(feature = "anchor")] +use light_compressed_account::instruction_data::data::NewAddressParamsAssignedPacked; +use light_hasher::DataHasher; +use solana_account_info::AccountInfo; +use solana_msg::msg; +use solana_pubkey::Pubkey; + +use crate::{ + account::sha::LightAccount, + compressible::HasCompressionInfo, + cpi::{CpiAccountsSmall, CpiInputs}, + error::{LightSdkError, Result}, + instruction::ValidityProof, + AnchorDeserialize, AnchorSerialize, LightDiscriminator, +}; + +/// Wrapper to init an Anchor account as compressible and directly compress it. +/// Close the source PDA account manually at the end of the caller program's +/// init instruction. +#[cfg(feature = "anchor")] +#[allow(clippy::too_many_arguments)] +pub fn compress_account_on_init<'info, A>( + solana_account: &Account<'info, A>, + address: &[u8; 32], + new_address_param: &NewAddressParamsAssignedPacked, + output_state_tree_index: u8, + cpi_accounts: CpiAccountsSmall<'_, 'info>, + address_space: &[Pubkey], + rent_recipient: &AccountInfo<'info>, + proof: ValidityProof, +) -> Result<()> +where + A: DataHasher + + LightDiscriminator + + AnchorSerialize + + AnchorDeserialize + + AccountSerialize + + AccountDeserialize + + Default + + Clone + + HasCompressionInfo, + A: std::fmt::Debug, +{ + let solana_accounts: [&Account<'info, A>; 1] = [&solana_account]; + let addresses: [[u8; 32]; 1] = [*address]; + let new_address_params: [NewAddressParamsAssignedPacked; 1] = [*new_address_param]; + let output_state_tree_indices: [u8; 1] = [output_state_tree_index]; + + let compressed_infos = prepare_accounts_for_compression_on_init( + &solana_accounts, + &addresses, + &new_address_params, + &output_state_tree_indices, + &cpi_accounts, + address_space, + rent_recipient, + )?; + + let cpi_inputs = + CpiInputs::new_with_assigned_address(proof, compressed_infos, vec![*new_address_param]); + + cpi_inputs.invoke_light_system_program_small(cpi_accounts)?; + + Ok(()) +} + +/// Helper function to initialize a multiple Anchor accounts as compressible. +/// Returns account_infos so that all compressible accounts can be compressed in +/// a single CPI at the end of the caller program's init instruction. +/// +/// # Arguments +/// * `solana_accounts` - The Anchor accounts to compress +/// * `addresses` - The addresses for the compressed accounts +/// * `new_address_params` - Address parameters for the compressed accounts +/// * `output_state_tree_indices` - Output state tree indices for the compressed +/// accounts +/// * `cpi_accounts` - Accounts needed for validation +/// * `owner_program` - The program that will own the compressed accounts +/// * `address_space` - The address space to validate uniqueness against +/// +/// # Returns +/// * `Ok(Vec)` - CompressedAccountInfo for CPI batching +/// * `Err(LightSdkError)` if there was an error +#[cfg(feature = "anchor")] +#[allow(clippy::too_many_arguments)] +pub fn prepare_accounts_for_compression_on_init<'info, A>( + solana_accounts: &[&Account<'info, A>], + addresses: &[[u8; 32]], + new_address_params: &[NewAddressParamsAssignedPacked], + output_state_tree_indices: &[u8], + cpi_accounts: &CpiAccountsSmall<'_, 'info>, + _address_space: &[Pubkey], // TODO: remove. + _rent_recipient: &AccountInfo<'info>, // TODO: remove. +) -> Result> +where + A: DataHasher + + LightDiscriminator + + AnchorSerialize + + AnchorDeserialize + + AccountSerialize + + AccountDeserialize + + Default + + Clone + + HasCompressionInfo, + A: std::fmt::Debug, +{ + if solana_accounts.len() != addresses.len() + || solana_accounts.len() != new_address_params.len() + || solana_accounts.len() != output_state_tree_indices.len() + { + msg!( + "Array length mismatch in prepare_accounts_for_compression_on_init - solana_accounts: {}, addresses: {}, new_address_params: {}, output_state_tree_indices: {}", + solana_accounts.len(), + addresses.len(), + new_address_params.len(), + output_state_tree_indices.len() + ); + return Err(LightSdkError::ConstraintViolation); + } + + // TODO: consider enabling, or move outside. + // Address space validation + // for params in new_address_params { + // let tree = cpi_accounts + // .get_tree_account_info(params.address_merkle_tree_account_index as usize) + // .map_err(|_| { + // msg!( + // "Failed to get tree account info at index {}", + // params.address_merkle_tree_account_index + // ); + // LightSdkError::ConstraintViolation + // })? + // .pubkey(); + // if !address_space.iter().any(|a| a == &tree) { + // msg!( + // "Address tree {:?} not found in allowed address space: {:?}", + // tree, + // address_space + // ); + // return Err(LightSdkError::ConstraintViolation); + // } + // } + + let mut compressed_account_infos = Vec::new(); + + for (((solana_account, &address), &_new_address_param), &output_state_tree_index) in + solana_accounts + .iter() + .zip(addresses.iter()) + .zip(new_address_params.iter()) + .zip(output_state_tree_indices.iter()) + { + // TODO: check security of not setting compressed so we don't need to pass as mut. + // Ensure the account is marked as compressed We need to init first + // because it's none. Setting to compressed prevents lamports funding + // attack. + // *solana_account.compression_info_mut_opt() = + // Some(super::CompressionInfo::new_decompressed()?); + // solana_account.compression_info_mut().set_compressed(); + + let owner_program_id = cpi_accounts.self_program_id(); + + let mut compressed_account = LightAccount::<'_, A>::new_init( + &owner_program_id, + Some(address), + output_state_tree_index, + ); + + // Clone the PDA data and set compression_info to None. + let mut compressed_data = (***solana_account).clone(); + compressed_data.set_compression_info_none(); + compressed_account.account = compressed_data; + + compressed_account_infos.push(compressed_account.to_account_info()?); + } + + Ok(compressed_account_infos) +} + +/// Wrapper to process a single onchain PDA for creating an empty compressed +/// account. +/// +/// The PDA account is NOT closed. +#[cfg(feature = "anchor")] +#[allow(clippy::too_many_arguments)] +pub fn compress_empty_account_on_init<'info, A>( + solana_account: &mut Account<'info, A>, + address: &[u8; 32], + new_address_param: &NewAddressParamsAssignedPacked, + output_state_tree_index: u8, + cpi_accounts: CpiAccountsSmall<'_, 'info>, + address_space: &[Pubkey], + proof: ValidityProof, +) -> Result<()> +where + A: DataHasher + + LightDiscriminator + + AnchorSerialize + + AnchorDeserialize + + AccountSerialize + + AccountDeserialize + + Default + + Clone + + HasCompressionInfo, +{ + let mut solana_accounts: [&mut Account<'info, A>; 1] = [solana_account]; + let addresses: [[u8; 32]; 1] = [*address]; + let new_address_params: [NewAddressParamsAssignedPacked; 1] = [*new_address_param]; + let output_state_tree_indices: [u8; 1] = [output_state_tree_index]; + + let compressed_infos = prepare_empty_compressed_accounts_on_init( + &mut solana_accounts, + &addresses, + &new_address_params, + &output_state_tree_indices, + &cpi_accounts, + address_space, + )?; + + let cpi_inputs = + CpiInputs::new_with_assigned_address(proof, compressed_infos, vec![*new_address_param]); + + cpi_inputs.invoke_light_system_program_small(cpi_accounts)?; + + Ok(()) +} + +/// Helper function to initialize multiple empty compressed PDA based on the +/// Anchor accounts addresses. +/// +/// Use this over `prepare_accounts_for_compression_on_init` if you want to +/// initialize your Anchor accounts as compressible **without** compressing them +/// atomically. +/// +/// # Arguments +/// * `solana_accounts` - The Anchor accounts +/// * `addresses` - The addresses for the compressed accounts +/// * `new_address_params` - Address parameters for the compressed accounts +/// * `output_state_tree_indices` - Output state tree indices for the compressed +/// accounts +/// * `cpi_accounts` - Accounts needed for validation +/// * `address_space` - The address space to validate uniqueness against +/// +/// # Returns +/// * `Ok(Vec)` - CompressedAccountInfo for CPI batching +/// * `Err(LightSdkError)` if there was an error +#[cfg(feature = "anchor")] +#[allow(clippy::too_many_arguments)] +pub fn prepare_empty_compressed_accounts_on_init<'info, A>( + solana_accounts: &mut [&mut Account<'info, A>], + addresses: &[[u8; 32]], + new_address_params: &[NewAddressParamsAssignedPacked], + output_state_tree_indices: &[u8], + cpi_accounts: &CpiAccountsSmall<'_, 'info>, + address_space: &[Pubkey], +) -> Result> +where + A: DataHasher + + LightDiscriminator + + AnchorSerialize + + AnchorDeserialize + + AccountSerialize + + AccountDeserialize + + Default + + Clone + + HasCompressionInfo, +{ + if solana_accounts.len() != addresses.len() + || solana_accounts.len() != new_address_params.len() + || solana_accounts.len() != output_state_tree_indices.len() + { + msg!( + "Array length mismatch in prepare_empty_compressed_accounts_on_init - solana_accounts: {}, addresses: {}, new_address_params: {}, output_state_tree_indices: {}", + solana_accounts.len(), + addresses.len(), + new_address_params.len(), + output_state_tree_indices.len() + ); + return Err(LightSdkError::ConstraintViolation); + } + + let mut compressed_account_infos = Vec::new(); + + for (((_solana_account, &address), &_new_address_param), &output_state_tree_index) in + solana_accounts + .iter_mut() + .zip(addresses.iter()) + .zip(new_address_params.iter()) + .zip(output_state_tree_indices.iter()) + { + let owner_program_id = cpi_accounts.self_program_id(); + + // Create an empty compressed account with the specified address + let mut compressed_account = LightAccount::<'_, A>::new_init( + &owner_program_id, + Some(address), + output_state_tree_index, + ); + + // TODO: Remove this once we have a better error message for address + // mismatch. + { + use light_compressed_account::address::derive_address; + + let c_pda = compressed_account.address().ok_or_else(|| { + msg!("Compressed account address is missing in compress_account_on_init"); + LightSdkError::ConstraintViolation + })?; + + let derived_c_pda = derive_address( + &_solana_account.key().to_bytes(), + &address_space[0].to_bytes(), + &cpi_accounts.self_program_id().to_bytes(), + ); + + // CHECK: pda and c_pda are related + if c_pda != derived_c_pda { + msg!( + "cPDA {:?} does not match derived cPDA {:?} for PDA {:?} with address space {:?}", + c_pda, + derived_c_pda, + _solana_account.key(), + address_space, + ); + return Err(LightSdkError::ConstraintViolation); + } + } + + compressed_account.remove_data(); + compressed_account_infos.push(compressed_account.to_account_info()?); + } + + Ok(compressed_account_infos) +} diff --git a/sdk-libs/sdk/src/compressible/compress_account_on_init_native.rs b/sdk-libs/sdk/src/compressible/compress_account_on_init_native.rs new file mode 100644 index 0000000000..1fcaa60978 --- /dev/null +++ b/sdk-libs/sdk/src/compressible/compress_account_on_init_native.rs @@ -0,0 +1,401 @@ +//! Native Solana helpers for compressing accounts on init. Anchor-free. + +#![allow(clippy::all)] // TODO: Remove. +#[allow(unused_imports)] // TODO: Remove. +use light_compressed_account::instruction_data::data::NewAddressParamsAssignedPacked; +use light_hasher::DataHasher; +use solana_account_info::AccountInfo; +use solana_msg::msg; +use solana_pubkey::Pubkey; + +use crate::{ + account::sha::LightAccount, + address::PackedNewAddressParams, + compressible::HasCompressionInfo, + cpi::{CpiAccountsSmall, CpiInputs}, + error::{LightSdkError, Result}, + instruction::ValidityProof, + light_account_checks::AccountInfoTrait, + AnchorDeserialize, AnchorSerialize, LightDiscriminator, +}; + +/// Native Solana variant of compress_account_on_init that works with raw AccountInfo and pre-deserialized data. +/// +/// Wrapper to init an raw PDA as compressible and directly compress it. +/// Calls `prepare_accounts_for_compression_on_init_native` with single-element +/// slices and invokes the CPI. Close the source PDA account manually. +#[allow(clippy::too_many_arguments)] +pub fn compress_account_on_init_native<'info, A>( + pda_account_info: &mut AccountInfo<'info>, + pda_account_data: &mut A, + address: &[u8; 32], + new_address_param: &PackedNewAddressParams, + output_state_tree_index: u8, + cpi_accounts: CpiAccountsSmall<'_, 'info>, + address_space: &[Pubkey], + rent_recipient: &AccountInfo<'info>, + proof: ValidityProof, +) -> Result<()> +where + A: DataHasher + + LightDiscriminator + + AnchorSerialize + + AnchorDeserialize + + Default + + Clone + + HasCompressionInfo, +{ + // let pda_accounts_info: = &[pda_account_info]; + let mut pda_accounts_data: [&mut A; 1] = [pda_account_data]; + let addresses: [[u8; 32]; 1] = [*address]; + let new_address_params: [PackedNewAddressParams; 1] = [*new_address_param]; + let output_state_tree_indices: [u8; 1] = [output_state_tree_index]; + + let compressed_infos = prepare_accounts_for_compression_on_init_native( + &mut [pda_account_info], + &mut pda_accounts_data, + &addresses, + &new_address_params, + &output_state_tree_indices, + &cpi_accounts, + address_space, + rent_recipient, + )?; + + let cpi_inputs = CpiInputs::new_with_assigned_address( + proof, + compressed_infos, + vec![ + light_compressed_account::instruction_data::data::NewAddressParamsAssignedPacked::new( + *new_address_param, + None, + ), + ], + ); + + cpi_inputs.invoke_light_system_program_small(cpi_accounts)?; + + Ok(()) +} + +/// Native Solana variant of prepare_accounts_for_compression_on_init that works +/// with AccountInfo and pre-deserialized data. +/// +/// Helper function to process multiple onchain PDAs for compression into new +/// compressed accounts. +/// +/// This function processes accounts of a single type and returns +/// CompressedAccountInfo for CPI batching. It allows the caller to handle the +/// CPI invocation separately, enabling batching of multiple different account +/// types. +/// +/// # Arguments +/// * `pda_accounts_info` - The PDA AccountInfos to compress +/// * `pda_accounts_data` - The pre-deserialized PDA account data +/// * `addresses` - The addresses for the compressed accounts +/// * `new_address_params` - Address parameters for the compressed accounts +/// * `output_state_tree_indices` - Output state tree indices for the compressed +/// accounts +/// * `cpi_accounts` - Accounts needed for validation +/// * `address_space` - The address space to validate uniqueness against +/// * `rent_recipient` - The account to receive the PDAs' rent +/// +/// # Returns +/// * `Ok(Vec)` - CompressedAccountInfo for CPI batching +/// * `Err(LightSdkError)` if there was an error +#[allow(clippy::too_many_arguments)] +pub fn prepare_accounts_for_compression_on_init_native<'info, A>( + pda_accounts_info: &mut [&mut AccountInfo<'info>], + pda_accounts_data: &mut [&mut A], + addresses: &[[u8; 32]], + new_address_params: &[PackedNewAddressParams], + output_state_tree_indices: &[u8], + cpi_accounts: &CpiAccountsSmall<'_, 'info>, + address_space: &[Pubkey], + rent_recipient: &AccountInfo<'info>, +) -> Result> +where + A: DataHasher + + LightDiscriminator + + AnchorSerialize + + AnchorDeserialize + + Default + + Clone + + HasCompressionInfo, +{ + if pda_accounts_info.len() != pda_accounts_data.len() + || pda_accounts_info.len() != addresses.len() + || pda_accounts_info.len() != new_address_params.len() + || pda_accounts_info.len() != output_state_tree_indices.len() + { + msg!("pda_accounts_info.len(): {:?}", pda_accounts_info.len()); + msg!("pda_accounts_data.len(): {:?}", pda_accounts_data.len()); + msg!("addresses.len(): {:?}", addresses.len()); + msg!("new_address_params.len(): {:?}", new_address_params.len()); + msg!( + "output_state_tree_indices.len(): {:?}", + output_state_tree_indices.len() + ); + return Err(LightSdkError::ConstraintViolation); + } + + // Address space validation + for params in new_address_params { + let tree = cpi_accounts + .get_tree_account_info(params.address_merkle_tree_account_index as usize) + .map_err(|_| { + msg!( + "Failed to get tree account info at index {} in prepare_accounts_for_compression_on_init_native", + params.address_merkle_tree_account_index + ); + LightSdkError::ConstraintViolation + })? + .pubkey(); + if !address_space.iter().any(|a| a == &tree) { + msg!("address tree: {:?}", tree); + msg!("expected address_space: {:?}", address_space); + msg!("Address tree {} not found in allowed address space in prepare_accounts_for_compression_on_init_native", tree); + return Err(LightSdkError::ConstraintViolation); + } + } + + let mut compressed_account_infos = Vec::new(); + + for ( + (((pda_account_info, pda_account_data), &address), &_new_address_param), + &output_state_tree_index, + ) in pda_accounts_info + .iter_mut() + .zip(pda_accounts_data.iter_mut()) + .zip(addresses.iter()) + .zip(new_address_params.iter()) + .zip(output_state_tree_indices.iter()) + { + // Ensure the account is marked as compressed We need to init first + // because it's none. Setting to compressed prevents lamports funding + // attack. + *pda_account_data.compression_info_mut_opt() = + Some(super::CompressionInfo::new_decompressed()?); + pda_account_data.compression_info_mut().set_compressed(); + + // Create the compressed account with the PDA data + let owner_program_id = cpi_accounts.self_program_id(); + let mut compressed_account = LightAccount::<'_, A>::new_init( + &owner_program_id, + Some(address), + output_state_tree_index, + ); + + // Clone the PDA data and set compression_info to None for compressed + // storage + let mut compressed_data = (*pda_account_data).clone(); + compressed_data.set_compression_info_none(); + compressed_account.account = compressed_data; + + compressed_account_infos.push(compressed_account.to_account_info()?); + + // Close PDA account manually + close(pda_account_info, rent_recipient.clone()).map_err(|err| { + msg!("Failed to close PDA account in prepare_accounts_for_compression_on_init_native: {:?}", err); + err + })?; + } + + Ok(compressed_account_infos) +} + +/// Native Solana variant to create an EMPTY compressed account from a PDA. +/// +/// This creates an empty compressed account without closing the source PDA, +/// similar to decompress_idempotent behavior. The PDA remains intact with its data. +/// +/// # Arguments +/// * `pda_account_info` - The PDA AccountInfo (will NOT be closed) +/// * `pda_account_data` - The pre-deserialized PDA 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 for the compressed account +/// * `cpi_accounts` - Accounts needed for validation +/// * `address_space` - The address space to validate uniqueness against +/// * `proof` - Validity proof for the address tree operation +#[allow(clippy::too_many_arguments)] +pub fn compress_empty_account_on_init_native<'info, A>( + pda_account_info: &mut AccountInfo<'info>, + pda_account_data: &mut A, + address: &[u8; 32], + new_address_param: &PackedNewAddressParams, + output_state_tree_index: u8, + cpi_accounts: CpiAccountsSmall<'_, 'info>, + address_space: &[Pubkey], + proof: ValidityProof, +) -> Result<()> +where + A: DataHasher + + LightDiscriminator + + AnchorSerialize + + AnchorDeserialize + + Default + + Clone + + HasCompressionInfo, +{ + let mut pda_accounts_data: [&mut A; 1] = [pda_account_data]; + let addresses: [[u8; 32]; 1] = [*address]; + let new_address_params: [PackedNewAddressParams; 1] = [*new_address_param]; + let output_state_tree_indices: [u8; 1] = [output_state_tree_index]; + + let compressed_infos = prepare_empty_compressed_accounts_on_init_native( + &mut [pda_account_info], + &mut pda_accounts_data, + &addresses, + &new_address_params, + &output_state_tree_indices, + &cpi_accounts, + address_space, + )?; + + let cpi_inputs = CpiInputs::new_with_assigned_address( + proof, + compressed_infos, + vec![ + light_compressed_account::instruction_data::data::NewAddressParamsAssignedPacked::new( + *new_address_param, + None, + ), + ], + ); + + cpi_inputs.invoke_light_system_program_small(cpi_accounts)?; + + Ok(()) +} + +/// Native Solana variant to create EMPTY compressed accounts from PDAs. +/// +/// This creates empty compressed accounts without closing the source PDAs. +/// The PDAs remain intact with their data, similar to decompress_idempotent behavior. +/// +/// # Arguments +/// * `pda_accounts_info` - The PDA AccountInfos (will NOT be closed) +/// * `pda_accounts_data` - The pre-deserialized PDA account data +/// * `addresses` - The addresses for the compressed accounts +/// * `new_address_params` - Address parameters for the compressed accounts +/// * `output_state_tree_indices` - Output state tree indices for the compressed accounts +/// * `cpi_accounts` - Accounts needed for validation +/// * `address_space` - The address space to validate uniqueness against +/// +/// # Returns +/// * `Ok(Vec)` - CompressedAccountInfo for CPI batching +/// * `Err(LightSdkError)` if there was an error +#[allow(clippy::too_many_arguments)] +pub fn prepare_empty_compressed_accounts_on_init_native<'info, A>( + _pda_accounts_info: &mut [&mut AccountInfo<'info>], + pda_accounts_data: &mut [&mut A], + addresses: &[[u8; 32]], + new_address_params: &[PackedNewAddressParams], + output_state_tree_indices: &[u8], + cpi_accounts: &CpiAccountsSmall<'_, 'info>, + address_space: &[Pubkey], +) -> Result> +where + A: DataHasher + + LightDiscriminator + + AnchorSerialize + + AnchorDeserialize + + Default + + Clone + + HasCompressionInfo, +{ + if pda_accounts_data.len() != addresses.len() + || pda_accounts_data.len() != new_address_params.len() + || pda_accounts_data.len() != output_state_tree_indices.len() + { + msg!("pda_accounts_data.len(): {:?}", pda_accounts_data.len()); + msg!("addresses.len(): {:?}", addresses.len()); + msg!("new_address_params.len(): {:?}", new_address_params.len()); + msg!( + "output_state_tree_indices.len(): {:?}", + output_state_tree_indices.len() + ); + return Err(LightSdkError::ConstraintViolation); + } + + // Address space validation + for params in new_address_params { + let tree = cpi_accounts + .get_tree_account_info(params.address_merkle_tree_account_index as usize) + .map_err(|_| { + msg!( + "Failed to get tree account info at index {} in prepare_empty_compressed_accounts_on_init_native", + params.address_merkle_tree_account_index + ); + LightSdkError::ConstraintViolation + })? + .pubkey(); + if !address_space.iter().any(|a| a == &tree) { + msg!("address tree: {:?}", tree); + msg!("expected address_space: {:?}", address_space); + return Err(LightSdkError::ConstraintViolation); + } + } + + let mut compressed_account_infos = Vec::new(); + + for (((pda_account_data, &address), &_new_address_param), &output_state_tree_index) in + pda_accounts_data + .iter_mut() + .zip(addresses.iter()) + .zip(new_address_params.iter()) + .zip(output_state_tree_indices.iter()) + { + *pda_account_data.compression_info_mut_opt() = + Some(super::CompressionInfo::new_decompressed()?); + pda_account_data + .compression_info_mut() + .bump_last_written_slot()?; + + let owner_program_id = cpi_accounts.self_program_id(); + let mut light_account = LightAccount::<'_, A>::new_init( + &owner_program_id, + Some(address), + output_state_tree_index, + ); + light_account.remove_data(); + + compressed_account_infos.push(light_account.to_account_info()?); + } + + Ok(compressed_account_infos) +} + +// Proper native Solana account closing implementation +pub fn close<'info>( + info: &mut AccountInfo<'info>, + sol_destination: AccountInfo<'info>, +) -> Result<()> { + // Transfer all lamports from the account to the destination + let lamports_to_transfer = info.lamports(); + + // Use try_borrow_mut_lamports for proper borrow management + **info + .try_borrow_mut_lamports() + .map_err(|_| LightSdkError::ConstraintViolation)? = 0; + + let dest_lamports = sol_destination.lamports(); + **sol_destination + .try_borrow_mut_lamports() + .map_err(|_| LightSdkError::ConstraintViolation)? = + dest_lamports.checked_add(lamports_to_transfer).unwrap(); + + // Assign to system program first + let system_program_id = solana_pubkey::pubkey!("11111111111111111111111111111111"); + + info.assign(&system_program_id); + + // Realloc to 0 size - this should work after assigning to system program + info.realloc(0, false).map_err(|e| { + msg!("Error during realloc: {:?}", e); + LightSdkError::ConstraintViolation + })?; + + Ok(()) +} 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..871622fdf5 --- /dev/null +++ b/sdk-libs/sdk/src/compressible/compression_info.rs @@ -0,0 +1,222 @@ +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}; + +/// Trait for types that can be packed for compression. +/// +/// Packing is a space optimization technique where 32-byte `Pubkey` fields are replaced +/// with 1-byte indices that reference positions in a `remaining_accounts` array. +/// This significantly reduces instruction data size. +/// +/// For types without Pubkeys, implement identity packing (return self). +pub trait Pack { + /// The packed version of this type + type Packed: AnchorSerialize + Clone + std::fmt::Debug; + + /// Pack this type, replacing Pubkeys with indices + fn pack(&self, remaining_accounts: &mut PackedAccounts) -> Self::Packed; +} + +/// Trait for types that can be unpacked from their compressed form. +/// +/// This is used on-chain to convert packed instruction data back to the original types. +/// The unpacking resolves u8 indices back to Pubkeys using the remaining_accounts array. +/// +/// For identity-packed types, unpack returns a clone of self. +pub trait Unpack { + /// The unpacked version of this type + type Unpacked; + + /// Unpack this type, resolving indices to Pubkeys from remaining_accounts + fn unpack( + &self, + remaining_accounts: &[AccountInfo], + ) -> Result; +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, AnchorSerialize, AnchorDeserialize)] +#[repr(u8)] +pub enum AccountState { + Initialized, + Frozen, +} + +/// Trait for compressible accounts. +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); +} + +/// Trait for accounts that want to customize how their state gets compressed, +/// instead of just copying the current onchain state. +pub trait CompressAs { + /// The type that will be stored in the compressed state. + /// Can be `Self` or a different type entirely for maximum flexibility. + type Output: crate::AnchorSerialize + + crate::AnchorDeserialize + + crate::LightDiscriminator + + crate::account::Size + + HasCompressionInfo + + Default + + Clone; + + /// Returns the data that should be stored in the compressed state. This + /// allows developers to reset some fields while keeping others, or even + /// return a completely different type during compression. + /// + /// compression_info must ALWAYS be None in the returned data. This + /// eliminates the need for mutation after calling compress_as(). + /// + /// Uses Cow (Clone on Write) for performance - typically returns owned data + /// since compression_info must be None (different from onchain state). + /// + /// # Example - Default. + /// ```rust + /// impl CompressAs for UserRecord { + /// type Output = Self; + /// + /// fn compress_as(&self) -> Cow<'_, Self::Output> { + /// Cow::Owned(Self { + /// compression_info: None, // ALWAYS None + /// owner: self.owner, + /// name: self.name.clone(), + /// score: self.score, + /// }) + /// } + /// } + /// ``` + /// + /// # Example - Custom Compression (reset some values) + /// ```rust + /// impl CompressAs for Oracle { + /// type Output = Self; + /// + /// fn compress_as(&self) -> Cow<'_, Self::Output> { + /// Cow::Owned(Self { + /// compression_info: None, // ALWAYS None + /// initialized: false, // set false + /// observation_index: 0, // set 0 + /// pool_id: self.pool_id, // default + /// observations: None, // set None + /// padding: self.padding, + /// }) + /// } + /// } + /// ``` + /// + /// # Example - Different Type + /// ```rust + /// impl CompressAs for LargeGameState { + /// type Output = CompactGameState; + /// + /// fn compress_as(&self) -> Cow<'_, Self::Output> { + /// Cow::Owned(CompactGameState { + /// compression_info: None, // ALWAYS None + /// player_id: self.player_id, + /// level: self.level, + /// // Skip large arrays, temporary state, etc. + /// }) + /// } + /// } + /// ``` + fn compress_as(&self) -> Cow<'_, Self::Output>; +} + +/// Information for compressible accounts that tracks when the account was last +/// written +#[derive(Debug, Clone, Default, AnchorSerialize, AnchorDeserialize)] +pub struct CompressionInfo { + /// The slot when this account was last written/decompressed + pub last_written_slot: u64, + /// 0 not inited, 1 decompressed, 2 compressed + pub state: CompressionState, +} + +#[derive(Debug, Clone, Default, AnchorSerialize, AnchorDeserialize, PartialEq)] +pub enum CompressionState { + #[default] + Uninitialized, + Decompressed, + Compressed, +} + +impl CompressionInfo { + /// Creates new compression info with the current slot and sets state to + /// decompressed. + pub fn new_decompressed() -> Result { + Ok(Self { + last_written_slot: Clock::get()?.slot, + state: CompressionState::Decompressed, + }) + } + + /// Updates the last written slot to the current slot + pub fn bump_last_written_slot(&mut self) -> Result<(), crate::ProgramError> { + self.last_written_slot = Clock::get()?.slot; + Ok(()) + } + + /// Sets the last written slot to a specific value + pub fn set_last_written_slot(&mut self, slot: u64) { + self.last_written_slot = slot; + } + + /// Gets the last written slot + pub fn last_written_slot(&self) -> u64 { + self.last_written_slot + } + + /// Checks if the account can be compressed based on the compression delay constant. + pub fn can_compress(&self, compression_delay: u64) -> Result { + let current_slot = Clock::get()?.slot; + Ok(current_slot >= self.last_written_slot + compression_delay) + } + + /// Gets the number of slots remaining before compression is allowed + pub fn slots_until_compressible( + &self, + compression_delay: u64, + ) -> Result { + let current_slot = Clock::get()?.slot; + Ok((self.last_written_slot + compression_delay).saturating_sub(current_slot)) + } + + /// Set compressed + pub fn set_compressed(&mut self) { + self.state = CompressionState::Compressed; + } + + /// Check if the account is compressed + pub fn is_compressed(&self) -> bool { + self.state == CompressionState::Compressed + } +} + +#[cfg(feature = "anchor")] +impl anchor_lang::Space for CompressionInfo { + const INIT_SPACE: usize = 8 + 1; // u64 + state enum +} + +/// Generic compressed account data structure for decompress operations +/// This is generic over the account variant type, allowing programs to use their specific enums +/// +/// # Type Parameters +/// * `T` - The program-specific compressed account variant enum (e.g., CompressedAccountVariant) +/// +/// # Fields +/// * `meta` - The compressed account metadata containing tree info, address, and output index +/// * `data` - The program-specific account variant enum +/// * `seeds` - The PDA seeds (without bump) used to derive the PDA address +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)] +pub struct CompressedAccountData { + pub meta: CompressedAccountMetaNoLamportsNoAddress, + /// Program-specific account variant enum + 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..cc0ff9bb2e --- /dev/null +++ b/sdk-libs/sdk/src/compressible/config.rs @@ -0,0 +1,483 @@ +use std::collections::HashSet; + +use solana_account_info::AccountInfo; +use solana_cpi::invoke_signed; +use solana_msg::msg; +use solana_program::bpf_loader_upgradeable::UpgradeableLoaderState; +use solana_pubkey::Pubkey; +use solana_rent::Rent; +use solana_system_interface::instruction as system_instruction; +use solana_sysvar::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)] +pub struct CompressibleConfig { + /// Config version for future upgrades + pub version: u8, + /// Number of slots to wait before compression is allowed + pub compression_delay: u32, + /// Authority that can update the config + pub update_authority: Pubkey, + /// Account that receives rent from compressed PDAs + pub rent_recipient: Pubkey, + /// Config bump seed (currently always 0)å + pub config_bump: u8, + /// PDA bump seed + pub bump: u8, + /// Address space for compressed accounts (currently 1 address_tree allowed) + pub address_space: Vec, +} + +impl CompressibleConfig { + pub const LEN: usize = 1 + 4 + 32 + 32 + 1 + 4 + (32 * MAX_ADDRESS_TREES_PER_SPACE) + 1; // 107 bytes max + + /// Calculate the exact size needed for a CompressibleConfig with the given + /// number of address spaces + pub fn size_for_address_space(num_address_trees: usize) -> usize { + 1 + 4 + 32 + 32 + 1 + 4 + (32 * num_address_trees) + 1 + } + + /// Derives the config PDA address with config bump + pub fn derive_pda(program_id: &Pubkey, config_bump: u8) -> (Pubkey, u8) { + Pubkey::find_program_address(&[COMPRESSIBLE_CONFIG_SEED, &[config_bump]], program_id) + } + + /// Derives the default config PDA address (config_bump = 0) + pub fn derive_default_pda(program_id: &Pubkey) -> (Pubkey, u8) { + Self::derive_pda(program_id, 0) + } + + /// Checks the config account + pub fn validate(&self) -> Result<(), crate::ProgramError> { + if self.version != 1 { + msg!( + "CompressibleConfig validation failed: Unsupported config version: {}", + self.version + ); + return Err(LightSdkError::ConstraintViolation.into()); + } + if self.address_space.len() != 1 { + msg!( + "CompressibleConfig validation failed: Address space must contain exactly 1 pubkey, found: {}", + self.address_space.len() + ); + return Err(LightSdkError::ConstraintViolation.into()); + } + // For now, only allow config_bump = 0 to keep it simple + if self.config_bump != 0 { + msg!( + "CompressibleConfig validation failed: Config bump must be 0 for now, found: {}", + self.config_bump + ); + return Err(LightSdkError::ConstraintViolation.into()); + } + Ok(()) + } + + /// Loads and validates config from account, checking owner and PDA derivation + #[inline(never)] + pub fn load_checked( + account: &AccountInfo, + program_id: &Pubkey, + ) -> Result { + if account.owner != program_id { + msg!( + "CompressibleConfig::load_checked failed: Config account owner mismatch. Expected: {:?}. Found: {:?}.", + program_id, + account.owner + ); + return Err(LightSdkError::ConstraintViolation.into()); + } + let data = account.try_borrow_data()?; + let config = Self::try_from_slice(&data).map_err(|err| { + msg!( + "CompressibleConfig::load_checked failed: Failed to deserialize config data: {:?}", + err + ); + LightSdkError::Borsh + })?; + config.validate()?; + + // CHECK: PDA derivation + let (expected_pda, _) = Self::derive_pda(program_id, config.config_bump); + if expected_pda != *account.key { + msg!( + "CompressibleConfig::load_checked failed: Config account key mismatch. Expected PDA: {:?}. Found: {:?}.", + expected_pda, + account.key + ); + return Err(LightSdkError::ConstraintViolation.into()); + } + + Ok(config) + } +} + +/// Creates a new compressible config PDA +/// +/// # Security - Solana Best Practice +/// This function follows the standard Solana pattern where only the program's +/// upgrade authority can create the initial config. This prevents unauthorized +/// parties from hijacking the config system. +/// +/// # Arguments +/// * `config_account` - The config PDA account to initialize +/// * `update_authority` - Authority that can update the config after creation +/// * `rent_recipient` - Account that receives rent from compressed PDAs +/// * `address_space` - Address space for compressed accounts (currently 1 address_tree allowed) +/// * `compression_delay` - Number of slots to wait before compression +/// * `config_bump` - Config bump seed (must be 0 for now) +/// * `payer` - Account paying for the PDA creation +/// * `system_program` - System program +/// * `program_id` - The program that owns the config +/// +/// # Required Validation (must be done by caller) +/// The caller MUST validate that the signer is the program's upgrade authority +/// by checking against the program data account. This cannot be done in the SDK +/// due to dependency constraints. +/// +/// # Returns +/// * `Ok(())` if config was created successfully +/// * `Err(ProgramError)` if there was an error +#[allow(clippy::too_many_arguments)] +pub fn process_initialize_compression_config_account_info<'info>( + config_account: &AccountInfo<'info>, + update_authority: &AccountInfo<'info>, + rent_recipient: &Pubkey, + address_space: Vec, + compression_delay: u32, + config_bump: u8, + payer: &AccountInfo<'info>, + system_program: &AccountInfo<'info>, + program_id: &Pubkey, +) -> Result<(), crate::ProgramError> { + // CHECK: only 1 address_space + if config_bump != 0 { + msg!("Config bump must be 0 for now, found: {}", config_bump); + return Err(LightSdkError::ConstraintViolation.into()); + } + + // CHECK: not already initialized + if config_account.data_len() > 0 { + msg!("Config account already initialized"); + return Err(LightSdkError::ConstraintViolation.into()); + } + + // CHECK: only 1 address_space + if address_space.len() != 1 { + msg!( + "Address space must contain exactly 1 pubkey, found: {}", + address_space.len() + ); + return Err(LightSdkError::ConstraintViolation.into()); + } + + // CHECK: unique pubkeys in address_space + validate_address_space_no_duplicates(&address_space)?; + + // CHECK: signer + if !update_authority.is_signer { + msg!("Update authority must be signer for initial config creation"); + return Err(LightSdkError::ConstraintViolation.into()); + } + + // CHECK: pda derivation + let (derived_pda, bump) = CompressibleConfig::derive_pda(program_id, config_bump); + if derived_pda != *config_account.key { + msg!("Invalid config PDA"); + return Err(LightSdkError::ConstraintViolation.into()); + } + + let rent = Rent::get().map_err(LightSdkError::from)?; + let account_size = CompressibleConfig::size_for_address_space(address_space.len()); + let rent_lamports = rent.minimum_balance(account_size); + + let seeds = &[COMPRESSIBLE_CONFIG_SEED, &[config_bump], &[bump]]; + let create_account_ix = system_instruction::create_account( + payer.key, + config_account.key, + rent_lamports, + account_size as u64, + program_id, + ); + + invoke_signed( + &create_account_ix, + &[ + payer.clone(), + config_account.clone(), + system_program.clone(), + ], + &[seeds], + ) + .map_err(LightSdkError::from)?; + + let config = CompressibleConfig { + version: 1, + compression_delay, + update_authority: *update_authority.key, + rent_recipient: *rent_recipient, + config_bump, + address_space, + bump, + }; + + let mut data = config_account + .try_borrow_mut_data() + .map_err(LightSdkError::from)?; + config + .serialize(&mut &mut data[..]) + .map_err(|_| LightSdkError::Borsh)?; + + Ok(()) +} + +/// Updates an existing compressible config +/// +/// # Arguments +/// * `config_account` - The config PDA account to update +/// * `authority` - Current update authority (must match config) +/// * `new_update_authority` - Optional new update authority +/// * `new_rent_recipient` - Optional new rent recipient +/// * `new_address_space` - Optional new address space (currently 1 address_tree allowed) +/// * `new_compression_delay` - Optional new compression delay +/// * `owner_program_id` - The program that owns the config +/// +/// # Returns +/// * `Ok(())` if config was updated successfully +/// * `Err(ProgramError)` if there was an error +pub fn process_update_compression_config<'info>( + config_account: &AccountInfo<'info>, + authority: &AccountInfo<'info>, + new_update_authority: Option<&Pubkey>, + new_rent_recipient: Option<&Pubkey>, + new_address_space: Option>, + new_compression_delay: Option, + owner_program_id: &Pubkey, +) -> Result<(), crate::ProgramError> { + // CHECK: PDA derivation + let mut config = CompressibleConfig::load_checked(config_account, owner_program_id)?; + + // CHECK: signer + if !authority.is_signer { + msg!("Update authority must be signer"); + return Err(LightSdkError::ConstraintViolation.into()); + } + // CHECK: authority + if *authority.key != config.update_authority { + msg!("Invalid update authority"); + return Err(LightSdkError::ConstraintViolation.into()); + } + + if let Some(new_authority) = new_update_authority { + config.update_authority = *new_authority; + } + if let Some(new_recipient) = new_rent_recipient { + config.rent_recipient = *new_recipient; + } + if let Some(new_address_space) = new_address_space { + // CHECK: address space length + if new_address_space.len() != MAX_ADDRESS_TREES_PER_SPACE { + msg!( + "New address space must contain exactly 1 pubkey, found: {}", + new_address_space.len() + ); + return Err(LightSdkError::ConstraintViolation.into()); + } + + validate_address_space_no_duplicates(&new_address_space)?; + + validate_address_space_only_adds(&config.address_space, &new_address_space)?; + + config.address_space = new_address_space; + } + if let Some(new_delay) = new_compression_delay { + config.compression_delay = new_delay; + } + + let mut data = config_account.try_borrow_mut_data().map_err(|e| { + msg!("Failed to borrow mut data for config_account: {:?}", e); + LightSdkError::from(e) + })?; + config.serialize(&mut &mut data[..]).map_err(|e| { + msg!("Failed to serialize updated config: {:?}", e); + LightSdkError::Borsh + })?; + + Ok(()) +} + +/// Verifies that the signer is the program's upgrade authority +/// +/// # Arguments +/// * `program_id` - The program to check +/// * `program_data_account` - The program's data account (ProgramData) +/// * `authority` - The authority to verify +/// +/// # Returns +/// * `Ok(())` if authority is valid +/// * `Err(LightSdkError)` if authority is invalid or verification fails +pub fn check_program_upgrade_authority( + program_id: &Pubkey, + program_data_account: &AccountInfo, + authority: &AccountInfo, +) -> Result<(), crate::ProgramError> { + // CHECK: program data PDA + let (expected_program_data, _) = + Pubkey::find_program_address(&[program_id.as_ref()], &BPF_LOADER_UPGRADEABLE_ID); + if program_data_account.key != &expected_program_data { + msg!("Invalid program data account"); + return Err(LightSdkError::ConstraintViolation.into()); + } + + let data = program_data_account.try_borrow_data()?; + let program_state: UpgradeableLoaderState = bincode::deserialize(&data).map_err(|_| { + msg!("Failed to deserialize program data account"); + LightSdkError::ConstraintViolation + })?; + + // Extract upgrade authority + let upgrade_authority = match program_state { + UpgradeableLoaderState::ProgramData { + slot: _, + upgrade_authority_address, + } => { + match upgrade_authority_address { + Some(auth) => { + // Check for invalid zero authority when authority exists + if auth == Pubkey::default() { + msg!("Invalid state: authority is zero pubkey"); + return Err(LightSdkError::ConstraintViolation.into()); + } + auth + } + None => { + msg!("Program has no upgrade authority"); + return Err(LightSdkError::ConstraintViolation.into()); + } + } + } + _ => { + msg!("Account is not ProgramData, found: {:?}", program_state); + return Err(LightSdkError::ConstraintViolation.into()); + } + }; + + // CHECK: upgrade authority is signer + if !authority.is_signer { + msg!("Authority must be signer"); + return Err(LightSdkError::ConstraintViolation.into()); + } + + // CHECK: upgrade authority is program's upgrade authority + if *authority.key != upgrade_authority { + msg!( + "Signer is not the program's upgrade authority. Signer: {:?}, Expected Authority: {:?}", + authority.key, + upgrade_authority + ); + return Err(LightSdkError::ConstraintViolation.into()); + } + + Ok(()) +} + +/// Creates a new compressible config PDA with program upgrade authority +/// validation +/// +/// # Security +/// This function verifies that the signer is the program's upgrade authority +/// before creating the config. This ensures only the program deployer can +/// initialize the configuration. +/// +/// # Arguments +/// * `config_account` - The config PDA account to initialize +/// * `update_authority` - Must be the program's upgrade authority +/// * `program_data_account` - The program's data account for validation +/// * `rent_recipient` - Account that receives rent from compressed PDAs +/// * `address_space` - Address spaces for compressed accounts (exactly 1 +/// allowed) +/// * `compression_delay` - Number of slots to wait before compression +/// * `config_bump` - Config bump seed (must be 0 for now) +/// * `payer` - Account paying for the PDA creation +/// * `system_program` - System program +/// * `program_id` - The program that owns the config +/// +/// # Returns +/// * `Ok(())` if config was created successfully +/// * `Err(ProgramError)` if there was an error or authority validation fails +#[allow(clippy::too_many_arguments)] +pub fn process_initialize_compression_config_checked<'info>( + config_account: &AccountInfo<'info>, + update_authority: &AccountInfo<'info>, + program_data_account: &AccountInfo<'info>, + rent_recipient: &Pubkey, + address_space: Vec, + compression_delay: u32, + config_bump: u8, + payer: &AccountInfo<'info>, + system_program: &AccountInfo<'info>, + program_id: &Pubkey, +) -> Result<(), crate::ProgramError> { + msg!( + "create_compression_config_checked program_data_account: {:?}", + program_data_account.key + ); + msg!( + "create_compression_config_checked program_id: {:?}", + program_id + ); + // Verify the signer is the program's upgrade authority + check_program_upgrade_authority(program_id, program_data_account, update_authority)?; + + // Create the config with validated authority + process_initialize_compression_config_account_info( + config_account, + update_authority, + rent_recipient, + address_space, + compression_delay, + config_bump, + payer, + system_program, + program_id, + ) +} + +/// Validates that address_space contains no duplicate pubkeys +fn validate_address_space_no_duplicates(address_space: &[Pubkey]) -> Result<(), LightSdkError> { + let mut seen = HashSet::new(); + for pubkey in address_space { + if !seen.insert(pubkey) { + msg!("Duplicate pubkey found in address_space: {}", pubkey); + return Err(LightSdkError::ConstraintViolation); + } + } + Ok(()) +} + +/// Validates that new_address_space only adds to existing address_space (no removals) +fn validate_address_space_only_adds( + existing_address_space: &[Pubkey], + new_address_space: &[Pubkey], +) -> Result<(), LightSdkError> { + // Check that all existing pubkeys are still present in new address space + for existing_pubkey in existing_address_space { + if !new_address_space.contains(existing_pubkey) { + msg!( + "Cannot remove existing pubkey from address_space: {}", + existing_pubkey + ); + return Err(LightSdkError::ConstraintViolation); + } + } + Ok(()) +} diff --git a/sdk-libs/sdk/src/compressible/decompress_idempotent.rs b/sdk-libs/sdk/src/compressible/decompress_idempotent.rs new file mode 100644 index 0000000000..af6db8c57b --- /dev/null +++ b/sdk-libs/sdk/src/compressible/decompress_idempotent.rs @@ -0,0 +1,148 @@ +#![allow(clippy::all)] // TODO: Remove. + +use light_compressed_account::address::derive_compressed_address; +use light_hasher::DataHasher; +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_rent::Rent; +use solana_system_interface::instruction as system_instruction; +use solana_sysvar::Sysvar; + +use crate::{ + account::sha::LightAccount, compressible::compression_info::HasCompressionInfo, + cpi::CpiAccountsSmall, error::LightSdkError, AnchorDeserialize, AnchorSerialize, + LightDiscriminator, +}; + +/// Helper to invoke create_account on heap. +#[inline(never)] +#[cold] +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, + ); + + let accounts = vec![ + rent_payer.clone(), + solana_account.clone(), + system_program.clone(), + ]; + + invoke_signed(&create_account_ix, &accounts, &[seeds]) + .map_err(|e| LightSdkError::ProgramError(e)) +} + +/// 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_compressed_address( + &solana_account.key.into(), + &address_space.into(), + &program_id.into(), + ); + + 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 +} + +/// Helper function to decompress multiple compressed accounts into PDAs +/// idempotently with seeds. Does not invoke the zk compression CPI. This +/// function processes accounts of a single type and returns +/// CompressedAccountInfo for CPI batching. It's idempotent, meaning it can be +/// called multiple times with the same compressed accounts and it will only +/// decompress them once. +#[inline(never)] +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: &CpiAccountsSmall<'a, 'info>, + signer_seeds: &[&[u8]], +) -> Result< + Option, + LightSdkError, +> +where + T: Clone + + crate::account::Size + + DataHasher + + 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 mut light_account = LightAccount::<'_, T>::new_mut(&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()?, + )?; + + // set compression info + let mut decompressed_pda = light_account.account.clone(); + *decompressed_pda.compression_info_mut_opt() = + Some(super::CompressionInfo::new_decompressed()?); + + // serialize onchain account + 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 + })?; + + light_account.remove_data(); + 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..c3b0009115 --- /dev/null +++ b/sdk-libs/sdk/src/compressible/mod.rs @@ -0,0 +1,33 @@ +//! SDK helpers for compressing and decompressing compressible PDAs accounts. + +pub mod allocate; +pub mod compress_account; +pub mod compress_account_on_init; +pub mod compress_account_on_init_native; +pub mod compression_info; +pub mod config; +pub mod decompress_idempotent; + +pub use allocate::*; +#[cfg(feature = "anchor")] +pub use compress_account::compress_account; +pub use compress_account::compress_pda_native; +#[cfg(feature = "anchor")] +pub use compress_account_on_init::{ + compress_account_on_init, compress_empty_account_on_init, + prepare_accounts_for_compression_on_init, prepare_empty_compressed_accounts_on_init, +}; +pub use compress_account_on_init_native::{ + compress_account_on_init_native, compress_empty_account_on_init_native, + prepare_accounts_for_compression_on_init_native, + prepare_empty_compressed_accounts_on_init_native, +}; +pub use compression_info::{CompressAs, CompressionInfo, HasCompressionInfo, Pack, 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, +}; +pub use decompress_idempotent::{ + into_compressed_meta_with_address, prepare_account_for_decompression_idempotent, +}; diff --git a/sdk-libs/sdk/src/cpi/invoke.rs b/sdk-libs/sdk/src/cpi/invoke.rs index fe11129a29..245b520aa6 100644 --- a/sdk-libs/sdk/src/cpi/invoke.rs +++ b/sdk-libs/sdk/src/cpi/invoke.rs @@ -11,6 +11,8 @@ use light_sdk_types::{ constants::{CPI_AUTHORITY_PDA_SEED, LIGHT_SYSTEM_PROGRAM_ID}, cpi_context_write::CpiContextWriteAccounts, }; +#[allow(unused_imports)] // TODO: Remove. +use solana_msg::msg; use crate::{ cpi::{ @@ -36,6 +38,38 @@ pub struct CpiInputs { pub cpi_context: Option, } +/// Builder pattern implementation for CpiInputs. +/// +/// This provides a fluent API for constructing CPI inputs with various configurations. +/// The most common pattern is to use one of the constructor methods and then chain +/// builder methods to add additional configuration. +/// +/// # Examples +/// +/// Most common CPI context usage (no proof, assigned addresses): +/// ```rust +/// let cpi_inputs = CpiInputs::new_for_cpi_context( +/// all_compressed_infos, +/// vec![pool_new_address_params, observation_new_address_params], +/// ); +/// ``` +/// +/// Basic usage with CPI context and custom proof: +/// ```rust +/// let cpi_inputs = CpiInputs::new_with_assigned_address( +/// light_proof, +/// all_compressed_infos, +/// vec![pool_new_address_params, observation_new_address_params], +/// ) +/// .with_first_set_cpi_context(); +/// ``` +/// +/// Advanced usage with multiple configurations: +/// ```rust +/// let cpi_inputs = CpiInputs::new(proof, account_infos) +/// .with_first_set_cpi_context() +/// .with_compress_lamports(1000000); +/// ``` impl CpiInputs { pub fn new(proof: ValidityProof, account_infos: Vec) -> Self { Self { @@ -71,6 +105,88 @@ impl CpiInputs { } } + // TODO: check if always unused! + /// Creates CpiInputs for the common CPI context pattern: no proof (None), + /// assigned addresses, and first set CPI context. + /// + /// This is the most common pattern when using CPI context for cross-program + /// compressed account operations. + /// + /// # Example + /// ```rust + /// let cpi_inputs = CpiInputs::new_for_cpi_context( + /// all_compressed_infos, + /// vec![user_new_address_params, game_new_address_params], + /// ); + /// ``` + pub fn new_first_cpi( + account_infos: Vec, + new_addresses: Vec, + ) -> Self { + Self { + proof: ValidityProof(None), + account_infos: Some(account_infos), + new_assigned_addresses: Some(new_addresses), + cpi_context: Some(CompressedCpiContext { + set_context: false, + first_set_context: true, + cpi_context_account_index: 0, // unused + }), + ..Default::default() + } + } + + /// Sets a custom CPI context. + /// + /// # Example + /// ``` + /// let cpi_inputs = CpiInputs::new_with_assigned_address(proof, infos, addresses) + /// .with_cpi_context(CompressedCpiContext { + /// set_context: true, + /// first_set_context: false, + /// cpi_context_account_index: 1, + /// }); + /// ``` + pub fn with_cpi_context(mut self, cpi_context: CompressedCpiContext) -> Self { + self.cpi_context = Some(cpi_context); + self + } + + // TODO: check if always unused! + /// Sets CPI context to first set context (clears any existing context). + /// This is the most common pattern for initializing CPI context. + /// + /// # Example + /// ``` + /// let cpi_inputs = CpiInputs::new_with_assigned_address(proof, infos, addresses) + /// .with_first_set_cpi_context(); + /// ``` + pub fn with_first_set_cpi_context(mut self) -> Self { + self.cpi_context = Some(CompressedCpiContext { + set_context: false, + first_set_context: true, + cpi_context_account_index: 0, // unused. + }); + self + } + + /// Sets CPI context to set context (updates existing context). + /// Use this when you want to update an existing CPI context. + /// + /// # Example + /// ``` + /// let cpi_inputs = CpiInputs::new_with_assigned_address(proof, infos, addresses) + /// .with_set_cpi_context(0); + /// ``` + pub fn with_last_cpi_context(mut self, cpi_context_account_index: u8) -> Self { + self.cpi_context = Some(CompressedCpiContext { + set_context: true, + first_set_context: false, + cpi_context_account_index, + }); + self + } + pub fn invoke_light_system_program(self, cpi_accounts: CpiAccounts<'_, '_>) -> Result<()> { let bump = cpi_accounts.bump(); let account_infos = cpi_accounts.to_account_infos(); @@ -88,6 +204,8 @@ impl CpiInputs { create_light_system_progam_instruction_invoke_cpi_small(self, cpi_accounts)?; invoke_light_system_program(account_infos.as_slice(), instruction, bump) } + #[inline(never)] + #[cold] pub fn invoke_light_system_program_cpi_context( self, cpi_accounts: CpiContextWriteAccounts, @@ -143,6 +261,8 @@ pub fn create_light_system_progam_instruction_invoke_cpi_small( }) } +#[inline(never)] +#[cold] pub fn create_light_system_progam_instruction_invoke_cpi_context_write( cpi_inputs: CpiInputs, cpi_accounts: CpiContextWriteAccounts, diff --git a/sdk-libs/sdk/src/cpi/mod.rs b/sdk-libs/sdk/src/cpi/mod.rs index be5adfa6fd..6461d83abd 100644 --- a/sdk-libs/sdk/src/cpi/mod.rs +++ b/sdk-libs/sdk/src/cpi/mod.rs @@ -8,12 +8,11 @@ //! pub const LIGHT_CPI_SIGNER: CpiSigner = //! derive_light_cpi_signer!("2tzfijPBGbrR5PboyFUFKzfEoLTwdDSHUjANCw929wyt"); //! -//! let light_cpi_accounts = CpiAccounts::new( +//! let light_cpi_accounts = CpiAccountsSmall::new( //! ctx.accounts.fee_payer.as_ref(), //! ctx.remaining_accounts, //! crate::LIGHT_CPI_SIGNER, -//! ) -//! .map_err(ProgramError::from)?; +//! ); //! //! let (address, address_seed) = derive_address( //! &[b"compressed", name.as_bytes()], @@ -43,8 +42,7 @@ //! ); //! //! cpi_inputs -//! .invoke_light_system_program(light_cpi_accounts) -//! .map_err(ProgramError::from)?; +//! .invoke_light_system_program_small(light_cpi_accounts)?; //! ``` mod accounts; diff --git a/sdk-libs/sdk/src/error.rs b/sdk-libs/sdk/src/error.rs index 3bdbfae0a3..9cda42844c 100644 --- a/sdk-libs/sdk/src/error.rs +++ b/sdk-libs/sdk/src/error.rs @@ -96,6 +96,14 @@ impl From for ProgramError { } } +#[cfg(feature = "anchor")] +impl From for anchor_lang::error::Error { + fn from(e: LightSdkError) -> Self { + let error_code = u32::from(e); + anchor_lang::error::Error::from(anchor_lang::prelude::ProgramError::Custom(error_code)) + } +} + impl From for LightSdkError { fn from(e: LightSdkTypesError) -> Self { match e { diff --git a/sdk-libs/sdk/src/instruction/mod.rs b/sdk-libs/sdk/src/instruction/mod.rs index 49cd82bd60..69745da9ce 100644 --- a/sdk-libs/sdk/src/instruction/mod.rs +++ b/sdk-libs/sdk/src/instruction/mod.rs @@ -176,6 +176,9 @@ mod pack_accounts; mod system_accounts; mod tree_info; +/// Borsh compatible validity proof implementation. Proves the validity of +/// existing compressed accounts and new addresses. +pub use light_compressed_account::instruction_data::compressed_proof::borsh_compat; /// Zero-knowledge proof to prove the validity of existing compressed accounts and new addresses. pub use light_compressed_account::instruction_data::compressed_proof::ValidityProof; pub use light_sdk_types::instruction::*; diff --git a/sdk-libs/sdk/src/lib.rs b/sdk-libs/sdk/src/lib.rs index b8eef1be97..13ec853678 100644 --- a/sdk-libs/sdk/src/lib.rs +++ b/sdk-libs/sdk/src/lib.rs @@ -103,8 +103,21 @@ /// Compressed account abstraction similar to anchor Account. pub mod account; +pub use account::LightAccount; + +/// SHA256-based variants +pub mod sha { + pub use light_sdk_macros::{ + LightDiscriminatorSha as LightDiscriminator, LightHasherSha as LightHasher, + }; + + pub use crate::account::sha::LightAccount; +} + /// Functions to derive compressed account addresses. pub mod address; +/// SDK helpers for compressing and decompressing PDAs. +pub mod compressible; /// Utilities to invoke the light-system-program via cpi. pub mod cpi; pub mod error; @@ -116,14 +129,17 @@ pub mod token; pub mod transfer; pub mod utils; +pub use account::Size; #[cfg(feature = "anchor")] -use anchor_lang::{AnchorDeserialize, AnchorSerialize}; +pub use anchor_lang::{AnchorDeserialize, AnchorSerialize}; #[cfg(not(feature = "anchor"))] -use borsh::{BorshDeserialize as AnchorDeserialize, BorshSerialize as AnchorSerialize}; +pub use borsh::{BorshDeserialize as AnchorDeserialize, BorshSerialize as AnchorSerialize}; pub use light_account_checks::{self, discriminator::Discriminator as LightDiscriminator}; pub use light_hasher; pub use light_sdk_macros::{ - derive_light_cpi_signer, light_system_accounts, LightDiscriminator, LightHasher, LightTraits, + add_compressible_instructions, derive_light_cpi_signer, generate_seed_functions, + light_system_accounts, Compressible, CompressiblePack, DeriveSeeds, LightDiscriminator, + LightDiscriminatorSha, LightHasher, LightHasherSha, LightTraits, }; pub use light_sdk_types::constants; use solana_account_info::AccountInfo; diff --git a/sdk-libs/sdk/src/token.rs b/sdk-libs/sdk/src/token.rs index 2edf2311d6..1f9553d097 100644 --- a/sdk-libs/sdk/src/token.rs +++ b/sdk-libs/sdk/src/token.rs @@ -1,6 +1,11 @@ use light_compressed_account::compressed_account::CompressedAccountWithMerkleContext; +use solana_account_info::AccountInfo; -use crate::{AnchorDeserialize, AnchorSerialize, Pubkey}; +use crate::{ + compressible::{Pack, Unpack}, + instruction::PackedAccounts, + AnchorDeserialize, AnchorSerialize, Pubkey, +}; #[derive(Clone, Copy, Debug, PartialEq, Eq, AnchorDeserialize, AnchorSerialize, Default)] #[repr(u8)] @@ -32,3 +37,165 @@ pub struct TokenDataWithMerkleContext { pub token_data: TokenData, pub compressed_account: CompressedAccountWithMerkleContext, } + +/// Implementation for TokenData - packs into InputTokenDataCompressible +impl Pack for TokenData { + type Packed = InputTokenDataCompressible; + + fn pack(&self, remaining_accounts: &mut PackedAccounts) -> Self::Packed { + InputTokenDataCompressible { + owner: remaining_accounts.insert_or_get(self.owner), + amount: self.amount, + has_delegate: self.delegate.is_some(), + delegate: if let Some(delegate) = self.delegate { + remaining_accounts.insert_or_get(delegate) + } else { + 0 // Unused when has_delegate is false + }, + mint: remaining_accounts.insert_or_get_read_only(self.mint), + version: 2, // Default version for compressed token accounts + } + } +} + +impl Unpack for TokenData { + type Unpacked = Self; + + fn unpack( + &self, + _remaining_accounts: &[AccountInfo], + ) -> std::result::Result { + Ok(self.clone()) + } +} + +/// Unpack implementation for InputTokenDataCompressible +impl Unpack for InputTokenDataCompressible { + type Unpacked = TokenData; + + fn unpack( + &self, + remaining_accounts: &[AccountInfo], + ) -> std::result::Result { + Ok(TokenData { + owner: *remaining_accounts + .get(self.owner as usize) + .ok_or(solana_program_error::ProgramError::InvalidAccountData)? + .key, + amount: self.amount, + delegate: if self.has_delegate { + Some( + *remaining_accounts + .get(self.delegate as usize) + .ok_or(solana_program_error::ProgramError::InvalidAccountData)? + .key, + ) + } else { + None + }, + mint: *remaining_accounts + .get(self.mint as usize) + .ok_or(solana_program_error::ProgramError::InvalidAccountData)? + .key, + state: AccountState::Initialized, // Default state for unpacked + tlv: None, // No TLV data in packed version + }) + } +} + +/// Wrapper for token data with variant information +/// The variant is user-defined and doesn't get altered during packing +#[derive(AnchorSerialize, AnchorDeserialize, Debug, Clone)] +pub struct TokenDataWithVariant { + pub variant: V, + pub token_data: TokenData, +} + +#[derive(AnchorSerialize, AnchorDeserialize, Debug, Clone)] +pub struct PackedCompressibleTokenDataWithVariant { + pub variant: V, + pub token_data: InputTokenDataCompressible, +} +#[derive(AnchorSerialize, AnchorDeserialize, Debug, Clone)] +pub struct CompressibleTokenDataWithVariant { + pub variant: V, + pub token_data: TokenData, +} + +/// Pack implementation for CompressibleTokenDataWithVariant +impl Pack for CompressibleTokenDataWithVariant +where + V: AnchorSerialize + Clone + std::fmt::Debug, +{ + type Packed = PackedCompressibleTokenDataWithVariant; + + fn pack(&self, remaining_accounts: &mut PackedAccounts) -> Self::Packed { + PackedCompressibleTokenDataWithVariant { + variant: self.variant.clone(), + token_data: self.token_data.pack(remaining_accounts), + } + } +} + +/// Unpack implementation for CompressibleTokenDataWithVariant +impl Unpack for CompressibleTokenDataWithVariant +where + V: Clone, +{ + type Unpacked = TokenDataWithVariant; + + fn unpack( + &self, + remaining_accounts: &[AccountInfo], + ) -> std::result::Result { + Ok(TokenDataWithVariant { + variant: self.variant.clone(), + token_data: self.token_data.unpack(remaining_accounts)?, + }) + } +} + +/// Pack implementation for TokenDataWithVariant +impl Pack for TokenDataWithVariant +where + V: AnchorSerialize + Clone + std::fmt::Debug, +{ + type Packed = PackedCompressibleTokenDataWithVariant; + + fn pack(&self, remaining_accounts: &mut PackedAccounts) -> Self::Packed { + PackedCompressibleTokenDataWithVariant { + variant: self.variant.clone(), + token_data: self.token_data.pack(remaining_accounts), + } + } +} + +/// Unpack implementation for PackedCompressibleTokenDataWithVariant +impl Unpack for PackedCompressibleTokenDataWithVariant +where + V: Clone, +{ + type Unpacked = TokenDataWithVariant; + + fn unpack( + &self, + remaining_accounts: &[AccountInfo], + ) -> std::result::Result { + Ok(TokenDataWithVariant { + variant: self.variant.clone(), + token_data: self.token_data.unpack(remaining_accounts)?, + }) + } +} + +// custom replacement for MultiInputTokenDataWithContext +// without root_index and without merkle_context +#[derive(Clone, Debug, AnchorDeserialize, AnchorSerialize, Default)] +pub struct InputTokenDataCompressible { + pub owner: u8, + pub amount: u64, + pub has_delegate: bool, // Optional delegate is set + pub delegate: u8, + pub mint: u8, + pub version: u8, +} diff --git a/sdk-libs/token-client/src/actions/create_token_pool.rs b/sdk-libs/token-client/src/actions/create_token_pool.rs new file mode 100644 index 0000000000..05a57c7300 --- /dev/null +++ b/sdk-libs/token-client/src/actions/create_token_pool.rs @@ -0,0 +1,73 @@ +use light_client::rpc::{Rpc, RpcError}; +use solana_keypair::Keypair; +use solana_pubkey::Pubkey; +use solana_signature::Signature; +use solana_signer::Signer; + +use crate::instructions::create_token_pool::create_token_pool_instruction; + +/// Creates a token pool PDA for a given mint and sends the transaction. +/// +/// This action creates a token pool account that can hold SPL tokens for +/// compression/decompression operations. The token pool is owned by the +/// CPI authority PDA and can be used by the compressed token program. +/// +/// # Arguments +/// * `rpc` - RPC client +/// * `mint` - The SPL mint for which to create the token pool +/// * `is_token_22` - Whether this is a Token-2022 mint (vs regular SPL Token) +/// * `payer` - Transaction fee payer keypair +/// +/// # Returns +/// `Result` - The transaction signature +pub async fn create_token_pool( + rpc: &mut R, + mint: &Pubkey, + is_token_22: bool, + payer: &Keypair, +) -> Result { + // Create the instruction + let instruction = create_token_pool_instruction(&payer.pubkey(), mint, is_token_22)?; + + // Send the transaction (only payer needs to sign) + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer]) + .await +} + +/// Creates a token pool PDA for a regular SPL Token mint and sends the transaction. +/// +/// This is a convenience function for creating token pools for regular SPL Token mints. +/// +/// # Arguments +/// * `rpc` - RPC client +/// * `mint` - The SPL mint for which to create the token pool +/// * `payer` - Transaction fee payer keypair +/// +/// # Returns +/// `Result` - The transaction signature +pub async fn create_spl_token_pool( + rpc: &mut R, + mint: &Pubkey, + payer: &Keypair, +) -> Result { + create_token_pool(rpc, mint, false, payer).await +} + +/// Creates a token pool PDA for a Token-2022 mint and sends the transaction. +/// +/// This is a convenience function for creating token pools for Token-2022 mints. +/// +/// # Arguments +/// * `rpc` - RPC client +/// * `mint` - The Token-2022 mint for which to create the token pool +/// * `payer` - Transaction fee payer keypair +/// +/// # Returns +/// `Result` - The transaction signature +pub async fn create_token_22_pool( + rpc: &mut R, + mint: &Pubkey, + payer: &Keypair, +) -> Result { + create_token_pool(rpc, mint, true, payer).await +} diff --git a/sdk-libs/token-client/src/actions/mod.rs b/sdk-libs/token-client/src/actions/mod.rs index 3c780eaa41..fbbfb9fcf1 100644 --- a/sdk-libs/token-client/src/actions/mod.rs +++ b/sdk-libs/token-client/src/actions/mod.rs @@ -1,11 +1,13 @@ mod create_mint; mod create_spl_mint; +mod create_token_pool; mod decompressed_token_transfer; mod mint_action; mod mint_to_compressed; pub mod transfer2; pub use create_mint::*; pub use create_spl_mint::*; +pub use create_token_pool::*; pub use decompressed_token_transfer::*; pub use mint_action::*; pub use mint_to_compressed::*; 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..0f98391206 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 @@ -11,6 +11,7 @@ use solana_signature::Signature; use solana_signer::Signer; /// Transfer tokens from a compressed token account to an SPL token account +#[allow(clippy::too_many_arguments)] pub async fn ctoken_to_spl_transfer( rpc: &mut R, source_ctoken_account: Pubkey, @@ -19,20 +20,22 @@ pub async fn ctoken_to_spl_transfer( authority: &Keypair, mint: Pubkey, payer: &Keypair, + spl_token_program: Pubkey, ) -> Result { // Derive token pool PDA with bump 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( + payer.pubkey(), + authority.pubkey(), source_ctoken_account, destination_spl_token_account, - amount, - authority.pubkey(), mint, - payer.pubkey(), + spl_token_program, token_pool_pda, token_pool_pda_bump, + amount, ) .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..a1763cadbf 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 @@ -51,14 +51,15 @@ pub async fn spl_to_ctoken_transfer( // Create the SPL to CToken transfer instruction let ix = create_spl_to_ctoken_transfer_instruction( + payer.pubkey(), + authority.pubkey(), source_spl_token_account, to, - amount, - authority.pubkey(), mint, - payer.pubkey(), + authority.pubkey(), token_pool_pda, bump, + amount, ) .map_err(|e| RpcError::CustomError(e.to_string()))?; diff --git a/sdk-libs/token-client/src/instructions/create_mint.rs b/sdk-libs/token-client/src/instructions/create_mint.rs index a8365025e7..0475cfc061 100644 --- a/sdk-libs/token-client/src/instructions/create_mint.rs +++ b/sdk-libs/token-client/src/instructions/create_mint.rs @@ -16,7 +16,7 @@ use solana_keypair::Keypair; use solana_pubkey::Pubkey; use solana_signer::Signer; -/// Create a compressed mint instruction with automatic setup. +/// Create a compressed mint instruction. /// /// # Arguments /// * `rpc` - RPC client with indexer capabilities @@ -38,7 +38,6 @@ pub async fn create_compressed_mint_instruction( payer: Pubkey, metadata: Option, ) -> Result { - // Get address tree and output queue from RPC let address_tree_pubkey = rpc.get_address_tree_v2().tree; let output_queue = rpc.get_random_state_tree_info()?.queue; @@ -46,16 +45,13 @@ pub async fn create_compressed_mint_instruction( let compressed_mint_address = derive_compressed_mint_address(&mint_seed.pubkey(), &address_tree_pubkey); - // Find mint bump for the instruction let (_, mint_bump) = Pubkey::find_program_address( &[COMPRESSED_MINT_SEED, mint_seed.pubkey().as_ref()], &Pubkey::new_from_array(light_ctoken_types::COMPRESSED_TOKEN_PROGRAM_ID), ); - // Create extensions if metadata is provided let extensions = metadata.map(|meta| vec![ExtensionInstructionData::TokenMetadata(meta)]); - // Get validity proof for address creation let rpc_result = rpc .get_validity_proof( vec![], @@ -70,7 +66,6 @@ pub async fn create_compressed_mint_instruction( let address_merkle_tree_root_index = rpc_result.addresses[0].root_index; - // Create instruction using the existing SDK function let inputs = CreateCompressedMintInputs { decimals, mint_authority, 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 88c7fa0787..028d26d97f 100644 --- a/sdk-libs/token-client/src/instructions/create_spl_mint.rs +++ b/sdk-libs/token-client/src/instructions/create_spl_mint.rs @@ -15,7 +15,7 @@ use solana_keypair::Keypair; use solana_pubkey::Pubkey; use solana_signer::Signer; -/// Creates a create_spl_mint instruction with automatic RPC integration +/// Creates a create_spl_mint instruction with rpc. /// /// This function automatically: /// - Fetches the compressed mint account data @@ -39,13 +39,11 @@ pub async fn create_spl_mint_instruction( mint_authority: Pubkey, payer: Pubkey, ) -> Result { - // Get the compressed mint account let compressed_mint_account = rpc .get_compressed_account(compressed_mint_address, None) .await? .value; - // Deserialize the compressed mint data let compressed_mint: CompressedMint = BorshDeserialize::deserialize( &mut compressed_mint_account .data @@ -58,27 +56,21 @@ pub async fn create_spl_mint_instruction( ) .map_err(|e| RpcError::CustomError(format!("Failed to deserialize compressed mint: {}", e)))?; - // Get validity proof for the compressed mint let proof_result = rpc .get_validity_proof(vec![compressed_mint_account.hash], vec![], None) .await? .value; - // Derive SPL mint PDA and bump let (spl_mint_pda, mint_bump) = find_spl_mint_address(&mint_seed.pubkey()); - // Derive token pool for the SPL mint let token_pool = derive_token_pool(&spl_mint_pda, 0); - // Get tree and queue information let input_tree = compressed_mint_account.tree_info.tree; let input_queue = compressed_mint_account.tree_info.queue; - // Get a separate output queue for the new compressed mint state let output_tree_info = rpc.get_random_state_tree_info()?; let output_queue = output_tree_info.queue; - // Prepare compressed mint inputs let compressed_mint_inputs = CompressedMintWithContext { leaf_index: compressed_mint_account.leaf_index, prove_by_index: true, @@ -92,7 +84,6 @@ pub async fn create_spl_mint_instruction( })?, }; - // Create the instruction using the SDK function let instruction = sdk_create_spl_mint_instruction(CreateSplMintInputs { mint_signer: mint_seed.pubkey(), mint_bump, @@ -106,6 +97,6 @@ pub async fn create_spl_mint_instruction( token_pool, }) .map_err(|e| RpcError::CustomError(format!("Failed to create SPL mint instruction: {}", e)))?; - println!("instruction {:?}", instruction); + Ok(instruction) } diff --git a/sdk-libs/token-client/src/instructions/create_token_pool.rs b/sdk-libs/token-client/src/instructions/create_token_pool.rs new file mode 100644 index 0000000000..136b161e89 --- /dev/null +++ b/sdk-libs/token-client/src/instructions/create_token_pool.rs @@ -0,0 +1,93 @@ +use light_client::rpc::RpcError; +use light_compressed_token_sdk::{SPL_TOKEN_2022_PROGRAM_ID, SPL_TOKEN_PROGRAM_ID}; +use light_sdk::constants::CPI_AUTHORITY_PDA_SEED; +use solana_instruction::{AccountMeta, Instruction}; +use solana_pubkey::Pubkey; + +pub const CREATE_TOKEN_POOL_DISCRIMINATOR: [u8; 8] = [23, 169, 27, 122, 147, 169, 209, 152]; +pub const TOKEN_POOL_SEED: &[u8] = b"pool"; + +/// Creates an instruction to create a token pool PDA for a given mint +/// +/// This creates a token pool account that is owned by the CPI authority PDA +/// and can hold SPL tokens for compression/decompression operations. +/// +/// # Arguments +/// * `fee_payer` - Account that pays for the transaction fees +/// * `mint` - The SPL mint for which to create the token pool +/// * `is_token_22` - Whether this is a Token-2022 mint (vs regular SPL Token) +/// +/// # Returns +/// `Result` - The create token pool instruction +pub fn create_token_pool_instruction( + fee_payer: &Pubkey, + mint: &Pubkey, + is_token_22: bool, +) -> Result { + let compressed_token_program_id = Pubkey::from(light_ctoken_types::COMPRESSED_TOKEN_PROGRAM_ID); + + let (token_pool_pda, _bump) = Pubkey::find_program_address( + &[TOKEN_POOL_SEED, mint.as_ref()], + &compressed_token_program_id, + ); + + let (cpi_authority_pda, _cpi_bump) = + Pubkey::find_program_address(&[CPI_AUTHORITY_PDA_SEED], &compressed_token_program_id); + + let token_program = if is_token_22 { + Pubkey::from(SPL_TOKEN_2022_PROGRAM_ID) + } else { + Pubkey::from(SPL_TOKEN_PROGRAM_ID) + }; + + let mut instruction_data = Vec::new(); + instruction_data.extend_from_slice(&CREATE_TOKEN_POOL_DISCRIMINATOR); + + let instruction = Instruction { + program_id: compressed_token_program_id, + accounts: vec![ + AccountMeta::new(*fee_payer, true), // fee_payer (signer, writable) + AccountMeta::new(token_pool_pda, false), // token_pool_pda (writable) + AccountMeta::new_readonly(Pubkey::from([0; 32]), false), // system_program + AccountMeta::new(*mint, false), // mint (writable) + AccountMeta::new_readonly(token_program, false), // token_program + AccountMeta::new_readonly(cpi_authority_pda, false), // cpi_authority_pda + ], + data: instruction_data, + }; + + Ok(instruction) +} + +/// Helper function to derive token pool PDA address +pub fn get_token_pool_pda(mint: &Pubkey) -> Pubkey { + get_token_pool_pda_with_index(mint, 0) +} + +/// Helper function to derive token pool PDA address with specific 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 +} + +/// Helper function to find token pool PDA address and bump with specific index +pub fn find_token_pool_pda_with_index(mint: &Pubkey, token_pool_index: u8) -> (Pubkey, u8) { + const POOL_SEED: &[u8] = b"pool"; + let compressed_token_program_id = Pubkey::from(light_ctoken_types::COMPRESSED_TOKEN_PROGRAM_ID); + + let seeds = &[POOL_SEED, mint.as_ref(), &[token_pool_index]]; + let seeds = if token_pool_index == 0 { + &seeds[..2] // For index 0, we don't include the index byte + } else { + &seeds[..] + }; + + Pubkey::find_program_address(seeds, &compressed_token_program_id) +} + +/// Helper function to derive CPI authority PDA +pub fn get_cpi_authority_pda() -> Pubkey { + const CPI_AUTHORITY_PDA_SEED: &[u8] = b"cpi_authority"; + let compressed_token_program_id = Pubkey::from(light_ctoken_types::COMPRESSED_TOKEN_PROGRAM_ID); + + Pubkey::find_program_address(&[CPI_AUTHORITY_PDA_SEED], &compressed_token_program_id).0 +} 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 fcba4aeb35..08407267b3 100644 --- a/sdk-libs/token-client/src/instructions/mint_to_compressed.rs +++ b/sdk-libs/token-client/src/instructions/mint_to_compressed.rs @@ -17,7 +17,7 @@ use light_ctoken_types::{ use solana_instruction::Instruction; use solana_pubkey::Pubkey; -/// Creates a mint_to_compressed instruction that mints compressed tokens to recipients +/// Creates a mint_to_compressed instruction that mints compressed tokens to recipients, with Rpc. pub async fn mint_to_compressed_instruction( rpc: &mut R, spl_mint_pda: Pubkey, diff --git a/sdk-libs/token-client/src/instructions/mod.rs b/sdk-libs/token-client/src/instructions/mod.rs index a3b1af946f..7a692e0530 100644 --- a/sdk-libs/token-client/src/instructions/mod.rs +++ b/sdk-libs/token-client/src/instructions/mod.rs @@ -1,5 +1,6 @@ pub mod create_mint; pub mod create_spl_mint; +pub mod create_token_pool; pub mod mint_action; pub mod mint_to_compressed; pub mod transfer2; diff --git a/sdk-libs/token-client/src/lib.rs b/sdk-libs/token-client/src/lib.rs index 8f5d67d6dc..2b8f59f701 100644 --- a/sdk-libs/token-client/src/lib.rs +++ b/sdk-libs/token-client/src/lib.rs @@ -1,2 +1,68 @@ pub mod actions; pub mod instructions; + +use solana_pubkey::{pubkey, Pubkey}; + +pub const COMPRESSED_TOKEN_PROGRAM_ID: Pubkey = + pubkey!("cTokenmWW8bLPjZEBAUgYy3zKxQZW6VKi7bqNFEVv3m"); + +pub const COMPRESSED_TOKEN_CPI_AUTHORITY: Pubkey = + pubkey!("GXtd2izAiMJPwMEjfgTRH3d7k9mjn4Jq3JrWFv9gySYy"); + +pub mod compressed_token { + use light_compressed_account::address::derive_address; + use light_compressed_token_sdk::POOL_SEED; + use solana_pubkey::Pubkey; + + use super::{COMPRESSED_TOKEN_CPI_AUTHORITY, COMPRESSED_TOKEN_PROGRAM_ID}; + + pub const ID: Pubkey = COMPRESSED_TOKEN_PROGRAM_ID; + + /// Returns the program ID for the Compressed Token Program + pub fn id() -> Pubkey { + ID + } + /// Return the cpi authority pda of the Compressed Token Program. + pub fn cpi_authority() -> Pubkey { + COMPRESSED_TOKEN_CPI_AUTHORITY + } + + pub fn get_token_pool_address_and_bump(mint: &Pubkey) -> (Pubkey, u8) { + Pubkey::find_program_address(&[POOL_SEED, mint.as_ref()], &COMPRESSED_TOKEN_PROGRAM_ID) + } + /// Returns the associated ctoken address for a given owner and mint. + pub fn get_associated_ctoken_address(owner: &Pubkey, mint: &Pubkey) -> Pubkey { + Pubkey::find_program_address( + &[&owner.to_bytes(), &id().to_bytes(), &mint.to_bytes()], + &id(), + ) + .0 + } + /// Returns the associated ctoken address and bump for a given owner and mint. + pub fn get_associated_ctoken_address_and_bump(owner: &Pubkey, mint: &Pubkey) -> (Pubkey, u8) { + Pubkey::find_program_address( + &[&owner.to_bytes(), &id().to_bytes(), &mint.to_bytes()], + &id(), + ) + } + + pub const COMPRESSED_MINT_SEED: &[u8] = &[ + // b"compressed_mint" + 99, 111, 109, 112, 114, 101, 115, 115, 101, 100, 95, 109, 105, 110, 116, + ]; + + pub fn find_mint_address(mint_signer: Pubkey) -> (Pubkey, u8) { + Pubkey::find_program_address( + &[COMPRESSED_MINT_SEED, mint_signer.to_bytes().as_ref()], + &ID, + ) + } + + pub fn derive_compressed_mint_address(mint_address: Pubkey, address_tree: &Pubkey) -> [u8; 32] { + derive_address( + &mint_address.to_bytes(), + &address_tree.to_bytes(), + &ID.to_bytes(), + ) + } +} diff --git a/sdk-tests/anchor-compressible-derived/Cargo.toml b/sdk-tests/anchor-compressible-derived/Cargo.toml new file mode 100644 index 0000000000..854ee39f47 --- /dev/null +++ b/sdk-tests/anchor-compressible-derived/Cargo.toml @@ -0,0 +1,58 @@ +[package] +name = "anchor-compressible-derived" +version = "0.1.0" +description = "Anchor program template with user records and derived accounts" +edition = "2021" + +[lib] +crate-type = ["cdylib", "lib"] +name = "anchor_compressible_derived" + +[features] +no-entrypoint = [] +no-idl = [] +no-log-ix-name = [] +cpi = ["no-entrypoint"] +default = ["idl-build"] +idl-build = ["anchor-lang/idl-build", "light-sdk/idl-build"] + +test-sbf = [] + + +[dependencies] +light-sdk = { workspace = true, features = ["anchor", "idl-build", "v2", "anchor-discriminator-compat"] } +light-sdk-types = { workspace = true, features = ["v2"] } +light-hasher = { workspace = true, features = ["solana"] } +solana-program = { workspace = true } +light-macros = { workspace = true, features = ["solana"] } +light-sdk-macros = { workspace = true } +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 } +light-compressed-token-sdk = { workspace = true, features = ["anchor"] } +light-compressed-token-types = { workspace = true, features = ["anchor"] } + +[dev-dependencies] +light-program-test = { workspace = true, features = ["v2"] } +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 } +solana-program-error = { workspace = true } + +[lints.rust.unexpected_cfgs] +level = "allow" +check-cfg = [ + 'cfg(target_os, values("solana"))', + 'cfg(feature, values("frozen-abi", "no-entrypoint"))', +] diff --git a/sdk-tests/anchor-compressible-derived/README.md b/sdk-tests/anchor-compressible-derived/README.md new file mode 100644 index 0000000000..de24ffffcc --- /dev/null +++ b/sdk-tests/anchor-compressible-derived/README.md @@ -0,0 +1,278 @@ +# Example: Using the add_compressible_instructions Macro + +This example shows how to use the `add_compressible_instructions` macro to automatically generate compression-related instructions for your Anchor program. + +## Basic Setup + +```rust +use anchor_lang::prelude::*; +use light_sdk::{ + compressible::{CompressionInfo, HasCompressionInfo}, + derive_light_cpi_signer, LightDiscriminator, LightHasher, +}; +use light_sdk_macros::add_compressible_instructions; + +declare_id!("YourProgramId11111111111111111111111111111"); + +// Define your CPI signer +pub const LIGHT_CPI_SIGNER: CpiSigner = + derive_light_cpi_signer!("YourCpiSignerPubkey11111111111111111111111"); + +// Apply the macro to your program module +#[add_compressible_instructions(UserRecord, GameSession)] +#[program] +pub mod my_program { + use super::*; + + // The macro automatically generates these instructions: + // - create_compression_config (config management) + // - update_compression_config (config management) + // - compress_user_record (compress existing PDA) + // - compress_game_session (compress existing PDA) + // - decompress_multiple_pdas (decompress compressed accounts) + // + // NOTE: create_user_record and create_game_session are NOT generated + // because they typically need custom initialization logic + + // You can still add your own custom instructions here +} +``` + +## Define Your Account Structures + +```rust +#[derive(Debug, LightHasher, LightDiscriminator, Default)] +#[account] +pub struct UserRecord { + #[skip] // Skip compression_info from hashing + pub compression_info: CompressionInfo, + #[hash] // Include in hash + pub owner: Pubkey, + #[hash] + pub name: String, + pub score: u64, +} + +// Implement the required trait +impl HasCompressionInfo for UserRecord { + fn compression_info(&self) -> &CompressionInfo { + &self.compression_info + } + + fn compression_info_mut(&mut self) -> &mut CompressionInfo { + &mut self.compression_info + } +} +``` + +## Generated Instructions + +### 1. Config Management + +```typescript +// Create config (only program upgrade authority can call) +await program.methods + .createCompressibleConfig( + 100, // compression_delay + rentRecipient, + [addressSpace] // Now accepts an array of address trees (1-4 allowed) + ) + .accounts({ + payer: wallet.publicKey, + config: configPda, + programData: programDataPda, + authority: upgradeAuthority, + systemProgram: SystemProgram.programId, + }) + .signers([upgradeAuthority]) + .rpc(); + +// Update config +await program.methods + .updateCompressibleConfig( + 200, // new_compression_delay (optional) + newRentRecipient, // (optional) + [newAddressSpace1, newAddressSpace2], // (optional) - array of 1-4 address trees + newUpdateAuthority // (optional) + ) + .accounts({ + config: configPda, + authority: configUpdateAuthority, + }) + .signers([configUpdateAuthority]) + .rpc(); +``` + +### 2. Compress Existing PDA + +```typescript +await program.methods + .compressUserRecord(proof, compressedAccountMeta) + .accounts({ + user: user.publicKey, + pdaAccount: userRecordPda, + systemProgram: SystemProgram.programId, + config: configPda, + rentRecipient: rentRecipient, + }) + .remainingAccounts(lightSystemAccounts) + .signers([user]) + .rpc(); +``` + +### 3. Decompress Multiple PDAs + +```typescript +const compressedAccounts = [ + { + meta: compressedAccountMeta1, + data: { userRecord: userData }, + seeds: [Buffer.from("user_record"), user.publicKey.toBuffer()], + }, + { + meta: compressedAccountMeta2, + data: { gameSession: gameData }, + seeds: [ + Buffer.from("game_session"), + sessionId.toArrayLike(Buffer, "le", 8), + ], + }, +]; + +await program.methods + .decompressMultiplePdas( + proof, + compressedAccounts, + [userBump, gameBump], // PDA bumps + systemAccountsOffset + ) + .accounts({ + feePayer: payer.publicKey, + rentPayer: payer.publicKey, + systemProgram: SystemProgram.programId, + }) + .remainingAccounts([ + ...pdaAccounts, // PDAs to decompress into + ...lightSystemAccounts, // Light Protocol system accounts + ]) + .signers([payer]) + .rpc(); +``` + +## Address Space Configuration + +The config now supports multiple address trees per address space (1-4 allowed): + +```typescript +// Single address tree (backward compatible) +const addressSpace = [addressTree1]; + +// Multiple address trees for better scalability +const addressSpace = [addressTree1, addressTree2, addressTree3]; + +// When creating config +await program.methods + .createCompressibleConfig( + 100, + rentRecipient, + addressSpace // Array of 1-4 unique address tree pubkeys + ) + // ... accounts + .rpc(); +``` + +### Address Space Validation Rules + +**Create Config:** + +- Must contain 1-4 unique address tree pubkeys +- No duplicate pubkeys allowed +- All pubkeys must be valid address trees + +**Update Config:** + +- Can only **add** new address trees, never remove existing ones +- No duplicate pubkeys allowed in the new configuration +- Must maintain all existing address trees + +```typescript +// Valid update: adding new trees +const currentAddressSpace = [tree1, tree2]; +const newAddressSpace = [tree1, tree2, tree3]; // ✅ Valid: adds tree3 + +// Invalid update: removing existing trees +const invalidAddressSpace = [tree2, tree3]; // ❌ Invalid: removes tree1 +``` + +The system validates that compressed accounts use address trees from the configured address space, providing flexibility while maintaining security and preventing accidental removal of active trees. + +## What You Need to Implement + +Since the macro only generates compression-related instructions, you need to implement: + +### 1. Create Instructions + +Implement your own create instructions for each account type: + +```rust +#[derive(Accounts)] +pub struct CreateUserRecord<'info> { + #[account(mut)] + pub user: Signer<'info>, + #[account( + init, + payer = user, + space = 8 + UserRecord::INIT_SPACE, + seeds = [b"user_record", user.key().as_ref()], + bump, + )] + pub user_record: Account<'info, UserRecord>, + pub system_program: Program<'info, System>, +} + +pub fn create_user_record( + ctx: Context, + name: String, +) -> Result<()> { + let user_record = &mut ctx.accounts.user_record; + + // Your custom initialization logic here + user_record.compression_info = CompressionInfo::new_decompressed()?; + user_record.owner = ctx.accounts.user.key(); + user_record.name = name; + user_record.score = 0; + + Ok(()) +} +``` + +### 2. Update Instructions + +Implement update instructions for your account types with your custom business logic. + +## Customization + +### Custom Seeds + +Use custom seeds in your PDA derivation and pass them in the `seeds` parameter when decompressing: + +```rust +seeds = [b"custom_prefix", user.key().as_ref(), &session_id.to_le_bytes()] +``` + +## Best Practices + +1. **Create Config Early**: Create the config immediately after program deployment +2. **Use Config Values**: Always use config values instead of hardcoded constants +3. **Validate Rent Recipient**: The macro automatically validates rent recipient matches config +4. **Handle Compression Timing**: Respect the compression delay from config +5. **Batch Operations**: Use decompress_multiple_pdas for efficiency + +## Migration from Manual Implementation + +If migrating from a manual implementation: + +1. Update your account structs to use `CompressionInfo` instead of separate fields +2. Implement the `HasCompressionInfo` trait +3. Replace your manual instructions with the macro +4. Update client code to use the new instruction names diff --git a/sdk-tests/anchor-compressible-derived/Xargo.toml b/sdk-tests/anchor-compressible-derived/Xargo.toml new file mode 100644 index 0000000000..9e7d95be7f --- /dev/null +++ b/sdk-tests/anchor-compressible-derived/Xargo.toml @@ -0,0 +1,2 @@ +[target.bpfel-unknown-unknown.dependencies.std] +features = [] \ No newline at end of file diff --git a/sdk-tests/anchor-compressible-derived/expanded_macro-f.rs b/sdk-tests/anchor-compressible-derived/expanded_macro-f.rs new file mode 100644 index 0000000000..2010d1c549 --- /dev/null +++ b/sdk-tests/anchor-compressible-derived/expanded_macro-f.rs @@ -0,0 +1,16331 @@ +#![feature(prelude_import)] +#[prelude_import] +use std::prelude::rust_2021::*; +#[macro_use] +extern crate std; +use anchor_lang::{ + prelude::*, + solana_program::{ + instruction::AccountMeta, program::{invoke, invoke_signed}, + pubkey::Pubkey, + }, +}; +use anchor_spl::token_interface::TokenAccount; +use light_ctoken_types::{ + instructions::mint_action::CompressedMintWithContext, COMPRESSED_TOKEN_PROGRAM_ID, +}; +use light_sdk::{ + add_compressible_instructions_enhanced, compressed_account_variant, + compressible::{ + compress_account_on_init, compress_empty_account_on_init, + prepare_account_for_decompression_idempotent, + prepare_accounts_for_compression_on_init, + process_initialize_compression_config_checked, process_update_compression_config, + CompressibleConfig, CompressionInfo, HasCompressionInfo, Unpack, + }, + cpi::CpiInputs, derive_light_cpi_signer, + instruction::{ + account_meta::CompressedAccountMetaNoLamportsNoAddress, PackedAddressTreeInfo, + ValidityProof, + }, + LightDiscriminator, +}; +use light_sdk_types::{CpiAccountsConfig, CpiAccountsSmall, CpiSigner}; +pub mod instructions { + pub mod create_record { + use anchor_lang::prelude::*; + use crate::state::UserRecord; + pub struct CreateRecord<'info> { + #[account(mut)] + pub user: Signer<'info>, + #[account( + init, + payer = user, + space = 8+32+4+32+8+10, + seeds = [b"user_record", + user.key().as_ref()], + bump, + )] + pub user_record: Account<'info, UserRecord>, + /// UNCHECKED: checked via config. + #[account(mut)] + pub rent_recipient: AccountInfo<'info>, + /// The global config account + /// UNCHECKED: checked via load_checked. + pub config: AccountInfo<'info>, + pub system_program: Program<'info, System>, + } + #[automatically_derived] + impl<'info> anchor_lang::Accounts<'info, CreateRecordBumps> + for CreateRecord<'info> + where + 'info: 'info, + { + #[inline(never)] + fn try_accounts( + __program_id: &anchor_lang::solana_program::pubkey::Pubkey, + __accounts: &mut &'info [anchor_lang::solana_program::account_info::AccountInfo< + 'info, + >], + __ix_data: &[u8], + __bumps: &mut CreateRecordBumps, + __reallocs: &mut std::collections::BTreeSet< + anchor_lang::solana_program::pubkey::Pubkey, + >, + ) -> anchor_lang::Result { + let user: Signer = anchor_lang::Accounts::try_accounts( + __program_id, + __accounts, + __ix_data, + __bumps, + __reallocs, + ) + .map_err(|e| e.with_account_name("user"))?; + if __accounts.is_empty() { + return Err( + anchor_lang::error::ErrorCode::AccountNotEnoughKeys.into(), + ); + } + let user_record = &__accounts[0]; + *__accounts = &__accounts[1..]; + let rent_recipient: AccountInfo = anchor_lang::Accounts::try_accounts( + __program_id, + __accounts, + __ix_data, + __bumps, + __reallocs, + ) + .map_err(|e| e.with_account_name("rent_recipient"))?; + let config: AccountInfo = anchor_lang::Accounts::try_accounts( + __program_id, + __accounts, + __ix_data, + __bumps, + __reallocs, + ) + .map_err(|e| e.with_account_name("config"))?; + let system_program: anchor_lang::accounts::program::Program = anchor_lang::Accounts::try_accounts( + __program_id, + __accounts, + __ix_data, + __bumps, + __reallocs, + ) + .map_err(|e| e.with_account_name("system_program"))?; + let __anchor_rent = Rent::get()?; + let (__pda_address, __bump) = Pubkey::find_program_address( + &[b"user_record", user.key().as_ref()], + __program_id, + ); + __bumps.user_record = __bump; + if user_record.key() != __pda_address { + return Err( + anchor_lang::error::Error::from( + anchor_lang::error::ErrorCode::ConstraintSeeds, + ) + .with_account_name("user_record") + .with_pubkeys((user_record.key(), __pda_address)), + ); + } + let user_record = ({ + #[inline(never)] + || { + let actual_field = AsRef::::as_ref(&user_record); + let actual_owner = actual_field.owner; + let space = 8 + 32 + 4 + 32 + 8 + 10; + let pa: anchor_lang::accounts::account::Account = if !false + || actual_owner + == &anchor_lang::solana_program::system_program::ID + { + let __current_lamports = user_record.lamports(); + if __current_lamports == 0 { + let space = space; + let lamports = __anchor_rent.minimum_balance(space); + let cpi_accounts = anchor_lang::system_program::CreateAccount { + from: user.to_account_info(), + to: user_record.to_account_info(), + }; + let cpi_context = anchor_lang::context::CpiContext::new( + system_program.to_account_info(), + cpi_accounts, + ); + anchor_lang::system_program::create_account( + cpi_context + .with_signer( + &[&[b"user_record", user.key().as_ref(), &[__bump][..]][..]], + ), + lamports, + space as u64, + __program_id, + )?; + } else { + if user.key() == user_record.key() { + return Err( + anchor_lang::error::Error::from(anchor_lang::error::AnchorError { + error_name: anchor_lang::error::ErrorCode::TryingToInitPayerAsProgramAccount + .name(), + error_code_number: anchor_lang::error::ErrorCode::TryingToInitPayerAsProgramAccount + .into(), + error_msg: anchor_lang::error::ErrorCode::TryingToInitPayerAsProgramAccount + .to_string(), + error_origin: Some( + anchor_lang::error::ErrorOrigin::Source(anchor_lang::error::Source { + filename: "sdk-tests/anchor-compressible-derived/src/instructions/create_record.rs", + line: 6u32, + }), + ), + compared_values: None, + }) + .with_pubkeys((user.key(), user_record.key())), + ); + } + let required_lamports = __anchor_rent + .minimum_balance(space) + .max(1) + .saturating_sub(__current_lamports); + if required_lamports > 0 { + let cpi_accounts = anchor_lang::system_program::Transfer { + from: user.to_account_info(), + to: user_record.to_account_info(), + }; + let cpi_context = anchor_lang::context::CpiContext::new( + system_program.to_account_info(), + cpi_accounts, + ); + anchor_lang::system_program::transfer( + cpi_context, + required_lamports, + )?; + } + let cpi_accounts = anchor_lang::system_program::Allocate { + account_to_allocate: user_record.to_account_info(), + }; + let cpi_context = anchor_lang::context::CpiContext::new( + system_program.to_account_info(), + cpi_accounts, + ); + anchor_lang::system_program::allocate( + cpi_context + .with_signer( + &[&[b"user_record", user.key().as_ref(), &[__bump][..]][..]], + ), + space as u64, + )?; + let cpi_accounts = anchor_lang::system_program::Assign { + account_to_assign: user_record.to_account_info(), + }; + let cpi_context = anchor_lang::context::CpiContext::new( + system_program.to_account_info(), + cpi_accounts, + ); + anchor_lang::system_program::assign( + cpi_context + .with_signer( + &[&[b"user_record", user.key().as_ref(), &[__bump][..]][..]], + ), + __program_id, + )?; + } + match anchor_lang::accounts::account::Account::try_from_unchecked( + &user_record, + ) { + Ok(val) => val, + Err(e) => return Err(e.with_account_name("user_record")), + } + } else { + match anchor_lang::accounts::account::Account::try_from( + &user_record, + ) { + Ok(val) => val, + Err(e) => return Err(e.with_account_name("user_record")), + } + }; + if false { + if space != actual_field.data_len() { + return Err( + anchor_lang::error::Error::from( + anchor_lang::error::ErrorCode::ConstraintSpace, + ) + .with_account_name("user_record") + .with_values((space, actual_field.data_len())), + ); + } + if actual_owner != __program_id { + return Err( + anchor_lang::error::Error::from( + anchor_lang::error::ErrorCode::ConstraintOwner, + ) + .with_account_name("user_record") + .with_pubkeys((*actual_owner, *__program_id)), + ); + } + { + let required_lamports = __anchor_rent + .minimum_balance(space); + if pa.to_account_info().lamports() < required_lamports { + return Err( + anchor_lang::error::Error::from( + anchor_lang::error::ErrorCode::ConstraintRentExempt, + ) + .with_account_name("user_record"), + ); + } + } + } + Ok(pa) + } + })()?; + if !AsRef::::as_ref(&user_record).is_writable { + return Err( + anchor_lang::error::Error::from( + anchor_lang::error::ErrorCode::ConstraintMut, + ) + .with_account_name("user_record"), + ); + } + if !__anchor_rent + .is_exempt( + user_record.to_account_info().lamports(), + user_record.to_account_info().try_data_len()?, + ) + { + return Err( + anchor_lang::error::Error::from( + anchor_lang::error::ErrorCode::ConstraintRentExempt, + ) + .with_account_name("user_record"), + ); + } + if !AsRef::::as_ref(&user).is_writable { + return Err( + anchor_lang::error::Error::from( + anchor_lang::error::ErrorCode::ConstraintMut, + ) + .with_account_name("user"), + ); + } + if !&rent_recipient.is_writable { + return Err( + anchor_lang::error::Error::from( + anchor_lang::error::ErrorCode::ConstraintMut, + ) + .with_account_name("rent_recipient"), + ); + } + Ok(CreateRecord { + user, + user_record, + rent_recipient, + config, + system_program, + }) + } + } + #[automatically_derived] + impl<'info> anchor_lang::ToAccountInfos<'info> for CreateRecord<'info> + where + 'info: 'info, + { + fn to_account_infos( + &self, + ) -> Vec> { + let mut account_infos = ::alloc::vec::Vec::new(); + account_infos.extend(self.user.to_account_infos()); + account_infos.extend(self.user_record.to_account_infos()); + account_infos.extend(self.rent_recipient.to_account_infos()); + account_infos.extend(self.config.to_account_infos()); + account_infos.extend(self.system_program.to_account_infos()); + account_infos + } + } + #[automatically_derived] + impl<'info> anchor_lang::ToAccountMetas for CreateRecord<'info> { + fn to_account_metas( + &self, + is_signer: Option, + ) -> Vec { + let mut account_metas = ::alloc::vec::Vec::new(); + account_metas.extend(self.user.to_account_metas(None)); + account_metas.extend(self.user_record.to_account_metas(None)); + account_metas.extend(self.rent_recipient.to_account_metas(None)); + account_metas.extend(self.config.to_account_metas(None)); + account_metas.extend(self.system_program.to_account_metas(None)); + account_metas + } + } + #[automatically_derived] + impl<'info> anchor_lang::AccountsExit<'info> for CreateRecord<'info> + where + 'info: 'info, + { + fn exit( + &self, + program_id: &anchor_lang::solana_program::pubkey::Pubkey, + ) -> anchor_lang::Result<()> { + anchor_lang::AccountsExit::exit(&self.user, program_id) + .map_err(|e| e.with_account_name("user"))?; + anchor_lang::AccountsExit::exit(&self.user_record, program_id) + .map_err(|e| e.with_account_name("user_record"))?; + anchor_lang::AccountsExit::exit(&self.rent_recipient, program_id) + .map_err(|e| e.with_account_name("rent_recipient"))?; + Ok(()) + } + } + pub struct CreateRecordBumps { + pub user_record: u8, + } + #[automatically_derived] + impl ::core::fmt::Debug for CreateRecordBumps { + #[inline] + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + ::core::fmt::Formatter::debug_struct_field1_finish( + f, + "CreateRecordBumps", + "user_record", + &&self.user_record, + ) + } + } + impl Default for CreateRecordBumps { + fn default() -> Self { + CreateRecordBumps { + user_record: u8::MAX, + } + } + } + impl<'info> anchor_lang::Bumps for CreateRecord<'info> + where + 'info: 'info, + { + type Bumps = CreateRecordBumps; + } + /// An internal, Anchor generated module. This is used (as an + /// implementation detail), to generate a struct for a given + /// `#[derive(Accounts)]` implementation, where each field is a Pubkey, + /// instead of an `AccountInfo`. This is useful for clients that want + /// to generate a list of accounts, without explicitly knowing the + /// order all the fields should be in. + /// + /// To access the struct in this module, one should use the sibling + /// `accounts` module (also generated), which re-exports this. + pub(crate) mod __client_accounts_create_record { + use super::*; + use anchor_lang::prelude::borsh; + /// Generated client accounts for [`CreateRecord`]. + pub struct CreateRecord { + pub user: Pubkey, + pub user_record: Pubkey, + ///UNCHECKED: checked via config. + pub rent_recipient: Pubkey, + ///The global config account + ///UNCHECKED: checked via load_checked. + pub config: Pubkey, + pub system_program: Pubkey, + } + impl borsh::ser::BorshSerialize for CreateRecord + where + Pubkey: borsh::ser::BorshSerialize, + Pubkey: borsh::ser::BorshSerialize, + Pubkey: borsh::ser::BorshSerialize, + Pubkey: borsh::ser::BorshSerialize, + Pubkey: borsh::ser::BorshSerialize, + { + fn serialize( + &self, + writer: &mut W, + ) -> ::core::result::Result<(), borsh::maybestd::io::Error> { + borsh::BorshSerialize::serialize(&self.user, writer)?; + borsh::BorshSerialize::serialize(&self.user_record, writer)?; + borsh::BorshSerialize::serialize(&self.rent_recipient, writer)?; + borsh::BorshSerialize::serialize(&self.config, writer)?; + borsh::BorshSerialize::serialize(&self.system_program, writer)?; + Ok(()) + } + } + impl anchor_lang::idl::build::IdlBuild for CreateRecord { + fn create_type() -> Option { + Some(anchor_lang::idl::types::IdlTypeDef { + name: Self::get_full_path(), + docs: <[_]>::into_vec( + ::alloc::boxed::box_new([ + "Generated client accounts for [`CreateRecord`].".into(), + ]), + ), + serialization: anchor_lang::idl::types::IdlSerialization::default(), + repr: None, + generics: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlTypeDefTy::Struct { + fields: Some( + anchor_lang::idl::types::IdlDefinedFields::Named( + <[_]>::into_vec( + ::alloc::boxed::box_new([ + anchor_lang::idl::types::IdlField { + name: "user".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::Pubkey, + }, + anchor_lang::idl::types::IdlField { + name: "user_record".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::Pubkey, + }, + anchor_lang::idl::types::IdlField { + name: "rent_recipient".into(), + docs: <[_]>::into_vec( + ::alloc::boxed::box_new([ + "UNCHECKED: checked via config.".into(), + ]), + ), + ty: anchor_lang::idl::types::IdlType::Pubkey, + }, + anchor_lang::idl::types::IdlField { + name: "config".into(), + docs: <[_]>::into_vec( + ::alloc::boxed::box_new([ + "The global config account".into(), + "UNCHECKED: checked via load_checked.".into(), + ]), + ), + ty: anchor_lang::idl::types::IdlType::Pubkey, + }, + anchor_lang::idl::types::IdlField { + name: "system_program".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::Pubkey, + }, + ]), + ), + ), + ), + }, + }) + } + fn insert_types( + types: &mut std::collections::BTreeMap< + String, + anchor_lang::idl::types::IdlTypeDef, + >, + ) {} + fn get_full_path() -> String { + ::alloc::__export::must_use({ + let res = ::alloc::fmt::format( + format_args!( + "{0}::{1}", + "anchor_compressible_derived::instructions::create_record::__client_accounts_create_record", + "CreateRecord", + ), + ); + res + }) + } + } + #[automatically_derived] + impl anchor_lang::ToAccountMetas for CreateRecord { + fn to_account_metas( + &self, + is_signer: Option, + ) -> Vec { + let mut account_metas = ::alloc::vec::Vec::new(); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new( + self.user, + true, + ), + ); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new( + self.user_record, + false, + ), + ); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new( + self.rent_recipient, + false, + ), + ); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new_readonly( + self.config, + false, + ), + ); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new_readonly( + self.system_program, + false, + ), + ); + account_metas + } + } + } + /// An internal, Anchor generated module. This is used (as an + /// implementation detail), to generate a CPI struct for a given + /// `#[derive(Accounts)]` implementation, where each field is an + /// AccountInfo. + /// + /// To access the struct in this module, one should use the sibling + /// [`cpi::accounts`] module (also generated), which re-exports this. + pub(crate) mod __cpi_client_accounts_create_record { + use super::*; + /// Generated CPI struct of the accounts for [`CreateRecord`]. + pub struct CreateRecord<'info> { + pub user: anchor_lang::solana_program::account_info::AccountInfo<'info>, + pub user_record: anchor_lang::solana_program::account_info::AccountInfo< + 'info, + >, + ///UNCHECKED: checked via config. + pub rent_recipient: anchor_lang::solana_program::account_info::AccountInfo< + 'info, + >, + ///The global config account + ///UNCHECKED: checked via load_checked. + pub config: anchor_lang::solana_program::account_info::AccountInfo< + 'info, + >, + pub system_program: anchor_lang::solana_program::account_info::AccountInfo< + 'info, + >, + } + #[automatically_derived] + impl<'info> anchor_lang::ToAccountMetas for CreateRecord<'info> { + fn to_account_metas( + &self, + is_signer: Option, + ) -> Vec { + let mut account_metas = ::alloc::vec::Vec::new(); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new( + anchor_lang::Key::key(&self.user), + true, + ), + ); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new( + anchor_lang::Key::key(&self.user_record), + false, + ), + ); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new( + anchor_lang::Key::key(&self.rent_recipient), + false, + ), + ); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new_readonly( + anchor_lang::Key::key(&self.config), + false, + ), + ); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new_readonly( + anchor_lang::Key::key(&self.system_program), + false, + ), + ); + account_metas + } + } + #[automatically_derived] + impl<'info> anchor_lang::ToAccountInfos<'info> for CreateRecord<'info> { + fn to_account_infos( + &self, + ) -> Vec> { + let mut account_infos = ::alloc::vec::Vec::new(); + account_infos + .extend( + anchor_lang::ToAccountInfos::to_account_infos(&self.user), + ); + account_infos + .extend( + anchor_lang::ToAccountInfos::to_account_infos( + &self.user_record, + ), + ); + account_infos + .extend( + anchor_lang::ToAccountInfos::to_account_infos( + &self.rent_recipient, + ), + ); + account_infos + .extend( + anchor_lang::ToAccountInfos::to_account_infos(&self.config), + ); + account_infos + .extend( + anchor_lang::ToAccountInfos::to_account_infos( + &self.system_program, + ), + ); + account_infos + } + } + } + impl<'info> CreateRecord<'info> { + pub fn __anchor_private_gen_idl_accounts( + accounts: &mut std::collections::BTreeMap< + String, + anchor_lang::idl::types::IdlAccount, + >, + types: &mut std::collections::BTreeMap< + String, + anchor_lang::idl::types::IdlTypeDef, + >, + ) -> Vec { + if let Some(ty) = ::create_type() { + let account = anchor_lang::idl::types::IdlAccount { + name: ty.name.clone(), + discriminator: UserRecord::DISCRIMINATOR.into(), + }; + accounts.insert(account.name.clone(), account); + types.insert(ty.name.clone(), ty); + ::insert_types(types); + } + <[_]>::into_vec( + ::alloc::boxed::box_new([ + anchor_lang::idl::types::IdlInstructionAccountItem::Single(anchor_lang::idl::types::IdlInstructionAccount { + name: "user".into(), + docs: ::alloc::vec::Vec::new(), + writable: true, + signer: true, + optional: false, + address: None, + pda: None, + relations: ::alloc::vec::Vec::new(), + }), + anchor_lang::idl::types::IdlInstructionAccountItem::Single(anchor_lang::idl::types::IdlInstructionAccount { + name: "user_record".into(), + docs: ::alloc::vec::Vec::new(), + writable: true, + signer: false, + optional: false, + address: None, + pda: None, + relations: ::alloc::vec::Vec::new(), + }), + anchor_lang::idl::types::IdlInstructionAccountItem::Single(anchor_lang::idl::types::IdlInstructionAccount { + name: "rent_recipient".into(), + docs: <[_]>::into_vec( + ::alloc::boxed::box_new([ + "UNCHECKED: checked via config.".into(), + ]), + ), + writable: true, + signer: false, + optional: false, + address: None, + pda: None, + relations: ::alloc::vec::Vec::new(), + }), + anchor_lang::idl::types::IdlInstructionAccountItem::Single(anchor_lang::idl::types::IdlInstructionAccount { + name: "config".into(), + docs: <[_]>::into_vec( + ::alloc::boxed::box_new([ + "The global config account".into(), + "UNCHECKED: checked via load_checked.".into(), + ]), + ), + writable: false, + signer: false, + optional: false, + address: None, + pda: None, + relations: ::alloc::vec::Vec::new(), + }), + anchor_lang::idl::types::IdlInstructionAccountItem::Single(anchor_lang::idl::types::IdlInstructionAccount { + name: "system_program".into(), + docs: ::alloc::vec::Vec::new(), + writable: false, + signer: false, + optional: false, + address: None, + pda: None, + relations: ::alloc::vec::Vec::new(), + }), + ]), + ) + } + } + } + pub use create_record::*; +} +pub mod state { + use anchor_lang::prelude::*; + use light_sdk::{compressible::CompressionInfo, LightDiscriminator, LightHasher}; + use light_sdk::{Compressible, CompressiblePack}; + #[light_seeds(b"user_record", owner.as_ref())] + pub struct UserRecord { + #[skip] + pub compression_info: Option, + #[hash] + pub owner: Pubkey, + #[hash] + #[max_len(32)] + pub name: String, + pub score: u64, + } + impl borsh::ser::BorshSerialize for UserRecord + where + Option: borsh::ser::BorshSerialize, + Pubkey: borsh::ser::BorshSerialize, + String: borsh::ser::BorshSerialize, + u64: borsh::ser::BorshSerialize, + { + fn serialize( + &self, + writer: &mut W, + ) -> ::core::result::Result<(), borsh::maybestd::io::Error> { + borsh::BorshSerialize::serialize(&self.compression_info, writer)?; + borsh::BorshSerialize::serialize(&self.owner, writer)?; + borsh::BorshSerialize::serialize(&self.name, writer)?; + borsh::BorshSerialize::serialize(&self.score, writer)?; + Ok(()) + } + } + impl anchor_lang::idl::build::IdlBuild for UserRecord { + fn create_type() -> Option { + Some(anchor_lang::idl::types::IdlTypeDef { + name: Self::get_full_path(), + docs: ::alloc::vec::Vec::new(), + serialization: anchor_lang::idl::types::IdlSerialization::default(), + repr: None, + generics: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlTypeDefTy::Struct { + fields: Some( + anchor_lang::idl::types::IdlDefinedFields::Named( + <[_]>::into_vec( + ::alloc::boxed::box_new([ + anchor_lang::idl::types::IdlField { + name: "compression_info".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::Option( + Box::new(anchor_lang::idl::types::IdlType::Defined { + name: ::get_full_path(), + generics: ::alloc::vec::Vec::new(), + }), + ), + }, + anchor_lang::idl::types::IdlField { + name: "owner".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::Pubkey, + }, + anchor_lang::idl::types::IdlField { + name: "name".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::String, + }, + anchor_lang::idl::types::IdlField { + name: "score".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::U64, + }, + ]), + ), + ), + ), + }, + }) + } + fn insert_types( + types: &mut std::collections::BTreeMap< + String, + anchor_lang::idl::types::IdlTypeDef, + >, + ) { + if let Some(ty) = ::create_type() { + types.insert(::get_full_path(), ty); + ::insert_types(types); + } + } + fn get_full_path() -> String { + ::alloc::__export::must_use({ + let res = ::alloc::fmt::format( + format_args!( + "{0}::{1}", + "anchor_compressible_derived::state", + "UserRecord", + ), + ); + res + }) + } + } + impl borsh::de::BorshDeserialize for UserRecord + where + Option: borsh::BorshDeserialize, + Pubkey: borsh::BorshDeserialize, + String: borsh::BorshDeserialize, + u64: borsh::BorshDeserialize, + { + fn deserialize_reader( + reader: &mut R, + ) -> ::core::result::Result { + Ok(Self { + compression_info: borsh::BorshDeserialize::deserialize_reader(reader)?, + owner: borsh::BorshDeserialize::deserialize_reader(reader)?, + name: borsh::BorshDeserialize::deserialize_reader(reader)?, + score: borsh::BorshDeserialize::deserialize_reader(reader)?, + }) + } + } + #[automatically_derived] + impl ::core::clone::Clone for UserRecord { + #[inline] + fn clone(&self) -> UserRecord { + UserRecord { + compression_info: ::core::clone::Clone::clone(&self.compression_info), + owner: ::core::clone::Clone::clone(&self.owner), + name: ::core::clone::Clone::clone(&self.name), + score: ::core::clone::Clone::clone(&self.score), + } + } + } + #[automatically_derived] + impl anchor_lang::AccountSerialize for UserRecord { + fn try_serialize( + &self, + writer: &mut W, + ) -> anchor_lang::Result<()> { + if writer.write_all(UserRecord::DISCRIMINATOR).is_err() { + return Err(anchor_lang::error::ErrorCode::AccountDidNotSerialize.into()); + } + if AnchorSerialize::serialize(self, writer).is_err() { + return Err(anchor_lang::error::ErrorCode::AccountDidNotSerialize.into()); + } + Ok(()) + } + } + #[automatically_derived] + impl anchor_lang::AccountDeserialize for UserRecord { + fn try_deserialize(buf: &mut &[u8]) -> anchor_lang::Result { + if buf.len() < UserRecord::DISCRIMINATOR.len() { + return Err( + anchor_lang::error::ErrorCode::AccountDiscriminatorNotFound.into(), + ); + } + let given_disc = &buf[..UserRecord::DISCRIMINATOR.len()]; + if UserRecord::DISCRIMINATOR != given_disc { + return Err( + anchor_lang::error::Error::from(anchor_lang::error::AnchorError { + error_name: anchor_lang::error::ErrorCode::AccountDiscriminatorMismatch + .name(), + error_code_number: anchor_lang::error::ErrorCode::AccountDiscriminatorMismatch + .into(), + error_msg: anchor_lang::error::ErrorCode::AccountDiscriminatorMismatch + .to_string(), + error_origin: Some( + anchor_lang::error::ErrorOrigin::Source(anchor_lang::error::Source { + filename: "sdk-tests/anchor-compressible-derived/src/state.rs", + line: 9u32, + }), + ), + compared_values: None, + }) + .with_account_name("UserRecord"), + ); + } + Self::try_deserialize_unchecked(buf) + } + fn try_deserialize_unchecked(buf: &mut &[u8]) -> anchor_lang::Result { + let mut data: &[u8] = &buf[UserRecord::DISCRIMINATOR.len()..]; + AnchorDeserialize::deserialize(&mut data) + .map_err(|_| { + anchor_lang::error::ErrorCode::AccountDidNotDeserialize.into() + }) + } + } + #[automatically_derived] + impl anchor_lang::Discriminator for UserRecord { + const DISCRIMINATOR: &'static [u8] = &[210, 252, 132, 218, 191, 85, 173, 167]; + } + #[automatically_derived] + impl anchor_lang::Owner for UserRecord { + fn owner() -> Pubkey { + crate::ID + } + } + #[automatically_derived] + impl ::core::fmt::Debug for UserRecord { + #[inline] + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + ::core::fmt::Formatter::debug_struct_field4_finish( + f, + "UserRecord", + "compression_info", + &self.compression_info, + "owner", + &self.owner, + "name", + &self.name, + "score", + &&self.score, + ) + } + } + impl ::light_hasher::to_byte_array::ToByteArray for UserRecord { + const NUM_FIELDS: usize = 4usize; + fn to_byte_array( + &self, + ) -> ::std::result::Result<[u8; 32], ::light_hasher::HasherError> { + use ::light_hasher::to_byte_array::ToByteArray; + use ::light_hasher::hash_to_field_size::HashToFieldSize; + use ::light_hasher::Hasher; + let mut result = ::light_hasher::Poseidon::hashv( + &[ + ::light_hasher::hash_to_field_size::hash_to_bn254_field_size_be( + self.owner.as_ref(), + ) + .as_slice(), + self.name.hash_to_field_size()?.as_slice(), + self.score.to_byte_array()?.as_slice(), + ], + )?; + if ::light_hasher::Poseidon::ID != 0 { + result[0] = 0; + } + Ok(result) + } + } + impl ::light_hasher::DataHasher for UserRecord { + fn hash(&self) -> ::std::result::Result<[u8; 32], ::light_hasher::HasherError> + where + H: ::light_hasher::Hasher, + { + use ::light_hasher::DataHasher; + use ::light_hasher::Hasher; + use ::light_hasher::to_byte_array::ToByteArray; + use ::light_hasher::hash_to_field_size::HashToFieldSize; + #[cfg(debug_assertions)] + { + if std::env::var("RUST_BACKTRACE").is_ok() { + let debug_prints: Vec<[u8; 32]> = <[_]>::into_vec( + ::alloc::boxed::box_new([ + ::light_hasher::hash_to_field_size::hash_to_bn254_field_size_be( + self.owner.as_ref(), + ), + self.name.hash_to_field_size()?, + self.score.to_byte_array()?, + ]), + ); + { + ::std::io::_print( + format_args!("DataHasher::hash inputs {0:?}\n", debug_prints), + ); + }; + } + } + let mut result = H::hashv( + &[ + ::light_hasher::hash_to_field_size::hash_to_bn254_field_size_be( + self.owner.as_ref(), + ) + .as_slice(), + self.name.hash_to_field_size()?.as_slice(), + self.score.to_byte_array()?.as_slice(), + ], + )?; + if H::ID != 0 { + result[0] = 0; + } + Ok(result) + } + } + impl LightDiscriminator for UserRecord { + const LIGHT_DISCRIMINATOR: [u8; 8] = [210, 252, 132, 218, 191, 85, 173, 167]; + const LIGHT_DISCRIMINATOR_SLICE: &'static [u8] = &Self::LIGHT_DISCRIMINATOR; + fn discriminator() -> [u8; 8] { + Self::LIGHT_DISCRIMINATOR + } + } + impl light_sdk::compressible::HasCompressionInfo for UserRecord { + fn compression_info(&self) -> &light_sdk::compressible::CompressionInfo { + self.compression_info + .as_ref() + .expect("CompressionInfo must be Some on-chain") + } + fn compression_info_mut( + &mut self, + ) -> &mut light_sdk::compressible::CompressionInfo { + self.compression_info + .as_mut() + .expect("CompressionInfo must be Some on-chain") + } + fn compression_info_mut_opt( + &mut self, + ) -> &mut Option { + &mut self.compression_info + } + fn set_compression_info_none(&mut self) { + self.compression_info = None; + } + } + impl light_sdk::account::Size for UserRecord { + fn size(&self) -> usize { + Self::LIGHT_DISCRIMINATOR.len() + Self::INIT_SPACE + } + } + impl light_sdk::compressible::CompressAs for UserRecord { + 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(), + score: self.score, + }) + } + } + pub struct PackedUserRecord { + pub compression_info: Option, + pub owner: u8, + pub name: String, + pub score: u64, + } + #[automatically_derived] + impl ::core::fmt::Debug for PackedUserRecord { + #[inline] + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + ::core::fmt::Formatter::debug_struct_field4_finish( + f, + "PackedUserRecord", + "compression_info", + &self.compression_info, + "owner", + &self.owner, + "name", + &self.name, + "score", + &&self.score, + ) + } + } + #[automatically_derived] + impl ::core::clone::Clone for PackedUserRecord { + #[inline] + fn clone(&self) -> PackedUserRecord { + PackedUserRecord { + compression_info: ::core::clone::Clone::clone(&self.compression_info), + owner: ::core::clone::Clone::clone(&self.owner), + name: ::core::clone::Clone::clone(&self.name), + score: ::core::clone::Clone::clone(&self.score), + } + } + } + impl borsh::ser::BorshSerialize for PackedUserRecord + where + Option: borsh::ser::BorshSerialize, + u8: borsh::ser::BorshSerialize, + String: borsh::ser::BorshSerialize, + u64: borsh::ser::BorshSerialize, + { + fn serialize( + &self, + writer: &mut W, + ) -> ::core::result::Result<(), borsh::maybestd::io::Error> { + borsh::BorshSerialize::serialize(&self.compression_info, writer)?; + borsh::BorshSerialize::serialize(&self.owner, writer)?; + borsh::BorshSerialize::serialize(&self.name, writer)?; + borsh::BorshSerialize::serialize(&self.score, writer)?; + Ok(()) + } + } + impl anchor_lang::idl::build::IdlBuild for PackedUserRecord { + fn create_type() -> Option { + Some(anchor_lang::idl::types::IdlTypeDef { + name: Self::get_full_path(), + docs: ::alloc::vec::Vec::new(), + serialization: anchor_lang::idl::types::IdlSerialization::default(), + repr: None, + generics: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlTypeDefTy::Struct { + fields: Some( + anchor_lang::idl::types::IdlDefinedFields::Named( + <[_]>::into_vec( + ::alloc::boxed::box_new([ + anchor_lang::idl::types::IdlField { + name: "compression_info".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::Option( + Box::new(anchor_lang::idl::types::IdlType::Defined { + name: ::get_full_path(), + generics: ::alloc::vec::Vec::new(), + }), + ), + }, + anchor_lang::idl::types::IdlField { + name: "owner".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::U8, + }, + anchor_lang::idl::types::IdlField { + name: "name".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::String, + }, + anchor_lang::idl::types::IdlField { + name: "score".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::U64, + }, + ]), + ), + ), + ), + }, + }) + } + fn insert_types( + types: &mut std::collections::BTreeMap< + String, + anchor_lang::idl::types::IdlTypeDef, + >, + ) { + if let Some(ty) = ::create_type() { + types.insert(::get_full_path(), ty); + ::insert_types(types); + } + } + fn get_full_path() -> String { + ::alloc::__export::must_use({ + let res = ::alloc::fmt::format( + format_args!( + "{0}::{1}", + "anchor_compressible_derived::state", + "PackedUserRecord", + ), + ); + res + }) + } + } + impl borsh::de::BorshDeserialize for PackedUserRecord + where + Option: borsh::BorshDeserialize, + u8: borsh::BorshDeserialize, + String: borsh::BorshDeserialize, + u64: borsh::BorshDeserialize, + { + fn deserialize_reader( + reader: &mut R, + ) -> ::core::result::Result { + Ok(Self { + compression_info: borsh::BorshDeserialize::deserialize_reader(reader)?, + owner: borsh::BorshDeserialize::deserialize_reader(reader)?, + name: borsh::BorshDeserialize::deserialize_reader(reader)?, + score: borsh::BorshDeserialize::deserialize_reader(reader)?, + }) + } + } + impl light_sdk::compressible::Pack for UserRecord { + type Packed = PackedUserRecord; + fn pack( + &self, + remaining_accounts: &mut light_sdk::instruction::PackedAccounts, + ) -> Self::Packed { + PackedUserRecord { + compression_info: None, + owner: remaining_accounts.insert_or_get(self.owner), + name: self.name.clone(), + score: self.score, + } + } + } + impl light_sdk::compressible::Unpack for UserRecord { + type Unpacked = Self; + fn unpack( + &self, + _remaining_accounts: &[anchor_lang::prelude::AccountInfo], + ) -> std::result::Result { + Ok(self.clone()) + } + } + impl light_sdk::compressible::Pack for PackedUserRecord { + type Packed = Self; + fn pack( + &self, + _remaining_accounts: &mut light_sdk::instruction::PackedAccounts, + ) -> Self::Packed { + self.clone() + } + } + impl light_sdk::compressible::Unpack for PackedUserRecord { + type Unpacked = UserRecord; + fn unpack( + &self, + remaining_accounts: &[anchor_lang::prelude::AccountInfo], + ) -> std::result::Result { + Ok(UserRecord { + compression_info: None, + owner: *remaining_accounts[self.owner as usize].key, + name: self.name.clone(), + score: self.score, + }) + } + } + #[automatically_derived] + impl ::core::default::Default for UserRecord { + #[inline] + fn default() -> UserRecord { + UserRecord { + compression_info: ::core::default::Default::default(), + owner: ::core::default::Default::default(), + name: ::core::default::Default::default(), + score: ::core::default::Default::default(), + } + } + } + #[automatically_derived] + impl anchor_lang::Space for UserRecord { + const INIT_SPACE: usize = 0 + + (1 + ::INIT_SPACE) + 32 + (4 + 32) + + 8; + } + #[light_seeds(b"game_session", session_id.to_le_bytes().as_ref())] + #[compress_as(start_time = 0, end_time = None, score = 0)] + pub struct GameSession { + #[skip] + pub compression_info: Option, + pub session_id: u64, + #[hash] + pub player: Pubkey, + #[hash] + #[max_len(32)] + pub game_type: String, + pub start_time: u64, + pub end_time: Option, + pub score: u64, + } + impl borsh::ser::BorshSerialize for GameSession + where + Option: borsh::ser::BorshSerialize, + u64: borsh::ser::BorshSerialize, + Pubkey: borsh::ser::BorshSerialize, + String: borsh::ser::BorshSerialize, + u64: borsh::ser::BorshSerialize, + Option: borsh::ser::BorshSerialize, + u64: borsh::ser::BorshSerialize, + { + fn serialize( + &self, + writer: &mut W, + ) -> ::core::result::Result<(), borsh::maybestd::io::Error> { + borsh::BorshSerialize::serialize(&self.compression_info, writer)?; + borsh::BorshSerialize::serialize(&self.session_id, writer)?; + borsh::BorshSerialize::serialize(&self.player, writer)?; + borsh::BorshSerialize::serialize(&self.game_type, writer)?; + borsh::BorshSerialize::serialize(&self.start_time, writer)?; + borsh::BorshSerialize::serialize(&self.end_time, writer)?; + borsh::BorshSerialize::serialize(&self.score, writer)?; + Ok(()) + } + } + impl anchor_lang::idl::build::IdlBuild for GameSession { + fn create_type() -> Option { + Some(anchor_lang::idl::types::IdlTypeDef { + name: Self::get_full_path(), + docs: ::alloc::vec::Vec::new(), + serialization: anchor_lang::idl::types::IdlSerialization::default(), + repr: None, + generics: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlTypeDefTy::Struct { + fields: Some( + anchor_lang::idl::types::IdlDefinedFields::Named( + <[_]>::into_vec( + ::alloc::boxed::box_new([ + anchor_lang::idl::types::IdlField { + name: "compression_info".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::Option( + Box::new(anchor_lang::idl::types::IdlType::Defined { + name: ::get_full_path(), + generics: ::alloc::vec::Vec::new(), + }), + ), + }, + anchor_lang::idl::types::IdlField { + name: "session_id".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::U64, + }, + anchor_lang::idl::types::IdlField { + name: "player".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::Pubkey, + }, + anchor_lang::idl::types::IdlField { + name: "game_type".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::String, + }, + anchor_lang::idl::types::IdlField { + name: "start_time".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::U64, + }, + anchor_lang::idl::types::IdlField { + name: "end_time".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::Option( + Box::new(anchor_lang::idl::types::IdlType::U64), + ), + }, + anchor_lang::idl::types::IdlField { + name: "score".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::U64, + }, + ]), + ), + ), + ), + }, + }) + } + fn insert_types( + types: &mut std::collections::BTreeMap< + String, + anchor_lang::idl::types::IdlTypeDef, + >, + ) { + if let Some(ty) = ::create_type() { + types.insert(::get_full_path(), ty); + ::insert_types(types); + } + } + fn get_full_path() -> String { + ::alloc::__export::must_use({ + let res = ::alloc::fmt::format( + format_args!( + "{0}::{1}", + "anchor_compressible_derived::state", + "GameSession", + ), + ); + res + }) + } + } + impl borsh::de::BorshDeserialize for GameSession + where + Option: borsh::BorshDeserialize, + u64: borsh::BorshDeserialize, + Pubkey: borsh::BorshDeserialize, + String: borsh::BorshDeserialize, + u64: borsh::BorshDeserialize, + Option: borsh::BorshDeserialize, + u64: borsh::BorshDeserialize, + { + fn deserialize_reader( + reader: &mut R, + ) -> ::core::result::Result { + Ok(Self { + compression_info: borsh::BorshDeserialize::deserialize_reader(reader)?, + session_id: borsh::BorshDeserialize::deserialize_reader(reader)?, + player: borsh::BorshDeserialize::deserialize_reader(reader)?, + game_type: borsh::BorshDeserialize::deserialize_reader(reader)?, + start_time: borsh::BorshDeserialize::deserialize_reader(reader)?, + end_time: borsh::BorshDeserialize::deserialize_reader(reader)?, + score: borsh::BorshDeserialize::deserialize_reader(reader)?, + }) + } + } + #[automatically_derived] + impl ::core::clone::Clone for GameSession { + #[inline] + fn clone(&self) -> GameSession { + GameSession { + compression_info: ::core::clone::Clone::clone(&self.compression_info), + session_id: ::core::clone::Clone::clone(&self.session_id), + player: ::core::clone::Clone::clone(&self.player), + game_type: ::core::clone::Clone::clone(&self.game_type), + start_time: ::core::clone::Clone::clone(&self.start_time), + end_time: ::core::clone::Clone::clone(&self.end_time), + score: ::core::clone::Clone::clone(&self.score), + } + } + } + #[automatically_derived] + impl anchor_lang::AccountSerialize for GameSession { + fn try_serialize( + &self, + writer: &mut W, + ) -> anchor_lang::Result<()> { + if writer.write_all(GameSession::DISCRIMINATOR).is_err() { + return Err(anchor_lang::error::ErrorCode::AccountDidNotSerialize.into()); + } + if AnchorSerialize::serialize(self, writer).is_err() { + return Err(anchor_lang::error::ErrorCode::AccountDidNotSerialize.into()); + } + Ok(()) + } + } + #[automatically_derived] + impl anchor_lang::AccountDeserialize for GameSession { + fn try_deserialize(buf: &mut &[u8]) -> anchor_lang::Result { + if buf.len() < GameSession::DISCRIMINATOR.len() { + return Err( + anchor_lang::error::ErrorCode::AccountDiscriminatorNotFound.into(), + ); + } + let given_disc = &buf[..GameSession::DISCRIMINATOR.len()]; + if GameSession::DISCRIMINATOR != given_disc { + return Err( + anchor_lang::error::Error::from(anchor_lang::error::AnchorError { + error_name: anchor_lang::error::ErrorCode::AccountDiscriminatorMismatch + .name(), + error_code_number: anchor_lang::error::ErrorCode::AccountDiscriminatorMismatch + .into(), + error_msg: anchor_lang::error::ErrorCode::AccountDiscriminatorMismatch + .to_string(), + error_origin: Some( + anchor_lang::error::ErrorOrigin::Source(anchor_lang::error::Source { + filename: "sdk-tests/anchor-compressible-derived/src/state.rs", + line: 31u32, + }), + ), + compared_values: None, + }) + .with_account_name("GameSession"), + ); + } + Self::try_deserialize_unchecked(buf) + } + fn try_deserialize_unchecked(buf: &mut &[u8]) -> anchor_lang::Result { + let mut data: &[u8] = &buf[GameSession::DISCRIMINATOR.len()..]; + AnchorDeserialize::deserialize(&mut data) + .map_err(|_| { + anchor_lang::error::ErrorCode::AccountDidNotDeserialize.into() + }) + } + } + #[automatically_derived] + impl anchor_lang::Discriminator for GameSession { + const DISCRIMINATOR: &'static [u8] = &[150, 116, 20, 197, 205, 121, 220, 240]; + } + #[automatically_derived] + impl anchor_lang::Owner for GameSession { + fn owner() -> Pubkey { + crate::ID + } + } + #[automatically_derived] + impl ::core::fmt::Debug for GameSession { + #[inline] + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + let names: &'static _ = &[ + "compression_info", + "session_id", + "player", + "game_type", + "start_time", + "end_time", + "score", + ]; + let values: &[&dyn ::core::fmt::Debug] = &[ + &self.compression_info, + &self.session_id, + &self.player, + &self.game_type, + &self.start_time, + &self.end_time, + &&self.score, + ]; + ::core::fmt::Formatter::debug_struct_fields_finish( + f, + "GameSession", + names, + values, + ) + } + } + impl ::light_hasher::to_byte_array::ToByteArray for GameSession { + const NUM_FIELDS: usize = 7usize; + fn to_byte_array( + &self, + ) -> ::std::result::Result<[u8; 32], ::light_hasher::HasherError> { + use ::light_hasher::to_byte_array::ToByteArray; + use ::light_hasher::hash_to_field_size::HashToFieldSize; + use ::light_hasher::Hasher; + let mut result = ::light_hasher::Poseidon::hashv( + &[ + self.session_id.to_byte_array()?.as_slice(), + ::light_hasher::hash_to_field_size::hash_to_bn254_field_size_be( + self.player.as_ref(), + ) + .as_slice(), + self.game_type.hash_to_field_size()?.as_slice(), + self.start_time.to_byte_array()?.as_slice(), + self.end_time.to_byte_array()?.as_slice(), + self.score.to_byte_array()?.as_slice(), + ], + )?; + if ::light_hasher::Poseidon::ID != 0 { + result[0] = 0; + } + Ok(result) + } + } + impl ::light_hasher::DataHasher for GameSession { + fn hash(&self) -> ::std::result::Result<[u8; 32], ::light_hasher::HasherError> + where + H: ::light_hasher::Hasher, + { + use ::light_hasher::DataHasher; + use ::light_hasher::Hasher; + use ::light_hasher::to_byte_array::ToByteArray; + use ::light_hasher::hash_to_field_size::HashToFieldSize; + #[cfg(debug_assertions)] + { + if std::env::var("RUST_BACKTRACE").is_ok() { + let debug_prints: Vec<[u8; 32]> = <[_]>::into_vec( + ::alloc::boxed::box_new([ + self.session_id.to_byte_array()?, + ::light_hasher::hash_to_field_size::hash_to_bn254_field_size_be( + self.player.as_ref(), + ), + self.game_type.hash_to_field_size()?, + self.start_time.to_byte_array()?, + self.end_time.to_byte_array()?, + self.score.to_byte_array()?, + ]), + ); + { + ::std::io::_print( + format_args!("DataHasher::hash inputs {0:?}\n", debug_prints), + ); + }; + } + } + let mut result = H::hashv( + &[ + self.session_id.to_byte_array()?.as_slice(), + ::light_hasher::hash_to_field_size::hash_to_bn254_field_size_be( + self.player.as_ref(), + ) + .as_slice(), + self.game_type.hash_to_field_size()?.as_slice(), + self.start_time.to_byte_array()?.as_slice(), + self.end_time.to_byte_array()?.as_slice(), + self.score.to_byte_array()?.as_slice(), + ], + )?; + if H::ID != 0 { + result[0] = 0; + } + Ok(result) + } + } + impl LightDiscriminator for GameSession { + const LIGHT_DISCRIMINATOR: [u8; 8] = [150, 116, 20, 197, 205, 121, 220, 240]; + const LIGHT_DISCRIMINATOR_SLICE: &'static [u8] = &Self::LIGHT_DISCRIMINATOR; + fn discriminator() -> [u8; 8] { + Self::LIGHT_DISCRIMINATOR + } + } + #[automatically_derived] + impl ::core::default::Default for GameSession { + #[inline] + fn default() -> GameSession { + GameSession { + compression_info: ::core::default::Default::default(), + session_id: ::core::default::Default::default(), + player: ::core::default::Default::default(), + game_type: ::core::default::Default::default(), + start_time: ::core::default::Default::default(), + end_time: ::core::default::Default::default(), + score: ::core::default::Default::default(), + } + } + } + #[automatically_derived] + impl anchor_lang::Space for GameSession { + const INIT_SPACE: usize = 0 + + (1 + ::INIT_SPACE) + 8 + 32 + + (4 + 32) + 8 + (1 + 8) + 8; + } + impl light_sdk::compressible::HasCompressionInfo for GameSession { + fn compression_info(&self) -> &light_sdk::compressible::CompressionInfo { + self.compression_info + .as_ref() + .expect("CompressionInfo must be Some on-chain") + } + fn compression_info_mut( + &mut self, + ) -> &mut light_sdk::compressible::CompressionInfo { + self.compression_info + .as_mut() + .expect("CompressionInfo must be Some on-chain") + } + fn compression_info_mut_opt( + &mut self, + ) -> &mut Option { + &mut self.compression_info + } + fn set_compression_info_none(&mut self) { + self.compression_info = None; + } + } + impl light_sdk::account::Size for GameSession { + fn size(&self) -> usize { + Self::LIGHT_DISCRIMINATOR.len() + Self::INIT_SPACE + } + } + impl light_sdk::compressible::CompressAs for GameSession { + type Output = Self; + fn compress_as(&self) -> std::borrow::Cow<'_, Self::Output> { + std::borrow::Cow::Owned(Self { + compression_info: None, + session_id: self.session_id, + player: self.player, + game_type: self.game_type.clone(), + start_time: 0, + end_time: None, + score: 0, + }) + } + } + 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, + } + #[automatically_derived] + impl ::core::fmt::Debug for PackedGameSession { + #[inline] + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + let names: &'static _ = &[ + "compression_info", + "session_id", + "player", + "game_type", + "start_time", + "end_time", + "score", + ]; + let values: &[&dyn ::core::fmt::Debug] = &[ + &self.compression_info, + &self.session_id, + &self.player, + &self.game_type, + &self.start_time, + &self.end_time, + &&self.score, + ]; + ::core::fmt::Formatter::debug_struct_fields_finish( + f, + "PackedGameSession", + names, + values, + ) + } + } + #[automatically_derived] + impl ::core::clone::Clone for PackedGameSession { + #[inline] + fn clone(&self) -> PackedGameSession { + PackedGameSession { + compression_info: ::core::clone::Clone::clone(&self.compression_info), + session_id: ::core::clone::Clone::clone(&self.session_id), + player: ::core::clone::Clone::clone(&self.player), + game_type: ::core::clone::Clone::clone(&self.game_type), + start_time: ::core::clone::Clone::clone(&self.start_time), + end_time: ::core::clone::Clone::clone(&self.end_time), + score: ::core::clone::Clone::clone(&self.score), + } + } + } + impl borsh::ser::BorshSerialize for PackedGameSession + where + Option: borsh::ser::BorshSerialize, + u64: borsh::ser::BorshSerialize, + u8: borsh::ser::BorshSerialize, + String: borsh::ser::BorshSerialize, + u64: borsh::ser::BorshSerialize, + Option: borsh::ser::BorshSerialize, + u64: borsh::ser::BorshSerialize, + { + fn serialize( + &self, + writer: &mut W, + ) -> ::core::result::Result<(), borsh::maybestd::io::Error> { + borsh::BorshSerialize::serialize(&self.compression_info, writer)?; + borsh::BorshSerialize::serialize(&self.session_id, writer)?; + borsh::BorshSerialize::serialize(&self.player, writer)?; + borsh::BorshSerialize::serialize(&self.game_type, writer)?; + borsh::BorshSerialize::serialize(&self.start_time, writer)?; + borsh::BorshSerialize::serialize(&self.end_time, writer)?; + borsh::BorshSerialize::serialize(&self.score, writer)?; + Ok(()) + } + } + impl anchor_lang::idl::build::IdlBuild for PackedGameSession { + fn create_type() -> Option { + Some(anchor_lang::idl::types::IdlTypeDef { + name: Self::get_full_path(), + docs: ::alloc::vec::Vec::new(), + serialization: anchor_lang::idl::types::IdlSerialization::default(), + repr: None, + generics: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlTypeDefTy::Struct { + fields: Some( + anchor_lang::idl::types::IdlDefinedFields::Named( + <[_]>::into_vec( + ::alloc::boxed::box_new([ + anchor_lang::idl::types::IdlField { + name: "compression_info".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::Option( + Box::new(anchor_lang::idl::types::IdlType::Defined { + name: ::get_full_path(), + generics: ::alloc::vec::Vec::new(), + }), + ), + }, + anchor_lang::idl::types::IdlField { + name: "session_id".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::U64, + }, + anchor_lang::idl::types::IdlField { + name: "player".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::U8, + }, + anchor_lang::idl::types::IdlField { + name: "game_type".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::String, + }, + anchor_lang::idl::types::IdlField { + name: "start_time".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::U64, + }, + anchor_lang::idl::types::IdlField { + name: "end_time".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::Option( + Box::new(anchor_lang::idl::types::IdlType::U64), + ), + }, + anchor_lang::idl::types::IdlField { + name: "score".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::U64, + }, + ]), + ), + ), + ), + }, + }) + } + fn insert_types( + types: &mut std::collections::BTreeMap< + String, + anchor_lang::idl::types::IdlTypeDef, + >, + ) { + if let Some(ty) = ::create_type() { + types.insert(::get_full_path(), ty); + ::insert_types(types); + } + } + fn get_full_path() -> String { + ::alloc::__export::must_use({ + let res = ::alloc::fmt::format( + format_args!( + "{0}::{1}", + "anchor_compressible_derived::state", + "PackedGameSession", + ), + ); + res + }) + } + } + impl borsh::de::BorshDeserialize for PackedGameSession + where + Option: borsh::BorshDeserialize, + u64: borsh::BorshDeserialize, + u8: borsh::BorshDeserialize, + String: borsh::BorshDeserialize, + u64: borsh::BorshDeserialize, + Option: borsh::BorshDeserialize, + u64: borsh::BorshDeserialize, + { + fn deserialize_reader( + reader: &mut R, + ) -> ::core::result::Result { + Ok(Self { + compression_info: borsh::BorshDeserialize::deserialize_reader(reader)?, + session_id: borsh::BorshDeserialize::deserialize_reader(reader)?, + player: borsh::BorshDeserialize::deserialize_reader(reader)?, + game_type: borsh::BorshDeserialize::deserialize_reader(reader)?, + start_time: borsh::BorshDeserialize::deserialize_reader(reader)?, + end_time: borsh::BorshDeserialize::deserialize_reader(reader)?, + score: borsh::BorshDeserialize::deserialize_reader(reader)?, + }) + } + } + impl light_sdk::compressible::Pack for GameSession { + type Packed = PackedGameSession; + fn pack( + &self, + remaining_accounts: &mut light_sdk::instruction::PackedAccounts, + ) -> Self::Packed { + PackedGameSession { + compression_info: None, + session_id: self.session_id, + player: remaining_accounts.insert_or_get(self.player), + game_type: self.game_type.clone(), + start_time: self.start_time, + end_time: self.end_time, + score: self.score, + } + } + } + impl light_sdk::compressible::Unpack for GameSession { + type Unpacked = Self; + fn unpack( + &self, + _remaining_accounts: &[anchor_lang::prelude::AccountInfo], + ) -> std::result::Result { + Ok(self.clone()) + } + } + impl light_sdk::compressible::Pack for PackedGameSession { + type Packed = Self; + fn pack( + &self, + _remaining_accounts: &mut light_sdk::instruction::PackedAccounts, + ) -> Self::Packed { + self.clone() + } + } + impl light_sdk::compressible::Unpack for PackedGameSession { + type Unpacked = GameSession; + fn unpack( + &self, + remaining_accounts: &[anchor_lang::prelude::AccountInfo], + ) -> std::result::Result { + Ok(GameSession { + compression_info: None, + session_id: self.session_id, + player: *remaining_accounts[self.player as usize].key, + game_type: self.game_type.clone(), + start_time: self.start_time, + end_time: self.end_time, + score: self.score, + }) + } + } + #[light_seeds(b"placeholder_record", placeholder_id.to_le_bytes().as_ref())] + pub struct PlaceholderRecord { + #[skip] + pub compression_info: Option, + #[hash] + pub owner: Pubkey, + #[hash] + #[max_len(32)] + pub name: String, + pub placeholder_id: u64, + } + impl borsh::ser::BorshSerialize for PlaceholderRecord + where + Option: borsh::ser::BorshSerialize, + Pubkey: borsh::ser::BorshSerialize, + String: borsh::ser::BorshSerialize, + u64: borsh::ser::BorshSerialize, + { + fn serialize( + &self, + writer: &mut W, + ) -> ::core::result::Result<(), borsh::maybestd::io::Error> { + borsh::BorshSerialize::serialize(&self.compression_info, writer)?; + borsh::BorshSerialize::serialize(&self.owner, writer)?; + borsh::BorshSerialize::serialize(&self.name, writer)?; + borsh::BorshSerialize::serialize(&self.placeholder_id, writer)?; + Ok(()) + } + } + impl anchor_lang::idl::build::IdlBuild for PlaceholderRecord { + fn create_type() -> Option { + Some(anchor_lang::idl::types::IdlTypeDef { + name: Self::get_full_path(), + docs: ::alloc::vec::Vec::new(), + serialization: anchor_lang::idl::types::IdlSerialization::default(), + repr: None, + generics: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlTypeDefTy::Struct { + fields: Some( + anchor_lang::idl::types::IdlDefinedFields::Named( + <[_]>::into_vec( + ::alloc::boxed::box_new([ + anchor_lang::idl::types::IdlField { + name: "compression_info".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::Option( + Box::new(anchor_lang::idl::types::IdlType::Defined { + name: ::get_full_path(), + generics: ::alloc::vec::Vec::new(), + }), + ), + }, + anchor_lang::idl::types::IdlField { + name: "owner".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::Pubkey, + }, + anchor_lang::idl::types::IdlField { + name: "name".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::String, + }, + anchor_lang::idl::types::IdlField { + name: "placeholder_id".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::U64, + }, + ]), + ), + ), + ), + }, + }) + } + fn insert_types( + types: &mut std::collections::BTreeMap< + String, + anchor_lang::idl::types::IdlTypeDef, + >, + ) { + if let Some(ty) = ::create_type() { + types.insert(::get_full_path(), ty); + ::insert_types(types); + } + } + fn get_full_path() -> String { + ::alloc::__export::must_use({ + let res = ::alloc::fmt::format( + format_args!( + "{0}::{1}", + "anchor_compressible_derived::state", + "PlaceholderRecord", + ), + ); + res + }) + } + } + impl borsh::de::BorshDeserialize for PlaceholderRecord + where + Option: borsh::BorshDeserialize, + Pubkey: borsh::BorshDeserialize, + String: borsh::BorshDeserialize, + u64: borsh::BorshDeserialize, + { + fn deserialize_reader( + reader: &mut R, + ) -> ::core::result::Result { + Ok(Self { + compression_info: borsh::BorshDeserialize::deserialize_reader(reader)?, + owner: borsh::BorshDeserialize::deserialize_reader(reader)?, + name: borsh::BorshDeserialize::deserialize_reader(reader)?, + placeholder_id: borsh::BorshDeserialize::deserialize_reader(reader)?, + }) + } + } + #[automatically_derived] + impl ::core::clone::Clone for PlaceholderRecord { + #[inline] + fn clone(&self) -> PlaceholderRecord { + PlaceholderRecord { + compression_info: ::core::clone::Clone::clone(&self.compression_info), + owner: ::core::clone::Clone::clone(&self.owner), + name: ::core::clone::Clone::clone(&self.name), + placeholder_id: ::core::clone::Clone::clone(&self.placeholder_id), + } + } + } + #[automatically_derived] + impl anchor_lang::AccountSerialize for PlaceholderRecord { + fn try_serialize( + &self, + writer: &mut W, + ) -> anchor_lang::Result<()> { + if writer.write_all(PlaceholderRecord::DISCRIMINATOR).is_err() { + return Err(anchor_lang::error::ErrorCode::AccountDidNotSerialize.into()); + } + if AnchorSerialize::serialize(self, writer).is_err() { + return Err(anchor_lang::error::ErrorCode::AccountDidNotSerialize.into()); + } + Ok(()) + } + } + #[automatically_derived] + impl anchor_lang::AccountDeserialize for PlaceholderRecord { + fn try_deserialize(buf: &mut &[u8]) -> anchor_lang::Result { + if buf.len() < PlaceholderRecord::DISCRIMINATOR.len() { + return Err( + anchor_lang::error::ErrorCode::AccountDiscriminatorNotFound.into(), + ); + } + let given_disc = &buf[..PlaceholderRecord::DISCRIMINATOR.len()]; + if PlaceholderRecord::DISCRIMINATOR != given_disc { + return Err( + anchor_lang::error::Error::from(anchor_lang::error::AnchorError { + error_name: anchor_lang::error::ErrorCode::AccountDiscriminatorMismatch + .name(), + error_code_number: anchor_lang::error::ErrorCode::AccountDiscriminatorMismatch + .into(), + error_msg: anchor_lang::error::ErrorCode::AccountDiscriminatorMismatch + .to_string(), + error_origin: Some( + anchor_lang::error::ErrorOrigin::Source(anchor_lang::error::Source { + filename: "sdk-tests/anchor-compressible-derived/src/state.rs", + line: 51u32, + }), + ), + compared_values: None, + }) + .with_account_name("PlaceholderRecord"), + ); + } + Self::try_deserialize_unchecked(buf) + } + fn try_deserialize_unchecked(buf: &mut &[u8]) -> anchor_lang::Result { + let mut data: &[u8] = &buf[PlaceholderRecord::DISCRIMINATOR.len()..]; + AnchorDeserialize::deserialize(&mut data) + .map_err(|_| { + anchor_lang::error::ErrorCode::AccountDidNotDeserialize.into() + }) + } + } + #[automatically_derived] + impl anchor_lang::Discriminator for PlaceholderRecord { + const DISCRIMINATOR: &'static [u8] = &[70, 2, 95, 178, 67, 74, 56, 8]; + } + #[automatically_derived] + impl anchor_lang::Owner for PlaceholderRecord { + fn owner() -> Pubkey { + crate::ID + } + } + #[automatically_derived] + impl ::core::fmt::Debug for PlaceholderRecord { + #[inline] + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + ::core::fmt::Formatter::debug_struct_field4_finish( + f, + "PlaceholderRecord", + "compression_info", + &self.compression_info, + "owner", + &self.owner, + "name", + &self.name, + "placeholder_id", + &&self.placeholder_id, + ) + } + } + impl ::light_hasher::to_byte_array::ToByteArray for PlaceholderRecord { + const NUM_FIELDS: usize = 4usize; + fn to_byte_array( + &self, + ) -> ::std::result::Result<[u8; 32], ::light_hasher::HasherError> { + use ::light_hasher::to_byte_array::ToByteArray; + use ::light_hasher::hash_to_field_size::HashToFieldSize; + use ::light_hasher::Hasher; + let mut result = ::light_hasher::Poseidon::hashv( + &[ + ::light_hasher::hash_to_field_size::hash_to_bn254_field_size_be( + self.owner.as_ref(), + ) + .as_slice(), + self.name.hash_to_field_size()?.as_slice(), + self.placeholder_id.to_byte_array()?.as_slice(), + ], + )?; + if ::light_hasher::Poseidon::ID != 0 { + result[0] = 0; + } + Ok(result) + } + } + impl ::light_hasher::DataHasher for PlaceholderRecord { + fn hash(&self) -> ::std::result::Result<[u8; 32], ::light_hasher::HasherError> + where + H: ::light_hasher::Hasher, + { + use ::light_hasher::DataHasher; + use ::light_hasher::Hasher; + use ::light_hasher::to_byte_array::ToByteArray; + use ::light_hasher::hash_to_field_size::HashToFieldSize; + #[cfg(debug_assertions)] + { + if std::env::var("RUST_BACKTRACE").is_ok() { + let debug_prints: Vec<[u8; 32]> = <[_]>::into_vec( + ::alloc::boxed::box_new([ + ::light_hasher::hash_to_field_size::hash_to_bn254_field_size_be( + self.owner.as_ref(), + ), + self.name.hash_to_field_size()?, + self.placeholder_id.to_byte_array()?, + ]), + ); + { + ::std::io::_print( + format_args!("DataHasher::hash inputs {0:?}\n", debug_prints), + ); + }; + } + } + let mut result = H::hashv( + &[ + ::light_hasher::hash_to_field_size::hash_to_bn254_field_size_be( + self.owner.as_ref(), + ) + .as_slice(), + self.name.hash_to_field_size()?.as_slice(), + self.placeholder_id.to_byte_array()?.as_slice(), + ], + )?; + if H::ID != 0 { + result[0] = 0; + } + Ok(result) + } + } + impl LightDiscriminator for PlaceholderRecord { + const LIGHT_DISCRIMINATOR: [u8; 8] = [70, 2, 95, 178, 67, 74, 56, 8]; + const LIGHT_DISCRIMINATOR_SLICE: &'static [u8] = &Self::LIGHT_DISCRIMINATOR; + fn discriminator() -> [u8; 8] { + Self::LIGHT_DISCRIMINATOR + } + } + #[automatically_derived] + impl ::core::default::Default for PlaceholderRecord { + #[inline] + fn default() -> PlaceholderRecord { + PlaceholderRecord { + compression_info: ::core::default::Default::default(), + owner: ::core::default::Default::default(), + name: ::core::default::Default::default(), + placeholder_id: ::core::default::Default::default(), + } + } + } + #[automatically_derived] + impl anchor_lang::Space for PlaceholderRecord { + const INIT_SPACE: usize = 0 + + (1 + ::INIT_SPACE) + 32 + (4 + 32) + + 8; + } + impl light_sdk::compressible::HasCompressionInfo for PlaceholderRecord { + fn compression_info(&self) -> &light_sdk::compressible::CompressionInfo { + self.compression_info + .as_ref() + .expect("CompressionInfo must be Some on-chain") + } + fn compression_info_mut( + &mut self, + ) -> &mut light_sdk::compressible::CompressionInfo { + self.compression_info + .as_mut() + .expect("CompressionInfo must be Some on-chain") + } + fn compression_info_mut_opt( + &mut self, + ) -> &mut Option { + &mut self.compression_info + } + fn set_compression_info_none(&mut self) { + self.compression_info = None; + } + } + impl light_sdk::account::Size for PlaceholderRecord { + fn size(&self) -> usize { + Self::LIGHT_DISCRIMINATOR.len() + Self::INIT_SPACE + } + } + impl light_sdk::compressible::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, + }) + } + } + pub struct PackedPlaceholderRecord { + pub compression_info: Option, + pub owner: u8, + pub name: String, + pub placeholder_id: u64, + } + #[automatically_derived] + impl ::core::fmt::Debug for PackedPlaceholderRecord { + #[inline] + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + ::core::fmt::Formatter::debug_struct_field4_finish( + f, + "PackedPlaceholderRecord", + "compression_info", + &self.compression_info, + "owner", + &self.owner, + "name", + &self.name, + "placeholder_id", + &&self.placeholder_id, + ) + } + } + #[automatically_derived] + impl ::core::clone::Clone for PackedPlaceholderRecord { + #[inline] + fn clone(&self) -> PackedPlaceholderRecord { + PackedPlaceholderRecord { + compression_info: ::core::clone::Clone::clone(&self.compression_info), + owner: ::core::clone::Clone::clone(&self.owner), + name: ::core::clone::Clone::clone(&self.name), + placeholder_id: ::core::clone::Clone::clone(&self.placeholder_id), + } + } + } + impl borsh::ser::BorshSerialize for PackedPlaceholderRecord + where + Option: borsh::ser::BorshSerialize, + u8: borsh::ser::BorshSerialize, + String: borsh::ser::BorshSerialize, + u64: borsh::ser::BorshSerialize, + { + fn serialize( + &self, + writer: &mut W, + ) -> ::core::result::Result<(), borsh::maybestd::io::Error> { + borsh::BorshSerialize::serialize(&self.compression_info, writer)?; + borsh::BorshSerialize::serialize(&self.owner, writer)?; + borsh::BorshSerialize::serialize(&self.name, writer)?; + borsh::BorshSerialize::serialize(&self.placeholder_id, writer)?; + Ok(()) + } + } + impl anchor_lang::idl::build::IdlBuild for PackedPlaceholderRecord { + fn create_type() -> Option { + Some(anchor_lang::idl::types::IdlTypeDef { + name: Self::get_full_path(), + docs: ::alloc::vec::Vec::new(), + serialization: anchor_lang::idl::types::IdlSerialization::default(), + repr: None, + generics: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlTypeDefTy::Struct { + fields: Some( + anchor_lang::idl::types::IdlDefinedFields::Named( + <[_]>::into_vec( + ::alloc::boxed::box_new([ + anchor_lang::idl::types::IdlField { + name: "compression_info".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::Option( + Box::new(anchor_lang::idl::types::IdlType::Defined { + name: ::get_full_path(), + generics: ::alloc::vec::Vec::new(), + }), + ), + }, + anchor_lang::idl::types::IdlField { + name: "owner".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::U8, + }, + anchor_lang::idl::types::IdlField { + name: "name".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::String, + }, + anchor_lang::idl::types::IdlField { + name: "placeholder_id".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::U64, + }, + ]), + ), + ), + ), + }, + }) + } + fn insert_types( + types: &mut std::collections::BTreeMap< + String, + anchor_lang::idl::types::IdlTypeDef, + >, + ) { + if let Some(ty) = ::create_type() { + types.insert(::get_full_path(), ty); + ::insert_types(types); + } + } + fn get_full_path() -> String { + ::alloc::__export::must_use({ + let res = ::alloc::fmt::format( + format_args!( + "{0}::{1}", + "anchor_compressible_derived::state", + "PackedPlaceholderRecord", + ), + ); + res + }) + } + } + impl borsh::de::BorshDeserialize for PackedPlaceholderRecord + where + Option: borsh::BorshDeserialize, + u8: borsh::BorshDeserialize, + String: borsh::BorshDeserialize, + u64: borsh::BorshDeserialize, + { + fn deserialize_reader( + reader: &mut R, + ) -> ::core::result::Result { + Ok(Self { + compression_info: borsh::BorshDeserialize::deserialize_reader(reader)?, + owner: borsh::BorshDeserialize::deserialize_reader(reader)?, + name: borsh::BorshDeserialize::deserialize_reader(reader)?, + placeholder_id: borsh::BorshDeserialize::deserialize_reader(reader)?, + }) + } + } + impl light_sdk::compressible::Pack for PlaceholderRecord { + type Packed = PackedPlaceholderRecord; + fn pack( + &self, + remaining_accounts: &mut light_sdk::instruction::PackedAccounts, + ) -> Self::Packed { + PackedPlaceholderRecord { + compression_info: None, + owner: remaining_accounts.insert_or_get(self.owner), + name: self.name.clone(), + placeholder_id: self.placeholder_id, + } + } + } + impl light_sdk::compressible::Unpack for PlaceholderRecord { + type Unpacked = Self; + fn unpack( + &self, + _remaining_accounts: &[anchor_lang::prelude::AccountInfo], + ) -> std::result::Result { + Ok(self.clone()) + } + } + impl light_sdk::compressible::Pack for PackedPlaceholderRecord { + type Packed = Self; + fn pack( + &self, + _remaining_accounts: &mut light_sdk::instruction::PackedAccounts, + ) -> Self::Packed { + self.clone() + } + } + impl light_sdk::compressible::Unpack for PackedPlaceholderRecord { + type Unpacked = PlaceholderRecord; + fn unpack( + &self, + remaining_accounts: &[anchor_lang::prelude::AccountInfo], + ) -> std::result::Result { + Ok(PlaceholderRecord { + compression_info: None, + owner: *remaining_accounts[self.owner as usize].key, + name: self.name.clone(), + placeholder_id: self.placeholder_id, + }) + } + } + #[repr(u8)] + pub enum CTokenAccountVariant { + CTokenSigner = 0, + AssociatedTokenAccount = 255, + } + impl borsh::ser::BorshSerialize for CTokenAccountVariant { + fn serialize( + &self, + writer: &mut W, + ) -> ::core::result::Result<(), borsh::maybestd::io::Error> { + let variant_idx: u8 = match self { + CTokenAccountVariant::CTokenSigner => 0u8, + CTokenAccountVariant::AssociatedTokenAccount => 1u8, + }; + writer.write_all(&variant_idx.to_le_bytes())?; + match self { + CTokenAccountVariant::CTokenSigner => {} + CTokenAccountVariant::AssociatedTokenAccount => {} + } + Ok(()) + } + } + impl anchor_lang::idl::build::IdlBuild for CTokenAccountVariant { + fn create_type() -> Option { + Some(anchor_lang::idl::types::IdlTypeDef { + name: Self::get_full_path(), + docs: ::alloc::vec::Vec::new(), + serialization: anchor_lang::idl::types::IdlSerialization::default(), + repr: Some( + anchor_lang::idl::types::IdlRepr::Rust(anchor_lang::idl::types::IdlReprModifier { + packed: false, + align: None, + }), + ), + generics: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlTypeDefTy::Enum { + variants: <[_]>::into_vec( + ::alloc::boxed::box_new([ + anchor_lang::idl::types::IdlEnumVariant { + name: "CTokenSigner".into(), + fields: None, + }, + anchor_lang::idl::types::IdlEnumVariant { + name: "AssociatedTokenAccount".into(), + fields: None, + }, + ]), + ), + }, + }) + } + fn insert_types( + types: &mut std::collections::BTreeMap< + String, + anchor_lang::idl::types::IdlTypeDef, + >, + ) {} + fn get_full_path() -> String { + ::alloc::__export::must_use({ + let res = ::alloc::fmt::format( + format_args!( + "{0}::{1}", + "anchor_compressible_derived::state", + "CTokenAccountVariant", + ), + ); + res + }) + } + } + impl borsh::de::BorshDeserialize for CTokenAccountVariant { + fn deserialize_reader( + reader: &mut R, + ) -> ::core::result::Result { + let tag = ::deserialize_reader(reader)?; + ::deserialize_variant(reader, tag) + } + } + impl borsh::de::EnumExt for CTokenAccountVariant { + fn deserialize_variant( + reader: &mut R, + variant_idx: u8, + ) -> ::core::result::Result { + let mut return_value = match variant_idx { + 0u8 => CTokenAccountVariant::CTokenSigner, + 1u8 => CTokenAccountVariant::AssociatedTokenAccount, + _ => { + return Err( + borsh::maybestd::io::Error::new( + borsh::maybestd::io::ErrorKind::InvalidInput, + ::alloc::__export::must_use({ + let res = ::alloc::fmt::format( + format_args!("Unexpected variant index: {0:?}", variant_idx), + ); + res + }), + ), + ); + } + }; + Ok(return_value) + } + } + #[automatically_derived] + impl ::core::fmt::Debug for CTokenAccountVariant { + #[inline] + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + ::core::fmt::Formatter::write_str( + f, + match self { + CTokenAccountVariant::CTokenSigner => "CTokenSigner", + CTokenAccountVariant::AssociatedTokenAccount => { + "AssociatedTokenAccount" + } + }, + ) + } + } + #[automatically_derived] + impl ::core::clone::Clone for CTokenAccountVariant { + #[inline] + fn clone(&self) -> CTokenAccountVariant { + *self + } + } + #[automatically_derived] + impl ::core::marker::Copy for CTokenAccountVariant {} +} +use crate::state::*; +/// The static program ID +pub static ID: anchor_lang::solana_program::pubkey::Pubkey = anchor_lang::solana_program::pubkey::Pubkey::new_from_array([ + 229u8, + 27u8, + 189u8, + 177u8, + 59u8, + 219u8, + 216u8, + 77u8, + 57u8, + 234u8, + 132u8, + 178u8, + 253u8, + 183u8, + 68u8, + 203u8, + 122u8, + 149u8, + 156u8, + 116u8, + 234u8, + 189u8, + 90u8, + 28u8, + 138u8, + 204u8, + 148u8, + 223u8, + 113u8, + 189u8, + 253u8, + 126u8, +]); +/// Const version of `ID` +pub const ID_CONST: anchor_lang::solana_program::pubkey::Pubkey = anchor_lang::solana_program::pubkey::Pubkey::new_from_array([ + 229u8, + 27u8, + 189u8, + 177u8, + 59u8, + 219u8, + 216u8, + 77u8, + 57u8, + 234u8, + 132u8, + 178u8, + 253u8, + 183u8, + 68u8, + 203u8, + 122u8, + 149u8, + 156u8, + 116u8, + 234u8, + 189u8, + 90u8, + 28u8, + 138u8, + 204u8, + 148u8, + 223u8, + 113u8, + 189u8, + 253u8, + 126u8, +]); +/// Confirms that a given pubkey is equivalent to the program ID +pub fn check_id(id: &anchor_lang::solana_program::pubkey::Pubkey) -> bool { + id == &ID +} +/// Returns the program ID +pub fn id() -> anchor_lang::solana_program::pubkey::Pubkey { + ID +} +/// Const version of `ID` +pub const fn id_const() -> anchor_lang::solana_program::pubkey::Pubkey { + ID_CONST +} +pub const LIGHT_CPI_SIGNER: CpiSigner = { + ::light_sdk_types::CpiSigner { + program_id: [ + 229, + 27, + 189, + 177, + 59, + 219, + 216, + 77, + 57, + 234, + 132, + 178, + 253, + 183, + 68, + 203, + 122, + 149, + 156, + 116, + 234, + 189, + 90, + 28, + 138, + 204, + 148, + 223, + 113, + 189, + 253, + 126, + ], + cpi_signer: [ + 149, + 132, + 159, + 193, + 10, + 184, + 134, + 173, + 175, + 180, + 232, + 110, + 145, + 4, + 235, + 205, + 133, + 172, + 125, + 46, + 47, + 215, + 196, + 60, + 67, + 148, + 248, + 69, + 200, + 71, + 227, + 250, + ], + bump: 255u8, + } +}; +pub fn get_ctoken_signer_seeds<'a>( + user: &'a Pubkey, + mint: &'a Pubkey, +) -> (Vec>, Pubkey) { + let mut seeds = <[_]>::into_vec( + ::alloc::boxed::box_new([ + 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(<[_]>::into_vec(::alloc::boxed::box_new([bump]))); + (seeds, pda) +} +pub fn get_user_record_seeds(user: &Pubkey) -> (Vec>, Pubkey) { + let seeds = [b"user_record".as_ref(), user.as_ref()]; + let (pda, bump) = Pubkey::find_program_address(&seeds, &crate::ID); + let bump_slice = <[_]>::into_vec(::alloc::boxed::box_new([bump])); + let seeds_vec = <[_]>::into_vec( + ::alloc::boxed::box_new([seeds[0].to_vec(), seeds[1].to_vec(), bump_slice]), + ); + (seeds_vec, pda) +} +pub fn get_game_session_seeds(session_id: u64) -> (Vec>, Pubkey) { + let session_id_le = session_id.to_le_bytes(); + let seeds = [b"game_session".as_ref(), session_id_le.as_ref()]; + let (pda, bump) = Pubkey::find_program_address(&seeds, &crate::ID); + let bump_slice = <[_]>::into_vec(::alloc::boxed::box_new([bump])); + let seeds_vec = <[_]>::into_vec( + ::alloc::boxed::box_new([seeds[0].to_vec(), seeds[1].to_vec(), bump_slice]), + ); + (seeds_vec, pda) +} +pub fn get_placeholder_record_seeds(placeholder_id: u64) -> (Vec>, Pubkey) { + let placeholder_id_le = placeholder_id.to_le_bytes(); + let seeds = [b"placeholder_record".as_ref(), placeholder_id_le.as_ref()]; + let (pda, bump) = Pubkey::find_program_address(&seeds, &crate::ID); + let bump_slice = <[_]>::into_vec(::alloc::boxed::box_new([bump])); + let seeds_vec = <[_]>::into_vec( + ::alloc::boxed::box_new([seeds[0].to_vec(), seeds[1].to_vec(), bump_slice]), + ); + (seeds_vec, pda) +} +pub enum CompressedAccountVariant { + UserRecord(UserRecord), + PackedUserRecord(PackedUserRecord), + GameSession(GameSession), + PackedGameSession(PackedGameSession), + PlaceholderRecord(PlaceholderRecord), + PackedPlaceholderRecord(PackedPlaceholderRecord), + CompressibleTokenAccountPacked( + light_sdk::token::PackedCompressibleTokenDataWithVariant, + ), + CompressibleTokenData( + light_sdk::token::CompressibleTokenDataWithVariant, + ), +} +#[automatically_derived] +impl ::core::clone::Clone for CompressedAccountVariant { + #[inline] + fn clone(&self) -> CompressedAccountVariant { + match self { + CompressedAccountVariant::UserRecord(__self_0) => { + CompressedAccountVariant::UserRecord( + ::core::clone::Clone::clone(__self_0), + ) + } + CompressedAccountVariant::PackedUserRecord(__self_0) => { + CompressedAccountVariant::PackedUserRecord( + ::core::clone::Clone::clone(__self_0), + ) + } + CompressedAccountVariant::GameSession(__self_0) => { + CompressedAccountVariant::GameSession( + ::core::clone::Clone::clone(__self_0), + ) + } + CompressedAccountVariant::PackedGameSession(__self_0) => { + CompressedAccountVariant::PackedGameSession( + ::core::clone::Clone::clone(__self_0), + ) + } + CompressedAccountVariant::PlaceholderRecord(__self_0) => { + CompressedAccountVariant::PlaceholderRecord( + ::core::clone::Clone::clone(__self_0), + ) + } + CompressedAccountVariant::PackedPlaceholderRecord(__self_0) => { + CompressedAccountVariant::PackedPlaceholderRecord( + ::core::clone::Clone::clone(__self_0), + ) + } + CompressedAccountVariant::CompressibleTokenAccountPacked(__self_0) => { + CompressedAccountVariant::CompressibleTokenAccountPacked( + ::core::clone::Clone::clone(__self_0), + ) + } + CompressedAccountVariant::CompressibleTokenData(__self_0) => { + CompressedAccountVariant::CompressibleTokenData( + ::core::clone::Clone::clone(__self_0), + ) + } + } + } +} +#[automatically_derived] +impl ::core::fmt::Debug for CompressedAccountVariant { + #[inline] + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + match self { + CompressedAccountVariant::UserRecord(__self_0) => { + ::core::fmt::Formatter::debug_tuple_field1_finish( + f, + "UserRecord", + &__self_0, + ) + } + CompressedAccountVariant::PackedUserRecord(__self_0) => { + ::core::fmt::Formatter::debug_tuple_field1_finish( + f, + "PackedUserRecord", + &__self_0, + ) + } + CompressedAccountVariant::GameSession(__self_0) => { + ::core::fmt::Formatter::debug_tuple_field1_finish( + f, + "GameSession", + &__self_0, + ) + } + CompressedAccountVariant::PackedGameSession(__self_0) => { + ::core::fmt::Formatter::debug_tuple_field1_finish( + f, + "PackedGameSession", + &__self_0, + ) + } + CompressedAccountVariant::PlaceholderRecord(__self_0) => { + ::core::fmt::Formatter::debug_tuple_field1_finish( + f, + "PlaceholderRecord", + &__self_0, + ) + } + CompressedAccountVariant::PackedPlaceholderRecord(__self_0) => { + ::core::fmt::Formatter::debug_tuple_field1_finish( + f, + "PackedPlaceholderRecord", + &__self_0, + ) + } + CompressedAccountVariant::CompressibleTokenAccountPacked(__self_0) => { + ::core::fmt::Formatter::debug_tuple_field1_finish( + f, + "CompressibleTokenAccountPacked", + &__self_0, + ) + } + CompressedAccountVariant::CompressibleTokenData(__self_0) => { + ::core::fmt::Formatter::debug_tuple_field1_finish( + f, + "CompressibleTokenData", + &__self_0, + ) + } + } + } +} +impl borsh::ser::BorshSerialize for CompressedAccountVariant +where + UserRecord: borsh::ser::BorshSerialize, + PackedUserRecord: borsh::ser::BorshSerialize, + GameSession: borsh::ser::BorshSerialize, + PackedGameSession: borsh::ser::BorshSerialize, + PlaceholderRecord: borsh::ser::BorshSerialize, + PackedPlaceholderRecord: borsh::ser::BorshSerialize, + light_sdk::token::PackedCompressibleTokenDataWithVariant< + CTokenAccountVariant, + >: borsh::ser::BorshSerialize, + light_sdk::token::CompressibleTokenDataWithVariant< + CTokenAccountVariant, + >: borsh::ser::BorshSerialize, +{ + fn serialize( + &self, + writer: &mut W, + ) -> ::core::result::Result<(), borsh::maybestd::io::Error> { + let variant_idx: u8 = match self { + CompressedAccountVariant::UserRecord(..) => 0u8, + CompressedAccountVariant::PackedUserRecord(..) => 1u8, + CompressedAccountVariant::GameSession(..) => 2u8, + CompressedAccountVariant::PackedGameSession(..) => 3u8, + CompressedAccountVariant::PlaceholderRecord(..) => 4u8, + CompressedAccountVariant::PackedPlaceholderRecord(..) => 5u8, + CompressedAccountVariant::CompressibleTokenAccountPacked(..) => 6u8, + CompressedAccountVariant::CompressibleTokenData(..) => 7u8, + }; + writer.write_all(&variant_idx.to_le_bytes())?; + match self { + CompressedAccountVariant::UserRecord(id0) => { + borsh::BorshSerialize::serialize(id0, writer)?; + } + CompressedAccountVariant::PackedUserRecord(id0) => { + borsh::BorshSerialize::serialize(id0, writer)?; + } + CompressedAccountVariant::GameSession(id0) => { + borsh::BorshSerialize::serialize(id0, writer)?; + } + CompressedAccountVariant::PackedGameSession(id0) => { + borsh::BorshSerialize::serialize(id0, writer)?; + } + CompressedAccountVariant::PlaceholderRecord(id0) => { + borsh::BorshSerialize::serialize(id0, writer)?; + } + CompressedAccountVariant::PackedPlaceholderRecord(id0) => { + borsh::BorshSerialize::serialize(id0, writer)?; + } + CompressedAccountVariant::CompressibleTokenAccountPacked(id0) => { + borsh::BorshSerialize::serialize(id0, writer)?; + } + CompressedAccountVariant::CompressibleTokenData(id0) => { + borsh::BorshSerialize::serialize(id0, writer)?; + } + } + Ok(()) + } +} +impl anchor_lang::idl::build::IdlBuild for CompressedAccountVariant { + fn create_type() -> Option { + Some(anchor_lang::idl::types::IdlTypeDef { + name: Self::get_full_path(), + docs: ::alloc::vec::Vec::new(), + serialization: anchor_lang::idl::types::IdlSerialization::default(), + repr: None, + generics: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlTypeDefTy::Enum { + variants: <[_]>::into_vec( + ::alloc::boxed::box_new([ + anchor_lang::idl::types::IdlEnumVariant { + name: "UserRecord".into(), + fields: Some( + anchor_lang::idl::types::IdlDefinedFields::Tuple( + <[_]>::into_vec( + ::alloc::boxed::box_new([ + anchor_lang::idl::types::IdlType::Defined { + name: ::get_full_path(), + generics: ::alloc::vec::Vec::new(), + }, + ]), + ), + ), + ), + }, + anchor_lang::idl::types::IdlEnumVariant { + name: "PackedUserRecord".into(), + fields: Some( + anchor_lang::idl::types::IdlDefinedFields::Tuple( + <[_]>::into_vec( + ::alloc::boxed::box_new([ + anchor_lang::idl::types::IdlType::Defined { + name: ::get_full_path(), + generics: ::alloc::vec::Vec::new(), + }, + ]), + ), + ), + ), + }, + anchor_lang::idl::types::IdlEnumVariant { + name: "GameSession".into(), + fields: Some( + anchor_lang::idl::types::IdlDefinedFields::Tuple( + <[_]>::into_vec( + ::alloc::boxed::box_new([ + anchor_lang::idl::types::IdlType::Defined { + name: ::get_full_path(), + generics: ::alloc::vec::Vec::new(), + }, + ]), + ), + ), + ), + }, + anchor_lang::idl::types::IdlEnumVariant { + name: "PackedGameSession".into(), + fields: Some( + anchor_lang::idl::types::IdlDefinedFields::Tuple( + <[_]>::into_vec( + ::alloc::boxed::box_new([ + anchor_lang::idl::types::IdlType::Defined { + name: ::get_full_path(), + generics: ::alloc::vec::Vec::new(), + }, + ]), + ), + ), + ), + }, + anchor_lang::idl::types::IdlEnumVariant { + name: "PlaceholderRecord".into(), + fields: Some( + anchor_lang::idl::types::IdlDefinedFields::Tuple( + <[_]>::into_vec( + ::alloc::boxed::box_new([ + anchor_lang::idl::types::IdlType::Defined { + name: ::get_full_path(), + generics: ::alloc::vec::Vec::new(), + }, + ]), + ), + ), + ), + }, + anchor_lang::idl::types::IdlEnumVariant { + name: "PackedPlaceholderRecord".into(), + fields: Some( + anchor_lang::idl::types::IdlDefinedFields::Tuple( + <[_]>::into_vec( + ::alloc::boxed::box_new([ + anchor_lang::idl::types::IdlType::Defined { + name: ::get_full_path(), + generics: ::alloc::vec::Vec::new(), + }, + ]), + ), + ), + ), + }, + anchor_lang::idl::types::IdlEnumVariant { + name: "CompressibleTokenAccountPacked".into(), + fields: Some( + anchor_lang::idl::types::IdlDefinedFields::Tuple( + <[_]>::into_vec( + ::alloc::boxed::box_new([ + anchor_lang::idl::types::IdlType::Defined { + name: >::get_full_path(), + generics: <[_]>::into_vec( + ::alloc::boxed::box_new([ + anchor_lang::idl::types::IdlGenericArg::Type { + ty: anchor_lang::idl::types::IdlType::Defined { + name: ::get_full_path(), + generics: ::alloc::vec::Vec::new(), + }, + }, + ]), + ), + }, + ]), + ), + ), + ), + }, + anchor_lang::idl::types::IdlEnumVariant { + name: "CompressibleTokenData".into(), + fields: Some( + anchor_lang::idl::types::IdlDefinedFields::Tuple( + <[_]>::into_vec( + ::alloc::boxed::box_new([ + anchor_lang::idl::types::IdlType::Defined { + name: >::get_full_path(), + generics: <[_]>::into_vec( + ::alloc::boxed::box_new([ + anchor_lang::idl::types::IdlGenericArg::Type { + ty: anchor_lang::idl::types::IdlType::Defined { + name: ::get_full_path(), + generics: ::alloc::vec::Vec::new(), + }, + }, + ]), + ), + }, + ]), + ), + ), + ), + }, + ]), + ), + }, + }) + } + fn insert_types( + types: &mut std::collections::BTreeMap< + String, + anchor_lang::idl::types::IdlTypeDef, + >, + ) { + if let Some(ty) = ::create_type() { + types.insert(::get_full_path(), ty); + ::insert_types(types); + } + if let Some(ty) = ::create_type() { + types.insert(::get_full_path(), ty); + ::insert_types(types); + } + if let Some(ty) = ::create_type() { + types.insert(::get_full_path(), ty); + ::insert_types(types); + } + if let Some(ty) = ::create_type() { + types.insert(::get_full_path(), ty); + ::insert_types(types); + } + if let Some(ty) = ::create_type() { + types.insert(::get_full_path(), ty); + ::insert_types(types); + } + if let Some(ty) = ::create_type() { + types.insert(::get_full_path(), ty); + ::insert_types(types); + } + if let Some(ty) = >::create_type() { + types + .insert( + >::get_full_path(), + ty, + ); + >::insert_types(types); + } + if let Some(ty) = ::create_type() { + types.insert(::get_full_path(), ty); + ::insert_types(types); + } + if let Some(ty) = >::create_type() { + types + .insert( + >::get_full_path(), + ty, + ); + >::insert_types(types); + } + if let Some(ty) = ::create_type() { + types.insert(::get_full_path(), ty); + ::insert_types(types); + } + } + fn get_full_path() -> String { + ::alloc::__export::must_use({ + let res = ::alloc::fmt::format( + format_args!( + "{0}::{1}", + "anchor_compressible_derived", + "CompressedAccountVariant", + ), + ); + res + }) + } +} +impl borsh::de::BorshDeserialize for CompressedAccountVariant +where + UserRecord: borsh::BorshDeserialize, + PackedUserRecord: borsh::BorshDeserialize, + GameSession: borsh::BorshDeserialize, + PackedGameSession: borsh::BorshDeserialize, + PlaceholderRecord: borsh::BorshDeserialize, + PackedPlaceholderRecord: borsh::BorshDeserialize, + light_sdk::token::PackedCompressibleTokenDataWithVariant< + CTokenAccountVariant, + >: borsh::BorshDeserialize, + light_sdk::token::CompressibleTokenDataWithVariant< + CTokenAccountVariant, + >: borsh::BorshDeserialize, +{ + fn deserialize_reader( + reader: &mut R, + ) -> ::core::result::Result { + let tag = ::deserialize_reader(reader)?; + ::deserialize_variant(reader, tag) + } +} +impl borsh::de::EnumExt for CompressedAccountVariant +where + UserRecord: borsh::BorshDeserialize, + PackedUserRecord: borsh::BorshDeserialize, + GameSession: borsh::BorshDeserialize, + PackedGameSession: borsh::BorshDeserialize, + PlaceholderRecord: borsh::BorshDeserialize, + PackedPlaceholderRecord: borsh::BorshDeserialize, + light_sdk::token::PackedCompressibleTokenDataWithVariant< + CTokenAccountVariant, + >: borsh::BorshDeserialize, + light_sdk::token::CompressibleTokenDataWithVariant< + CTokenAccountVariant, + >: borsh::BorshDeserialize, +{ + fn deserialize_variant( + reader: &mut R, + variant_idx: u8, + ) -> ::core::result::Result { + let mut return_value = match variant_idx { + 0u8 => { + CompressedAccountVariant::UserRecord( + borsh::BorshDeserialize::deserialize_reader(reader)?, + ) + } + 1u8 => { + CompressedAccountVariant::PackedUserRecord( + borsh::BorshDeserialize::deserialize_reader(reader)?, + ) + } + 2u8 => { + CompressedAccountVariant::GameSession( + borsh::BorshDeserialize::deserialize_reader(reader)?, + ) + } + 3u8 => { + CompressedAccountVariant::PackedGameSession( + borsh::BorshDeserialize::deserialize_reader(reader)?, + ) + } + 4u8 => { + CompressedAccountVariant::PlaceholderRecord( + borsh::BorshDeserialize::deserialize_reader(reader)?, + ) + } + 5u8 => { + CompressedAccountVariant::PackedPlaceholderRecord( + borsh::BorshDeserialize::deserialize_reader(reader)?, + ) + } + 6u8 => { + CompressedAccountVariant::CompressibleTokenAccountPacked( + borsh::BorshDeserialize::deserialize_reader(reader)?, + ) + } + 7u8 => { + CompressedAccountVariant::CompressibleTokenData( + borsh::BorshDeserialize::deserialize_reader(reader)?, + ) + } + _ => { + return Err( + borsh::maybestd::io::Error::new( + borsh::maybestd::io::ErrorKind::InvalidInput, + ::alloc::__export::must_use({ + let res = ::alloc::fmt::format( + format_args!("Unexpected variant index: {0:?}", variant_idx), + ); + res + }), + ), + ); + } + }; + Ok(return_value) + } +} +impl Default for CompressedAccountVariant { + fn default() -> Self { + Self::UserRecord(UserRecord::default()) + } +} +impl light_hasher::DataHasher for CompressedAccountVariant { + fn hash( + &self, + ) -> std::result::Result<[u8; 32], light_hasher::HasherError> { + match self { + CompressedAccountVariant::UserRecord(data) => { + ::hash::(data) + } + CompressedAccountVariant::PackedUserRecord(_) => { + ::core::panicking::panic("internal error: entered unreachable code") + } + CompressedAccountVariant::GameSession(data) => { + ::hash::(data) + } + CompressedAccountVariant::PackedGameSession(_) => { + ::core::panicking::panic("internal error: entered unreachable code") + } + CompressedAccountVariant::PlaceholderRecord(data) => { + ::hash::(data) + } + CompressedAccountVariant::PackedPlaceholderRecord(_) => { + ::core::panicking::panic("internal error: entered unreachable code") + } + Self::CompressibleTokenAccountPacked(_) => { + ::core::panicking::panic("internal error: entered unreachable code") + } + Self::CompressibleTokenData(_) => { + ::core::panicking::panic("internal error: entered unreachable code") + } + } + } +} +impl light_sdk::LightDiscriminator for CompressedAccountVariant { + const LIGHT_DISCRIMINATOR: [u8; 8] = [0; 8]; + const LIGHT_DISCRIMINATOR_SLICE: &'static [u8] = &Self::LIGHT_DISCRIMINATOR; +} +impl light_sdk::compressible::HasCompressionInfo for CompressedAccountVariant { + fn compression_info(&self) -> &light_sdk::compressible::CompressionInfo { + match self { + CompressedAccountVariant::UserRecord(data) => { + ::compression_info( + data, + ) + } + CompressedAccountVariant::PackedUserRecord(_) => { + ::core::panicking::panic("internal error: entered unreachable code") + } + CompressedAccountVariant::GameSession(data) => { + ::compression_info( + data, + ) + } + CompressedAccountVariant::PackedGameSession(_) => { + ::core::panicking::panic("internal error: entered unreachable code") + } + CompressedAccountVariant::PlaceholderRecord(data) => { + ::compression_info( + data, + ) + } + CompressedAccountVariant::PackedPlaceholderRecord(_) => { + ::core::panicking::panic("internal error: entered unreachable code") + } + Self::CompressibleTokenAccountPacked(_) => { + ::core::panicking::panic("internal error: entered unreachable code") + } + Self::CompressibleTokenData(_) => { + ::core::panicking::panic("internal error: entered unreachable code") + } + } + } + fn compression_info_mut(&mut self) -> &mut light_sdk::compressible::CompressionInfo { + match self { + CompressedAccountVariant::UserRecord(data) => { + ::compression_info_mut( + data, + ) + } + CompressedAccountVariant::PackedUserRecord(_) => { + ::core::panicking::panic("internal error: entered unreachable code") + } + CompressedAccountVariant::GameSession(data) => { + ::compression_info_mut( + data, + ) + } + CompressedAccountVariant::PackedGameSession(_) => { + ::core::panicking::panic("internal error: entered unreachable code") + } + CompressedAccountVariant::PlaceholderRecord(data) => { + ::compression_info_mut( + data, + ) + } + CompressedAccountVariant::PackedPlaceholderRecord(_) => { + ::core::panicking::panic("internal error: entered unreachable code") + } + Self::CompressibleTokenAccountPacked(_) => { + ::core::panicking::panic("internal error: entered unreachable code") + } + Self::CompressibleTokenData(_) => { + ::core::panicking::panic("internal error: entered unreachable code") + } + } + } + fn compression_info_mut_opt( + &mut self, + ) -> &mut Option { + match self { + CompressedAccountVariant::UserRecord(data) => { + ::compression_info_mut_opt( + data, + ) + } + CompressedAccountVariant::PackedUserRecord(_) => { + ::core::panicking::panic("internal error: entered unreachable code") + } + CompressedAccountVariant::GameSession(data) => { + ::compression_info_mut_opt( + data, + ) + } + CompressedAccountVariant::PackedGameSession(_) => { + ::core::panicking::panic("internal error: entered unreachable code") + } + CompressedAccountVariant::PlaceholderRecord(data) => { + ::compression_info_mut_opt( + data, + ) + } + CompressedAccountVariant::PackedPlaceholderRecord(_) => { + ::core::panicking::panic("internal error: entered unreachable code") + } + Self::CompressibleTokenAccountPacked(_) => { + ::core::panicking::panic("internal error: entered unreachable code") + } + Self::CompressibleTokenData(_) => { + ::core::panicking::panic("internal error: entered unreachable code") + } + } + } + fn set_compression_info_none(&mut self) { + match self { + CompressedAccountVariant::UserRecord(data) => { + ::set_compression_info_none( + data, + ) + } + CompressedAccountVariant::PackedUserRecord(_) => { + ::core::panicking::panic("internal error: entered unreachable code") + } + CompressedAccountVariant::GameSession(data) => { + ::set_compression_info_none( + data, + ) + } + CompressedAccountVariant::PackedGameSession(_) => { + ::core::panicking::panic("internal error: entered unreachable code") + } + CompressedAccountVariant::PlaceholderRecord(data) => { + ::set_compression_info_none( + data, + ) + } + CompressedAccountVariant::PackedPlaceholderRecord(_) => { + ::core::panicking::panic("internal error: entered unreachable code") + } + Self::CompressibleTokenAccountPacked(_) => { + ::core::panicking::panic("internal error: entered unreachable code") + } + Self::CompressibleTokenData(_) => { + ::core::panicking::panic("internal error: entered unreachable code") + } + } + } +} +impl light_sdk::account::Size for CompressedAccountVariant { + fn size(&self) -> usize { + match self { + CompressedAccountVariant::UserRecord(data) => { + ::size(data) + } + CompressedAccountVariant::PackedUserRecord(_) => { + ::core::panicking::panic("internal error: entered unreachable code") + } + CompressedAccountVariant::GameSession(data) => { + ::size(data) + } + CompressedAccountVariant::PackedGameSession(_) => { + ::core::panicking::panic("internal error: entered unreachable code") + } + CompressedAccountVariant::PlaceholderRecord(data) => { + ::size(data) + } + CompressedAccountVariant::PackedPlaceholderRecord(_) => { + ::core::panicking::panic("internal error: entered unreachable code") + } + Self::CompressibleTokenAccountPacked(_) => { + ::core::panicking::panic("internal error: entered unreachable code") + } + Self::CompressibleTokenData(_) => { + ::core::panicking::panic("internal error: entered unreachable code") + } + } + } +} +impl light_sdk::compressible::Pack for CompressedAccountVariant { + type Packed = Self; + fn pack( + &self, + remaining_accounts: &mut light_sdk::instruction::PackedAccounts, + ) -> Self::Packed { + match self { + CompressedAccountVariant::PackedUserRecord(_) => { + ::core::panicking::panic("internal error: entered unreachable code") + } + CompressedAccountVariant::UserRecord(data) => { + CompressedAccountVariant::PackedUserRecord( + ::pack( + data, + remaining_accounts, + ), + ) + } + CompressedAccountVariant::PackedGameSession(_) => { + ::core::panicking::panic("internal error: entered unreachable code") + } + CompressedAccountVariant::GameSession(data) => { + CompressedAccountVariant::PackedGameSession( + ::pack( + data, + remaining_accounts, + ), + ) + } + CompressedAccountVariant::PackedPlaceholderRecord(_) => { + ::core::panicking::panic("internal error: entered unreachable code") + } + CompressedAccountVariant::PlaceholderRecord(data) => { + CompressedAccountVariant::PackedPlaceholderRecord( + ::pack( + data, + remaining_accounts, + ), + ) + } + Self::CompressibleTokenAccountPacked(_) => { + ::core::panicking::panic("internal error: entered unreachable code") + } + Self::CompressibleTokenData(data) => { + Self::CompressibleTokenAccountPacked(data.pack(remaining_accounts)) + } + } + } +} +impl light_sdk::compressible::Unpack for CompressedAccountVariant { + type Unpacked = Self; + fn unpack( + &self, + remaining_accounts: &[anchor_lang::prelude::AccountInfo], + ) -> std::result::Result { + match self { + CompressedAccountVariant::PackedUserRecord(data) => { + Ok( + CompressedAccountVariant::UserRecord( + ::unpack( + data, + remaining_accounts, + )?, + ), + ) + } + CompressedAccountVariant::UserRecord(_) => { + ::core::panicking::panic("internal error: entered unreachable code") + } + CompressedAccountVariant::PackedGameSession(data) => { + Ok( + CompressedAccountVariant::GameSession( + ::unpack( + data, + remaining_accounts, + )?, + ), + ) + } + CompressedAccountVariant::GameSession(_) => { + ::core::panicking::panic("internal error: entered unreachable code") + } + CompressedAccountVariant::PackedPlaceholderRecord(data) => { + Ok( + CompressedAccountVariant::PlaceholderRecord( + ::unpack( + data, + remaining_accounts, + )?, + ), + ) + } + CompressedAccountVariant::PlaceholderRecord(_) => { + ::core::panicking::panic("internal error: entered unreachable code") + } + Self::CompressibleTokenAccountPacked(_data) => Ok(self.clone()), + Self::CompressibleTokenData(_data) => { + ::core::panicking::panic("internal error: entered unreachable code") + } + } + } +} +pub struct CompressedAccountData { + pub meta: light_sdk::instruction::account_meta::CompressedAccountMetaNoLamportsNoAddress, + pub data: CompressedAccountVariant, +} +#[automatically_derived] +impl ::core::clone::Clone for CompressedAccountData { + #[inline] + fn clone(&self) -> CompressedAccountData { + CompressedAccountData { + meta: ::core::clone::Clone::clone(&self.meta), + data: ::core::clone::Clone::clone(&self.data), + } + } +} +#[automatically_derived] +impl ::core::fmt::Debug for CompressedAccountData { + #[inline] + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + ::core::fmt::Formatter::debug_struct_field2_finish( + f, + "CompressedAccountData", + "meta", + &self.meta, + "data", + &&self.data, + ) + } +} +impl borsh::de::BorshDeserialize for CompressedAccountData +where + light_sdk::instruction::account_meta::CompressedAccountMetaNoLamportsNoAddress: borsh::BorshDeserialize, + CompressedAccountVariant: borsh::BorshDeserialize, +{ + fn deserialize_reader( + reader: &mut R, + ) -> ::core::result::Result { + Ok(Self { + meta: borsh::BorshDeserialize::deserialize_reader(reader)?, + data: borsh::BorshDeserialize::deserialize_reader(reader)?, + }) + } +} +impl borsh::ser::BorshSerialize for CompressedAccountData +where + light_sdk::instruction::account_meta::CompressedAccountMetaNoLamportsNoAddress: borsh::ser::BorshSerialize, + CompressedAccountVariant: borsh::ser::BorshSerialize, +{ + fn serialize( + &self, + writer: &mut W, + ) -> ::core::result::Result<(), borsh::maybestd::io::Error> { + borsh::BorshSerialize::serialize(&self.meta, writer)?; + borsh::BorshSerialize::serialize(&self.data, writer)?; + Ok(()) + } +} +impl anchor_lang::idl::build::IdlBuild for CompressedAccountData { + fn create_type() -> Option { + Some(anchor_lang::idl::types::IdlTypeDef { + name: Self::get_full_path(), + docs: ::alloc::vec::Vec::new(), + serialization: anchor_lang::idl::types::IdlSerialization::default(), + repr: None, + generics: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlTypeDefTy::Struct { + fields: Some( + anchor_lang::idl::types::IdlDefinedFields::Named( + <[_]>::into_vec( + ::alloc::boxed::box_new([ + anchor_lang::idl::types::IdlField { + name: "meta".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::Defined { + name: ::get_full_path(), + generics: ::alloc::vec::Vec::new(), + }, + }, + anchor_lang::idl::types::IdlField { + name: "data".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::Defined { + name: ::get_full_path(), + generics: ::alloc::vec::Vec::new(), + }, + }, + ]), + ), + ), + ), + }, + }) + } + fn insert_types( + types: &mut std::collections::BTreeMap< + String, + anchor_lang::idl::types::IdlTypeDef, + >, + ) { + if let Some(ty) = ::create_type() { + types + .insert( + ::get_full_path(), + ty, + ); + ::insert_types( + types, + ); + } + if let Some(ty) = ::create_type() { + types.insert(::get_full_path(), ty); + ::insert_types(types); + } + } + fn get_full_path() -> String { + ::alloc::__export::must_use({ + let res = ::alloc::fmt::format( + format_args!( + "{0}::{1}", + "anchor_compressible_derived", + "CompressedAccountData", + ), + ); + res + }) + } +} +use self::anchor_compressible_derived::*; +/// # Safety +#[no_mangle] +pub unsafe extern "C" fn entrypoint(input: *mut u8) -> u64 { + let (program_id, accounts, instruction_data) = unsafe { + ::solana_program_entrypoint::deserialize(input) + }; + match entry(program_id, &accounts, instruction_data) { + Ok(()) => ::solana_program_entrypoint::SUCCESS, + Err(error) => error.into(), + } +} +/// The Anchor codegen exposes a programming model where a user defines +/// a set of methods inside of a `#[program]` module in a way similar +/// to writing RPC request handlers. The macro then generates a bunch of +/// code wrapping these user defined methods into something that can be +/// executed on Solana. +/// +/// These methods fall into one category for now. +/// +/// Global methods - regular methods inside of the `#[program]`. +/// +/// Care must be taken by the codegen to prevent collisions between +/// methods in these different namespaces. For this reason, Anchor uses +/// a variant of sighash to perform method dispatch, rather than +/// something like a simple enum variant discriminator. +/// +/// The execution flow of the generated code can be roughly outlined: +/// +/// * Start program via the entrypoint. +/// * Check whether the declared program id matches the input program +/// id. If it's not, return an error. +/// * Find and invoke the method based on whether the instruction data +/// starts with the method's discriminator. +/// * Run the method handler wrapper. This wraps the code the user +/// actually wrote, deserializing the accounts, constructing the +/// context, invoking the user's code, and finally running the exit +/// routine, which typically persists account changes. +/// +/// The `entry` function here, defines the standard entry to a Solana +/// program, where execution begins. +pub fn entry<'info>( + program_id: &Pubkey, + accounts: &'info [AccountInfo<'info>], + data: &[u8], +) -> anchor_lang::solana_program::entrypoint::ProgramResult { + try_entry(program_id, accounts, data) + .map_err(|e| { + e.log(); + e.into() + }) +} +fn try_entry<'info>( + program_id: &Pubkey, + accounts: &'info [AccountInfo<'info>], + data: &[u8], +) -> anchor_lang::Result<()> { + if *program_id != ID { + return Err(anchor_lang::error::ErrorCode::DeclaredProgramIdMismatch.into()); + } + dispatch(program_id, accounts, data) +} +/// Module representing the program. +pub mod program { + use super::*; + /// Type representing the program. + pub struct AnchorCompressibleDerived; + #[automatically_derived] + impl ::core::clone::Clone for AnchorCompressibleDerived { + #[inline] + fn clone(&self) -> AnchorCompressibleDerived { + AnchorCompressibleDerived + } + } + impl anchor_lang::Id for AnchorCompressibleDerived { + fn id() -> Pubkey { + ID + } + } +} +/// Performs method dispatch. +/// +/// Each instruction's discriminator is checked until the given instruction data starts with +/// the current discriminator. +/// +/// If a match is found, the instruction handler is called using the given instruction data +/// excluding the prepended discriminator bytes. +/// +/// If no match is found, the fallback function is executed if it exists, or an error is +/// returned if it doesn't exist. +fn dispatch<'info>( + program_id: &Pubkey, + accounts: &'info [AccountInfo<'info>], + data: &[u8], +) -> anchor_lang::Result<()> { + if data.starts_with(instruction::InitializeCompressionConfig::DISCRIMINATOR) { + return __private::__global::initialize_compression_config( + program_id, + accounts, + &data[instruction::InitializeCompressionConfig::DISCRIMINATOR.len()..], + ); + } + if data.starts_with(instruction::UpdateCompressionConfig::DISCRIMINATOR) { + return __private::__global::update_compression_config( + program_id, + accounts, + &data[instruction::UpdateCompressionConfig::DISCRIMINATOR.len()..], + ); + } + if data.starts_with(instruction::CompressAccountsIdempotent::DISCRIMINATOR) { + return __private::__global::compress_accounts_idempotent( + program_id, + accounts, + &data[instruction::CompressAccountsIdempotent::DISCRIMINATOR.len()..], + ); + } + if data.starts_with(instruction::CreateRecord::DISCRIMINATOR) { + return __private::__global::create_record( + program_id, + accounts, + &data[instruction::CreateRecord::DISCRIMINATOR.len()..], + ); + } + if data.starts_with(instruction::CreateGameSession::DISCRIMINATOR) { + return __private::__global::create_game_session( + program_id, + accounts, + &data[instruction::CreateGameSession::DISCRIMINATOR.len()..], + ); + } + if data.starts_with(instruction::CreateUserRecordAndGameSession::DISCRIMINATOR) { + return __private::__global::create_user_record_and_game_session( + program_id, + accounts, + &data[instruction::CreateUserRecordAndGameSession::DISCRIMINATOR.len()..], + ); + } + if data.starts_with(instruction::CreatePlaceholderRecord::DISCRIMINATOR) { + return __private::__global::create_placeholder_record( + program_id, + accounts, + &data[instruction::CreatePlaceholderRecord::DISCRIMINATOR.len()..], + ); + } + if data.starts_with(instruction::UpdateRecord::DISCRIMINATOR) { + return __private::__global::update_record( + program_id, + accounts, + &data[instruction::UpdateRecord::DISCRIMINATOR.len()..], + ); + } + if data.starts_with(instruction::UpdateGameSession::DISCRIMINATOR) { + return __private::__global::update_game_session( + program_id, + accounts, + &data[instruction::UpdateGameSession::DISCRIMINATOR.len()..], + ); + } + if data.starts_with(instruction::DecompressAccountsIdempotent::DISCRIMINATOR) { + return __private::__global::decompress_accounts_idempotent( + program_id, + accounts, + &data[instruction::DecompressAccountsIdempotent::DISCRIMINATOR.len()..], + ); + } + if data.starts_with(anchor_lang::idl::IDL_IX_TAG_LE) { + #[cfg(not(feature = "no-idl"))] + return __private::__idl::__idl_dispatch( + program_id, + accounts, + &data[anchor_lang::idl::IDL_IX_TAG_LE.len()..], + ); + } + if data.starts_with(anchor_lang::event::EVENT_IX_TAG_LE) { + return Err(anchor_lang::error::ErrorCode::EventInstructionStub.into()); + } + Err(anchor_lang::error::ErrorCode::InstructionFallbackNotFound.into()) +} +/// Create a private module to not clutter the program's namespace. +/// Defines an entrypoint for each individual instruction handler +/// wrapper. +mod __private { + use super::*; + /// __idl mod defines handlers for injected Anchor IDL instructions. + pub mod __idl { + use super::*; + #[inline(never)] + #[cfg(not(feature = "no-idl"))] + pub fn __idl_dispatch<'info>( + program_id: &Pubkey, + accounts: &'info [AccountInfo<'info>], + idl_ix_data: &[u8], + ) -> anchor_lang::Result<()> { + let mut accounts = accounts; + let mut data: &[u8] = idl_ix_data; + let ix = anchor_lang::idl::IdlInstruction::deserialize(&mut data) + .map_err(|_| { + anchor_lang::error::ErrorCode::InstructionDidNotDeserialize + })?; + match ix { + anchor_lang::idl::IdlInstruction::Create { data_len } => { + let mut bumps = ::Bumps::default(); + let mut reallocs = std::collections::BTreeSet::new(); + let mut accounts = IdlCreateAccounts::try_accounts( + program_id, + &mut accounts, + &[], + &mut bumps, + &mut reallocs, + )?; + __idl_create_account(program_id, &mut accounts, data_len)?; + accounts.exit(program_id)?; + } + anchor_lang::idl::IdlInstruction::Resize { data_len } => { + let mut bumps = ::Bumps::default(); + let mut reallocs = std::collections::BTreeSet::new(); + let mut accounts = IdlResizeAccount::try_accounts( + program_id, + &mut accounts, + &[], + &mut bumps, + &mut reallocs, + )?; + __idl_resize_account(program_id, &mut accounts, data_len)?; + accounts.exit(program_id)?; + } + anchor_lang::idl::IdlInstruction::Close => { + let mut bumps = ::Bumps::default(); + let mut reallocs = std::collections::BTreeSet::new(); + let mut accounts = IdlCloseAccount::try_accounts( + program_id, + &mut accounts, + &[], + &mut bumps, + &mut reallocs, + )?; + __idl_close_account(program_id, &mut accounts)?; + accounts.exit(program_id)?; + } + anchor_lang::idl::IdlInstruction::CreateBuffer => { + let mut bumps = ::Bumps::default(); + let mut reallocs = std::collections::BTreeSet::new(); + let mut accounts = IdlCreateBuffer::try_accounts( + program_id, + &mut accounts, + &[], + &mut bumps, + &mut reallocs, + )?; + __idl_create_buffer(program_id, &mut accounts)?; + accounts.exit(program_id)?; + } + anchor_lang::idl::IdlInstruction::Write { data } => { + let mut bumps = ::Bumps::default(); + let mut reallocs = std::collections::BTreeSet::new(); + let mut accounts = IdlAccounts::try_accounts( + program_id, + &mut accounts, + &[], + &mut bumps, + &mut reallocs, + )?; + __idl_write(program_id, &mut accounts, data)?; + accounts.exit(program_id)?; + } + anchor_lang::idl::IdlInstruction::SetAuthority { new_authority } => { + let mut bumps = ::Bumps::default(); + let mut reallocs = std::collections::BTreeSet::new(); + let mut accounts = IdlAccounts::try_accounts( + program_id, + &mut accounts, + &[], + &mut bumps, + &mut reallocs, + )?; + __idl_set_authority(program_id, &mut accounts, new_authority)?; + accounts.exit(program_id)?; + } + anchor_lang::idl::IdlInstruction::SetBuffer => { + let mut bumps = ::Bumps::default(); + let mut reallocs = std::collections::BTreeSet::new(); + let mut accounts = IdlSetBuffer::try_accounts( + program_id, + &mut accounts, + &[], + &mut bumps, + &mut reallocs, + )?; + __idl_set_buffer(program_id, &mut accounts)?; + accounts.exit(program_id)?; + } + } + Ok(()) + } + use anchor_lang::idl::ERASED_AUTHORITY; + pub struct IdlAccount { + pub authority: Pubkey, + pub data_len: u32, + } + #[automatically_derived] + impl ::core::fmt::Debug for IdlAccount { + #[inline] + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + ::core::fmt::Formatter::debug_struct_field2_finish( + f, + "IdlAccount", + "authority", + &self.authority, + "data_len", + &&self.data_len, + ) + } + } + impl borsh::ser::BorshSerialize for IdlAccount + where + Pubkey: borsh::ser::BorshSerialize, + u32: borsh::ser::BorshSerialize, + { + fn serialize( + &self, + writer: &mut W, + ) -> ::core::result::Result<(), borsh::maybestd::io::Error> { + borsh::BorshSerialize::serialize(&self.authority, writer)?; + borsh::BorshSerialize::serialize(&self.data_len, writer)?; + Ok(()) + } + } + impl anchor_lang::idl::build::IdlBuild for IdlAccount { + fn create_type() -> Option { + Some(anchor_lang::idl::types::IdlTypeDef { + name: Self::get_full_path(), + docs: ::alloc::vec::Vec::new(), + serialization: anchor_lang::idl::types::IdlSerialization::default(), + repr: None, + generics: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlTypeDefTy::Struct { + fields: Some( + anchor_lang::idl::types::IdlDefinedFields::Named( + <[_]>::into_vec( + ::alloc::boxed::box_new([ + anchor_lang::idl::types::IdlField { + name: "authority".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::Pubkey, + }, + anchor_lang::idl::types::IdlField { + name: "data_len".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::U32, + }, + ]), + ), + ), + ), + }, + }) + } + fn insert_types( + types: &mut std::collections::BTreeMap< + String, + anchor_lang::idl::types::IdlTypeDef, + >, + ) {} + fn get_full_path() -> String { + ::alloc::__export::must_use({ + let res = ::alloc::fmt::format( + format_args!( + "{0}::{1}", + "anchor_compressible_derived::__private::__idl", + "IdlAccount", + ), + ); + res + }) + } + } + impl borsh::de::BorshDeserialize for IdlAccount + where + Pubkey: borsh::BorshDeserialize, + u32: borsh::BorshDeserialize, + { + fn deserialize_reader( + reader: &mut R, + ) -> ::core::result::Result { + Ok(Self { + authority: borsh::BorshDeserialize::deserialize_reader(reader)?, + data_len: borsh::BorshDeserialize::deserialize_reader(reader)?, + }) + } + } + #[automatically_derived] + impl ::core::clone::Clone for IdlAccount { + #[inline] + fn clone(&self) -> IdlAccount { + IdlAccount { + authority: ::core::clone::Clone::clone(&self.authority), + data_len: ::core::clone::Clone::clone(&self.data_len), + } + } + } + #[automatically_derived] + impl anchor_lang::AccountSerialize for IdlAccount { + fn try_serialize( + &self, + writer: &mut W, + ) -> anchor_lang::Result<()> { + if writer.write_all(IdlAccount::DISCRIMINATOR).is_err() { + return Err( + anchor_lang::error::ErrorCode::AccountDidNotSerialize.into(), + ); + } + if AnchorSerialize::serialize(self, writer).is_err() { + return Err( + anchor_lang::error::ErrorCode::AccountDidNotSerialize.into(), + ); + } + Ok(()) + } + } + #[automatically_derived] + impl anchor_lang::AccountDeserialize for IdlAccount { + fn try_deserialize(buf: &mut &[u8]) -> anchor_lang::Result { + if buf.len() < IdlAccount::DISCRIMINATOR.len() { + return Err( + anchor_lang::error::ErrorCode::AccountDiscriminatorNotFound + .into(), + ); + } + let given_disc = &buf[..IdlAccount::DISCRIMINATOR.len()]; + if IdlAccount::DISCRIMINATOR != given_disc { + return Err( + anchor_lang::error::Error::from(anchor_lang::error::AnchorError { + error_name: anchor_lang::error::ErrorCode::AccountDiscriminatorMismatch + .name(), + error_code_number: anchor_lang::error::ErrorCode::AccountDiscriminatorMismatch + .into(), + error_msg: anchor_lang::error::ErrorCode::AccountDiscriminatorMismatch + .to_string(), + error_origin: Some( + anchor_lang::error::ErrorOrigin::Source(anchor_lang::error::Source { + filename: "sdk-tests/anchor-compressible-derived/src/lib.rs", + line: 87u32, + }), + ), + compared_values: None, + }) + .with_account_name("IdlAccount"), + ); + } + Self::try_deserialize_unchecked(buf) + } + fn try_deserialize_unchecked(buf: &mut &[u8]) -> anchor_lang::Result { + let mut data: &[u8] = &buf[IdlAccount::DISCRIMINATOR.len()..]; + AnchorDeserialize::deserialize(&mut data) + .map_err(|_| { + anchor_lang::error::ErrorCode::AccountDidNotDeserialize.into() + }) + } + } + #[automatically_derived] + impl anchor_lang::Discriminator for IdlAccount { + const DISCRIMINATOR: &'static [u8] = &[24, 70, 98, 191, 58, 144, 123, 158]; + } + impl IdlAccount { + pub fn address(program_id: &Pubkey) -> Pubkey { + let program_signer = Pubkey::find_program_address(&[], program_id).0; + Pubkey::create_with_seed(&program_signer, IdlAccount::seed(), program_id) + .expect("Seed is always valid") + } + pub fn seed() -> &'static str { + "anchor:idl" + } + } + impl anchor_lang::Owner for IdlAccount { + fn owner() -> Pubkey { + crate::ID + } + } + pub struct IdlCreateAccounts<'info> { + #[account(signer)] + pub from: AccountInfo<'info>, + #[account(mut)] + pub to: AccountInfo<'info>, + #[account(seeds = [], bump)] + pub base: AccountInfo<'info>, + pub system_program: Program<'info, System>, + #[account(executable)] + pub program: AccountInfo<'info>, + } + #[automatically_derived] + impl<'info> anchor_lang::Accounts<'info, IdlCreateAccountsBumps> + for IdlCreateAccounts<'info> + where + 'info: 'info, + { + #[inline(never)] + fn try_accounts( + __program_id: &anchor_lang::solana_program::pubkey::Pubkey, + __accounts: &mut &'info [anchor_lang::solana_program::account_info::AccountInfo< + 'info, + >], + __ix_data: &[u8], + __bumps: &mut IdlCreateAccountsBumps, + __reallocs: &mut std::collections::BTreeSet< + anchor_lang::solana_program::pubkey::Pubkey, + >, + ) -> anchor_lang::Result { + let from: AccountInfo = anchor_lang::Accounts::try_accounts( + __program_id, + __accounts, + __ix_data, + __bumps, + __reallocs, + ) + .map_err(|e| e.with_account_name("from"))?; + let to: AccountInfo = anchor_lang::Accounts::try_accounts( + __program_id, + __accounts, + __ix_data, + __bumps, + __reallocs, + ) + .map_err(|e| e.with_account_name("to"))?; + let base: AccountInfo = anchor_lang::Accounts::try_accounts( + __program_id, + __accounts, + __ix_data, + __bumps, + __reallocs, + ) + .map_err(|e| e.with_account_name("base"))?; + let system_program: anchor_lang::accounts::program::Program = anchor_lang::Accounts::try_accounts( + __program_id, + __accounts, + __ix_data, + __bumps, + __reallocs, + ) + .map_err(|e| e.with_account_name("system_program"))?; + let program: AccountInfo = anchor_lang::Accounts::try_accounts( + __program_id, + __accounts, + __ix_data, + __bumps, + __reallocs, + ) + .map_err(|e| e.with_account_name("program"))?; + if !&from.is_signer { + return Err( + anchor_lang::error::Error::from( + anchor_lang::error::ErrorCode::ConstraintSigner, + ) + .with_account_name("from"), + ); + } + if !&to.is_writable { + return Err( + anchor_lang::error::Error::from( + anchor_lang::error::ErrorCode::ConstraintMut, + ) + .with_account_name("to"), + ); + } + let (__pda_address, __bump) = Pubkey::find_program_address( + &[], + &__program_id, + ); + __bumps.base = __bump; + if base.key() != __pda_address { + return Err( + anchor_lang::error::Error::from( + anchor_lang::error::ErrorCode::ConstraintSeeds, + ) + .with_account_name("base") + .with_pubkeys((base.key(), __pda_address)), + ); + } + if !&program.executable { + return Err( + anchor_lang::error::Error::from( + anchor_lang::error::ErrorCode::ConstraintExecutable, + ) + .with_account_name("program"), + ); + } + Ok(IdlCreateAccounts { + from, + to, + base, + system_program, + program, + }) + } + } + #[automatically_derived] + impl<'info> anchor_lang::ToAccountInfos<'info> for IdlCreateAccounts<'info> + where + 'info: 'info, + { + fn to_account_infos( + &self, + ) -> Vec> { + let mut account_infos = ::alloc::vec::Vec::new(); + account_infos.extend(self.from.to_account_infos()); + account_infos.extend(self.to.to_account_infos()); + account_infos.extend(self.base.to_account_infos()); + account_infos.extend(self.system_program.to_account_infos()); + account_infos.extend(self.program.to_account_infos()); + account_infos + } + } + #[automatically_derived] + impl<'info> anchor_lang::ToAccountMetas for IdlCreateAccounts<'info> { + fn to_account_metas( + &self, + is_signer: Option, + ) -> Vec { + let mut account_metas = ::alloc::vec::Vec::new(); + account_metas.extend(self.from.to_account_metas(Some(true))); + account_metas.extend(self.to.to_account_metas(None)); + account_metas.extend(self.base.to_account_metas(None)); + account_metas.extend(self.system_program.to_account_metas(None)); + account_metas.extend(self.program.to_account_metas(None)); + account_metas + } + } + #[automatically_derived] + impl<'info> anchor_lang::AccountsExit<'info> for IdlCreateAccounts<'info> + where + 'info: 'info, + { + fn exit( + &self, + program_id: &anchor_lang::solana_program::pubkey::Pubkey, + ) -> anchor_lang::Result<()> { + anchor_lang::AccountsExit::exit(&self.to, program_id) + .map_err(|e| e.with_account_name("to"))?; + Ok(()) + } + } + pub struct IdlCreateAccountsBumps { + pub base: u8, + } + #[automatically_derived] + impl ::core::fmt::Debug for IdlCreateAccountsBumps { + #[inline] + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + ::core::fmt::Formatter::debug_struct_field1_finish( + f, + "IdlCreateAccountsBumps", + "base", + &&self.base, + ) + } + } + impl Default for IdlCreateAccountsBumps { + fn default() -> Self { + IdlCreateAccountsBumps { + base: u8::MAX, + } + } + } + impl<'info> anchor_lang::Bumps for IdlCreateAccounts<'info> + where + 'info: 'info, + { + type Bumps = IdlCreateAccountsBumps; + } + /// An internal, Anchor generated module. This is used (as an + /// implementation detail), to generate a struct for a given + /// `#[derive(Accounts)]` implementation, where each field is a Pubkey, + /// instead of an `AccountInfo`. This is useful for clients that want + /// to generate a list of accounts, without explicitly knowing the + /// order all the fields should be in. + /// + /// To access the struct in this module, one should use the sibling + /// `accounts` module (also generated), which re-exports this. + pub(crate) mod __client_accounts_idl_create_accounts { + use super::*; + use anchor_lang::prelude::borsh; + /// Generated client accounts for [`IdlCreateAccounts`]. + pub struct IdlCreateAccounts { + pub from: Pubkey, + pub to: Pubkey, + pub base: Pubkey, + pub system_program: Pubkey, + pub program: Pubkey, + } + impl borsh::ser::BorshSerialize for IdlCreateAccounts + where + Pubkey: borsh::ser::BorshSerialize, + Pubkey: borsh::ser::BorshSerialize, + Pubkey: borsh::ser::BorshSerialize, + Pubkey: borsh::ser::BorshSerialize, + Pubkey: borsh::ser::BorshSerialize, + { + fn serialize( + &self, + writer: &mut W, + ) -> ::core::result::Result<(), borsh::maybestd::io::Error> { + borsh::BorshSerialize::serialize(&self.from, writer)?; + borsh::BorshSerialize::serialize(&self.to, writer)?; + borsh::BorshSerialize::serialize(&self.base, writer)?; + borsh::BorshSerialize::serialize(&self.system_program, writer)?; + borsh::BorshSerialize::serialize(&self.program, writer)?; + Ok(()) + } + } + impl anchor_lang::idl::build::IdlBuild for IdlCreateAccounts { + fn create_type() -> Option { + Some(anchor_lang::idl::types::IdlTypeDef { + name: Self::get_full_path(), + docs: <[_]>::into_vec( + ::alloc::boxed::box_new([ + "Generated client accounts for [`IdlCreateAccounts`]." + .into(), + ]), + ), + serialization: anchor_lang::idl::types::IdlSerialization::default(), + repr: None, + generics: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlTypeDefTy::Struct { + fields: Some( + anchor_lang::idl::types::IdlDefinedFields::Named( + <[_]>::into_vec( + ::alloc::boxed::box_new([ + anchor_lang::idl::types::IdlField { + name: "from".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::Pubkey, + }, + anchor_lang::idl::types::IdlField { + name: "to".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::Pubkey, + }, + anchor_lang::idl::types::IdlField { + name: "base".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::Pubkey, + }, + anchor_lang::idl::types::IdlField { + name: "system_program".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::Pubkey, + }, + anchor_lang::idl::types::IdlField { + name: "program".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::Pubkey, + }, + ]), + ), + ), + ), + }, + }) + } + fn insert_types( + types: &mut std::collections::BTreeMap< + String, + anchor_lang::idl::types::IdlTypeDef, + >, + ) {} + fn get_full_path() -> String { + ::alloc::__export::must_use({ + let res = ::alloc::fmt::format( + format_args!( + "{0}::{1}", + "anchor_compressible_derived::__private::__idl::__client_accounts_idl_create_accounts", + "IdlCreateAccounts", + ), + ); + res + }) + } + } + #[automatically_derived] + impl anchor_lang::ToAccountMetas for IdlCreateAccounts { + fn to_account_metas( + &self, + is_signer: Option, + ) -> Vec { + let mut account_metas = ::alloc::vec::Vec::new(); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new_readonly( + self.from, + true, + ), + ); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new( + self.to, + false, + ), + ); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new_readonly( + self.base, + false, + ), + ); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new_readonly( + self.system_program, + false, + ), + ); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new_readonly( + self.program, + false, + ), + ); + account_metas + } + } + } + /// An internal, Anchor generated module. This is used (as an + /// implementation detail), to generate a CPI struct for a given + /// `#[derive(Accounts)]` implementation, where each field is an + /// AccountInfo. + /// + /// To access the struct in this module, one should use the sibling + /// [`cpi::accounts`] module (also generated), which re-exports this. + pub(crate) mod __cpi_client_accounts_idl_create_accounts { + use super::*; + /// Generated CPI struct of the accounts for [`IdlCreateAccounts`]. + pub struct IdlCreateAccounts<'info> { + pub from: anchor_lang::solana_program::account_info::AccountInfo<'info>, + pub to: anchor_lang::solana_program::account_info::AccountInfo<'info>, + pub base: anchor_lang::solana_program::account_info::AccountInfo<'info>, + pub system_program: anchor_lang::solana_program::account_info::AccountInfo< + 'info, + >, + pub program: anchor_lang::solana_program::account_info::AccountInfo< + 'info, + >, + } + #[automatically_derived] + impl<'info> anchor_lang::ToAccountMetas for IdlCreateAccounts<'info> { + fn to_account_metas( + &self, + is_signer: Option, + ) -> Vec { + let mut account_metas = ::alloc::vec::Vec::new(); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new_readonly( + anchor_lang::Key::key(&self.from), + true, + ), + ); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new( + anchor_lang::Key::key(&self.to), + false, + ), + ); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new_readonly( + anchor_lang::Key::key(&self.base), + false, + ), + ); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new_readonly( + anchor_lang::Key::key(&self.system_program), + false, + ), + ); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new_readonly( + anchor_lang::Key::key(&self.program), + false, + ), + ); + account_metas + } + } + #[automatically_derived] + impl<'info> anchor_lang::ToAccountInfos<'info> for IdlCreateAccounts<'info> { + fn to_account_infos( + &self, + ) -> Vec> { + let mut account_infos = ::alloc::vec::Vec::new(); + account_infos + .extend( + anchor_lang::ToAccountInfos::to_account_infos(&self.from), + ); + account_infos + .extend(anchor_lang::ToAccountInfos::to_account_infos(&self.to)); + account_infos + .extend( + anchor_lang::ToAccountInfos::to_account_infos(&self.base), + ); + account_infos + .extend( + anchor_lang::ToAccountInfos::to_account_infos( + &self.system_program, + ), + ); + account_infos + .extend( + anchor_lang::ToAccountInfos::to_account_infos(&self.program), + ); + account_infos + } + } + } + impl<'info> IdlCreateAccounts<'info> { + pub fn __anchor_private_gen_idl_accounts( + accounts: &mut std::collections::BTreeMap< + String, + anchor_lang::idl::types::IdlAccount, + >, + types: &mut std::collections::BTreeMap< + String, + anchor_lang::idl::types::IdlTypeDef, + >, + ) -> Vec { + <[_]>::into_vec( + ::alloc::boxed::box_new([ + anchor_lang::idl::types::IdlInstructionAccountItem::Single(anchor_lang::idl::types::IdlInstructionAccount { + name: "from".into(), + docs: ::alloc::vec::Vec::new(), + writable: false, + signer: true, + optional: false, + address: None, + pda: None, + relations: ::alloc::vec::Vec::new(), + }), + anchor_lang::idl::types::IdlInstructionAccountItem::Single(anchor_lang::idl::types::IdlInstructionAccount { + name: "to".into(), + docs: ::alloc::vec::Vec::new(), + writable: true, + signer: false, + optional: false, + address: None, + pda: None, + relations: ::alloc::vec::Vec::new(), + }), + anchor_lang::idl::types::IdlInstructionAccountItem::Single(anchor_lang::idl::types::IdlInstructionAccount { + name: "base".into(), + docs: ::alloc::vec::Vec::new(), + writable: false, + signer: false, + optional: false, + address: None, + pda: None, + relations: ::alloc::vec::Vec::new(), + }), + anchor_lang::idl::types::IdlInstructionAccountItem::Single(anchor_lang::idl::types::IdlInstructionAccount { + name: "system_program".into(), + docs: ::alloc::vec::Vec::new(), + writable: false, + signer: false, + optional: false, + address: None, + pda: None, + relations: ::alloc::vec::Vec::new(), + }), + anchor_lang::idl::types::IdlInstructionAccountItem::Single(anchor_lang::idl::types::IdlInstructionAccount { + name: "program".into(), + docs: ::alloc::vec::Vec::new(), + writable: false, + signer: false, + optional: false, + address: None, + pda: None, + relations: ::alloc::vec::Vec::new(), + }), + ]), + ) + } + } + pub struct IdlAccounts<'info> { + #[account(mut, has_one = authority)] + pub idl: Account<'info, IdlAccount>, + #[account(constraint = authority.key!= &ERASED_AUTHORITY)] + pub authority: Signer<'info>, + } + #[automatically_derived] + impl<'info> anchor_lang::Accounts<'info, IdlAccountsBumps> for IdlAccounts<'info> + where + 'info: 'info, + { + #[inline(never)] + fn try_accounts( + __program_id: &anchor_lang::solana_program::pubkey::Pubkey, + __accounts: &mut &'info [anchor_lang::solana_program::account_info::AccountInfo< + 'info, + >], + __ix_data: &[u8], + __bumps: &mut IdlAccountsBumps, + __reallocs: &mut std::collections::BTreeSet< + anchor_lang::solana_program::pubkey::Pubkey, + >, + ) -> anchor_lang::Result { + let idl: anchor_lang::accounts::account::Account = anchor_lang::Accounts::try_accounts( + __program_id, + __accounts, + __ix_data, + __bumps, + __reallocs, + ) + .map_err(|e| e.with_account_name("idl"))?; + let authority: Signer = anchor_lang::Accounts::try_accounts( + __program_id, + __accounts, + __ix_data, + __bumps, + __reallocs, + ) + .map_err(|e| e.with_account_name("authority"))?; + if !AsRef::::as_ref(&idl).is_writable { + return Err( + anchor_lang::error::Error::from( + anchor_lang::error::ErrorCode::ConstraintMut, + ) + .with_account_name("idl"), + ); + } + { + let my_key = idl.authority; + let target_key = authority.key(); + if my_key != target_key { + return Err( + anchor_lang::error::Error::from( + anchor_lang::error::ErrorCode::ConstraintHasOne, + ) + .with_account_name("idl") + .with_pubkeys((my_key, target_key)), + ); + } + } + if !(authority.key != &ERASED_AUTHORITY) { + return Err( + anchor_lang::error::Error::from( + anchor_lang::error::ErrorCode::ConstraintRaw, + ) + .with_account_name("authority"), + ); + } + Ok(IdlAccounts { idl, authority }) + } + } + #[automatically_derived] + impl<'info> anchor_lang::ToAccountInfos<'info> for IdlAccounts<'info> + where + 'info: 'info, + { + fn to_account_infos( + &self, + ) -> Vec> { + let mut account_infos = ::alloc::vec::Vec::new(); + account_infos.extend(self.idl.to_account_infos()); + account_infos.extend(self.authority.to_account_infos()); + account_infos + } + } + #[automatically_derived] + impl<'info> anchor_lang::ToAccountMetas for IdlAccounts<'info> { + fn to_account_metas( + &self, + is_signer: Option, + ) -> Vec { + let mut account_metas = ::alloc::vec::Vec::new(); + account_metas.extend(self.idl.to_account_metas(None)); + account_metas.extend(self.authority.to_account_metas(None)); + account_metas + } + } + #[automatically_derived] + impl<'info> anchor_lang::AccountsExit<'info> for IdlAccounts<'info> + where + 'info: 'info, + { + fn exit( + &self, + program_id: &anchor_lang::solana_program::pubkey::Pubkey, + ) -> anchor_lang::Result<()> { + anchor_lang::AccountsExit::exit(&self.idl, program_id) + .map_err(|e| e.with_account_name("idl"))?; + Ok(()) + } + } + pub struct IdlAccountsBumps {} + #[automatically_derived] + impl ::core::fmt::Debug for IdlAccountsBumps { + #[inline] + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + ::core::fmt::Formatter::write_str(f, "IdlAccountsBumps") + } + } + impl Default for IdlAccountsBumps { + fn default() -> Self { + IdlAccountsBumps {} + } + } + impl<'info> anchor_lang::Bumps for IdlAccounts<'info> + where + 'info: 'info, + { + type Bumps = IdlAccountsBumps; + } + /// An internal, Anchor generated module. This is used (as an + /// implementation detail), to generate a struct for a given + /// `#[derive(Accounts)]` implementation, where each field is a Pubkey, + /// instead of an `AccountInfo`. This is useful for clients that want + /// to generate a list of accounts, without explicitly knowing the + /// order all the fields should be in. + /// + /// To access the struct in this module, one should use the sibling + /// `accounts` module (also generated), which re-exports this. + pub(crate) mod __client_accounts_idl_accounts { + use super::*; + use anchor_lang::prelude::borsh; + /// Generated client accounts for [`IdlAccounts`]. + pub struct IdlAccounts { + pub idl: Pubkey, + pub authority: Pubkey, + } + impl borsh::ser::BorshSerialize for IdlAccounts + where + Pubkey: borsh::ser::BorshSerialize, + Pubkey: borsh::ser::BorshSerialize, + { + fn serialize( + &self, + writer: &mut W, + ) -> ::core::result::Result<(), borsh::maybestd::io::Error> { + borsh::BorshSerialize::serialize(&self.idl, writer)?; + borsh::BorshSerialize::serialize(&self.authority, writer)?; + Ok(()) + } + } + impl anchor_lang::idl::build::IdlBuild for IdlAccounts { + fn create_type() -> Option { + Some(anchor_lang::idl::types::IdlTypeDef { + name: Self::get_full_path(), + docs: <[_]>::into_vec( + ::alloc::boxed::box_new([ + "Generated client accounts for [`IdlAccounts`].".into(), + ]), + ), + serialization: anchor_lang::idl::types::IdlSerialization::default(), + repr: None, + generics: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlTypeDefTy::Struct { + fields: Some( + anchor_lang::idl::types::IdlDefinedFields::Named( + <[_]>::into_vec( + ::alloc::boxed::box_new([ + anchor_lang::idl::types::IdlField { + name: "idl".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::Pubkey, + }, + anchor_lang::idl::types::IdlField { + name: "authority".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::Pubkey, + }, + ]), + ), + ), + ), + }, + }) + } + fn insert_types( + types: &mut std::collections::BTreeMap< + String, + anchor_lang::idl::types::IdlTypeDef, + >, + ) {} + fn get_full_path() -> String { + ::alloc::__export::must_use({ + let res = ::alloc::fmt::format( + format_args!( + "{0}::{1}", + "anchor_compressible_derived::__private::__idl::__client_accounts_idl_accounts", + "IdlAccounts", + ), + ); + res + }) + } + } + #[automatically_derived] + impl anchor_lang::ToAccountMetas for IdlAccounts { + fn to_account_metas( + &self, + is_signer: Option, + ) -> Vec { + let mut account_metas = ::alloc::vec::Vec::new(); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new( + self.idl, + false, + ), + ); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new_readonly( + self.authority, + true, + ), + ); + account_metas + } + } + } + /// An internal, Anchor generated module. This is used (as an + /// implementation detail), to generate a CPI struct for a given + /// `#[derive(Accounts)]` implementation, where each field is an + /// AccountInfo. + /// + /// To access the struct in this module, one should use the sibling + /// [`cpi::accounts`] module (also generated), which re-exports this. + pub(crate) mod __cpi_client_accounts_idl_accounts { + use super::*; + /// Generated CPI struct of the accounts for [`IdlAccounts`]. + pub struct IdlAccounts<'info> { + pub idl: anchor_lang::solana_program::account_info::AccountInfo<'info>, + pub authority: anchor_lang::solana_program::account_info::AccountInfo< + 'info, + >, + } + #[automatically_derived] + impl<'info> anchor_lang::ToAccountMetas for IdlAccounts<'info> { + fn to_account_metas( + &self, + is_signer: Option, + ) -> Vec { + let mut account_metas = ::alloc::vec::Vec::new(); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new( + anchor_lang::Key::key(&self.idl), + false, + ), + ); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new_readonly( + anchor_lang::Key::key(&self.authority), + true, + ), + ); + account_metas + } + } + #[automatically_derived] + impl<'info> anchor_lang::ToAccountInfos<'info> for IdlAccounts<'info> { + fn to_account_infos( + &self, + ) -> Vec> { + let mut account_infos = ::alloc::vec::Vec::new(); + account_infos + .extend( + anchor_lang::ToAccountInfos::to_account_infos(&self.idl), + ); + account_infos + .extend( + anchor_lang::ToAccountInfos::to_account_infos( + &self.authority, + ), + ); + account_infos + } + } + } + impl<'info> IdlAccounts<'info> { + pub fn __anchor_private_gen_idl_accounts( + accounts: &mut std::collections::BTreeMap< + String, + anchor_lang::idl::types::IdlAccount, + >, + types: &mut std::collections::BTreeMap< + String, + anchor_lang::idl::types::IdlTypeDef, + >, + ) -> Vec { + if let Some(ty) = ::create_type() { + let account = anchor_lang::idl::types::IdlAccount { + name: ty.name.clone(), + discriminator: IdlAccount::DISCRIMINATOR.into(), + }; + accounts.insert(account.name.clone(), account); + types.insert(ty.name.clone(), ty); + ::insert_types(types); + } + <[_]>::into_vec( + ::alloc::boxed::box_new([ + anchor_lang::idl::types::IdlInstructionAccountItem::Single(anchor_lang::idl::types::IdlInstructionAccount { + name: "idl".into(), + docs: ::alloc::vec::Vec::new(), + writable: true, + signer: false, + optional: false, + address: None, + pda: None, + relations: ::alloc::vec::Vec::new(), + }), + anchor_lang::idl::types::IdlInstructionAccountItem::Single(anchor_lang::idl::types::IdlInstructionAccount { + name: "authority".into(), + docs: ::alloc::vec::Vec::new(), + writable: false, + signer: true, + optional: false, + address: None, + pda: None, + relations: ::alloc::vec::Vec::new(), + }), + ]), + ) + } + } + pub struct IdlResizeAccount<'info> { + #[account(mut, has_one = authority)] + pub idl: Account<'info, IdlAccount>, + #[account(mut, constraint = authority.key!= &ERASED_AUTHORITY)] + pub authority: Signer<'info>, + pub system_program: Program<'info, System>, + } + #[automatically_derived] + impl<'info> anchor_lang::Accounts<'info, IdlResizeAccountBumps> + for IdlResizeAccount<'info> + where + 'info: 'info, + { + #[inline(never)] + fn try_accounts( + __program_id: &anchor_lang::solana_program::pubkey::Pubkey, + __accounts: &mut &'info [anchor_lang::solana_program::account_info::AccountInfo< + 'info, + >], + __ix_data: &[u8], + __bumps: &mut IdlResizeAccountBumps, + __reallocs: &mut std::collections::BTreeSet< + anchor_lang::solana_program::pubkey::Pubkey, + >, + ) -> anchor_lang::Result { + let idl: anchor_lang::accounts::account::Account = anchor_lang::Accounts::try_accounts( + __program_id, + __accounts, + __ix_data, + __bumps, + __reallocs, + ) + .map_err(|e| e.with_account_name("idl"))?; + let authority: Signer = anchor_lang::Accounts::try_accounts( + __program_id, + __accounts, + __ix_data, + __bumps, + __reallocs, + ) + .map_err(|e| e.with_account_name("authority"))?; + let system_program: anchor_lang::accounts::program::Program = anchor_lang::Accounts::try_accounts( + __program_id, + __accounts, + __ix_data, + __bumps, + __reallocs, + ) + .map_err(|e| e.with_account_name("system_program"))?; + if !AsRef::::as_ref(&idl).is_writable { + return Err( + anchor_lang::error::Error::from( + anchor_lang::error::ErrorCode::ConstraintMut, + ) + .with_account_name("idl"), + ); + } + { + let my_key = idl.authority; + let target_key = authority.key(); + if my_key != target_key { + return Err( + anchor_lang::error::Error::from( + anchor_lang::error::ErrorCode::ConstraintHasOne, + ) + .with_account_name("idl") + .with_pubkeys((my_key, target_key)), + ); + } + } + if !AsRef::::as_ref(&authority).is_writable { + return Err( + anchor_lang::error::Error::from( + anchor_lang::error::ErrorCode::ConstraintMut, + ) + .with_account_name("authority"), + ); + } + if !(authority.key != &ERASED_AUTHORITY) { + return Err( + anchor_lang::error::Error::from( + anchor_lang::error::ErrorCode::ConstraintRaw, + ) + .with_account_name("authority"), + ); + } + Ok(IdlResizeAccount { + idl, + authority, + system_program, + }) + } + } + #[automatically_derived] + impl<'info> anchor_lang::ToAccountInfos<'info> for IdlResizeAccount<'info> + where + 'info: 'info, + { + fn to_account_infos( + &self, + ) -> Vec> { + let mut account_infos = ::alloc::vec::Vec::new(); + account_infos.extend(self.idl.to_account_infos()); + account_infos.extend(self.authority.to_account_infos()); + account_infos.extend(self.system_program.to_account_infos()); + account_infos + } + } + #[automatically_derived] + impl<'info> anchor_lang::ToAccountMetas for IdlResizeAccount<'info> { + fn to_account_metas( + &self, + is_signer: Option, + ) -> Vec { + let mut account_metas = ::alloc::vec::Vec::new(); + account_metas.extend(self.idl.to_account_metas(None)); + account_metas.extend(self.authority.to_account_metas(None)); + account_metas.extend(self.system_program.to_account_metas(None)); + account_metas + } + } + #[automatically_derived] + impl<'info> anchor_lang::AccountsExit<'info> for IdlResizeAccount<'info> + where + 'info: 'info, + { + fn exit( + &self, + program_id: &anchor_lang::solana_program::pubkey::Pubkey, + ) -> anchor_lang::Result<()> { + anchor_lang::AccountsExit::exit(&self.idl, program_id) + .map_err(|e| e.with_account_name("idl"))?; + anchor_lang::AccountsExit::exit(&self.authority, program_id) + .map_err(|e| e.with_account_name("authority"))?; + Ok(()) + } + } + pub struct IdlResizeAccountBumps {} + #[automatically_derived] + impl ::core::fmt::Debug for IdlResizeAccountBumps { + #[inline] + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + ::core::fmt::Formatter::write_str(f, "IdlResizeAccountBumps") + } + } + impl Default for IdlResizeAccountBumps { + fn default() -> Self { + IdlResizeAccountBumps {} + } + } + impl<'info> anchor_lang::Bumps for IdlResizeAccount<'info> + where + 'info: 'info, + { + type Bumps = IdlResizeAccountBumps; + } + /// An internal, Anchor generated module. This is used (as an + /// implementation detail), to generate a struct for a given + /// `#[derive(Accounts)]` implementation, where each field is a Pubkey, + /// instead of an `AccountInfo`. This is useful for clients that want + /// to generate a list of accounts, without explicitly knowing the + /// order all the fields should be in. + /// + /// To access the struct in this module, one should use the sibling + /// `accounts` module (also generated), which re-exports this. + pub(crate) mod __client_accounts_idl_resize_account { + use super::*; + use anchor_lang::prelude::borsh; + /// Generated client accounts for [`IdlResizeAccount`]. + pub struct IdlResizeAccount { + pub idl: Pubkey, + pub authority: Pubkey, + pub system_program: Pubkey, + } + impl borsh::ser::BorshSerialize for IdlResizeAccount + where + Pubkey: borsh::ser::BorshSerialize, + Pubkey: borsh::ser::BorshSerialize, + Pubkey: borsh::ser::BorshSerialize, + { + fn serialize( + &self, + writer: &mut W, + ) -> ::core::result::Result<(), borsh::maybestd::io::Error> { + borsh::BorshSerialize::serialize(&self.idl, writer)?; + borsh::BorshSerialize::serialize(&self.authority, writer)?; + borsh::BorshSerialize::serialize(&self.system_program, writer)?; + Ok(()) + } + } + impl anchor_lang::idl::build::IdlBuild for IdlResizeAccount { + fn create_type() -> Option { + Some(anchor_lang::idl::types::IdlTypeDef { + name: Self::get_full_path(), + docs: <[_]>::into_vec( + ::alloc::boxed::box_new([ + "Generated client accounts for [`IdlResizeAccount`].".into(), + ]), + ), + serialization: anchor_lang::idl::types::IdlSerialization::default(), + repr: None, + generics: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlTypeDefTy::Struct { + fields: Some( + anchor_lang::idl::types::IdlDefinedFields::Named( + <[_]>::into_vec( + ::alloc::boxed::box_new([ + anchor_lang::idl::types::IdlField { + name: "idl".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::Pubkey, + }, + anchor_lang::idl::types::IdlField { + name: "authority".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::Pubkey, + }, + anchor_lang::idl::types::IdlField { + name: "system_program".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::Pubkey, + }, + ]), + ), + ), + ), + }, + }) + } + fn insert_types( + types: &mut std::collections::BTreeMap< + String, + anchor_lang::idl::types::IdlTypeDef, + >, + ) {} + fn get_full_path() -> String { + ::alloc::__export::must_use({ + let res = ::alloc::fmt::format( + format_args!( + "{0}::{1}", + "anchor_compressible_derived::__private::__idl::__client_accounts_idl_resize_account", + "IdlResizeAccount", + ), + ); + res + }) + } + } + #[automatically_derived] + impl anchor_lang::ToAccountMetas for IdlResizeAccount { + fn to_account_metas( + &self, + is_signer: Option, + ) -> Vec { + let mut account_metas = ::alloc::vec::Vec::new(); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new( + self.idl, + false, + ), + ); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new( + self.authority, + true, + ), + ); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new_readonly( + self.system_program, + false, + ), + ); + account_metas + } + } + } + /// An internal, Anchor generated module. This is used (as an + /// implementation detail), to generate a CPI struct for a given + /// `#[derive(Accounts)]` implementation, where each field is an + /// AccountInfo. + /// + /// To access the struct in this module, one should use the sibling + /// [`cpi::accounts`] module (also generated), which re-exports this. + pub(crate) mod __cpi_client_accounts_idl_resize_account { + use super::*; + /// Generated CPI struct of the accounts for [`IdlResizeAccount`]. + pub struct IdlResizeAccount<'info> { + pub idl: anchor_lang::solana_program::account_info::AccountInfo<'info>, + pub authority: anchor_lang::solana_program::account_info::AccountInfo< + 'info, + >, + pub system_program: anchor_lang::solana_program::account_info::AccountInfo< + 'info, + >, + } + #[automatically_derived] + impl<'info> anchor_lang::ToAccountMetas for IdlResizeAccount<'info> { + fn to_account_metas( + &self, + is_signer: Option, + ) -> Vec { + let mut account_metas = ::alloc::vec::Vec::new(); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new( + anchor_lang::Key::key(&self.idl), + false, + ), + ); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new( + anchor_lang::Key::key(&self.authority), + true, + ), + ); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new_readonly( + anchor_lang::Key::key(&self.system_program), + false, + ), + ); + account_metas + } + } + #[automatically_derived] + impl<'info> anchor_lang::ToAccountInfos<'info> for IdlResizeAccount<'info> { + fn to_account_infos( + &self, + ) -> Vec> { + let mut account_infos = ::alloc::vec::Vec::new(); + account_infos + .extend( + anchor_lang::ToAccountInfos::to_account_infos(&self.idl), + ); + account_infos + .extend( + anchor_lang::ToAccountInfos::to_account_infos( + &self.authority, + ), + ); + account_infos + .extend( + anchor_lang::ToAccountInfos::to_account_infos( + &self.system_program, + ), + ); + account_infos + } + } + } + impl<'info> IdlResizeAccount<'info> { + pub fn __anchor_private_gen_idl_accounts( + accounts: &mut std::collections::BTreeMap< + String, + anchor_lang::idl::types::IdlAccount, + >, + types: &mut std::collections::BTreeMap< + String, + anchor_lang::idl::types::IdlTypeDef, + >, + ) -> Vec { + if let Some(ty) = ::create_type() { + let account = anchor_lang::idl::types::IdlAccount { + name: ty.name.clone(), + discriminator: IdlAccount::DISCRIMINATOR.into(), + }; + accounts.insert(account.name.clone(), account); + types.insert(ty.name.clone(), ty); + ::insert_types(types); + } + <[_]>::into_vec( + ::alloc::boxed::box_new([ + anchor_lang::idl::types::IdlInstructionAccountItem::Single(anchor_lang::idl::types::IdlInstructionAccount { + name: "idl".into(), + docs: ::alloc::vec::Vec::new(), + writable: true, + signer: false, + optional: false, + address: None, + pda: None, + relations: ::alloc::vec::Vec::new(), + }), + anchor_lang::idl::types::IdlInstructionAccountItem::Single(anchor_lang::idl::types::IdlInstructionAccount { + name: "authority".into(), + docs: ::alloc::vec::Vec::new(), + writable: true, + signer: true, + optional: false, + address: None, + pda: None, + relations: ::alloc::vec::Vec::new(), + }), + anchor_lang::idl::types::IdlInstructionAccountItem::Single(anchor_lang::idl::types::IdlInstructionAccount { + name: "system_program".into(), + docs: ::alloc::vec::Vec::new(), + writable: false, + signer: false, + optional: false, + address: None, + pda: None, + relations: ::alloc::vec::Vec::new(), + }), + ]), + ) + } + } + pub struct IdlCreateBuffer<'info> { + #[account(zero)] + pub buffer: Account<'info, IdlAccount>, + #[account(constraint = authority.key!= &ERASED_AUTHORITY)] + pub authority: Signer<'info>, + } + #[automatically_derived] + impl<'info> anchor_lang::Accounts<'info, IdlCreateBufferBumps> + for IdlCreateBuffer<'info> + where + 'info: 'info, + { + #[inline(never)] + fn try_accounts( + __program_id: &anchor_lang::solana_program::pubkey::Pubkey, + __accounts: &mut &'info [anchor_lang::solana_program::account_info::AccountInfo< + 'info, + >], + __ix_data: &[u8], + __bumps: &mut IdlCreateBufferBumps, + __reallocs: &mut std::collections::BTreeSet< + anchor_lang::solana_program::pubkey::Pubkey, + >, + ) -> anchor_lang::Result { + if __accounts.is_empty() { + return Err( + anchor_lang::error::ErrorCode::AccountNotEnoughKeys.into(), + ); + } + let buffer = &__accounts[0]; + *__accounts = &__accounts[1..]; + let authority: Signer = anchor_lang::Accounts::try_accounts( + __program_id, + __accounts, + __ix_data, + __bumps, + __reallocs, + ) + .map_err(|e| e.with_account_name("authority"))?; + let __anchor_rent = Rent::get()?; + let buffer: anchor_lang::accounts::account::Account = { + let mut __data: &[u8] = &buffer.try_borrow_data()?; + let __disc = &__data[..IdlAccount::DISCRIMINATOR.len()]; + let __has_disc = __disc.iter().any(|b| *b != 0); + if __has_disc { + return Err( + anchor_lang::error::Error::from( + anchor_lang::error::ErrorCode::ConstraintZero, + ) + .with_account_name("buffer"), + ); + } + match anchor_lang::accounts::account::Account::try_from_unchecked( + &buffer, + ) { + Ok(val) => val, + Err(e) => return Err(e.with_account_name("buffer")), + } + }; + if !AsRef::::as_ref(&buffer).is_writable { + return Err( + anchor_lang::error::Error::from( + anchor_lang::error::ErrorCode::ConstraintMut, + ) + .with_account_name("buffer"), + ); + } + if !__anchor_rent + .is_exempt( + buffer.to_account_info().lamports(), + buffer.to_account_info().try_data_len()?, + ) + { + return Err( + anchor_lang::error::Error::from( + anchor_lang::error::ErrorCode::ConstraintRentExempt, + ) + .with_account_name("buffer"), + ); + } + if !(authority.key != &ERASED_AUTHORITY) { + return Err( + anchor_lang::error::Error::from( + anchor_lang::error::ErrorCode::ConstraintRaw, + ) + .with_account_name("authority"), + ); + } + Ok(IdlCreateBuffer { + buffer, + authority, + }) + } + } + #[automatically_derived] + impl<'info> anchor_lang::ToAccountInfos<'info> for IdlCreateBuffer<'info> + where + 'info: 'info, + { + fn to_account_infos( + &self, + ) -> Vec> { + let mut account_infos = ::alloc::vec::Vec::new(); + account_infos.extend(self.buffer.to_account_infos()); + account_infos.extend(self.authority.to_account_infos()); + account_infos + } + } + #[automatically_derived] + impl<'info> anchor_lang::ToAccountMetas for IdlCreateBuffer<'info> { + fn to_account_metas( + &self, + is_signer: Option, + ) -> Vec { + let mut account_metas = ::alloc::vec::Vec::new(); + account_metas.extend(self.buffer.to_account_metas(None)); + account_metas.extend(self.authority.to_account_metas(None)); + account_metas + } + } + #[automatically_derived] + impl<'info> anchor_lang::AccountsExit<'info> for IdlCreateBuffer<'info> + where + 'info: 'info, + { + fn exit( + &self, + program_id: &anchor_lang::solana_program::pubkey::Pubkey, + ) -> anchor_lang::Result<()> { + anchor_lang::AccountsExit::exit(&self.buffer, program_id) + .map_err(|e| e.with_account_name("buffer"))?; + Ok(()) + } + } + pub struct IdlCreateBufferBumps {} + #[automatically_derived] + impl ::core::fmt::Debug for IdlCreateBufferBumps { + #[inline] + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + ::core::fmt::Formatter::write_str(f, "IdlCreateBufferBumps") + } + } + impl Default for IdlCreateBufferBumps { + fn default() -> Self { + IdlCreateBufferBumps {} + } + } + impl<'info> anchor_lang::Bumps for IdlCreateBuffer<'info> + where + 'info: 'info, + { + type Bumps = IdlCreateBufferBumps; + } + /// An internal, Anchor generated module. This is used (as an + /// implementation detail), to generate a struct for a given + /// `#[derive(Accounts)]` implementation, where each field is a Pubkey, + /// instead of an `AccountInfo`. This is useful for clients that want + /// to generate a list of accounts, without explicitly knowing the + /// order all the fields should be in. + /// + /// To access the struct in this module, one should use the sibling + /// `accounts` module (also generated), which re-exports this. + pub(crate) mod __client_accounts_idl_create_buffer { + use super::*; + use anchor_lang::prelude::borsh; + /// Generated client accounts for [`IdlCreateBuffer`]. + pub struct IdlCreateBuffer { + pub buffer: Pubkey, + pub authority: Pubkey, + } + impl borsh::ser::BorshSerialize for IdlCreateBuffer + where + Pubkey: borsh::ser::BorshSerialize, + Pubkey: borsh::ser::BorshSerialize, + { + fn serialize( + &self, + writer: &mut W, + ) -> ::core::result::Result<(), borsh::maybestd::io::Error> { + borsh::BorshSerialize::serialize(&self.buffer, writer)?; + borsh::BorshSerialize::serialize(&self.authority, writer)?; + Ok(()) + } + } + impl anchor_lang::idl::build::IdlBuild for IdlCreateBuffer { + fn create_type() -> Option { + Some(anchor_lang::idl::types::IdlTypeDef { + name: Self::get_full_path(), + docs: <[_]>::into_vec( + ::alloc::boxed::box_new([ + "Generated client accounts for [`IdlCreateBuffer`].".into(), + ]), + ), + serialization: anchor_lang::idl::types::IdlSerialization::default(), + repr: None, + generics: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlTypeDefTy::Struct { + fields: Some( + anchor_lang::idl::types::IdlDefinedFields::Named( + <[_]>::into_vec( + ::alloc::boxed::box_new([ + anchor_lang::idl::types::IdlField { + name: "buffer".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::Pubkey, + }, + anchor_lang::idl::types::IdlField { + name: "authority".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::Pubkey, + }, + ]), + ), + ), + ), + }, + }) + } + fn insert_types( + types: &mut std::collections::BTreeMap< + String, + anchor_lang::idl::types::IdlTypeDef, + >, + ) {} + fn get_full_path() -> String { + ::alloc::__export::must_use({ + let res = ::alloc::fmt::format( + format_args!( + "{0}::{1}", + "anchor_compressible_derived::__private::__idl::__client_accounts_idl_create_buffer", + "IdlCreateBuffer", + ), + ); + res + }) + } + } + #[automatically_derived] + impl anchor_lang::ToAccountMetas for IdlCreateBuffer { + fn to_account_metas( + &self, + is_signer: Option, + ) -> Vec { + let mut account_metas = ::alloc::vec::Vec::new(); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new( + self.buffer, + false, + ), + ); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new_readonly( + self.authority, + true, + ), + ); + account_metas + } + } + } + /// An internal, Anchor generated module. This is used (as an + /// implementation detail), to generate a CPI struct for a given + /// `#[derive(Accounts)]` implementation, where each field is an + /// AccountInfo. + /// + /// To access the struct in this module, one should use the sibling + /// [`cpi::accounts`] module (also generated), which re-exports this. + pub(crate) mod __cpi_client_accounts_idl_create_buffer { + use super::*; + /// Generated CPI struct of the accounts for [`IdlCreateBuffer`]. + pub struct IdlCreateBuffer<'info> { + pub buffer: anchor_lang::solana_program::account_info::AccountInfo< + 'info, + >, + pub authority: anchor_lang::solana_program::account_info::AccountInfo< + 'info, + >, + } + #[automatically_derived] + impl<'info> anchor_lang::ToAccountMetas for IdlCreateBuffer<'info> { + fn to_account_metas( + &self, + is_signer: Option, + ) -> Vec { + let mut account_metas = ::alloc::vec::Vec::new(); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new( + anchor_lang::Key::key(&self.buffer), + false, + ), + ); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new_readonly( + anchor_lang::Key::key(&self.authority), + true, + ), + ); + account_metas + } + } + #[automatically_derived] + impl<'info> anchor_lang::ToAccountInfos<'info> for IdlCreateBuffer<'info> { + fn to_account_infos( + &self, + ) -> Vec> { + let mut account_infos = ::alloc::vec::Vec::new(); + account_infos + .extend( + anchor_lang::ToAccountInfos::to_account_infos(&self.buffer), + ); + account_infos + .extend( + anchor_lang::ToAccountInfos::to_account_infos( + &self.authority, + ), + ); + account_infos + } + } + } + impl<'info> IdlCreateBuffer<'info> { + pub fn __anchor_private_gen_idl_accounts( + accounts: &mut std::collections::BTreeMap< + String, + anchor_lang::idl::types::IdlAccount, + >, + types: &mut std::collections::BTreeMap< + String, + anchor_lang::idl::types::IdlTypeDef, + >, + ) -> Vec { + if let Some(ty) = ::create_type() { + let account = anchor_lang::idl::types::IdlAccount { + name: ty.name.clone(), + discriminator: IdlAccount::DISCRIMINATOR.into(), + }; + accounts.insert(account.name.clone(), account); + types.insert(ty.name.clone(), ty); + ::insert_types(types); + } + <[_]>::into_vec( + ::alloc::boxed::box_new([ + anchor_lang::idl::types::IdlInstructionAccountItem::Single(anchor_lang::idl::types::IdlInstructionAccount { + name: "buffer".into(), + docs: ::alloc::vec::Vec::new(), + writable: true, + signer: false, + optional: false, + address: None, + pda: None, + relations: ::alloc::vec::Vec::new(), + }), + anchor_lang::idl::types::IdlInstructionAccountItem::Single(anchor_lang::idl::types::IdlInstructionAccount { + name: "authority".into(), + docs: ::alloc::vec::Vec::new(), + writable: false, + signer: true, + optional: false, + address: None, + pda: None, + relations: ::alloc::vec::Vec::new(), + }), + ]), + ) + } + } + pub struct IdlSetBuffer<'info> { + #[account(mut, constraint = buffer.authority = = idl.authority)] + pub buffer: Account<'info, IdlAccount>, + #[account(mut, has_one = authority)] + pub idl: Account<'info, IdlAccount>, + #[account(constraint = authority.key!= &ERASED_AUTHORITY)] + pub authority: Signer<'info>, + } + #[automatically_derived] + impl<'info> anchor_lang::Accounts<'info, IdlSetBufferBumps> + for IdlSetBuffer<'info> + where + 'info: 'info, + { + #[inline(never)] + fn try_accounts( + __program_id: &anchor_lang::solana_program::pubkey::Pubkey, + __accounts: &mut &'info [anchor_lang::solana_program::account_info::AccountInfo< + 'info, + >], + __ix_data: &[u8], + __bumps: &mut IdlSetBufferBumps, + __reallocs: &mut std::collections::BTreeSet< + anchor_lang::solana_program::pubkey::Pubkey, + >, + ) -> anchor_lang::Result { + let buffer: anchor_lang::accounts::account::Account = anchor_lang::Accounts::try_accounts( + __program_id, + __accounts, + __ix_data, + __bumps, + __reallocs, + ) + .map_err(|e| e.with_account_name("buffer"))?; + let idl: anchor_lang::accounts::account::Account = anchor_lang::Accounts::try_accounts( + __program_id, + __accounts, + __ix_data, + __bumps, + __reallocs, + ) + .map_err(|e| e.with_account_name("idl"))?; + let authority: Signer = anchor_lang::Accounts::try_accounts( + __program_id, + __accounts, + __ix_data, + __bumps, + __reallocs, + ) + .map_err(|e| e.with_account_name("authority"))?; + if !AsRef::::as_ref(&buffer).is_writable { + return Err( + anchor_lang::error::Error::from( + anchor_lang::error::ErrorCode::ConstraintMut, + ) + .with_account_name("buffer"), + ); + } + if !(buffer.authority == idl.authority) { + return Err( + anchor_lang::error::Error::from( + anchor_lang::error::ErrorCode::ConstraintRaw, + ) + .with_account_name("buffer"), + ); + } + if !AsRef::::as_ref(&idl).is_writable { + return Err( + anchor_lang::error::Error::from( + anchor_lang::error::ErrorCode::ConstraintMut, + ) + .with_account_name("idl"), + ); + } + { + let my_key = idl.authority; + let target_key = authority.key(); + if my_key != target_key { + return Err( + anchor_lang::error::Error::from( + anchor_lang::error::ErrorCode::ConstraintHasOne, + ) + .with_account_name("idl") + .with_pubkeys((my_key, target_key)), + ); + } + } + if !(authority.key != &ERASED_AUTHORITY) { + return Err( + anchor_lang::error::Error::from( + anchor_lang::error::ErrorCode::ConstraintRaw, + ) + .with_account_name("authority"), + ); + } + Ok(IdlSetBuffer { + buffer, + idl, + authority, + }) + } + } + #[automatically_derived] + impl<'info> anchor_lang::ToAccountInfos<'info> for IdlSetBuffer<'info> + where + 'info: 'info, + { + fn to_account_infos( + &self, + ) -> Vec> { + let mut account_infos = ::alloc::vec::Vec::new(); + account_infos.extend(self.buffer.to_account_infos()); + account_infos.extend(self.idl.to_account_infos()); + account_infos.extend(self.authority.to_account_infos()); + account_infos + } + } + #[automatically_derived] + impl<'info> anchor_lang::ToAccountMetas for IdlSetBuffer<'info> { + fn to_account_metas( + &self, + is_signer: Option, + ) -> Vec { + let mut account_metas = ::alloc::vec::Vec::new(); + account_metas.extend(self.buffer.to_account_metas(None)); + account_metas.extend(self.idl.to_account_metas(None)); + account_metas.extend(self.authority.to_account_metas(None)); + account_metas + } + } + #[automatically_derived] + impl<'info> anchor_lang::AccountsExit<'info> for IdlSetBuffer<'info> + where + 'info: 'info, + { + fn exit( + &self, + program_id: &anchor_lang::solana_program::pubkey::Pubkey, + ) -> anchor_lang::Result<()> { + anchor_lang::AccountsExit::exit(&self.buffer, program_id) + .map_err(|e| e.with_account_name("buffer"))?; + anchor_lang::AccountsExit::exit(&self.idl, program_id) + .map_err(|e| e.with_account_name("idl"))?; + Ok(()) + } + } + pub struct IdlSetBufferBumps {} + #[automatically_derived] + impl ::core::fmt::Debug for IdlSetBufferBumps { + #[inline] + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + ::core::fmt::Formatter::write_str(f, "IdlSetBufferBumps") + } + } + impl Default for IdlSetBufferBumps { + fn default() -> Self { + IdlSetBufferBumps {} + } + } + impl<'info> anchor_lang::Bumps for IdlSetBuffer<'info> + where + 'info: 'info, + { + type Bumps = IdlSetBufferBumps; + } + /// An internal, Anchor generated module. This is used (as an + /// implementation detail), to generate a struct for a given + /// `#[derive(Accounts)]` implementation, where each field is a Pubkey, + /// instead of an `AccountInfo`. This is useful for clients that want + /// to generate a list of accounts, without explicitly knowing the + /// order all the fields should be in. + /// + /// To access the struct in this module, one should use the sibling + /// `accounts` module (also generated), which re-exports this. + pub(crate) mod __client_accounts_idl_set_buffer { + use super::*; + use anchor_lang::prelude::borsh; + /// Generated client accounts for [`IdlSetBuffer`]. + pub struct IdlSetBuffer { + pub buffer: Pubkey, + pub idl: Pubkey, + pub authority: Pubkey, + } + impl borsh::ser::BorshSerialize for IdlSetBuffer + where + Pubkey: borsh::ser::BorshSerialize, + Pubkey: borsh::ser::BorshSerialize, + Pubkey: borsh::ser::BorshSerialize, + { + fn serialize( + &self, + writer: &mut W, + ) -> ::core::result::Result<(), borsh::maybestd::io::Error> { + borsh::BorshSerialize::serialize(&self.buffer, writer)?; + borsh::BorshSerialize::serialize(&self.idl, writer)?; + borsh::BorshSerialize::serialize(&self.authority, writer)?; + Ok(()) + } + } + impl anchor_lang::idl::build::IdlBuild for IdlSetBuffer { + fn create_type() -> Option { + Some(anchor_lang::idl::types::IdlTypeDef { + name: Self::get_full_path(), + docs: <[_]>::into_vec( + ::alloc::boxed::box_new([ + "Generated client accounts for [`IdlSetBuffer`].".into(), + ]), + ), + serialization: anchor_lang::idl::types::IdlSerialization::default(), + repr: None, + generics: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlTypeDefTy::Struct { + fields: Some( + anchor_lang::idl::types::IdlDefinedFields::Named( + <[_]>::into_vec( + ::alloc::boxed::box_new([ + anchor_lang::idl::types::IdlField { + name: "buffer".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::Pubkey, + }, + anchor_lang::idl::types::IdlField { + name: "idl".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::Pubkey, + }, + anchor_lang::idl::types::IdlField { + name: "authority".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::Pubkey, + }, + ]), + ), + ), + ), + }, + }) + } + fn insert_types( + types: &mut std::collections::BTreeMap< + String, + anchor_lang::idl::types::IdlTypeDef, + >, + ) {} + fn get_full_path() -> String { + ::alloc::__export::must_use({ + let res = ::alloc::fmt::format( + format_args!( + "{0}::{1}", + "anchor_compressible_derived::__private::__idl::__client_accounts_idl_set_buffer", + "IdlSetBuffer", + ), + ); + res + }) + } + } + #[automatically_derived] + impl anchor_lang::ToAccountMetas for IdlSetBuffer { + fn to_account_metas( + &self, + is_signer: Option, + ) -> Vec { + let mut account_metas = ::alloc::vec::Vec::new(); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new( + self.buffer, + false, + ), + ); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new( + self.idl, + false, + ), + ); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new_readonly( + self.authority, + true, + ), + ); + account_metas + } + } + } + /// An internal, Anchor generated module. This is used (as an + /// implementation detail), to generate a CPI struct for a given + /// `#[derive(Accounts)]` implementation, where each field is an + /// AccountInfo. + /// + /// To access the struct in this module, one should use the sibling + /// [`cpi::accounts`] module (also generated), which re-exports this. + pub(crate) mod __cpi_client_accounts_idl_set_buffer { + use super::*; + /// Generated CPI struct of the accounts for [`IdlSetBuffer`]. + pub struct IdlSetBuffer<'info> { + pub buffer: anchor_lang::solana_program::account_info::AccountInfo< + 'info, + >, + pub idl: anchor_lang::solana_program::account_info::AccountInfo<'info>, + pub authority: anchor_lang::solana_program::account_info::AccountInfo< + 'info, + >, + } + #[automatically_derived] + impl<'info> anchor_lang::ToAccountMetas for IdlSetBuffer<'info> { + fn to_account_metas( + &self, + is_signer: Option, + ) -> Vec { + let mut account_metas = ::alloc::vec::Vec::new(); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new( + anchor_lang::Key::key(&self.buffer), + false, + ), + ); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new( + anchor_lang::Key::key(&self.idl), + false, + ), + ); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new_readonly( + anchor_lang::Key::key(&self.authority), + true, + ), + ); + account_metas + } + } + #[automatically_derived] + impl<'info> anchor_lang::ToAccountInfos<'info> for IdlSetBuffer<'info> { + fn to_account_infos( + &self, + ) -> Vec> { + let mut account_infos = ::alloc::vec::Vec::new(); + account_infos + .extend( + anchor_lang::ToAccountInfos::to_account_infos(&self.buffer), + ); + account_infos + .extend( + anchor_lang::ToAccountInfos::to_account_infos(&self.idl), + ); + account_infos + .extend( + anchor_lang::ToAccountInfos::to_account_infos( + &self.authority, + ), + ); + account_infos + } + } + } + impl<'info> IdlSetBuffer<'info> { + pub fn __anchor_private_gen_idl_accounts( + accounts: &mut std::collections::BTreeMap< + String, + anchor_lang::idl::types::IdlAccount, + >, + types: &mut std::collections::BTreeMap< + String, + anchor_lang::idl::types::IdlTypeDef, + >, + ) -> Vec { + if let Some(ty) = ::create_type() { + let account = anchor_lang::idl::types::IdlAccount { + name: ty.name.clone(), + discriminator: IdlAccount::DISCRIMINATOR.into(), + }; + accounts.insert(account.name.clone(), account); + types.insert(ty.name.clone(), ty); + ::insert_types(types); + } + if let Some(ty) = ::create_type() { + let account = anchor_lang::idl::types::IdlAccount { + name: ty.name.clone(), + discriminator: IdlAccount::DISCRIMINATOR.into(), + }; + accounts.insert(account.name.clone(), account); + types.insert(ty.name.clone(), ty); + ::insert_types(types); + } + <[_]>::into_vec( + ::alloc::boxed::box_new([ + anchor_lang::idl::types::IdlInstructionAccountItem::Single(anchor_lang::idl::types::IdlInstructionAccount { + name: "buffer".into(), + docs: ::alloc::vec::Vec::new(), + writable: true, + signer: false, + optional: false, + address: None, + pda: None, + relations: ::alloc::vec::Vec::new(), + }), + anchor_lang::idl::types::IdlInstructionAccountItem::Single(anchor_lang::idl::types::IdlInstructionAccount { + name: "idl".into(), + docs: ::alloc::vec::Vec::new(), + writable: true, + signer: false, + optional: false, + address: None, + pda: None, + relations: ::alloc::vec::Vec::new(), + }), + anchor_lang::idl::types::IdlInstructionAccountItem::Single(anchor_lang::idl::types::IdlInstructionAccount { + name: "authority".into(), + docs: ::alloc::vec::Vec::new(), + writable: false, + signer: true, + optional: false, + address: None, + pda: None, + relations: ::alloc::vec::Vec::new(), + }), + ]), + ) + } + } + pub struct IdlCloseAccount<'info> { + #[account(mut, has_one = authority, close = sol_destination)] + pub account: Account<'info, IdlAccount>, + #[account(constraint = authority.key!= &ERASED_AUTHORITY)] + pub authority: Signer<'info>, + #[account(mut)] + pub sol_destination: AccountInfo<'info>, + } + #[automatically_derived] + impl<'info> anchor_lang::Accounts<'info, IdlCloseAccountBumps> + for IdlCloseAccount<'info> + where + 'info: 'info, + { + #[inline(never)] + fn try_accounts( + __program_id: &anchor_lang::solana_program::pubkey::Pubkey, + __accounts: &mut &'info [anchor_lang::solana_program::account_info::AccountInfo< + 'info, + >], + __ix_data: &[u8], + __bumps: &mut IdlCloseAccountBumps, + __reallocs: &mut std::collections::BTreeSet< + anchor_lang::solana_program::pubkey::Pubkey, + >, + ) -> anchor_lang::Result { + let account: anchor_lang::accounts::account::Account = anchor_lang::Accounts::try_accounts( + __program_id, + __accounts, + __ix_data, + __bumps, + __reallocs, + ) + .map_err(|e| e.with_account_name("account"))?; + let authority: Signer = anchor_lang::Accounts::try_accounts( + __program_id, + __accounts, + __ix_data, + __bumps, + __reallocs, + ) + .map_err(|e| e.with_account_name("authority"))?; + let sol_destination: AccountInfo = anchor_lang::Accounts::try_accounts( + __program_id, + __accounts, + __ix_data, + __bumps, + __reallocs, + ) + .map_err(|e| e.with_account_name("sol_destination"))?; + if !AsRef::::as_ref(&account).is_writable { + return Err( + anchor_lang::error::Error::from( + anchor_lang::error::ErrorCode::ConstraintMut, + ) + .with_account_name("account"), + ); + } + { + let my_key = account.authority; + let target_key = authority.key(); + if my_key != target_key { + return Err( + anchor_lang::error::Error::from( + anchor_lang::error::ErrorCode::ConstraintHasOne, + ) + .with_account_name("account") + .with_pubkeys((my_key, target_key)), + ); + } + } + { + if account.key() == sol_destination.key() { + return Err( + anchor_lang::error::Error::from( + anchor_lang::error::ErrorCode::ConstraintClose, + ) + .with_account_name("account"), + ); + } + } + if !(authority.key != &ERASED_AUTHORITY) { + return Err( + anchor_lang::error::Error::from( + anchor_lang::error::ErrorCode::ConstraintRaw, + ) + .with_account_name("authority"), + ); + } + if !&sol_destination.is_writable { + return Err( + anchor_lang::error::Error::from( + anchor_lang::error::ErrorCode::ConstraintMut, + ) + .with_account_name("sol_destination"), + ); + } + Ok(IdlCloseAccount { + account, + authority, + sol_destination, + }) + } + } + #[automatically_derived] + impl<'info> anchor_lang::ToAccountInfos<'info> for IdlCloseAccount<'info> + where + 'info: 'info, + { + fn to_account_infos( + &self, + ) -> Vec> { + let mut account_infos = ::alloc::vec::Vec::new(); + account_infos.extend(self.account.to_account_infos()); + account_infos.extend(self.authority.to_account_infos()); + account_infos.extend(self.sol_destination.to_account_infos()); + account_infos + } + } + #[automatically_derived] + impl<'info> anchor_lang::ToAccountMetas for IdlCloseAccount<'info> { + fn to_account_metas( + &self, + is_signer: Option, + ) -> Vec { + let mut account_metas = ::alloc::vec::Vec::new(); + account_metas.extend(self.account.to_account_metas(None)); + account_metas.extend(self.authority.to_account_metas(None)); + account_metas.extend(self.sol_destination.to_account_metas(None)); + account_metas + } + } + #[automatically_derived] + impl<'info> anchor_lang::AccountsExit<'info> for IdlCloseAccount<'info> + where + 'info: 'info, + { + fn exit( + &self, + program_id: &anchor_lang::solana_program::pubkey::Pubkey, + ) -> anchor_lang::Result<()> { + { + let sol_destination = &self.sol_destination; + anchor_lang::AccountsClose::close( + &self.account, + sol_destination.to_account_info(), + ) + .map_err(|e| e.with_account_name("account"))?; + } + anchor_lang::AccountsExit::exit(&self.sol_destination, program_id) + .map_err(|e| e.with_account_name("sol_destination"))?; + Ok(()) + } + } + pub struct IdlCloseAccountBumps {} + #[automatically_derived] + impl ::core::fmt::Debug for IdlCloseAccountBumps { + #[inline] + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + ::core::fmt::Formatter::write_str(f, "IdlCloseAccountBumps") + } + } + impl Default for IdlCloseAccountBumps { + fn default() -> Self { + IdlCloseAccountBumps {} + } + } + impl<'info> anchor_lang::Bumps for IdlCloseAccount<'info> + where + 'info: 'info, + { + type Bumps = IdlCloseAccountBumps; + } + /// An internal, Anchor generated module. This is used (as an + /// implementation detail), to generate a struct for a given + /// `#[derive(Accounts)]` implementation, where each field is a Pubkey, + /// instead of an `AccountInfo`. This is useful for clients that want + /// to generate a list of accounts, without explicitly knowing the + /// order all the fields should be in. + /// + /// To access the struct in this module, one should use the sibling + /// `accounts` module (also generated), which re-exports this. + pub(crate) mod __client_accounts_idl_close_account { + use super::*; + use anchor_lang::prelude::borsh; + /// Generated client accounts for [`IdlCloseAccount`]. + pub struct IdlCloseAccount { + pub account: Pubkey, + pub authority: Pubkey, + pub sol_destination: Pubkey, + } + impl borsh::ser::BorshSerialize for IdlCloseAccount + where + Pubkey: borsh::ser::BorshSerialize, + Pubkey: borsh::ser::BorshSerialize, + Pubkey: borsh::ser::BorshSerialize, + { + fn serialize( + &self, + writer: &mut W, + ) -> ::core::result::Result<(), borsh::maybestd::io::Error> { + borsh::BorshSerialize::serialize(&self.account, writer)?; + borsh::BorshSerialize::serialize(&self.authority, writer)?; + borsh::BorshSerialize::serialize(&self.sol_destination, writer)?; + Ok(()) + } + } + impl anchor_lang::idl::build::IdlBuild for IdlCloseAccount { + fn create_type() -> Option { + Some(anchor_lang::idl::types::IdlTypeDef { + name: Self::get_full_path(), + docs: <[_]>::into_vec( + ::alloc::boxed::box_new([ + "Generated client accounts for [`IdlCloseAccount`].".into(), + ]), + ), + serialization: anchor_lang::idl::types::IdlSerialization::default(), + repr: None, + generics: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlTypeDefTy::Struct { + fields: Some( + anchor_lang::idl::types::IdlDefinedFields::Named( + <[_]>::into_vec( + ::alloc::boxed::box_new([ + anchor_lang::idl::types::IdlField { + name: "account".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::Pubkey, + }, + anchor_lang::idl::types::IdlField { + name: "authority".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::Pubkey, + }, + anchor_lang::idl::types::IdlField { + name: "sol_destination".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::Pubkey, + }, + ]), + ), + ), + ), + }, + }) + } + fn insert_types( + types: &mut std::collections::BTreeMap< + String, + anchor_lang::idl::types::IdlTypeDef, + >, + ) {} + fn get_full_path() -> String { + ::alloc::__export::must_use({ + let res = ::alloc::fmt::format( + format_args!( + "{0}::{1}", + "anchor_compressible_derived::__private::__idl::__client_accounts_idl_close_account", + "IdlCloseAccount", + ), + ); + res + }) + } + } + #[automatically_derived] + impl anchor_lang::ToAccountMetas for IdlCloseAccount { + fn to_account_metas( + &self, + is_signer: Option, + ) -> Vec { + let mut account_metas = ::alloc::vec::Vec::new(); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new( + self.account, + false, + ), + ); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new_readonly( + self.authority, + true, + ), + ); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new( + self.sol_destination, + false, + ), + ); + account_metas + } + } + } + /// An internal, Anchor generated module. This is used (as an + /// implementation detail), to generate a CPI struct for a given + /// `#[derive(Accounts)]` implementation, where each field is an + /// AccountInfo. + /// + /// To access the struct in this module, one should use the sibling + /// [`cpi::accounts`] module (also generated), which re-exports this. + pub(crate) mod __cpi_client_accounts_idl_close_account { + use super::*; + /// Generated CPI struct of the accounts for [`IdlCloseAccount`]. + pub struct IdlCloseAccount<'info> { + pub account: anchor_lang::solana_program::account_info::AccountInfo< + 'info, + >, + pub authority: anchor_lang::solana_program::account_info::AccountInfo< + 'info, + >, + pub sol_destination: anchor_lang::solana_program::account_info::AccountInfo< + 'info, + >, + } + #[automatically_derived] + impl<'info> anchor_lang::ToAccountMetas for IdlCloseAccount<'info> { + fn to_account_metas( + &self, + is_signer: Option, + ) -> Vec { + let mut account_metas = ::alloc::vec::Vec::new(); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new( + anchor_lang::Key::key(&self.account), + false, + ), + ); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new_readonly( + anchor_lang::Key::key(&self.authority), + true, + ), + ); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new( + anchor_lang::Key::key(&self.sol_destination), + false, + ), + ); + account_metas + } + } + #[automatically_derived] + impl<'info> anchor_lang::ToAccountInfos<'info> for IdlCloseAccount<'info> { + fn to_account_infos( + &self, + ) -> Vec> { + let mut account_infos = ::alloc::vec::Vec::new(); + account_infos + .extend( + anchor_lang::ToAccountInfos::to_account_infos(&self.account), + ); + account_infos + .extend( + anchor_lang::ToAccountInfos::to_account_infos( + &self.authority, + ), + ); + account_infos + .extend( + anchor_lang::ToAccountInfos::to_account_infos( + &self.sol_destination, + ), + ); + account_infos + } + } + } + impl<'info> IdlCloseAccount<'info> { + pub fn __anchor_private_gen_idl_accounts( + accounts: &mut std::collections::BTreeMap< + String, + anchor_lang::idl::types::IdlAccount, + >, + types: &mut std::collections::BTreeMap< + String, + anchor_lang::idl::types::IdlTypeDef, + >, + ) -> Vec { + if let Some(ty) = ::create_type() { + let account = anchor_lang::idl::types::IdlAccount { + name: ty.name.clone(), + discriminator: IdlAccount::DISCRIMINATOR.into(), + }; + accounts.insert(account.name.clone(), account); + types.insert(ty.name.clone(), ty); + ::insert_types(types); + } + <[_]>::into_vec( + ::alloc::boxed::box_new([ + anchor_lang::idl::types::IdlInstructionAccountItem::Single(anchor_lang::idl::types::IdlInstructionAccount { + name: "account".into(), + docs: ::alloc::vec::Vec::new(), + writable: true, + signer: false, + optional: false, + address: None, + pda: None, + relations: ::alloc::vec::Vec::new(), + }), + anchor_lang::idl::types::IdlInstructionAccountItem::Single(anchor_lang::idl::types::IdlInstructionAccount { + name: "authority".into(), + docs: ::alloc::vec::Vec::new(), + writable: false, + signer: true, + optional: false, + address: None, + pda: None, + relations: ::alloc::vec::Vec::new(), + }), + anchor_lang::idl::types::IdlInstructionAccountItem::Single(anchor_lang::idl::types::IdlInstructionAccount { + name: "sol_destination".into(), + docs: ::alloc::vec::Vec::new(), + writable: true, + signer: false, + optional: false, + address: None, + pda: None, + relations: ::alloc::vec::Vec::new(), + }), + ]), + ) + } + } + use std::cell::{Ref, RefMut}; + pub trait IdlTrailingData<'info> { + fn trailing_data(self) -> Ref<'info, [u8]>; + fn trailing_data_mut(self) -> RefMut<'info, [u8]>; + } + impl<'a, 'info: 'a> IdlTrailingData<'a> for &'a Account<'info, IdlAccount> { + fn trailing_data(self) -> Ref<'a, [u8]> { + let info: &AccountInfo<'info> = self.as_ref(); + Ref::map(info.try_borrow_data().unwrap(), |d| &d[44..]) + } + fn trailing_data_mut(self) -> RefMut<'a, [u8]> { + let info: &AccountInfo<'info> = self.as_ref(); + RefMut::map(info.try_borrow_mut_data().unwrap(), |d| &mut d[44..]) + } + } + #[inline(never)] + pub fn __idl_create_account( + program_id: &Pubkey, + accounts: &mut IdlCreateAccounts, + data_len: u64, + ) -> anchor_lang::Result<()> { + ::solana_msg::sol_log("Instruction: IdlCreateAccount"); + if program_id != accounts.program.key { + return Err( + anchor_lang::error::ErrorCode::IdlInstructionInvalidProgram.into(), + ); + } + let from = accounts.from.key; + let (base, nonce) = Pubkey::find_program_address(&[], program_id); + let seed = IdlAccount::seed(); + let owner = accounts.program.key; + let to = Pubkey::create_with_seed(&base, seed, owner).unwrap(); + let space = std::cmp::min( + IdlAccount::DISCRIMINATOR.len() + 32 + 4 + data_len as usize, + 10_000, + ); + let rent = Rent::get()?; + let lamports = rent.minimum_balance(space); + let seeds = &[&[nonce][..]]; + let ix = anchor_lang::solana_program::system_instruction::create_account_with_seed( + from, + &to, + &base, + seed, + lamports, + space as u64, + owner, + ); + anchor_lang::solana_program::program::invoke_signed( + &ix, + &[ + accounts.from.clone(), + accounts.to.clone(), + accounts.base.clone(), + accounts.system_program.to_account_info(), + ], + &[seeds], + )?; + let mut idl_account = { + let mut account_data = accounts.to.try_borrow_data()?; + let mut account_data_slice: &[u8] = &account_data; + IdlAccount::try_deserialize_unchecked(&mut account_data_slice)? + }; + idl_account.authority = *accounts.from.key; + let mut data = accounts.to.try_borrow_mut_data()?; + let dst: &mut [u8] = &mut data; + let mut cursor = std::io::Cursor::new(dst); + idl_account.try_serialize(&mut cursor)?; + Ok(()) + } + #[inline(never)] + pub fn __idl_resize_account( + program_id: &Pubkey, + accounts: &mut IdlResizeAccount, + data_len: u64, + ) -> anchor_lang::Result<()> { + ::solana_msg::sol_log("Instruction: IdlResizeAccount"); + let data_len: usize = data_len as usize; + if accounts.idl.data_len != 0 { + return Err(anchor_lang::error::ErrorCode::IdlAccountNotEmpty.into()); + } + let idl_ref = AsRef::::as_ref(&accounts.idl); + let new_account_space = idl_ref + .data_len() + .checked_add( + std::cmp::min( + data_len + .checked_sub(idl_ref.data_len()) + .expect( + "data_len should always be >= the current account space", + ), + 10_000, + ), + ) + .unwrap(); + if new_account_space > idl_ref.data_len() { + let sysvar_rent = Rent::get()?; + let new_rent_minimum = sysvar_rent.minimum_balance(new_account_space); + anchor_lang::system_program::transfer( + anchor_lang::context::CpiContext::new( + accounts.system_program.to_account_info(), + anchor_lang::system_program::Transfer { + from: accounts.authority.to_account_info(), + to: accounts.idl.to_account_info(), + }, + ), + new_rent_minimum.checked_sub(idl_ref.lamports()).unwrap(), + )?; + idl_ref.realloc(new_account_space, false)?; + } + Ok(()) + } + #[inline(never)] + pub fn __idl_close_account( + program_id: &Pubkey, + accounts: &mut IdlCloseAccount, + ) -> anchor_lang::Result<()> { + ::solana_msg::sol_log("Instruction: IdlCloseAccount"); + Ok(()) + } + #[inline(never)] + pub fn __idl_create_buffer( + program_id: &Pubkey, + accounts: &mut IdlCreateBuffer, + ) -> anchor_lang::Result<()> { + ::solana_msg::sol_log("Instruction: IdlCreateBuffer"); + let mut buffer = &mut accounts.buffer; + buffer.authority = *accounts.authority.key; + Ok(()) + } + #[inline(never)] + pub fn __idl_write( + program_id: &Pubkey, + accounts: &mut IdlAccounts, + idl_data: Vec, + ) -> anchor_lang::Result<()> { + ::solana_msg::sol_log("Instruction: IdlWrite"); + let prev_len: usize = ::std::convert::TryInto::< + usize, + >::try_into(accounts.idl.data_len) + .unwrap(); + let new_len: usize = prev_len.checked_add(idl_data.len()).unwrap() as usize; + accounts.idl.data_len = accounts + .idl + .data_len + .checked_add( + ::std::convert::TryInto::::try_into(idl_data.len()).unwrap(), + ) + .unwrap(); + use IdlTrailingData; + let mut idl_bytes = accounts.idl.trailing_data_mut(); + let idl_expansion = &mut idl_bytes[prev_len..new_len]; + if idl_expansion.len() != idl_data.len() { + return Err( + anchor_lang::error::Error::from(anchor_lang::error::AnchorError { + error_name: anchor_lang::error::ErrorCode::RequireEqViolated + .name(), + error_code_number: anchor_lang::error::ErrorCode::RequireEqViolated + .into(), + error_msg: anchor_lang::error::ErrorCode::RequireEqViolated + .to_string(), + error_origin: Some( + anchor_lang::error::ErrorOrigin::Source(anchor_lang::error::Source { + filename: "sdk-tests/anchor-compressible-derived/src/lib.rs", + line: 87u32, + }), + ), + compared_values: None, + }) + .with_values((idl_expansion.len(), idl_data.len())), + ); + } + idl_expansion.copy_from_slice(&idl_data[..]); + Ok(()) + } + #[inline(never)] + pub fn __idl_set_authority( + program_id: &Pubkey, + accounts: &mut IdlAccounts, + new_authority: Pubkey, + ) -> anchor_lang::Result<()> { + ::solana_msg::sol_log("Instruction: IdlSetAuthority"); + accounts.idl.authority = new_authority; + Ok(()) + } + #[inline(never)] + pub fn __idl_set_buffer( + program_id: &Pubkey, + accounts: &mut IdlSetBuffer, + ) -> anchor_lang::Result<()> { + ::solana_msg::sol_log("Instruction: IdlSetBuffer"); + accounts.idl.data_len = accounts.buffer.data_len; + use IdlTrailingData; + let buffer_len = ::std::convert::TryInto::< + usize, + >::try_into(accounts.buffer.data_len) + .unwrap(); + let mut target = accounts.idl.trailing_data_mut(); + let source = &accounts.buffer.trailing_data()[..buffer_len]; + if target.len() < buffer_len { + return Err( + anchor_lang::error::Error::from(anchor_lang::error::AnchorError { + error_name: anchor_lang::error::ErrorCode::RequireGteViolated + .name(), + error_code_number: anchor_lang::error::ErrorCode::RequireGteViolated + .into(), + error_msg: anchor_lang::error::ErrorCode::RequireGteViolated + .to_string(), + error_origin: Some( + anchor_lang::error::ErrorOrigin::Source(anchor_lang::error::Source { + filename: "sdk-tests/anchor-compressible-derived/src/lib.rs", + line: 87u32, + }), + ), + compared_values: None, + }) + .with_values((target.len(), buffer_len)), + ); + } + target[..buffer_len].copy_from_slice(source); + Ok(()) + } + } + /// __global mod defines wrapped handlers for global instructions. + pub mod __global { + use super::*; + #[inline(never)] + pub fn initialize_compression_config<'info>( + __program_id: &Pubkey, + __accounts: &'info [AccountInfo<'info>], + __ix_data: &[u8], + ) -> anchor_lang::Result<()> { + ::solana_msg::sol_log("Instruction: InitializeCompressionConfig"); + let ix = instruction::InitializeCompressionConfig::deserialize( + &mut &__ix_data[..], + ) + .map_err(|_| { + anchor_lang::error::ErrorCode::InstructionDidNotDeserialize + })?; + let instruction::InitializeCompressionConfig { + compression_delay, + rent_recipient, + address_space, + } = ix; + let mut __bumps = ::Bumps::default(); + let mut __reallocs = std::collections::BTreeSet::new(); + let mut __remaining_accounts: &[AccountInfo] = __accounts; + let mut __accounts = InitializeCompressionConfig::try_accounts( + __program_id, + &mut __remaining_accounts, + __ix_data, + &mut __bumps, + &mut __reallocs, + )?; + let result = anchor_compressible_derived::initialize_compression_config( + anchor_lang::context::Context::new( + __program_id, + &mut __accounts, + __remaining_accounts, + __bumps, + ), + compression_delay, + rent_recipient, + address_space, + )?; + __accounts.exit(__program_id) + } + #[inline(never)] + pub fn update_compression_config<'info>( + __program_id: &Pubkey, + __accounts: &'info [AccountInfo<'info>], + __ix_data: &[u8], + ) -> anchor_lang::Result<()> { + ::solana_msg::sol_log("Instruction: UpdateCompressionConfig"); + let ix = instruction::UpdateCompressionConfig::deserialize( + &mut &__ix_data[..], + ) + .map_err(|_| { + anchor_lang::error::ErrorCode::InstructionDidNotDeserialize + })?; + let instruction::UpdateCompressionConfig { + new_compression_delay, + new_rent_recipient, + new_address_space, + new_update_authority, + } = ix; + let mut __bumps = ::Bumps::default(); + let mut __reallocs = std::collections::BTreeSet::new(); + let mut __remaining_accounts: &[AccountInfo] = __accounts; + let mut __accounts = UpdateCompressionConfig::try_accounts( + __program_id, + &mut __remaining_accounts, + __ix_data, + &mut __bumps, + &mut __reallocs, + )?; + let result = anchor_compressible_derived::update_compression_config( + anchor_lang::context::Context::new( + __program_id, + &mut __accounts, + __remaining_accounts, + __bumps, + ), + new_compression_delay, + new_rent_recipient, + new_address_space, + new_update_authority, + )?; + __accounts.exit(__program_id) + } + #[inline(never)] + pub fn compress_accounts_idempotent<'info>( + __program_id: &Pubkey, + __accounts: &'info [AccountInfo<'info>], + __ix_data: &[u8], + ) -> anchor_lang::Result<()> { + ::solana_msg::sol_log("Instruction: CompressAccountsIdempotent"); + let ix = instruction::CompressAccountsIdempotent::deserialize( + &mut &__ix_data[..], + ) + .map_err(|_| { + anchor_lang::error::ErrorCode::InstructionDidNotDeserialize + })?; + let instruction::CompressAccountsIdempotent { + proof, + compressed_accounts, + signer_seeds, + system_accounts_offset, + } = ix; + let mut __bumps = ::Bumps::default(); + let mut __reallocs = std::collections::BTreeSet::new(); + let mut __remaining_accounts: &[AccountInfo] = __accounts; + let mut __accounts = CompressAccountsIdempotent::try_accounts( + __program_id, + &mut __remaining_accounts, + __ix_data, + &mut __bumps, + &mut __reallocs, + )?; + let result = anchor_compressible_derived::compress_accounts_idempotent( + anchor_lang::context::Context::new( + __program_id, + &mut __accounts, + __remaining_accounts, + __bumps, + ), + proof, + compressed_accounts, + signer_seeds, + system_accounts_offset, + )?; + __accounts.exit(__program_id) + } + #[inline(never)] + pub fn create_record<'info>( + __program_id: &Pubkey, + __accounts: &'info [AccountInfo<'info>], + __ix_data: &[u8], + ) -> anchor_lang::Result<()> { + ::solana_msg::sol_log("Instruction: CreateRecord"); + let ix = instruction::CreateRecord::deserialize(&mut &__ix_data[..]) + .map_err(|_| { + anchor_lang::error::ErrorCode::InstructionDidNotDeserialize + })?; + let instruction::CreateRecord { + name, + proof, + compressed_address, + address_tree_info, + output_state_tree_index, + } = ix; + let mut __bumps = ::Bumps::default(); + let mut __reallocs = std::collections::BTreeSet::new(); + let mut __remaining_accounts: &[AccountInfo] = __accounts; + let mut __accounts = CreateRecord::try_accounts( + __program_id, + &mut __remaining_accounts, + __ix_data, + &mut __bumps, + &mut __reallocs, + )?; + let result = anchor_compressible_derived::create_record( + anchor_lang::context::Context::new( + __program_id, + &mut __accounts, + __remaining_accounts, + __bumps, + ), + name, + proof, + compressed_address, + address_tree_info, + output_state_tree_index, + )?; + __accounts.exit(__program_id) + } + #[inline(never)] + pub fn create_game_session<'info>( + __program_id: &Pubkey, + __accounts: &'info [AccountInfo<'info>], + __ix_data: &[u8], + ) -> anchor_lang::Result<()> { + ::solana_msg::sol_log("Instruction: CreateGameSession"); + let ix = instruction::CreateGameSession::deserialize(&mut &__ix_data[..]) + .map_err(|_| { + anchor_lang::error::ErrorCode::InstructionDidNotDeserialize + })?; + let instruction::CreateGameSession { + session_id, + game_type, + proof, + compressed_address, + address_tree_info, + output_state_tree_index, + } = ix; + let mut __bumps = ::Bumps::default(); + let mut __reallocs = std::collections::BTreeSet::new(); + let mut __remaining_accounts: &[AccountInfo] = __accounts; + let mut __accounts = CreateGameSession::try_accounts( + __program_id, + &mut __remaining_accounts, + __ix_data, + &mut __bumps, + &mut __reallocs, + )?; + let result = anchor_compressible_derived::create_game_session( + anchor_lang::context::Context::new( + __program_id, + &mut __accounts, + __remaining_accounts, + __bumps, + ), + session_id, + game_type, + proof, + compressed_address, + address_tree_info, + output_state_tree_index, + )?; + __accounts.exit(__program_id) + } + #[inline(never)] + pub fn create_user_record_and_game_session<'info>( + __program_id: &Pubkey, + __accounts: &'info [AccountInfo<'info>], + __ix_data: &[u8], + ) -> anchor_lang::Result<()> { + ::solana_msg::sol_log("Instruction: CreateUserRecordAndGameSession"); + let ix = instruction::CreateUserRecordAndGameSession::deserialize( + &mut &__ix_data[..], + ) + .map_err(|_| { + anchor_lang::error::ErrorCode::InstructionDidNotDeserialize + })?; + let instruction::CreateUserRecordAndGameSession { + account_data, + compression_params, + } = ix; + let mut __bumps = ::Bumps::default(); + let mut __reallocs = std::collections::BTreeSet::new(); + let mut __remaining_accounts: &[AccountInfo] = __accounts; + let mut __accounts = CreateUserRecordAndGameSession::try_accounts( + __program_id, + &mut __remaining_accounts, + __ix_data, + &mut __bumps, + &mut __reallocs, + )?; + let result = anchor_compressible_derived::create_user_record_and_game_session( + anchor_lang::context::Context::new( + __program_id, + &mut __accounts, + __remaining_accounts, + __bumps, + ), + account_data, + compression_params, + )?; + __accounts.exit(__program_id) + } + #[inline(never)] + pub fn create_placeholder_record<'info>( + __program_id: &Pubkey, + __accounts: &'info [AccountInfo<'info>], + __ix_data: &[u8], + ) -> anchor_lang::Result<()> { + ::solana_msg::sol_log("Instruction: CreatePlaceholderRecord"); + let ix = instruction::CreatePlaceholderRecord::deserialize( + &mut &__ix_data[..], + ) + .map_err(|_| { + anchor_lang::error::ErrorCode::InstructionDidNotDeserialize + })?; + let instruction::CreatePlaceholderRecord { + placeholder_id, + name, + proof, + compressed_address, + address_tree_info, + output_state_tree_index, + } = ix; + let mut __bumps = ::Bumps::default(); + let mut __reallocs = std::collections::BTreeSet::new(); + let mut __remaining_accounts: &[AccountInfo] = __accounts; + let mut __accounts = CreatePlaceholderRecord::try_accounts( + __program_id, + &mut __remaining_accounts, + __ix_data, + &mut __bumps, + &mut __reallocs, + )?; + let result = anchor_compressible_derived::create_placeholder_record( + anchor_lang::context::Context::new( + __program_id, + &mut __accounts, + __remaining_accounts, + __bumps, + ), + placeholder_id, + name, + proof, + compressed_address, + address_tree_info, + output_state_tree_index, + )?; + __accounts.exit(__program_id) + } + #[inline(never)] + pub fn update_record<'info>( + __program_id: &Pubkey, + __accounts: &'info [AccountInfo<'info>], + __ix_data: &[u8], + ) -> anchor_lang::Result<()> { + ::solana_msg::sol_log("Instruction: UpdateRecord"); + let ix = instruction::UpdateRecord::deserialize(&mut &__ix_data[..]) + .map_err(|_| { + anchor_lang::error::ErrorCode::InstructionDidNotDeserialize + })?; + let instruction::UpdateRecord { name, score } = ix; + let mut __bumps = ::Bumps::default(); + let mut __reallocs = std::collections::BTreeSet::new(); + let mut __remaining_accounts: &[AccountInfo] = __accounts; + let mut __accounts = UpdateRecord::try_accounts( + __program_id, + &mut __remaining_accounts, + __ix_data, + &mut __bumps, + &mut __reallocs, + )?; + let result = anchor_compressible_derived::update_record( + anchor_lang::context::Context::new( + __program_id, + &mut __accounts, + __remaining_accounts, + __bumps, + ), + name, + score, + )?; + __accounts.exit(__program_id) + } + #[inline(never)] + pub fn update_game_session<'info>( + __program_id: &Pubkey, + __accounts: &'info [AccountInfo<'info>], + __ix_data: &[u8], + ) -> anchor_lang::Result<()> { + ::solana_msg::sol_log("Instruction: UpdateGameSession"); + let ix = instruction::UpdateGameSession::deserialize(&mut &__ix_data[..]) + .map_err(|_| { + anchor_lang::error::ErrorCode::InstructionDidNotDeserialize + })?; + let instruction::UpdateGameSession { _session_id, new_score } = ix; + let mut __bumps = ::Bumps::default(); + let mut __reallocs = std::collections::BTreeSet::new(); + let mut __remaining_accounts: &[AccountInfo] = __accounts; + let mut __accounts = UpdateGameSession::try_accounts( + __program_id, + &mut __remaining_accounts, + __ix_data, + &mut __bumps, + &mut __reallocs, + )?; + let result = anchor_compressible_derived::update_game_session( + anchor_lang::context::Context::new( + __program_id, + &mut __accounts, + __remaining_accounts, + __bumps, + ), + _session_id, + new_score, + )?; + __accounts.exit(__program_id) + } + #[inline(never)] + pub fn decompress_accounts_idempotent<'info>( + __program_id: &Pubkey, + __accounts: &'info [AccountInfo<'info>], + __ix_data: &[u8], + ) -> anchor_lang::Result<()> { + ::solana_msg::sol_log("Instruction: DecompressAccountsIdempotent"); + let ix = instruction::DecompressAccountsIdempotent::deserialize( + &mut &__ix_data[..], + ) + .map_err(|_| { + anchor_lang::error::ErrorCode::InstructionDidNotDeserialize + })?; + let instruction::DecompressAccountsIdempotent { + proof, + compressed_accounts, + system_accounts_offset, + } = ix; + let mut __bumps = ::Bumps::default(); + let mut __reallocs = std::collections::BTreeSet::new(); + let mut __remaining_accounts: &[AccountInfo] = __accounts; + let mut __accounts = DecompressAccountsIdempotent::try_accounts( + __program_id, + &mut __remaining_accounts, + __ix_data, + &mut __bumps, + &mut __reallocs, + )?; + let result = anchor_compressible_derived::decompress_accounts_idempotent( + anchor_lang::context::Context::new( + __program_id, + &mut __accounts, + __remaining_accounts, + __bumps, + ), + proof, + compressed_accounts, + system_accounts_offset, + )?; + __accounts.exit(__program_id) + } + } +} +pub mod anchor_compressible_derived { + use light_compressed_token_sdk::{ + create_compressible_token_account, + instructions::{ + create_mint_action_cpi, decompress_full_ctoken_accounts_with_indices, + find_spl_mint_address, DecompressFullIndices, MintActionInputs, + }, + }; + use light_sdk::compressible::{ + compress_account::prepare_account_for_compression, + into_compressed_meta_with_address, + }; + use light_sdk_types::cpi_context_write::CpiContextWriteAccounts; + use super::*; + pub fn initialize_compression_config( + ctx: Context, + compression_delay: u32, + rent_recipient: Pubkey, + address_space: Vec, + ) -> Result<()> { + process_initialize_compression_config_checked( + &ctx.accounts.config.to_account_info(), + &ctx.accounts.authority.to_account_info(), + &ctx.accounts.program_data.to_account_info(), + &rent_recipient, + address_space, + compression_delay, + 0, + &ctx.accounts.payer.to_account_info(), + &ctx.accounts.system_program.to_account_info(), + &crate::ID, + )?; + Ok(()) + } + pub fn update_compression_config( + ctx: Context, + new_compression_delay: Option, + new_rent_recipient: Option, + new_address_space: Option>, + new_update_authority: Option, + ) -> Result<()> { + process_update_compression_config( + &ctx.accounts.config.to_account_info(), + &ctx.accounts.authority.to_account_info(), + new_update_authority.as_ref(), + new_rent_recipient.as_ref(), + new_address_space, + new_compression_delay, + &crate::ID, + )?; + Ok(()) + } + /// Compress multiple accounts (PDAs and token accounts) in a single instruction. + pub fn compress_accounts_idempotent<'info>( + ctx: Context<'_, '_, 'info, 'info, CompressAccountsIdempotent<'info>>, + proof: ValidityProof, + compressed_accounts: Vec, + signer_seeds: Vec>>, + system_accounts_offset: u8, + ) -> Result<()> { + let compression_config = CompressibleConfig::load_checked( + &ctx.accounts.config, + &crate::ID, + )?; + if ctx.accounts.rent_recipient.key() != compression_config.rent_recipient { + return Err( + anchor_lang::error::Error::from(anchor_lang::error::AnchorError { + error_name: ErrorCode::InvalidRentRecipient.name(), + error_code_number: ErrorCode::InvalidRentRecipient.into(), + error_msg: ErrorCode::InvalidRentRecipient.to_string(), + error_origin: Some( + anchor_lang::error::ErrorOrigin::Source(anchor_lang::error::Source { + filename: "sdk-tests/anchor-compressible-derived/src/lib.rs", + line: 159u32, + }), + ), + compared_values: None, + }), + ); + } + let cpi_accounts = CpiAccountsSmall::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 token_accounts_to_compress = Vec::new(); + let mut compressed_pda_infos = Vec::new(); + let mut user_records = Vec::new(); + let mut game_sessions = Vec::new(); + let mut placeholder_records = Vec::new(); + for (i, account_info) in solana_accounts.iter().enumerate() { + if account_info.data_is_empty() { + ::solana_msg::sol_log( + "No data. Account already compressed or uninitialized. Skipping.", + ); + continue; + } + if account_info.owner == &COMPRESSED_TOKEN_PROGRAM_ID.into() { + if let Ok(token_account) = InterfaceAccount::< + TokenAccount, + >::try_from(account_info) { + let account_signer_seeds = signer_seeds[i].clone(); + token_accounts_to_compress + .push(light_compressed_token_sdk::TokenAccountToCompress { + token_account, + signer_seeds: account_signer_seeds, + }); + } + } else if account_info.owner == &crate::ID { + let data = account_info.try_borrow_data()?; + let discriminator = &data[0..8]; + let meta = compressed_accounts[i]; + match discriminator { + d if d == UserRecord::discriminator() => { + let mut anchor_account = Account::< + UserRecord, + >::try_from(account_info)?; + let compressed_info = prepare_account_for_compression::< + UserRecord, + >( + &crate::ID, + &mut anchor_account, + &meta, + &cpi_accounts, + &compression_config.compression_delay, + &compression_config.address_space, + )?; + user_records.push(anchor_account); + compressed_pda_infos.push(compressed_info); + } + d if d == GameSession::discriminator() => { + let mut anchor_account = Account::< + GameSession, + >::try_from(account_info)?; + let compressed_info = prepare_account_for_compression::< + GameSession, + >( + &crate::ID, + &mut anchor_account, + &meta, + &cpi_accounts, + &compression_config.compression_delay, + &compression_config.address_space, + )?; + game_sessions.push(anchor_account); + compressed_pda_infos.push(compressed_info); + } + d if d == PlaceholderRecord::discriminator() => { + let mut anchor_account = Account::< + PlaceholderRecord, + >::try_from(account_info)?; + let compressed_info = prepare_account_for_compression::< + PlaceholderRecord, + >( + &crate::ID, + &mut anchor_account, + &meta, + &cpi_accounts, + &compression_config.compression_delay, + &compression_config.address_space, + )?; + placeholder_records.push(anchor_account); + compressed_pda_infos.push(compressed_info); + } + _ => { + { + ::core::panicking::panic_fmt( + format_args!( + "Trying to compress with invalid account discriminator", + ), + ); + }; + } + } + } + } + let has_pdas = !compressed_pda_infos.is_empty(); + let has_tokens = !token_accounts_to_compress.is_empty(); + if has_tokens { + light_compressed_token_sdk::compress_and_close_token_accounts( + crate::ID, + &ctx.accounts.fee_payer, + cpi_accounts.authority().unwrap(), + ctx.accounts.compressed_token_cpi_authority.as_ref().unwrap(), + ctx.accounts.compressed_token_program.as_ref().unwrap(), + &ctx.accounts.config, + &ctx.accounts.rent_recipient, + ctx.remaining_accounts, + token_accounts_to_compress, + LIGHT_CPI_SIGNER, + )?; + } + if has_pdas { + let cpi_inputs = CpiInputs::new(proof, compressed_pda_infos); + cpi_inputs.invoke_light_system_program_small(cpi_accounts)?; + } + for anchor_account in user_records.iter() { + anchor_account.close(ctx.accounts.rent_recipient.clone())?; + } + for anchor_account in game_sessions.iter() { + anchor_account.close(ctx.accounts.rent_recipient.clone())?; + } + for anchor_account in placeholder_records.iter() { + anchor_account.close(ctx.accounts.rent_recipient.clone())?; + } + Ok(()) + } + pub fn create_record<'info>( + ctx: Context<'_, '_, '_, 'info, CreateRecord<'info>>, + name: String, + proof: ValidityProof, + compressed_address: [u8; 32], + address_tree_info: PackedAddressTreeInfo, + output_state_tree_index: u8, + ) -> Result<()> { + let user_record = &mut ctx.accounts.user_record; + 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; + if ctx.accounts.rent_recipient.key() != config.rent_recipient { + return Err( + anchor_lang::error::Error::from(anchor_lang::error::AnchorError { + error_name: ErrorCode::InvalidRentRecipient.name(), + error_code_number: ErrorCode::InvalidRentRecipient.into(), + error_msg: ErrorCode::InvalidRentRecipient.to_string(), + error_origin: Some( + anchor_lang::error::ErrorOrigin::Source(anchor_lang::error::Source { + filename: "sdk-tests/anchor-compressible-derived/src/lib.rs", + line: 580u32, + }), + ), + compared_values: None, + }), + ); + } + let user_account_info = ctx.accounts.user.to_account_info(); + let cpi_accounts = CpiAccountsSmall::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(), + true, + Some(0), + ); + compress_account_on_init::< + UserRecord, + >( + user_record, + &compressed_address, + &new_address_params, + output_state_tree_index, + cpi_accounts, + &config.address_space, + &ctx.accounts.rent_recipient, + proof, + )?; + user_record.close(ctx.accounts.rent_recipient.to_account_info())?; + Ok(()) + } + 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; + let config = CompressibleConfig::load_checked(&ctx.accounts.config, &crate::ID)?; + 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; + if ctx.accounts.rent_recipient.key() != config.rent_recipient { + return Err( + anchor_lang::error::Error::from(anchor_lang::error::AnchorError { + error_name: ErrorCode::InvalidRentRecipient.name(), + error_code_number: ErrorCode::InvalidRentRecipient.into(), + error_msg: ErrorCode::InvalidRentRecipient.to_string(), + error_origin: Some( + anchor_lang::error::ErrorOrigin::Source(anchor_lang::error::Source { + filename: "sdk-tests/anchor-compressible-derived/src/lib.rs", + line: 636u32, + }), + ), + compared_values: None, + }), + ); + } + let player_account_info = ctx.accounts.player.to_account_info(); + let cpi_accounts = CpiAccountsSmall::new( + &player_account_info, + ctx.remaining_accounts, + LIGHT_CPI_SIGNER, + ); + let new_address_params = address_tree_info + .into_new_address_params_assigned_packed( + game_session.key().to_bytes(), + true, + Some(0), + ); + compress_account_on_init::< + GameSession, + >( + game_session, + &compressed_address, + &new_address_params, + output_state_tree_index, + cpi_accounts, + &config.address_space, + &ctx.accounts.rent_recipient, + proof, + )?; + game_session.close(ctx.accounts.rent_recipient.to_account_info())?; + Ok(()) + } + 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; + let config = CompressibleConfig::load_checked(&ctx.accounts.config, &crate::ID)?; + if ctx.accounts.rent_recipient.key() != config.rent_recipient { + return Err( + anchor_lang::error::Error::from(anchor_lang::error::AnchorError { + error_name: ErrorCode::InvalidRentRecipient.name(), + error_code_number: ErrorCode::InvalidRentRecipient.into(), + error_msg: ErrorCode::InvalidRentRecipient.to_string(), + error_origin: Some( + anchor_lang::error::ErrorOrigin::Source(anchor_lang::error::Source { + filename: "sdk-tests/anchor-compressible-derived/src/lib.rs", + line: 690u32, + }), + ), + compared_values: None, + }), + ); + } + 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; + let cpi_accounts = CpiAccountsSmall::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(); + let user_new_address_params = compression_params + .user_address_tree_info + .into_new_address_params_assigned_packed( + user_record.key().to_bytes(), + true, + Some(0), + ); + let game_new_address_params = compression_params + .game_address_tree_info + .into_new_address_params_assigned_packed( + game_session.key().to_bytes(), + true, + Some(1), + ); + let mut all_compressed_infos = Vec::new(); + let user_compressed_infos = prepare_accounts_for_compression_on_init::< + UserRecord, + >( + &[user_record], + &[compression_params.user_compressed_address], + &[user_new_address_params], + &[compression_params.user_output_state_tree_index], + &cpi_accounts, + &config.address_space, + &ctx.accounts.rent_recipient, + )?; + all_compressed_infos.extend(user_compressed_infos); + let game_compressed_infos = prepare_accounts_for_compression_on_init::< + GameSession, + >( + &[game_session], + &[compression_params.game_compressed_address], + &[game_new_address_params], + &[compression_params.game_output_state_tree_index], + &cpi_accounts, + &config.address_space, + &ctx.accounts.rent_recipient, + )?; + all_compressed_infos.extend(game_compressed_infos); + let cpi_inputs = CpiInputs::new_first_cpi( + all_compressed_infos, + <[_]>::into_vec( + ::alloc::boxed::box_new([ + user_new_address_params, + game_new_address_params, + ]), + ), + ); + 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, + }; + cpi_inputs.invoke_light_system_program_cpi_context(cpi_context_accounts)?; + 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 = <[_]>::into_vec( + ::alloc::boxed::box_new([ + light_compressed_token_sdk::instructions::mint_action::MintActionType::MintTo { + recipients: <[_]>::into_vec( + ::alloc::boxed::box_new([ + light_compressed_token_sdk::instructions::mint_action::MintToRecipient { + recipient: token_account_address, + amount: 1000, + }, + ]), + ), + lamports: None, + token_account_version: 2, + }, + ]), + ); + let output_queue = *cpi_accounts.tree_accounts().unwrap()[0].key; + let address_tree_pubkey = *cpi_accounts.tree_accounts().unwrap()[1].key; + 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, + output_queue, + tokens_out_queue: Some(output_queue), + address_tree_pubkey, + token_pool: None, + }; + let mint_action_instruction = create_mint_action_cpi( + mint_action_inputs, + Some(light_ctoken_types::instructions::mint_action::CpiContext { + set_context: false, + first_set_context: false, + in_tree_index: 1, + in_queue_index: 0, + out_queue_index: 0, + token_out_queue_index: 0, + assigned_account_index: 2, + }), + Some(cpi_context_pubkey), + ) + .unwrap(); + 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.compressed_token_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(&mint_action_instruction, &account_infos)?; + user_record.close(ctx.accounts.rent_recipient.to_account_info())?; + game_session.close(ctx.accounts.rent_recipient.to_account_info())?; + Ok(()) + } + /// Creates an empty compressed account while keeping the PDA intact. + /// This demonstrates the compress_empty_account_on_init functionality. + pub fn create_placeholder_record<'info>( + ctx: Context<'_, '_, '_, 'info, CreatePlaceholderRecord<'info>>, + placeholder_id: u64, + name: String, + proof: ValidityProof, + compressed_address: [u8; 32], + address_tree_info: PackedAddressTreeInfo, + output_state_tree_index: u8, + ) -> Result<()> { + let placeholder_record = &mut ctx.accounts.placeholder_record; + let config = CompressibleConfig::load_checked(&ctx.accounts.config, &crate::ID)?; + placeholder_record.owner = ctx.accounts.user.key(); + placeholder_record.name = name; + placeholder_record.placeholder_id = placeholder_id; + *placeholder_record.compression_info_mut_opt() = Some( + super::CompressionInfo::new_decompressed()?, + ); + placeholder_record.compression_info_mut().bump_last_written_slot()?; + if ctx.accounts.rent_recipient.key() != config.rent_recipient { + return Err( + anchor_lang::error::Error::from(anchor_lang::error::AnchorError { + error_name: ErrorCode::InvalidRentRecipient.name(), + error_code_number: ErrorCode::InvalidRentRecipient.into(), + error_msg: ErrorCode::InvalidRentRecipient.to_string(), + error_origin: Some( + anchor_lang::error::ErrorOrigin::Source(anchor_lang::error::Source { + filename: "sdk-tests/anchor-compressible-derived/src/lib.rs", + line: 873u32, + }), + ), + compared_values: None, + }), + ); + } + let user_account_info = ctx.accounts.user.to_account_info(); + let cpi_accounts = CpiAccountsSmall::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(), + true, + Some(0), + ); + compress_empty_account_on_init::< + PlaceholderRecord, + >( + placeholder_record, + &compressed_address, + &new_address_params, + output_state_tree_index, + cpi_accounts, + &config.address_space, + proof, + )?; + Ok(()) + } + pub fn update_record( + ctx: Context, + name: String, + score: u64, + ) -> Result<()> { + let user_record = &mut ctx.accounts.user_record; + user_record.name = name; + user_record.score = score; + user_record.compression_info_mut().bump_last_written_slot()?; + Ok(()) + } + pub fn update_game_session( + ctx: Context, + _session_id: u64, + new_score: u64, + ) -> Result<()> { + let game_session = &mut ctx.accounts.game_session; + game_session.score = new_score; + game_session.end_time = Some(Clock::get()?.unix_timestamp as u64); + game_session.compression_info_mut().bump_last_written_slot()?; + Ok(()) + } + pub struct DecompressAccountsIdempotent<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + /// UNCHECKED: Anyone can pay to init. + #[account(mut)] + pub rent_payer: Signer<'info>, + /// The global config account + /// CHECK: load_checked. + pub config: AccountInfo<'info>, + /// Compressed token program + /// CHECK: Program ID validated to be cTokenmWW8bLPjZEBAUgYy3zKxQZW6VKi7bqNFEVv3m + pub compressed_token_program: Option>, + /// CPI authority PDA of the compressed token program + /// CHECK: PDA derivation validated with seeds ["cpi_authority"] and bump 254 + pub compressed_token_cpi_authority: Option>, + } + #[automatically_derived] + impl<'info> anchor_lang::Accounts<'info, DecompressAccountsIdempotentBumps> + for DecompressAccountsIdempotent<'info> + where + 'info: 'info, + { + #[inline(never)] + fn try_accounts( + __program_id: &anchor_lang::solana_program::pubkey::Pubkey, + __accounts: &mut &'info [anchor_lang::solana_program::account_info::AccountInfo< + 'info, + >], + __ix_data: &[u8], + __bumps: &mut DecompressAccountsIdempotentBumps, + __reallocs: &mut std::collections::BTreeSet< + anchor_lang::solana_program::pubkey::Pubkey, + >, + ) -> anchor_lang::Result { + let fee_payer: Signer = anchor_lang::Accounts::try_accounts( + __program_id, + __accounts, + __ix_data, + __bumps, + __reallocs, + ) + .map_err(|e| e.with_account_name("fee_payer"))?; + let rent_payer: Signer = anchor_lang::Accounts::try_accounts( + __program_id, + __accounts, + __ix_data, + __bumps, + __reallocs, + ) + .map_err(|e| e.with_account_name("rent_payer"))?; + let config: AccountInfo = anchor_lang::Accounts::try_accounts( + __program_id, + __accounts, + __ix_data, + __bumps, + __reallocs, + ) + .map_err(|e| e.with_account_name("config"))?; + let compressed_token_program: Option = anchor_lang::Accounts::try_accounts( + __program_id, + __accounts, + __ix_data, + __bumps, + __reallocs, + ) + .map_err(|e| e.with_account_name("compressed_token_program"))?; + let compressed_token_cpi_authority: Option = anchor_lang::Accounts::try_accounts( + __program_id, + __accounts, + __ix_data, + __bumps, + __reallocs, + ) + .map_err(|e| e.with_account_name("compressed_token_cpi_authority"))?; + if !AsRef::::as_ref(&fee_payer).is_writable { + return Err( + anchor_lang::error::Error::from( + anchor_lang::error::ErrorCode::ConstraintMut, + ) + .with_account_name("fee_payer"), + ); + } + if !AsRef::::as_ref(&rent_payer).is_writable { + return Err( + anchor_lang::error::Error::from( + anchor_lang::error::ErrorCode::ConstraintMut, + ) + .with_account_name("rent_payer"), + ); + } + Ok(DecompressAccountsIdempotent { + fee_payer, + rent_payer, + config, + compressed_token_program, + compressed_token_cpi_authority, + }) + } + } + #[automatically_derived] + impl<'info> anchor_lang::ToAccountInfos<'info> + for DecompressAccountsIdempotent<'info> + where + 'info: 'info, + { + fn to_account_infos( + &self, + ) -> Vec> { + let mut account_infos = ::alloc::vec::Vec::new(); + account_infos.extend(self.fee_payer.to_account_infos()); + account_infos.extend(self.rent_payer.to_account_infos()); + account_infos.extend(self.config.to_account_infos()); + account_infos.extend(self.compressed_token_program.to_account_infos()); + account_infos.extend(self.compressed_token_cpi_authority.to_account_infos()); + account_infos + } + } + #[automatically_derived] + impl<'info> anchor_lang::ToAccountMetas for DecompressAccountsIdempotent<'info> { + fn to_account_metas( + &self, + is_signer: Option, + ) -> Vec { + let mut account_metas = ::alloc::vec::Vec::new(); + account_metas.extend(self.fee_payer.to_account_metas(None)); + account_metas.extend(self.rent_payer.to_account_metas(None)); + account_metas.extend(self.config.to_account_metas(None)); + if let Some(compressed_token_program) = &self.compressed_token_program { + account_metas.extend(compressed_token_program.to_account_metas(None)); + } else { + account_metas.push(AccountMeta::new_readonly(crate::ID, false)); + } + if let Some(compressed_token_cpi_authority) = &self + .compressed_token_cpi_authority + { + account_metas + .extend(compressed_token_cpi_authority.to_account_metas(None)); + } else { + account_metas.push(AccountMeta::new_readonly(crate::ID, false)); + } + account_metas + } + } + #[automatically_derived] + impl<'info> anchor_lang::AccountsExit<'info> for DecompressAccountsIdempotent<'info> + where + 'info: 'info, + { + fn exit( + &self, + program_id: &anchor_lang::solana_program::pubkey::Pubkey, + ) -> anchor_lang::Result<()> { + anchor_lang::AccountsExit::exit(&self.fee_payer, program_id) + .map_err(|e| e.with_account_name("fee_payer"))?; + anchor_lang::AccountsExit::exit(&self.rent_payer, program_id) + .map_err(|e| e.with_account_name("rent_payer"))?; + Ok(()) + } + } + pub struct DecompressAccountsIdempotentBumps {} + #[automatically_derived] + impl ::core::fmt::Debug for DecompressAccountsIdempotentBumps { + #[inline] + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + ::core::fmt::Formatter::write_str(f, "DecompressAccountsIdempotentBumps") + } + } + impl Default for DecompressAccountsIdempotentBumps { + fn default() -> Self { + DecompressAccountsIdempotentBumps { + } + } + } + impl<'info> anchor_lang::Bumps for DecompressAccountsIdempotent<'info> + where + 'info: 'info, + { + type Bumps = DecompressAccountsIdempotentBumps; + } + /// An internal, Anchor generated module. This is used (as an + /// implementation detail), to generate a struct for a given + /// `#[derive(Accounts)]` implementation, where each field is a Pubkey, + /// instead of an `AccountInfo`. This is useful for clients that want + /// to generate a list of accounts, without explicitly knowing the + /// order all the fields should be in. + /// + /// To access the struct in this module, one should use the sibling + /// `accounts` module (also generated), which re-exports this. + pub(crate) mod __client_accounts_decompress_accounts_idempotent { + use super::*; + use anchor_lang::prelude::borsh; + /// Generated client accounts for [`DecompressAccountsIdempotent`]. + pub struct DecompressAccountsIdempotent { + pub fee_payer: Pubkey, + ///UNCHECKED: Anyone can pay to init. + pub rent_payer: Pubkey, + ///The global config account + pub config: Pubkey, + ///Compressed token program + pub compressed_token_program: Option, + ///CPI authority PDA of the compressed token program + pub compressed_token_cpi_authority: Option, + } + impl borsh::ser::BorshSerialize for DecompressAccountsIdempotent + where + Pubkey: borsh::ser::BorshSerialize, + Pubkey: borsh::ser::BorshSerialize, + Pubkey: borsh::ser::BorshSerialize, + Option: borsh::ser::BorshSerialize, + Option: borsh::ser::BorshSerialize, + { + fn serialize( + &self, + writer: &mut W, + ) -> ::core::result::Result<(), borsh::maybestd::io::Error> { + borsh::BorshSerialize::serialize(&self.fee_payer, writer)?; + borsh::BorshSerialize::serialize(&self.rent_payer, writer)?; + borsh::BorshSerialize::serialize(&self.config, writer)?; + borsh::BorshSerialize::serialize( + &self.compressed_token_program, + writer, + )?; + borsh::BorshSerialize::serialize( + &self.compressed_token_cpi_authority, + writer, + )?; + Ok(()) + } + } + impl anchor_lang::idl::build::IdlBuild for DecompressAccountsIdempotent { + fn create_type() -> Option { + Some(anchor_lang::idl::types::IdlTypeDef { + name: Self::get_full_path(), + docs: <[_]>::into_vec( + ::alloc::boxed::box_new([ + "Generated client accounts for [`DecompressAccountsIdempotent`]." + .into(), + ]), + ), + serialization: anchor_lang::idl::types::IdlSerialization::default(), + repr: None, + generics: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlTypeDefTy::Struct { + fields: Some( + anchor_lang::idl::types::IdlDefinedFields::Named( + <[_]>::into_vec( + ::alloc::boxed::box_new([ + anchor_lang::idl::types::IdlField { + name: "fee_payer".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::Pubkey, + }, + anchor_lang::idl::types::IdlField { + name: "rent_payer".into(), + docs: <[_]>::into_vec( + ::alloc::boxed::box_new([ + "UNCHECKED: Anyone can pay to init.".into(), + ]), + ), + ty: anchor_lang::idl::types::IdlType::Pubkey, + }, + anchor_lang::idl::types::IdlField { + name: "config".into(), + docs: <[_]>::into_vec( + ::alloc::boxed::box_new([ + "The global config account".into(), + ]), + ), + ty: anchor_lang::idl::types::IdlType::Pubkey, + }, + anchor_lang::idl::types::IdlField { + name: "compressed_token_program".into(), + docs: <[_]>::into_vec( + ::alloc::boxed::box_new(["Compressed token program".into()]), + ), + ty: anchor_lang::idl::types::IdlType::Option( + Box::new(anchor_lang::idl::types::IdlType::Pubkey), + ), + }, + anchor_lang::idl::types::IdlField { + name: "compressed_token_cpi_authority".into(), + docs: <[_]>::into_vec( + ::alloc::boxed::box_new([ + "CPI authority PDA of the compressed token program".into(), + ]), + ), + ty: anchor_lang::idl::types::IdlType::Option( + Box::new(anchor_lang::idl::types::IdlType::Pubkey), + ), + }, + ]), + ), + ), + ), + }, + }) + } + fn insert_types( + types: &mut std::collections::BTreeMap< + String, + anchor_lang::idl::types::IdlTypeDef, + >, + ) {} + fn get_full_path() -> String { + ::alloc::__export::must_use({ + let res = ::alloc::fmt::format( + format_args!( + "{0}::{1}", + "anchor_compressible_derived::anchor_compressible_derived::__client_accounts_decompress_accounts_idempotent", + "DecompressAccountsIdempotent", + ), + ); + res + }) + } + } + #[automatically_derived] + impl anchor_lang::ToAccountMetas for DecompressAccountsIdempotent { + fn to_account_metas( + &self, + is_signer: Option, + ) -> Vec { + let mut account_metas = ::alloc::vec::Vec::new(); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new( + self.fee_payer, + true, + ), + ); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new( + self.rent_payer, + true, + ), + ); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new_readonly( + self.config, + false, + ), + ); + if let Some(compressed_token_program) = &self.compressed_token_program { + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new_readonly( + *compressed_token_program, + false, + ), + ); + } else { + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new_readonly( + crate::ID, + false, + ), + ); + } + if let Some(compressed_token_cpi_authority) = &self + .compressed_token_cpi_authority + { + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new_readonly( + *compressed_token_cpi_authority, + false, + ), + ); + } else { + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new_readonly( + crate::ID, + false, + ), + ); + } + account_metas + } + } + } + /// An internal, Anchor generated module. This is used (as an + /// implementation detail), to generate a CPI struct for a given + /// `#[derive(Accounts)]` implementation, where each field is an + /// AccountInfo. + /// + /// To access the struct in this module, one should use the sibling + /// [`cpi::accounts`] module (also generated), which re-exports this. + pub(crate) mod __cpi_client_accounts_decompress_accounts_idempotent { + use super::*; + /// Generated CPI struct of the accounts for [`DecompressAccountsIdempotent`]. + pub struct DecompressAccountsIdempotent<'info> { + pub fee_payer: anchor_lang::solana_program::account_info::AccountInfo<'info>, + ///UNCHECKED: Anyone can pay to init. + pub rent_payer: anchor_lang::solana_program::account_info::AccountInfo< + 'info, + >, + ///The global config account + pub config: anchor_lang::solana_program::account_info::AccountInfo<'info>, + ///Compressed token program + pub compressed_token_program: Option< + anchor_lang::solana_program::account_info::AccountInfo<'info>, + >, + ///CPI authority PDA of the compressed token program + pub compressed_token_cpi_authority: Option< + anchor_lang::solana_program::account_info::AccountInfo<'info>, + >, + } + #[automatically_derived] + impl<'info> anchor_lang::ToAccountMetas for DecompressAccountsIdempotent<'info> { + fn to_account_metas( + &self, + is_signer: Option, + ) -> Vec { + let mut account_metas = ::alloc::vec::Vec::new(); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new( + anchor_lang::Key::key(&self.fee_payer), + true, + ), + ); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new( + anchor_lang::Key::key(&self.rent_payer), + true, + ), + ); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new_readonly( + anchor_lang::Key::key(&self.config), + false, + ), + ); + if let Some(compressed_token_program) = &self.compressed_token_program { + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new_readonly( + anchor_lang::Key::key(compressed_token_program), + false, + ), + ); + } else { + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new_readonly( + crate::ID, + false, + ), + ); + } + if let Some(compressed_token_cpi_authority) = &self + .compressed_token_cpi_authority + { + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new_readonly( + anchor_lang::Key::key(compressed_token_cpi_authority), + false, + ), + ); + } else { + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new_readonly( + crate::ID, + false, + ), + ); + } + account_metas + } + } + #[automatically_derived] + impl<'info> anchor_lang::ToAccountInfos<'info> + for DecompressAccountsIdempotent<'info> { + fn to_account_infos( + &self, + ) -> Vec> { + let mut account_infos = ::alloc::vec::Vec::new(); + account_infos + .extend( + anchor_lang::ToAccountInfos::to_account_infos(&self.fee_payer), + ); + account_infos + .extend( + anchor_lang::ToAccountInfos::to_account_infos(&self.rent_payer), + ); + account_infos + .extend(anchor_lang::ToAccountInfos::to_account_infos(&self.config)); + account_infos + .extend( + anchor_lang::ToAccountInfos::to_account_infos( + &self.compressed_token_program, + ), + ); + account_infos + .extend( + anchor_lang::ToAccountInfos::to_account_infos( + &self.compressed_token_cpi_authority, + ), + ); + account_infos + } + } + } + impl<'info> DecompressAccountsIdempotent<'info> { + pub fn __anchor_private_gen_idl_accounts( + accounts: &mut std::collections::BTreeMap< + String, + anchor_lang::idl::types::IdlAccount, + >, + types: &mut std::collections::BTreeMap< + String, + anchor_lang::idl::types::IdlTypeDef, + >, + ) -> Vec { + <[_]>::into_vec( + ::alloc::boxed::box_new([ + anchor_lang::idl::types::IdlInstructionAccountItem::Single(anchor_lang::idl::types::IdlInstructionAccount { + name: "fee_payer".into(), + docs: ::alloc::vec::Vec::new(), + writable: true, + signer: true, + optional: false, + address: None, + pda: None, + relations: ::alloc::vec::Vec::new(), + }), + anchor_lang::idl::types::IdlInstructionAccountItem::Single(anchor_lang::idl::types::IdlInstructionAccount { + name: "rent_payer".into(), + docs: <[_]>::into_vec( + ::alloc::boxed::box_new([ + "UNCHECKED: Anyone can pay to init.".into(), + ]), + ), + writable: true, + signer: true, + optional: false, + address: None, + pda: None, + relations: ::alloc::vec::Vec::new(), + }), + anchor_lang::idl::types::IdlInstructionAccountItem::Single(anchor_lang::idl::types::IdlInstructionAccount { + name: "config".into(), + docs: <[_]>::into_vec( + ::alloc::boxed::box_new(["The global config account".into()]), + ), + writable: false, + signer: false, + optional: false, + address: None, + pda: None, + relations: ::alloc::vec::Vec::new(), + }), + anchor_lang::idl::types::IdlInstructionAccountItem::Single(anchor_lang::idl::types::IdlInstructionAccount { + name: "compressed_token_program".into(), + docs: <[_]>::into_vec( + ::alloc::boxed::box_new(["Compressed token program".into()]), + ), + writable: false, + signer: false, + optional: true, + address: None, + pda: None, + relations: ::alloc::vec::Vec::new(), + }), + anchor_lang::idl::types::IdlInstructionAccountItem::Single(anchor_lang::idl::types::IdlInstructionAccount { + name: "compressed_token_cpi_authority".into(), + docs: <[_]>::into_vec( + ::alloc::boxed::box_new([ + "CPI authority PDA of the compressed token program".into(), + ]), + ), + writable: false, + signer: false, + optional: true, + address: None, + pda: None, + relations: ::alloc::vec::Vec::new(), + }), + ]), + ) + } + } + /// Auto-generated decompress_accounts_idempotent instruction + pub fn decompress_accounts_idempotent<'info>( + ctx: Context<'_, '_, '_, 'info, DecompressAccountsIdempotent<'info>>, + proof: light_sdk::instruction::ValidityProof, + compressed_accounts: Vec, + system_accounts_offset: u8, + ) -> Result<()> { + let compression_config = light_sdk::compressible::CompressibleConfig::load_checked( + &ctx.accounts.config, + &crate::ID, + )?; + let address_space = compression_config.address_space[0]; + let (mut has_tokens, mut has_pdas) = (false, false); + for c in &compressed_accounts { + match c.data { + CompressedAccountVariant::CompressibleTokenAccountPacked(_) => { + has_tokens = true; + } + _ => has_pdas = true, + } + if has_tokens && has_pdas { + break; + } + } + let cpi_accounts = if has_tokens && has_pdas { + light_sdk_types::CpiAccountsSmall::new_with_config( + ctx.accounts.fee_payer.as_ref(), + &ctx.remaining_accounts[system_accounts_offset as usize..], + light_sdk_types::CpiAccountsConfig::new_with_cpi_context( + LIGHT_CPI_SIGNER, + ), + ) + } else { + light_sdk_types::CpiAccountsSmall::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 mut compressed_token_accounts = Vec::new(); + let mut compressed_pda_infos = Vec::new(); + for (i, compressed_data) in compressed_accounts.clone().into_iter().enumerate() { + let unpacked_data = compressed_data + .data + .unpack(cpi_accounts.post_system_accounts().unwrap())?; + match unpacked_data { + CompressedAccountVariant::UserRecord(data) => { + let (seeds_vec, _) = get_user_record_seeds(&data.owner); + let compressed_infos = light_sdk::compressible::prepare_account_for_decompression_idempotent::< + UserRecord, + >( + &crate::ID, + data, + light_sdk::compressible::into_compressed_meta_with_address( + &compressed_data.meta, + &solana_accounts[i], + address_space, + &crate::ID, + ), + &solana_accounts[i], + &ctx.accounts.rent_payer, + &cpi_accounts, + seeds_vec + .iter() + .map(|v| v.as_slice()) + .collect::>() + .as_slice(), + )?; + compressed_pda_infos.extend(compressed_infos); + } + CompressedAccountVariant::GameSession(data) => { + let (seeds_vec, _) = get_game_session_seeds(data.session_id); + let compressed_infos = light_sdk::compressible::prepare_account_for_decompression_idempotent::< + GameSession, + >( + &crate::ID, + data, + light_sdk::compressible::into_compressed_meta_with_address( + &compressed_data.meta, + &solana_accounts[i], + address_space, + &crate::ID, + ), + &solana_accounts[i], + &ctx.accounts.rent_payer, + &cpi_accounts, + seeds_vec + .iter() + .map(|v| v.as_slice()) + .collect::>() + .as_slice(), + )?; + compressed_pda_infos.extend(compressed_infos); + } + CompressedAccountVariant::PlaceholderRecord(data) => { + let (seeds_vec, _) = get_placeholder_record_seeds( + data.placeholder_id, + ); + let compressed_infos = light_sdk::compressible::prepare_account_for_decompression_idempotent::< + PlaceholderRecord, + >( + &crate::ID, + data, + light_sdk::compressible::into_compressed_meta_with_address( + &compressed_data.meta, + &solana_accounts[i], + address_space, + &crate::ID, + ), + &solana_accounts[i], + &ctx.accounts.rent_payer, + &cpi_accounts, + seeds_vec + .iter() + .map(|v| v.as_slice()) + .collect::>() + .as_slice(), + )?; + compressed_pda_infos.extend(compressed_infos); + } + CompressedAccountVariant::PackedUserRecord(_) => { + ::core::panicking::panic("internal error: entered unreachable code"); + } + CompressedAccountVariant::PackedGameSession(_) => { + ::core::panicking::panic("internal error: entered unreachable code"); + } + CompressedAccountVariant::PackedPlaceholderRecord(_) => { + ::core::panicking::panic("internal error: entered unreachable code"); + } + CompressedAccountVariant::CompressibleTokenAccountPacked(data) => { + compressed_token_accounts.push((data, compressed_data.meta)); + } + CompressedAccountVariant::CompressibleTokenData(_) => { + ::core::panicking::panic("internal error: entered unreachable code"); + } + } + } + let has_pdas = !compressed_pda_infos.is_empty(); + let has_tokens = !compressed_token_accounts.is_empty(); + if !has_pdas && !has_tokens { + ::solana_msg::sol_log("All accounts already initialized."); + return Ok(()); + } + let fee_payer = ctx.accounts.fee_payer.as_ref(); + let authority = cpi_accounts.authority().unwrap(); + let cpi_context = cpi_accounts.cpi_context().unwrap(); + if has_pdas && has_tokens { + let system_cpi_accounts = light_sdk_types::cpi_context_write::CpiContextWriteAccounts { + fee_payer, + authority, + cpi_context, + cpi_signer: LIGHT_CPI_SIGNER, + }; + let cpi_inputs = light_sdk::cpi::CpiInputs::new_first_cpi( + compressed_pda_infos, + Vec::new(), + ); + cpi_inputs.invoke_light_system_program_cpi_context(system_cpi_accounts)?; + } else if has_pdas { + let cpi_inputs = light_sdk::cpi::CpiInputs::new(proof, compressed_pda_infos); + cpi_inputs.invoke_light_system_program_small(cpi_accounts.clone())?; + } + Ok(()) + } +} +/// An Anchor generated module containing the program's set of +/// instructions, where each method handler in the `#[program]` mod is +/// associated with a struct defining the input arguments to the +/// method. These should be used directly, when one wants to serialize +/// Anchor instruction data, for example, when speciying +/// instructions on a client. +pub mod instruction { + use super::*; + /// Instruction. + pub struct InitializeCompressionConfig { + pub compression_delay: u32, + pub rent_recipient: Pubkey, + pub address_space: Vec, + } + impl borsh::ser::BorshSerialize for InitializeCompressionConfig + where + u32: borsh::ser::BorshSerialize, + Pubkey: borsh::ser::BorshSerialize, + Vec: borsh::ser::BorshSerialize, + { + fn serialize( + &self, + writer: &mut W, + ) -> ::core::result::Result<(), borsh::maybestd::io::Error> { + borsh::BorshSerialize::serialize(&self.compression_delay, writer)?; + borsh::BorshSerialize::serialize(&self.rent_recipient, writer)?; + borsh::BorshSerialize::serialize(&self.address_space, writer)?; + Ok(()) + } + } + impl anchor_lang::idl::build::IdlBuild for InitializeCompressionConfig { + fn create_type() -> Option { + Some(anchor_lang::idl::types::IdlTypeDef { + name: Self::get_full_path(), + docs: <[_]>::into_vec(::alloc::boxed::box_new(["Instruction.".into()])), + serialization: anchor_lang::idl::types::IdlSerialization::default(), + repr: None, + generics: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlTypeDefTy::Struct { + fields: Some( + anchor_lang::idl::types::IdlDefinedFields::Named( + <[_]>::into_vec( + ::alloc::boxed::box_new([ + anchor_lang::idl::types::IdlField { + name: "compression_delay".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::U32, + }, + anchor_lang::idl::types::IdlField { + name: "rent_recipient".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::Pubkey, + }, + anchor_lang::idl::types::IdlField { + name: "address_space".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::Vec( + Box::new(anchor_lang::idl::types::IdlType::Pubkey), + ), + }, + ]), + ), + ), + ), + }, + }) + } + fn insert_types( + types: &mut std::collections::BTreeMap< + String, + anchor_lang::idl::types::IdlTypeDef, + >, + ) {} + fn get_full_path() -> String { + ::alloc::__export::must_use({ + let res = ::alloc::fmt::format( + format_args!( + "{0}::{1}", + "anchor_compressible_derived::instruction", + "InitializeCompressionConfig", + ), + ); + res + }) + } + } + impl borsh::de::BorshDeserialize for InitializeCompressionConfig + where + u32: borsh::BorshDeserialize, + Pubkey: borsh::BorshDeserialize, + Vec: borsh::BorshDeserialize, + { + fn deserialize_reader( + reader: &mut R, + ) -> ::core::result::Result { + Ok(Self { + compression_delay: borsh::BorshDeserialize::deserialize_reader(reader)?, + rent_recipient: borsh::BorshDeserialize::deserialize_reader(reader)?, + address_space: borsh::BorshDeserialize::deserialize_reader(reader)?, + }) + } + } + impl anchor_lang::Discriminator for InitializeCompressionConfig { + const DISCRIMINATOR: &'static [u8] = &[133, 228, 12, 169, 56, 76, 222, 61]; + } + impl anchor_lang::InstructionData for InitializeCompressionConfig {} + impl anchor_lang::Owner for InitializeCompressionConfig { + fn owner() -> Pubkey { + ID + } + } + /// Instruction. + pub struct UpdateCompressionConfig { + pub new_compression_delay: Option, + pub new_rent_recipient: Option, + pub new_address_space: Option>, + pub new_update_authority: Option, + } + impl borsh::ser::BorshSerialize for UpdateCompressionConfig + where + Option: borsh::ser::BorshSerialize, + Option: borsh::ser::BorshSerialize, + Option>: borsh::ser::BorshSerialize, + Option: borsh::ser::BorshSerialize, + { + fn serialize( + &self, + writer: &mut W, + ) -> ::core::result::Result<(), borsh::maybestd::io::Error> { + borsh::BorshSerialize::serialize(&self.new_compression_delay, writer)?; + borsh::BorshSerialize::serialize(&self.new_rent_recipient, writer)?; + borsh::BorshSerialize::serialize(&self.new_address_space, writer)?; + borsh::BorshSerialize::serialize(&self.new_update_authority, writer)?; + Ok(()) + } + } + impl anchor_lang::idl::build::IdlBuild for UpdateCompressionConfig { + fn create_type() -> Option { + Some(anchor_lang::idl::types::IdlTypeDef { + name: Self::get_full_path(), + docs: <[_]>::into_vec(::alloc::boxed::box_new(["Instruction.".into()])), + serialization: anchor_lang::idl::types::IdlSerialization::default(), + repr: None, + generics: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlTypeDefTy::Struct { + fields: Some( + anchor_lang::idl::types::IdlDefinedFields::Named( + <[_]>::into_vec( + ::alloc::boxed::box_new([ + anchor_lang::idl::types::IdlField { + name: "new_compression_delay".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::Option( + Box::new(anchor_lang::idl::types::IdlType::U32), + ), + }, + anchor_lang::idl::types::IdlField { + name: "new_rent_recipient".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::Option( + Box::new(anchor_lang::idl::types::IdlType::Pubkey), + ), + }, + anchor_lang::idl::types::IdlField { + name: "new_address_space".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::Option( + Box::new( + anchor_lang::idl::types::IdlType::Vec( + Box::new(anchor_lang::idl::types::IdlType::Pubkey), + ), + ), + ), + }, + anchor_lang::idl::types::IdlField { + name: "new_update_authority".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::Option( + Box::new(anchor_lang::idl::types::IdlType::Pubkey), + ), + }, + ]), + ), + ), + ), + }, + }) + } + fn insert_types( + types: &mut std::collections::BTreeMap< + String, + anchor_lang::idl::types::IdlTypeDef, + >, + ) {} + fn get_full_path() -> String { + ::alloc::__export::must_use({ + let res = ::alloc::fmt::format( + format_args!( + "{0}::{1}", + "anchor_compressible_derived::instruction", + "UpdateCompressionConfig", + ), + ); + res + }) + } + } + impl borsh::de::BorshDeserialize for UpdateCompressionConfig + where + Option: borsh::BorshDeserialize, + Option: borsh::BorshDeserialize, + Option>: borsh::BorshDeserialize, + Option: borsh::BorshDeserialize, + { + fn deserialize_reader( + reader: &mut R, + ) -> ::core::result::Result { + Ok(Self { + new_compression_delay: borsh::BorshDeserialize::deserialize_reader( + reader, + )?, + new_rent_recipient: borsh::BorshDeserialize::deserialize_reader(reader)?, + new_address_space: borsh::BorshDeserialize::deserialize_reader(reader)?, + new_update_authority: borsh::BorshDeserialize::deserialize_reader( + reader, + )?, + }) + } + } + impl anchor_lang::Discriminator for UpdateCompressionConfig { + const DISCRIMINATOR: &'static [u8] = &[135, 215, 243, 81, 163, 146, 33, 70]; + } + impl anchor_lang::InstructionData for UpdateCompressionConfig {} + impl anchor_lang::Owner for UpdateCompressionConfig { + fn owner() -> Pubkey { + ID + } + } + /// Instruction. + pub struct CompressAccountsIdempotent { + pub proof: ValidityProof, + pub compressed_accounts: Vec, + pub signer_seeds: Vec>>, + pub system_accounts_offset: u8, + } + impl borsh::ser::BorshSerialize for CompressAccountsIdempotent + where + ValidityProof: borsh::ser::BorshSerialize, + Vec: borsh::ser::BorshSerialize, + Vec>>: borsh::ser::BorshSerialize, + u8: borsh::ser::BorshSerialize, + { + fn serialize( + &self, + writer: &mut W, + ) -> ::core::result::Result<(), borsh::maybestd::io::Error> { + borsh::BorshSerialize::serialize(&self.proof, writer)?; + borsh::BorshSerialize::serialize(&self.compressed_accounts, writer)?; + borsh::BorshSerialize::serialize(&self.signer_seeds, writer)?; + borsh::BorshSerialize::serialize(&self.system_accounts_offset, writer)?; + Ok(()) + } + } + impl anchor_lang::idl::build::IdlBuild for CompressAccountsIdempotent { + fn create_type() -> Option { + Some(anchor_lang::idl::types::IdlTypeDef { + name: Self::get_full_path(), + docs: <[_]>::into_vec(::alloc::boxed::box_new(["Instruction.".into()])), + serialization: anchor_lang::idl::types::IdlSerialization::default(), + repr: None, + generics: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlTypeDefTy::Struct { + fields: Some( + anchor_lang::idl::types::IdlDefinedFields::Named( + <[_]>::into_vec( + ::alloc::boxed::box_new([ + anchor_lang::idl::types::IdlField { + name: "proof".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::Defined { + name: ::get_full_path(), + generics: ::alloc::vec::Vec::new(), + }, + }, + anchor_lang::idl::types::IdlField { + name: "compressed_accounts".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::Vec( + Box::new(anchor_lang::idl::types::IdlType::Defined { + name: ::get_full_path(), + generics: ::alloc::vec::Vec::new(), + }), + ), + }, + anchor_lang::idl::types::IdlField { + name: "signer_seeds".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::Vec( + Box::new( + anchor_lang::idl::types::IdlType::Vec( + Box::new(anchor_lang::idl::types::IdlType::Bytes), + ), + ), + ), + }, + anchor_lang::idl::types::IdlField { + name: "system_accounts_offset".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::U8, + }, + ]), + ), + ), + ), + }, + }) + } + fn insert_types( + types: &mut std::collections::BTreeMap< + String, + anchor_lang::idl::types::IdlTypeDef, + >, + ) { + if let Some(ty) = ::create_type() { + types.insert(::get_full_path(), ty); + ::insert_types(types); + } + if let Some(ty) = ::create_type() { + types + .insert( + ::get_full_path(), + ty, + ); + ::insert_types(types); + } + } + fn get_full_path() -> String { + ::alloc::__export::must_use({ + let res = ::alloc::fmt::format( + format_args!( + "{0}::{1}", + "anchor_compressible_derived::instruction", + "CompressAccountsIdempotent", + ), + ); + res + }) + } + } + impl borsh::de::BorshDeserialize for CompressAccountsIdempotent + where + ValidityProof: borsh::BorshDeserialize, + Vec: borsh::BorshDeserialize, + Vec>>: borsh::BorshDeserialize, + u8: borsh::BorshDeserialize, + { + fn deserialize_reader( + reader: &mut R, + ) -> ::core::result::Result { + Ok(Self { + proof: borsh::BorshDeserialize::deserialize_reader(reader)?, + compressed_accounts: borsh::BorshDeserialize::deserialize_reader( + reader, + )?, + signer_seeds: borsh::BorshDeserialize::deserialize_reader(reader)?, + system_accounts_offset: borsh::BorshDeserialize::deserialize_reader( + reader, + )?, + }) + } + } + impl anchor_lang::Discriminator for CompressAccountsIdempotent { + const DISCRIMINATOR: &'static [u8] = &[70, 236, 171, 120, 164, 93, 113, 181]; + } + impl anchor_lang::InstructionData for CompressAccountsIdempotent {} + impl anchor_lang::Owner for CompressAccountsIdempotent { + fn owner() -> Pubkey { + ID + } + } + /// Instruction. + pub struct CreateRecord { + pub name: String, + pub proof: ValidityProof, + pub compressed_address: [u8; 32], + pub address_tree_info: PackedAddressTreeInfo, + pub output_state_tree_index: u8, + } + impl borsh::ser::BorshSerialize for CreateRecord + where + String: borsh::ser::BorshSerialize, + ValidityProof: borsh::ser::BorshSerialize, + [u8; 32]: borsh::ser::BorshSerialize, + PackedAddressTreeInfo: borsh::ser::BorshSerialize, + u8: borsh::ser::BorshSerialize, + { + fn serialize( + &self, + writer: &mut W, + ) -> ::core::result::Result<(), borsh::maybestd::io::Error> { + borsh::BorshSerialize::serialize(&self.name, writer)?; + borsh::BorshSerialize::serialize(&self.proof, writer)?; + borsh::BorshSerialize::serialize(&self.compressed_address, writer)?; + borsh::BorshSerialize::serialize(&self.address_tree_info, writer)?; + borsh::BorshSerialize::serialize(&self.output_state_tree_index, writer)?; + Ok(()) + } + } + impl anchor_lang::idl::build::IdlBuild for CreateRecord { + fn create_type() -> Option { + Some(anchor_lang::idl::types::IdlTypeDef { + name: Self::get_full_path(), + docs: <[_]>::into_vec(::alloc::boxed::box_new(["Instruction.".into()])), + serialization: anchor_lang::idl::types::IdlSerialization::default(), + repr: None, + generics: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlTypeDefTy::Struct { + fields: Some( + anchor_lang::idl::types::IdlDefinedFields::Named( + <[_]>::into_vec( + ::alloc::boxed::box_new([ + anchor_lang::idl::types::IdlField { + name: "name".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::String, + }, + anchor_lang::idl::types::IdlField { + name: "proof".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::Defined { + name: ::get_full_path(), + generics: ::alloc::vec::Vec::new(), + }, + }, + anchor_lang::idl::types::IdlField { + name: "compressed_address".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::Array( + Box::new(anchor_lang::idl::types::IdlType::U8), + anchor_lang::idl::types::IdlArrayLen::Value(32), + ), + }, + anchor_lang::idl::types::IdlField { + name: "address_tree_info".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::Defined { + name: ::get_full_path(), + generics: ::alloc::vec::Vec::new(), + }, + }, + anchor_lang::idl::types::IdlField { + name: "output_state_tree_index".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::U8, + }, + ]), + ), + ), + ), + }, + }) + } + fn insert_types( + types: &mut std::collections::BTreeMap< + String, + anchor_lang::idl::types::IdlTypeDef, + >, + ) { + if let Some(ty) = ::create_type() { + types.insert(::get_full_path(), ty); + ::insert_types(types); + } + if let Some(ty) = ::create_type() { + types.insert(::get_full_path(), ty); + ::insert_types(types); + } + } + fn get_full_path() -> String { + ::alloc::__export::must_use({ + let res = ::alloc::fmt::format( + format_args!( + "{0}::{1}", + "anchor_compressible_derived::instruction", + "CreateRecord", + ), + ); + res + }) + } + } + impl borsh::de::BorshDeserialize for CreateRecord + where + String: borsh::BorshDeserialize, + ValidityProof: borsh::BorshDeserialize, + [u8; 32]: borsh::BorshDeserialize, + PackedAddressTreeInfo: borsh::BorshDeserialize, + u8: borsh::BorshDeserialize, + { + fn deserialize_reader( + reader: &mut R, + ) -> ::core::result::Result { + Ok(Self { + name: borsh::BorshDeserialize::deserialize_reader(reader)?, + proof: borsh::BorshDeserialize::deserialize_reader(reader)?, + compressed_address: borsh::BorshDeserialize::deserialize_reader(reader)?, + address_tree_info: borsh::BorshDeserialize::deserialize_reader(reader)?, + output_state_tree_index: borsh::BorshDeserialize::deserialize_reader( + reader, + )?, + }) + } + } + impl anchor_lang::Discriminator for CreateRecord { + const DISCRIMINATOR: &'static [u8] = &[116, 124, 63, 58, 126, 204, 178, 10]; + } + impl anchor_lang::InstructionData for CreateRecord {} + impl anchor_lang::Owner for CreateRecord { + fn owner() -> Pubkey { + ID + } + } + /// Instruction. + pub struct CreateGameSession { + pub session_id: u64, + pub game_type: String, + pub proof: ValidityProof, + pub compressed_address: [u8; 32], + pub address_tree_info: PackedAddressTreeInfo, + pub output_state_tree_index: u8, + } + impl borsh::ser::BorshSerialize for CreateGameSession + where + u64: borsh::ser::BorshSerialize, + String: borsh::ser::BorshSerialize, + ValidityProof: borsh::ser::BorshSerialize, + [u8; 32]: borsh::ser::BorshSerialize, + PackedAddressTreeInfo: borsh::ser::BorshSerialize, + u8: borsh::ser::BorshSerialize, + { + fn serialize( + &self, + writer: &mut W, + ) -> ::core::result::Result<(), borsh::maybestd::io::Error> { + borsh::BorshSerialize::serialize(&self.session_id, writer)?; + borsh::BorshSerialize::serialize(&self.game_type, writer)?; + borsh::BorshSerialize::serialize(&self.proof, writer)?; + borsh::BorshSerialize::serialize(&self.compressed_address, writer)?; + borsh::BorshSerialize::serialize(&self.address_tree_info, writer)?; + borsh::BorshSerialize::serialize(&self.output_state_tree_index, writer)?; + Ok(()) + } + } + impl anchor_lang::idl::build::IdlBuild for CreateGameSession { + fn create_type() -> Option { + Some(anchor_lang::idl::types::IdlTypeDef { + name: Self::get_full_path(), + docs: <[_]>::into_vec(::alloc::boxed::box_new(["Instruction.".into()])), + serialization: anchor_lang::idl::types::IdlSerialization::default(), + repr: None, + generics: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlTypeDefTy::Struct { + fields: Some( + anchor_lang::idl::types::IdlDefinedFields::Named( + <[_]>::into_vec( + ::alloc::boxed::box_new([ + anchor_lang::idl::types::IdlField { + name: "session_id".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::U64, + }, + anchor_lang::idl::types::IdlField { + name: "game_type".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::String, + }, + anchor_lang::idl::types::IdlField { + name: "proof".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::Defined { + name: ::get_full_path(), + generics: ::alloc::vec::Vec::new(), + }, + }, + anchor_lang::idl::types::IdlField { + name: "compressed_address".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::Array( + Box::new(anchor_lang::idl::types::IdlType::U8), + anchor_lang::idl::types::IdlArrayLen::Value(32), + ), + }, + anchor_lang::idl::types::IdlField { + name: "address_tree_info".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::Defined { + name: ::get_full_path(), + generics: ::alloc::vec::Vec::new(), + }, + }, + anchor_lang::idl::types::IdlField { + name: "output_state_tree_index".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::U8, + }, + ]), + ), + ), + ), + }, + }) + } + fn insert_types( + types: &mut std::collections::BTreeMap< + String, + anchor_lang::idl::types::IdlTypeDef, + >, + ) { + if let Some(ty) = ::create_type() { + types.insert(::get_full_path(), ty); + ::insert_types(types); + } + if let Some(ty) = ::create_type() { + types.insert(::get_full_path(), ty); + ::insert_types(types); + } + } + fn get_full_path() -> String { + ::alloc::__export::must_use({ + let res = ::alloc::fmt::format( + format_args!( + "{0}::{1}", + "anchor_compressible_derived::instruction", + "CreateGameSession", + ), + ); + res + }) + } + } + impl borsh::de::BorshDeserialize for CreateGameSession + where + u64: borsh::BorshDeserialize, + String: borsh::BorshDeserialize, + ValidityProof: borsh::BorshDeserialize, + [u8; 32]: borsh::BorshDeserialize, + PackedAddressTreeInfo: borsh::BorshDeserialize, + u8: borsh::BorshDeserialize, + { + fn deserialize_reader( + reader: &mut R, + ) -> ::core::result::Result { + Ok(Self { + session_id: borsh::BorshDeserialize::deserialize_reader(reader)?, + game_type: borsh::BorshDeserialize::deserialize_reader(reader)?, + proof: borsh::BorshDeserialize::deserialize_reader(reader)?, + compressed_address: borsh::BorshDeserialize::deserialize_reader(reader)?, + address_tree_info: borsh::BorshDeserialize::deserialize_reader(reader)?, + output_state_tree_index: borsh::BorshDeserialize::deserialize_reader( + reader, + )?, + }) + } + } + impl anchor_lang::Discriminator for CreateGameSession { + const DISCRIMINATOR: &'static [u8] = &[130, 34, 251, 80, 77, 159, 113, 224]; + } + impl anchor_lang::InstructionData for CreateGameSession {} + impl anchor_lang::Owner for CreateGameSession { + fn owner() -> Pubkey { + ID + } + } + /// Instruction. + pub struct CreateUserRecordAndGameSession { + pub account_data: AccountCreationData, + pub compression_params: CompressionParams, + } + impl borsh::ser::BorshSerialize for CreateUserRecordAndGameSession + where + AccountCreationData: borsh::ser::BorshSerialize, + CompressionParams: borsh::ser::BorshSerialize, + { + fn serialize( + &self, + writer: &mut W, + ) -> ::core::result::Result<(), borsh::maybestd::io::Error> { + borsh::BorshSerialize::serialize(&self.account_data, writer)?; + borsh::BorshSerialize::serialize(&self.compression_params, writer)?; + Ok(()) + } + } + impl anchor_lang::idl::build::IdlBuild for CreateUserRecordAndGameSession { + fn create_type() -> Option { + Some(anchor_lang::idl::types::IdlTypeDef { + name: Self::get_full_path(), + docs: <[_]>::into_vec(::alloc::boxed::box_new(["Instruction.".into()])), + serialization: anchor_lang::idl::types::IdlSerialization::default(), + repr: None, + generics: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlTypeDefTy::Struct { + fields: Some( + anchor_lang::idl::types::IdlDefinedFields::Named( + <[_]>::into_vec( + ::alloc::boxed::box_new([ + anchor_lang::idl::types::IdlField { + name: "account_data".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::Defined { + name: ::get_full_path(), + generics: ::alloc::vec::Vec::new(), + }, + }, + anchor_lang::idl::types::IdlField { + name: "compression_params".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::Defined { + name: ::get_full_path(), + generics: ::alloc::vec::Vec::new(), + }, + }, + ]), + ), + ), + ), + }, + }) + } + fn insert_types( + types: &mut std::collections::BTreeMap< + String, + anchor_lang::idl::types::IdlTypeDef, + >, + ) { + if let Some(ty) = ::create_type() { + types.insert(::get_full_path(), ty); + ::insert_types(types); + } + if let Some(ty) = ::create_type() { + types.insert(::get_full_path(), ty); + ::insert_types(types); + } + } + fn get_full_path() -> String { + ::alloc::__export::must_use({ + let res = ::alloc::fmt::format( + format_args!( + "{0}::{1}", + "anchor_compressible_derived::instruction", + "CreateUserRecordAndGameSession", + ), + ); + res + }) + } + } + impl borsh::de::BorshDeserialize for CreateUserRecordAndGameSession + where + AccountCreationData: borsh::BorshDeserialize, + CompressionParams: borsh::BorshDeserialize, + { + fn deserialize_reader( + reader: &mut R, + ) -> ::core::result::Result { + Ok(Self { + account_data: borsh::BorshDeserialize::deserialize_reader(reader)?, + compression_params: borsh::BorshDeserialize::deserialize_reader(reader)?, + }) + } + } + impl anchor_lang::Discriminator for CreateUserRecordAndGameSession { + const DISCRIMINATOR: &'static [u8] = &[130, 196, 129, 145, 131, 124, 218, 98]; + } + impl anchor_lang::InstructionData for CreateUserRecordAndGameSession {} + impl anchor_lang::Owner for CreateUserRecordAndGameSession { + fn owner() -> Pubkey { + ID + } + } + /// Instruction. + pub struct CreatePlaceholderRecord { + pub placeholder_id: u64, + pub name: String, + pub proof: ValidityProof, + pub compressed_address: [u8; 32], + pub address_tree_info: PackedAddressTreeInfo, + pub output_state_tree_index: u8, + } + impl borsh::ser::BorshSerialize for CreatePlaceholderRecord + where + u64: borsh::ser::BorshSerialize, + String: borsh::ser::BorshSerialize, + ValidityProof: borsh::ser::BorshSerialize, + [u8; 32]: borsh::ser::BorshSerialize, + PackedAddressTreeInfo: borsh::ser::BorshSerialize, + u8: borsh::ser::BorshSerialize, + { + fn serialize( + &self, + writer: &mut W, + ) -> ::core::result::Result<(), borsh::maybestd::io::Error> { + borsh::BorshSerialize::serialize(&self.placeholder_id, writer)?; + borsh::BorshSerialize::serialize(&self.name, writer)?; + borsh::BorshSerialize::serialize(&self.proof, writer)?; + borsh::BorshSerialize::serialize(&self.compressed_address, writer)?; + borsh::BorshSerialize::serialize(&self.address_tree_info, writer)?; + borsh::BorshSerialize::serialize(&self.output_state_tree_index, writer)?; + Ok(()) + } + } + impl anchor_lang::idl::build::IdlBuild for CreatePlaceholderRecord { + fn create_type() -> Option { + Some(anchor_lang::idl::types::IdlTypeDef { + name: Self::get_full_path(), + docs: <[_]>::into_vec(::alloc::boxed::box_new(["Instruction.".into()])), + serialization: anchor_lang::idl::types::IdlSerialization::default(), + repr: None, + generics: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlTypeDefTy::Struct { + fields: Some( + anchor_lang::idl::types::IdlDefinedFields::Named( + <[_]>::into_vec( + ::alloc::boxed::box_new([ + anchor_lang::idl::types::IdlField { + name: "placeholder_id".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::U64, + }, + anchor_lang::idl::types::IdlField { + name: "name".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::String, + }, + anchor_lang::idl::types::IdlField { + name: "proof".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::Defined { + name: ::get_full_path(), + generics: ::alloc::vec::Vec::new(), + }, + }, + anchor_lang::idl::types::IdlField { + name: "compressed_address".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::Array( + Box::new(anchor_lang::idl::types::IdlType::U8), + anchor_lang::idl::types::IdlArrayLen::Value(32), + ), + }, + anchor_lang::idl::types::IdlField { + name: "address_tree_info".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::Defined { + name: ::get_full_path(), + generics: ::alloc::vec::Vec::new(), + }, + }, + anchor_lang::idl::types::IdlField { + name: "output_state_tree_index".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::U8, + }, + ]), + ), + ), + ), + }, + }) + } + fn insert_types( + types: &mut std::collections::BTreeMap< + String, + anchor_lang::idl::types::IdlTypeDef, + >, + ) { + if let Some(ty) = ::create_type() { + types.insert(::get_full_path(), ty); + ::insert_types(types); + } + if let Some(ty) = ::create_type() { + types.insert(::get_full_path(), ty); + ::insert_types(types); + } + } + fn get_full_path() -> String { + ::alloc::__export::must_use({ + let res = ::alloc::fmt::format( + format_args!( + "{0}::{1}", + "anchor_compressible_derived::instruction", + "CreatePlaceholderRecord", + ), + ); + res + }) + } + } + impl borsh::de::BorshDeserialize for CreatePlaceholderRecord + where + u64: borsh::BorshDeserialize, + String: borsh::BorshDeserialize, + ValidityProof: borsh::BorshDeserialize, + [u8; 32]: borsh::BorshDeserialize, + PackedAddressTreeInfo: borsh::BorshDeserialize, + u8: borsh::BorshDeserialize, + { + fn deserialize_reader( + reader: &mut R, + ) -> ::core::result::Result { + Ok(Self { + placeholder_id: borsh::BorshDeserialize::deserialize_reader(reader)?, + name: borsh::BorshDeserialize::deserialize_reader(reader)?, + proof: borsh::BorshDeserialize::deserialize_reader(reader)?, + compressed_address: borsh::BorshDeserialize::deserialize_reader(reader)?, + address_tree_info: borsh::BorshDeserialize::deserialize_reader(reader)?, + output_state_tree_index: borsh::BorshDeserialize::deserialize_reader( + reader, + )?, + }) + } + } + impl anchor_lang::Discriminator for CreatePlaceholderRecord { + const DISCRIMINATOR: &'static [u8] = &[66, 70, 108, 128, 12, 105, 73, 250]; + } + impl anchor_lang::InstructionData for CreatePlaceholderRecord {} + impl anchor_lang::Owner for CreatePlaceholderRecord { + fn owner() -> Pubkey { + ID + } + } + /// Instruction. + pub struct UpdateRecord { + pub name: String, + pub score: u64, + } + impl borsh::ser::BorshSerialize for UpdateRecord + where + String: borsh::ser::BorshSerialize, + u64: borsh::ser::BorshSerialize, + { + fn serialize( + &self, + writer: &mut W, + ) -> ::core::result::Result<(), borsh::maybestd::io::Error> { + borsh::BorshSerialize::serialize(&self.name, writer)?; + borsh::BorshSerialize::serialize(&self.score, writer)?; + Ok(()) + } + } + impl anchor_lang::idl::build::IdlBuild for UpdateRecord { + fn create_type() -> Option { + Some(anchor_lang::idl::types::IdlTypeDef { + name: Self::get_full_path(), + docs: <[_]>::into_vec(::alloc::boxed::box_new(["Instruction.".into()])), + serialization: anchor_lang::idl::types::IdlSerialization::default(), + repr: None, + generics: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlTypeDefTy::Struct { + fields: Some( + anchor_lang::idl::types::IdlDefinedFields::Named( + <[_]>::into_vec( + ::alloc::boxed::box_new([ + anchor_lang::idl::types::IdlField { + name: "name".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::String, + }, + anchor_lang::idl::types::IdlField { + name: "score".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::U64, + }, + ]), + ), + ), + ), + }, + }) + } + fn insert_types( + types: &mut std::collections::BTreeMap< + String, + anchor_lang::idl::types::IdlTypeDef, + >, + ) {} + fn get_full_path() -> String { + ::alloc::__export::must_use({ + let res = ::alloc::fmt::format( + format_args!( + "{0}::{1}", + "anchor_compressible_derived::instruction", + "UpdateRecord", + ), + ); + res + }) + } + } + impl borsh::de::BorshDeserialize for UpdateRecord + where + String: borsh::BorshDeserialize, + u64: borsh::BorshDeserialize, + { + fn deserialize_reader( + reader: &mut R, + ) -> ::core::result::Result { + Ok(Self { + name: borsh::BorshDeserialize::deserialize_reader(reader)?, + score: borsh::BorshDeserialize::deserialize_reader(reader)?, + }) + } + } + impl anchor_lang::Discriminator for UpdateRecord { + const DISCRIMINATOR: &'static [u8] = &[54, 194, 108, 162, 199, 12, 5, 60]; + } + impl anchor_lang::InstructionData for UpdateRecord {} + impl anchor_lang::Owner for UpdateRecord { + fn owner() -> Pubkey { + ID + } + } + /// Instruction. + pub struct UpdateGameSession { + pub _session_id: u64, + pub new_score: u64, + } + impl borsh::ser::BorshSerialize for UpdateGameSession + where + u64: borsh::ser::BorshSerialize, + u64: borsh::ser::BorshSerialize, + { + fn serialize( + &self, + writer: &mut W, + ) -> ::core::result::Result<(), borsh::maybestd::io::Error> { + borsh::BorshSerialize::serialize(&self._session_id, writer)?; + borsh::BorshSerialize::serialize(&self.new_score, writer)?; + Ok(()) + } + } + impl anchor_lang::idl::build::IdlBuild for UpdateGameSession { + fn create_type() -> Option { + Some(anchor_lang::idl::types::IdlTypeDef { + name: Self::get_full_path(), + docs: <[_]>::into_vec(::alloc::boxed::box_new(["Instruction.".into()])), + serialization: anchor_lang::idl::types::IdlSerialization::default(), + repr: None, + generics: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlTypeDefTy::Struct { + fields: Some( + anchor_lang::idl::types::IdlDefinedFields::Named( + <[_]>::into_vec( + ::alloc::boxed::box_new([ + anchor_lang::idl::types::IdlField { + name: "_session_id".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::U64, + }, + anchor_lang::idl::types::IdlField { + name: "new_score".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::U64, + }, + ]), + ), + ), + ), + }, + }) + } + fn insert_types( + types: &mut std::collections::BTreeMap< + String, + anchor_lang::idl::types::IdlTypeDef, + >, + ) {} + fn get_full_path() -> String { + ::alloc::__export::must_use({ + let res = ::alloc::fmt::format( + format_args!( + "{0}::{1}", + "anchor_compressible_derived::instruction", + "UpdateGameSession", + ), + ); + res + }) + } + } + impl borsh::de::BorshDeserialize for UpdateGameSession + where + u64: borsh::BorshDeserialize, + u64: borsh::BorshDeserialize, + { + fn deserialize_reader( + reader: &mut R, + ) -> ::core::result::Result { + Ok(Self { + _session_id: borsh::BorshDeserialize::deserialize_reader(reader)?, + new_score: borsh::BorshDeserialize::deserialize_reader(reader)?, + }) + } + } + impl anchor_lang::Discriminator for UpdateGameSession { + const DISCRIMINATOR: &'static [u8] = &[154, 197, 225, 250, 40, 214, 248, 224]; + } + impl anchor_lang::InstructionData for UpdateGameSession {} + impl anchor_lang::Owner for UpdateGameSession { + fn owner() -> Pubkey { + ID + } + } + /// Instruction. + pub struct DecompressAccountsIdempotent { + pub proof: light_sdk::instruction::ValidityProof, + pub compressed_accounts: Vec, + pub system_accounts_offset: u8, + } + impl borsh::ser::BorshSerialize for DecompressAccountsIdempotent + where + light_sdk::instruction::ValidityProof: borsh::ser::BorshSerialize, + Vec: borsh::ser::BorshSerialize, + u8: borsh::ser::BorshSerialize, + { + fn serialize( + &self, + writer: &mut W, + ) -> ::core::result::Result<(), borsh::maybestd::io::Error> { + borsh::BorshSerialize::serialize(&self.proof, writer)?; + borsh::BorshSerialize::serialize(&self.compressed_accounts, writer)?; + borsh::BorshSerialize::serialize(&self.system_accounts_offset, writer)?; + Ok(()) + } + } + impl anchor_lang::idl::build::IdlBuild for DecompressAccountsIdempotent { + fn create_type() -> Option { + Some(anchor_lang::idl::types::IdlTypeDef { + name: Self::get_full_path(), + docs: <[_]>::into_vec(::alloc::boxed::box_new(["Instruction.".into()])), + serialization: anchor_lang::idl::types::IdlSerialization::default(), + repr: None, + generics: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlTypeDefTy::Struct { + fields: Some( + anchor_lang::idl::types::IdlDefinedFields::Named( + <[_]>::into_vec( + ::alloc::boxed::box_new([ + anchor_lang::idl::types::IdlField { + name: "proof".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::Defined { + name: ::get_full_path(), + generics: ::alloc::vec::Vec::new(), + }, + }, + anchor_lang::idl::types::IdlField { + name: "compressed_accounts".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::Vec( + Box::new(anchor_lang::idl::types::IdlType::Defined { + name: ::get_full_path(), + generics: ::alloc::vec::Vec::new(), + }), + ), + }, + anchor_lang::idl::types::IdlField { + name: "system_accounts_offset".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::U8, + }, + ]), + ), + ), + ), + }, + }) + } + fn insert_types( + types: &mut std::collections::BTreeMap< + String, + anchor_lang::idl::types::IdlTypeDef, + >, + ) { + if let Some(ty) = ::create_type() { + types + .insert( + ::get_full_path(), + ty, + ); + ::insert_types(types); + } + if let Some(ty) = ::create_type() { + types.insert(::get_full_path(), ty); + ::insert_types(types); + } + } + fn get_full_path() -> String { + ::alloc::__export::must_use({ + let res = ::alloc::fmt::format( + format_args!( + "{0}::{1}", + "anchor_compressible_derived::instruction", + "DecompressAccountsIdempotent", + ), + ); + res + }) + } + } + impl borsh::de::BorshDeserialize for DecompressAccountsIdempotent + where + light_sdk::instruction::ValidityProof: borsh::BorshDeserialize, + Vec: borsh::BorshDeserialize, + u8: borsh::BorshDeserialize, + { + fn deserialize_reader( + reader: &mut R, + ) -> ::core::result::Result { + Ok(Self { + proof: borsh::BorshDeserialize::deserialize_reader(reader)?, + compressed_accounts: borsh::BorshDeserialize::deserialize_reader( + reader, + )?, + system_accounts_offset: borsh::BorshDeserialize::deserialize_reader( + reader, + )?, + }) + } + } + impl anchor_lang::Discriminator for DecompressAccountsIdempotent { + const DISCRIMINATOR: &'static [u8] = &[114, 67, 61, 123, 234, 31, 1, 112]; + } + impl anchor_lang::InstructionData for DecompressAccountsIdempotent {} + impl anchor_lang::Owner for DecompressAccountsIdempotent { + fn owner() -> Pubkey { + ID + } + } +} +/// An Anchor generated module, providing a set of structs +/// mirroring the structs deriving `Accounts`, where each field is +/// a `Pubkey`. This is useful for specifying accounts for a client. +pub mod accounts { + pub use crate::__client_accounts_create_user_record_and_game_session::*; + pub use crate::__client_accounts_update_game_session::*; + pub use crate::__client_accounts_create_placeholder_record::*; + pub use crate::__client_accounts_compress_accounts_idempotent::*; + pub use crate::__client_accounts_update_record::*; + pub use crate::__client_accounts_update_compression_config::*; + pub use crate::__client_accounts_create_record::*; + pub use crate::__client_accounts_decompress_accounts_idempotent::*; + pub use crate::__client_accounts_create_game_session::*; + pub use crate::__client_accounts_initialize_compression_config::*; +} +pub struct CreateRecord<'info> { + #[account(mut)] + pub user: Signer<'info>, + #[account( + init, + payer = user, + space = 8+32+4+32+8+10, + seeds = [b"user_record", + user.key().as_ref()], + bump, + )] + pub user_record: Account<'info, UserRecord>, + /// Needs to be here for the init anchor macro to work. + pub system_program: Program<'info, System>, + /// The global config account + /// CHECK: Config is validated by the SDK's load_checked method + pub config: AccountInfo<'info>, + /// Rent recipient - must match config + /// CHECK: Rent recipient is validated against the config + #[account(mut)] + pub rent_recipient: AccountInfo<'info>, +} +#[automatically_derived] +impl<'info> anchor_lang::Accounts<'info, CreateRecordBumps> for CreateRecord<'info> +where + 'info: 'info, +{ + #[inline(never)] + fn try_accounts( + __program_id: &anchor_lang::solana_program::pubkey::Pubkey, + __accounts: &mut &'info [anchor_lang::solana_program::account_info::AccountInfo< + 'info, + >], + __ix_data: &[u8], + __bumps: &mut CreateRecordBumps, + __reallocs: &mut std::collections::BTreeSet< + anchor_lang::solana_program::pubkey::Pubkey, + >, + ) -> anchor_lang::Result { + let user: Signer = anchor_lang::Accounts::try_accounts( + __program_id, + __accounts, + __ix_data, + __bumps, + __reallocs, + ) + .map_err(|e| e.with_account_name("user"))?; + if __accounts.is_empty() { + return Err(anchor_lang::error::ErrorCode::AccountNotEnoughKeys.into()); + } + let user_record = &__accounts[0]; + *__accounts = &__accounts[1..]; + let system_program: anchor_lang::accounts::program::Program = anchor_lang::Accounts::try_accounts( + __program_id, + __accounts, + __ix_data, + __bumps, + __reallocs, + ) + .map_err(|e| e.with_account_name("system_program"))?; + let config: AccountInfo = anchor_lang::Accounts::try_accounts( + __program_id, + __accounts, + __ix_data, + __bumps, + __reallocs, + ) + .map_err(|e| e.with_account_name("config"))?; + let rent_recipient: AccountInfo = anchor_lang::Accounts::try_accounts( + __program_id, + __accounts, + __ix_data, + __bumps, + __reallocs, + ) + .map_err(|e| e.with_account_name("rent_recipient"))?; + let __anchor_rent = Rent::get()?; + let (__pda_address, __bump) = Pubkey::find_program_address( + &[b"user_record", user.key().as_ref()], + __program_id, + ); + __bumps.user_record = __bump; + if user_record.key() != __pda_address { + return Err( + anchor_lang::error::Error::from( + anchor_lang::error::ErrorCode::ConstraintSeeds, + ) + .with_account_name("user_record") + .with_pubkeys((user_record.key(), __pda_address)), + ); + } + let user_record = ({ + #[inline(never)] + || { + let actual_field = AsRef::::as_ref(&user_record); + let actual_owner = actual_field.owner; + let space = 8 + 32 + 4 + 32 + 8 + 10; + let pa: anchor_lang::accounts::account::Account = if !false + || actual_owner == &anchor_lang::solana_program::system_program::ID + { + let __current_lamports = user_record.lamports(); + if __current_lamports == 0 { + let space = space; + let lamports = __anchor_rent.minimum_balance(space); + let cpi_accounts = anchor_lang::system_program::CreateAccount { + from: user.to_account_info(), + to: user_record.to_account_info(), + }; + let cpi_context = anchor_lang::context::CpiContext::new( + system_program.to_account_info(), + cpi_accounts, + ); + anchor_lang::system_program::create_account( + cpi_context + .with_signer( + &[&[b"user_record", user.key().as_ref(), &[__bump][..]][..]], + ), + lamports, + space as u64, + __program_id, + )?; + } else { + if user.key() == user_record.key() { + return Err( + anchor_lang::error::Error::from(anchor_lang::error::AnchorError { + error_name: anchor_lang::error::ErrorCode::TryingToInitPayerAsProgramAccount + .name(), + error_code_number: anchor_lang::error::ErrorCode::TryingToInitPayerAsProgramAccount + .into(), + error_msg: anchor_lang::error::ErrorCode::TryingToInitPayerAsProgramAccount + .to_string(), + error_origin: Some( + anchor_lang::error::ErrorOrigin::Source(anchor_lang::error::Source { + filename: "sdk-tests/anchor-compressible-derived/src/lib.rs", + line: 938u32, + }), + ), + compared_values: None, + }) + .with_pubkeys((user.key(), user_record.key())), + ); + } + let required_lamports = __anchor_rent + .minimum_balance(space) + .max(1) + .saturating_sub(__current_lamports); + if required_lamports > 0 { + let cpi_accounts = anchor_lang::system_program::Transfer { + from: user.to_account_info(), + to: user_record.to_account_info(), + }; + let cpi_context = anchor_lang::context::CpiContext::new( + system_program.to_account_info(), + cpi_accounts, + ); + anchor_lang::system_program::transfer( + cpi_context, + required_lamports, + )?; + } + let cpi_accounts = anchor_lang::system_program::Allocate { + account_to_allocate: user_record.to_account_info(), + }; + let cpi_context = anchor_lang::context::CpiContext::new( + system_program.to_account_info(), + cpi_accounts, + ); + anchor_lang::system_program::allocate( + cpi_context + .with_signer( + &[&[b"user_record", user.key().as_ref(), &[__bump][..]][..]], + ), + space as u64, + )?; + let cpi_accounts = anchor_lang::system_program::Assign { + account_to_assign: user_record.to_account_info(), + }; + let cpi_context = anchor_lang::context::CpiContext::new( + system_program.to_account_info(), + cpi_accounts, + ); + anchor_lang::system_program::assign( + cpi_context + .with_signer( + &[&[b"user_record", user.key().as_ref(), &[__bump][..]][..]], + ), + __program_id, + )?; + } + match anchor_lang::accounts::account::Account::try_from_unchecked( + &user_record, + ) { + Ok(val) => val, + Err(e) => return Err(e.with_account_name("user_record")), + } + } else { + match anchor_lang::accounts::account::Account::try_from( + &user_record, + ) { + Ok(val) => val, + Err(e) => return Err(e.with_account_name("user_record")), + } + }; + if false { + if space != actual_field.data_len() { + return Err( + anchor_lang::error::Error::from( + anchor_lang::error::ErrorCode::ConstraintSpace, + ) + .with_account_name("user_record") + .with_values((space, actual_field.data_len())), + ); + } + if actual_owner != __program_id { + return Err( + anchor_lang::error::Error::from( + anchor_lang::error::ErrorCode::ConstraintOwner, + ) + .with_account_name("user_record") + .with_pubkeys((*actual_owner, *__program_id)), + ); + } + { + let required_lamports = __anchor_rent.minimum_balance(space); + if pa.to_account_info().lamports() < required_lamports { + return Err( + anchor_lang::error::Error::from( + anchor_lang::error::ErrorCode::ConstraintRentExempt, + ) + .with_account_name("user_record"), + ); + } + } + } + Ok(pa) + } + })()?; + if !AsRef::::as_ref(&user_record).is_writable { + return Err( + anchor_lang::error::Error::from( + anchor_lang::error::ErrorCode::ConstraintMut, + ) + .with_account_name("user_record"), + ); + } + if !__anchor_rent + .is_exempt( + user_record.to_account_info().lamports(), + user_record.to_account_info().try_data_len()?, + ) + { + return Err( + anchor_lang::error::Error::from( + anchor_lang::error::ErrorCode::ConstraintRentExempt, + ) + .with_account_name("user_record"), + ); + } + if !AsRef::::as_ref(&user).is_writable { + return Err( + anchor_lang::error::Error::from( + anchor_lang::error::ErrorCode::ConstraintMut, + ) + .with_account_name("user"), + ); + } + if !&rent_recipient.is_writable { + return Err( + anchor_lang::error::Error::from( + anchor_lang::error::ErrorCode::ConstraintMut, + ) + .with_account_name("rent_recipient"), + ); + } + Ok(CreateRecord { + user, + user_record, + system_program, + config, + rent_recipient, + }) + } +} +#[automatically_derived] +impl<'info> anchor_lang::ToAccountInfos<'info> for CreateRecord<'info> +where + 'info: 'info, +{ + fn to_account_infos( + &self, + ) -> Vec> { + let mut account_infos = ::alloc::vec::Vec::new(); + account_infos.extend(self.user.to_account_infos()); + account_infos.extend(self.user_record.to_account_infos()); + account_infos.extend(self.system_program.to_account_infos()); + account_infos.extend(self.config.to_account_infos()); + account_infos.extend(self.rent_recipient.to_account_infos()); + account_infos + } +} +#[automatically_derived] +impl<'info> anchor_lang::ToAccountMetas for CreateRecord<'info> { + fn to_account_metas( + &self, + is_signer: Option, + ) -> Vec { + let mut account_metas = ::alloc::vec::Vec::new(); + account_metas.extend(self.user.to_account_metas(None)); + account_metas.extend(self.user_record.to_account_metas(None)); + account_metas.extend(self.system_program.to_account_metas(None)); + account_metas.extend(self.config.to_account_metas(None)); + account_metas.extend(self.rent_recipient.to_account_metas(None)); + account_metas + } +} +#[automatically_derived] +impl<'info> anchor_lang::AccountsExit<'info> for CreateRecord<'info> +where + 'info: 'info, +{ + fn exit( + &self, + program_id: &anchor_lang::solana_program::pubkey::Pubkey, + ) -> anchor_lang::Result<()> { + anchor_lang::AccountsExit::exit(&self.user, program_id) + .map_err(|e| e.with_account_name("user"))?; + anchor_lang::AccountsExit::exit(&self.user_record, program_id) + .map_err(|e| e.with_account_name("user_record"))?; + anchor_lang::AccountsExit::exit(&self.rent_recipient, program_id) + .map_err(|e| e.with_account_name("rent_recipient"))?; + Ok(()) + } +} +pub struct CreateRecordBumps { + pub user_record: u8, +} +#[automatically_derived] +impl ::core::fmt::Debug for CreateRecordBumps { + #[inline] + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + ::core::fmt::Formatter::debug_struct_field1_finish( + f, + "CreateRecordBumps", + "user_record", + &&self.user_record, + ) + } +} +impl Default for CreateRecordBumps { + fn default() -> Self { + CreateRecordBumps { + user_record: u8::MAX, + } + } +} +impl<'info> anchor_lang::Bumps for CreateRecord<'info> +where + 'info: 'info, +{ + type Bumps = CreateRecordBumps; +} +/// An internal, Anchor generated module. This is used (as an +/// implementation detail), to generate a struct for a given +/// `#[derive(Accounts)]` implementation, where each field is a Pubkey, +/// instead of an `AccountInfo`. This is useful for clients that want +/// to generate a list of accounts, without explicitly knowing the +/// order all the fields should be in. +/// +/// To access the struct in this module, one should use the sibling +/// `accounts` module (also generated), which re-exports this. +pub(crate) mod __client_accounts_create_record { + use super::*; + use anchor_lang::prelude::borsh; + /// Generated client accounts for [`CreateRecord`]. + pub struct CreateRecord { + pub user: Pubkey, + pub user_record: Pubkey, + ///Needs to be here for the init anchor macro to work. + pub system_program: Pubkey, + ///The global config account + pub config: Pubkey, + ///Rent recipient - must match config + pub rent_recipient: Pubkey, + } + impl borsh::ser::BorshSerialize for CreateRecord + where + Pubkey: borsh::ser::BorshSerialize, + Pubkey: borsh::ser::BorshSerialize, + Pubkey: borsh::ser::BorshSerialize, + Pubkey: borsh::ser::BorshSerialize, + Pubkey: borsh::ser::BorshSerialize, + { + fn serialize( + &self, + writer: &mut W, + ) -> ::core::result::Result<(), borsh::maybestd::io::Error> { + borsh::BorshSerialize::serialize(&self.user, writer)?; + borsh::BorshSerialize::serialize(&self.user_record, writer)?; + borsh::BorshSerialize::serialize(&self.system_program, writer)?; + borsh::BorshSerialize::serialize(&self.config, writer)?; + borsh::BorshSerialize::serialize(&self.rent_recipient, writer)?; + Ok(()) + } + } + impl anchor_lang::idl::build::IdlBuild for CreateRecord { + fn create_type() -> Option { + Some(anchor_lang::idl::types::IdlTypeDef { + name: Self::get_full_path(), + docs: <[_]>::into_vec( + ::alloc::boxed::box_new([ + "Generated client accounts for [`CreateRecord`].".into(), + ]), + ), + serialization: anchor_lang::idl::types::IdlSerialization::default(), + repr: None, + generics: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlTypeDefTy::Struct { + fields: Some( + anchor_lang::idl::types::IdlDefinedFields::Named( + <[_]>::into_vec( + ::alloc::boxed::box_new([ + anchor_lang::idl::types::IdlField { + name: "user".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::Pubkey, + }, + anchor_lang::idl::types::IdlField { + name: "user_record".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::Pubkey, + }, + anchor_lang::idl::types::IdlField { + name: "system_program".into(), + docs: <[_]>::into_vec( + ::alloc::boxed::box_new([ + "Needs to be here for the init anchor macro to work.".into(), + ]), + ), + ty: anchor_lang::idl::types::IdlType::Pubkey, + }, + anchor_lang::idl::types::IdlField { + name: "config".into(), + docs: <[_]>::into_vec( + ::alloc::boxed::box_new([ + "The global config account".into(), + ]), + ), + ty: anchor_lang::idl::types::IdlType::Pubkey, + }, + anchor_lang::idl::types::IdlField { + name: "rent_recipient".into(), + docs: <[_]>::into_vec( + ::alloc::boxed::box_new([ + "Rent recipient - must match config".into(), + ]), + ), + ty: anchor_lang::idl::types::IdlType::Pubkey, + }, + ]), + ), + ), + ), + }, + }) + } + fn insert_types( + types: &mut std::collections::BTreeMap< + String, + anchor_lang::idl::types::IdlTypeDef, + >, + ) {} + fn get_full_path() -> String { + ::alloc::__export::must_use({ + let res = ::alloc::fmt::format( + format_args!( + "{0}::{1}", + "anchor_compressible_derived::__client_accounts_create_record", + "CreateRecord", + ), + ); + res + }) + } + } + #[automatically_derived] + impl anchor_lang::ToAccountMetas for CreateRecord { + fn to_account_metas( + &self, + is_signer: Option, + ) -> Vec { + let mut account_metas = ::alloc::vec::Vec::new(); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new( + self.user, + true, + ), + ); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new( + self.user_record, + false, + ), + ); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new_readonly( + self.system_program, + false, + ), + ); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new_readonly( + self.config, + false, + ), + ); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new( + self.rent_recipient, + false, + ), + ); + account_metas + } + } +} +/// An internal, Anchor generated module. This is used (as an +/// implementation detail), to generate a CPI struct for a given +/// `#[derive(Accounts)]` implementation, where each field is an +/// AccountInfo. +/// +/// To access the struct in this module, one should use the sibling +/// [`cpi::accounts`] module (also generated), which re-exports this. +pub(crate) mod __cpi_client_accounts_create_record { + use super::*; + /// Generated CPI struct of the accounts for [`CreateRecord`]. + pub struct CreateRecord<'info> { + pub user: anchor_lang::solana_program::account_info::AccountInfo<'info>, + pub user_record: anchor_lang::solana_program::account_info::AccountInfo<'info>, + ///Needs to be here for the init anchor macro to work. + pub system_program: anchor_lang::solana_program::account_info::AccountInfo< + 'info, + >, + ///The global config account + pub config: anchor_lang::solana_program::account_info::AccountInfo<'info>, + ///Rent recipient - must match config + pub rent_recipient: anchor_lang::solana_program::account_info::AccountInfo< + 'info, + >, + } + #[automatically_derived] + impl<'info> anchor_lang::ToAccountMetas for CreateRecord<'info> { + fn to_account_metas( + &self, + is_signer: Option, + ) -> Vec { + let mut account_metas = ::alloc::vec::Vec::new(); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new( + anchor_lang::Key::key(&self.user), + true, + ), + ); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new( + anchor_lang::Key::key(&self.user_record), + false, + ), + ); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new_readonly( + anchor_lang::Key::key(&self.system_program), + false, + ), + ); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new_readonly( + anchor_lang::Key::key(&self.config), + false, + ), + ); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new( + anchor_lang::Key::key(&self.rent_recipient), + false, + ), + ); + account_metas + } + } + #[automatically_derived] + impl<'info> anchor_lang::ToAccountInfos<'info> for CreateRecord<'info> { + fn to_account_infos( + &self, + ) -> Vec> { + let mut account_infos = ::alloc::vec::Vec::new(); + account_infos + .extend(anchor_lang::ToAccountInfos::to_account_infos(&self.user)); + account_infos + .extend( + anchor_lang::ToAccountInfos::to_account_infos(&self.user_record), + ); + account_infos + .extend( + anchor_lang::ToAccountInfos::to_account_infos(&self.system_program), + ); + account_infos + .extend(anchor_lang::ToAccountInfos::to_account_infos(&self.config)); + account_infos + .extend( + anchor_lang::ToAccountInfos::to_account_infos(&self.rent_recipient), + ); + account_infos + } + } +} +impl<'info> CreateRecord<'info> { + pub fn __anchor_private_gen_idl_accounts( + accounts: &mut std::collections::BTreeMap< + String, + anchor_lang::idl::types::IdlAccount, + >, + types: &mut std::collections::BTreeMap< + String, + anchor_lang::idl::types::IdlTypeDef, + >, + ) -> Vec { + if let Some(ty) = ::create_type() { + let account = anchor_lang::idl::types::IdlAccount { + name: ty.name.clone(), + discriminator: UserRecord::DISCRIMINATOR.into(), + }; + accounts.insert(account.name.clone(), account); + types.insert(ty.name.clone(), ty); + ::insert_types(types); + } + <[_]>::into_vec( + ::alloc::boxed::box_new([ + anchor_lang::idl::types::IdlInstructionAccountItem::Single(anchor_lang::idl::types::IdlInstructionAccount { + name: "user".into(), + docs: ::alloc::vec::Vec::new(), + writable: true, + signer: true, + optional: false, + address: None, + pda: None, + relations: ::alloc::vec::Vec::new(), + }), + anchor_lang::idl::types::IdlInstructionAccountItem::Single(anchor_lang::idl::types::IdlInstructionAccount { + name: "user_record".into(), + docs: ::alloc::vec::Vec::new(), + writable: true, + signer: false, + optional: false, + address: None, + pda: None, + relations: ::alloc::vec::Vec::new(), + }), + anchor_lang::idl::types::IdlInstructionAccountItem::Single(anchor_lang::idl::types::IdlInstructionAccount { + name: "system_program".into(), + docs: <[_]>::into_vec( + ::alloc::boxed::box_new([ + "Needs to be here for the init anchor macro to work.".into(), + ]), + ), + writable: false, + signer: false, + optional: false, + address: None, + pda: None, + relations: ::alloc::vec::Vec::new(), + }), + anchor_lang::idl::types::IdlInstructionAccountItem::Single(anchor_lang::idl::types::IdlInstructionAccount { + name: "config".into(), + docs: <[_]>::into_vec( + ::alloc::boxed::box_new(["The global config account".into()]), + ), + writable: false, + signer: false, + optional: false, + address: None, + pda: None, + relations: ::alloc::vec::Vec::new(), + }), + anchor_lang::idl::types::IdlInstructionAccountItem::Single(anchor_lang::idl::types::IdlInstructionAccount { + name: "rent_recipient".into(), + docs: <[_]>::into_vec( + ::alloc::boxed::box_new([ + "Rent recipient - must match config".into(), + ]), + ), + writable: true, + signer: false, + optional: false, + address: None, + pda: None, + relations: ::alloc::vec::Vec::new(), + }), + ]), + ) + } +} +#[instruction(placeholder_id:u64)] +pub struct CreatePlaceholderRecord<'info> { + #[account(mut)] + pub user: Signer<'info>, + #[account( + init, + payer = user, + space = 8+10+32+4+32+8, + seeds = [b"placeholder_record", + placeholder_id.to_le_bytes().as_ref()], + bump, + )] + pub placeholder_record: Account<'info, PlaceholderRecord>, + /// Needs to be here for the init anchor macro to work. + pub system_program: Program<'info, System>, + /// The global config account + /// CHECK: Config is validated by the SDK's load_checked method + pub config: AccountInfo<'info>, + /// Rent recipient - must match config + /// CHECK: Rent recipient is validated against the config + #[account(mut)] + pub rent_recipient: AccountInfo<'info>, +} +#[automatically_derived] +impl<'info> anchor_lang::Accounts<'info, CreatePlaceholderRecordBumps> +for CreatePlaceholderRecord<'info> +where + 'info: 'info, +{ + #[inline(never)] + fn try_accounts( + __program_id: &anchor_lang::solana_program::pubkey::Pubkey, + __accounts: &mut &'info [anchor_lang::solana_program::account_info::AccountInfo< + 'info, + >], + __ix_data: &[u8], + __bumps: &mut CreatePlaceholderRecordBumps, + __reallocs: &mut std::collections::BTreeSet< + anchor_lang::solana_program::pubkey::Pubkey, + >, + ) -> anchor_lang::Result { + let mut __ix_data = __ix_data; + struct __Args { + placeholder_id: u64, + } + impl borsh::ser::BorshSerialize for __Args + where + u64: borsh::ser::BorshSerialize, + { + fn serialize( + &self, + writer: &mut W, + ) -> ::core::result::Result<(), borsh::maybestd::io::Error> { + borsh::BorshSerialize::serialize(&self.placeholder_id, writer)?; + Ok(()) + } + } + impl anchor_lang::idl::build::IdlBuild for __Args { + fn create_type() -> Option { + Some(anchor_lang::idl::types::IdlTypeDef { + name: Self::get_full_path(), + docs: ::alloc::vec::Vec::new(), + serialization: anchor_lang::idl::types::IdlSerialization::default(), + repr: None, + generics: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlTypeDefTy::Struct { + fields: Some( + anchor_lang::idl::types::IdlDefinedFields::Named( + <[_]>::into_vec( + ::alloc::boxed::box_new([ + anchor_lang::idl::types::IdlField { + name: "placeholder_id".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::U64, + }, + ]), + ), + ), + ), + }, + }) + } + fn insert_types( + types: &mut std::collections::BTreeMap< + String, + anchor_lang::idl::types::IdlTypeDef, + >, + ) {} + fn get_full_path() -> String { + ::alloc::__export::must_use({ + let res = ::alloc::fmt::format( + format_args!("{0}::{1}", "anchor_compressible_derived", "__Args"), + ); + res + }) + } + } + impl borsh::de::BorshDeserialize for __Args + where + u64: borsh::BorshDeserialize, + { + fn deserialize_reader( + reader: &mut R, + ) -> ::core::result::Result { + Ok(Self { + placeholder_id: borsh::BorshDeserialize::deserialize_reader(reader)?, + }) + } + } + let __Args { placeholder_id } = __Args::deserialize(&mut __ix_data) + .map_err(|_| anchor_lang::error::ErrorCode::InstructionDidNotDeserialize)?; + let user: Signer = anchor_lang::Accounts::try_accounts( + __program_id, + __accounts, + __ix_data, + __bumps, + __reallocs, + ) + .map_err(|e| e.with_account_name("user"))?; + if __accounts.is_empty() { + return Err(anchor_lang::error::ErrorCode::AccountNotEnoughKeys.into()); + } + let placeholder_record = &__accounts[0]; + *__accounts = &__accounts[1..]; + let system_program: anchor_lang::accounts::program::Program = anchor_lang::Accounts::try_accounts( + __program_id, + __accounts, + __ix_data, + __bumps, + __reallocs, + ) + .map_err(|e| e.with_account_name("system_program"))?; + let config: AccountInfo = anchor_lang::Accounts::try_accounts( + __program_id, + __accounts, + __ix_data, + __bumps, + __reallocs, + ) + .map_err(|e| e.with_account_name("config"))?; + let rent_recipient: AccountInfo = anchor_lang::Accounts::try_accounts( + __program_id, + __accounts, + __ix_data, + __bumps, + __reallocs, + ) + .map_err(|e| e.with_account_name("rent_recipient"))?; + let __anchor_rent = Rent::get()?; + let (__pda_address, __bump) = Pubkey::find_program_address( + &[b"placeholder_record", placeholder_id.to_le_bytes().as_ref()], + __program_id, + ); + __bumps.placeholder_record = __bump; + if placeholder_record.key() != __pda_address { + return Err( + anchor_lang::error::Error::from( + anchor_lang::error::ErrorCode::ConstraintSeeds, + ) + .with_account_name("placeholder_record") + .with_pubkeys((placeholder_record.key(), __pda_address)), + ); + } + let placeholder_record = ({ + #[inline(never)] + || { + let actual_field = AsRef::::as_ref(&placeholder_record); + let actual_owner = actual_field.owner; + let space = 8 + 10 + 32 + 4 + 32 + 8; + let pa: anchor_lang::accounts::account::Account = if !false + || actual_owner == &anchor_lang::solana_program::system_program::ID + { + let __current_lamports = placeholder_record.lamports(); + if __current_lamports == 0 { + let space = space; + let lamports = __anchor_rent.minimum_balance(space); + let cpi_accounts = anchor_lang::system_program::CreateAccount { + from: user.to_account_info(), + to: placeholder_record.to_account_info(), + }; + let cpi_context = anchor_lang::context::CpiContext::new( + system_program.to_account_info(), + cpi_accounts, + ); + anchor_lang::system_program::create_account( + cpi_context + .with_signer( + &[ + &[ + b"placeholder_record", + placeholder_id.to_le_bytes().as_ref(), + &[__bump][..], + ][..], + ], + ), + lamports, + space as u64, + __program_id, + )?; + } else { + if user.key() == placeholder_record.key() { + return Err( + anchor_lang::error::Error::from(anchor_lang::error::AnchorError { + error_name: anchor_lang::error::ErrorCode::TryingToInitPayerAsProgramAccount + .name(), + error_code_number: anchor_lang::error::ErrorCode::TryingToInitPayerAsProgramAccount + .into(), + error_msg: anchor_lang::error::ErrorCode::TryingToInitPayerAsProgramAccount + .to_string(), + error_origin: Some( + anchor_lang::error::ErrorOrigin::Source(anchor_lang::error::Source { + filename: "sdk-tests/anchor-compressible-derived/src/lib.rs", + line: 964u32, + }), + ), + compared_values: None, + }) + .with_pubkeys((user.key(), placeholder_record.key())), + ); + } + let required_lamports = __anchor_rent + .minimum_balance(space) + .max(1) + .saturating_sub(__current_lamports); + if required_lamports > 0 { + let cpi_accounts = anchor_lang::system_program::Transfer { + from: user.to_account_info(), + to: placeholder_record.to_account_info(), + }; + let cpi_context = anchor_lang::context::CpiContext::new( + system_program.to_account_info(), + cpi_accounts, + ); + anchor_lang::system_program::transfer( + cpi_context, + required_lamports, + )?; + } + let cpi_accounts = anchor_lang::system_program::Allocate { + account_to_allocate: placeholder_record.to_account_info(), + }; + let cpi_context = anchor_lang::context::CpiContext::new( + system_program.to_account_info(), + cpi_accounts, + ); + anchor_lang::system_program::allocate( + cpi_context + .with_signer( + &[ + &[ + b"placeholder_record", + placeholder_id.to_le_bytes().as_ref(), + &[__bump][..], + ][..], + ], + ), + space as u64, + )?; + let cpi_accounts = anchor_lang::system_program::Assign { + account_to_assign: placeholder_record.to_account_info(), + }; + let cpi_context = anchor_lang::context::CpiContext::new( + system_program.to_account_info(), + cpi_accounts, + ); + anchor_lang::system_program::assign( + cpi_context + .with_signer( + &[ + &[ + b"placeholder_record", + placeholder_id.to_le_bytes().as_ref(), + &[__bump][..], + ][..], + ], + ), + __program_id, + )?; + } + match anchor_lang::accounts::account::Account::try_from_unchecked( + &placeholder_record, + ) { + Ok(val) => val, + Err(e) => return Err(e.with_account_name("placeholder_record")), + } + } else { + match anchor_lang::accounts::account::Account::try_from( + &placeholder_record, + ) { + Ok(val) => val, + Err(e) => return Err(e.with_account_name("placeholder_record")), + } + }; + if false { + if space != actual_field.data_len() { + return Err( + anchor_lang::error::Error::from( + anchor_lang::error::ErrorCode::ConstraintSpace, + ) + .with_account_name("placeholder_record") + .with_values((space, actual_field.data_len())), + ); + } + if actual_owner != __program_id { + return Err( + anchor_lang::error::Error::from( + anchor_lang::error::ErrorCode::ConstraintOwner, + ) + .with_account_name("placeholder_record") + .with_pubkeys((*actual_owner, *__program_id)), + ); + } + { + let required_lamports = __anchor_rent.minimum_balance(space); + if pa.to_account_info().lamports() < required_lamports { + return Err( + anchor_lang::error::Error::from( + anchor_lang::error::ErrorCode::ConstraintRentExempt, + ) + .with_account_name("placeholder_record"), + ); + } + } + } + Ok(pa) + } + })()?; + if !AsRef::::as_ref(&placeholder_record).is_writable { + return Err( + anchor_lang::error::Error::from( + anchor_lang::error::ErrorCode::ConstraintMut, + ) + .with_account_name("placeholder_record"), + ); + } + if !__anchor_rent + .is_exempt( + placeholder_record.to_account_info().lamports(), + placeholder_record.to_account_info().try_data_len()?, + ) + { + return Err( + anchor_lang::error::Error::from( + anchor_lang::error::ErrorCode::ConstraintRentExempt, + ) + .with_account_name("placeholder_record"), + ); + } + if !AsRef::::as_ref(&user).is_writable { + return Err( + anchor_lang::error::Error::from( + anchor_lang::error::ErrorCode::ConstraintMut, + ) + .with_account_name("user"), + ); + } + if !&rent_recipient.is_writable { + return Err( + anchor_lang::error::Error::from( + anchor_lang::error::ErrorCode::ConstraintMut, + ) + .with_account_name("rent_recipient"), + ); + } + Ok(CreatePlaceholderRecord { + user, + placeholder_record, + system_program, + config, + rent_recipient, + }) + } +} +#[automatically_derived] +impl<'info> anchor_lang::ToAccountInfos<'info> for CreatePlaceholderRecord<'info> +where + 'info: 'info, +{ + fn to_account_infos( + &self, + ) -> Vec> { + let mut account_infos = ::alloc::vec::Vec::new(); + account_infos.extend(self.user.to_account_infos()); + account_infos.extend(self.placeholder_record.to_account_infos()); + account_infos.extend(self.system_program.to_account_infos()); + account_infos.extend(self.config.to_account_infos()); + account_infos.extend(self.rent_recipient.to_account_infos()); + account_infos + } +} +#[automatically_derived] +impl<'info> anchor_lang::ToAccountMetas for CreatePlaceholderRecord<'info> { + fn to_account_metas( + &self, + is_signer: Option, + ) -> Vec { + let mut account_metas = ::alloc::vec::Vec::new(); + account_metas.extend(self.user.to_account_metas(None)); + account_metas.extend(self.placeholder_record.to_account_metas(None)); + account_metas.extend(self.system_program.to_account_metas(None)); + account_metas.extend(self.config.to_account_metas(None)); + account_metas.extend(self.rent_recipient.to_account_metas(None)); + account_metas + } +} +#[automatically_derived] +impl<'info> anchor_lang::AccountsExit<'info> for CreatePlaceholderRecord<'info> +where + 'info: 'info, +{ + fn exit( + &self, + program_id: &anchor_lang::solana_program::pubkey::Pubkey, + ) -> anchor_lang::Result<()> { + anchor_lang::AccountsExit::exit(&self.user, program_id) + .map_err(|e| e.with_account_name("user"))?; + anchor_lang::AccountsExit::exit(&self.placeholder_record, program_id) + .map_err(|e| e.with_account_name("placeholder_record"))?; + anchor_lang::AccountsExit::exit(&self.rent_recipient, program_id) + .map_err(|e| e.with_account_name("rent_recipient"))?; + Ok(()) + } +} +pub struct CreatePlaceholderRecordBumps { + pub placeholder_record: u8, +} +#[automatically_derived] +impl ::core::fmt::Debug for CreatePlaceholderRecordBumps { + #[inline] + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + ::core::fmt::Formatter::debug_struct_field1_finish( + f, + "CreatePlaceholderRecordBumps", + "placeholder_record", + &&self.placeholder_record, + ) + } +} +impl Default for CreatePlaceholderRecordBumps { + fn default() -> Self { + CreatePlaceholderRecordBumps { + placeholder_record: u8::MAX, + } + } +} +impl<'info> anchor_lang::Bumps for CreatePlaceholderRecord<'info> +where + 'info: 'info, +{ + type Bumps = CreatePlaceholderRecordBumps; +} +/// An internal, Anchor generated module. This is used (as an +/// implementation detail), to generate a struct for a given +/// `#[derive(Accounts)]` implementation, where each field is a Pubkey, +/// instead of an `AccountInfo`. This is useful for clients that want +/// to generate a list of accounts, without explicitly knowing the +/// order all the fields should be in. +/// +/// To access the struct in this module, one should use the sibling +/// `accounts` module (also generated), which re-exports this. +pub(crate) mod __client_accounts_create_placeholder_record { + use super::*; + use anchor_lang::prelude::borsh; + /// Generated client accounts for [`CreatePlaceholderRecord`]. + pub struct CreatePlaceholderRecord { + pub user: Pubkey, + pub placeholder_record: Pubkey, + ///Needs to be here for the init anchor macro to work. + pub system_program: Pubkey, + ///The global config account + pub config: Pubkey, + ///Rent recipient - must match config + pub rent_recipient: Pubkey, + } + impl borsh::ser::BorshSerialize for CreatePlaceholderRecord + where + Pubkey: borsh::ser::BorshSerialize, + Pubkey: borsh::ser::BorshSerialize, + Pubkey: borsh::ser::BorshSerialize, + Pubkey: borsh::ser::BorshSerialize, + Pubkey: borsh::ser::BorshSerialize, + { + fn serialize( + &self, + writer: &mut W, + ) -> ::core::result::Result<(), borsh::maybestd::io::Error> { + borsh::BorshSerialize::serialize(&self.user, writer)?; + borsh::BorshSerialize::serialize(&self.placeholder_record, writer)?; + borsh::BorshSerialize::serialize(&self.system_program, writer)?; + borsh::BorshSerialize::serialize(&self.config, writer)?; + borsh::BorshSerialize::serialize(&self.rent_recipient, writer)?; + Ok(()) + } + } + impl anchor_lang::idl::build::IdlBuild for CreatePlaceholderRecord { + fn create_type() -> Option { + Some(anchor_lang::idl::types::IdlTypeDef { + name: Self::get_full_path(), + docs: <[_]>::into_vec( + ::alloc::boxed::box_new([ + "Generated client accounts for [`CreatePlaceholderRecord`]." + .into(), + ]), + ), + serialization: anchor_lang::idl::types::IdlSerialization::default(), + repr: None, + generics: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlTypeDefTy::Struct { + fields: Some( + anchor_lang::idl::types::IdlDefinedFields::Named( + <[_]>::into_vec( + ::alloc::boxed::box_new([ + anchor_lang::idl::types::IdlField { + name: "user".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::Pubkey, + }, + anchor_lang::idl::types::IdlField { + name: "placeholder_record".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::Pubkey, + }, + anchor_lang::idl::types::IdlField { + name: "system_program".into(), + docs: <[_]>::into_vec( + ::alloc::boxed::box_new([ + "Needs to be here for the init anchor macro to work.".into(), + ]), + ), + ty: anchor_lang::idl::types::IdlType::Pubkey, + }, + anchor_lang::idl::types::IdlField { + name: "config".into(), + docs: <[_]>::into_vec( + ::alloc::boxed::box_new([ + "The global config account".into(), + ]), + ), + ty: anchor_lang::idl::types::IdlType::Pubkey, + }, + anchor_lang::idl::types::IdlField { + name: "rent_recipient".into(), + docs: <[_]>::into_vec( + ::alloc::boxed::box_new([ + "Rent recipient - must match config".into(), + ]), + ), + ty: anchor_lang::idl::types::IdlType::Pubkey, + }, + ]), + ), + ), + ), + }, + }) + } + fn insert_types( + types: &mut std::collections::BTreeMap< + String, + anchor_lang::idl::types::IdlTypeDef, + >, + ) {} + fn get_full_path() -> String { + ::alloc::__export::must_use({ + let res = ::alloc::fmt::format( + format_args!( + "{0}::{1}", + "anchor_compressible_derived::__client_accounts_create_placeholder_record", + "CreatePlaceholderRecord", + ), + ); + res + }) + } + } + #[automatically_derived] + impl anchor_lang::ToAccountMetas for CreatePlaceholderRecord { + fn to_account_metas( + &self, + is_signer: Option, + ) -> Vec { + let mut account_metas = ::alloc::vec::Vec::new(); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new( + self.user, + true, + ), + ); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new( + self.placeholder_record, + false, + ), + ); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new_readonly( + self.system_program, + false, + ), + ); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new_readonly( + self.config, + false, + ), + ); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new( + self.rent_recipient, + false, + ), + ); + account_metas + } + } +} +/// An internal, Anchor generated module. This is used (as an +/// implementation detail), to generate a CPI struct for a given +/// `#[derive(Accounts)]` implementation, where each field is an +/// AccountInfo. +/// +/// To access the struct in this module, one should use the sibling +/// [`cpi::accounts`] module (also generated), which re-exports this. +pub(crate) mod __cpi_client_accounts_create_placeholder_record { + use super::*; + /// Generated CPI struct of the accounts for [`CreatePlaceholderRecord`]. + pub struct CreatePlaceholderRecord<'info> { + pub user: anchor_lang::solana_program::account_info::AccountInfo<'info>, + pub placeholder_record: anchor_lang::solana_program::account_info::AccountInfo< + 'info, + >, + ///Needs to be here for the init anchor macro to work. + pub system_program: anchor_lang::solana_program::account_info::AccountInfo< + 'info, + >, + ///The global config account + pub config: anchor_lang::solana_program::account_info::AccountInfo<'info>, + ///Rent recipient - must match config + pub rent_recipient: anchor_lang::solana_program::account_info::AccountInfo< + 'info, + >, + } + #[automatically_derived] + impl<'info> anchor_lang::ToAccountMetas for CreatePlaceholderRecord<'info> { + fn to_account_metas( + &self, + is_signer: Option, + ) -> Vec { + let mut account_metas = ::alloc::vec::Vec::new(); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new( + anchor_lang::Key::key(&self.user), + true, + ), + ); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new( + anchor_lang::Key::key(&self.placeholder_record), + false, + ), + ); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new_readonly( + anchor_lang::Key::key(&self.system_program), + false, + ), + ); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new_readonly( + anchor_lang::Key::key(&self.config), + false, + ), + ); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new( + anchor_lang::Key::key(&self.rent_recipient), + false, + ), + ); + account_metas + } + } + #[automatically_derived] + impl<'info> anchor_lang::ToAccountInfos<'info> for CreatePlaceholderRecord<'info> { + fn to_account_infos( + &self, + ) -> Vec> { + let mut account_infos = ::alloc::vec::Vec::new(); + account_infos + .extend(anchor_lang::ToAccountInfos::to_account_infos(&self.user)); + account_infos + .extend( + anchor_lang::ToAccountInfos::to_account_infos( + &self.placeholder_record, + ), + ); + account_infos + .extend( + anchor_lang::ToAccountInfos::to_account_infos(&self.system_program), + ); + account_infos + .extend(anchor_lang::ToAccountInfos::to_account_infos(&self.config)); + account_infos + .extend( + anchor_lang::ToAccountInfos::to_account_infos(&self.rent_recipient), + ); + account_infos + } + } +} +impl<'info> CreatePlaceholderRecord<'info> { + pub fn __anchor_private_gen_idl_accounts( + accounts: &mut std::collections::BTreeMap< + String, + anchor_lang::idl::types::IdlAccount, + >, + types: &mut std::collections::BTreeMap< + String, + anchor_lang::idl::types::IdlTypeDef, + >, + ) -> Vec { + if let Some(ty) = ::create_type() { + let account = anchor_lang::idl::types::IdlAccount { + name: ty.name.clone(), + discriminator: PlaceholderRecord::DISCRIMINATOR.into(), + }; + accounts.insert(account.name.clone(), account); + types.insert(ty.name.clone(), ty); + ::insert_types(types); + } + <[_]>::into_vec( + ::alloc::boxed::box_new([ + anchor_lang::idl::types::IdlInstructionAccountItem::Single(anchor_lang::idl::types::IdlInstructionAccount { + name: "user".into(), + docs: ::alloc::vec::Vec::new(), + writable: true, + signer: true, + optional: false, + address: None, + pda: None, + relations: ::alloc::vec::Vec::new(), + }), + anchor_lang::idl::types::IdlInstructionAccountItem::Single(anchor_lang::idl::types::IdlInstructionAccount { + name: "placeholder_record".into(), + docs: ::alloc::vec::Vec::new(), + writable: true, + signer: false, + optional: false, + address: None, + pda: None, + relations: ::alloc::vec::Vec::new(), + }), + anchor_lang::idl::types::IdlInstructionAccountItem::Single(anchor_lang::idl::types::IdlInstructionAccount { + name: "system_program".into(), + docs: <[_]>::into_vec( + ::alloc::boxed::box_new([ + "Needs to be here for the init anchor macro to work.".into(), + ]), + ), + writable: false, + signer: false, + optional: false, + address: None, + pda: None, + relations: ::alloc::vec::Vec::new(), + }), + anchor_lang::idl::types::IdlInstructionAccountItem::Single(anchor_lang::idl::types::IdlInstructionAccount { + name: "config".into(), + docs: <[_]>::into_vec( + ::alloc::boxed::box_new(["The global config account".into()]), + ), + writable: false, + signer: false, + optional: false, + address: None, + pda: None, + relations: ::alloc::vec::Vec::new(), + }), + anchor_lang::idl::types::IdlInstructionAccountItem::Single(anchor_lang::idl::types::IdlInstructionAccount { + name: "rent_recipient".into(), + docs: <[_]>::into_vec( + ::alloc::boxed::box_new([ + "Rent recipient - must match config".into(), + ]), + ), + writable: true, + signer: false, + optional: false, + address: None, + pda: None, + relations: ::alloc::vec::Vec::new(), + }), + ]), + ) + } +} +#[instruction(account_data:AccountCreationData)] +pub struct CreateUserRecordAndGameSession<'info> { + #[account(mut)] + pub user: Signer<'info>, + #[account( + init, + payer = user, + 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, + 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>, + /// 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 COMPRESSED_TOKEN_PROGRAM_ID constant + pub compressed_token_program: UncheckedAccount<'info>, + /// CHECK: CPI authority of the compressed token program + pub compress_token_program_cpi_authority: UncheckedAccount<'info>, + /// Needs to be here for the init anchor macro to work. + pub system_program: Program<'info, System>, + /// The global config account + /// CHECK: Config is validated by the SDK's load_checked method + pub config: AccountInfo<'info>, + /// Rent recipient - must match config + /// CHECK: Rent recipient is validated against the config + #[account(mut)] + pub rent_recipient: AccountInfo<'info>, +} +#[automatically_derived] +impl<'info> anchor_lang::Accounts<'info, CreateUserRecordAndGameSessionBumps> +for CreateUserRecordAndGameSession<'info> +where + 'info: 'info, +{ + #[inline(never)] + fn try_accounts( + __program_id: &anchor_lang::solana_program::pubkey::Pubkey, + __accounts: &mut &'info [anchor_lang::solana_program::account_info::AccountInfo< + 'info, + >], + __ix_data: &[u8], + __bumps: &mut CreateUserRecordAndGameSessionBumps, + __reallocs: &mut std::collections::BTreeSet< + anchor_lang::solana_program::pubkey::Pubkey, + >, + ) -> anchor_lang::Result { + let mut __ix_data = __ix_data; + struct __Args { + account_data: AccountCreationData, + } + impl borsh::ser::BorshSerialize for __Args + where + AccountCreationData: borsh::ser::BorshSerialize, + { + fn serialize( + &self, + writer: &mut W, + ) -> ::core::result::Result<(), borsh::maybestd::io::Error> { + borsh::BorshSerialize::serialize(&self.account_data, writer)?; + Ok(()) + } + } + impl anchor_lang::idl::build::IdlBuild for __Args { + fn create_type() -> Option { + Some(anchor_lang::idl::types::IdlTypeDef { + name: Self::get_full_path(), + docs: ::alloc::vec::Vec::new(), + serialization: anchor_lang::idl::types::IdlSerialization::default(), + repr: None, + generics: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlTypeDefTy::Struct { + fields: Some( + anchor_lang::idl::types::IdlDefinedFields::Named( + <[_]>::into_vec( + ::alloc::boxed::box_new([ + anchor_lang::idl::types::IdlField { + name: "account_data".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::Defined { + name: ::get_full_path(), + generics: ::alloc::vec::Vec::new(), + }, + }, + ]), + ), + ), + ), + }, + }) + } + fn insert_types( + types: &mut std::collections::BTreeMap< + String, + anchor_lang::idl::types::IdlTypeDef, + >, + ) { + if let Some(ty) = ::create_type() { + types.insert(::get_full_path(), ty); + ::insert_types(types); + } + } + fn get_full_path() -> String { + ::alloc::__export::must_use({ + let res = ::alloc::fmt::format( + format_args!("{0}::{1}", "anchor_compressible_derived", "__Args"), + ); + res + }) + } + } + impl borsh::de::BorshDeserialize for __Args + where + AccountCreationData: borsh::BorshDeserialize, + { + fn deserialize_reader( + reader: &mut R, + ) -> ::core::result::Result { + Ok(Self { + account_data: borsh::BorshDeserialize::deserialize_reader(reader)?, + }) + } + } + let __Args { account_data } = __Args::deserialize(&mut __ix_data) + .map_err(|_| anchor_lang::error::ErrorCode::InstructionDidNotDeserialize)?; + let user: Signer = anchor_lang::Accounts::try_accounts( + __program_id, + __accounts, + __ix_data, + __bumps, + __reallocs, + ) + .map_err(|e| e.with_account_name("user"))?; + if __accounts.is_empty() { + return Err(anchor_lang::error::ErrorCode::AccountNotEnoughKeys.into()); + } + let user_record = &__accounts[0]; + *__accounts = &__accounts[1..]; + if __accounts.is_empty() { + return Err(anchor_lang::error::ErrorCode::AccountNotEnoughKeys.into()); + } + let game_session = &__accounts[0]; + *__accounts = &__accounts[1..]; + let mint_signer: Signer = anchor_lang::Accounts::try_accounts( + __program_id, + __accounts, + __ix_data, + __bumps, + __reallocs, + ) + .map_err(|e| e.with_account_name("mint_signer"))?; + let mint_authority: Signer = anchor_lang::Accounts::try_accounts( + __program_id, + __accounts, + __ix_data, + __bumps, + __reallocs, + ) + .map_err(|e| e.with_account_name("mint_authority"))?; + let compressed_token_program: UncheckedAccount = anchor_lang::Accounts::try_accounts( + __program_id, + __accounts, + __ix_data, + __bumps, + __reallocs, + ) + .map_err(|e| e.with_account_name("compressed_token_program"))?; + let compress_token_program_cpi_authority: UncheckedAccount = anchor_lang::Accounts::try_accounts( + __program_id, + __accounts, + __ix_data, + __bumps, + __reallocs, + ) + .map_err(|e| e.with_account_name("compress_token_program_cpi_authority"))?; + let system_program: anchor_lang::accounts::program::Program = anchor_lang::Accounts::try_accounts( + __program_id, + __accounts, + __ix_data, + __bumps, + __reallocs, + ) + .map_err(|e| e.with_account_name("system_program"))?; + let config: AccountInfo = anchor_lang::Accounts::try_accounts( + __program_id, + __accounts, + __ix_data, + __bumps, + __reallocs, + ) + .map_err(|e| e.with_account_name("config"))?; + let rent_recipient: AccountInfo = anchor_lang::Accounts::try_accounts( + __program_id, + __accounts, + __ix_data, + __bumps, + __reallocs, + ) + .map_err(|e| e.with_account_name("rent_recipient"))?; + let __anchor_rent = Rent::get()?; + let (__pda_address, __bump) = Pubkey::find_program_address( + &[b"user_record", user.key().as_ref()], + __program_id, + ); + __bumps.user_record = __bump; + if user_record.key() != __pda_address { + return Err( + anchor_lang::error::Error::from( + anchor_lang::error::ErrorCode::ConstraintSeeds, + ) + .with_account_name("user_record") + .with_pubkeys((user_record.key(), __pda_address)), + ); + } + let user_record = ({ + #[inline(never)] + || { + let actual_field = AsRef::::as_ref(&user_record); + let actual_owner = actual_field.owner; + let space = 8 + 32 + 4 + 32 + 8 + 10; + let pa: anchor_lang::accounts::account::Account = if !false + || actual_owner == &anchor_lang::solana_program::system_program::ID + { + let __current_lamports = user_record.lamports(); + if __current_lamports == 0 { + let space = space; + let lamports = __anchor_rent.minimum_balance(space); + let cpi_accounts = anchor_lang::system_program::CreateAccount { + from: user.to_account_info(), + to: user_record.to_account_info(), + }; + let cpi_context = anchor_lang::context::CpiContext::new( + system_program.to_account_info(), + cpi_accounts, + ); + anchor_lang::system_program::create_account( + cpi_context + .with_signer( + &[&[b"user_record", user.key().as_ref(), &[__bump][..]][..]], + ), + lamports, + space as u64, + __program_id, + )?; + } else { + if user.key() == user_record.key() { + return Err( + anchor_lang::error::Error::from(anchor_lang::error::AnchorError { + error_name: anchor_lang::error::ErrorCode::TryingToInitPayerAsProgramAccount + .name(), + error_code_number: anchor_lang::error::ErrorCode::TryingToInitPayerAsProgramAccount + .into(), + error_msg: anchor_lang::error::ErrorCode::TryingToInitPayerAsProgramAccount + .to_string(), + error_origin: Some( + anchor_lang::error::ErrorOrigin::Source(anchor_lang::error::Source { + filename: "sdk-tests/anchor-compressible-derived/src/lib.rs", + line: 989u32, + }), + ), + compared_values: None, + }) + .with_pubkeys((user.key(), user_record.key())), + ); + } + let required_lamports = __anchor_rent + .minimum_balance(space) + .max(1) + .saturating_sub(__current_lamports); + if required_lamports > 0 { + let cpi_accounts = anchor_lang::system_program::Transfer { + from: user.to_account_info(), + to: user_record.to_account_info(), + }; + let cpi_context = anchor_lang::context::CpiContext::new( + system_program.to_account_info(), + cpi_accounts, + ); + anchor_lang::system_program::transfer( + cpi_context, + required_lamports, + )?; + } + let cpi_accounts = anchor_lang::system_program::Allocate { + account_to_allocate: user_record.to_account_info(), + }; + let cpi_context = anchor_lang::context::CpiContext::new( + system_program.to_account_info(), + cpi_accounts, + ); + anchor_lang::system_program::allocate( + cpi_context + .with_signer( + &[&[b"user_record", user.key().as_ref(), &[__bump][..]][..]], + ), + space as u64, + )?; + let cpi_accounts = anchor_lang::system_program::Assign { + account_to_assign: user_record.to_account_info(), + }; + let cpi_context = anchor_lang::context::CpiContext::new( + system_program.to_account_info(), + cpi_accounts, + ); + anchor_lang::system_program::assign( + cpi_context + .with_signer( + &[&[b"user_record", user.key().as_ref(), &[__bump][..]][..]], + ), + __program_id, + )?; + } + match anchor_lang::accounts::account::Account::try_from_unchecked( + &user_record, + ) { + Ok(val) => val, + Err(e) => return Err(e.with_account_name("user_record")), + } + } else { + match anchor_lang::accounts::account::Account::try_from( + &user_record, + ) { + Ok(val) => val, + Err(e) => return Err(e.with_account_name("user_record")), + } + }; + if false { + if space != actual_field.data_len() { + return Err( + anchor_lang::error::Error::from( + anchor_lang::error::ErrorCode::ConstraintSpace, + ) + .with_account_name("user_record") + .with_values((space, actual_field.data_len())), + ); + } + if actual_owner != __program_id { + return Err( + anchor_lang::error::Error::from( + anchor_lang::error::ErrorCode::ConstraintOwner, + ) + .with_account_name("user_record") + .with_pubkeys((*actual_owner, *__program_id)), + ); + } + { + let required_lamports = __anchor_rent.minimum_balance(space); + if pa.to_account_info().lamports() < required_lamports { + return Err( + anchor_lang::error::Error::from( + anchor_lang::error::ErrorCode::ConstraintRentExempt, + ) + .with_account_name("user_record"), + ); + } + } + } + Ok(pa) + } + })()?; + if !AsRef::::as_ref(&user_record).is_writable { + return Err( + anchor_lang::error::Error::from( + anchor_lang::error::ErrorCode::ConstraintMut, + ) + .with_account_name("user_record"), + ); + } + if !__anchor_rent + .is_exempt( + user_record.to_account_info().lamports(), + user_record.to_account_info().try_data_len()?, + ) + { + return Err( + anchor_lang::error::Error::from( + anchor_lang::error::ErrorCode::ConstraintRentExempt, + ) + .with_account_name("user_record"), + ); + } + let __anchor_rent = Rent::get()?; + let (__pda_address, __bump) = Pubkey::find_program_address( + &[b"game_session", account_data.session_id.to_le_bytes().as_ref()], + __program_id, + ); + __bumps.game_session = __bump; + if game_session.key() != __pda_address { + return Err( + anchor_lang::error::Error::from( + anchor_lang::error::ErrorCode::ConstraintSeeds, + ) + .with_account_name("game_session") + .with_pubkeys((game_session.key(), __pda_address)), + ); + } + let game_session = ({ + #[inline(never)] + || { + let actual_field = AsRef::::as_ref(&game_session); + let actual_owner = actual_field.owner; + let space = 8 + 10 + 8 + 32 + 4 + 32 + 8 + 9 + 8; + let pa: anchor_lang::accounts::account::Account = if !false + || actual_owner == &anchor_lang::solana_program::system_program::ID + { + let __current_lamports = game_session.lamports(); + if __current_lamports == 0 { + let space = space; + let lamports = __anchor_rent.minimum_balance(space); + let cpi_accounts = anchor_lang::system_program::CreateAccount { + from: user.to_account_info(), + to: game_session.to_account_info(), + }; + let cpi_context = anchor_lang::context::CpiContext::new( + system_program.to_account_info(), + cpi_accounts, + ); + anchor_lang::system_program::create_account( + cpi_context + .with_signer( + &[ + &[ + b"game_session", + account_data.session_id.to_le_bytes().as_ref(), + &[__bump][..], + ][..], + ], + ), + lamports, + space as u64, + __program_id, + )?; + } else { + if user.key() == game_session.key() { + return Err( + anchor_lang::error::Error::from(anchor_lang::error::AnchorError { + error_name: anchor_lang::error::ErrorCode::TryingToInitPayerAsProgramAccount + .name(), + error_code_number: anchor_lang::error::ErrorCode::TryingToInitPayerAsProgramAccount + .into(), + error_msg: anchor_lang::error::ErrorCode::TryingToInitPayerAsProgramAccount + .to_string(), + error_origin: Some( + anchor_lang::error::ErrorOrigin::Source(anchor_lang::error::Source { + filename: "sdk-tests/anchor-compressible-derived/src/lib.rs", + line: 989u32, + }), + ), + compared_values: None, + }) + .with_pubkeys((user.key(), game_session.key())), + ); + } + let required_lamports = __anchor_rent + .minimum_balance(space) + .max(1) + .saturating_sub(__current_lamports); + if required_lamports > 0 { + let cpi_accounts = anchor_lang::system_program::Transfer { + from: user.to_account_info(), + to: game_session.to_account_info(), + }; + let cpi_context = anchor_lang::context::CpiContext::new( + system_program.to_account_info(), + cpi_accounts, + ); + anchor_lang::system_program::transfer( + cpi_context, + required_lamports, + )?; + } + let cpi_accounts = anchor_lang::system_program::Allocate { + account_to_allocate: game_session.to_account_info(), + }; + let cpi_context = anchor_lang::context::CpiContext::new( + system_program.to_account_info(), + cpi_accounts, + ); + anchor_lang::system_program::allocate( + cpi_context + .with_signer( + &[ + &[ + b"game_session", + account_data.session_id.to_le_bytes().as_ref(), + &[__bump][..], + ][..], + ], + ), + space as u64, + )?; + let cpi_accounts = anchor_lang::system_program::Assign { + account_to_assign: game_session.to_account_info(), + }; + let cpi_context = anchor_lang::context::CpiContext::new( + system_program.to_account_info(), + cpi_accounts, + ); + anchor_lang::system_program::assign( + cpi_context + .with_signer( + &[ + &[ + b"game_session", + account_data.session_id.to_le_bytes().as_ref(), + &[__bump][..], + ][..], + ], + ), + __program_id, + )?; + } + match anchor_lang::accounts::account::Account::try_from_unchecked( + &game_session, + ) { + Ok(val) => val, + Err(e) => return Err(e.with_account_name("game_session")), + } + } else { + match anchor_lang::accounts::account::Account::try_from( + &game_session, + ) { + Ok(val) => val, + Err(e) => return Err(e.with_account_name("game_session")), + } + }; + if false { + if space != actual_field.data_len() { + return Err( + anchor_lang::error::Error::from( + anchor_lang::error::ErrorCode::ConstraintSpace, + ) + .with_account_name("game_session") + .with_values((space, actual_field.data_len())), + ); + } + if actual_owner != __program_id { + return Err( + anchor_lang::error::Error::from( + anchor_lang::error::ErrorCode::ConstraintOwner, + ) + .with_account_name("game_session") + .with_pubkeys((*actual_owner, *__program_id)), + ); + } + { + let required_lamports = __anchor_rent.minimum_balance(space); + if pa.to_account_info().lamports() < required_lamports { + return Err( + anchor_lang::error::Error::from( + anchor_lang::error::ErrorCode::ConstraintRentExempt, + ) + .with_account_name("game_session"), + ); + } + } + } + Ok(pa) + } + })()?; + if !AsRef::::as_ref(&game_session).is_writable { + return Err( + anchor_lang::error::Error::from( + anchor_lang::error::ErrorCode::ConstraintMut, + ) + .with_account_name("game_session"), + ); + } + if !__anchor_rent + .is_exempt( + game_session.to_account_info().lamports(), + game_session.to_account_info().try_data_len()?, + ) + { + return Err( + anchor_lang::error::Error::from( + anchor_lang::error::ErrorCode::ConstraintRentExempt, + ) + .with_account_name("game_session"), + ); + } + if !AsRef::::as_ref(&user).is_writable { + return Err( + anchor_lang::error::Error::from( + anchor_lang::error::ErrorCode::ConstraintMut, + ) + .with_account_name("user"), + ); + } + if !&rent_recipient.is_writable { + return Err( + anchor_lang::error::Error::from( + anchor_lang::error::ErrorCode::ConstraintMut, + ) + .with_account_name("rent_recipient"), + ); + } + Ok(CreateUserRecordAndGameSession { + user, + user_record, + game_session, + mint_signer, + mint_authority, + compressed_token_program, + compress_token_program_cpi_authority, + system_program, + config, + rent_recipient, + }) + } +} +#[automatically_derived] +impl<'info> anchor_lang::ToAccountInfos<'info> for CreateUserRecordAndGameSession<'info> +where + 'info: 'info, +{ + fn to_account_infos( + &self, + ) -> Vec> { + let mut account_infos = ::alloc::vec::Vec::new(); + account_infos.extend(self.user.to_account_infos()); + account_infos.extend(self.user_record.to_account_infos()); + account_infos.extend(self.game_session.to_account_infos()); + account_infos.extend(self.mint_signer.to_account_infos()); + account_infos.extend(self.mint_authority.to_account_infos()); + account_infos.extend(self.compressed_token_program.to_account_infos()); + account_infos + .extend(self.compress_token_program_cpi_authority.to_account_infos()); + account_infos.extend(self.system_program.to_account_infos()); + account_infos.extend(self.config.to_account_infos()); + account_infos.extend(self.rent_recipient.to_account_infos()); + account_infos + } +} +#[automatically_derived] +impl<'info> anchor_lang::ToAccountMetas for CreateUserRecordAndGameSession<'info> { + fn to_account_metas( + &self, + is_signer: Option, + ) -> Vec { + let mut account_metas = ::alloc::vec::Vec::new(); + account_metas.extend(self.user.to_account_metas(None)); + account_metas.extend(self.user_record.to_account_metas(None)); + account_metas.extend(self.game_session.to_account_metas(None)); + account_metas.extend(self.mint_signer.to_account_metas(None)); + account_metas.extend(self.mint_authority.to_account_metas(None)); + account_metas.extend(self.compressed_token_program.to_account_metas(None)); + account_metas + .extend(self.compress_token_program_cpi_authority.to_account_metas(None)); + account_metas.extend(self.system_program.to_account_metas(None)); + account_metas.extend(self.config.to_account_metas(None)); + account_metas.extend(self.rent_recipient.to_account_metas(None)); + account_metas + } +} +#[automatically_derived] +impl<'info> anchor_lang::AccountsExit<'info> for CreateUserRecordAndGameSession<'info> +where + 'info: 'info, +{ + fn exit( + &self, + program_id: &anchor_lang::solana_program::pubkey::Pubkey, + ) -> anchor_lang::Result<()> { + anchor_lang::AccountsExit::exit(&self.user, program_id) + .map_err(|e| e.with_account_name("user"))?; + anchor_lang::AccountsExit::exit(&self.user_record, program_id) + .map_err(|e| e.with_account_name("user_record"))?; + anchor_lang::AccountsExit::exit(&self.game_session, program_id) + .map_err(|e| e.with_account_name("game_session"))?; + anchor_lang::AccountsExit::exit(&self.rent_recipient, program_id) + .map_err(|e| e.with_account_name("rent_recipient"))?; + Ok(()) + } +} +pub struct CreateUserRecordAndGameSessionBumps { + pub user_record: u8, + pub game_session: u8, +} +#[automatically_derived] +impl ::core::fmt::Debug for CreateUserRecordAndGameSessionBumps { + #[inline] + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + ::core::fmt::Formatter::debug_struct_field2_finish( + f, + "CreateUserRecordAndGameSessionBumps", + "user_record", + &self.user_record, + "game_session", + &&self.game_session, + ) + } +} +impl Default for CreateUserRecordAndGameSessionBumps { + fn default() -> Self { + CreateUserRecordAndGameSessionBumps { + user_record: u8::MAX, + game_session: u8::MAX, + } + } +} +impl<'info> anchor_lang::Bumps for CreateUserRecordAndGameSession<'info> +where + 'info: 'info, +{ + type Bumps = CreateUserRecordAndGameSessionBumps; +} +/// An internal, Anchor generated module. This is used (as an +/// implementation detail), to generate a struct for a given +/// `#[derive(Accounts)]` implementation, where each field is a Pubkey, +/// instead of an `AccountInfo`. This is useful for clients that want +/// to generate a list of accounts, without explicitly knowing the +/// order all the fields should be in. +/// +/// To access the struct in this module, one should use the sibling +/// `accounts` module (also generated), which re-exports this. +pub(crate) mod __client_accounts_create_user_record_and_game_session { + use super::*; + use anchor_lang::prelude::borsh; + /// Generated client accounts for [`CreateUserRecordAndGameSession`]. + pub struct CreateUserRecordAndGameSession { + pub user: Pubkey, + pub user_record: Pubkey, + pub game_session: Pubkey, + ///The mint signer used for PDA derivation + pub mint_signer: Pubkey, + ///The mint authority used for PDA derivation + pub mint_authority: Pubkey, + ///Compressed token program + pub compressed_token_program: Pubkey, + pub compress_token_program_cpi_authority: Pubkey, + ///Needs to be here for the init anchor macro to work. + pub system_program: Pubkey, + ///The global config account + pub config: Pubkey, + ///Rent recipient - must match config + pub rent_recipient: Pubkey, + } + impl borsh::ser::BorshSerialize for CreateUserRecordAndGameSession + where + Pubkey: borsh::ser::BorshSerialize, + Pubkey: borsh::ser::BorshSerialize, + Pubkey: borsh::ser::BorshSerialize, + Pubkey: borsh::ser::BorshSerialize, + Pubkey: borsh::ser::BorshSerialize, + Pubkey: borsh::ser::BorshSerialize, + Pubkey: borsh::ser::BorshSerialize, + Pubkey: borsh::ser::BorshSerialize, + Pubkey: borsh::ser::BorshSerialize, + Pubkey: borsh::ser::BorshSerialize, + { + fn serialize( + &self, + writer: &mut W, + ) -> ::core::result::Result<(), borsh::maybestd::io::Error> { + borsh::BorshSerialize::serialize(&self.user, writer)?; + borsh::BorshSerialize::serialize(&self.user_record, writer)?; + borsh::BorshSerialize::serialize(&self.game_session, writer)?; + borsh::BorshSerialize::serialize(&self.mint_signer, writer)?; + borsh::BorshSerialize::serialize(&self.mint_authority, writer)?; + borsh::BorshSerialize::serialize(&self.compressed_token_program, writer)?; + borsh::BorshSerialize::serialize( + &self.compress_token_program_cpi_authority, + writer, + )?; + borsh::BorshSerialize::serialize(&self.system_program, writer)?; + borsh::BorshSerialize::serialize(&self.config, writer)?; + borsh::BorshSerialize::serialize(&self.rent_recipient, writer)?; + Ok(()) + } + } + impl anchor_lang::idl::build::IdlBuild for CreateUserRecordAndGameSession { + fn create_type() -> Option { + Some(anchor_lang::idl::types::IdlTypeDef { + name: Self::get_full_path(), + docs: <[_]>::into_vec( + ::alloc::boxed::box_new([ + "Generated client accounts for [`CreateUserRecordAndGameSession`]." + .into(), + ]), + ), + serialization: anchor_lang::idl::types::IdlSerialization::default(), + repr: None, + generics: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlTypeDefTy::Struct { + fields: Some( + anchor_lang::idl::types::IdlDefinedFields::Named( + <[_]>::into_vec( + ::alloc::boxed::box_new([ + anchor_lang::idl::types::IdlField { + name: "user".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::Pubkey, + }, + anchor_lang::idl::types::IdlField { + name: "user_record".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::Pubkey, + }, + anchor_lang::idl::types::IdlField { + name: "game_session".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::Pubkey, + }, + anchor_lang::idl::types::IdlField { + name: "mint_signer".into(), + docs: <[_]>::into_vec( + ::alloc::boxed::box_new([ + "The mint signer used for PDA derivation".into(), + ]), + ), + ty: anchor_lang::idl::types::IdlType::Pubkey, + }, + anchor_lang::idl::types::IdlField { + name: "mint_authority".into(), + docs: <[_]>::into_vec( + ::alloc::boxed::box_new([ + "The mint authority used for PDA derivation".into(), + ]), + ), + ty: anchor_lang::idl::types::IdlType::Pubkey, + }, + anchor_lang::idl::types::IdlField { + name: "compressed_token_program".into(), + docs: <[_]>::into_vec( + ::alloc::boxed::box_new(["Compressed token program".into()]), + ), + ty: anchor_lang::idl::types::IdlType::Pubkey, + }, + anchor_lang::idl::types::IdlField { + name: "compress_token_program_cpi_authority".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::Pubkey, + }, + anchor_lang::idl::types::IdlField { + name: "system_program".into(), + docs: <[_]>::into_vec( + ::alloc::boxed::box_new([ + "Needs to be here for the init anchor macro to work.".into(), + ]), + ), + ty: anchor_lang::idl::types::IdlType::Pubkey, + }, + anchor_lang::idl::types::IdlField { + name: "config".into(), + docs: <[_]>::into_vec( + ::alloc::boxed::box_new([ + "The global config account".into(), + ]), + ), + ty: anchor_lang::idl::types::IdlType::Pubkey, + }, + anchor_lang::idl::types::IdlField { + name: "rent_recipient".into(), + docs: <[_]>::into_vec( + ::alloc::boxed::box_new([ + "Rent recipient - must match config".into(), + ]), + ), + ty: anchor_lang::idl::types::IdlType::Pubkey, + }, + ]), + ), + ), + ), + }, + }) + } + fn insert_types( + types: &mut std::collections::BTreeMap< + String, + anchor_lang::idl::types::IdlTypeDef, + >, + ) {} + fn get_full_path() -> String { + ::alloc::__export::must_use({ + let res = ::alloc::fmt::format( + format_args!( + "{0}::{1}", + "anchor_compressible_derived::__client_accounts_create_user_record_and_game_session", + "CreateUserRecordAndGameSession", + ), + ); + res + }) + } + } + #[automatically_derived] + impl anchor_lang::ToAccountMetas for CreateUserRecordAndGameSession { + fn to_account_metas( + &self, + is_signer: Option, + ) -> Vec { + let mut account_metas = ::alloc::vec::Vec::new(); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new( + self.user, + true, + ), + ); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new( + self.user_record, + false, + ), + ); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new( + self.game_session, + false, + ), + ); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new_readonly( + self.mint_signer, + true, + ), + ); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new_readonly( + self.mint_authority, + true, + ), + ); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new_readonly( + self.compressed_token_program, + false, + ), + ); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new_readonly( + self.compress_token_program_cpi_authority, + false, + ), + ); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new_readonly( + self.system_program, + false, + ), + ); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new_readonly( + self.config, + false, + ), + ); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new( + self.rent_recipient, + false, + ), + ); + account_metas + } + } +} +/// An internal, Anchor generated module. This is used (as an +/// implementation detail), to generate a CPI struct for a given +/// `#[derive(Accounts)]` implementation, where each field is an +/// AccountInfo. +/// +/// To access the struct in this module, one should use the sibling +/// [`cpi::accounts`] module (also generated), which re-exports this. +pub(crate) mod __cpi_client_accounts_create_user_record_and_game_session { + use super::*; + /// Generated CPI struct of the accounts for [`CreateUserRecordAndGameSession`]. + pub struct CreateUserRecordAndGameSession<'info> { + pub user: anchor_lang::solana_program::account_info::AccountInfo<'info>, + pub user_record: anchor_lang::solana_program::account_info::AccountInfo<'info>, + pub game_session: anchor_lang::solana_program::account_info::AccountInfo<'info>, + ///The mint signer used for PDA derivation + pub mint_signer: anchor_lang::solana_program::account_info::AccountInfo<'info>, + ///The mint authority used for PDA derivation + pub mint_authority: anchor_lang::solana_program::account_info::AccountInfo< + 'info, + >, + ///Compressed token program + pub compressed_token_program: anchor_lang::solana_program::account_info::AccountInfo< + 'info, + >, + pub compress_token_program_cpi_authority: anchor_lang::solana_program::account_info::AccountInfo< + 'info, + >, + ///Needs to be here for the init anchor macro to work. + pub system_program: anchor_lang::solana_program::account_info::AccountInfo< + 'info, + >, + ///The global config account + pub config: anchor_lang::solana_program::account_info::AccountInfo<'info>, + ///Rent recipient - must match config + pub rent_recipient: anchor_lang::solana_program::account_info::AccountInfo< + 'info, + >, + } + #[automatically_derived] + impl<'info> anchor_lang::ToAccountMetas for CreateUserRecordAndGameSession<'info> { + fn to_account_metas( + &self, + is_signer: Option, + ) -> Vec { + let mut account_metas = ::alloc::vec::Vec::new(); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new( + anchor_lang::Key::key(&self.user), + true, + ), + ); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new( + anchor_lang::Key::key(&self.user_record), + false, + ), + ); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new( + anchor_lang::Key::key(&self.game_session), + false, + ), + ); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new_readonly( + anchor_lang::Key::key(&self.mint_signer), + true, + ), + ); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new_readonly( + anchor_lang::Key::key(&self.mint_authority), + true, + ), + ); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new_readonly( + anchor_lang::Key::key(&self.compressed_token_program), + false, + ), + ); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new_readonly( + anchor_lang::Key::key( + &self.compress_token_program_cpi_authority, + ), + false, + ), + ); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new_readonly( + anchor_lang::Key::key(&self.system_program), + false, + ), + ); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new_readonly( + anchor_lang::Key::key(&self.config), + false, + ), + ); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new( + anchor_lang::Key::key(&self.rent_recipient), + false, + ), + ); + account_metas + } + } + #[automatically_derived] + impl<'info> anchor_lang::ToAccountInfos<'info> + for CreateUserRecordAndGameSession<'info> { + fn to_account_infos( + &self, + ) -> Vec> { + let mut account_infos = ::alloc::vec::Vec::new(); + account_infos + .extend(anchor_lang::ToAccountInfos::to_account_infos(&self.user)); + account_infos + .extend( + anchor_lang::ToAccountInfos::to_account_infos(&self.user_record), + ); + account_infos + .extend( + anchor_lang::ToAccountInfos::to_account_infos(&self.game_session), + ); + account_infos + .extend( + anchor_lang::ToAccountInfos::to_account_infos(&self.mint_signer), + ); + account_infos + .extend( + anchor_lang::ToAccountInfos::to_account_infos(&self.mint_authority), + ); + account_infos + .extend( + anchor_lang::ToAccountInfos::to_account_infos( + &self.compressed_token_program, + ), + ); + account_infos + .extend( + anchor_lang::ToAccountInfos::to_account_infos( + &self.compress_token_program_cpi_authority, + ), + ); + account_infos + .extend( + anchor_lang::ToAccountInfos::to_account_infos(&self.system_program), + ); + account_infos + .extend(anchor_lang::ToAccountInfos::to_account_infos(&self.config)); + account_infos + .extend( + anchor_lang::ToAccountInfos::to_account_infos(&self.rent_recipient), + ); + account_infos + } + } +} +impl<'info> CreateUserRecordAndGameSession<'info> { + pub fn __anchor_private_gen_idl_accounts( + accounts: &mut std::collections::BTreeMap< + String, + anchor_lang::idl::types::IdlAccount, + >, + types: &mut std::collections::BTreeMap< + String, + anchor_lang::idl::types::IdlTypeDef, + >, + ) -> Vec { + if let Some(ty) = ::create_type() { + let account = anchor_lang::idl::types::IdlAccount { + name: ty.name.clone(), + discriminator: UserRecord::DISCRIMINATOR.into(), + }; + accounts.insert(account.name.clone(), account); + types.insert(ty.name.clone(), ty); + ::insert_types(types); + } + if let Some(ty) = ::create_type() { + let account = anchor_lang::idl::types::IdlAccount { + name: ty.name.clone(), + discriminator: GameSession::DISCRIMINATOR.into(), + }; + accounts.insert(account.name.clone(), account); + types.insert(ty.name.clone(), ty); + ::insert_types(types); + } + <[_]>::into_vec( + ::alloc::boxed::box_new([ + anchor_lang::idl::types::IdlInstructionAccountItem::Single(anchor_lang::idl::types::IdlInstructionAccount { + name: "user".into(), + docs: ::alloc::vec::Vec::new(), + writable: true, + signer: true, + optional: false, + address: None, + pda: None, + relations: ::alloc::vec::Vec::new(), + }), + anchor_lang::idl::types::IdlInstructionAccountItem::Single(anchor_lang::idl::types::IdlInstructionAccount { + name: "user_record".into(), + docs: ::alloc::vec::Vec::new(), + writable: true, + signer: false, + optional: false, + address: None, + pda: None, + relations: ::alloc::vec::Vec::new(), + }), + anchor_lang::idl::types::IdlInstructionAccountItem::Single(anchor_lang::idl::types::IdlInstructionAccount { + name: "game_session".into(), + docs: ::alloc::vec::Vec::new(), + writable: true, + signer: false, + optional: false, + address: None, + pda: None, + relations: ::alloc::vec::Vec::new(), + }), + anchor_lang::idl::types::IdlInstructionAccountItem::Single(anchor_lang::idl::types::IdlInstructionAccount { + name: "mint_signer".into(), + docs: <[_]>::into_vec( + ::alloc::boxed::box_new([ + "The mint signer used for PDA derivation".into(), + ]), + ), + writable: false, + signer: true, + optional: false, + address: None, + pda: None, + relations: ::alloc::vec::Vec::new(), + }), + anchor_lang::idl::types::IdlInstructionAccountItem::Single(anchor_lang::idl::types::IdlInstructionAccount { + name: "mint_authority".into(), + docs: <[_]>::into_vec( + ::alloc::boxed::box_new([ + "The mint authority used for PDA derivation".into(), + ]), + ), + writable: false, + signer: true, + optional: false, + address: None, + pda: None, + relations: ::alloc::vec::Vec::new(), + }), + anchor_lang::idl::types::IdlInstructionAccountItem::Single(anchor_lang::idl::types::IdlInstructionAccount { + name: "compressed_token_program".into(), + docs: <[_]>::into_vec( + ::alloc::boxed::box_new(["Compressed token program".into()]), + ), + writable: false, + signer: false, + optional: false, + address: None, + pda: None, + relations: ::alloc::vec::Vec::new(), + }), + anchor_lang::idl::types::IdlInstructionAccountItem::Single(anchor_lang::idl::types::IdlInstructionAccount { + name: "compress_token_program_cpi_authority".into(), + docs: ::alloc::vec::Vec::new(), + writable: false, + signer: false, + optional: false, + address: None, + pda: None, + relations: ::alloc::vec::Vec::new(), + }), + anchor_lang::idl::types::IdlInstructionAccountItem::Single(anchor_lang::idl::types::IdlInstructionAccount { + name: "system_program".into(), + docs: <[_]>::into_vec( + ::alloc::boxed::box_new([ + "Needs to be here for the init anchor macro to work.".into(), + ]), + ), + writable: false, + signer: false, + optional: false, + address: None, + pda: None, + relations: ::alloc::vec::Vec::new(), + }), + anchor_lang::idl::types::IdlInstructionAccountItem::Single(anchor_lang::idl::types::IdlInstructionAccount { + name: "config".into(), + docs: <[_]>::into_vec( + ::alloc::boxed::box_new(["The global config account".into()]), + ), + writable: false, + signer: false, + optional: false, + address: None, + pda: None, + relations: ::alloc::vec::Vec::new(), + }), + anchor_lang::idl::types::IdlInstructionAccountItem::Single(anchor_lang::idl::types::IdlInstructionAccount { + name: "rent_recipient".into(), + docs: <[_]>::into_vec( + ::alloc::boxed::box_new([ + "Rent recipient - must match config".into(), + ]), + ), + writable: true, + signer: false, + optional: false, + address: None, + pda: None, + relations: ::alloc::vec::Vec::new(), + }), + ]), + ) + } +} +#[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, + seeds = [b"game_session", + session_id.to_le_bytes().as_ref()], + bump, + )] + pub game_session: Account<'info, GameSession>, + pub system_program: Program<'info, System>, + /// The global config account + /// CHECK: Config is validated by the SDK's load_checked method + pub config: AccountInfo<'info>, + /// Rent recipient - must match config + /// CHECK: Rent recipient is validated against the config + #[account(mut)] + pub rent_recipient: AccountInfo<'info>, +} +#[automatically_derived] +impl<'info> anchor_lang::Accounts<'info, CreateGameSessionBumps> +for CreateGameSession<'info> +where + 'info: 'info, +{ + #[inline(never)] + fn try_accounts( + __program_id: &anchor_lang::solana_program::pubkey::Pubkey, + __accounts: &mut &'info [anchor_lang::solana_program::account_info::AccountInfo< + 'info, + >], + __ix_data: &[u8], + __bumps: &mut CreateGameSessionBumps, + __reallocs: &mut std::collections::BTreeSet< + anchor_lang::solana_program::pubkey::Pubkey, + >, + ) -> anchor_lang::Result { + let mut __ix_data = __ix_data; + struct __Args { + session_id: u64, + } + impl borsh::ser::BorshSerialize for __Args + where + u64: borsh::ser::BorshSerialize, + { + fn serialize( + &self, + writer: &mut W, + ) -> ::core::result::Result<(), borsh::maybestd::io::Error> { + borsh::BorshSerialize::serialize(&self.session_id, writer)?; + Ok(()) + } + } + impl anchor_lang::idl::build::IdlBuild for __Args { + fn create_type() -> Option { + Some(anchor_lang::idl::types::IdlTypeDef { + name: Self::get_full_path(), + docs: ::alloc::vec::Vec::new(), + serialization: anchor_lang::idl::types::IdlSerialization::default(), + repr: None, + generics: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlTypeDefTy::Struct { + fields: Some( + anchor_lang::idl::types::IdlDefinedFields::Named( + <[_]>::into_vec( + ::alloc::boxed::box_new([ + anchor_lang::idl::types::IdlField { + name: "session_id".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::U64, + }, + ]), + ), + ), + ), + }, + }) + } + fn insert_types( + types: &mut std::collections::BTreeMap< + String, + anchor_lang::idl::types::IdlTypeDef, + >, + ) {} + fn get_full_path() -> String { + ::alloc::__export::must_use({ + let res = ::alloc::fmt::format( + format_args!("{0}::{1}", "anchor_compressible_derived", "__Args"), + ); + res + }) + } + } + impl borsh::de::BorshDeserialize for __Args + where + u64: borsh::BorshDeserialize, + { + fn deserialize_reader( + reader: &mut R, + ) -> ::core::result::Result { + Ok(Self { + session_id: borsh::BorshDeserialize::deserialize_reader(reader)?, + }) + } + } + let __Args { session_id } = __Args::deserialize(&mut __ix_data) + .map_err(|_| anchor_lang::error::ErrorCode::InstructionDidNotDeserialize)?; + let player: Signer = anchor_lang::Accounts::try_accounts( + __program_id, + __accounts, + __ix_data, + __bumps, + __reallocs, + ) + .map_err(|e| e.with_account_name("player"))?; + if __accounts.is_empty() { + return Err(anchor_lang::error::ErrorCode::AccountNotEnoughKeys.into()); + } + let game_session = &__accounts[0]; + *__accounts = &__accounts[1..]; + let system_program: anchor_lang::accounts::program::Program = anchor_lang::Accounts::try_accounts( + __program_id, + __accounts, + __ix_data, + __bumps, + __reallocs, + ) + .map_err(|e| e.with_account_name("system_program"))?; + let config: AccountInfo = anchor_lang::Accounts::try_accounts( + __program_id, + __accounts, + __ix_data, + __bumps, + __reallocs, + ) + .map_err(|e| e.with_account_name("config"))?; + let rent_recipient: AccountInfo = anchor_lang::Accounts::try_accounts( + __program_id, + __accounts, + __ix_data, + __bumps, + __reallocs, + ) + .map_err(|e| e.with_account_name("rent_recipient"))?; + let __anchor_rent = Rent::get()?; + let (__pda_address, __bump) = Pubkey::find_program_address( + &[b"game_session", session_id.to_le_bytes().as_ref()], + __program_id, + ); + __bumps.game_session = __bump; + if game_session.key() != __pda_address { + return Err( + anchor_lang::error::Error::from( + anchor_lang::error::ErrorCode::ConstraintSeeds, + ) + .with_account_name("game_session") + .with_pubkeys((game_session.key(), __pda_address)), + ); + } + let game_session = ({ + #[inline(never)] + || { + let actual_field = AsRef::::as_ref(&game_session); + let actual_owner = actual_field.owner; + let space = 8 + 9 + 8 + 32 + 4 + 32 + 8 + 9 + 8; + let pa: anchor_lang::accounts::account::Account = if !false + || actual_owner == &anchor_lang::solana_program::system_program::ID + { + let __current_lamports = game_session.lamports(); + if __current_lamports == 0 { + let space = space; + let lamports = __anchor_rent.minimum_balance(space); + let cpi_accounts = anchor_lang::system_program::CreateAccount { + from: player.to_account_info(), + to: game_session.to_account_info(), + }; + let cpi_context = anchor_lang::context::CpiContext::new( + system_program.to_account_info(), + cpi_accounts, + ); + anchor_lang::system_program::create_account( + cpi_context + .with_signer( + &[ + &[ + b"game_session", + session_id.to_le_bytes().as_ref(), + &[__bump][..], + ][..], + ], + ), + lamports, + space as u64, + __program_id, + )?; + } else { + if player.key() == game_session.key() { + return Err( + anchor_lang::error::Error::from(anchor_lang::error::AnchorError { + error_name: anchor_lang::error::ErrorCode::TryingToInitPayerAsProgramAccount + .name(), + error_code_number: anchor_lang::error::ErrorCode::TryingToInitPayerAsProgramAccount + .into(), + error_msg: anchor_lang::error::ErrorCode::TryingToInitPayerAsProgramAccount + .to_string(), + error_origin: Some( + anchor_lang::error::ErrorOrigin::Source(anchor_lang::error::Source { + filename: "sdk-tests/anchor-compressible-derived/src/lib.rs", + line: 1041u32, + }), + ), + compared_values: None, + }) + .with_pubkeys((player.key(), game_session.key())), + ); + } + let required_lamports = __anchor_rent + .minimum_balance(space) + .max(1) + .saturating_sub(__current_lamports); + if required_lamports > 0 { + let cpi_accounts = anchor_lang::system_program::Transfer { + from: player.to_account_info(), + to: game_session.to_account_info(), + }; + let cpi_context = anchor_lang::context::CpiContext::new( + system_program.to_account_info(), + cpi_accounts, + ); + anchor_lang::system_program::transfer( + cpi_context, + required_lamports, + )?; + } + let cpi_accounts = anchor_lang::system_program::Allocate { + account_to_allocate: game_session.to_account_info(), + }; + let cpi_context = anchor_lang::context::CpiContext::new( + system_program.to_account_info(), + cpi_accounts, + ); + anchor_lang::system_program::allocate( + cpi_context + .with_signer( + &[ + &[ + b"game_session", + session_id.to_le_bytes().as_ref(), + &[__bump][..], + ][..], + ], + ), + space as u64, + )?; + let cpi_accounts = anchor_lang::system_program::Assign { + account_to_assign: game_session.to_account_info(), + }; + let cpi_context = anchor_lang::context::CpiContext::new( + system_program.to_account_info(), + cpi_accounts, + ); + anchor_lang::system_program::assign( + cpi_context + .with_signer( + &[ + &[ + b"game_session", + session_id.to_le_bytes().as_ref(), + &[__bump][..], + ][..], + ], + ), + __program_id, + )?; + } + match anchor_lang::accounts::account::Account::try_from_unchecked( + &game_session, + ) { + Ok(val) => val, + Err(e) => return Err(e.with_account_name("game_session")), + } + } else { + match anchor_lang::accounts::account::Account::try_from( + &game_session, + ) { + Ok(val) => val, + Err(e) => return Err(e.with_account_name("game_session")), + } + }; + if false { + if space != actual_field.data_len() { + return Err( + anchor_lang::error::Error::from( + anchor_lang::error::ErrorCode::ConstraintSpace, + ) + .with_account_name("game_session") + .with_values((space, actual_field.data_len())), + ); + } + if actual_owner != __program_id { + return Err( + anchor_lang::error::Error::from( + anchor_lang::error::ErrorCode::ConstraintOwner, + ) + .with_account_name("game_session") + .with_pubkeys((*actual_owner, *__program_id)), + ); + } + { + let required_lamports = __anchor_rent.minimum_balance(space); + if pa.to_account_info().lamports() < required_lamports { + return Err( + anchor_lang::error::Error::from( + anchor_lang::error::ErrorCode::ConstraintRentExempt, + ) + .with_account_name("game_session"), + ); + } + } + } + Ok(pa) + } + })()?; + if !AsRef::::as_ref(&game_session).is_writable { + return Err( + anchor_lang::error::Error::from( + anchor_lang::error::ErrorCode::ConstraintMut, + ) + .with_account_name("game_session"), + ); + } + if !__anchor_rent + .is_exempt( + game_session.to_account_info().lamports(), + game_session.to_account_info().try_data_len()?, + ) + { + return Err( + anchor_lang::error::Error::from( + anchor_lang::error::ErrorCode::ConstraintRentExempt, + ) + .with_account_name("game_session"), + ); + } + if !AsRef::::as_ref(&player).is_writable { + return Err( + anchor_lang::error::Error::from( + anchor_lang::error::ErrorCode::ConstraintMut, + ) + .with_account_name("player"), + ); + } + if !&rent_recipient.is_writable { + return Err( + anchor_lang::error::Error::from( + anchor_lang::error::ErrorCode::ConstraintMut, + ) + .with_account_name("rent_recipient"), + ); + } + Ok(CreateGameSession { + player, + game_session, + system_program, + config, + rent_recipient, + }) + } +} +#[automatically_derived] +impl<'info> anchor_lang::ToAccountInfos<'info> for CreateGameSession<'info> +where + 'info: 'info, +{ + fn to_account_infos( + &self, + ) -> Vec> { + let mut account_infos = ::alloc::vec::Vec::new(); + account_infos.extend(self.player.to_account_infos()); + account_infos.extend(self.game_session.to_account_infos()); + account_infos.extend(self.system_program.to_account_infos()); + account_infos.extend(self.config.to_account_infos()); + account_infos.extend(self.rent_recipient.to_account_infos()); + account_infos + } +} +#[automatically_derived] +impl<'info> anchor_lang::ToAccountMetas for CreateGameSession<'info> { + fn to_account_metas( + &self, + is_signer: Option, + ) -> Vec { + let mut account_metas = ::alloc::vec::Vec::new(); + account_metas.extend(self.player.to_account_metas(None)); + account_metas.extend(self.game_session.to_account_metas(None)); + account_metas.extend(self.system_program.to_account_metas(None)); + account_metas.extend(self.config.to_account_metas(None)); + account_metas.extend(self.rent_recipient.to_account_metas(None)); + account_metas + } +} +#[automatically_derived] +impl<'info> anchor_lang::AccountsExit<'info> for CreateGameSession<'info> +where + 'info: 'info, +{ + fn exit( + &self, + program_id: &anchor_lang::solana_program::pubkey::Pubkey, + ) -> anchor_lang::Result<()> { + anchor_lang::AccountsExit::exit(&self.player, program_id) + .map_err(|e| e.with_account_name("player"))?; + anchor_lang::AccountsExit::exit(&self.game_session, program_id) + .map_err(|e| e.with_account_name("game_session"))?; + anchor_lang::AccountsExit::exit(&self.rent_recipient, program_id) + .map_err(|e| e.with_account_name("rent_recipient"))?; + Ok(()) + } +} +pub struct CreateGameSessionBumps { + pub game_session: u8, +} +#[automatically_derived] +impl ::core::fmt::Debug for CreateGameSessionBumps { + #[inline] + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + ::core::fmt::Formatter::debug_struct_field1_finish( + f, + "CreateGameSessionBumps", + "game_session", + &&self.game_session, + ) + } +} +impl Default for CreateGameSessionBumps { + fn default() -> Self { + CreateGameSessionBumps { + game_session: u8::MAX, + } + } +} +impl<'info> anchor_lang::Bumps for CreateGameSession<'info> +where + 'info: 'info, +{ + type Bumps = CreateGameSessionBumps; +} +/// An internal, Anchor generated module. This is used (as an +/// implementation detail), to generate a struct for a given +/// `#[derive(Accounts)]` implementation, where each field is a Pubkey, +/// instead of an `AccountInfo`. This is useful for clients that want +/// to generate a list of accounts, without explicitly knowing the +/// order all the fields should be in. +/// +/// To access the struct in this module, one should use the sibling +/// `accounts` module (also generated), which re-exports this. +pub(crate) mod __client_accounts_create_game_session { + use super::*; + use anchor_lang::prelude::borsh; + /// Generated client accounts for [`CreateGameSession`]. + pub struct CreateGameSession { + pub player: Pubkey, + pub game_session: Pubkey, + pub system_program: Pubkey, + ///The global config account + pub config: Pubkey, + ///Rent recipient - must match config + pub rent_recipient: Pubkey, + } + impl borsh::ser::BorshSerialize for CreateGameSession + where + Pubkey: borsh::ser::BorshSerialize, + Pubkey: borsh::ser::BorshSerialize, + Pubkey: borsh::ser::BorshSerialize, + Pubkey: borsh::ser::BorshSerialize, + Pubkey: borsh::ser::BorshSerialize, + { + fn serialize( + &self, + writer: &mut W, + ) -> ::core::result::Result<(), borsh::maybestd::io::Error> { + borsh::BorshSerialize::serialize(&self.player, writer)?; + borsh::BorshSerialize::serialize(&self.game_session, writer)?; + borsh::BorshSerialize::serialize(&self.system_program, writer)?; + borsh::BorshSerialize::serialize(&self.config, writer)?; + borsh::BorshSerialize::serialize(&self.rent_recipient, writer)?; + Ok(()) + } + } + impl anchor_lang::idl::build::IdlBuild for CreateGameSession { + fn create_type() -> Option { + Some(anchor_lang::idl::types::IdlTypeDef { + name: Self::get_full_path(), + docs: <[_]>::into_vec( + ::alloc::boxed::box_new([ + "Generated client accounts for [`CreateGameSession`].".into(), + ]), + ), + serialization: anchor_lang::idl::types::IdlSerialization::default(), + repr: None, + generics: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlTypeDefTy::Struct { + fields: Some( + anchor_lang::idl::types::IdlDefinedFields::Named( + <[_]>::into_vec( + ::alloc::boxed::box_new([ + anchor_lang::idl::types::IdlField { + name: "player".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::Pubkey, + }, + anchor_lang::idl::types::IdlField { + name: "game_session".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::Pubkey, + }, + anchor_lang::idl::types::IdlField { + name: "system_program".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::Pubkey, + }, + anchor_lang::idl::types::IdlField { + name: "config".into(), + docs: <[_]>::into_vec( + ::alloc::boxed::box_new([ + "The global config account".into(), + ]), + ), + ty: anchor_lang::idl::types::IdlType::Pubkey, + }, + anchor_lang::idl::types::IdlField { + name: "rent_recipient".into(), + docs: <[_]>::into_vec( + ::alloc::boxed::box_new([ + "Rent recipient - must match config".into(), + ]), + ), + ty: anchor_lang::idl::types::IdlType::Pubkey, + }, + ]), + ), + ), + ), + }, + }) + } + fn insert_types( + types: &mut std::collections::BTreeMap< + String, + anchor_lang::idl::types::IdlTypeDef, + >, + ) {} + fn get_full_path() -> String { + ::alloc::__export::must_use({ + let res = ::alloc::fmt::format( + format_args!( + "{0}::{1}", + "anchor_compressible_derived::__client_accounts_create_game_session", + "CreateGameSession", + ), + ); + res + }) + } + } + #[automatically_derived] + impl anchor_lang::ToAccountMetas for CreateGameSession { + fn to_account_metas( + &self, + is_signer: Option, + ) -> Vec { + let mut account_metas = ::alloc::vec::Vec::new(); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new( + self.player, + true, + ), + ); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new( + self.game_session, + false, + ), + ); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new_readonly( + self.system_program, + false, + ), + ); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new_readonly( + self.config, + false, + ), + ); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new( + self.rent_recipient, + false, + ), + ); + account_metas + } + } +} +/// An internal, Anchor generated module. This is used (as an +/// implementation detail), to generate a CPI struct for a given +/// `#[derive(Accounts)]` implementation, where each field is an +/// AccountInfo. +/// +/// To access the struct in this module, one should use the sibling +/// [`cpi::accounts`] module (also generated), which re-exports this. +pub(crate) mod __cpi_client_accounts_create_game_session { + use super::*; + /// Generated CPI struct of the accounts for [`CreateGameSession`]. + pub struct CreateGameSession<'info> { + pub player: anchor_lang::solana_program::account_info::AccountInfo<'info>, + pub game_session: anchor_lang::solana_program::account_info::AccountInfo<'info>, + pub system_program: anchor_lang::solana_program::account_info::AccountInfo< + 'info, + >, + ///The global config account + pub config: anchor_lang::solana_program::account_info::AccountInfo<'info>, + ///Rent recipient - must match config + pub rent_recipient: anchor_lang::solana_program::account_info::AccountInfo< + 'info, + >, + } + #[automatically_derived] + impl<'info> anchor_lang::ToAccountMetas for CreateGameSession<'info> { + fn to_account_metas( + &self, + is_signer: Option, + ) -> Vec { + let mut account_metas = ::alloc::vec::Vec::new(); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new( + anchor_lang::Key::key(&self.player), + true, + ), + ); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new( + anchor_lang::Key::key(&self.game_session), + false, + ), + ); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new_readonly( + anchor_lang::Key::key(&self.system_program), + false, + ), + ); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new_readonly( + anchor_lang::Key::key(&self.config), + false, + ), + ); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new( + anchor_lang::Key::key(&self.rent_recipient), + false, + ), + ); + account_metas + } + } + #[automatically_derived] + impl<'info> anchor_lang::ToAccountInfos<'info> for CreateGameSession<'info> { + fn to_account_infos( + &self, + ) -> Vec> { + let mut account_infos = ::alloc::vec::Vec::new(); + account_infos + .extend(anchor_lang::ToAccountInfos::to_account_infos(&self.player)); + account_infos + .extend( + anchor_lang::ToAccountInfos::to_account_infos(&self.game_session), + ); + account_infos + .extend( + anchor_lang::ToAccountInfos::to_account_infos(&self.system_program), + ); + account_infos + .extend(anchor_lang::ToAccountInfos::to_account_infos(&self.config)); + account_infos + .extend( + anchor_lang::ToAccountInfos::to_account_infos(&self.rent_recipient), + ); + account_infos + } + } +} +impl<'info> CreateGameSession<'info> { + pub fn __anchor_private_gen_idl_accounts( + accounts: &mut std::collections::BTreeMap< + String, + anchor_lang::idl::types::IdlAccount, + >, + types: &mut std::collections::BTreeMap< + String, + anchor_lang::idl::types::IdlTypeDef, + >, + ) -> Vec { + if let Some(ty) = ::create_type() { + let account = anchor_lang::idl::types::IdlAccount { + name: ty.name.clone(), + discriminator: GameSession::DISCRIMINATOR.into(), + }; + accounts.insert(account.name.clone(), account); + types.insert(ty.name.clone(), ty); + ::insert_types(types); + } + <[_]>::into_vec( + ::alloc::boxed::box_new([ + anchor_lang::idl::types::IdlInstructionAccountItem::Single(anchor_lang::idl::types::IdlInstructionAccount { + name: "player".into(), + docs: ::alloc::vec::Vec::new(), + writable: true, + signer: true, + optional: false, + address: None, + pda: None, + relations: ::alloc::vec::Vec::new(), + }), + anchor_lang::idl::types::IdlInstructionAccountItem::Single(anchor_lang::idl::types::IdlInstructionAccount { + name: "game_session".into(), + docs: ::alloc::vec::Vec::new(), + writable: true, + signer: false, + optional: false, + address: None, + pda: None, + relations: ::alloc::vec::Vec::new(), + }), + anchor_lang::idl::types::IdlInstructionAccountItem::Single(anchor_lang::idl::types::IdlInstructionAccount { + name: "system_program".into(), + docs: ::alloc::vec::Vec::new(), + writable: false, + signer: false, + optional: false, + address: None, + pda: None, + relations: ::alloc::vec::Vec::new(), + }), + anchor_lang::idl::types::IdlInstructionAccountItem::Single(anchor_lang::idl::types::IdlInstructionAccount { + name: "config".into(), + docs: <[_]>::into_vec( + ::alloc::boxed::box_new(["The global config account".into()]), + ), + writable: false, + signer: false, + optional: false, + address: None, + pda: None, + relations: ::alloc::vec::Vec::new(), + }), + anchor_lang::idl::types::IdlInstructionAccountItem::Single(anchor_lang::idl::types::IdlInstructionAccount { + name: "rent_recipient".into(), + docs: <[_]>::into_vec( + ::alloc::boxed::box_new([ + "Rent recipient - must match config".into(), + ]), + ), + writable: true, + signer: false, + optional: false, + address: None, + pda: None, + relations: ::alloc::vec::Vec::new(), + }), + ]), + ) + } +} +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>, +} +#[automatically_derived] +impl<'info> anchor_lang::Accounts<'info, UpdateRecordBumps> for UpdateRecord<'info> +where + 'info: 'info, +{ + #[inline(never)] + fn try_accounts( + __program_id: &anchor_lang::solana_program::pubkey::Pubkey, + __accounts: &mut &'info [anchor_lang::solana_program::account_info::AccountInfo< + 'info, + >], + __ix_data: &[u8], + __bumps: &mut UpdateRecordBumps, + __reallocs: &mut std::collections::BTreeSet< + anchor_lang::solana_program::pubkey::Pubkey, + >, + ) -> anchor_lang::Result { + let user: Signer = anchor_lang::Accounts::try_accounts( + __program_id, + __accounts, + __ix_data, + __bumps, + __reallocs, + ) + .map_err(|e| e.with_account_name("user"))?; + let user_record: anchor_lang::accounts::account::Account = anchor_lang::Accounts::try_accounts( + __program_id, + __accounts, + __ix_data, + __bumps, + __reallocs, + ) + .map_err(|e| e.with_account_name("user_record"))?; + if !AsRef::::as_ref(&user).is_writable { + return Err( + anchor_lang::error::Error::from( + anchor_lang::error::ErrorCode::ConstraintMut, + ) + .with_account_name("user"), + ); + } + let (__pda_address, __bump) = Pubkey::find_program_address( + &[b"user_record", user.key().as_ref()], + &__program_id, + ); + __bumps.user_record = __bump; + if user_record.key() != __pda_address { + return Err( + anchor_lang::error::Error::from( + anchor_lang::error::ErrorCode::ConstraintSeeds, + ) + .with_account_name("user_record") + .with_pubkeys((user_record.key(), __pda_address)), + ); + } + if !AsRef::::as_ref(&user_record).is_writable { + return Err( + anchor_lang::error::Error::from( + anchor_lang::error::ErrorCode::ConstraintMut, + ) + .with_account_name("user_record"), + ); + } + if !(user_record.owner == user.key()) { + return Err( + anchor_lang::error::Error::from( + anchor_lang::error::ErrorCode::ConstraintRaw, + ) + .with_account_name("user_record"), + ); + } + Ok(UpdateRecord { user, user_record }) + } +} +#[automatically_derived] +impl<'info> anchor_lang::ToAccountInfos<'info> for UpdateRecord<'info> +where + 'info: 'info, +{ + fn to_account_infos( + &self, + ) -> Vec> { + let mut account_infos = ::alloc::vec::Vec::new(); + account_infos.extend(self.user.to_account_infos()); + account_infos.extend(self.user_record.to_account_infos()); + account_infos + } +} +#[automatically_derived] +impl<'info> anchor_lang::ToAccountMetas for UpdateRecord<'info> { + fn to_account_metas( + &self, + is_signer: Option, + ) -> Vec { + let mut account_metas = ::alloc::vec::Vec::new(); + account_metas.extend(self.user.to_account_metas(None)); + account_metas.extend(self.user_record.to_account_metas(None)); + account_metas + } +} +#[automatically_derived] +impl<'info> anchor_lang::AccountsExit<'info> for UpdateRecord<'info> +where + 'info: 'info, +{ + fn exit( + &self, + program_id: &anchor_lang::solana_program::pubkey::Pubkey, + ) -> anchor_lang::Result<()> { + anchor_lang::AccountsExit::exit(&self.user, program_id) + .map_err(|e| e.with_account_name("user"))?; + anchor_lang::AccountsExit::exit(&self.user_record, program_id) + .map_err(|e| e.with_account_name("user_record"))?; + Ok(()) + } +} +pub struct UpdateRecordBumps { + pub user_record: u8, +} +#[automatically_derived] +impl ::core::fmt::Debug for UpdateRecordBumps { + #[inline] + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + ::core::fmt::Formatter::debug_struct_field1_finish( + f, + "UpdateRecordBumps", + "user_record", + &&self.user_record, + ) + } +} +impl Default for UpdateRecordBumps { + fn default() -> Self { + UpdateRecordBumps { + user_record: u8::MAX, + } + } +} +impl<'info> anchor_lang::Bumps for UpdateRecord<'info> +where + 'info: 'info, +{ + type Bumps = UpdateRecordBumps; +} +/// An internal, Anchor generated module. This is used (as an +/// implementation detail), to generate a struct for a given +/// `#[derive(Accounts)]` implementation, where each field is a Pubkey, +/// instead of an `AccountInfo`. This is useful for clients that want +/// to generate a list of accounts, without explicitly knowing the +/// order all the fields should be in. +/// +/// To access the struct in this module, one should use the sibling +/// `accounts` module (also generated), which re-exports this. +pub(crate) mod __client_accounts_update_record { + use super::*; + use anchor_lang::prelude::borsh; + /// Generated client accounts for [`UpdateRecord`]. + pub struct UpdateRecord { + pub user: Pubkey, + pub user_record: Pubkey, + } + impl borsh::ser::BorshSerialize for UpdateRecord + where + Pubkey: borsh::ser::BorshSerialize, + Pubkey: borsh::ser::BorshSerialize, + { + fn serialize( + &self, + writer: &mut W, + ) -> ::core::result::Result<(), borsh::maybestd::io::Error> { + borsh::BorshSerialize::serialize(&self.user, writer)?; + borsh::BorshSerialize::serialize(&self.user_record, writer)?; + Ok(()) + } + } + impl anchor_lang::idl::build::IdlBuild for UpdateRecord { + fn create_type() -> Option { + Some(anchor_lang::idl::types::IdlTypeDef { + name: Self::get_full_path(), + docs: <[_]>::into_vec( + ::alloc::boxed::box_new([ + "Generated client accounts for [`UpdateRecord`].".into(), + ]), + ), + serialization: anchor_lang::idl::types::IdlSerialization::default(), + repr: None, + generics: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlTypeDefTy::Struct { + fields: Some( + anchor_lang::idl::types::IdlDefinedFields::Named( + <[_]>::into_vec( + ::alloc::boxed::box_new([ + anchor_lang::idl::types::IdlField { + name: "user".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::Pubkey, + }, + anchor_lang::idl::types::IdlField { + name: "user_record".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::Pubkey, + }, + ]), + ), + ), + ), + }, + }) + } + fn insert_types( + types: &mut std::collections::BTreeMap< + String, + anchor_lang::idl::types::IdlTypeDef, + >, + ) {} + fn get_full_path() -> String { + ::alloc::__export::must_use({ + let res = ::alloc::fmt::format( + format_args!( + "{0}::{1}", + "anchor_compressible_derived::__client_accounts_update_record", + "UpdateRecord", + ), + ); + res + }) + } + } + #[automatically_derived] + impl anchor_lang::ToAccountMetas for UpdateRecord { + fn to_account_metas( + &self, + is_signer: Option, + ) -> Vec { + let mut account_metas = ::alloc::vec::Vec::new(); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new( + self.user, + true, + ), + ); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new( + self.user_record, + false, + ), + ); + account_metas + } + } +} +/// An internal, Anchor generated module. This is used (as an +/// implementation detail), to generate a CPI struct for a given +/// `#[derive(Accounts)]` implementation, where each field is an +/// AccountInfo. +/// +/// To access the struct in this module, one should use the sibling +/// [`cpi::accounts`] module (also generated), which re-exports this. +pub(crate) mod __cpi_client_accounts_update_record { + use super::*; + /// Generated CPI struct of the accounts for [`UpdateRecord`]. + pub struct UpdateRecord<'info> { + pub user: anchor_lang::solana_program::account_info::AccountInfo<'info>, + pub user_record: anchor_lang::solana_program::account_info::AccountInfo<'info>, + } + #[automatically_derived] + impl<'info> anchor_lang::ToAccountMetas for UpdateRecord<'info> { + fn to_account_metas( + &self, + is_signer: Option, + ) -> Vec { + let mut account_metas = ::alloc::vec::Vec::new(); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new( + anchor_lang::Key::key(&self.user), + true, + ), + ); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new( + anchor_lang::Key::key(&self.user_record), + false, + ), + ); + account_metas + } + } + #[automatically_derived] + impl<'info> anchor_lang::ToAccountInfos<'info> for UpdateRecord<'info> { + fn to_account_infos( + &self, + ) -> Vec> { + let mut account_infos = ::alloc::vec::Vec::new(); + account_infos + .extend(anchor_lang::ToAccountInfos::to_account_infos(&self.user)); + account_infos + .extend( + anchor_lang::ToAccountInfos::to_account_infos(&self.user_record), + ); + account_infos + } + } +} +impl<'info> UpdateRecord<'info> { + pub fn __anchor_private_gen_idl_accounts( + accounts: &mut std::collections::BTreeMap< + String, + anchor_lang::idl::types::IdlAccount, + >, + types: &mut std::collections::BTreeMap< + String, + anchor_lang::idl::types::IdlTypeDef, + >, + ) -> Vec { + if let Some(ty) = ::create_type() { + let account = anchor_lang::idl::types::IdlAccount { + name: ty.name.clone(), + discriminator: UserRecord::DISCRIMINATOR.into(), + }; + accounts.insert(account.name.clone(), account); + types.insert(ty.name.clone(), ty); + ::insert_types(types); + } + <[_]>::into_vec( + ::alloc::boxed::box_new([ + anchor_lang::idl::types::IdlInstructionAccountItem::Single(anchor_lang::idl::types::IdlInstructionAccount { + name: "user".into(), + docs: ::alloc::vec::Vec::new(), + writable: true, + signer: true, + optional: false, + address: None, + pda: None, + relations: ::alloc::vec::Vec::new(), + }), + anchor_lang::idl::types::IdlInstructionAccountItem::Single(anchor_lang::idl::types::IdlInstructionAccount { + name: "user_record".into(), + docs: ::alloc::vec::Vec::new(), + writable: true, + signer: false, + optional: false, + address: None, + pda: None, + relations: ::alloc::vec::Vec::new(), + }), + ]), + ) + } +} +#[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>, +} +#[automatically_derived] +impl<'info> anchor_lang::Accounts<'info, UpdateGameSessionBumps> +for UpdateGameSession<'info> +where + 'info: 'info, +{ + #[inline(never)] + fn try_accounts( + __program_id: &anchor_lang::solana_program::pubkey::Pubkey, + __accounts: &mut &'info [anchor_lang::solana_program::account_info::AccountInfo< + 'info, + >], + __ix_data: &[u8], + __bumps: &mut UpdateGameSessionBumps, + __reallocs: &mut std::collections::BTreeSet< + anchor_lang::solana_program::pubkey::Pubkey, + >, + ) -> anchor_lang::Result { + let mut __ix_data = __ix_data; + struct __Args { + session_id: u64, + } + impl borsh::ser::BorshSerialize for __Args + where + u64: borsh::ser::BorshSerialize, + { + fn serialize( + &self, + writer: &mut W, + ) -> ::core::result::Result<(), borsh::maybestd::io::Error> { + borsh::BorshSerialize::serialize(&self.session_id, writer)?; + Ok(()) + } + } + impl anchor_lang::idl::build::IdlBuild for __Args { + fn create_type() -> Option { + Some(anchor_lang::idl::types::IdlTypeDef { + name: Self::get_full_path(), + docs: ::alloc::vec::Vec::new(), + serialization: anchor_lang::idl::types::IdlSerialization::default(), + repr: None, + generics: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlTypeDefTy::Struct { + fields: Some( + anchor_lang::idl::types::IdlDefinedFields::Named( + <[_]>::into_vec( + ::alloc::boxed::box_new([ + anchor_lang::idl::types::IdlField { + name: "session_id".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::U64, + }, + ]), + ), + ), + ), + }, + }) + } + fn insert_types( + types: &mut std::collections::BTreeMap< + String, + anchor_lang::idl::types::IdlTypeDef, + >, + ) {} + fn get_full_path() -> String { + ::alloc::__export::must_use({ + let res = ::alloc::fmt::format( + format_args!("{0}::{1}", "anchor_compressible_derived", "__Args"), + ); + res + }) + } + } + impl borsh::de::BorshDeserialize for __Args + where + u64: borsh::BorshDeserialize, + { + fn deserialize_reader( + reader: &mut R, + ) -> ::core::result::Result { + Ok(Self { + session_id: borsh::BorshDeserialize::deserialize_reader(reader)?, + }) + } + } + let __Args { session_id } = __Args::deserialize(&mut __ix_data) + .map_err(|_| anchor_lang::error::ErrorCode::InstructionDidNotDeserialize)?; + let player: Signer = anchor_lang::Accounts::try_accounts( + __program_id, + __accounts, + __ix_data, + __bumps, + __reallocs, + ) + .map_err(|e| e.with_account_name("player"))?; + let game_session: anchor_lang::accounts::account::Account = anchor_lang::Accounts::try_accounts( + __program_id, + __accounts, + __ix_data, + __bumps, + __reallocs, + ) + .map_err(|e| e.with_account_name("game_session"))?; + if !AsRef::::as_ref(&player).is_writable { + return Err( + anchor_lang::error::Error::from( + anchor_lang::error::ErrorCode::ConstraintMut, + ) + .with_account_name("player"), + ); + } + let (__pda_address, __bump) = Pubkey::find_program_address( + &[b"game_session", session_id.to_le_bytes().as_ref()], + &__program_id, + ); + __bumps.game_session = __bump; + if game_session.key() != __pda_address { + return Err( + anchor_lang::error::Error::from( + anchor_lang::error::ErrorCode::ConstraintSeeds, + ) + .with_account_name("game_session") + .with_pubkeys((game_session.key(), __pda_address)), + ); + } + if !AsRef::::as_ref(&game_session).is_writable { + return Err( + anchor_lang::error::Error::from( + anchor_lang::error::ErrorCode::ConstraintMut, + ) + .with_account_name("game_session"), + ); + } + if !(game_session.player == player.key()) { + return Err( + anchor_lang::error::Error::from( + anchor_lang::error::ErrorCode::ConstraintRaw, + ) + .with_account_name("game_session"), + ); + } + Ok(UpdateGameSession { + player, + game_session, + }) + } +} +#[automatically_derived] +impl<'info> anchor_lang::ToAccountInfos<'info> for UpdateGameSession<'info> +where + 'info: 'info, +{ + fn to_account_infos( + &self, + ) -> Vec> { + let mut account_infos = ::alloc::vec::Vec::new(); + account_infos.extend(self.player.to_account_infos()); + account_infos.extend(self.game_session.to_account_infos()); + account_infos + } +} +#[automatically_derived] +impl<'info> anchor_lang::ToAccountMetas for UpdateGameSession<'info> { + fn to_account_metas( + &self, + is_signer: Option, + ) -> Vec { + let mut account_metas = ::alloc::vec::Vec::new(); + account_metas.extend(self.player.to_account_metas(None)); + account_metas.extend(self.game_session.to_account_metas(None)); + account_metas + } +} +#[automatically_derived] +impl<'info> anchor_lang::AccountsExit<'info> for UpdateGameSession<'info> +where + 'info: 'info, +{ + fn exit( + &self, + program_id: &anchor_lang::solana_program::pubkey::Pubkey, + ) -> anchor_lang::Result<()> { + anchor_lang::AccountsExit::exit(&self.player, program_id) + .map_err(|e| e.with_account_name("player"))?; + anchor_lang::AccountsExit::exit(&self.game_session, program_id) + .map_err(|e| e.with_account_name("game_session"))?; + Ok(()) + } +} +pub struct UpdateGameSessionBumps { + pub game_session: u8, +} +#[automatically_derived] +impl ::core::fmt::Debug for UpdateGameSessionBumps { + #[inline] + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + ::core::fmt::Formatter::debug_struct_field1_finish( + f, + "UpdateGameSessionBumps", + "game_session", + &&self.game_session, + ) + } +} +impl Default for UpdateGameSessionBumps { + fn default() -> Self { + UpdateGameSessionBumps { + game_session: u8::MAX, + } + } +} +impl<'info> anchor_lang::Bumps for UpdateGameSession<'info> +where + 'info: 'info, +{ + type Bumps = UpdateGameSessionBumps; +} +/// An internal, Anchor generated module. This is used (as an +/// implementation detail), to generate a struct for a given +/// `#[derive(Accounts)]` implementation, where each field is a Pubkey, +/// instead of an `AccountInfo`. This is useful for clients that want +/// to generate a list of accounts, without explicitly knowing the +/// order all the fields should be in. +/// +/// To access the struct in this module, one should use the sibling +/// `accounts` module (also generated), which re-exports this. +pub(crate) mod __client_accounts_update_game_session { + use super::*; + use anchor_lang::prelude::borsh; + /// Generated client accounts for [`UpdateGameSession`]. + pub struct UpdateGameSession { + pub player: Pubkey, + pub game_session: Pubkey, + } + impl borsh::ser::BorshSerialize for UpdateGameSession + where + Pubkey: borsh::ser::BorshSerialize, + Pubkey: borsh::ser::BorshSerialize, + { + fn serialize( + &self, + writer: &mut W, + ) -> ::core::result::Result<(), borsh::maybestd::io::Error> { + borsh::BorshSerialize::serialize(&self.player, writer)?; + borsh::BorshSerialize::serialize(&self.game_session, writer)?; + Ok(()) + } + } + impl anchor_lang::idl::build::IdlBuild for UpdateGameSession { + fn create_type() -> Option { + Some(anchor_lang::idl::types::IdlTypeDef { + name: Self::get_full_path(), + docs: <[_]>::into_vec( + ::alloc::boxed::box_new([ + "Generated client accounts for [`UpdateGameSession`].".into(), + ]), + ), + serialization: anchor_lang::idl::types::IdlSerialization::default(), + repr: None, + generics: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlTypeDefTy::Struct { + fields: Some( + anchor_lang::idl::types::IdlDefinedFields::Named( + <[_]>::into_vec( + ::alloc::boxed::box_new([ + anchor_lang::idl::types::IdlField { + name: "player".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::Pubkey, + }, + anchor_lang::idl::types::IdlField { + name: "game_session".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::Pubkey, + }, + ]), + ), + ), + ), + }, + }) + } + fn insert_types( + types: &mut std::collections::BTreeMap< + String, + anchor_lang::idl::types::IdlTypeDef, + >, + ) {} + fn get_full_path() -> String { + ::alloc::__export::must_use({ + let res = ::alloc::fmt::format( + format_args!( + "{0}::{1}", + "anchor_compressible_derived::__client_accounts_update_game_session", + "UpdateGameSession", + ), + ); + res + }) + } + } + #[automatically_derived] + impl anchor_lang::ToAccountMetas for UpdateGameSession { + fn to_account_metas( + &self, + is_signer: Option, + ) -> Vec { + let mut account_metas = ::alloc::vec::Vec::new(); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new( + self.player, + true, + ), + ); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new( + self.game_session, + false, + ), + ); + account_metas + } + } +} +/// An internal, Anchor generated module. This is used (as an +/// implementation detail), to generate a CPI struct for a given +/// `#[derive(Accounts)]` implementation, where each field is an +/// AccountInfo. +/// +/// To access the struct in this module, one should use the sibling +/// [`cpi::accounts`] module (also generated), which re-exports this. +pub(crate) mod __cpi_client_accounts_update_game_session { + use super::*; + /// Generated CPI struct of the accounts for [`UpdateGameSession`]. + pub struct UpdateGameSession<'info> { + pub player: anchor_lang::solana_program::account_info::AccountInfo<'info>, + pub game_session: anchor_lang::solana_program::account_info::AccountInfo<'info>, + } + #[automatically_derived] + impl<'info> anchor_lang::ToAccountMetas for UpdateGameSession<'info> { + fn to_account_metas( + &self, + is_signer: Option, + ) -> Vec { + let mut account_metas = ::alloc::vec::Vec::new(); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new( + anchor_lang::Key::key(&self.player), + true, + ), + ); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new( + anchor_lang::Key::key(&self.game_session), + false, + ), + ); + account_metas + } + } + #[automatically_derived] + impl<'info> anchor_lang::ToAccountInfos<'info> for UpdateGameSession<'info> { + fn to_account_infos( + &self, + ) -> Vec> { + let mut account_infos = ::alloc::vec::Vec::new(); + account_infos + .extend(anchor_lang::ToAccountInfos::to_account_infos(&self.player)); + account_infos + .extend( + anchor_lang::ToAccountInfos::to_account_infos(&self.game_session), + ); + account_infos + } + } +} +impl<'info> UpdateGameSession<'info> { + pub fn __anchor_private_gen_idl_accounts( + accounts: &mut std::collections::BTreeMap< + String, + anchor_lang::idl::types::IdlAccount, + >, + types: &mut std::collections::BTreeMap< + String, + anchor_lang::idl::types::IdlTypeDef, + >, + ) -> Vec { + if let Some(ty) = ::create_type() { + let account = anchor_lang::idl::types::IdlAccount { + name: ty.name.clone(), + discriminator: GameSession::DISCRIMINATOR.into(), + }; + accounts.insert(account.name.clone(), account); + types.insert(ty.name.clone(), ty); + ::insert_types(types); + } + <[_]>::into_vec( + ::alloc::boxed::box_new([ + anchor_lang::idl::types::IdlInstructionAccountItem::Single(anchor_lang::idl::types::IdlInstructionAccount { + name: "player".into(), + docs: ::alloc::vec::Vec::new(), + writable: true, + signer: true, + optional: false, + address: None, + pda: None, + relations: ::alloc::vec::Vec::new(), + }), + anchor_lang::idl::types::IdlInstructionAccountItem::Single(anchor_lang::idl::types::IdlInstructionAccount { + name: "game_session".into(), + docs: ::alloc::vec::Vec::new(), + writable: true, + signer: false, + optional: false, + address: None, + pda: None, + relations: ::alloc::vec::Vec::new(), + }), + ]), + ) + } +} +pub struct CompressAccountsIdempotent<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + /// The global config account + /// CHECK: Config is validated by the SDK's load_checked method + pub config: AccountInfo<'info>, + /// Rent recipient - must match config + /// CHECK: Rent recipient is validated against the config + #[account(mut)] + pub rent_recipient: AccountInfo<'info>, + /// CHECK: compression_authority must be the rent_authority defined when creating the token account. + #[account(mut)] + pub token_compression_authority: AccountInfo<'info>, + /// Compressed token program + /// CHECK: Program ID validated to be cTokenmWW8bLPjZEBAUgYy3zKxQZW6VKi7bqNFEVv3m + pub compressed_token_program: Option>, + /// CPI authority PDA of the compressed token program + /// CHECK: PDA derivation validated with seeds ["cpi_authority"] and bump 254 + pub compressed_token_cpi_authority: Option>, +} +#[automatically_derived] +impl<'info> anchor_lang::Accounts<'info, CompressAccountsIdempotentBumps> +for CompressAccountsIdempotent<'info> +where + 'info: 'info, +{ + #[inline(never)] + fn try_accounts( + __program_id: &anchor_lang::solana_program::pubkey::Pubkey, + __accounts: &mut &'info [anchor_lang::solana_program::account_info::AccountInfo< + 'info, + >], + __ix_data: &[u8], + __bumps: &mut CompressAccountsIdempotentBumps, + __reallocs: &mut std::collections::BTreeSet< + anchor_lang::solana_program::pubkey::Pubkey, + >, + ) -> anchor_lang::Result { + let fee_payer: Signer = anchor_lang::Accounts::try_accounts( + __program_id, + __accounts, + __ix_data, + __bumps, + __reallocs, + ) + .map_err(|e| e.with_account_name("fee_payer"))?; + let config: AccountInfo = anchor_lang::Accounts::try_accounts( + __program_id, + __accounts, + __ix_data, + __bumps, + __reallocs, + ) + .map_err(|e| e.with_account_name("config"))?; + let rent_recipient: AccountInfo = anchor_lang::Accounts::try_accounts( + __program_id, + __accounts, + __ix_data, + __bumps, + __reallocs, + ) + .map_err(|e| e.with_account_name("rent_recipient"))?; + let token_compression_authority: AccountInfo = anchor_lang::Accounts::try_accounts( + __program_id, + __accounts, + __ix_data, + __bumps, + __reallocs, + ) + .map_err(|e| e.with_account_name("token_compression_authority"))?; + let compressed_token_program: Option = anchor_lang::Accounts::try_accounts( + __program_id, + __accounts, + __ix_data, + __bumps, + __reallocs, + ) + .map_err(|e| e.with_account_name("compressed_token_program"))?; + let compressed_token_cpi_authority: Option = anchor_lang::Accounts::try_accounts( + __program_id, + __accounts, + __ix_data, + __bumps, + __reallocs, + ) + .map_err(|e| e.with_account_name("compressed_token_cpi_authority"))?; + if !AsRef::::as_ref(&fee_payer).is_writable { + return Err( + anchor_lang::error::Error::from( + anchor_lang::error::ErrorCode::ConstraintMut, + ) + .with_account_name("fee_payer"), + ); + } + if !&rent_recipient.is_writable { + return Err( + anchor_lang::error::Error::from( + anchor_lang::error::ErrorCode::ConstraintMut, + ) + .with_account_name("rent_recipient"), + ); + } + if !&token_compression_authority.is_writable { + return Err( + anchor_lang::error::Error::from( + anchor_lang::error::ErrorCode::ConstraintMut, + ) + .with_account_name("token_compression_authority"), + ); + } + Ok(CompressAccountsIdempotent { + fee_payer, + config, + rent_recipient, + token_compression_authority, + compressed_token_program, + compressed_token_cpi_authority, + }) + } +} +#[automatically_derived] +impl<'info> anchor_lang::ToAccountInfos<'info> for CompressAccountsIdempotent<'info> +where + 'info: 'info, +{ + fn to_account_infos( + &self, + ) -> Vec> { + let mut account_infos = ::alloc::vec::Vec::new(); + account_infos.extend(self.fee_payer.to_account_infos()); + account_infos.extend(self.config.to_account_infos()); + account_infos.extend(self.rent_recipient.to_account_infos()); + account_infos.extend(self.token_compression_authority.to_account_infos()); + account_infos.extend(self.compressed_token_program.to_account_infos()); + account_infos.extend(self.compressed_token_cpi_authority.to_account_infos()); + account_infos + } +} +#[automatically_derived] +impl<'info> anchor_lang::ToAccountMetas for CompressAccountsIdempotent<'info> { + fn to_account_metas( + &self, + is_signer: Option, + ) -> Vec { + let mut account_metas = ::alloc::vec::Vec::new(); + account_metas.extend(self.fee_payer.to_account_metas(None)); + account_metas.extend(self.config.to_account_metas(None)); + account_metas.extend(self.rent_recipient.to_account_metas(None)); + account_metas.extend(self.token_compression_authority.to_account_metas(None)); + if let Some(compressed_token_program) = &self.compressed_token_program { + account_metas.extend(compressed_token_program.to_account_metas(None)); + } else { + account_metas.push(AccountMeta::new_readonly(crate::ID, false)); + } + if let Some(compressed_token_cpi_authority) = &self + .compressed_token_cpi_authority + { + account_metas.extend(compressed_token_cpi_authority.to_account_metas(None)); + } else { + account_metas.push(AccountMeta::new_readonly(crate::ID, false)); + } + account_metas + } +} +#[automatically_derived] +impl<'info> anchor_lang::AccountsExit<'info> for CompressAccountsIdempotent<'info> +where + 'info: 'info, +{ + fn exit( + &self, + program_id: &anchor_lang::solana_program::pubkey::Pubkey, + ) -> anchor_lang::Result<()> { + anchor_lang::AccountsExit::exit(&self.fee_payer, program_id) + .map_err(|e| e.with_account_name("fee_payer"))?; + anchor_lang::AccountsExit::exit(&self.rent_recipient, program_id) + .map_err(|e| e.with_account_name("rent_recipient"))?; + anchor_lang::AccountsExit::exit(&self.token_compression_authority, program_id) + .map_err(|e| e.with_account_name("token_compression_authority"))?; + Ok(()) + } +} +pub struct CompressAccountsIdempotentBumps {} +#[automatically_derived] +impl ::core::fmt::Debug for CompressAccountsIdempotentBumps { + #[inline] + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + ::core::fmt::Formatter::write_str(f, "CompressAccountsIdempotentBumps") + } +} +impl Default for CompressAccountsIdempotentBumps { + fn default() -> Self { + CompressAccountsIdempotentBumps {} + } +} +impl<'info> anchor_lang::Bumps for CompressAccountsIdempotent<'info> +where + 'info: 'info, +{ + type Bumps = CompressAccountsIdempotentBumps; +} +/// An internal, Anchor generated module. This is used (as an +/// implementation detail), to generate a struct for a given +/// `#[derive(Accounts)]` implementation, where each field is a Pubkey, +/// instead of an `AccountInfo`. This is useful for clients that want +/// to generate a list of accounts, without explicitly knowing the +/// order all the fields should be in. +/// +/// To access the struct in this module, one should use the sibling +/// `accounts` module (also generated), which re-exports this. +pub(crate) mod __client_accounts_compress_accounts_idempotent { + use super::*; + use anchor_lang::prelude::borsh; + /// Generated client accounts for [`CompressAccountsIdempotent`]. + pub struct CompressAccountsIdempotent { + pub fee_payer: Pubkey, + ///The global config account + pub config: Pubkey, + ///Rent recipient - must match config + pub rent_recipient: Pubkey, + pub token_compression_authority: Pubkey, + ///Compressed token program + pub compressed_token_program: Option, + ///CPI authority PDA of the compressed token program + pub compressed_token_cpi_authority: Option, + } + impl borsh::ser::BorshSerialize for CompressAccountsIdempotent + where + Pubkey: borsh::ser::BorshSerialize, + Pubkey: borsh::ser::BorshSerialize, + Pubkey: borsh::ser::BorshSerialize, + Pubkey: borsh::ser::BorshSerialize, + Option: borsh::ser::BorshSerialize, + Option: borsh::ser::BorshSerialize, + { + fn serialize( + &self, + writer: &mut W, + ) -> ::core::result::Result<(), borsh::maybestd::io::Error> { + borsh::BorshSerialize::serialize(&self.fee_payer, writer)?; + borsh::BorshSerialize::serialize(&self.config, writer)?; + borsh::BorshSerialize::serialize(&self.rent_recipient, writer)?; + borsh::BorshSerialize::serialize(&self.token_compression_authority, writer)?; + borsh::BorshSerialize::serialize(&self.compressed_token_program, writer)?; + borsh::BorshSerialize::serialize( + &self.compressed_token_cpi_authority, + writer, + )?; + Ok(()) + } + } + impl anchor_lang::idl::build::IdlBuild for CompressAccountsIdempotent { + fn create_type() -> Option { + Some(anchor_lang::idl::types::IdlTypeDef { + name: Self::get_full_path(), + docs: <[_]>::into_vec( + ::alloc::boxed::box_new([ + "Generated client accounts for [`CompressAccountsIdempotent`]." + .into(), + ]), + ), + serialization: anchor_lang::idl::types::IdlSerialization::default(), + repr: None, + generics: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlTypeDefTy::Struct { + fields: Some( + anchor_lang::idl::types::IdlDefinedFields::Named( + <[_]>::into_vec( + ::alloc::boxed::box_new([ + anchor_lang::idl::types::IdlField { + name: "fee_payer".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::Pubkey, + }, + anchor_lang::idl::types::IdlField { + name: "config".into(), + docs: <[_]>::into_vec( + ::alloc::boxed::box_new([ + "The global config account".into(), + ]), + ), + ty: anchor_lang::idl::types::IdlType::Pubkey, + }, + anchor_lang::idl::types::IdlField { + name: "rent_recipient".into(), + docs: <[_]>::into_vec( + ::alloc::boxed::box_new([ + "Rent recipient - must match config".into(), + ]), + ), + ty: anchor_lang::idl::types::IdlType::Pubkey, + }, + anchor_lang::idl::types::IdlField { + name: "token_compression_authority".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::Pubkey, + }, + anchor_lang::idl::types::IdlField { + name: "compressed_token_program".into(), + docs: <[_]>::into_vec( + ::alloc::boxed::box_new(["Compressed token program".into()]), + ), + ty: anchor_lang::idl::types::IdlType::Option( + Box::new(anchor_lang::idl::types::IdlType::Pubkey), + ), + }, + anchor_lang::idl::types::IdlField { + name: "compressed_token_cpi_authority".into(), + docs: <[_]>::into_vec( + ::alloc::boxed::box_new([ + "CPI authority PDA of the compressed token program".into(), + ]), + ), + ty: anchor_lang::idl::types::IdlType::Option( + Box::new(anchor_lang::idl::types::IdlType::Pubkey), + ), + }, + ]), + ), + ), + ), + }, + }) + } + fn insert_types( + types: &mut std::collections::BTreeMap< + String, + anchor_lang::idl::types::IdlTypeDef, + >, + ) {} + fn get_full_path() -> String { + ::alloc::__export::must_use({ + let res = ::alloc::fmt::format( + format_args!( + "{0}::{1}", + "anchor_compressible_derived::__client_accounts_compress_accounts_idempotent", + "CompressAccountsIdempotent", + ), + ); + res + }) + } + } + #[automatically_derived] + impl anchor_lang::ToAccountMetas for CompressAccountsIdempotent { + fn to_account_metas( + &self, + is_signer: Option, + ) -> Vec { + let mut account_metas = ::alloc::vec::Vec::new(); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new( + self.fee_payer, + true, + ), + ); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new_readonly( + self.config, + false, + ), + ); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new( + self.rent_recipient, + false, + ), + ); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new( + self.token_compression_authority, + false, + ), + ); + if let Some(compressed_token_program) = &self.compressed_token_program { + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new_readonly( + *compressed_token_program, + false, + ), + ); + } else { + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new_readonly( + crate::ID, + false, + ), + ); + } + if let Some(compressed_token_cpi_authority) = &self + .compressed_token_cpi_authority + { + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new_readonly( + *compressed_token_cpi_authority, + false, + ), + ); + } else { + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new_readonly( + crate::ID, + false, + ), + ); + } + account_metas + } + } +} +/// An internal, Anchor generated module. This is used (as an +/// implementation detail), to generate a CPI struct for a given +/// `#[derive(Accounts)]` implementation, where each field is an +/// AccountInfo. +/// +/// To access the struct in this module, one should use the sibling +/// [`cpi::accounts`] module (also generated), which re-exports this. +pub(crate) mod __cpi_client_accounts_compress_accounts_idempotent { + use super::*; + /// Generated CPI struct of the accounts for [`CompressAccountsIdempotent`]. + pub struct CompressAccountsIdempotent<'info> { + pub fee_payer: anchor_lang::solana_program::account_info::AccountInfo<'info>, + ///The global config account + pub config: anchor_lang::solana_program::account_info::AccountInfo<'info>, + ///Rent recipient - must match config + pub rent_recipient: anchor_lang::solana_program::account_info::AccountInfo< + 'info, + >, + pub token_compression_authority: anchor_lang::solana_program::account_info::AccountInfo< + 'info, + >, + ///Compressed token program + pub compressed_token_program: Option< + anchor_lang::solana_program::account_info::AccountInfo<'info>, + >, + ///CPI authority PDA of the compressed token program + pub compressed_token_cpi_authority: Option< + anchor_lang::solana_program::account_info::AccountInfo<'info>, + >, + } + #[automatically_derived] + impl<'info> anchor_lang::ToAccountMetas for CompressAccountsIdempotent<'info> { + fn to_account_metas( + &self, + is_signer: Option, + ) -> Vec { + let mut account_metas = ::alloc::vec::Vec::new(); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new( + anchor_lang::Key::key(&self.fee_payer), + true, + ), + ); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new_readonly( + anchor_lang::Key::key(&self.config), + false, + ), + ); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new( + anchor_lang::Key::key(&self.rent_recipient), + false, + ), + ); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new( + anchor_lang::Key::key(&self.token_compression_authority), + false, + ), + ); + if let Some(compressed_token_program) = &self.compressed_token_program { + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new_readonly( + anchor_lang::Key::key(compressed_token_program), + false, + ), + ); + } else { + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new_readonly( + crate::ID, + false, + ), + ); + } + if let Some(compressed_token_cpi_authority) = &self + .compressed_token_cpi_authority + { + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new_readonly( + anchor_lang::Key::key(compressed_token_cpi_authority), + false, + ), + ); + } else { + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new_readonly( + crate::ID, + false, + ), + ); + } + account_metas + } + } + #[automatically_derived] + impl<'info> anchor_lang::ToAccountInfos<'info> + for CompressAccountsIdempotent<'info> { + fn to_account_infos( + &self, + ) -> Vec> { + let mut account_infos = ::alloc::vec::Vec::new(); + account_infos + .extend(anchor_lang::ToAccountInfos::to_account_infos(&self.fee_payer)); + account_infos + .extend(anchor_lang::ToAccountInfos::to_account_infos(&self.config)); + account_infos + .extend( + anchor_lang::ToAccountInfos::to_account_infos(&self.rent_recipient), + ); + account_infos + .extend( + anchor_lang::ToAccountInfos::to_account_infos( + &self.token_compression_authority, + ), + ); + account_infos + .extend( + anchor_lang::ToAccountInfos::to_account_infos( + &self.compressed_token_program, + ), + ); + account_infos + .extend( + anchor_lang::ToAccountInfos::to_account_infos( + &self.compressed_token_cpi_authority, + ), + ); + account_infos + } + } +} +impl<'info> CompressAccountsIdempotent<'info> { + pub fn __anchor_private_gen_idl_accounts( + accounts: &mut std::collections::BTreeMap< + String, + anchor_lang::idl::types::IdlAccount, + >, + types: &mut std::collections::BTreeMap< + String, + anchor_lang::idl::types::IdlTypeDef, + >, + ) -> Vec { + <[_]>::into_vec( + ::alloc::boxed::box_new([ + anchor_lang::idl::types::IdlInstructionAccountItem::Single(anchor_lang::idl::types::IdlInstructionAccount { + name: "fee_payer".into(), + docs: ::alloc::vec::Vec::new(), + writable: true, + signer: true, + optional: false, + address: None, + pda: None, + relations: ::alloc::vec::Vec::new(), + }), + anchor_lang::idl::types::IdlInstructionAccountItem::Single(anchor_lang::idl::types::IdlInstructionAccount { + name: "config".into(), + docs: <[_]>::into_vec( + ::alloc::boxed::box_new(["The global config account".into()]), + ), + writable: false, + signer: false, + optional: false, + address: None, + pda: None, + relations: ::alloc::vec::Vec::new(), + }), + anchor_lang::idl::types::IdlInstructionAccountItem::Single(anchor_lang::idl::types::IdlInstructionAccount { + name: "rent_recipient".into(), + docs: <[_]>::into_vec( + ::alloc::boxed::box_new([ + "Rent recipient - must match config".into(), + ]), + ), + writable: true, + signer: false, + optional: false, + address: None, + pda: None, + relations: ::alloc::vec::Vec::new(), + }), + anchor_lang::idl::types::IdlInstructionAccountItem::Single(anchor_lang::idl::types::IdlInstructionAccount { + name: "token_compression_authority".into(), + docs: ::alloc::vec::Vec::new(), + writable: true, + signer: false, + optional: false, + address: None, + pda: None, + relations: ::alloc::vec::Vec::new(), + }), + anchor_lang::idl::types::IdlInstructionAccountItem::Single(anchor_lang::idl::types::IdlInstructionAccount { + name: "compressed_token_program".into(), + docs: <[_]>::into_vec( + ::alloc::boxed::box_new(["Compressed token program".into()]), + ), + writable: false, + signer: false, + optional: true, + address: None, + pda: None, + relations: ::alloc::vec::Vec::new(), + }), + anchor_lang::idl::types::IdlInstructionAccountItem::Single(anchor_lang::idl::types::IdlInstructionAccount { + name: "compressed_token_cpi_authority".into(), + docs: <[_]>::into_vec( + ::alloc::boxed::box_new([ + "CPI authority PDA of the compressed token program".into(), + ]), + ), + writable: false, + signer: false, + optional: true, + address: None, + pda: None, + relations: ::alloc::vec::Vec::new(), + }), + ]), + ) + } +} +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>, +} +#[automatically_derived] +impl<'info> anchor_lang::Accounts<'info, InitializeCompressionConfigBumps> +for InitializeCompressionConfig<'info> +where + 'info: 'info, +{ + #[inline(never)] + fn try_accounts( + __program_id: &anchor_lang::solana_program::pubkey::Pubkey, + __accounts: &mut &'info [anchor_lang::solana_program::account_info::AccountInfo< + 'info, + >], + __ix_data: &[u8], + __bumps: &mut InitializeCompressionConfigBumps, + __reallocs: &mut std::collections::BTreeSet< + anchor_lang::solana_program::pubkey::Pubkey, + >, + ) -> anchor_lang::Result { + let payer: Signer = anchor_lang::Accounts::try_accounts( + __program_id, + __accounts, + __ix_data, + __bumps, + __reallocs, + ) + .map_err(|e| e.with_account_name("payer"))?; + let config: AccountInfo = anchor_lang::Accounts::try_accounts( + __program_id, + __accounts, + __ix_data, + __bumps, + __reallocs, + ) + .map_err(|e| e.with_account_name("config"))?; + let program_data: AccountInfo = anchor_lang::Accounts::try_accounts( + __program_id, + __accounts, + __ix_data, + __bumps, + __reallocs, + ) + .map_err(|e| e.with_account_name("program_data"))?; + let authority: Signer = anchor_lang::Accounts::try_accounts( + __program_id, + __accounts, + __ix_data, + __bumps, + __reallocs, + ) + .map_err(|e| e.with_account_name("authority"))?; + let system_program: anchor_lang::accounts::program::Program = anchor_lang::Accounts::try_accounts( + __program_id, + __accounts, + __ix_data, + __bumps, + __reallocs, + ) + .map_err(|e| e.with_account_name("system_program"))?; + if !AsRef::::as_ref(&payer).is_writable { + return Err( + anchor_lang::error::Error::from( + anchor_lang::error::ErrorCode::ConstraintMut, + ) + .with_account_name("payer"), + ); + } + if !&config.is_writable { + return Err( + anchor_lang::error::Error::from( + anchor_lang::error::ErrorCode::ConstraintMut, + ) + .with_account_name("config"), + ); + } + Ok(InitializeCompressionConfig { + payer, + config, + program_data, + authority, + system_program, + }) + } +} +#[automatically_derived] +impl<'info> anchor_lang::ToAccountInfos<'info> for InitializeCompressionConfig<'info> +where + 'info: 'info, +{ + fn to_account_infos( + &self, + ) -> Vec> { + let mut account_infos = ::alloc::vec::Vec::new(); + account_infos.extend(self.payer.to_account_infos()); + account_infos.extend(self.config.to_account_infos()); + account_infos.extend(self.program_data.to_account_infos()); + account_infos.extend(self.authority.to_account_infos()); + account_infos.extend(self.system_program.to_account_infos()); + account_infos + } +} +#[automatically_derived] +impl<'info> anchor_lang::ToAccountMetas for InitializeCompressionConfig<'info> { + fn to_account_metas( + &self, + is_signer: Option, + ) -> Vec { + let mut account_metas = ::alloc::vec::Vec::new(); + account_metas.extend(self.payer.to_account_metas(None)); + account_metas.extend(self.config.to_account_metas(None)); + account_metas.extend(self.program_data.to_account_metas(None)); + account_metas.extend(self.authority.to_account_metas(None)); + account_metas.extend(self.system_program.to_account_metas(None)); + account_metas + } +} +#[automatically_derived] +impl<'info> anchor_lang::AccountsExit<'info> for InitializeCompressionConfig<'info> +where + 'info: 'info, +{ + fn exit( + &self, + program_id: &anchor_lang::solana_program::pubkey::Pubkey, + ) -> anchor_lang::Result<()> { + anchor_lang::AccountsExit::exit(&self.payer, program_id) + .map_err(|e| e.with_account_name("payer"))?; + anchor_lang::AccountsExit::exit(&self.config, program_id) + .map_err(|e| e.with_account_name("config"))?; + Ok(()) + } +} +pub struct InitializeCompressionConfigBumps {} +#[automatically_derived] +impl ::core::fmt::Debug for InitializeCompressionConfigBumps { + #[inline] + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + ::core::fmt::Formatter::write_str(f, "InitializeCompressionConfigBumps") + } +} +impl Default for InitializeCompressionConfigBumps { + fn default() -> Self { + InitializeCompressionConfigBumps { + } + } +} +impl<'info> anchor_lang::Bumps for InitializeCompressionConfig<'info> +where + 'info: 'info, +{ + type Bumps = InitializeCompressionConfigBumps; +} +/// An internal, Anchor generated module. This is used (as an +/// implementation detail), to generate a struct for a given +/// `#[derive(Accounts)]` implementation, where each field is a Pubkey, +/// instead of an `AccountInfo`. This is useful for clients that want +/// to generate a list of accounts, without explicitly knowing the +/// order all the fields should be in. +/// +/// To access the struct in this module, one should use the sibling +/// `accounts` module (also generated), which re-exports this. +pub(crate) mod __client_accounts_initialize_compression_config { + use super::*; + use anchor_lang::prelude::borsh; + /// Generated client accounts for [`InitializeCompressionConfig`]. + pub struct InitializeCompressionConfig { + pub payer: Pubkey, + pub config: Pubkey, + ///The program's data account + pub program_data: Pubkey, + ///The program's upgrade authority (must sign) + pub authority: Pubkey, + pub system_program: Pubkey, + } + impl borsh::ser::BorshSerialize for InitializeCompressionConfig + where + Pubkey: borsh::ser::BorshSerialize, + Pubkey: borsh::ser::BorshSerialize, + Pubkey: borsh::ser::BorshSerialize, + Pubkey: borsh::ser::BorshSerialize, + Pubkey: borsh::ser::BorshSerialize, + { + fn serialize( + &self, + writer: &mut W, + ) -> ::core::result::Result<(), borsh::maybestd::io::Error> { + borsh::BorshSerialize::serialize(&self.payer, writer)?; + borsh::BorshSerialize::serialize(&self.config, writer)?; + borsh::BorshSerialize::serialize(&self.program_data, writer)?; + borsh::BorshSerialize::serialize(&self.authority, writer)?; + borsh::BorshSerialize::serialize(&self.system_program, writer)?; + Ok(()) + } + } + impl anchor_lang::idl::build::IdlBuild for InitializeCompressionConfig { + fn create_type() -> Option { + Some(anchor_lang::idl::types::IdlTypeDef { + name: Self::get_full_path(), + docs: <[_]>::into_vec( + ::alloc::boxed::box_new([ + "Generated client accounts for [`InitializeCompressionConfig`]." + .into(), + ]), + ), + serialization: anchor_lang::idl::types::IdlSerialization::default(), + repr: None, + generics: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlTypeDefTy::Struct { + fields: Some( + anchor_lang::idl::types::IdlDefinedFields::Named( + <[_]>::into_vec( + ::alloc::boxed::box_new([ + anchor_lang::idl::types::IdlField { + name: "payer".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::Pubkey, + }, + anchor_lang::idl::types::IdlField { + name: "config".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::Pubkey, + }, + anchor_lang::idl::types::IdlField { + name: "program_data".into(), + docs: <[_]>::into_vec( + ::alloc::boxed::box_new([ + "The program's data account".into(), + ]), + ), + ty: anchor_lang::idl::types::IdlType::Pubkey, + }, + anchor_lang::idl::types::IdlField { + name: "authority".into(), + docs: <[_]>::into_vec( + ::alloc::boxed::box_new([ + "The program's upgrade authority (must sign)".into(), + ]), + ), + ty: anchor_lang::idl::types::IdlType::Pubkey, + }, + anchor_lang::idl::types::IdlField { + name: "system_program".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::Pubkey, + }, + ]), + ), + ), + ), + }, + }) + } + fn insert_types( + types: &mut std::collections::BTreeMap< + String, + anchor_lang::idl::types::IdlTypeDef, + >, + ) {} + fn get_full_path() -> String { + ::alloc::__export::must_use({ + let res = ::alloc::fmt::format( + format_args!( + "{0}::{1}", + "anchor_compressible_derived::__client_accounts_initialize_compression_config", + "InitializeCompressionConfig", + ), + ); + res + }) + } + } + #[automatically_derived] + impl anchor_lang::ToAccountMetas for InitializeCompressionConfig { + fn to_account_metas( + &self, + is_signer: Option, + ) -> Vec { + let mut account_metas = ::alloc::vec::Vec::new(); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new( + self.payer, + true, + ), + ); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new( + self.config, + false, + ), + ); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new_readonly( + self.program_data, + false, + ), + ); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new_readonly( + self.authority, + true, + ), + ); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new_readonly( + self.system_program, + false, + ), + ); + account_metas + } + } +} +/// An internal, Anchor generated module. This is used (as an +/// implementation detail), to generate a CPI struct for a given +/// `#[derive(Accounts)]` implementation, where each field is an +/// AccountInfo. +/// +/// To access the struct in this module, one should use the sibling +/// [`cpi::accounts`] module (also generated), which re-exports this. +pub(crate) mod __cpi_client_accounts_initialize_compression_config { + use super::*; + /// Generated CPI struct of the accounts for [`InitializeCompressionConfig`]. + pub struct InitializeCompressionConfig<'info> { + pub payer: anchor_lang::solana_program::account_info::AccountInfo<'info>, + pub config: anchor_lang::solana_program::account_info::AccountInfo<'info>, + ///The program's data account + pub program_data: anchor_lang::solana_program::account_info::AccountInfo<'info>, + ///The program's upgrade authority (must sign) + pub authority: anchor_lang::solana_program::account_info::AccountInfo<'info>, + pub system_program: anchor_lang::solana_program::account_info::AccountInfo< + 'info, + >, + } + #[automatically_derived] + impl<'info> anchor_lang::ToAccountMetas for InitializeCompressionConfig<'info> { + fn to_account_metas( + &self, + is_signer: Option, + ) -> Vec { + let mut account_metas = ::alloc::vec::Vec::new(); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new( + anchor_lang::Key::key(&self.payer), + true, + ), + ); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new( + anchor_lang::Key::key(&self.config), + false, + ), + ); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new_readonly( + anchor_lang::Key::key(&self.program_data), + false, + ), + ); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new_readonly( + anchor_lang::Key::key(&self.authority), + true, + ), + ); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new_readonly( + anchor_lang::Key::key(&self.system_program), + false, + ), + ); + account_metas + } + } + #[automatically_derived] + impl<'info> anchor_lang::ToAccountInfos<'info> + for InitializeCompressionConfig<'info> { + fn to_account_infos( + &self, + ) -> Vec> { + let mut account_infos = ::alloc::vec::Vec::new(); + account_infos + .extend(anchor_lang::ToAccountInfos::to_account_infos(&self.payer)); + account_infos + .extend(anchor_lang::ToAccountInfos::to_account_infos(&self.config)); + account_infos + .extend( + anchor_lang::ToAccountInfos::to_account_infos(&self.program_data), + ); + account_infos + .extend(anchor_lang::ToAccountInfos::to_account_infos(&self.authority)); + account_infos + .extend( + anchor_lang::ToAccountInfos::to_account_infos(&self.system_program), + ); + account_infos + } + } +} +impl<'info> InitializeCompressionConfig<'info> { + pub fn __anchor_private_gen_idl_accounts( + accounts: &mut std::collections::BTreeMap< + String, + anchor_lang::idl::types::IdlAccount, + >, + types: &mut std::collections::BTreeMap< + String, + anchor_lang::idl::types::IdlTypeDef, + >, + ) -> Vec { + <[_]>::into_vec( + ::alloc::boxed::box_new([ + anchor_lang::idl::types::IdlInstructionAccountItem::Single(anchor_lang::idl::types::IdlInstructionAccount { + name: "payer".into(), + docs: ::alloc::vec::Vec::new(), + writable: true, + signer: true, + optional: false, + address: None, + pda: None, + relations: ::alloc::vec::Vec::new(), + }), + anchor_lang::idl::types::IdlInstructionAccountItem::Single(anchor_lang::idl::types::IdlInstructionAccount { + name: "config".into(), + docs: ::alloc::vec::Vec::new(), + writable: true, + signer: false, + optional: false, + address: None, + pda: None, + relations: ::alloc::vec::Vec::new(), + }), + anchor_lang::idl::types::IdlInstructionAccountItem::Single(anchor_lang::idl::types::IdlInstructionAccount { + name: "program_data".into(), + docs: <[_]>::into_vec( + ::alloc::boxed::box_new(["The program's data account".into()]), + ), + writable: false, + signer: false, + optional: false, + address: None, + pda: None, + relations: ::alloc::vec::Vec::new(), + }), + anchor_lang::idl::types::IdlInstructionAccountItem::Single(anchor_lang::idl::types::IdlInstructionAccount { + name: "authority".into(), + docs: <[_]>::into_vec( + ::alloc::boxed::box_new([ + "The program's upgrade authority (must sign)".into(), + ]), + ), + writable: false, + signer: true, + optional: false, + address: None, + pda: None, + relations: ::alloc::vec::Vec::new(), + }), + anchor_lang::idl::types::IdlInstructionAccountItem::Single(anchor_lang::idl::types::IdlInstructionAccount { + name: "system_program".into(), + docs: ::alloc::vec::Vec::new(), + writable: false, + signer: false, + optional: false, + address: None, + pda: None, + relations: ::alloc::vec::Vec::new(), + }), + ]), + ) + } +} +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>, +} +#[automatically_derived] +impl<'info> anchor_lang::Accounts<'info, UpdateCompressionConfigBumps> +for UpdateCompressionConfig<'info> +where + 'info: 'info, +{ + #[inline(never)] + fn try_accounts( + __program_id: &anchor_lang::solana_program::pubkey::Pubkey, + __accounts: &mut &'info [anchor_lang::solana_program::account_info::AccountInfo< + 'info, + >], + __ix_data: &[u8], + __bumps: &mut UpdateCompressionConfigBumps, + __reallocs: &mut std::collections::BTreeSet< + anchor_lang::solana_program::pubkey::Pubkey, + >, + ) -> anchor_lang::Result { + let config: AccountInfo = anchor_lang::Accounts::try_accounts( + __program_id, + __accounts, + __ix_data, + __bumps, + __reallocs, + ) + .map_err(|e| e.with_account_name("config"))?; + let authority: Signer = anchor_lang::Accounts::try_accounts( + __program_id, + __accounts, + __ix_data, + __bumps, + __reallocs, + ) + .map_err(|e| e.with_account_name("authority"))?; + if !&config.is_writable { + return Err( + anchor_lang::error::Error::from( + anchor_lang::error::ErrorCode::ConstraintMut, + ) + .with_account_name("config"), + ); + } + Ok(UpdateCompressionConfig { + config, + authority, + }) + } +} +#[automatically_derived] +impl<'info> anchor_lang::ToAccountInfos<'info> for UpdateCompressionConfig<'info> +where + 'info: 'info, +{ + fn to_account_infos( + &self, + ) -> Vec> { + let mut account_infos = ::alloc::vec::Vec::new(); + account_infos.extend(self.config.to_account_infos()); + account_infos.extend(self.authority.to_account_infos()); + account_infos + } +} +#[automatically_derived] +impl<'info> anchor_lang::ToAccountMetas for UpdateCompressionConfig<'info> { + fn to_account_metas( + &self, + is_signer: Option, + ) -> Vec { + let mut account_metas = ::alloc::vec::Vec::new(); + account_metas.extend(self.config.to_account_metas(None)); + account_metas.extend(self.authority.to_account_metas(None)); + account_metas + } +} +#[automatically_derived] +impl<'info> anchor_lang::AccountsExit<'info> for UpdateCompressionConfig<'info> +where + 'info: 'info, +{ + fn exit( + &self, + program_id: &anchor_lang::solana_program::pubkey::Pubkey, + ) -> anchor_lang::Result<()> { + anchor_lang::AccountsExit::exit(&self.config, program_id) + .map_err(|e| e.with_account_name("config"))?; + Ok(()) + } +} +pub struct UpdateCompressionConfigBumps {} +#[automatically_derived] +impl ::core::fmt::Debug for UpdateCompressionConfigBumps { + #[inline] + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + ::core::fmt::Formatter::write_str(f, "UpdateCompressionConfigBumps") + } +} +impl Default for UpdateCompressionConfigBumps { + fn default() -> Self { + UpdateCompressionConfigBumps {} + } +} +impl<'info> anchor_lang::Bumps for UpdateCompressionConfig<'info> +where + 'info: 'info, +{ + type Bumps = UpdateCompressionConfigBumps; +} +/// An internal, Anchor generated module. This is used (as an +/// implementation detail), to generate a struct for a given +/// `#[derive(Accounts)]` implementation, where each field is a Pubkey, +/// instead of an `AccountInfo`. This is useful for clients that want +/// to generate a list of accounts, without explicitly knowing the +/// order all the fields should be in. +/// +/// To access the struct in this module, one should use the sibling +/// `accounts` module (also generated), which re-exports this. +pub(crate) mod __client_accounts_update_compression_config { + use super::*; + use anchor_lang::prelude::borsh; + /// Generated client accounts for [`UpdateCompressionConfig`]. + pub struct UpdateCompressionConfig { + pub config: Pubkey, + ///Must match the update authority stored in config + pub authority: Pubkey, + } + impl borsh::ser::BorshSerialize for UpdateCompressionConfig + where + Pubkey: borsh::ser::BorshSerialize, + Pubkey: borsh::ser::BorshSerialize, + { + fn serialize( + &self, + writer: &mut W, + ) -> ::core::result::Result<(), borsh::maybestd::io::Error> { + borsh::BorshSerialize::serialize(&self.config, writer)?; + borsh::BorshSerialize::serialize(&self.authority, writer)?; + Ok(()) + } + } + impl anchor_lang::idl::build::IdlBuild for UpdateCompressionConfig { + fn create_type() -> Option { + Some(anchor_lang::idl::types::IdlTypeDef { + name: Self::get_full_path(), + docs: <[_]>::into_vec( + ::alloc::boxed::box_new([ + "Generated client accounts for [`UpdateCompressionConfig`]." + .into(), + ]), + ), + serialization: anchor_lang::idl::types::IdlSerialization::default(), + repr: None, + generics: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlTypeDefTy::Struct { + fields: Some( + anchor_lang::idl::types::IdlDefinedFields::Named( + <[_]>::into_vec( + ::alloc::boxed::box_new([ + anchor_lang::idl::types::IdlField { + name: "config".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::Pubkey, + }, + anchor_lang::idl::types::IdlField { + name: "authority".into(), + docs: <[_]>::into_vec( + ::alloc::boxed::box_new([ + "Must match the update authority stored in config".into(), + ]), + ), + ty: anchor_lang::idl::types::IdlType::Pubkey, + }, + ]), + ), + ), + ), + }, + }) + } + fn insert_types( + types: &mut std::collections::BTreeMap< + String, + anchor_lang::idl::types::IdlTypeDef, + >, + ) {} + fn get_full_path() -> String { + ::alloc::__export::must_use({ + let res = ::alloc::fmt::format( + format_args!( + "{0}::{1}", + "anchor_compressible_derived::__client_accounts_update_compression_config", + "UpdateCompressionConfig", + ), + ); + res + }) + } + } + #[automatically_derived] + impl anchor_lang::ToAccountMetas for UpdateCompressionConfig { + fn to_account_metas( + &self, + is_signer: Option, + ) -> Vec { + let mut account_metas = ::alloc::vec::Vec::new(); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new( + self.config, + false, + ), + ); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new_readonly( + self.authority, + true, + ), + ); + account_metas + } + } +} +/// An internal, Anchor generated module. This is used (as an +/// implementation detail), to generate a CPI struct for a given +/// `#[derive(Accounts)]` implementation, where each field is an +/// AccountInfo. +/// +/// To access the struct in this module, one should use the sibling +/// [`cpi::accounts`] module (also generated), which re-exports this. +pub(crate) mod __cpi_client_accounts_update_compression_config { + use super::*; + /// Generated CPI struct of the accounts for [`UpdateCompressionConfig`]. + pub struct UpdateCompressionConfig<'info> { + pub config: anchor_lang::solana_program::account_info::AccountInfo<'info>, + ///Must match the update authority stored in config + pub authority: anchor_lang::solana_program::account_info::AccountInfo<'info>, + } + #[automatically_derived] + impl<'info> anchor_lang::ToAccountMetas for UpdateCompressionConfig<'info> { + fn to_account_metas( + &self, + is_signer: Option, + ) -> Vec { + let mut account_metas = ::alloc::vec::Vec::new(); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new( + anchor_lang::Key::key(&self.config), + false, + ), + ); + account_metas + .push( + anchor_lang::solana_program::instruction::AccountMeta::new_readonly( + anchor_lang::Key::key(&self.authority), + true, + ), + ); + account_metas + } + } + #[automatically_derived] + impl<'info> anchor_lang::ToAccountInfos<'info> for UpdateCompressionConfig<'info> { + fn to_account_infos( + &self, + ) -> Vec> { + let mut account_infos = ::alloc::vec::Vec::new(); + account_infos + .extend(anchor_lang::ToAccountInfos::to_account_infos(&self.config)); + account_infos + .extend(anchor_lang::ToAccountInfos::to_account_infos(&self.authority)); + account_infos + } + } +} +impl<'info> UpdateCompressionConfig<'info> { + pub fn __anchor_private_gen_idl_accounts( + accounts: &mut std::collections::BTreeMap< + String, + anchor_lang::idl::types::IdlAccount, + >, + types: &mut std::collections::BTreeMap< + String, + anchor_lang::idl::types::IdlTypeDef, + >, + ) -> Vec { + <[_]>::into_vec( + ::alloc::boxed::box_new([ + anchor_lang::idl::types::IdlInstructionAccountItem::Single(anchor_lang::idl::types::IdlInstructionAccount { + name: "config".into(), + docs: ::alloc::vec::Vec::new(), + writable: true, + signer: false, + optional: false, + address: None, + pda: None, + relations: ::alloc::vec::Vec::new(), + }), + anchor_lang::idl::types::IdlInstructionAccountItem::Single(anchor_lang::idl::types::IdlInstructionAccount { + name: "authority".into(), + docs: <[_]>::into_vec( + ::alloc::boxed::box_new([ + "Must match the update authority stored in config".into(), + ]), + ), + writable: false, + signer: true, + optional: false, + address: None, + pda: None, + relations: ::alloc::vec::Vec::new(), + }), + ]), + ) + } +} +#[repr(u32)] +pub enum ErrorCode { + InvalidAccountCount, + InvalidRentRecipient, + MintCreationFailed, + MissingCompressedTokenProgram, + MissingCompressedTokenProgramAuthorityPDA, + CTokenDecompressionNotImplemented, +} +#[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::CTokenDecompressionNotImplemented => { + "CTokenDecompressionNotImplemented" + } + }, + ) + } +} +#[automatically_derived] +impl ::core::clone::Clone for ErrorCode { + #[inline] + fn clone(&self) -> ErrorCode { + *self + } +} +#[automatically_derived] +impl ::core::marker::Copy for ErrorCode {} +impl ErrorCode { + /// Gets the name of this [#enum_name]. + pub fn name(&self) -> String { + match self { + ErrorCode::InvalidAccountCount => "InvalidAccountCount".to_string(), + ErrorCode::InvalidRentRecipient => "InvalidRentRecipient".to_string(), + ErrorCode::MintCreationFailed => "MintCreationFailed".to_string(), + ErrorCode::MissingCompressedTokenProgram => { + "MissingCompressedTokenProgram".to_string() + } + ErrorCode::MissingCompressedTokenProgramAuthorityPDA => { + "MissingCompressedTokenProgramAuthorityPDA".to_string() + } + ErrorCode::CTokenDecompressionNotImplemented => { + "CTokenDecompressionNotImplemented".to_string() + } + } + } +} +impl From for u32 { + fn from(e: ErrorCode) -> u32 { + e as u32 + anchor_lang::error::ERROR_CODE_OFFSET + } +} +impl From for anchor_lang::error::Error { + fn from(error_code: ErrorCode) -> anchor_lang::error::Error { + anchor_lang::error::Error::from(anchor_lang::error::AnchorError { + error_name: error_code.name(), + error_code_number: error_code.into(), + error_msg: error_code.to_string(), + error_origin: None, + compared_values: None, + }) + } +} +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::CTokenDecompressionNotImplemented => { + fmt.write_fmt(format_args!("CToken decompression not yet implemented")) + } + } + } +} +pub struct AccountCreationData { + pub user_name: String, + pub session_id: u64, + pub game_type: String, + 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>, +} +impl borsh::ser::BorshSerialize for AccountCreationData +where + String: borsh::ser::BorshSerialize, + u64: borsh::ser::BorshSerialize, + String: borsh::ser::BorshSerialize, + String: borsh::ser::BorshSerialize, + String: borsh::ser::BorshSerialize, + String: borsh::ser::BorshSerialize, + u8: borsh::ser::BorshSerialize, + u64: borsh::ser::BorshSerialize, + Option: borsh::ser::BorshSerialize, + Option: borsh::ser::BorshSerialize, + Option>: borsh::ser::BorshSerialize, +{ + fn serialize( + &self, + writer: &mut W, + ) -> ::core::result::Result<(), borsh::maybestd::io::Error> { + borsh::BorshSerialize::serialize(&self.user_name, writer)?; + borsh::BorshSerialize::serialize(&self.session_id, writer)?; + borsh::BorshSerialize::serialize(&self.game_type, writer)?; + borsh::BorshSerialize::serialize(&self.mint_name, writer)?; + borsh::BorshSerialize::serialize(&self.mint_symbol, writer)?; + borsh::BorshSerialize::serialize(&self.mint_uri, writer)?; + borsh::BorshSerialize::serialize(&self.mint_decimals, writer)?; + borsh::BorshSerialize::serialize(&self.mint_supply, writer)?; + borsh::BorshSerialize::serialize(&self.mint_update_authority, writer)?; + borsh::BorshSerialize::serialize(&self.mint_freeze_authority, writer)?; + borsh::BorshSerialize::serialize(&self.additional_metadata, writer)?; + Ok(()) + } +} +impl anchor_lang::idl::build::IdlBuild for AccountCreationData { + fn create_type() -> Option { + None + } + fn insert_types( + types: &mut std::collections::BTreeMap< + String, + anchor_lang::idl::types::IdlTypeDef, + >, + ) {} + fn get_full_path() -> String { + ::alloc::__export::must_use({ + let res = ::alloc::fmt::format( + format_args!( + "{0}::{1}", + "anchor_compressible_derived", + "AccountCreationData", + ), + ); + res + }) + } +} +impl borsh::de::BorshDeserialize for AccountCreationData +where + String: borsh::BorshDeserialize, + u64: borsh::BorshDeserialize, + String: borsh::BorshDeserialize, + String: borsh::BorshDeserialize, + String: borsh::BorshDeserialize, + String: borsh::BorshDeserialize, + u8: borsh::BorshDeserialize, + u64: borsh::BorshDeserialize, + Option: borsh::BorshDeserialize, + Option: borsh::BorshDeserialize, + Option>: borsh::BorshDeserialize, +{ + fn deserialize_reader( + reader: &mut R, + ) -> ::core::result::Result { + Ok(Self { + user_name: borsh::BorshDeserialize::deserialize_reader(reader)?, + session_id: borsh::BorshDeserialize::deserialize_reader(reader)?, + game_type: borsh::BorshDeserialize::deserialize_reader(reader)?, + mint_name: borsh::BorshDeserialize::deserialize_reader(reader)?, + mint_symbol: borsh::BorshDeserialize::deserialize_reader(reader)?, + mint_uri: borsh::BorshDeserialize::deserialize_reader(reader)?, + mint_decimals: borsh::BorshDeserialize::deserialize_reader(reader)?, + mint_supply: borsh::BorshDeserialize::deserialize_reader(reader)?, + mint_update_authority: borsh::BorshDeserialize::deserialize_reader(reader)?, + mint_freeze_authority: borsh::BorshDeserialize::deserialize_reader(reader)?, + additional_metadata: borsh::BorshDeserialize::deserialize_reader(reader)?, + }) + } +} +/// Information about a token account to compress +pub struct TokenAccountInfo { + pub user: Pubkey, + pub mint: Pubkey, +} +impl borsh::ser::BorshSerialize for TokenAccountInfo +where + Pubkey: borsh::ser::BorshSerialize, + Pubkey: borsh::ser::BorshSerialize, +{ + fn serialize( + &self, + writer: &mut W, + ) -> ::core::result::Result<(), borsh::maybestd::io::Error> { + borsh::BorshSerialize::serialize(&self.user, writer)?; + borsh::BorshSerialize::serialize(&self.mint, writer)?; + Ok(()) + } +} +impl anchor_lang::idl::build::IdlBuild for TokenAccountInfo { + fn create_type() -> Option { + Some(anchor_lang::idl::types::IdlTypeDef { + name: Self::get_full_path(), + docs: <[_]>::into_vec( + ::alloc::boxed::box_new([ + "Information about a token account to compress".into(), + ]), + ), + serialization: anchor_lang::idl::types::IdlSerialization::default(), + repr: None, + generics: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlTypeDefTy::Struct { + fields: Some( + anchor_lang::idl::types::IdlDefinedFields::Named( + <[_]>::into_vec( + ::alloc::boxed::box_new([ + anchor_lang::idl::types::IdlField { + name: "user".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::Pubkey, + }, + anchor_lang::idl::types::IdlField { + name: "mint".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::Pubkey, + }, + ]), + ), + ), + ), + }, + }) + } + fn insert_types( + types: &mut std::collections::BTreeMap< + String, + anchor_lang::idl::types::IdlTypeDef, + >, + ) {} + fn get_full_path() -> String { + ::alloc::__export::must_use({ + let res = ::alloc::fmt::format( + format_args!( + "{0}::{1}", + "anchor_compressible_derived", + "TokenAccountInfo", + ), + ); + res + }) + } +} +impl borsh::de::BorshDeserialize for TokenAccountInfo +where + Pubkey: borsh::BorshDeserialize, + Pubkey: borsh::BorshDeserialize, +{ + fn deserialize_reader( + reader: &mut R, + ) -> ::core::result::Result { + Ok(Self { + user: borsh::BorshDeserialize::deserialize_reader(reader)?, + mint: borsh::BorshDeserialize::deserialize_reader(reader)?, + }) + } +} +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, +} +impl borsh::ser::BorshSerialize for CompressionParams +where + ValidityProof: borsh::ser::BorshSerialize, + [u8; 32]: borsh::ser::BorshSerialize, + PackedAddressTreeInfo: borsh::ser::BorshSerialize, + u8: borsh::ser::BorshSerialize, + [u8; 32]: borsh::ser::BorshSerialize, + PackedAddressTreeInfo: borsh::ser::BorshSerialize, + u8: borsh::ser::BorshSerialize, + u8: borsh::ser::BorshSerialize, + CompressedMintWithContext: borsh::ser::BorshSerialize, +{ + fn serialize( + &self, + writer: &mut W, + ) -> ::core::result::Result<(), borsh::maybestd::io::Error> { + borsh::BorshSerialize::serialize(&self.proof, writer)?; + borsh::BorshSerialize::serialize(&self.user_compressed_address, writer)?; + borsh::BorshSerialize::serialize(&self.user_address_tree_info, writer)?; + borsh::BorshSerialize::serialize(&self.user_output_state_tree_index, writer)?; + borsh::BorshSerialize::serialize(&self.game_compressed_address, writer)?; + borsh::BorshSerialize::serialize(&self.game_address_tree_info, writer)?; + borsh::BorshSerialize::serialize(&self.game_output_state_tree_index, writer)?; + borsh::BorshSerialize::serialize(&self.mint_bump, writer)?; + borsh::BorshSerialize::serialize(&self.mint_with_context, writer)?; + Ok(()) + } +} +impl anchor_lang::idl::build::IdlBuild for CompressionParams { + fn create_type() -> Option { + Some(anchor_lang::idl::types::IdlTypeDef { + name: Self::get_full_path(), + docs: ::alloc::vec::Vec::new(), + serialization: anchor_lang::idl::types::IdlSerialization::default(), + repr: None, + generics: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlTypeDefTy::Struct { + fields: Some( + anchor_lang::idl::types::IdlDefinedFields::Named( + <[_]>::into_vec( + ::alloc::boxed::box_new([ + anchor_lang::idl::types::IdlField { + name: "proof".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::Defined { + name: ::get_full_path(), + generics: ::alloc::vec::Vec::new(), + }, + }, + anchor_lang::idl::types::IdlField { + name: "user_compressed_address".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::Array( + Box::new(anchor_lang::idl::types::IdlType::U8), + anchor_lang::idl::types::IdlArrayLen::Value(32), + ), + }, + anchor_lang::idl::types::IdlField { + name: "user_address_tree_info".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::Defined { + name: ::get_full_path(), + generics: ::alloc::vec::Vec::new(), + }, + }, + anchor_lang::idl::types::IdlField { + name: "user_output_state_tree_index".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::U8, + }, + anchor_lang::idl::types::IdlField { + name: "game_compressed_address".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::Array( + Box::new(anchor_lang::idl::types::IdlType::U8), + anchor_lang::idl::types::IdlArrayLen::Value(32), + ), + }, + anchor_lang::idl::types::IdlField { + name: "game_address_tree_info".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::Defined { + name: ::get_full_path(), + generics: ::alloc::vec::Vec::new(), + }, + }, + anchor_lang::idl::types::IdlField { + name: "game_output_state_tree_index".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::U8, + }, + anchor_lang::idl::types::IdlField { + name: "mint_bump".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::U8, + }, + anchor_lang::idl::types::IdlField { + name: "mint_with_context".into(), + docs: ::alloc::vec::Vec::new(), + ty: anchor_lang::idl::types::IdlType::Defined { + name: ::get_full_path(), + generics: ::alloc::vec::Vec::new(), + }, + }, + ]), + ), + ), + ), + }, + }) + } + fn insert_types( + types: &mut std::collections::BTreeMap< + String, + anchor_lang::idl::types::IdlTypeDef, + >, + ) { + if let Some(ty) = ::create_type() { + types.insert(::get_full_path(), ty); + ::insert_types(types); + } + if let Some(ty) = ::create_type() { + types.insert(::get_full_path(), ty); + ::insert_types(types); + } + if let Some(ty) = ::create_type() { + types.insert(::get_full_path(), ty); + ::insert_types(types); + } + if let Some(ty) = ::create_type() { + types.insert(::get_full_path(), ty); + ::insert_types(types); + } + } + fn get_full_path() -> String { + ::alloc::__export::must_use({ + let res = ::alloc::fmt::format( + format_args!( + "{0}::{1}", + "anchor_compressible_derived", + "CompressionParams", + ), + ); + res + }) + } +} +impl borsh::de::BorshDeserialize for CompressionParams +where + ValidityProof: borsh::BorshDeserialize, + [u8; 32]: borsh::BorshDeserialize, + PackedAddressTreeInfo: borsh::BorshDeserialize, + u8: borsh::BorshDeserialize, + [u8; 32]: borsh::BorshDeserialize, + PackedAddressTreeInfo: borsh::BorshDeserialize, + u8: borsh::BorshDeserialize, + u8: borsh::BorshDeserialize, + CompressedMintWithContext: borsh::BorshDeserialize, +{ + fn deserialize_reader( + reader: &mut R, + ) -> ::core::result::Result { + Ok(Self { + proof: borsh::BorshDeserialize::deserialize_reader(reader)?, + user_compressed_address: borsh::BorshDeserialize::deserialize_reader( + reader, + )?, + user_address_tree_info: borsh::BorshDeserialize::deserialize_reader(reader)?, + user_output_state_tree_index: borsh::BorshDeserialize::deserialize_reader( + reader, + )?, + game_compressed_address: borsh::BorshDeserialize::deserialize_reader( + reader, + )?, + game_address_tree_info: borsh::BorshDeserialize::deserialize_reader(reader)?, + game_output_state_tree_index: borsh::BorshDeserialize::deserialize_reader( + reader, + )?, + mint_bump: borsh::BorshDeserialize::deserialize_reader(reader)?, + mint_with_context: borsh::BorshDeserialize::deserialize_reader(reader)?, + }) + } +} +#[inline] +pub fn account_meta_from_account_info(account_info: &AccountInfo) -> AccountMeta { + AccountMeta { + pubkey: *account_info.key, + is_signer: account_info.is_signer, + is_writable: account_info.is_writable, + } +} diff --git a/sdk-tests/anchor-compressible-derived/src/instructions/create_record.rs b/sdk-tests/anchor-compressible-derived/src/instructions/create_record.rs new file mode 100644 index 0000000000..9a6a9669b5 --- /dev/null +++ b/sdk-tests/anchor-compressible-derived/src/instructions/create_record.rs @@ -0,0 +1,27 @@ +use anchor_lang::prelude::*; + +use crate::state::UserRecord; + +// In a standalone file to test macro support. +#[derive(Accounts)] +pub struct CreateRecord<'info> { + #[account(mut)] + pub user: Signer<'info>, + #[account( + init, + payer = user, + // Manually add 10 bytes! Discriminator + owner + string len + name + + // score + option + space = 8 + 32 + 4 + 32 + 8 + 10, + seeds = [b"user_record", user.key().as_ref()], + bump, + )] + pub user_record: Account<'info, UserRecord>, + /// UNCHECKED: checked via config. + #[account(mut)] + pub rent_recipient: AccountInfo<'info>, + /// The global config account + /// UNCHECKED: checked via load_checked. + pub config: AccountInfo<'info>, + pub system_program: Program<'info, System>, +} diff --git a/sdk-tests/anchor-compressible-derived/src/instructions/mod.rs b/sdk-tests/anchor-compressible-derived/src/instructions/mod.rs new file mode 100644 index 0000000000..fe807ef25e --- /dev/null +++ b/sdk-tests/anchor-compressible-derived/src/instructions/mod.rs @@ -0,0 +1,2 @@ +pub mod create_record; +pub use create_record::*; diff --git a/sdk-tests/anchor-compressible-derived/src/lib.rs b/sdk-tests/anchor-compressible-derived/src/lib.rs new file mode 100644 index 0000000000..774bf3325f --- /dev/null +++ b/sdk-tests/anchor-compressible-derived/src/lib.rs @@ -0,0 +1,634 @@ +use anchor_lang::{ + prelude::*, + solana_program::{instruction::AccountMeta, program::invoke, pubkey::Pubkey}, +}; + +use light_ctoken_types::instructions::mint_action::CompressedMintWithContext; +use light_sdk::{ + add_compressible_instructions, + compressible::{ + compress_account_on_init, compress_empty_account_on_init, + prepare_accounts_for_compression_on_init, CompressibleConfig, CompressionInfo, + HasCompressionInfo, Unpack, + }, + cpi::CpiInputs, + derive_light_cpi_signer, + instruction::{PackedAddressTreeInfo, ValidityProof}, + LightDiscriminator, +}; + +use light_sdk_types::{CpiAccountsConfig, CpiAccountsSmall, CpiSigner}; + +pub mod instructions; +pub mod state; + +use crate::state::*; + +declare_id!("GRLu2hKaAiMbxpkAM1HeXzks9YeGuz18SEgXEizVvPqX"); +pub const LIGHT_CPI_SIGNER: CpiSigner = + derive_light_cpi_signer!("GRLu2hKaAiMbxpkAM1HeXzks9YeGuz18SEgXEizVvPqX"); + +#[add_compressible_instructions( + UserRecord = ("user_record", data.owner), + GameSession = ("game_session", data.session_id.to_le_bytes()), + PlaceholderRecord = ("placeholder_record", data.placeholder_id.to_le_bytes()), + CTokenSigner = (is_token, "ctoken_signer", ctx.fee_payer, ctx.accounts.some_mint), + owner = Pubkey, + session_id = u64, + placeholder_id = u64 +)] +#[program] +pub mod anchor_compressible_derived { + + use light_compressed_token_sdk::instructions::{ + create_mint_action_cpi, find_spl_mint_address, MintActionInputs, + }; + use light_sdk_types::cpi_context_write::CpiContextWriteAccounts; + + 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<()> { + let user_record = &mut ctx.accounts.user_record; + + // 1. Load config from the config account + let config = CompressibleConfig::load_checked(&ctx.accounts.config, &crate::ID)?; + + user_record.owner = ctx.accounts.user.key(); + user_record.name = name; + user_record.score = 11; + + // 2. Verify rent recipient matches config + if ctx.accounts.rent_recipient.key() != config.rent_recipient { + return err!(ErrorCode::InvalidRentRecipient); + } + + // 3. Create CPI accounts + let user_account_info = ctx.accounts.user.to_account_info(); + let cpi_accounts = + CpiAccountsSmall::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(), + true, + Some(0), + ); + + compress_account_on_init::( + user_record, + &compressed_address, + &new_address_params, + output_state_tree_index, + cpi_accounts, + &config.address_space, + &ctx.accounts.rent_recipient, + proof, + )?; + + // at the end of the instruction we always clean up all onchain pdas that we compressed + user_record.close(ctx.accounts.rent_recipient.to_account_info())?; + + Ok(()) + } + + // Must be manually implemented. + pub fn create_game_session<'info>( + ctx: Context<'_, '_, '_, 'info, CreateGameSession<'info>>, + session_id: u64, + game_type: String, + proof: ValidityProof, + compressed_address: [u8; 32], + address_tree_info: PackedAddressTreeInfo, + output_state_tree_index: u8, + ) -> Result<()> { + let game_session = &mut ctx.accounts.game_session; + + // Load config from the config account + let config = CompressibleConfig::load_checked(&ctx.accounts.config, &crate::ID)?; + + // Set your account data. + game_session.session_id = session_id; + game_session.player = ctx.accounts.player.key(); + game_session.game_type = game_type; + game_session.start_time = Clock::get()?.unix_timestamp as u64; + game_session.end_time = None; + game_session.score = 0; + + // Check that rent recipient matches your config. + if ctx.accounts.rent_recipient.key() != config.rent_recipient { + return err!(ErrorCode::InvalidRentRecipient); + } + + // Create CPI accounts. + let player_account_info = ctx.accounts.player.to_account_info(); + let cpi_accounts = CpiAccountsSmall::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(), + true, + Some(0), + ); + + // Call at the end of your init instruction to compress the pda account + // safely. This also closes the pda account. The account can then be + // decompressed by anyone at any time via the + // decompress_accounts_idempotent instruction. Creates a unique cPDA to + // ensure that the account cannot be re-inited only decompressed. + compress_account_on_init::( + game_session, + &compressed_address, + &new_address_params, + output_state_tree_index, + cpi_accounts, + &config.address_space, + &ctx.accounts.rent_recipient, + proof, + )?; + + game_session.close(ctx.accounts.rent_recipient.to_account_info())?; + + Ok(()) + } + + // Must be manually implemented. + pub fn create_user_record_and_game_session<'info>( + ctx: Context<'_, '_, '_, 'info, CreateUserRecordAndGameSession<'info>>, + account_data: AccountCreationData, + compression_params: CompressionParams, + ) -> Result<()> { + let user_record = &mut ctx.accounts.user_record; + let game_session = &mut ctx.accounts.game_session; + + // Load your config checked. + let config = CompressibleConfig::load_checked(&ctx.accounts.config, &crate::ID)?; + + // Check that rent recipient matches your config. + if ctx.accounts.rent_recipient.key() != config.rent_recipient { + return err!(ErrorCode::InvalidRentRecipient); + } + + // 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 = CpiAccountsSmall::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(), true, Some(0)); + let game_new_address_params = compression_params + .game_address_tree_info + .into_new_address_params_assigned_packed(game_session.key().to_bytes(), true, Some(1)); + + let mut all_compressed_infos = Vec::new(); + + // Prepares the firstpda account for compression. compress the pda + // account safely. This also closes the pda account. safely. This also + // closes the pda account. The account can then be decompressed by + // anyone at any time via the decompress_accounts_idempotent + // instruction. Creates a unique cPDA to ensure that the account cannot + // be re-inited only decompressed. + let user_compressed_infos = prepare_accounts_for_compression_on_init::( + &[user_record], + &[compression_params.user_compressed_address], + &[user_new_address_params], + &[compression_params.user_output_state_tree_index], + &cpi_accounts, + &config.address_space, + &ctx.accounts.rent_recipient, + )?; + + all_compressed_infos.extend(user_compressed_infos); + + // Process GameSession for compression. compress the pda account safely. + // This also closes the pda account. The account can then be + // decompressed by anyone at any time via the + // decompress_accounts_idempotent instruction. Creates a unique cPDA to + // ensure that the account cannot be re-inited only decompressed. + let game_compressed_infos = prepare_accounts_for_compression_on_init::( + &[game_session], + &[compression_params.game_compressed_address], + &[game_new_address_params], + &[compression_params.game_output_state_tree_index], + &cpi_accounts, + &config.address_space, + &ctx.accounts.rent_recipient, + )?; + all_compressed_infos.extend(game_compressed_infos); + + let cpi_inputs = CpiInputs::new_first_cpi( + all_compressed_infos, + vec![user_new_address_params, game_new_address_params], + ); + + 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, + }; + cpi_inputs.invoke_light_system_program_cpi_context(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 = { + let user_key = ctx.accounts.user.key(); + let seeds = [b"ctoken_signer".as_ref(), user_key.as_ref(), mint.as_ref()]; + let (pda, _bump) = Pubkey::find_program_address(&seeds, &crate::ID); + pda + }; + + 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 + }, + ], + lamports: None, + token_account_version: 2, + }, + ]; + + let output_queue = *cpi_accounts.tree_accounts().unwrap()[0].key; // Same tree as PDA + let address_tree_pubkey = *cpi_accounts.tree_accounts().unwrap()[1].key; // Same tree as PDA + + let mint_action_inputs = MintActionInputs { + compressed_mint_inputs: compression_params.mint_with_context.clone(), + mint_seed: ctx.accounts.mint_signer.key(), + mint_bump: Some(compression_params.mint_bump), + create_mint: true, + authority: ctx.accounts.mint_authority.key(), + payer: ctx.accounts.user.key(), + proof: compression_params.proof.into(), + actions, + input_queue: None, // Not needed for create_mint: true + output_queue, + tokens_out_queue: Some(output_queue), // For MintTo actions + address_tree_pubkey, + token_pool: None, // Not needed for simple compressed mint creation + }; + + let mint_action_instruction = create_mint_action_cpi( + mint_action_inputs, + Some(light_ctoken_types::instructions::mint_action::CpiContext { + set_context: false, + first_set_context: false, + in_tree_index: 1, // address tree + in_queue_index: 0, + out_queue_index: 0, + token_out_queue_index: 0, + assigned_account_index: 2, + }), + 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.compressed_token_program.to_account_info()); + account_infos.push(ctx.accounts.mint_authority.to_account_info()); + account_infos.push(ctx.accounts.mint_signer.to_account_info()); + account_infos.push(ctx.accounts.user.to_account_info()); + + // Invoke the mint action instruction directly + invoke(&mint_action_instruction, &account_infos)?; + + // at the end of the instruction we always clean up all onchain pdas that we compressed + user_record.close(ctx.accounts.rent_recipient.to_account_info())?; + game_session.close(ctx.accounts.rent_recipient.to_account_info())?; + + Ok(()) + } + + /// Creates an empty compressed account while keeping the PDA intact. + /// This demonstrates the compress_empty_account_on_init functionality. + pub fn create_placeholder_record<'info>( + ctx: Context<'_, '_, '_, 'info, CreatePlaceholderRecord<'info>>, + placeholder_id: u64, + name: String, + proof: ValidityProof, + compressed_address: [u8; 32], + address_tree_info: PackedAddressTreeInfo, + output_state_tree_index: u8, + ) -> Result<()> { + let placeholder_record = &mut ctx.accounts.placeholder_record; + + let config = CompressibleConfig::load_checked(&ctx.accounts.config, &crate::ID)?; + + placeholder_record.owner = ctx.accounts.user.key(); + placeholder_record.name = name; + placeholder_record.placeholder_id = placeholder_id; + + // Initialize compression_info for the PDA + *placeholder_record.compression_info_mut_opt() = + Some(super::CompressionInfo::new_decompressed()?); + placeholder_record + .compression_info_mut() + .bump_last_written_slot()?; + + // Verify rent recipient matches config + if ctx.accounts.rent_recipient.key() != config.rent_recipient { + return err!(ErrorCode::InvalidRentRecipient); + } + + // Create CPI accounts + let user_account_info = ctx.accounts.user.to_account_info(); + let cpi_accounts = + CpiAccountsSmall::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(), + true, + Some(0), + ); + + // Use the new compress_empty_account_on_init function + // This creates an empty compressed account but does NOT close the PDA + compress_empty_account_on_init::( + placeholder_record, + &compressed_address, + &new_address_params, + output_state_tree_index, + cpi_accounts, + &config.address_space, + proof, + )?; + + // Note we do not actually close this account yet because in this + // example we only create _empty_ compressed account without fully + // compressing it yet. + Ok(()) + } + + pub fn update_record(ctx: Context, name: String, score: u64) -> Result<()> { + let user_record = &mut ctx.accounts.user_record; + + user_record.name = name; + user_record.score = score; + + // 1. Must manually set compression info + user_record + .compression_info_mut() + .bump_last_written_slot()?; + + Ok(()) + } + + pub fn update_game_session( + ctx: Context, + _session_id: u64, + new_score: u64, + ) -> Result<()> { + let game_session = &mut ctx.accounts.game_session; + + game_session.score = new_score; + game_session.end_time = Some(Clock::get()?.unix_timestamp as u64); + + // Must manually set compression info + game_session + .compression_info_mut() + .bump_last_written_slot()?; + + Ok(()) + } +} + +#[derive(Accounts)] +pub struct CreateRecord<'info> { + #[account(mut)] + pub user: Signer<'info>, + #[account( + init, + payer = user, + // discriminator + owner + string len + name + score + + // option. Note that in the onchain space + // CompressionInfo is always Some. + space = 8 + 32 + 4 + 32 + 8 + 10, + seeds = [b"user_record", user.key().as_ref()], + bump, + )] + pub user_record: Account<'info, UserRecord>, + /// Needs to be here for the init anchor macro to work. + pub system_program: Program<'info, System>, + /// The global config account + /// CHECK: Config is validated by the SDK's load_checked method + pub config: AccountInfo<'info>, + /// Rent recipient - must match config + /// CHECK: Rent recipient is validated against the config + #[account(mut)] + pub rent_recipient: AccountInfo<'info>, +} + +#[derive(Accounts)] +#[instruction(placeholder_id: u64)] +pub struct CreatePlaceholderRecord<'info> { + #[account(mut)] + pub user: Signer<'info>, + #[account( + init, + payer = user, + // discriminator + compression_info + owner + string len + name + placeholder_id + space = 8 + 10 + 32 + 4 + 32 + 8, + seeds = [b"placeholder_record", placeholder_id.to_le_bytes().as_ref()], + bump, + )] + pub placeholder_record: Account<'info, PlaceholderRecord>, + /// Needs to be here for the init anchor macro to work. + pub system_program: Program<'info, System>, + /// The global config account + /// CHECK: Config is validated by the SDK's load_checked method + pub config: AccountInfo<'info>, + /// Rent recipient - must match config + /// CHECK: Rent recipient is validated against the config + #[account(mut)] + pub rent_recipient: AccountInfo<'info>, +} + +#[derive(Accounts)] +#[instruction(account_data: AccountCreationData)] +pub struct CreateUserRecordAndGameSession<'info> { + #[account(mut)] + pub user: Signer<'info>, + #[account( + init, + payer = user, + // discriminator + owner + string len + name + score + + // option. Note that in the onchain space + // CompressionInfo is always Some. + space = 8 + 32 + 4 + 32 + 8 + 10, + seeds = [b"user_record", user.key().as_ref()], + bump, + )] + pub user_record: Account<'info, UserRecord>, + #[account( + init, + payer = user, + // discriminator + option + session_id + player + + // string len + game_type + start_time + end_time(Option) + score + space = 8 + 10 + 8 + 32 + 4 + 32 + 8 + 9 + 8, + seeds = [b"game_session", account_data.session_id.to_le_bytes().as_ref()], + bump, + )] + pub game_session: Account<'info, GameSession>, + + // Compressed mint creation accounts - only token-specific ones needed + /// The mint signer used for PDA derivation + pub mint_signer: Signer<'info>, + + /// The mint authority used for PDA derivation + pub mint_authority: Signer<'info>, + + /// Compressed token program + /// CHECK: Program ID validated using COMPRESSED_TOKEN_PROGRAM_ID constant + pub compressed_token_program: UncheckedAccount<'info>, + + /// CHECK: CPI authority of the compressed token program + pub compress_token_program_cpi_authority: UncheckedAccount<'info>, + + /// Needs to be here for the init anchor macro to work. + pub system_program: Program<'info, System>, + /// The global config account + /// CHECK: Config is validated by the SDK's load_checked method + pub config: AccountInfo<'info>, + /// Rent recipient - must match config + /// CHECK: Rent recipient is validated against the config + #[account(mut)] + pub rent_recipient: AccountInfo<'info>, +} + +#[derive(Accounts)] +#[instruction(session_id: u64)] +pub struct CreateGameSession<'info> { + #[account(mut)] + pub player: Signer<'info>, + #[account( + init, + payer = player, + space = 8 + 9 + 8 + 32 + 4 + 32 + 8 + 9 + 8, // discriminator + compression_info + session_id + player + string len + game_type + start_time + end_time(Option) + score + seeds = [b"game_session", session_id.to_le_bytes().as_ref()], + bump, + )] + pub game_session: Account<'info, GameSession>, + pub system_program: Program<'info, System>, + /// The global config account + /// CHECK: Config is validated by the SDK's load_checked method + pub config: AccountInfo<'info>, + /// Rent recipient - must match config + /// CHECK: Rent recipient is validated against the config + #[account(mut)] + pub rent_recipient: AccountInfo<'info>, +} + +#[derive(Accounts)] +pub struct UpdateRecord<'info> { + #[account(mut)] + pub user: Signer<'info>, + #[account( + mut, + seeds = [b"user_record", user.key().as_ref()], + bump, + constraint = user_record.owner == user.key() + )] + pub user_record: Account<'info, UserRecord>, +} + +#[derive(Accounts)] +#[instruction(session_id: u64)] +pub struct UpdateGameSession<'info> { + #[account(mut)] + pub player: Signer<'info>, + #[account( + mut, + seeds = [b"game_session", session_id.to_le_bytes().as_ref()], + bump, + constraint = game_session.player == player.key() + )] + pub game_session: Account<'info, GameSession>, +} + +#[error_code] +pub enum ErrorCode { + #[msg("Invalid account count: PDAs and compressed accounts must match")] + InvalidAccountCount, + #[msg("Rent recipient does not match config")] + InvalidRentRecipient, + #[msg("Failed to create compressed mint")] + MintCreationFailed, + #[msg("Compressed token program account not found in remaining accounts")] + MissingCompressedTokenProgram, + #[msg("Compressed token program authority PDA account not found in remaining accounts")] + MissingCompressedTokenProgramAuthorityPDA, + + #[msg("CToken decompression not yet implemented")] + CTokenDecompressionNotImplemented, +} + +// Add these struct definitions before the program module +#[derive(AnchorSerialize, AnchorDeserialize)] +pub struct AccountCreationData { + pub user_name: String, + pub session_id: u64, + pub game_type: String, + // TODO: Add mint metadata fields when implementing mint functionality + pub mint_name: String, + pub mint_symbol: String, + pub mint_uri: String, + pub mint_decimals: u8, + pub mint_supply: u64, + pub mint_update_authority: Option, + pub mint_freeze_authority: Option, + pub additional_metadata: Option>, +} + +#[derive(AnchorSerialize, AnchorDeserialize)] +pub struct CompressionParams { + pub proof: ValidityProof, + pub user_compressed_address: [u8; 32], + pub user_address_tree_info: PackedAddressTreeInfo, + pub user_output_state_tree_index: u8, + pub game_compressed_address: [u8; 32], + pub game_address_tree_info: PackedAddressTreeInfo, + pub game_output_state_tree_index: u8, + // TODO: Add mint compression parameters when implementing mint functionality + // pub mint_compressed_address: [u8; 32], + // pub mint_address_tree_info: PackedAddressTreeInfo, + // pub mint_output_state_tree_index: u8, + pub mint_bump: u8, + pub mint_with_context: CompressedMintWithContext, +} diff --git a/sdk-tests/anchor-compressible-derived/src/state.rs b/sdk-tests/anchor-compressible-derived/src/state.rs new file mode 100644 index 0000000000..d5948d7ca2 --- /dev/null +++ b/sdk-tests/anchor-compressible-derived/src/state.rs @@ -0,0 +1,60 @@ +use anchor_lang::prelude::*; +use light_sdk::{compressible::CompressionInfo, LightDiscriminator, LightHasher}; +use light_sdk::{Compressible, CompressiblePack}; +#[derive( + Debug, LightHasher, LightDiscriminator, Compressible, CompressiblePack, Default, InitSpace, +)] +#[account] +pub struct UserRecord { + #[skip] + pub compression_info: Option, + #[hash] + pub owner: Pubkey, + #[hash] + #[max_len(32)] + pub name: String, + pub score: u64, +} + +#[derive( + Debug, LightHasher, LightDiscriminator, Default, InitSpace, Compressible, CompressiblePack, +)] +#[compress_as( + start_time = 0, + end_time = None, + score = 0 + // session_id, player, game_type, compression_info are kept as-is +)] +#[account] +pub struct GameSession { + #[skip] + pub compression_info: Option, + pub session_id: u64, + #[hash] + pub player: Pubkey, + #[hash] + #[max_len(32)] + pub game_type: String, + pub start_time: u64, + pub end_time: Option, + pub score: u64, +} + +// PlaceholderRecord - demonstrates empty compressed account creation +#[derive( + Debug, LightHasher, LightDiscriminator, Default, InitSpace, Compressible, CompressiblePack, +)] +#[account] +pub struct PlaceholderRecord { + #[skip] + pub compression_info: Option, + #[hash] + pub owner: Pubkey, + #[hash] + #[max_len(32)] + pub name: String, + pub placeholder_id: u64, +} + +// CTokenAccountVariant is now auto-generated by the add_compressible_instructions macro +// based on the token seed specifications in lib.rs diff --git a/sdk-tests/anchor-compressible-derived/tests/test_decompress_multiple.rs b/sdk-tests/anchor-compressible-derived/tests/test_decompress_multiple.rs new file mode 100644 index 0000000000..661ed82112 --- /dev/null +++ b/sdk-tests/anchor-compressible-derived/tests/test_decompress_multiple.rs @@ -0,0 +1,2610 @@ +use anchor_compressible_derived::state::{GameSession, PlaceholderRecord, UserRecord}; + +use anchor_compressible_derived::{CTokenAccountVariant, CompressedAccountVariant}; +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::{ + instructions::{derive_compressed_mint_address, find_spl_mint_address}, + CPI_AUTHORITY_PDA, +}; +use light_compressible_client::CompressibleInstruction; +use light_ctoken_types::{ + instructions::mint_action::{CompressedMintInstructionData, CompressedMintWithContext}, + state::BaseCompressedMint, + COMPRESSED_TOKEN_PROGRAM_ID, +}; +use light_macros::pubkey; +use light_program_test::{ + initialize_compression_config, + program_test::{LightProgramTest, TestRpc}, + setup_mock_program_data, + utils::simulation::simulate_cu, + AddressWithTree, Indexer, ProgramTestConfig, Rpc, RpcError, +}; +use light_sdk::{ + compressible::{CompressAs, CompressibleConfig}, + instruction::{PackedAccounts, SystemAccountMetaConfig}, + token::CompressibleTokenDataWithVariant, +}; +use solana_account::Account; +use solana_instruction::Instruction; +use solana_keypair::Keypair; +use solana_pubkey::Pubkey; +use solana_signer::Signer; + +pub const ADDRESS_SPACE: [Pubkey; 1] = [pubkey!("EzKE84aVTkCUhDHLELqyJaq1Y7UVVmqxXqZjVHwHY3rK")]; +pub const RENT_RECIPIENT: Pubkey = pubkey!("CLEuMG7pzJX9xAuKCFzBP154uiG1GaNo4Fq7x6KAcAfG"); +pub const TOKEN_PROGRAM_ID: Pubkey = pubkey!("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"); + +#[tokio::test] +async fn test_create_and_decompress_two_accounts() { + let program_id = anchor_compressible_derived::ID; + let mut config = ProgramTestConfig::new_v2( + true, + Some(vec![("anchor_compressible_derived", program_id)]), + ); + config = config.with_light_protocol_events(); + + let mut rpc = LightProgramTest::new(config).await.unwrap(); + + let payer = rpc.get_payer().insecure_clone(); + + let config_pda = CompressibleConfig::derive_pda(&program_id, 0).0; + let _program_data_pda = setup_mock_program_data(&mut rpc, &payer, &program_id); + + let result = initialize_compression_config( + &mut rpc, + &payer, + &program_id, + &payer, + 100, + RENT_RECIPIENT, + vec![ADDRESS_SPACE[0]], + &CompressibleInstruction::INITIALIZE_COMPRESSION_CONFIG_DISCRIMINATOR, + None, + ) + .await; + assert!(result.is_ok(), "Initialize config should succeed"); + + let combined_user = Keypair::new(); + let fund_user_ix = solana_sdk::system_instruction::transfer( + &payer.pubkey(), + &combined_user.pubkey(), + 1e9 as u64, + ); + let fund_result = rpc + .create_and_send_transaction(&[fund_user_ix], &payer.pubkey(), &[&payer]) + .await; + assert!(fund_result.is_ok(), "Funding combined user should succeed"); + let combined_session_id = 99999u64; + let (combined_user_record_pda, _combined_user_record_bump) = Pubkey::find_program_address( + &[b"user_record", combined_user.pubkey().as_ref()], + &program_id, + ); + let (combined_game_session_pda, _combined_game_bump) = Pubkey::find_program_address( + &[b"game_session", combined_session_id.to_le_bytes().as_ref()], + &program_id, + ); + + let (compressed_token_account, _) = 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 (_, compressed_token_account_address) = anchor_compressible_derived::get_ctokensigner_seeds( + &combined_user.pubkey(), + &compressed_token_account.token.mint, + ); + + 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; + let game_session_before_decompression: CompressedAccount = rpc + .get_compressed_account(compressed_game_session_address, None) + .await + .unwrap() + .value; + + 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, + compressed_token_account.clone(), + compressed_token_account_address, // also the owner of the compressed token account! + ) + .await; + + // Now compress the decompressed token account back to compressed + rpc.warp_to_slot(300).unwrap(); + + compress_token_account_after_decompress( + &mut rpc, + &combined_user, + &program_id, + &config_pda, + compressed_token_account_address, + compressed_token_account.token.mint, + compressed_token_account.token.amount, + &combined_user_record_pda, + &combined_game_session_pda, + combined_session_id, + user_record_before_decompression.hash, + game_session_before_decompression.hash, + ) + .await; +} + +#[tokio::test] +async fn test_create_decompress_compress_single_account() { + let program_id = anchor_compressible_derived::ID; + let config = ProgramTestConfig::new_v2( + true, + Some(vec![("anchor_compressible_derived", program_id)]), + ); + let mut rpc = LightProgramTest::new(config).await.unwrap(); + let payer = rpc.get_payer().insecure_clone(); + let _program_data_pda = setup_mock_program_data(&mut rpc, &payer, &program_id); + + let result = initialize_compression_config( + &mut rpc, + &payer, + &program_id, + &payer, + 100, + RENT_RECIPIENT, + vec![ADDRESS_SPACE[0]], + &CompressibleInstruction::INITIALIZE_COMPRESSION_CONFIG_DISCRIMINATOR, + None, + ) + .await; + assert!(result.is_ok(), "Initialize config should succeed"); + + let (user_record_pda, user_record_bump) = + Pubkey::find_program_address(&[b"user_record", payer.pubkey().as_ref()], &program_id); + + create_record(&mut rpc, &payer, &program_id, &user_record_pda, None).await; + + rpc.warp_to_slot(100).unwrap(); + + decompress_single_user_record( + &mut rpc, + &payer, + &program_id, + &user_record_pda, + &user_record_bump, + "Test User", + 100, + ) + .await; + + rpc.warp_to_slot(101).unwrap(); + + let result = compress_record(&mut rpc, &payer, &program_id, &user_record_pda, true).await; + assert!(result.is_err(), "Compression should fail due to slot delay"); + if let Err(err) = result { + let err_msg = format!("{:?}", err); + assert!( + err_msg.contains("Custom(16001)"), + "Expected error message about slot delay, got: {}", + err_msg + ); + } + rpc.warp_to_slot(200).unwrap(); + let _result = compress_record(&mut rpc, &payer, &program_id, &user_record_pda, false).await; +} + +#[tokio::test] +async fn test_double_decompression_attack() { + let program_id = anchor_compressible_derived::ID; + let config = ProgramTestConfig::new_v2( + true, + Some(vec![("anchor_compressible_derived", program_id)]), + ); + let mut rpc = LightProgramTest::new(config).await.unwrap(); + let payer = rpc.get_payer().insecure_clone(); + + let _program_data_pda = setup_mock_program_data(&mut rpc, &payer, &program_id); + + let result = initialize_compression_config( + &mut rpc, + &payer, + &program_id, + &payer, + 100, + RENT_RECIPIENT, + vec![ADDRESS_SPACE[0]], + &CompressibleInstruction::INITIALIZE_COMPRESSION_CONFIG_DISCRIMINATOR, + None, + ) + .await; + assert!(result.is_ok(), "Initialize config should succeed"); + + let (user_record_pda, user_record_bump) = + Pubkey::find_program_address(&[b"user_record", payer.pubkey().as_ref()], &program_id); + + // Create and compress the account + create_record(&mut rpc, &payer, &program_id, &user_record_pda, None).await; + let address_tree_pubkey = rpc.get_address_tree_v2().queue; + let user_compressed_address = derive_address( + &user_record_pda.to_bytes(), + &address_tree_pubkey.to_bytes(), + &program_id.to_bytes(), + ); + let compressed_user_record = rpc + .get_compressed_account(user_compressed_address, None) + .await + .unwrap() + .value; + let c_user_record = + UserRecord::deserialize(&mut &compressed_user_record.data.unwrap().data[..]).unwrap(); + + rpc.warp_to_slot(100).unwrap(); + + // First decompression - should succeed + decompress_single_user_record( + &mut rpc, + &payer, + &program_id, + &user_record_pda, + &user_record_bump, + "Test User", + 100, + ) + .await; + + // Verify account is now decompressed + 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" + ); + + // Second decompression attempt - should be idempotent (skip already initialized account) + + let c_user_pda = rpc + .get_compressed_account(user_compressed_address, None) + .await + .unwrap() + .value; + + 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 named_accounts = anchor_compressible_derived::accounts::DecompressAccountsIdempotent { + fee_payer: payer.pubkey(), + rent_payer: payer.pubkey(), + config: CompressibleConfig::derive_pda(&program_id, 0).0, + compressed_token_program: None, + compressed_token_cpi_authority: None, + // The macro should have auto-added mint from ctx.accounts.mint: + some_mint: payer.pubkey(), // Should be auto-added by the macro! + }; + let named_accounts_metas = named_accounts.to_account_metas(None); + // Second decompression instruction - should still work (idempotent) + let instruction = + light_compressible_client::CompressibleInstruction::decompress_accounts_idempotent( + &program_id, + &CompressibleInstruction::DECOMPRESS_ACCOUNTS_IDEMPOTENT_DISCRIMINATOR, + &payer.pubkey(), + &payer.pubkey(), + &[user_record_pda], + &[( + c_user_pda, + CompressedAccountVariant::UserRecord(c_user_record), + )], + rpc_result, + output_state_tree_info, + ) + .unwrap(); + + let result = rpc + .create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer]) + .await; + + // Should succeed due to idempotent behavior (skips already initialized accounts) + assert!( + result.is_ok(), + "Second decompression should succeed idempotently" + ); + + // Verify account state is still correct and not corrupted + let user_pda_account = rpc.get_account(user_record_pda).await.unwrap(); + let user_pda_data = user_pda_account.unwrap().data; + let decompressed_user_record = UserRecord::try_deserialize(&mut &user_pda_data[..]).unwrap(); + + assert_eq!(decompressed_user_record.name, "Test User"); + assert_eq!(decompressed_user_record.score, 11); + assert_eq!(decompressed_user_record.owner, payer.pubkey()); + assert!(!decompressed_user_record + .compression_info + .as_ref() + .unwrap() + .is_compressed()); +} + +#[tokio::test] +async fn test_create_and_decompress_accounts_with_different_state_trees() { + let program_id = anchor_compressible_derived::ID; + let config = ProgramTestConfig::new_v2( + true, + Some(vec![("anchor_compressible_derived", program_id)]), + ); + let mut rpc = LightProgramTest::new(config).await.unwrap(); + let payer = rpc.get_payer().insecure_clone(); + + let config_pda = CompressibleConfig::derive_pda(&program_id, 0).0; + let _program_data_pda = setup_mock_program_data(&mut rpc, &payer, &program_id); + + let result = initialize_compression_config( + &mut rpc, + &payer, + &program_id, + &payer, + 100, + RENT_RECIPIENT, + vec![ADDRESS_SPACE[0]], + &CompressibleInstruction::INITIALIZE_COMPRESSION_CONFIG_DISCRIMINATOR, + None, + ) + .await; + assert!(result.is_ok(), "Initialize config should succeed"); + + let (user_record_pda, _user_record_bump) = + Pubkey::find_program_address(&[b"user_record", payer.pubkey().as_ref()], &program_id); + + let session_id = 54321u64; + let (game_session_pda, _game_bump) = Pubkey::find_program_address( + &[b"game_session", session_id.to_le_bytes().as_ref()], + &program_id, + ); + + // Get two different state trees + let first_state_tree_info = rpc.get_state_tree_infos()[0]; + let second_state_tree_info = rpc.get_state_tree_infos()[1]; + + // Create user record using first state tree + create_record( + &mut rpc, + &payer, + &program_id, + &user_record_pda, + Some(first_state_tree_info.queue), + ) + .await; + + // Create game session using second state tree + 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(); + + // Now decompress both accounts together - they come from different state trees + // This should succeed and validate that our decompression can handle mixed state tree sources + decompress_multiple_pdas( + &mut rpc, + &payer, + &program_id, + &user_record_pda, + &game_session_pda, + session_id, + "Test User", + "Battle Royale", + 100, + ) + .await; +} + +#[tokio::test] +async fn test_update_record_compression_info() { + let program_id = anchor_compressible_derived::ID; + let config = ProgramTestConfig::new_v2( + true, + Some(vec![("anchor_compressible_derived", program_id)]), + ); + let mut rpc = LightProgramTest::new(config).await.unwrap(); + let payer = rpc.get_payer().insecure_clone(); + + let _program_data_pda = setup_mock_program_data(&mut rpc, &payer, &program_id); + + let result = initialize_compression_config( + &mut rpc, + &payer, + &program_id, + &payer, + 100, + RENT_RECIPIENT, + vec![ADDRESS_SPACE[0]], + &CompressibleInstruction::INITIALIZE_COMPRESSION_CONFIG_DISCRIMINATOR, + None, + ) + .await; + assert!(result.is_ok(), "Initialize config should succeed"); + + let (user_record_pda, user_record_bump) = + Pubkey::find_program_address(&[b"user_record", payer.pubkey().as_ref()], &program_id); + + // Create and compress the account + create_record(&mut rpc, &payer, &program_id, &user_record_pda, None).await; + + // Warp to slot 100 and decompress + 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; + + // Warp to slot 150 for the update + rpc.warp_to_slot(150).unwrap(); + + // Create update instruction + let accounts = anchor_compressible_derived::accounts::UpdateRecord { + user: payer.pubkey(), + user_record: user_record_pda, + }; + + let instruction_data = anchor_compressible_derived::instruction::UpdateRecord { + name: "Updated User".to_string(), + score: 42, + }; + + let instruction = Instruction { + program_id, + accounts: accounts.to_account_metas(None), + data: instruction_data.data(), + }; + + // Execute the update + let result = rpc + .create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer]) + .await; + assert!(result.is_ok(), "Update record transaction should succeed"); + + // Warp to slot 200 to ensure we're past the update + rpc.warp_to_slot(200).unwrap(); + + // Fetch the account and verify compression_info.last_written_slot + 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(); + + // Verify the data was updated + assert_eq!(updated_user_record.name, "Updated User"); + assert_eq!(updated_user_record.score, 42); + assert_eq!(updated_user_record.owner, payer.pubkey()); + + // Verify compression_info.last_written_slot was updated to slot 150 + assert_eq!( + updated_user_record + .compression_info + .as_ref() + .unwrap() + .last_written_slot(), + 150 + ); + assert!(!updated_user_record + .compression_info + .as_ref() + .unwrap() + .is_compressed()); +} + +#[tokio::test] +async fn test_custom_compression_game_session() { + let program_id = anchor_compressible_derived::ID; + let config = ProgramTestConfig::new_v2( + true, + Some(vec![("anchor_compressible_derived", 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); + + // Initialize config + let result = initialize_compression_config( + &mut rpc, + &payer, + &program_id, + &payer, + 100, // compression delay + RENT_RECIPIENT, + vec![ADDRESS_SPACE[0]], + &CompressibleInstruction::INITIALIZE_COMPRESSION_CONFIG_DISCRIMINATOR, + None, + ) + .await; + assert!(result.is_ok(), "Initialize config should succeed"); + + // Create a game session + 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; + + // Warp forward to allow decompression + rpc.warp_to_slot(100).unwrap(); + + // Decompress the game session first to verify original state + decompress_single_game_session( + &mut rpc, + &payer, + &program_id, + &game_session_pda, + &_game_bump, + session_id, + "Battle Royale", + 100, + 0, // original score should be 0 + ) + .await; + + // Warp forward past compression delay to allow compression + rpc.warp_to_slot(250).unwrap(); + + // Test the custom compression trait - this demonstrates the core functionality + compress_game_session_with_custom_data( + &mut rpc, + &payer, + &program_id, + &game_session_pda, + session_id, + ) + .await; +} + +#[tokio::test] +async fn test_create_empty_compressed_account() { + let program_id = anchor_compressible_derived::ID; + let config = ProgramTestConfig::new_v2( + true, + Some(vec![("anchor_compressible_derived", 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); + + // Initialize compression config + let result = initialize_compression_config( + &mut rpc, + &payer, + &program_id, + &payer, + 100, + RENT_RECIPIENT, + vec![ADDRESS_SPACE[0]], + &CompressibleInstruction::INITIALIZE_COMPRESSION_CONFIG_DISCRIMINATOR, + None, + ) + .await; + assert!(result.is_ok(), "Initialize config should succeed"); + + // Create placeholder record using empty compressed account functionality + 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; + + // Verify the PDA still exists and has data + 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)" + ); + + // Verify we can read the PDA data + let placeholder_data = account.data; + let decompressed_placeholder_record = + 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()); + + // Verify empty compressed account was created + let address_tree_pubkey = rpc.get_address_tree_v2().queue; + let compressed_address = derive_address( + &placeholder_record_pda.to_bytes(), + &address_tree_pubkey.to_bytes(), + &program_id.to_bytes(), + ); + + let compressed_placeholder = rpc + .get_compressed_account(compressed_address, None) + .await + .unwrap() + .value; + + 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" + ); + + // Verify the compressed account is empty (length 0) + let compressed_data = compressed_placeholder.data.unwrap(); + assert_eq!( + compressed_data.data.len(), + 0, + "Compressed account data should be empty" + ); + + // This demonstrates the key difference from regular compression: + // The PDA still exists with data, and an empty compressed account was created + + // Step 2: Now compress the PDA (this will close the PDA and put data into the compressed account) + rpc.warp_to_slot(200).unwrap(); // Wait past compression delay + + compress_placeholder_record( + &mut rpc, + &payer, + &program_id, + &config_pda, + &placeholder_record_pda, + &placeholder_record_bump, + placeholder_id, + ) + .await; +} + +async fn create_record( + rpc: &mut LightProgramTest, + payer: &Keypair, + program_id: &Pubkey, + user_record_pda: &Pubkey, + state_tree_queue: Option, +) { + let config_pda = CompressibleConfig::derive_pda(program_id, 0).0; + + let mut remaining_accounts = PackedAccounts::default(); + let system_config = SystemAccountMetaConfig::new(*program_id); + let _ = remaining_accounts.add_system_accounts_small(system_config); + + let address_tree_pubkey = rpc.get_address_tree_v2().queue; + + let accounts = anchor_compressible_derived::accounts::CreateRecord { + user: payer.pubkey(), + user_record: *user_record_pda, + system_program: solana_sdk::system_program::ID, + config: config_pda, + rent_recipient: RENT_RECIPIENT, + }; + + let compressed_address = derive_address( + &user_record_pda.to_bytes(), + &address_tree_pubkey.to_bytes(), + &program_id.to_bytes(), + ); + + // Get validity proof from RPC + let rpc_result = rpc + .get_validity_proof( + vec![], + vec![AddressWithTree { + address: compressed_address, + tree: address_tree_pubkey, + }], + None, + ) + .await + .unwrap() + .value; + + // Pack tree infos into remaining accounts + let packed_tree_infos = rpc_result.pack_tree_infos(&mut remaining_accounts); + + // Get the packed address tree info + let address_tree_info = packed_tree_infos.address_trees[0]; + + // Get output state tree index + let output_state_tree_index = remaining_accounts.insert_or_get( + state_tree_queue.unwrap_or_else(|| rpc.get_random_state_tree_info().unwrap().queue), + ); + + // Get system accounts for the instruction + let (system_accounts, _, _) = remaining_accounts.to_account_metas(); + + // Create instruction data + let instruction_data = anchor_compressible_derived::instruction::CreateRecord { + name: "Test User".to_string(), + proof: rpc_result.proof, + compressed_address, + address_tree_info, + output_state_tree_index, + }; + + // Build the instruction + let instruction = Instruction { + program_id: *program_id, + accounts: [accounts.to_account_metas(None), system_accounts].concat(), + data: instruction_data.data(), + }; + + let cu = simulate_cu(rpc, payer, &instruction).await; + println!("CreateRecord CU consumed: {}", cu); + + // Create and send transaction + let result = rpc + .create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer]) + .await; + + assert!(result.is_ok(), "Transaction should succeed"); + + // should be empty + let user_record_account = rpc.get_account(*user_record_pda).await.unwrap(); + assert!( + user_record_account.is_some(), + "Account should exist after compression" + ); + + let account = user_record_account.unwrap(); + assert_eq!(account.lamports, 0, "Account lamports should be 0"); + + let user_record_data = account.data; + + assert!(user_record_data.is_empty(), "Account data should be empty"); +} + +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, +) { + // Setup remaining accounts for Light Protocol + let mut remaining_accounts = PackedAccounts::default(); + let system_config = SystemAccountMetaConfig::new(*program_id); + let _ = remaining_accounts.add_system_accounts_small(system_config); + + // Get address tree info + let address_tree_pubkey = rpc.get_address_tree_v2().queue; + + // Create the instruction + let accounts = anchor_compressible_derived::accounts::CreateGameSession { + player: payer.pubkey(), + game_session: *game_session_pda, + system_program: solana_sdk::system_program::ID, + config: *config_pda, + rent_recipient: RENT_RECIPIENT, + }; + + // Derive a new address for the compressed account + let compressed_address = derive_address( + &game_session_pda.to_bytes(), + &address_tree_pubkey.to_bytes(), + &program_id.to_bytes(), + ); + + // Get validity proof from RPC + let rpc_result = rpc + .get_validity_proof( + vec![], + vec![AddressWithTree { + address: compressed_address, + tree: address_tree_pubkey, + }], + None, + ) + .await + .unwrap() + .value; + + // Pack tree infos into remaining accounts + let packed_tree_infos = rpc_result.pack_tree_infos(&mut remaining_accounts); + + // Get the packed address tree info + let address_tree_info = packed_tree_infos.address_trees[0]; + + // Get output state tree index + let output_state_tree_index = remaining_accounts.insert_or_get( + state_tree_queue.unwrap_or_else(|| rpc.get_random_state_tree_info().unwrap().queue), + ); + + // Get system accounts for the instruction + let (system_accounts, _, _) = remaining_accounts.to_account_metas(); + + // Create instruction data + let instruction_data = anchor_compressible_derived::instruction::CreateGameSession { + session_id, + game_type: "Battle Royale".to_string(), + proof: rpc_result.proof, + compressed_address, + address_tree_info, + output_state_tree_index, + }; + + // Build the instruction + let instruction = Instruction { + program_id: *program_id, + accounts: [accounts.to_account_metas(None), system_accounts].concat(), + data: instruction_data.data(), + }; + + // Create and send transaction + let result = rpc + .create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer]) + .await; + + assert!(result.is_ok(), "Transaction should succeed"); + + // Verify the account is empty after compression + let game_session_account = rpc.get_account(*game_session_pda).await.unwrap(); + assert!( + game_session_account.is_some(), + "Account should exist after compression" + ); + + let account = game_session_account.unwrap(); + assert_eq!(account.lamports, 0, "Account lamports should be 0"); + assert!(account.data.is_empty(), "Account data should be empty"); + + let compressed_game_session = rpc + .get_compressed_account(compressed_address, None) + .await + .unwrap() + .value; + + assert_eq!(compressed_game_session.address, Some(compressed_address)); + assert!(compressed_game_session.data.is_some()); + + let buf = compressed_game_session.data.unwrap().data; + + let game_session = GameSession::deserialize(&mut &buf[..]).unwrap(); + + assert_eq!(game_session.session_id, session_id); + assert_eq!(game_session.game_type, "Battle Royale"); + assert_eq!(game_session.player, payer.pubkey()); + assert_eq!(game_session.score, 0); + assert!(game_session.compression_info.is_none()); +} + +#[allow(clippy::too_many_arguments)] +async fn decompress_multiple_pdas_with_ctoken( + rpc: &mut LightProgramTest, + payer: &Keypair, + program_id: &Pubkey, + user_record_pda: &Pubkey, + game_session_pda: &Pubkey, + session_id: u64, + expected_user_name: &str, + expected_game_type: &str, + expected_slot: u64, + compressed_token_account: light_client::indexer::CompressedTokenAccount, + native_token_account: Pubkey, +) { + let address_tree_pubkey = rpc.get_address_tree_v2().queue; + + // c pda USER_RECORD + 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; + + let user_account_data = c_user_pda.data.as_ref().unwrap(); + let c_user_record = UserRecord::deserialize(&mut &user_account_data.data[..]).unwrap(); + + // c pda GAME_SESSION + 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; + let game_account_data = c_game_pda.data.as_ref().unwrap(); + let c_game_session = GameSession::deserialize(&mut &game_account_data.data[..]).unwrap(); + + // Get validity proof for all three compressed accounts + let rpc_result = rpc + .get_validity_proof( + vec![ + c_user_pda.hash, + c_game_pda.hash, + compressed_token_account.clone().account.hash.clone(), + ], + vec![], + None, + ) + .await + .unwrap() + .value; + + let output_state_tree_info = rpc.get_random_state_tree_info().unwrap(); + + assert_eq!(compressed_token_account.token.owner, native_token_account); + + let instruction = + light_compressible_client::CompressibleInstruction::decompress_accounts_idempotent( + program_id, + &CompressibleInstruction::DECOMPRESS_ACCOUNTS_IDEMPOTENT_DISCRIMINATOR, + &payer.pubkey(), + &payer.pubkey(), + // must be same order as the compressed_accounts! + // &[*user_record_pda, *game_session_pda], + // &[native_token_account], + &[*user_record_pda, *game_session_pda, native_token_account], + &[ + // gets packed internally and never unpacked onchain: + ( + c_user_pda.clone(), + CompressedAccountVariant::UserRecord(c_user_record), + ), + ( + c_game_pda.clone(), + CompressedAccountVariant::GameSession(c_game_session), + ), + ( + compressed_token_account.clone().account, + CompressedAccountVariant::CompressibleTokenData( + CompressibleTokenDataWithVariant:: { + variant: CTokenAccountVariant::CTokenSigner, + token_data: compressed_token_account.clone().token, + }, + ), + ), + ], + rpc_result, + output_state_tree_info, + ) + .unwrap(); + + // Verify PDAs are uninitialized before decompression + 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"); + + // Verify UserRecord PDA is decompressed + 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 + ); + + // Verify GameSession PDA is decompressed + 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], + GameSession::DISCRIMINATOR, + "Game account anchor discriminator mismatch" + ); + + let decompressed_game_session = 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 + ); + + // Verify the native token account has the decompressed tokens + let token_account_data = rpc + .get_account(native_token_account) + .await + .unwrap() + .unwrap(); + // For now, just verify the account exists and has data + + assert!( + !token_account_data.data.is_empty(), + "Token account should have data" + ); + assert_eq!(token_account_data.owner, COMPRESSED_TOKEN_PROGRAM_ID.into()); + + // Ensure all compressed accounts are now empty (closed) + let compressed_user_record_data = rpc + .get_compressed_account(c_user_pda.clone().address.clone().unwrap(), None) + .await + .unwrap() + .value; + let compressed_game_session_data = rpc + .get_compressed_account(c_game_pda.clone().address.clone().unwrap(), None) + .await + .unwrap() + .value; + rpc.get_compressed_account_by_hash(compressed_token_account.clone().account.hash.clone(), None) + .await + .expect_err("Compressed token account should not be found"); + + assert!( + compressed_user_record_data.data.unwrap().data.is_empty(), + "Compressed user record should be closed/empty after decompression" + ); + assert!( + compressed_game_session_data.data.unwrap().data.is_empty(), + "Compressed game session should be closed/empty after decompression" + ); +} + +#[allow(clippy::too_many_arguments)] +async fn decompress_multiple_pdas( + rpc: &mut LightProgramTest, + payer: &Keypair, + program_id: &Pubkey, + user_record_pda: &Pubkey, + game_session_pda: &Pubkey, + session_id: u64, + expected_user_name: &str, + expected_game_type: &str, + expected_slot: u64, +) { + let address_tree_pubkey = rpc.get_address_tree_v2().queue; + + // c pda USER_RECORD + 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; + + let user_account_data = c_user_pda.data.as_ref().unwrap(); + + let c_user_record = UserRecord::deserialize(&mut &user_account_data.data[..]).unwrap(); + + // c pda GAME_SESSION + 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; + let game_account_data = c_game_pda.data.as_ref().unwrap(); + + let c_game_session = GameSession::deserialize(&mut &game_account_data.data[..]).unwrap(); + + // Get validity proof for both compressed accounts + 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(); + + // Use the new SDK helper function with typed data + let instruction = + light_compressible_client::CompressibleInstruction::decompress_accounts_idempotent( + program_id, + &CompressibleInstruction::DECOMPRESS_ACCOUNTS_IDEMPOTENT_DISCRIMINATOR, + &payer.pubkey(), + &payer.pubkey(), // rent_payer can be the same as fee_payer + &[*user_record_pda, *game_session_pda], + &[ + ( + c_user_pda, + CompressedAccountVariant::UserRecord(c_user_record), + ), + ( + c_game_pda, + CompressedAccountVariant::GameSession(c_game_session), + ), + ], + rpc_result, + output_state_tree_info, + ) + .unwrap(); + + // Verify PDAs are uninitialized before decompression + let user_pda_account = rpc.get_account(*user_record_pda).await.unwrap(); + assert_eq!( + user_pda_account.as_ref().map(|a| a.data.len()).unwrap_or(0), + 0, + "User PDA account data len must be 0 before decompression" + ); + + let game_pda_account = rpc.get_account(*game_session_pda).await.unwrap(); + assert_eq!( + game_pda_account.as_ref().map(|a| a.data.len()).unwrap_or(0), + 0, + "Game PDA account data len must be 0 before decompression" + ); + + let cu = simulate_cu(rpc, payer, &instruction).await; + println!("decompress_multiple_pdas CU consumed: {}", cu); + + let result = rpc + .create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer]) + .await; + assert!(result.is_ok(), "Decompress transaction should succeed"); + + // Verify UserRecord PDA is decompressed + 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 + ); + + // Verify GameSession PDA is decompressed + 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], + GameSession::DISCRIMINATOR, + "Game account anchor discriminator mismatch" + ); + + let decompressed_game_session = 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 + ); + + // Verify compressed accounts exist and have correct data + let c_game_pda = rpc + .get_compressed_account(game_compressed_address, None) + .await + .unwrap() + .value; + + assert!(c_game_pda.data.is_some()); + assert_eq!(c_game_pda.data.unwrap().data.len(), 0); +} + +async fn create_user_record_and_game_session( + rpc: &mut LightProgramTest, + user: &Keypair, + program_id: &Pubkey, + config_pda: &Pubkey, + user_record_pda: &Pubkey, + game_session_pda: &Pubkey, + session_id: u64, +) -> (light_client::indexer::CompressedTokenAccount, Pubkey) { + let state_tree_info = rpc.get_random_state_tree_info().unwrap(); + + // Setup remaining accounts for Light Protocol + 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_small(system_config); + + // Get address tree info + let address_tree_pubkey = rpc.get_address_tree_v2().queue; + + // Create a mint signer for the compressed mint + let decimals = 6u8; + let mint_authority_keypair = Keypair::new(); + let mint_authority = mint_authority_keypair.pubkey(); + let freeze_authority = mint_authority; // Same as mint authority for this example + let mint_signer = Keypair::new(); + let compressed_mint_address = + derive_compressed_mint_address(&mint_signer.pubkey(), &address_tree_pubkey); + + // Find mint bump for the instruction + let (spl_mint, mint_bump) = find_spl_mint_address(&mint_signer.pubkey()); + // Create the instruction + let accounts = anchor_compressible_derived::accounts::CreateUserRecordAndGameSession { + user: user.pubkey(), + user_record: *user_record_pda, + game_session: *game_session_pda, + mint_signer: mint_signer.pubkey(), + compressed_token_program: light_sdk_types::constants::C_TOKEN_PROGRAM_ID.into(), + system_program: solana_sdk::system_program::ID, + config: *config_pda, + rent_recipient: RENT_RECIPIENT, + mint_authority, + compress_token_program_cpi_authority: Pubkey::new_from_array(CPI_AUTHORITY_PDA), + }; + // Derive addresses for both compressed accounts + 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(), + ); + + // Get validity proof from RPC including mint address + 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); + + // Pack tree infos into remaining accounts + let packed_tree_infos = rpc_result.pack_tree_infos(&mut remaining_accounts); + + // Get the packed address tree info (all should use the same tree) + 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]; + + // Get system accounts for the instruction + let (system_accounts, _, _) = remaining_accounts.to_account_metas(); + + // Create instruction data + let instruction_data = + anchor_compressible_derived::instruction::CreateUserRecordAndGameSession { + account_data: anchor_compressible_derived::AccountCreationData { + user_name: "Combined User".to_string(), + session_id, + game_type: "Combined Game".to_string(), + // Add mint metadata + 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: anchor_compressible_derived::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, + // Add mint compression parameters + 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 { + base: BaseCompressedMint { + version: 1, + spl_mint: spl_mint.into(), + supply: 0, + decimals, + mint_authority: Some(mint_authority.into()), + freeze_authority: Some(freeze_authority.into()), + is_decompressed: false, + }, + extensions: None, + }, + }, + }, + }; + + // Build the instruction + let instruction = Instruction { + program_id: *program_id, + accounts: [accounts.to_account_metas(None), system_accounts].concat(), + data: instruction_data.data(), + }; + + // Create and send transaction + 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" + ); + + // Verify both accounts are empty after compression + let user_record_account = rpc.get_account(*user_record_pda).await.unwrap(); + assert!( + user_record_account.is_some(), + "User record account should exist after compression" + ); + let account = user_record_account.unwrap(); + assert_eq!( + account.lamports, 0, + "User record account lamports should be 0" + ); + assert!( + account.data.is_empty(), + "User record account data should be empty" + ); + + let game_session_account = rpc.get_account(*game_session_pda).await.unwrap(); + assert!( + game_session_account.is_some(), + "Game session account should exist after compression" + ); + let account = game_session_account.unwrap(); + assert_eq!( + account.lamports, 0, + "Game session account lamports should be 0" + ); + assert!( + account.data.is_empty(), + "Game session account data should be empty" + ); + + // Verify compressed accounts exist and have correct data + let compressed_user_record = rpc + .get_compressed_account(user_compressed_address, None) + .await + .unwrap() + .value; + + 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; + + 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); + + // SAME AS OWNER + let token_account_address = anchor_compressible_derived::get_ctokensigner_seeds( + &user.pubkey(), + &find_spl_mint_address(&mint_signer.pubkey()).0, + ) + .1; + + // Fetch the compressed token account that was created during the mint action + let compressed_token_accounts = rpc + .get_compressed_token_accounts_by_owner(&token_account_address, None, None) + .await + .unwrap() + .value; + + assert!( + !compressed_token_accounts.items.is_empty(), + "Should have at least one compressed token account" + ); + + // Get the first (and should be only) compressed token account + let compressed_token_account = compressed_token_accounts.items[0].clone(); + + (compressed_token_account, mint_signer.pubkey()) +} + +async fn compress_record( + rpc: &mut LightProgramTest, + payer: &Keypair, + program_id: &Pubkey, + user_record_pda: &Pubkey, + should_fail: bool, +) -> Result { + // Get the current decompressed user record data + 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" + ); + + // Setup remaining accounts for Light Protocol + let mut remaining_accounts = PackedAccounts::default(); + let system_config = SystemAccountMetaConfig::new(*program_id); + let _ = remaining_accounts.add_system_accounts_small(system_config); + + // Get address tree info + let address_tree_pubkey = rpc.get_address_tree_v2().queue; + + let address = derive_address( + &user_record_pda.to_bytes(), + &address_tree_pubkey.to_bytes(), + &program_id.to_bytes(), + ); + + let compressed_account = rpc + .get_compressed_account(address, None) + .await + .unwrap() + .value; + let compressed_address = compressed_account.address.unwrap(); + + // Get validity proof from RPC + 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, + anchor_compressible_derived::instruction::CompressAccountsIdempotent::DISCRIMINATOR, + &payer.pubkey(), + &payer.pubkey(), + &RENT_RECIPIENT, // rent_recipient + &[*user_record_pda], + &[account], + vec![anchor_compressible_derived::get_userrecord_seeds(&payer.pubkey()).0], // compressed_account + rpc_result, // validity_proof_with_context + output_state_tree_info, // output_state_tree_info + ) + .unwrap(); + + if !should_fail { + let cu = simulate_cu(rpc, payer, &instruction).await; + println!("CompressRecord CU consumed: {}", cu); + } + + // Create and send transaction + 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"); + } + + // Verify the PDA account is now empty (compressed) + let user_pda_account = rpc.get_account(*user_record_pda).await.unwrap(); + assert!( + user_pda_account.is_some(), + "Account should exist after compression" + ); + let account = user_pda_account.unwrap(); + assert_eq!( + account.lamports, 0, + "Account lamports should be 0 after compression" + ); + assert!( + account.data.is_empty(), + "Account data should be empty after compression" + ); + + // Verify the compressed account exists + let compressed_user_record = rpc + .get_compressed_account(compressed_address, None) + .await + .unwrap() + .value; + + assert_eq!(compressed_user_record.address, Some(compressed_address)); + assert!(compressed_user_record.data.is_some()); + + let buf = compressed_user_record.data.unwrap().data; + let user_record: UserRecord = UserRecord::deserialize(&mut &buf[..]).unwrap(); + + assert_eq!(user_record.name, "Test User"); + assert_eq!(user_record.score, 11); + assert_eq!(user_record.owner, payer.pubkey()); + assert!(user_record.compression_info.is_none()); + Ok(result.unwrap()) +} + +async fn decompress_single_user_record( + rpc: &mut LightProgramTest, + payer: &Keypair, + program_id: &Pubkey, + user_record_pda: &Pubkey, + _user_record_bump: &u8, + expected_user_name: &str, + expected_slot: u64, +) { + let address_tree_pubkey = rpc.get_address_tree_v2().queue; + + // Get compressed user record + 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; + + let user_account_data = c_user_pda.data.as_ref().unwrap(); + let c_user_record = UserRecord::deserialize(&mut &user_account_data.data[..]).unwrap(); + + // Get validity proof for the compressed account + 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(); + // Use the new SDK helper function with typed data + let instruction = + light_compressible_client::CompressibleInstruction::decompress_accounts_idempotent( + program_id, + &CompressibleInstruction::DECOMPRESS_ACCOUNTS_IDEMPOTENT_DISCRIMINATOR, + &payer.pubkey(), + &payer.pubkey(), // rent_payer can be the same as fee_payer + &[*user_record_pda], + &[( + c_user_pda, + CompressedAccountVariant::UserRecord(c_user_record), + )], + rpc_result, + output_state_tree_info, + ) + .unwrap(); + + // Verify PDA is uninitialized before decompression + 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"); + + // Verify UserRecord PDA is decompressed + 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 + ); +} + +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, +) { + // Setup remaining accounts for Light Protocol + let mut remaining_accounts = PackedAccounts::default(); + let system_config = SystemAccountMetaConfig::new(*program_id); + let _ = remaining_accounts.add_system_accounts_small(system_config); + + // Get address tree info + let address_tree_pubkey = rpc.get_address_tree_v2().queue; + + // Create the instruction + let accounts = anchor_compressible_derived::accounts::CreatePlaceholderRecord { + user: payer.pubkey(), + placeholder_record: *placeholder_record_pda, + system_program: solana_sdk::system_program::ID, + config: *config_pda, + rent_recipient: RENT_RECIPIENT, + }; + + // Derive a new address for the compressed account + let compressed_address = derive_address( + &placeholder_record_pda.to_bytes(), + &address_tree_pubkey.to_bytes(), + &program_id.to_bytes(), + ); + + // Get validity proof from RPC + let rpc_result = rpc + .get_validity_proof( + vec![], + vec![AddressWithTree { + address: compressed_address, + tree: address_tree_pubkey, + }], + None, + ) + .await + .unwrap() + .value; + + // Pack tree infos into remaining accounts + let packed_tree_infos = rpc_result.pack_tree_infos(&mut remaining_accounts); + + // Get the packed address tree info + let address_tree_info = packed_tree_infos.address_trees[0]; + + // Get output state tree index + let output_state_tree_index = + remaining_accounts.insert_or_get(rpc.get_random_state_tree_info().unwrap().queue); + + // Get system accounts for the instruction + let (system_accounts, _, _) = remaining_accounts.to_account_metas(); + + // Create instruction data + let instruction_data = anchor_compressible_derived::instruction::CreatePlaceholderRecord { + placeholder_id, + name: name.to_string(), + proof: rpc_result.proof, + compressed_address, + address_tree_info, + output_state_tree_index, + }; + + // Build the instruction + let instruction = Instruction { + program_id: *program_id, + accounts: [accounts.to_account_metas(None), system_accounts].concat(), + data: instruction_data.data(), + }; + + let cu = simulate_cu(rpc, payer, &instruction).await; + println!("CreatePlaceholderRecord CU consumed: {}", cu); + + // Create and send transaction + let result = rpc + .create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer]) + .await; + + assert!( + result.is_ok(), + "CreatePlaceholderRecord transaction should succeed" + ); +} + +async fn compress_placeholder_record( + rpc: &mut LightProgramTest, + payer: &Keypair, + program_id: &Pubkey, + _config_pda: &Pubkey, + placeholder_record_pda: &Pubkey, + _placeholder_record_bump: &u8, + placeholder_id: u64, +) { + let address_tree_pubkey = rpc.get_address_tree_v2().queue; + + // Get compressed placeholder record address + let placeholder_compressed_address = derive_address( + &placeholder_record_pda.to_bytes(), + &address_tree_pubkey.to_bytes(), + &program_id.to_bytes(), + ); + + // Get the compressed account that already exists (empty) + let compressed_placeholder = rpc + .get_compressed_account(placeholder_compressed_address, None) + .await + .unwrap() + .value; + + // Get validity proof from RPC + let rpc_result = rpc + .get_validity_proof(vec![compressed_placeholder.hash], vec![], None) + .await + .unwrap() + .value; + + let placeholder_seeds = + anchor_compressible_derived::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, + &anchor_compressible_derived::instruction::CompressAccountsIdempotent::DISCRIMINATOR, + &payer.pubkey(), + &payer.pubkey(), + &RENT_RECIPIENT, + &[*placeholder_record_pda], + &[account], + vec![placeholder_seeds.0], + rpc_result, + output_state_tree_info, + ) + .unwrap(); + + let cu = simulate_cu(rpc, payer, &instruction).await; + println!("CompressPlaceholderRecord CU consumed: {}", cu); + + let result = rpc + .create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer]) + .await; + + assert!( + result.is_ok(), + "CompressPlaceholderRecord transaction should succeed: {:?}", + result + ); + + // Check if PDA account is closed (it may or may not be depending on the compression behavior) + let _account = rpc.get_account(*placeholder_record_pda).await.unwrap(); + + // Verify compressed account now has the data + let compressed_placeholder_after = rpc + .get_compressed_account(placeholder_compressed_address, None) + .await + .unwrap() + .value; + + 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.len() > 0, + "Compressed account should contain the PDA data" + ); +} + +async fn compress_placeholder_record_for_double_test( + rpc: &mut LightProgramTest, + payer: &Keypair, + program_id: &Pubkey, + placeholder_record_pda: &Pubkey, + placeholder_id: u64, + previous_account: Option, +) -> Result { + let address_tree_pubkey = rpc.get_address_tree_v2().queue; + + // Get compressed placeholder record address + let placeholder_compressed_address = derive_address( + &placeholder_record_pda.to_bytes(), + &address_tree_pubkey.to_bytes(), + &program_id.to_bytes(), + ); + + // Get the compressed account that exists (initially empty, later with data) + let compressed_placeholder = rpc + .get_compressed_account(placeholder_compressed_address, None) + .await + .unwrap() + .value; + + // Get validity proof from RPC + let rpc_result = rpc + .get_validity_proof(vec![compressed_placeholder.hash], vec![], None) + .await + .unwrap() + .value; + + let placeholder_seeds = + anchor_compressible_derived::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, + &anchor_compressible_derived::instruction::CompressAccountsIdempotent::DISCRIMINATOR, + &payer.pubkey(), + &payer.pubkey(), + &RENT_RECIPIENT, + &[*placeholder_record_pda], + &accounts_to_compress, + vec![placeholder_seeds.0], + rpc_result, + output_state_tree_info, + ) + .unwrap(); + + // Create and send transaction + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer]) + .await +} + +async fn decompress_single_game_session( + rpc: &mut LightProgramTest, + payer: &Keypair, + program_id: &Pubkey, + game_session_pda: &Pubkey, + _game_bump: &u8, + session_id: u64, + expected_game_type: &str, + expected_slot: u64, + expected_score: u64, +) { + let address_tree_pubkey = rpc.get_address_tree_v2().queue; + + // Get compressed game session + 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; + + let game_account_data = c_game_pda.data.as_ref().unwrap(); + let c_game_session = GameSession::deserialize(&mut &game_account_data.data[..]).unwrap(); + + // Get validity proof for the compressed account + 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(); + + // Use the SDK helper function with typed data + let instruction = + light_compressible_client::CompressibleInstruction::decompress_accounts_idempotent( + program_id, + &CompressibleInstruction::DECOMPRESS_ACCOUNTS_IDEMPOTENT_DISCRIMINATOR, + &payer.pubkey(), + &payer.pubkey(), // rent_payer can be the same as fee_payer + &[*game_session_pda], + &[( + c_game_pda, + anchor_compressible_derived::CompressedAccountVariant::GameSession(c_game_session), + )], + 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"); + + // Verify GameSession PDA is decompressed + 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], + GameSession::DISCRIMINATOR, + "Game account anchor discriminator mismatch" + ); + + let decompressed_game_session = GameSession::try_deserialize(&mut &game_pda_data[..]).unwrap(); + assert_eq!(decompressed_game_session.session_id, session_id); + assert_eq!(decompressed_game_session.game_type, expected_game_type); + assert_eq!(decompressed_game_session.player, payer.pubkey()); + assert_eq!(decompressed_game_session.score, expected_score); + assert!(!decompressed_game_session + .compression_info + .as_ref() + .unwrap() + .is_compressed()); + assert_eq!( + decompressed_game_session + .compression_info + .as_ref() + .unwrap() + .last_written_slot(), + expected_slot + ); +} + +async fn compress_game_session_with_custom_data( + rpc: &mut LightProgramTest, + _payer: &Keypair, + _program_id: &Pubkey, + game_session_pda: &Pubkey, + _session_id: u64, +) { + let game_pda_account = rpc.get_account(*game_session_pda).await.unwrap().unwrap(); + let game_pda_data = game_pda_account.data; + let original_game_session = GameSession::try_deserialize(&mut &game_pda_data[..]).unwrap(); + + // Test the custom compression trait directly + let custom_compressed_data = match original_game_session.compress_as() { + std::borrow::Cow::Borrowed(data) => data.clone(), // Should never happen since compression_info must be None + std::borrow::Cow::Owned(data) => data, // Use owned data directly + }; + + // Verify that the custom compression works as expected + assert_eq!( + custom_compressed_data.session_id, original_game_session.session_id, + "Session ID should be kept" + ); + assert_eq!( + custom_compressed_data.player, original_game_session.player, + "Player should be kept" + ); + assert_eq!( + custom_compressed_data.game_type, original_game_session.game_type, + "Game type should be kept" + ); + assert_eq!( + custom_compressed_data.start_time, 0, + "Start time should be RESET to 0" + ); + assert_eq!( + custom_compressed_data.end_time, None, + "End time should be RESET to None" + ); + assert_eq!( + custom_compressed_data.score, 0, + "Score should be RESET to 0" + ); +} + +#[tokio::test] +async fn test_double_compression_attack() { + let program_id = anchor_compressible_derived::ID; + let config = ProgramTestConfig::new_v2( + true, + Some(vec![("anchor_compressible_derived", 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); + + // Initialize compression config + let result = initialize_compression_config( + &mut rpc, + &payer, + &program_id, + &payer, + 100, + RENT_RECIPIENT, + vec![ADDRESS_SPACE[0]], + &CompressibleInstruction::INITIALIZE_COMPRESSION_CONFIG_DISCRIMINATOR, + None, + ) + .await; + assert!(result.is_ok(), "Initialize config should succeed"); + + // Create placeholder record + 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; + + // Verify the PDA exists and has data before first compression + 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" + ); + + // Verify empty compressed account was created + let address_tree_pubkey = rpc.get_address_tree_v2().queue; + let compressed_address = derive_address( + &placeholder_record_pda.to_bytes(), + &address_tree_pubkey.to_bytes(), + &program_id.to_bytes(), + ); + + let compressed_placeholder_before = rpc + .get_compressed_account(compressed_address, None) + .await + .unwrap() + .value; + + assert_eq!( + compressed_placeholder_before.address, + Some(compressed_address), + "Empty compressed account should exist" + ); + assert_eq!( + compressed_placeholder_before.data.unwrap().data.len(), + 0, + "Compressed account should be empty initially" + ); + + // Wait past compression delay + rpc.warp_to_slot(200).unwrap(); + + // First compression - should succeed and move data from PDA to compressed account + 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 + ); + + // Verify PDA is now empty/closed after first compression + let placeholder_pda_after_first = rpc.get_account(placeholder_record_pda).await.unwrap(); + if let Some(account) = placeholder_pda_after_first { + assert_eq!( + account.lamports, 0, + "PDA should have 0 lamports after first compression" + ); + assert!( + account.data.is_empty(), + "PDA should have no data after first compression" + ); + } + + // Verify compressed account now has the data + let compressed_placeholder_after_first = rpc + .get_compressed_account(compressed_address, None) + .await + .unwrap() + .value; + + 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" + ); + + // Second compression attempt - should succeed idempotently (skip already compressed account) + let second_compression_result = compress_placeholder_record_for_double_test( + &mut rpc, + &payer, + &program_id, + &placeholder_record_pda, + placeholder_id, + Some(account_before), + ) + .await; + + // This should succeed because the instruction is idempotent + assert!( + second_compression_result.is_ok(), + "Second compression should succeed idempotently: {:?}", + second_compression_result + ); + + // Verify state hasn't changed after second compression attempt + let placeholder_pda_after_second = rpc.get_account(placeholder_record_pda).await.unwrap(); + if let Some(account) = placeholder_pda_after_second { + assert_eq!( + account.lamports, 0, + "PDA should still have 0 lamports after second compression" + ); + assert!( + account.data.is_empty(), + "PDA should still have no data after second compression" + ); + } + + let compressed_placeholder_after_second = rpc + .get_compressed_account(compressed_address, None) + .await + .unwrap() + .value; + + // Verify compressed account data is unchanged + 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" + ); +} + +async fn compress_token_account_after_decompress( + rpc: &mut LightProgramTest, + user: &Keypair, + program_id: &Pubkey, + _config_pda: &Pubkey, + token_account_address: 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], +) { + // Verify the token account exists and has the expected data + 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) = + anchor_compressible_derived::get_userrecord_seeds(&user.pubkey()); + let (game_session_seeds, game_session_pubkey) = + anchor_compressible_derived::get_gamesession_seeds(session_id); + let (token_account_seeds, token_account_address) = + anchor_compressible_derived::get_ctokensigner_seeds(&user.pubkey(), &mint); + + let mut accounts: Vec = vec![]; + + 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(); + + accounts.push(user_record_account); + accounts.push(game_session_account); + accounts.push(token_account); // must come last. + + assert_eq!(*user_record_pda, user_record_pubkey); + assert_eq!(*game_session_pda, game_session_pubkey); + assert_eq!(token_account_address, token_account_address); + + 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; + let game_session: CompressedAccount = rpc + .get_compressed_account(compressed_game_session_address, None) + .await + .unwrap() + .value; + + 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, + &anchor_compressible_derived::instruction::CompressAccountsIdempotent::DISCRIMINATOR, + &user.pubkey(), + &user.pubkey(), + &RENT_RECIPIENT, + &[ + user_record_pubkey, + game_session_pubkey, + token_account_address, + ], + &accounts, + vec![user_record_seeds, game_session_seeds, token_account_seeds], + proof_with_context, + random_tree_info, + ) + .unwrap(); + + // Send the transaction + let result = rpc + .create_and_send_transaction(&[instruction], &user.pubkey(), &[user]) + .await; + + assert!( + result.is_ok(), + "Compress token account transaction should succeed: {:?}", + result + ); + + // Verify the token account is now closed/empty + let token_account_after = rpc.get_account(token_account_address).await.unwrap(); + if let Some(account) = token_account_after { + assert_eq!( + account.lamports, 0, + "Token account should have 0 lamports after compression" + ); + assert!( + account.data.is_empty(), + "Token account should have no data after compression" + ); + } + + // Verify the compressed token account exists + let compressed_token_accounts = rpc + .get_compressed_token_accounts_by_owner(&token_account_address, None, None) + .await + .unwrap() + .value; + + assert!( + !compressed_token_accounts.items.is_empty(), + "Should have at least one compressed token account after compression" + ); + + let compressed_token = &compressed_token_accounts.items[0]; + assert_eq!( + compressed_token.token.mint, mint, + "Compressed token should have the same mint" + ); + assert_eq!( + compressed_token.token.owner, token_account_address, + "Compressed token owner should be the token account address" + ); + assert_eq!( + compressed_token.token.amount, amount, + "Compressed token should have the same amount" + ); + 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(); + + assert_eq!( + user_record_account.lamports, 0, + "User record account should be None" + ); + assert_eq!( + game_session_account.lamports, 0, + "Game session account should be None" + ); + assert_eq!(token_account.lamports, 0, "Token account should be None"); + assert!( + user_record_account.data.is_empty(), + "User record account should be empty" + ); + assert!( + game_session_account.data.is_empty(), + "Game session account should be empty" + ); + assert!( + token_account.data.is_empty(), + "Token account should be empty" + ); +} diff --git a/sdk-tests/anchor-compressible/CONFIG.md b/sdk-tests/anchor-compressible/CONFIG.md new file mode 100644 index 0000000000..387007e594 --- /dev/null +++ b/sdk-tests/anchor-compressible/CONFIG.md @@ -0,0 +1,94 @@ +# Compressible Config in anchor-compressible + +This program demonstrates how to use the Light SDK's compressible config system to manage compression parameters globally. + +## Overview + +The compressible config allows programs to: + +- Set global compression parameters (delay, rent recipient, address space) +- Ensure only authorized parties can modify these parameters +- Validate configuration at runtime + +## Instructions + +### 1. `initialize_compression_config` + +Creates the global config PDA. **Can only be called by the program's upgrade authority**. + +**Accounts:** + +- `payer`: Transaction fee payer +- `config`: Config PDA (derived with seed `"compressible_config"`) +- `program_data`: Program's data account (for upgrade authority validation) +- `authority`: Program's upgrade authority (must sign) +- `system_program`: System program + +**Parameters:** + +- `compression_delay`: Number of slots to wait before compression is allowed +- `rent_recipient`: Account that receives rent from compressed PDAs +- `address_space`: Address space for compressed accounts + +### 2. `update_compression_config` + +Updates the config. **Can only be called by the config's update authority**. + +**Accounts:** + +- `config`: Config PDA +- `authority`: Config's update authority (must sign) + +**Parameters (all optional):** + +- `new_compression_delay`: New compression delay +- `new_rent_recipient`: New rent recipient +- `new_address_space`: New address space +- `new_update_authority`: Transfer update authority to a new account + +### 3. `create_record` + +Creates a compressed user record using config values. + +**Additional Accounts:** + +- `config`: Config PDA +- `rent_recipient`: Must match the config's rent recipient + +### 4. `compress_record` + +Compresses a PDA using config values. + +**Additional Accounts:** + +- `config`: Config PDA +- `rent_recipient`: Must match the config's rent recipient + +The compression delay from the config is used to determine if enough time has passed since the last write. + +## Security Model + +1. **Config Creation**: Only the program's upgrade authority can create the initial config +2. **Config Updates**: Only the config's update authority can modify settings +3. **Rent Recipient Validation**: Instructions validate that the provided rent recipient matches the config +4. **Compression Delay**: Enforced based on config value + +## Deployment Process + +1. Deploy your program +2. **Immediately** call `initialize_compression_config` with the upgrade authority +3. Optionally transfer config update authority to a multisig or DAO +4. Monitor config changes + +## Example Usage + +See `examples/config_usage.rs` for complete examples. + +## Legacy Instructions + +The program still supports legacy instructions that use hardcoded values: + +- `create_record`: Uses hardcoded `ADDRESS_SPACE` and `RENT_RECIPIENT` +- `compress_record`: Uses hardcoded `COMPRESSION_DELAY` + +These are maintained for backward compatibility but new integrations should use the config-based versions. diff --git a/sdk-tests/anchor-compressible/Cargo.toml b/sdk-tests/anchor-compressible/Cargo.toml new file mode 100644 index 0000000000..82def91781 --- /dev/null +++ b/sdk-tests/anchor-compressible/Cargo.toml @@ -0,0 +1,54 @@ +[package] +name = "anchor-compressible" +version = "0.1.0" +description = "Simple Anchor program template with user records" +edition = "2021" + +[lib] +crate-type = ["cdylib", "lib"] +name = "anchor_compressible" + +[features] +no-entrypoint = [] +no-idl = [] +no-log-ix-name = [] +cpi = ["no-entrypoint"] +default = ["idl-build"] +idl-build = ["anchor-lang/idl-build", "light-sdk/idl-build"] +test-sbf = [] + +[dependencies] +light-sdk = { workspace = true, features = ["anchor", "idl-build", "v2", "anchor-discriminator-compat"] } +light-sdk-types = { workspace = true, features = ["v2"] } +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 } +light-compressed-token-sdk = { workspace = true, features = ["anchor"] } +light-compressed-token-types = { workspace = true, features = ["anchor"] } + +[dev-dependencies] +light-program-test = { workspace = true, features = ["v2"] } +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 } + +[lints.rust.unexpected_cfgs] +level = "allow" +check-cfg = [ + 'cfg(target_os, values("solana"))', + 'cfg(feature, values("frozen-abi", "no-entrypoint"))', +] diff --git a/sdk-tests/anchor-compressible/Xargo.toml b/sdk-tests/anchor-compressible/Xargo.toml new file mode 100644 index 0000000000..9e7d95be7f --- /dev/null +++ b/sdk-tests/anchor-compressible/Xargo.toml @@ -0,0 +1,2 @@ +[target.bpfel-unknown-unknown.dependencies.std] +features = [] \ No newline at end of file diff --git a/sdk-tests/anchor-compressible/src/lib.rs b/sdk-tests/anchor-compressible/src/lib.rs new file mode 100644 index 0000000000..c852498bab --- /dev/null +++ b/sdk-tests/anchor-compressible/src/lib.rs @@ -0,0 +1,1769 @@ +use anchor_lang::{ + prelude::*, + solana_program::{ + instruction::AccountMeta, + program::{invoke, invoke_signed}, + pubkey::Pubkey, + }, +}; +use anchor_spl::token_interface::TokenAccount; +use light_ctoken_types::{ + instructions::mint_action::CompressedMintWithContext, COMPRESSED_TOKEN_PROGRAM_ID, +}; +use light_sdk::{ + account::Size, + compressible::{ + compress_account_on_init, compress_empty_account_on_init, + prepare_account_for_decompression_idempotent, prepare_accounts_for_compression_on_init, + process_initialize_compression_config_checked, process_update_compression_config, + CompressAs, CompressibleConfig, CompressionInfo, HasCompressionInfo, Pack, Unpack, + }, + cpi::CpiInputs, + derive_light_cpi_signer, + instruction::{ + account_meta::CompressedAccountMetaNoLamportsNoAddress, PackedAccounts, + PackedAddressTreeInfo, ValidityProof, + }, + light_hasher::{DataHasher, Hasher}, + token::{CompressibleTokenDataWithVariant, PackedCompressibleTokenDataWithVariant}, + LightDiscriminator, LightHasher, +}; + +// Helper functions for getting PDA seeds - can be used by both program and client +pub fn get_user_record_seeds(fee_payer: &Pubkey) -> (Vec>, Pubkey) { + let seeds = [b"user_record".as_ref(), fee_payer.as_ref()]; + let (pda, bump) = Pubkey::find_program_address(&seeds, &crate::ID); + let bump_slice = vec![bump]; + let seeds_vec = vec![seeds[0].to_vec(), seeds[1].to_vec(), bump_slice]; + (seeds_vec, pda) +} + +pub fn get_game_session_seeds(session_id: u64) -> (Vec>, Pubkey) { + let session_id_le = session_id.to_le_bytes(); + let seeds = [b"game_session".as_ref(), session_id_le.as_ref()]; + let (pda, bump) = Pubkey::find_program_address(&seeds, &crate::ID); + let bump_slice = vec![bump]; + let seeds_vec = vec![seeds[0].to_vec(), seeds[1].to_vec(), bump_slice]; + (seeds_vec, pda) +} + +pub fn get_placeholder_record_seeds(placeholder_id: u64) -> (Vec>, Pubkey) { + let placeholder_id_le = placeholder_id.to_le_bytes(); + let seeds = [b"placeholder_record".as_ref(), placeholder_id_le.as_ref()]; + let (pda, bump) = Pubkey::find_program_address(&seeds, &crate::ID); + let bump_slice = vec![bump]; + let seeds_vec = vec![seeds[0].to_vec(), seeds[1].to_vec(), bump_slice]; + (seeds_vec, pda) +} + +use light_sdk_types::{CpiAccountsConfig, CpiAccountsSmall, CpiSigner}; + +declare_id!("FAMipfVEhN4hjCLpKCvjDXXfzLsoVTqQccXzePz1L1ah"); +pub const LIGHT_CPI_SIGNER: CpiSigner = + derive_light_cpi_signer!("FAMipfVEhN4hjCLpKCvjDXXfzLsoVTqQccXzePz1L1ah"); + +// You can implement this for each of your token account derivation paths. +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) +} + +#[derive(AnchorSerialize, AnchorDeserialize, Debug, Clone, Copy)] +#[repr(u8)] +pub enum CTokenAccountVariant { + CTokenSigner = 0, + AssociatedTokenAccount = 255, // TODO: add support. +} + +// Simple anchor program retrofitted with compressible accounts. +#[program] +pub mod anchor_compressible { + + use light_compressed_token_sdk::{ + create_compressible_token_account, + instructions::{ + create_mint_action_cpi, decompress_full_ctoken_accounts_with_indices, + find_spl_mint_address, DecompressFullIndices, MintActionInputs, + }, + }; + use light_sdk::compressible::{ + compress_account::prepare_account_for_compression, into_compressed_meta_with_address, + }; + use light_sdk_types::cpi_context_write::CpiContextWriteAccounts; + + use super::*; + + // auto-derived via macro. + pub fn initialize_compression_config( + ctx: Context, + compression_delay: u32, + rent_recipient: Pubkey, + address_space: Vec, + ) -> Result<()> { + process_initialize_compression_config_checked( + &ctx.accounts.config.to_account_info(), + &ctx.accounts.authority.to_account_info(), + &ctx.accounts.program_data.to_account_info(), + &rent_recipient, + address_space, + compression_delay, + 0, // one global config for now, so bump is 0. + &ctx.accounts.payer.to_account_info(), + &ctx.accounts.system_program.to_account_info(), + &crate::ID, + )?; + + Ok(()) + } + + // auto-derived via macro. + pub fn update_compression_config( + ctx: Context, + new_compression_delay: Option, + new_rent_recipient: Option, + new_address_space: Option>, + new_update_authority: Option, + ) -> Result<()> { + process_update_compression_config( + &ctx.accounts.config.to_account_info(), + &ctx.accounts.authority.to_account_info(), + new_update_authority.as_ref(), + new_rent_recipient.as_ref(), + new_address_space, + new_compression_delay, + &crate::ID, + )?; + + Ok(()) + } + + /// Compress multiple accounts (PDAs and token accounts) in a single instruction. + pub fn compress_accounts_idempotent<'info>( + ctx: Context<'_, '_, 'info, 'info, CompressAccountsIdempotent<'info>>, + proof: ValidityProof, + compressed_accounts: Vec, + signer_seeds: Vec>>, + system_accounts_offset: u8, + ) -> Result<()> { + let compression_config = + CompressibleConfig::load_checked(&ctx.accounts.config, &crate::ID)?; + if ctx.accounts.rent_recipient.key() != compression_config.rent_recipient { + return err!(ErrorCode::InvalidRentRecipient); + } + + let cpi_accounts = CpiAccountsSmall::new( + ctx.accounts.fee_payer.as_ref(), + &ctx.remaining_accounts[system_accounts_offset as usize..], + LIGHT_CPI_SIGNER, + ); + + // we use signer_seeds because compressed_accounts can be != accounts to + // decompress. + let pda_accounts_start = ctx.remaining_accounts.len() - signer_seeds.len(); + let solana_accounts = &ctx.remaining_accounts[pda_accounts_start..]; + + // Implement for tokens and for each of your program's compressible + // account types. + let mut token_accounts_to_compress = Vec::new(); + let mut compressed_pda_infos = Vec::new(); + let mut user_records = Vec::new(); + let mut game_sessions = Vec::new(); + let mut placeholder_records = 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 == &COMPRESSED_TOKEN_PROGRAM_ID.into() { + if let Ok(token_account) = InterfaceAccount::::try_from(account_info) + { + let account_signer_seeds = signer_seeds[i].clone(); + + token_accounts_to_compress.push( + light_compressed_token_sdk::TokenAccountToCompress { + token_account, + signer_seeds: account_signer_seeds, + }, + ); + } + } else if account_info.owner == &crate::ID { + let data = account_info.try_borrow_data()?; + // if data.len() < 8 { + // msg!("No. Account already compressed or uninitialized. Skipping."); + // continue; + // } + + let discriminator = &data[0..8]; + let meta = compressed_accounts[i]; + + // TOOD: consider CHECKING seeds. + match discriminator { + d if d == UserRecord::discriminator() => { + let mut anchor_account = Account::::try_from(account_info)?; + + let compressed_info = prepare_account_for_compression::( + &crate::ID, + &mut anchor_account, + &meta, + &cpi_accounts, + &compression_config.compression_delay, + &compression_config.address_space, + )?; + + user_records.push(anchor_account); + compressed_pda_infos.push(compressed_info); + } + d if d == GameSession::discriminator() => { + let mut anchor_account = Account::::try_from(account_info)?; + let compressed_info = prepare_account_for_compression::( + &crate::ID, + &mut anchor_account, + &meta, + &cpi_accounts, + &compression_config.compression_delay, + &compression_config.address_space, + )?; + + game_sessions.push(anchor_account); + compressed_pda_infos.push(compressed_info); + } + d if d == PlaceholderRecord::discriminator() => { + let mut anchor_account = + Account::::try_from(account_info)?; + let compressed_info = prepare_account_for_compression::( + &crate::ID, + &mut anchor_account, + &meta, + &cpi_accounts, + &compression_config.compression_delay, + &compression_config.address_space, + )?; + + placeholder_records.push(anchor_account); + compressed_pda_infos.push(compressed_info); + } + _ => { + panic!("Trying to compress with invalid account discriminator"); + } + } + } + } + let has_pdas = !compressed_pda_infos.is_empty(); + let has_tokens = !token_accounts_to_compress.is_empty(); + + // 1. compress and close token accounts in one CPI (no proof). + if has_tokens { + light_compressed_token_sdk::compress_and_close_token_accounts( + crate::ID, + &ctx.accounts.fee_payer, + cpi_accounts.authority().unwrap(), + ctx.accounts + .compressed_token_cpi_authority + .as_ref() + .unwrap(), + ctx.accounts.compressed_token_program.as_ref().unwrap(), + &ctx.accounts.config, + &ctx.accounts.rent_recipient, + ctx.remaining_accounts, + token_accounts_to_compress, + LIGHT_CPI_SIGNER, + )?; + } + // 2. compress and close PDAs in another CPI (with proof). + if has_pdas { + let cpi_inputs = CpiInputs::new(proof, compressed_pda_infos); + cpi_inputs.invoke_light_system_program_small(cpi_accounts)?; + } + + // Close all PDA accounts + for anchor_account in user_records.iter() { + anchor_account.close(ctx.accounts.rent_recipient.clone())?; + } + for anchor_account in game_sessions.iter() { + anchor_account.close(ctx.accounts.rent_recipient.clone())?; + } + for anchor_account in placeholder_records.iter() { + anchor_account.close(ctx.accounts.rent_recipient.clone())?; + } + + Ok(()) + } + + // auto-derived via macro. takes the tagged account structs via + // add_compressible_accounts macro and derives the relevant variant type and + // dispatcher. The instruction can be used with any number of any of the + // tagged account structs. It's idempotent; it will not fail if the accounts + // are already decompressed. + pub fn decompress_accounts_idempotent<'info>( + ctx: Context<'_, '_, '_, 'info, DecompressAccountsIdempotent<'info>>, + proof: ValidityProof, + compressed_accounts: Vec, + system_accounts_offset: u8, + ) -> Result<()> { + // Load config + let compression_config = + CompressibleConfig::load_checked(&ctx.accounts.config, &crate::ID)?; + let address_space = compression_config.address_space[0]; + + let (mut has_tokens, mut has_pdas) = (false, false); + for c in &compressed_accounts { + match c.data { + CompressedAccountVariant::CompressibleTokenAccountPacked(_) => has_tokens = true, + _ => has_pdas = true, + } + if has_tokens && has_pdas { + break; + } + } + + let cpi_accounts = if has_tokens && has_pdas { + CpiAccountsSmall::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 { + CpiAccountsSmall::new( + ctx.accounts.fee_payer.as_ref(), + &ctx.remaining_accounts[system_accounts_offset as usize..], + LIGHT_CPI_SIGNER, + ) + }; + + // the onchain pdas must always be the last accounts. + let pda_accounts_start = ctx.remaining_accounts.len() - compressed_accounts.len(); + let solana_accounts = &ctx.remaining_accounts[pda_accounts_start..]; + + let mut compressed_token_accounts = Vec::new(); + let mut compressed_pda_infos = Vec::new(); + + for (i, compressed_data) in compressed_accounts.clone().into_iter().enumerate() { + // Implement pack and unpack traits in such a way that unpack always + // returns the onchain struct as you want it to be stored onchain. + // The packed version should **only** be used to send over the wire + // more efficiently. Indices should also only reference the + // account_infos passed as remaining_accounts **after** the system + // accounts. + let unpacked_data = compressed_data + .data + .unpack(cpi_accounts.post_system_accounts().unwrap())?; + + match unpacked_data { + CompressedAccountVariant::UserRecord(data) => { + let (seeds_vec, _) = get_user_record_seeds(&ctx.accounts.fee_payer.key()); + + let compressed_infos = + prepare_account_for_decompression_idempotent::( + &crate::ID, + data, + into_compressed_meta_with_address( + &compressed_data.meta, + &solana_accounts[i], + address_space, + &crate::ID, + ), + &solana_accounts[i], + &ctx.accounts.rent_payer, + &cpi_accounts, + seeds_vec + .iter() + .map(|v| v.as_slice()) + .collect::>() + .as_slice(), + )?; + compressed_pda_infos.extend(compressed_infos); + } + CompressedAccountVariant::GameSession(data) => { + let (seeds_vec, _) = get_game_session_seeds(data.session_id); + + let compressed_infos = + prepare_account_for_decompression_idempotent::( + &crate::ID, + data, + into_compressed_meta_with_address( + &compressed_data.meta, + &solana_accounts[i], + address_space, + &crate::ID, + ), + &solana_accounts[i], + &ctx.accounts.rent_payer, + &cpi_accounts, + seeds_vec + .iter() + .map(|v| v.as_slice()) + .collect::>() + .as_slice(), + )?; + compressed_pda_infos.extend(compressed_infos); + } + CompressedAccountVariant::PlaceholderRecord(data) => { + let (seeds_vec, _) = get_placeholder_record_seeds(data.placeholder_id); + + let compressed_infos = + prepare_account_for_decompression_idempotent::( + &crate::ID, + data, + into_compressed_meta_with_address( + &compressed_data.meta, + &solana_accounts[i], + address_space, + &crate::ID, + ), + &solana_accounts[i], + &ctx.accounts.rent_payer, + &cpi_accounts, + seeds_vec + .iter() + .map(|v| v.as_slice()) + .collect::>() + .as_slice(), + )?; + compressed_pda_infos.extend(compressed_infos); + } + CompressedAccountVariant::CompressibleTokenAccountPacked(data) => { + compressed_token_accounts.push((data, compressed_data.meta)); + } + CompressedAccountVariant::CompressibleTokenData(_) => { + unreachable!(); + } + CompressedAccountVariant::PackedUserRecord(_) => { + unreachable!() + } + } + } + + // set new based on actually uninitialized accounts. + let has_pdas = !compressed_pda_infos.is_empty(); + let has_tokens = !compressed_token_accounts.is_empty(); + if !has_pdas && !has_tokens { + msg!("All accounts already initialized."); + return Ok(()); + } + + let fee_payer = ctx.accounts.fee_payer.as_ref(); + let authority = cpi_accounts.authority().unwrap(); + let cpi_context = cpi_accounts.cpi_context().unwrap(); + + // First CPI. + if has_pdas && has_tokens { + // we only need the subset for the first cpi because we write into + // the cpi_context. + let system_cpi_accounts = CpiContextWriteAccounts { + fee_payer, + authority, + cpi_context, + cpi_signer: LIGHT_CPI_SIGNER, + }; + let cpi_inputs = CpiInputs::new_first_cpi(compressed_pda_infos, vec![]); + cpi_inputs.invoke_light_system_program_cpi_context(system_cpi_accounts)?; + } else if has_pdas { + let cpi_inputs = CpiInputs::new(proof, compressed_pda_infos); + cpi_inputs.invoke_light_system_program_small(cpi_accounts.clone())?; + } + + let mut token_decompress_indices = Vec::new(); + let mut token_signers_seeds = Vec::new(); + let packed_accounts = cpi_accounts.post_system_accounts().unwrap(); + + for (token_data, meta) in compressed_token_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(); + + // seeds for ctoken. match on variant. + let ctoken_signer_seeds = match token_data.variant { + CTokenAccountVariant::CTokenSigner => { + let (seeds, _) = get_ctoken_signer_seeds(&fee_payer.key(), &mint_info.key()); + seeds + } + CTokenAccountVariant::AssociatedTokenAccount => unreachable!(), + }; + + create_compressible_token_account( + authority, + fee_payer, + &owner_info, + &mint_info, + cpi_accounts.system_program().unwrap(), + ctx.accounts.compressed_token_program.as_ref().unwrap(), + &ctoken_signer_seeds + .iter() + .map(|s| s.as_slice()) + .collect::>(), + fee_payer, // rent_auth + fee_payer, // rent_recipient + 0, // slots_until_compression + )?; + + let decompress_index = + DecompressFullIndices::from((token_data.token_data, meta, owner_index)); + + token_decompress_indices.push(decompress_index); + token_signers_seeds.extend(ctoken_signer_seeds); + } + + if has_tokens { + let ctoken_ix = 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(ProgramError::from)?; + + let mut all_account_infos = vec![fee_payer.to_account_info()]; + all_account_infos.extend( + ctx.accounts + .compressed_token_cpi_authority + .to_account_infos(), + ); + all_account_infos.extend(ctx.accounts.compressed_token_program.to_account_infos()); + all_account_infos.extend(ctx.accounts.rent_payer.to_account_infos()); + all_account_infos.extend(ctx.accounts.config.to_account_infos()); + all_account_infos.extend(cpi_accounts.to_account_infos()); + + let seed_refs = token_signers_seeds + .iter() + .map(|s| s.as_slice()) + .collect::>(); + invoke_signed( + &ctoken_ix, + all_account_infos.as_slice(), + &[seed_refs.as_slice()], + )?; + } + Ok(()) + } + + pub fn create_record<'info>( + ctx: Context<'_, '_, '_, 'info, CreateRecord<'info>>, + name: String, + proof: ValidityProof, + compressed_address: [u8; 32], + address_tree_info: PackedAddressTreeInfo, + output_state_tree_index: u8, + ) -> Result<()> { + let user_record = &mut ctx.accounts.user_record; + + // 1. Load config from the config account + let config = CompressibleConfig::load_checked(&ctx.accounts.config, &crate::ID)?; + + user_record.owner = ctx.accounts.user.key(); + user_record.name = name; + user_record.score = 11; + + // 2. Verify rent recipient matches config + if ctx.accounts.rent_recipient.key() != config.rent_recipient { + return err!(ErrorCode::InvalidRentRecipient); + } + + // 3. Create CPI accounts + let user_account_info = ctx.accounts.user.to_account_info(); + let cpi_accounts = + CpiAccountsSmall::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(), + true, + Some(0), + ); + + compress_account_on_init::( + user_record, + &compressed_address, + &new_address_params, + output_state_tree_index, + cpi_accounts, + &config.address_space, + &ctx.accounts.rent_recipient, + proof, + )?; + + // at the end of the instruction we always clean up all onchain pdas that we compressed + user_record.close(ctx.accounts.rent_recipient.to_account_info())?; + + Ok(()) + } + + // Must be manually implemented. + pub fn create_game_session<'info>( + ctx: Context<'_, '_, '_, 'info, CreateGameSession<'info>>, + session_id: u64, + game_type: String, + proof: ValidityProof, + compressed_address: [u8; 32], + address_tree_info: PackedAddressTreeInfo, + output_state_tree_index: u8, + ) -> Result<()> { + let game_session = &mut ctx.accounts.game_session; + + // Load config from the config account + let config = CompressibleConfig::load_checked(&ctx.accounts.config, &crate::ID)?; + + // Set your account data. + game_session.session_id = session_id; + game_session.player = ctx.accounts.player.key(); + game_session.game_type = game_type; + game_session.start_time = Clock::get()?.unix_timestamp as u64; + game_session.end_time = None; + game_session.score = 0; + + // Check that rent recipient matches your config. + if ctx.accounts.rent_recipient.key() != config.rent_recipient { + return err!(ErrorCode::InvalidRentRecipient); + } + + // Create CPI accounts. + let player_account_info = ctx.accounts.player.to_account_info(); + let cpi_accounts = CpiAccountsSmall::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(), + true, + Some(0), + ); + + // Call at the end of your init instruction to compress the pda account + // safely. This also closes the pda account. The account can then be + // decompressed by anyone at any time via the + // decompress_accounts_idempotent instruction. Creates a unique cPDA to + // ensure that the account cannot be re-inited only decompressed. + compress_account_on_init::( + game_session, + &compressed_address, + &new_address_params, + output_state_tree_index, + cpi_accounts, + &config.address_space, + &ctx.accounts.rent_recipient, + proof, + )?; + + game_session.close(ctx.accounts.rent_recipient.to_account_info())?; + + Ok(()) + } + + // Must be manually implemented. + pub fn create_user_record_and_game_session<'info>( + ctx: Context<'_, '_, '_, 'info, CreateUserRecordAndGameSession<'info>>, + account_data: AccountCreationData, + compression_params: CompressionParams, + ) -> Result<()> { + let user_record = &mut ctx.accounts.user_record; + let game_session = &mut ctx.accounts.game_session; + + // Load your config checked. + let config = CompressibleConfig::load_checked(&ctx.accounts.config, &crate::ID)?; + + // Check that rent recipient matches your config. + if ctx.accounts.rent_recipient.key() != config.rent_recipient { + return err!(ErrorCode::InvalidRentRecipient); + } + + // 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 = CpiAccountsSmall::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(), true, Some(0)); + let game_new_address_params = compression_params + .game_address_tree_info + .into_new_address_params_assigned_packed(game_session.key().to_bytes(), true, Some(1)); + + let mut all_compressed_infos = Vec::new(); + + // Prepares the firstpda account for compression. compress the pda + // account safely. This also closes the pda account. safely. This also + // closes the pda account. The account can then be decompressed by + // anyone at any time via the decompress_accounts_idempotent + // instruction. Creates a unique cPDA to ensure that the account cannot + // be re-inited only decompressed. + let user_compressed_infos = prepare_accounts_for_compression_on_init::( + &[user_record], + &[compression_params.user_compressed_address], + &[user_new_address_params], + &[compression_params.user_output_state_tree_index], + &cpi_accounts, + &config.address_space, + &ctx.accounts.rent_recipient, + )?; + + all_compressed_infos.extend(user_compressed_infos); + + // Process GameSession for compression. compress the pda account safely. + // This also closes the pda account. The account can then be + // decompressed by anyone at any time via the + // decompress_accounts_idempotent instruction. Creates a unique cPDA to + // ensure that the account cannot be re-inited only decompressed. + let game_compressed_infos = prepare_accounts_for_compression_on_init::( + &[game_session], + &[compression_params.game_compressed_address], + &[game_new_address_params], + &[compression_params.game_output_state_tree_index], + &cpi_accounts, + &config.address_space, + &ctx.accounts.rent_recipient, + )?; + all_compressed_infos.extend(game_compressed_infos); + + let cpi_inputs = CpiInputs::new_first_cpi( + all_compressed_infos, + vec![user_new_address_params, game_new_address_params], + ); + + 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, + }; + cpi_inputs.invoke_light_system_program_cpi_context(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 + }, + ], + lamports: None, + token_account_version: 2, + }, + ]; + + let output_queue = *cpi_accounts.tree_accounts().unwrap()[0].key; // Same tree as PDA + let address_tree_pubkey = *cpi_accounts.tree_accounts().unwrap()[1].key; // Same tree as PDA + + let mint_action_inputs = MintActionInputs { + compressed_mint_inputs: compression_params.mint_with_context.clone(), + mint_seed: ctx.accounts.mint_signer.key(), + mint_bump: Some(compression_params.mint_bump), + create_mint: true, + authority: ctx.accounts.mint_authority.key(), + payer: ctx.accounts.user.key(), + proof: compression_params.proof.into(), + actions, + input_queue: None, // Not needed for create_mint: true + output_queue, + tokens_out_queue: Some(output_queue), // For MintTo actions + address_tree_pubkey, + token_pool: None, // Not needed for simple compressed mint creation + }; + + let mint_action_instruction = create_mint_action_cpi( + mint_action_inputs, + Some(light_ctoken_types::instructions::mint_action::CpiContext { + set_context: false, + first_set_context: false, + in_tree_index: 1, // address tree + in_queue_index: 0, + out_queue_index: 0, + token_out_queue_index: 0, + assigned_account_index: 2, + }), + 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.compressed_token_program.to_account_info()); + account_infos.push(ctx.accounts.mint_authority.to_account_info()); + account_infos.push(ctx.accounts.mint_signer.to_account_info()); + account_infos.push(ctx.accounts.user.to_account_info()); + + // Invoke the mint action instruction directly + invoke(&mint_action_instruction, &account_infos)?; + + // at the end of the instruction we always clean up all onchain pdas that we compressed + user_record.close(ctx.accounts.rent_recipient.to_account_info())?; + game_session.close(ctx.accounts.rent_recipient.to_account_info())?; + + Ok(()) + } + + /// Creates an empty compressed account while keeping the PDA intact. + /// This demonstrates the compress_empty_account_on_init functionality. + pub fn create_placeholder_record<'info>( + ctx: Context<'_, '_, '_, 'info, CreatePlaceholderRecord<'info>>, + placeholder_id: u64, + name: String, + proof: ValidityProof, + compressed_address: [u8; 32], + address_tree_info: PackedAddressTreeInfo, + output_state_tree_index: u8, + ) -> Result<()> { + let placeholder_record = &mut ctx.accounts.placeholder_record; + + let config = CompressibleConfig::load_checked(&ctx.accounts.config, &crate::ID)?; + + placeholder_record.owner = ctx.accounts.user.key(); + placeholder_record.name = name; + placeholder_record.placeholder_id = placeholder_id; + + // Initialize compression_info for the PDA + *placeholder_record.compression_info_mut_opt() = + Some(super::CompressionInfo::new_decompressed()?); + placeholder_record + .compression_info_mut() + .bump_last_written_slot()?; + + // Verify rent recipient matches config + if ctx.accounts.rent_recipient.key() != config.rent_recipient { + return err!(ErrorCode::InvalidRentRecipient); + } + + // Create CPI accounts + let user_account_info = ctx.accounts.user.to_account_info(); + let cpi_accounts = + CpiAccountsSmall::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(), + true, + Some(0), + ); + + // Use the new compress_empty_account_on_init function + // This creates an empty compressed account but does NOT close the PDA + compress_empty_account_on_init::( + placeholder_record, + &compressed_address, + &new_address_params, + output_state_tree_index, + cpi_accounts, + &config.address_space, + proof, + )?; + + // Note we do not actually close this account yet because in this + // example we only create _empty_ compressed account without fully + // compressing it yet. + Ok(()) + } + + pub fn update_record(ctx: Context, name: String, score: u64) -> Result<()> { + let user_record = &mut ctx.accounts.user_record; + + user_record.name = name; + user_record.score = score; + + // 1. Must manually set compression info + user_record + .compression_info_mut() + .bump_last_written_slot()?; + + Ok(()) + } + + pub fn update_game_session( + ctx: Context, + _session_id: u64, + new_score: u64, + ) -> Result<()> { + let game_session = &mut ctx.accounts.game_session; + + game_session.score = new_score; + game_session.end_time = Some(Clock::get()?.unix_timestamp as u64); + + // Must manually set compression info + game_session + .compression_info_mut() + .bump_last_written_slot()?; + + Ok(()) + } +} + +#[derive(Accounts)] +pub struct CreateRecord<'info> { + #[account(mut)] + pub user: Signer<'info>, + #[account( + init, + payer = user, + // discriminator + owner + string len + name + score + + // option. Note that in the onchain space + // CompressionInfo is always Some. + space = 8 + 32 + 4 + 32 + 8 + 10, + seeds = [b"user_record", user.key().as_ref()], + bump, + )] + pub user_record: Account<'info, UserRecord>, + /// Needs to be here for the init anchor macro to work. + pub system_program: Program<'info, System>, + /// The global config account + /// CHECK: Config is validated by the SDK's load_checked method + pub config: AccountInfo<'info>, + /// Rent recipient - must match config + /// CHECK: Rent recipient is validated against the config + #[account(mut)] + pub rent_recipient: AccountInfo<'info>, +} + +#[derive(Accounts)] +#[instruction(placeholder_id: u64)] +pub struct CreatePlaceholderRecord<'info> { + #[account(mut)] + pub user: Signer<'info>, + #[account( + init, + payer = user, + // discriminator + compression_info + owner + string len + name + placeholder_id + space = 8 + 10 + 32 + 4 + 32 + 8, + seeds = [b"placeholder_record", placeholder_id.to_le_bytes().as_ref()], + bump, + )] + pub placeholder_record: Account<'info, PlaceholderRecord>, + /// Needs to be here for the init anchor macro to work. + pub system_program: Program<'info, System>, + /// The global config account + /// CHECK: Config is validated by the SDK's load_checked method + pub config: AccountInfo<'info>, + /// Rent recipient - must match config + /// CHECK: Rent recipient is validated against the config + #[account(mut)] + pub rent_recipient: AccountInfo<'info>, +} + +#[derive(Accounts)] +#[instruction(account_data: AccountCreationData)] +pub struct CreateUserRecordAndGameSession<'info> { + #[account(mut)] + pub user: Signer<'info>, + #[account( + init, + payer = user, + // discriminator + owner + string len + name + score + + // option. Note that in the onchain space + // CompressionInfo is always Some. + space = 8 + 32 + 4 + 32 + 8 + 10, + seeds = [b"user_record", user.key().as_ref()], + bump, + )] + pub user_record: Account<'info, UserRecord>, + #[account( + init, + payer = user, + // discriminator + option + session_id + player + + // string len + game_type + start_time + end_time(Option) + score + space = 8 + 10 + 8 + 32 + 4 + 32 + 8 + 9 + 8, + seeds = [b"game_session", account_data.session_id.to_le_bytes().as_ref()], + bump, + )] + pub game_session: Account<'info, GameSession>, + + // Compressed mint creation accounts - only token-specific ones needed + /// The mint signer used for PDA derivation + pub mint_signer: Signer<'info>, + + /// The mint authority used for PDA derivation + pub mint_authority: Signer<'info>, + + /// Compressed token program + /// CHECK: Program ID validated using COMPRESSED_TOKEN_PROGRAM_ID constant + pub compressed_token_program: UncheckedAccount<'info>, + + /// CHECK: CPI authority of the compressed token program + pub compress_token_program_cpi_authority: UncheckedAccount<'info>, + + /// Needs to be here for the init anchor macro to work. + pub system_program: Program<'info, System>, + /// The global config account + /// CHECK: Config is validated by the SDK's load_checked method + pub config: AccountInfo<'info>, + /// Rent recipient - must match config + /// CHECK: Rent recipient is validated against the config + #[account(mut)] + pub rent_recipient: AccountInfo<'info>, +} + +#[derive(Accounts)] +#[instruction(session_id: u64)] +pub struct CreateGameSession<'info> { + #[account(mut)] + pub player: Signer<'info>, + #[account( + init, + payer = player, + space = 8 + 9 + 8 + 32 + 4 + 32 + 8 + 9 + 8, // discriminator + compression_info + session_id + player + string len + game_type + start_time + end_time(Option) + score + seeds = [b"game_session", session_id.to_le_bytes().as_ref()], + bump, + )] + pub game_session: Account<'info, GameSession>, + pub system_program: Program<'info, System>, + /// The global config account + /// CHECK: Config is validated by the SDK's load_checked method + pub config: AccountInfo<'info>, + /// Rent recipient - must match config + /// CHECK: Rent recipient is validated against the config + #[account(mut)] + pub rent_recipient: AccountInfo<'info>, +} + +#[derive(Accounts)] +pub struct UpdateRecord<'info> { + #[account(mut)] + pub user: Signer<'info>, + #[account( + mut, + seeds = [b"user_record", user.key().as_ref()], + bump, + constraint = user_record.owner == user.key() + )] + pub user_record: Account<'info, UserRecord>, +} + +#[derive(Accounts)] +#[instruction(session_id: u64)] +pub struct UpdateGameSession<'info> { + #[account(mut)] + pub player: Signer<'info>, + #[account( + mut, + seeds = [b"game_session", session_id.to_le_bytes().as_ref()], + bump, + constraint = game_session.player == player.key() + )] + pub game_session: Account<'info, GameSession>, +} + +#[derive(Accounts)] +pub struct CompressRecord<'info> { + #[account(mut)] + pub user: Signer<'info>, + #[account( + mut, + seeds = [b"user_record", user.key().as_ref()], + bump, + constraint = pda_to_compress.owner == user.key() + )] + pub pda_to_compress: Account<'info, UserRecord>, + // pub system_program: Program<'info, System>, + /// The global config account + /// CHECK: Config is validated by the SDK's load_checked method + pub config: AccountInfo<'info>, + /// Rent recipient - must match config + /// CHECK: Rent recipient is validated against the config + #[account(mut)] + pub rent_recipient: AccountInfo<'info>, +} + +#[derive(Accounts)] +#[instruction(session_id: u64)] +pub struct CompressGameSession<'info> { + #[account(mut)] + pub player: Signer<'info>, + #[account( + mut, + seeds = [b"game_session", session_id.to_le_bytes().as_ref()], + bump, + constraint = pda_to_compress.player == player.key() + )] + pub pda_to_compress: Account<'info, GameSession>, + /// The global config account + /// CHECK: Config is validated by the SDK's load_checked method + pub config: AccountInfo<'info>, + /// Rent recipient - must match config + /// CHECK: Rent recipient is validated against the config + #[account(mut)] + pub rent_recipient: AccountInfo<'info>, +} + +#[derive(Accounts)] +pub struct CompressPlaceholderRecord<'info> { + #[account(mut)] + pub user: Signer<'info>, + #[account( + mut, + constraint = pda_to_compress.owner == user.key() + )] + pub pda_to_compress: Account<'info, PlaceholderRecord>, + /// The global config account + /// CHECK: Config is validated by the SDK's load_checked method + pub config: AccountInfo<'info>, + /// Rent recipient - must match config + /// CHECK: Rent recipient is validated against the config + #[account(mut)] + pub rent_recipient: AccountInfo<'info>, +} + +#[derive(Accounts)] +pub struct CompressTokenAccountCtokenSigner<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + pub rent_authority: Signer<'info>, + /// CHECK: todo + pub user: UncheckedAccount<'info>, + /// CHECK: todo + compressed_token_cpi_authority: UncheckedAccount<'info>, + /// CHECK: todo + compressed_token_program: UncheckedAccount<'info>, + + #[account( + mut, + seeds = [b"ctoken_signer", user.key().as_ref(), token_account_to_compress.mint.as_ref()], + bump, + )] + pub token_account_to_compress: InterfaceAccount<'info, TokenAccount>, + /// The global config account + /// CHECK: Config is validated by the SDK's load_checked method + pub config: AccountInfo<'info>, + /// Rent recipient - must match config + /// CHECK: Rent recipient is validated against the config + #[account(mut)] + pub rent_recipient: AccountInfo<'info>, +} + +#[derive(Accounts)] +pub struct CompressAccountsIdempotent<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + /// The global config account + /// CHECK: Config is validated by the SDK's load_checked method + pub config: AccountInfo<'info>, + /// Rent recipient - must match config + /// CHECK: Rent recipient is validated against the config + #[account(mut)] + pub rent_recipient: AccountInfo<'info>, + + /// CHECK: compression_authority must be the rent_authority defined when creating the token account. + #[account(mut)] + pub token_compression_authority: AccountInfo<'info>, + + // Optional token-specific accounts (only needed when compressing token accounts) + /// Compressed token program + /// CHECK: Program ID validated to be cTokenmWW8bLPjZEBAUgYy3zKxQZW6VKi7bqNFEVv3m + pub compressed_token_program: Option>, + + /// CPI authority PDA of the compressed token program + /// CHECK: PDA derivation validated with seeds ["cpi_authority"] and bump 254 + pub compressed_token_cpi_authority: Option>, + // Remaining accounts: + // - After system_accounts_offset: Light Protocol system accounts for CPI and tree accounts,... subject to packing. + // - Last N accounts: Accounts to compress (PDAs and/or token accounts) +} + +#[derive(Accounts)] +pub struct CompressMultipleTokenAccounts<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + /// The authority that owns all token accounts being compressed + /// CHECK: Validated by the SDK + pub authority: AccountInfo<'info>, + /// CHECK: CPI authority of the compressed token program + pub compressed_token_cpi_authority: UncheckedAccount<'info>, + /// CHECK: Compressed token program + pub compressed_token_program: UncheckedAccount<'info>, + /// The global config account + /// CHECK: Config is validated by the SDK's load_checked method + pub config: AccountInfo<'info>, + /// Rent recipient - must match config + /// CHECK: Rent recipient is validated against the config + #[account(mut)] + pub rent_recipient: AccountInfo<'info>, + // Remaining accounts: + // - First N accounts: Token accounts to compress + // - After that: Light Protocol system accounts +} + +// TODO: split into one ix with ctoken and one without. +#[derive(Accounts)] +pub struct DecompressAccountsIdempotent<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + /// UNCHECKED: Anyone can pay to init. + #[account(mut)] + pub rent_payer: Signer<'info>, + /// The global config account + /// CHECK: load_checked. + pub config: AccountInfo<'info>, + + // CToken-specific accounts (optional, only needed when decompressing CToken accounts) + /// Compressed token program + /// CHECK: Program ID validated to be cTokenmWW8bLPjZEBAUgYy3zKxQZW6VKi7bqNFEVv3m + pub compressed_token_program: Option>, + + /// CPI authority PDA of the compressed token program + /// CHECK: PDA derivation validated with seeds ["cpi_authority"] and bump 254 + pub compressed_token_cpi_authority: Option>, + // Remaining accounts: + // - First N accounts: PDA accounts to decompress into (native CToken accounts) + // - After system_accounts_offset: Light Protocol system accounts for CPI + // + // For CToken decompression, the PDA accounts must be native CToken accounts + // owned by the compressed token program (cTokenmWW8bLPjZEBAUgYy3zKxQZW6VKi7bqNFEVv3m) +} + +#[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>, +} + +/// Auto-derived via macro. Unified enum that can hold any account type. Crucial +/// for dispatching multiple compressed accounts of different types in +/// decompress_accounts_idempotent. +/// Implements: Default, DataHasher, LightDiscriminator, HasCompressionInfo. +#[derive(Clone, Debug, AnchorSerialize, AnchorDeserialize)] +pub enum CompressedAccountVariant { + UserRecord(UserRecord), + PackedUserRecord(PackedUserRecord), + GameSession(GameSession), + PlaceholderRecord(PlaceholderRecord), + // include these static ones. + CompressibleTokenAccountPacked(PackedCompressibleTokenDataWithVariant), + CompressibleTokenData(CompressibleTokenDataWithVariant), +} + +impl Default for CompressedAccountVariant { + fn default() -> Self { + Self::UserRecord(UserRecord::default()) + } +} + +impl DataHasher for CompressedAccountVariant { + fn hash(&self) -> std::result::Result<[u8; 32], light_hasher::HasherError> { + match self { + Self::UserRecord(data) => data.hash::(), + Self::PackedUserRecord(_) => unreachable!(), + Self::GameSession(data) => data.hash::(), + Self::PlaceholderRecord(data) => data.hash::(), + Self::CompressibleTokenAccountPacked(_) => unreachable!(), + Self::CompressibleTokenData(_) => unreachable!(), + } + } +} + +impl LightDiscriminator for CompressedAccountVariant { + const LIGHT_DISCRIMINATOR: [u8; 8] = [0; 8]; // This won't be used directly + const LIGHT_DISCRIMINATOR_SLICE: &'static [u8] = &Self::LIGHT_DISCRIMINATOR; +} + +impl HasCompressionInfo for CompressedAccountVariant { + fn compression_info(&self) -> &CompressionInfo { + match self { + Self::UserRecord(data) => data.compression_info(), + Self::PackedUserRecord(_) => unreachable!(), + Self::GameSession(data) => data.compression_info(), + Self::PlaceholderRecord(data) => data.compression_info(), + Self::CompressibleTokenAccountPacked(_) => unreachable!(), + Self::CompressibleTokenData(_) => 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::CompressibleTokenAccountPacked(_) => unreachable!(), + Self::CompressibleTokenData(_) => 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::CompressibleTokenAccountPacked(_) => unreachable!(), + Self::CompressibleTokenData(_) => 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::CompressibleTokenAccountPacked(_) => unreachable!(), + Self::CompressibleTokenData(_) => 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::CompressibleTokenAccountPacked(_) => unreachable!(), + Self::CompressibleTokenData(_) => unreachable!(), + } + } +} + +// Pack implementation for CompressedAccountVariant +// This delegates to the underlying type's Pack implementation +impl Pack for CompressedAccountVariant { + type Packed = Self; + + fn pack(&self, remaining_accounts: &mut PackedAccounts) -> Self::Packed { + match self { + Self::PackedUserRecord(_) => unreachable!(), + Self::UserRecord(data) => Self::PackedUserRecord(data.pack(remaining_accounts)), + Self::GameSession(data) => Self::GameSession(data.pack(remaining_accounts)), + Self::PlaceholderRecord(data) => Self::PlaceholderRecord(data.pack(remaining_accounts)), + Self::CompressibleTokenAccountPacked(_) => { + unreachable!() + } + Self::CompressibleTokenData(data) => { + Self::CompressibleTokenAccountPacked(data.pack(remaining_accounts)) + } + } + } +} + +// Unpack implementation for CompressedAccountVariant +// This delegates to the underlying type's Unpack implementation +impl Unpack for CompressedAccountVariant { + type Unpacked = Self; + + fn unpack( + &self, + remaining_accounts: &[AccountInfo], + ) -> std::result::Result { + match self { + Self::PackedUserRecord(data) => Ok(Self::UserRecord(data.unpack(remaining_accounts)?)), + Self::UserRecord(_) => unreachable!(), + Self::GameSession(data) => Ok(Self::GameSession(data.unpack(remaining_accounts)?)), + Self::PlaceholderRecord(data) => { + Ok(Self::PlaceholderRecord(data.unpack(remaining_accounts)?)) + } + Self::CompressibleTokenAccountPacked(_data) => Ok(self.clone()), // as-is + Self::CompressibleTokenData(_data) => unreachable!(), // as-is + } + } +} + +// 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 Size for UserRecord { + fn size(&self) -> usize { + Self::LIGHT_DISCRIMINATOR.len() + Self::INIT_SPACE + } +} + +impl CompressAs for UserRecord { + type Output = Self; + + fn compress_as(&self) -> std::borrow::Cow<'_, Self::Output> { + // Simple case: return owned data with compression_info = None + // We can't return Cow::Borrowed because compression_info must always be None for compressed storage + std::borrow::Cow::Owned(Self { + compression_info: None, // ALWAYS None for compressed storage + owner: self.owner, + name: self.name.clone(), + score: self.score, + }) + } +} + +#[derive(Debug, Clone, AnchorSerialize, AnchorDeserialize)] +pub struct PackedUserRecord { + pub compression_info: Option, + pub owner: u8, + pub name: String, + pub score: u64, +} + +// Identity Pack implementation - no custom packing needed for PDA types +impl Pack for UserRecord { + type Packed = PackedUserRecord; + + fn pack(&self, remaining_accounts: &mut PackedAccounts) -> Self::Packed { + PackedUserRecord { + compression_info: None, + owner: remaining_accounts.insert_or_get(self.owner), + name: self.name.clone(), + score: self.score, + } + } +} + +// Identity Unpack implementation - PDA types are sent unpacked +impl Unpack for UserRecord { + type Unpacked = Self; + + fn unpack( + &self, + _remaining_accounts: &[AccountInfo], + ) -> std::result::Result { + Ok(self.clone()) + } +} + +// Identity Pack implementation - no custom packing needed for PDA types +impl Pack for PackedUserRecord { + type Packed = Self; + + fn pack(&self, _remaining_accounts: &mut PackedAccounts) -> Self::Packed { + self.clone() + } +} + +// Identity Unpack implementation - PDA types are sent unpacked +impl Unpack for PackedUserRecord { + type Unpacked = UserRecord; + + fn unpack( + &self, + remaining_accounts: &[AccountInfo], + ) -> std::result::Result { + Ok(UserRecord { + compression_info: None, + owner: *remaining_accounts[self.owner as usize].key, + name: self.name.clone(), + score: self.score, + }) + } +} + +// Your existing account structs must be manually extended: +// 1. Add compression_info field to the struct, with type +// Option. +// 2. add a #[skip] field for the compression_info field. +// 3. Add LightHasher, LightDiscriminator. +// 4. Add #[hash] attribute to ALL fields that can be >31 bytes. (eg Pubkeys, +// Strings) +#[derive(Default, Debug, LightHasher, LightDiscriminator, InitSpace)] +#[account] +pub struct GameSession { + #[skip] + pub compression_info: Option, + pub session_id: u64, + #[hash] + pub player: Pubkey, + #[max_len(32)] + pub game_type: String, + pub start_time: u64, + pub end_time: Option, + pub score: u64, +} + +// Auto-derived via macro. +impl HasCompressionInfo for GameSession { + fn compression_info(&self) -> &CompressionInfo { + self.compression_info + .as_ref() + .expect("CompressionInfo must be Some on-chain") + } + + fn compression_info_mut(&mut self) -> &mut CompressionInfo { + self.compression_info + .as_mut() + .expect("CompressionInfo must be Some on-chain") + } + + fn compression_info_mut_opt(&mut self) -> &mut Option { + &mut self.compression_info + } + + fn set_compression_info_none(&mut self) { + self.compression_info = None; + } +} + +impl Size for GameSession { + fn size(&self) -> usize { + Self::LIGHT_DISCRIMINATOR.len() + Self::INIT_SPACE + } +} + +impl CompressAs for GameSession { + type Output = Self; + + fn compress_as(&self) -> std::borrow::Cow<'_, Self::Output> { + // Custom compression: return owned data with modified fields + std::borrow::Cow::Owned(Self { + compression_info: None, // ALWAYS None for compressed storage + session_id: self.session_id, // KEEP - identifier + player: self.player, // KEEP - identifier + game_type: self.game_type.clone(), // KEEP - core property + start_time: 0, // RESET - clear timing + end_time: None, // RESET - clear timing + score: 0, // RESET - clear progress + }) + } +} + +// Identity Pack implementation - no custom packing needed for PDA types +impl Pack for GameSession { + type Packed = Self; + + fn pack(&self, _remaining_accounts: &mut PackedAccounts) -> Self::Packed { + self.clone() + } +} + +// Identity Unpack implementation - PDA types are sent unpacked +impl Unpack for GameSession { + type Unpacked = Self; + + fn unpack( + &self, + _remaining_accounts: &[AccountInfo], + ) -> std::result::Result { + Ok(self.clone()) + } +} + +// PlaceholderRecord - demonstrates empty compressed account creation +// The PDA remains intact while an empty compressed account is created +#[derive(Default, Debug, LightHasher, LightDiscriminator, InitSpace)] +#[account] +pub struct PlaceholderRecord { + #[skip] + pub compression_info: Option, + #[hash] + pub owner: Pubkey, + #[max_len(32)] + pub name: String, + pub placeholder_id: u64, +} + +impl HasCompressionInfo for PlaceholderRecord { + fn compression_info(&self) -> &CompressionInfo { + self.compression_info + .as_ref() + .expect("CompressionInfo must be Some on-chain") + } + + fn compression_info_mut(&mut self) -> &mut CompressionInfo { + self.compression_info + .as_mut() + .expect("CompressionInfo must be Some on-chain") + } + + fn compression_info_mut_opt(&mut self) -> &mut Option { + &mut self.compression_info + } + + fn set_compression_info_none(&mut self) { + self.compression_info = None; + } +} + +impl Size for PlaceholderRecord { + fn size(&self) -> usize { + Self::LIGHT_DISCRIMINATOR.len() + Self::INIT_SPACE + } +} + +impl CompressAs for PlaceholderRecord { + type Output = Self; + + fn compress_as(&self) -> std::borrow::Cow<'_, Self::Output> { + std::borrow::Cow::Owned(Self { + compression_info: None, + owner: self.owner, + name: self.name.clone(), + placeholder_id: self.placeholder_id, + }) + } +} + +// Identity Pack implementation - no custom packing needed for PDA types +impl Pack for PlaceholderRecord { + type Packed = Self; + + fn pack(&self, _remaining_accounts: &mut PackedAccounts) -> Self::Packed { + self.clone() + } +} + +// Identity Unpack implementation - PDA types are sent unpacked +impl Unpack for PlaceholderRecord { + type Unpacked = Self; + + fn unpack( + &self, + _remaining_accounts: &[AccountInfo], + ) -> std::result::Result { + Ok(self.clone()) + } +} + +#[error_code] +pub enum ErrorCode { + #[msg("Invalid account count: PDAs and compressed accounts must match")] + InvalidAccountCount, + #[msg("Rent recipient does not match config")] + InvalidRentRecipient, + #[msg("Failed to create compressed mint")] + MintCreationFailed, + #[msg("Compressed token program account not found in remaining accounts")] + MissingCompressedTokenProgram, + #[msg("Compressed token program authority PDA account not found in remaining accounts")] + MissingCompressedTokenProgramAuthorityPDA, + + #[msg("CToken decompression not yet implemented")] + CTokenDecompressionNotImplemented, +} + +// Add these struct definitions before the program module +#[derive(AnchorSerialize, AnchorDeserialize)] +pub struct AccountCreationData { + pub user_name: String, + pub session_id: u64, + pub game_type: String, + // TODO: Add mint metadata fields when implementing mint functionality + pub mint_name: String, + pub mint_symbol: String, + pub mint_uri: String, + pub mint_decimals: u8, + pub mint_supply: u64, + pub mint_update_authority: Option, + pub mint_freeze_authority: Option, + pub additional_metadata: Option>, +} + +/// Information about a token account to compress +#[derive(AnchorSerialize, AnchorDeserialize)] +pub struct TokenAccountInfo { + pub user: Pubkey, + pub mint: Pubkey, +} + +#[derive(AnchorSerialize, AnchorDeserialize)] +pub struct CompressionParams { + pub proof: ValidityProof, + pub user_compressed_address: [u8; 32], + pub user_address_tree_info: PackedAddressTreeInfo, + pub user_output_state_tree_index: u8, + pub game_compressed_address: [u8; 32], + pub game_address_tree_info: PackedAddressTreeInfo, + pub game_output_state_tree_index: u8, + // TODO: Add mint compression parameters when implementing mint functionality + // pub mint_compressed_address: [u8; 32], + // pub mint_address_tree_info: PackedAddressTreeInfo, + // pub mint_output_state_tree_index: u8, + pub mint_bump: u8, + pub mint_with_context: CompressedMintWithContext, +} + +#[inline] +pub fn account_meta_from_account_info(account_info: &AccountInfo) -> AccountMeta { + AccountMeta { + pubkey: *account_info.key, + is_signer: account_info.is_signer, + is_writable: account_info.is_writable, + } +} diff --git a/sdk-tests/anchor-compressible/tests/test_config.rs b/sdk-tests/anchor-compressible/tests/test_config.rs new file mode 100644 index 0000000000..4a024557de --- /dev/null +++ b/sdk-tests/anchor-compressible/tests/test_config.rs @@ -0,0 +1,628 @@ +//! # Config Tests: anchor-compressible +//! +//! Checks covered: +//! - Successful config init +//! - Authority check (init/update) +//! - Config update by authority +//! - Prevent re-init +//! - Program data account check +//! - Prevent address space removal +//! - Update with non-authority +//! - Rent recipient check +#![cfg(feature = "test-sbf")] + +use anchor_lang::{InstructionData, ToAccountMetas}; +use light_compressible_client::CompressibleInstruction; +use light_macros::pubkey; +use light_program_test::{ + initialize_compression_config, + program_test::{create_mock_program_data, LightProgramTest, TestRpc}, + setup_mock_program_data, update_compression_config, ProgramTestConfig, Rpc, +}; +use light_sdk::compressible::CompressibleConfig; +use solana_sdk::{ + bpf_loader_upgradeable, + instruction::Instruction, + pubkey::Pubkey, + signature::{Keypair, Signer}, +}; + +pub const ADDRESS_SPACE: [Pubkey; 1] = [pubkey!("EzKE84aVTkCUhDHLELqyJaq1Y7UVVmqxXqZjVHwHY3rK")]; +pub const RENT_RECIPIENT: Pubkey = pubkey!("CLEuMG7pzJX9xAuKCFzBP154uiG1GaNo4Fq7x6KAcAfG"); + +#[tokio::test] +async fn test_initialize_compression_config() { + // Success: config can be initialized + let program_id = anchor_compressible::ID; + let config = ProgramTestConfig::new_v2(true, Some(vec![("anchor_compressible", program_id)])); + let mut rpc = LightProgramTest::new(config).await.unwrap(); + let payer = rpc.get_payer().insecure_clone(); + let _program_data_pda = setup_mock_program_data(&mut rpc, &payer, &program_id); + + let result = initialize_compression_config( + &mut rpc, + &payer, + &program_id, + &payer, + 100, + RENT_RECIPIENT, + vec![ADDRESS_SPACE[0]], + &CompressibleInstruction::INITIALIZE_COMPRESSION_CONFIG_DISCRIMINATOR, + None, + ) + .await; + assert!(result.is_ok(), "Initialize config should succeed"); +} + +#[tokio::test] +async fn test_config_validation() { + // Fail: non-authority cannot init + let program_id = anchor_compressible::ID; + let config = ProgramTestConfig::new_v2(true, Some(vec![("anchor_compressible", program_id)])); + let mut rpc = LightProgramTest::new(config).await.unwrap(); + let payer = rpc.get_payer().insecure_clone(); + let non_authority = Keypair::new(); + let _program_data_pda = setup_mock_program_data(&mut rpc, &payer, &program_id); + + rpc.airdrop_lamports(&non_authority.pubkey(), 1_000_000_000) + .await + .unwrap(); + let result = initialize_compression_config( + &mut rpc, + &payer, + &program_id, + &non_authority, + 100, + RENT_RECIPIENT, + vec![ADDRESS_SPACE[0]], + &CompressibleInstruction::INITIALIZE_COMPRESSION_CONFIG_DISCRIMINATOR, + None, + ) + .await; + assert!(result.is_err(), "Should fail with wrong authority"); +} + +#[tokio::test] +async fn test_config_multiple_address_spaces_validation() { + // Fail: cannot init with multiple address spaces + let program_id = anchor_compressible::ID; + let config = ProgramTestConfig::new_v2(true, Some(vec![("anchor_compressible", 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); + + // Try to init with multiple address spaces - should fail + let multiple_address_spaces = vec![ADDRESS_SPACE[0], Pubkey::new_unique()]; + let result = initialize_compression_config( + &mut rpc, + &payer, + &program_id, + &payer, + 100, + RENT_RECIPIENT, + multiple_address_spaces, + &CompressibleInstruction::INITIALIZE_COMPRESSION_CONFIG_DISCRIMINATOR, + None, + ) + .await; + assert!(result.is_err(), "Should fail with multiple address spaces"); + + // Try to init with empty address space - should also fail + let empty_address_space = vec![]; + let result = initialize_compression_config( + &mut rpc, + &payer, + &program_id, + &payer, + 100, + RENT_RECIPIENT, + empty_address_space, + &CompressibleInstruction::INITIALIZE_COMPRESSION_CONFIG_DISCRIMINATOR, + None, + ) + .await; + assert!(result.is_err(), "Should fail with empty address space"); +} + +#[tokio::test] +async fn test_update_compression_config() { + // Success: authority can update config + let program_id = anchor_compressible::ID; + let config = ProgramTestConfig::new_v2(true, Some(vec![("anchor_compressible", 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); + let _program_data_pda = setup_mock_program_data(&mut rpc, &payer, &program_id); + + let init_result = initialize_compression_config( + &mut rpc, + &payer, + &program_id, + &payer, + 100, + RENT_RECIPIENT, + ADDRESS_SPACE.to_vec(), + &CompressibleInstruction::INITIALIZE_COMPRESSION_CONFIG_DISCRIMINATOR, + None, + ) + .await; + assert!(init_result.is_ok(), "Init should succeed"); + let config_account = rpc.get_account(config_pda).await.unwrap(); + assert!(config_account.is_some(), "Config account should exist"); + + // Use the new mid-level helper - much cleaner! + let update_result = update_compression_config( + &mut rpc, + &payer, + &program_id, + &payer, + Some(200), + Some(RENT_RECIPIENT), + Some(vec![ADDRESS_SPACE[0]]), + None, + &CompressibleInstruction::UPDATE_COMPRESSION_CONFIG_DISCRIMINATOR, + ) + .await; + assert!(update_result.is_ok(), "Update config should succeed"); +} + +#[tokio::test] +async fn test_config_reinit_attack_prevention() { + // Fail: cannot re-init config + let program_id = anchor_compressible::ID; + let config = ProgramTestConfig::new_v2(true, Some(vec![("anchor_compressible", program_id)])); + let mut rpc = LightProgramTest::new(config).await.unwrap(); + let payer = rpc.get_payer().insecure_clone(); + setup_mock_program_data(&mut rpc, &payer, &program_id); + let result = initialize_compression_config( + &mut rpc, + &payer, + &program_id, + &payer, + 100, + RENT_RECIPIENT, + vec![ADDRESS_SPACE[0]], + &CompressibleInstruction::INITIALIZE_COMPRESSION_CONFIG_DISCRIMINATOR, + None, + ) + .await; + assert!(result.is_ok(), "First init should succeed"); + let reinit_result = initialize_compression_config( + &mut rpc, + &payer, + &program_id, + &payer, + 100, + RENT_RECIPIENT, + vec![ADDRESS_SPACE[0]], + &CompressibleInstruction::INITIALIZE_COMPRESSION_CONFIG_DISCRIMINATOR, + None, + ) + .await; + assert!(reinit_result.is_err(), "Config reinit should fail"); +} + +#[tokio::test] +async fn test_wrong_program_data_account() { + // Fail: wrong program data account + let program_id = anchor_compressible::ID; + let config = ProgramTestConfig::new_v2(true, Some(vec![("anchor_compressible", program_id)])); + let mut rpc = LightProgramTest::new(config).await.unwrap(); + let payer = rpc.get_payer().insecure_clone(); + let fake_program_data = Keypair::new(); + 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(fake_program_data.pubkey(), mock_account); + let result = initialize_compression_config( + &mut rpc, + &payer, + &program_id, + &payer, + 100, + RENT_RECIPIENT, + vec![ADDRESS_SPACE[0]], + &CompressibleInstruction::INITIALIZE_COMPRESSION_CONFIG_DISCRIMINATOR, + None, + ) + .await; + + assert!( + result.is_err(), + "Should fail with wrong program data account" + ); +} + +#[tokio::test] +async fn test_update_remove_address_space() { + // Fail: cannot remove/replace address space + let program_id = anchor_compressible::ID; + let config = ProgramTestConfig::new_v2(true, Some(vec![("anchor_compressible", program_id)])); + let mut rpc = LightProgramTest::new(config).await.unwrap(); + let payer = rpc.get_payer().insecure_clone(); + setup_mock_program_data(&mut rpc, &payer, &program_id); + let address_space_1 = vec![ADDRESS_SPACE[0]]; + let address_space_2 = vec![Pubkey::new_unique()]; + let init_result = initialize_compression_config( + &mut rpc, + &payer, + &program_id, + &payer, + 100, + RENT_RECIPIENT, + address_space_1, + &CompressibleInstruction::INITIALIZE_COMPRESSION_CONFIG_DISCRIMINATOR, + None, + ) + .await; + assert!(init_result.is_ok(), "Init should succeed"); + let update_result = update_compression_config( + &mut rpc, + &payer, + &program_id, + &payer, + None, + None, + Some(address_space_2), + None, + &CompressibleInstruction::UPDATE_COMPRESSION_CONFIG_DISCRIMINATOR, + ) + .await; + assert!( + update_result.is_err(), + "Should fail when trying to replace address space" + ); +} + +#[tokio::test] +async fn test_update_with_non_authority() { + // Fail: non-authority cannot update + let program_id = anchor_compressible::ID; + let config = ProgramTestConfig::new_v2(true, Some(vec![("anchor_compressible", program_id)])); + let mut rpc = LightProgramTest::new(config).await.unwrap(); + let payer = rpc.get_payer().insecure_clone(); + let non_authority = Keypair::new(); + rpc.airdrop_lamports(&non_authority.pubkey(), 1_000_000_000) + .await + .unwrap(); + setup_mock_program_data(&mut rpc, &payer, &program_id); + let init_result = initialize_compression_config( + &mut rpc, + &payer, + &program_id, + &payer, + 100, + RENT_RECIPIENT, + vec![ADDRESS_SPACE[0]], + &CompressibleInstruction::INITIALIZE_COMPRESSION_CONFIG_DISCRIMINATOR, + None, + ) + .await; + assert!(init_result.is_ok(), "Init should succeed"); + + // Use the new mid-level helper to test non-authority update + let update_result = update_compression_config( + &mut rpc, + &payer, + &program_id, + &non_authority, // This should fail - non_authority tries to update + Some(200), + None, + None, + None, + &CompressibleInstruction::UPDATE_COMPRESSION_CONFIG_DISCRIMINATOR, + ) + .await; + assert!( + update_result.is_err(), + "Should fail with non-authority update" + ); +} + +#[tokio::test] +async fn test_config_with_wrong_rent_recipient() { + // Fail: wrong rent recipient + let program_id = anchor_compressible::ID; + let config = ProgramTestConfig::new_v2(true, Some(vec![("anchor_compressible", 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); + setup_mock_program_data(&mut rpc, &payer, &program_id); + let init_result = initialize_compression_config( + &mut rpc, + &payer, + &program_id, + &payer, + 100, + RENT_RECIPIENT, + vec![ADDRESS_SPACE[0]], + &CompressibleInstruction::INITIALIZE_COMPRESSION_CONFIG_DISCRIMINATOR, + None, + ) + .await; + assert!(init_result.is_ok(), "Init should succeed"); + let user = payer; + let (user_record_pda, _bump) = + Pubkey::find_program_address(&[b"user_record", user.pubkey().as_ref()], &program_id); + let wrong_rent_recipient = Pubkey::new_unique(); + let accounts = anchor_compressible::accounts::CreateRecord { + user: user.pubkey(), + user_record: user_record_pda, + system_program: solana_sdk::system_program::ID, + config: config_pda, + rent_recipient: wrong_rent_recipient, + }; + let instruction_data = anchor_compressible::instruction::CreateRecord { + name: "Test".to_string(), + proof: light_sdk::instruction::ValidityProof::default(), + compressed_address: [0u8; 32], + address_tree_info: light_sdk::instruction::PackedAddressTreeInfo::default(), + output_state_tree_index: 0, + }; + let instruction = Instruction { + program_id, + accounts: accounts.to_account_metas(None), + data: instruction_data.data(), + }; + let result = rpc + .create_and_send_transaction(&[instruction], &user.pubkey(), &[&user]) + .await; + assert!(result.is_err(), "Should fail with wrong rent recipient"); +} + +#[tokio::test] +async fn test_config_discriminator_attacks() { + let program_id = anchor_compressible::ID; + let config = ProgramTestConfig::new_v2(true, Some(vec![("anchor_compressible", 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); + + setup_mock_program_data(&mut rpc, &payer, &program_id); + + // First, create a valid config + let init_result = initialize_compression_config( + &mut rpc, + &payer, + &program_id, + &payer, + 100, + RENT_RECIPIENT, + vec![ADDRESS_SPACE[0]], + &CompressibleInstruction::INITIALIZE_COMPRESSION_CONFIG_DISCRIMINATOR, + None, + ) + .await; + assert!(init_result.is_ok(), "Init should succeed"); + + // Test 1: Corrupt the discriminator in config account + { + let config_account = rpc.get_account(config_pda).await.unwrap().unwrap(); + let mut corrupted_data = config_account.data.clone(); + + // Corrupt the discriminator (first 8 bytes) + corrupted_data[0] = 0xFF; + corrupted_data[1] = 0xFF; + corrupted_data[7] = 0xFF; + + let corrupted_account = solana_sdk::account::Account { + lamports: config_account.lamports, + data: corrupted_data, + owner: config_account.owner, + executable: config_account.executable, + rent_epoch: config_account.rent_epoch, + }; + + // Set the corrupted account + rpc.set_account(config_pda, corrupted_account); + + // Try to use config with create_record - should fail + let user = rpc.get_payer().insecure_clone(); + let (user_record_pda, _bump) = + Pubkey::find_program_address(&[b"user_record", user.pubkey().as_ref()], &program_id); + + let accounts = anchor_compressible::accounts::CreateRecord { + user: user.pubkey(), + user_record: user_record_pda, + system_program: solana_sdk::system_program::ID, + config: config_pda, + rent_recipient: RENT_RECIPIENT, + }; + + let instruction_data = anchor_compressible::instruction::CreateRecord { + name: "Test".to_string(), + proof: light_sdk::instruction::ValidityProof::default(), + compressed_address: [0u8; 32], + address_tree_info: light_sdk::instruction::PackedAddressTreeInfo::default(), + output_state_tree_index: 0, + }; + + let instruction = Instruction { + program_id, + accounts: accounts.to_account_metas(None), + data: instruction_data.data(), + }; + + let result = rpc + .create_and_send_transaction(&[instruction], &user.pubkey(), &[&user]) + .await; + + assert!(result.is_err(), "Should fail with corrupted discriminator"); + + // Restore the original config for next test + let original_config_account = solana_sdk::account::Account { + lamports: config_account.lamports, + data: config_account.data, + owner: config_account.owner, + executable: config_account.executable, + rent_epoch: config_account.rent_epoch, + }; + rpc.set_account(config_pda, original_config_account); + } + + // Test 2: Corrupt the version field + { + let config_account = rpc.get_account(config_pda).await.unwrap().unwrap(); + let mut corrupted_data = config_account.data.clone(); + + // Corrupt the version (byte 8 - after discriminator) + corrupted_data[8] = 99; // Invalid version + + let corrupted_account = solana_sdk::account::Account { + lamports: config_account.lamports, + data: corrupted_data, + owner: config_account.owner, + executable: config_account.executable, + rent_epoch: config_account.rent_epoch, + }; + + rpc.set_account(config_pda, corrupted_account); + + // Try to use config - should fail due to invalid version + let user = rpc.get_payer().insecure_clone(); + let (user_record_pda, _bump) = + Pubkey::find_program_address(&[b"user_record", user.pubkey().as_ref()], &program_id); + + let accounts = anchor_compressible::accounts::CreateRecord { + user: user.pubkey(), + user_record: user_record_pda, + system_program: solana_sdk::system_program::ID, + config: config_pda, + rent_recipient: RENT_RECIPIENT, + }; + + let instruction_data = anchor_compressible::instruction::CreateRecord { + name: "Test".to_string(), + proof: light_sdk::instruction::ValidityProof::default(), + compressed_address: [0u8; 32], + address_tree_info: light_sdk::instruction::PackedAddressTreeInfo::default(), + output_state_tree_index: 0, + }; + + let instruction = Instruction { + program_id, + accounts: accounts.to_account_metas(None), + data: instruction_data.data(), + }; + + let result = rpc + .create_and_send_transaction(&[instruction], &user.pubkey(), &[&user]) + .await; + + assert!(result.is_err(), "Should fail with invalid version"); + } + + // Test 3: Corrupt the address_space field (set length to 0) + { + let config_account = rpc.get_account(config_pda).await.unwrap().unwrap(); + let mut corrupted_data = config_account.data.clone(); + + // Find and corrupt address_space length (4 bytes after: discriminator + + // version + compression_delay + update_authority + rent_recipient) + // discriminator (8) + version (1) + compression_delay (4) + + // update_authority (32) + rent_recipient (32) = 77 bytes The + // address_space length is at byte 77 + let address_space_len_offset = 8 + 1 + 4 + 32 + 32; // 77 + corrupted_data[address_space_len_offset] = 0; // Set length to 0 + corrupted_data[address_space_len_offset + 1] = 0; + corrupted_data[address_space_len_offset + 2] = 0; + corrupted_data[address_space_len_offset + 3] = 0; + + let corrupted_account = solana_sdk::account::Account { + lamports: config_account.lamports, + data: corrupted_data, + owner: config_account.owner, + executable: config_account.executable, + rent_epoch: config_account.rent_epoch, + }; + + rpc.set_account(config_pda, corrupted_account); + + // Try to use config - should fail due to empty address_space + let user = rpc.get_payer().insecure_clone(); + let (user_record_pda, _bump) = + Pubkey::find_program_address(&[b"user_record", user.pubkey().as_ref()], &program_id); + + let accounts = anchor_compressible::accounts::CreateRecord { + user: user.pubkey(), + user_record: user_record_pda, + system_program: solana_sdk::system_program::ID, + config: config_pda, + rent_recipient: RENT_RECIPIENT, + }; + + let instruction_data = anchor_compressible::instruction::CreateRecord { + name: "Test".to_string(), + proof: light_sdk::instruction::ValidityProof::default(), + compressed_address: [0u8; 32], + address_tree_info: light_sdk::instruction::PackedAddressTreeInfo::default(), + output_state_tree_index: 0, + }; + + let instruction = Instruction { + program_id, + accounts: accounts.to_account_metas(None), + data: instruction_data.data(), + }; + + let result = rpc + .create_and_send_transaction(&[instruction], &user.pubkey(), &[&user]) + .await; + + assert!(result.is_err(), "Should fail with empty address_space"); + } + + // Test 4: Try to load config with wrong owner (should fail in load_checked) + { + let config_account = rpc.get_account(config_pda).await.unwrap().unwrap(); + let wrong_owner = Pubkey::new_unique(); + + let wrong_owner_account = solana_sdk::account::Account { + lamports: config_account.lamports, + data: config_account.data, + owner: wrong_owner, // Wrong owner + executable: config_account.executable, + rent_epoch: config_account.rent_epoch, + }; + + rpc.set_account(config_pda, wrong_owner_account); + + // Try to use config - should fail due to wrong owner + let user = rpc.get_payer().insecure_clone(); + let (user_record_pda, _bump) = + Pubkey::find_program_address(&[b"user_record", user.pubkey().as_ref()], &program_id); + + let accounts = anchor_compressible::accounts::CreateRecord { + user: user.pubkey(), + user_record: user_record_pda, + system_program: solana_sdk::system_program::ID, + config: config_pda, + rent_recipient: RENT_RECIPIENT, + }; + + let instruction_data = anchor_compressible::instruction::CreateRecord { + name: "Test".to_string(), + proof: light_sdk::instruction::ValidityProof::default(), + compressed_address: [0u8; 32], + address_tree_info: light_sdk::instruction::PackedAddressTreeInfo::default(), + output_state_tree_index: 0, + }; + + let instruction = Instruction { + program_id, + accounts: accounts.to_account_metas(None), + data: instruction_data.data(), + }; + + let result = rpc + .create_and_send_transaction(&[instruction], &user.pubkey(), &[&user]) + .await; + + assert!(result.is_err(), "Should fail with wrong owner"); + } +} diff --git a/sdk-tests/anchor-compressible/tests/test_decompress_multiple.rs b/sdk-tests/anchor-compressible/tests/test_decompress_multiple.rs new file mode 100644 index 0000000000..66dd8d5483 --- /dev/null +++ b/sdk-tests/anchor-compressible/tests/test_decompress_multiple.rs @@ -0,0 +1,2580 @@ +use anchor_compressible::{ + get_ctoken_signer_seeds, CTokenAccountVariant, CompressedAccountVariant, GameSession, + UserRecord, +}; +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::{ + instructions::{derive_compressed_mint_address, find_spl_mint_address}, + CPI_AUTHORITY_PDA, +}; +use light_compressible_client::CompressibleInstruction; +use light_ctoken_types::{ + instructions::mint_action::{CompressedMintInstructionData, CompressedMintWithContext}, + state::BaseCompressedMint, + COMPRESSED_TOKEN_PROGRAM_ID, +}; +use light_macros::pubkey; +use light_program_test::{ + initialize_compression_config, + program_test::{LightProgramTest, TestRpc}, + setup_mock_program_data, + utils::simulation::simulate_cu, + AddressWithTree, Indexer, ProgramTestConfig, Rpc, RpcError, +}; +use light_sdk::{ + compressible::{CompressAs, CompressibleConfig}, + instruction::{PackedAccounts, SystemAccountMetaConfig}, + token::CompressibleTokenDataWithVariant, +}; +use solana_account::Account; +use solana_instruction::Instruction; +use solana_keypair::Keypair; +use solana_pubkey::Pubkey; +use solana_signer::Signer; + +pub const ADDRESS_SPACE: [Pubkey; 1] = [pubkey!("EzKE84aVTkCUhDHLELqyJaq1Y7UVVmqxXqZjVHwHY3rK")]; +pub const RENT_RECIPIENT: Pubkey = pubkey!("CLEuMG7pzJX9xAuKCFzBP154uiG1GaNo4Fq7x6KAcAfG"); +pub const TOKEN_PROGRAM_ID: Pubkey = pubkey!("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"); + +#[tokio::test] +async fn test_create_and_decompress_two_accounts() { + let program_id = anchor_compressible::ID; + let mut config = + ProgramTestConfig::new_v2(true, Some(vec![("anchor_compressible", program_id)])); + config = config.with_light_protocol_events(); + + let mut rpc = LightProgramTest::new(config).await.unwrap(); + + let payer = rpc.get_payer().insecure_clone(); + + let config_pda = CompressibleConfig::derive_pda(&program_id, 0).0; + let _program_data_pda = setup_mock_program_data(&mut rpc, &payer, &program_id); + + let result = initialize_compression_config( + &mut rpc, + &payer, + &program_id, + &payer, + 100, + RENT_RECIPIENT, + vec![ADDRESS_SPACE[0]], + &CompressibleInstruction::INITIALIZE_COMPRESSION_CONFIG_DISCRIMINATOR, + None, + ) + .await; + assert!(result.is_ok(), "Initialize config should succeed"); + + let combined_user = Keypair::new(); + let fund_user_ix = solana_sdk::system_instruction::transfer( + &payer.pubkey(), + &combined_user.pubkey(), + 1e9 as u64, + ); + let fund_result = rpc + .create_and_send_transaction(&[fund_user_ix], &payer.pubkey(), &[&payer]) + .await; + assert!(fund_result.is_ok(), "Funding combined user should succeed"); + let combined_session_id = 99999u64; + let (combined_user_record_pda, _combined_user_record_bump) = Pubkey::find_program_address( + &[b"user_record", combined_user.pubkey().as_ref()], + &program_id, + ); + let (combined_game_session_pda, _combined_game_bump) = Pubkey::find_program_address( + &[b"game_session", combined_session_id.to_le_bytes().as_ref()], + &program_id, + ); + + let (compressed_token_account, _) = 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 (_, compressed_token_account_address) = anchor_compressible::get_ctoken_signer_seeds( + &combined_user.pubkey(), + &compressed_token_account.token.mint, + ); + + 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; + let game_session_before_decompression: CompressedAccount = rpc + .get_compressed_account(compressed_game_session_address, None) + .await + .unwrap() + .value; + + 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, + compressed_token_account.clone(), + compressed_token_account_address, // also the owner of the compressed token account! + ) + .await; + + // Now compress the decompressed token account back to compressed + rpc.warp_to_slot(300).unwrap(); + + compress_token_account_after_decompress( + &mut rpc, + &combined_user, + &program_id, + &config_pda, + compressed_token_account_address, + compressed_token_account.token.mint, + compressed_token_account.token.amount, + &combined_user_record_pda, + &combined_game_session_pda, + combined_session_id, + user_record_before_decompression.hash, + game_session_before_decompression.hash, + ) + .await; +} + +#[tokio::test] +async fn test_create_decompress_compress_single_account() { + let program_id = anchor_compressible::ID; + let config = ProgramTestConfig::new_v2(true, Some(vec![("anchor_compressible", program_id)])); + let mut rpc = LightProgramTest::new(config).await.unwrap(); + let payer = rpc.get_payer().insecure_clone(); + let _program_data_pda = setup_mock_program_data(&mut rpc, &payer, &program_id); + + let result = initialize_compression_config( + &mut rpc, + &payer, + &program_id, + &payer, + 100, + RENT_RECIPIENT, + vec![ADDRESS_SPACE[0]], + &CompressibleInstruction::INITIALIZE_COMPRESSION_CONFIG_DISCRIMINATOR, + None, + ) + .await; + assert!(result.is_ok(), "Initialize config should succeed"); + + let (user_record_pda, user_record_bump) = + Pubkey::find_program_address(&[b"user_record", payer.pubkey().as_ref()], &program_id); + + create_record(&mut rpc, &payer, &program_id, &user_record_pda, None).await; + + rpc.warp_to_slot(100).unwrap(); + + decompress_single_user_record( + &mut rpc, + &payer, + &program_id, + &user_record_pda, + &user_record_bump, + "Test User", + 100, + ) + .await; + + rpc.warp_to_slot(101).unwrap(); + + let result = compress_record(&mut rpc, &payer, &program_id, &user_record_pda, true).await; + assert!(result.is_err(), "Compression should fail due to slot delay"); + if let Err(err) = result { + let err_msg = format!("{:?}", err); + assert!( + err_msg.contains("Custom(16001)"), + "Expected error message about slot delay, got: {}", + err_msg + ); + } + rpc.warp_to_slot(200).unwrap(); + let _result = compress_record(&mut rpc, &payer, &program_id, &user_record_pda, false).await; +} + +#[tokio::test] +async fn test_double_decompression_attack() { + let program_id = anchor_compressible::ID; + let config = ProgramTestConfig::new_v2(true, Some(vec![("anchor_compressible", program_id)])); + let mut rpc = LightProgramTest::new(config).await.unwrap(); + let payer = rpc.get_payer().insecure_clone(); + + let _program_data_pda = setup_mock_program_data(&mut rpc, &payer, &program_id); + + let result = initialize_compression_config( + &mut rpc, + &payer, + &program_id, + &payer, + 100, + RENT_RECIPIENT, + vec![ADDRESS_SPACE[0]], + &CompressibleInstruction::INITIALIZE_COMPRESSION_CONFIG_DISCRIMINATOR, + None, + ) + .await; + assert!(result.is_ok(), "Initialize config should succeed"); + + let (user_record_pda, user_record_bump) = + Pubkey::find_program_address(&[b"user_record", payer.pubkey().as_ref()], &program_id); + + // Create and compress the account + create_record(&mut rpc, &payer, &program_id, &user_record_pda, None).await; + let address_tree_pubkey = rpc.get_address_tree_v2().queue; + let user_compressed_address = derive_address( + &user_record_pda.to_bytes(), + &address_tree_pubkey.to_bytes(), + &program_id.to_bytes(), + ); + let compressed_user_record = rpc + .get_compressed_account(user_compressed_address, None) + .await + .unwrap() + .value; + let c_user_record = + UserRecord::deserialize(&mut &compressed_user_record.data.unwrap().data[..]).unwrap(); + + rpc.warp_to_slot(100).unwrap(); + + // First decompression - should succeed + decompress_single_user_record( + &mut rpc, + &payer, + &program_id, + &user_record_pda, + &user_record_bump, + "Test User", + 100, + ) + .await; + + // Verify account is now decompressed + 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" + ); + + // Second decompression attempt - should be idempotent (skip already initialized account) + + let c_user_pda = rpc + .get_compressed_account(user_compressed_address, None) + .await + .unwrap() + .value; + + 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(); + + // Second decompression instruction - should still work (idempotent) + let instruction = + light_compressible_client::CompressibleInstruction::decompress_accounts_idempotent( + &program_id, + &CompressibleInstruction::DECOMPRESS_ACCOUNTS_IDEMPOTENT_DISCRIMINATOR, + &payer.pubkey(), + &payer.pubkey(), + &[user_record_pda], + &[( + c_user_pda, + CompressedAccountVariant::UserRecord(c_user_record), + )], + rpc_result, + output_state_tree_info, + ) + .unwrap(); + + let result = rpc + .create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer]) + .await; + + // Should succeed due to idempotent behavior (skips already initialized accounts) + assert!( + result.is_ok(), + "Second decompression should succeed idempotently" + ); + + // Verify account state is still correct and not corrupted + let user_pda_account = rpc.get_account(user_record_pda).await.unwrap(); + let user_pda_data = user_pda_account.unwrap().data; + let decompressed_user_record = UserRecord::try_deserialize(&mut &user_pda_data[..]).unwrap(); + + assert_eq!(decompressed_user_record.name, "Test User"); + assert_eq!(decompressed_user_record.score, 11); + assert_eq!(decompressed_user_record.owner, payer.pubkey()); + assert!(!decompressed_user_record + .compression_info + .as_ref() + .unwrap() + .is_compressed()); +} + +#[tokio::test] +async fn test_create_and_decompress_accounts_with_different_state_trees() { + let program_id = anchor_compressible::ID; + let config = ProgramTestConfig::new_v2(true, Some(vec![("anchor_compressible", program_id)])); + let mut rpc = LightProgramTest::new(config).await.unwrap(); + let payer = rpc.get_payer().insecure_clone(); + + let config_pda = CompressibleConfig::derive_pda(&program_id, 0).0; + let _program_data_pda = setup_mock_program_data(&mut rpc, &payer, &program_id); + + let result = initialize_compression_config( + &mut rpc, + &payer, + &program_id, + &payer, + 100, + RENT_RECIPIENT, + vec![ADDRESS_SPACE[0]], + &CompressibleInstruction::INITIALIZE_COMPRESSION_CONFIG_DISCRIMINATOR, + None, + ) + .await; + assert!(result.is_ok(), "Initialize config should succeed"); + + let (user_record_pda, _user_record_bump) = + Pubkey::find_program_address(&[b"user_record", payer.pubkey().as_ref()], &program_id); + + let session_id = 54321u64; + let (game_session_pda, _game_bump) = Pubkey::find_program_address( + &[b"game_session", session_id.to_le_bytes().as_ref()], + &program_id, + ); + + // Get two different state trees + let first_state_tree_info = rpc.get_state_tree_infos()[0]; + let second_state_tree_info = rpc.get_state_tree_infos()[1]; + + // Create user record using first state tree + create_record( + &mut rpc, + &payer, + &program_id, + &user_record_pda, + Some(first_state_tree_info.queue), + ) + .await; + + // Create game session using second state tree + 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(); + + // Now decompress both accounts together - they come from different state trees + // This should succeed and validate that our decompression can handle mixed state tree sources + decompress_multiple_pdas( + &mut rpc, + &payer, + &program_id, + &user_record_pda, + &game_session_pda, + session_id, + "Test User", + "Battle Royale", + 100, + ) + .await; +} + +#[tokio::test] +async fn test_update_record_compression_info() { + let program_id = anchor_compressible::ID; + let config = ProgramTestConfig::new_v2(true, Some(vec![("anchor_compressible", program_id)])); + let mut rpc = LightProgramTest::new(config).await.unwrap(); + let payer = rpc.get_payer().insecure_clone(); + + let _program_data_pda = setup_mock_program_data(&mut rpc, &payer, &program_id); + + let result = initialize_compression_config( + &mut rpc, + &payer, + &program_id, + &payer, + 100, + RENT_RECIPIENT, + vec![ADDRESS_SPACE[0]], + &CompressibleInstruction::INITIALIZE_COMPRESSION_CONFIG_DISCRIMINATOR, + None, + ) + .await; + assert!(result.is_ok(), "Initialize config should succeed"); + + let (user_record_pda, user_record_bump) = + Pubkey::find_program_address(&[b"user_record", payer.pubkey().as_ref()], &program_id); + + // Create and compress the account + create_record(&mut rpc, &payer, &program_id, &user_record_pda, None).await; + + // Warp to slot 100 and decompress + 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; + + // Warp to slot 150 for the update + rpc.warp_to_slot(150).unwrap(); + + // Create update instruction + let accounts = anchor_compressible::accounts::UpdateRecord { + user: payer.pubkey(), + user_record: user_record_pda, + }; + + let instruction_data = anchor_compressible::instruction::UpdateRecord { + name: "Updated User".to_string(), + score: 42, + }; + + let instruction = Instruction { + program_id, + accounts: accounts.to_account_metas(None), + data: instruction_data.data(), + }; + + // Execute the update + let result = rpc + .create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer]) + .await; + assert!(result.is_ok(), "Update record transaction should succeed"); + + // Warp to slot 200 to ensure we're past the update + rpc.warp_to_slot(200).unwrap(); + + // Fetch the account and verify compression_info.last_written_slot + 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(); + + // Verify the data was updated + assert_eq!(updated_user_record.name, "Updated User"); + assert_eq!(updated_user_record.score, 42); + assert_eq!(updated_user_record.owner, payer.pubkey()); + + // Verify compression_info.last_written_slot was updated to slot 150 + assert_eq!( + updated_user_record + .compression_info + .as_ref() + .unwrap() + .last_written_slot(), + 150 + ); + assert!(!updated_user_record + .compression_info + .as_ref() + .unwrap() + .is_compressed()); +} + +#[tokio::test] +async fn test_custom_compression_game_session() { + let program_id = anchor_compressible::ID; + let config = ProgramTestConfig::new_v2(true, Some(vec![("anchor_compressible", 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); + + // Initialize config + let result = initialize_compression_config( + &mut rpc, + &payer, + &program_id, + &payer, + 100, // compression delay + RENT_RECIPIENT, + vec![ADDRESS_SPACE[0]], + &CompressibleInstruction::INITIALIZE_COMPRESSION_CONFIG_DISCRIMINATOR, + None, + ) + .await; + assert!(result.is_ok(), "Initialize config should succeed"); + + // Create a game session + 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; + + // Warp forward to allow decompression + rpc.warp_to_slot(100).unwrap(); + + // Decompress the game session first to verify original state + decompress_single_game_session( + &mut rpc, + &payer, + &program_id, + &game_session_pda, + &_game_bump, + session_id, + "Battle Royale", + 100, + 0, // original score should be 0 + ) + .await; + + // Warp forward past compression delay to allow compression + rpc.warp_to_slot(250).unwrap(); + + // Test the custom compression trait - this demonstrates the core functionality + compress_game_session_with_custom_data( + &mut rpc, + &payer, + &program_id, + &game_session_pda, + session_id, + ) + .await; +} + +#[tokio::test] +async fn test_create_empty_compressed_account() { + let program_id = anchor_compressible::ID; + let config = ProgramTestConfig::new_v2(true, Some(vec![("anchor_compressible", 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); + + // Initialize compression config + let result = initialize_compression_config( + &mut rpc, + &payer, + &program_id, + &payer, + 100, + RENT_RECIPIENT, + vec![ADDRESS_SPACE[0]], + &CompressibleInstruction::INITIALIZE_COMPRESSION_CONFIG_DISCRIMINATOR, + None, + ) + .await; + assert!(result.is_ok(), "Initialize config should succeed"); + + // Create placeholder record using empty compressed account functionality + 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; + + // Verify the PDA still exists and has data + 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)" + ); + + // Verify we can read the PDA data + let placeholder_data = account.data; + let decompressed_placeholder_record = + anchor_compressible::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()); + + // Verify empty compressed account was created + let address_tree_pubkey = rpc.get_address_tree_v2().queue; + let compressed_address = derive_address( + &placeholder_record_pda.to_bytes(), + &address_tree_pubkey.to_bytes(), + &program_id.to_bytes(), + ); + + let compressed_placeholder = rpc + .get_compressed_account(compressed_address, None) + .await + .unwrap() + .value; + + 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" + ); + + // Verify the compressed account is empty (length 0) + let compressed_data = compressed_placeholder.data.unwrap(); + assert_eq!( + compressed_data.data.len(), + 0, + "Compressed account data should be empty" + ); + + // This demonstrates the key difference from regular compression: + // The PDA still exists with data, and an empty compressed account was created + + // Step 2: Now compress the PDA (this will close the PDA and put data into the compressed account) + rpc.warp_to_slot(200).unwrap(); // Wait past compression delay + + compress_placeholder_record( + &mut rpc, + &payer, + &program_id, + &config_pda, + &placeholder_record_pda, + &placeholder_record_bump, + placeholder_id, + ) + .await; +} + +async fn create_record( + rpc: &mut LightProgramTest, + payer: &Keypair, + program_id: &Pubkey, + user_record_pda: &Pubkey, + state_tree_queue: Option, +) { + let config_pda = CompressibleConfig::derive_pda(program_id, 0).0; + + let mut remaining_accounts = PackedAccounts::default(); + let system_config = SystemAccountMetaConfig::new(*program_id); + let _ = remaining_accounts.add_system_accounts_small(system_config); + + let address_tree_pubkey = rpc.get_address_tree_v2().queue; + + let accounts = anchor_compressible::accounts::CreateRecord { + user: payer.pubkey(), + user_record: *user_record_pda, + system_program: solana_sdk::system_program::ID, + config: config_pda, + rent_recipient: RENT_RECIPIENT, + }; + + let compressed_address = derive_address( + &user_record_pda.to_bytes(), + &address_tree_pubkey.to_bytes(), + &program_id.to_bytes(), + ); + + // Get validity proof from RPC + let rpc_result = rpc + .get_validity_proof( + vec![], + vec![AddressWithTree { + address: compressed_address, + tree: address_tree_pubkey, + }], + None, + ) + .await + .unwrap() + .value; + + // Pack tree infos into remaining accounts + let packed_tree_infos = rpc_result.pack_tree_infos(&mut remaining_accounts); + + // Get the packed address tree info + let address_tree_info = packed_tree_infos.address_trees[0]; + + // Get output state tree index + let output_state_tree_index = remaining_accounts.insert_or_get( + state_tree_queue.unwrap_or_else(|| rpc.get_random_state_tree_info().unwrap().queue), + ); + + // Get system accounts for the instruction + let (system_accounts, _, _) = remaining_accounts.to_account_metas(); + + // Create instruction data + let instruction_data = anchor_compressible::instruction::CreateRecord { + name: "Test User".to_string(), + proof: rpc_result.proof, + compressed_address, + address_tree_info, + output_state_tree_index, + }; + + // Build the instruction + let instruction = Instruction { + program_id: *program_id, + accounts: [accounts.to_account_metas(None), system_accounts].concat(), + data: instruction_data.data(), + }; + + let cu = simulate_cu(rpc, payer, &instruction).await; + println!("CreateRecord CU consumed: {}", cu); + + // Create and send transaction + let result = rpc + .create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer]) + .await; + + assert!(result.is_ok(), "Transaction should succeed"); + + // should be empty + let user_record_account = rpc.get_account(*user_record_pda).await.unwrap(); + assert!( + user_record_account.is_some(), + "Account should exist after compression" + ); + + let account = user_record_account.unwrap(); + assert_eq!(account.lamports, 0, "Account lamports should be 0"); + + let user_record_data = account.data; + + assert!(user_record_data.is_empty(), "Account data should be empty"); +} + +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, +) { + // Setup remaining accounts for Light Protocol + let mut remaining_accounts = PackedAccounts::default(); + let system_config = SystemAccountMetaConfig::new(*program_id); + let _ = remaining_accounts.add_system_accounts_small(system_config); + + // Get address tree info + let address_tree_pubkey = rpc.get_address_tree_v2().queue; + + // Create the instruction + let accounts = anchor_compressible::accounts::CreateGameSession { + player: payer.pubkey(), + game_session: *game_session_pda, + system_program: solana_sdk::system_program::ID, + config: *config_pda, + rent_recipient: RENT_RECIPIENT, + }; + + // Derive a new address for the compressed account + let compressed_address = derive_address( + &game_session_pda.to_bytes(), + &address_tree_pubkey.to_bytes(), + &program_id.to_bytes(), + ); + + // Get validity proof from RPC + let rpc_result = rpc + .get_validity_proof( + vec![], + vec![AddressWithTree { + address: compressed_address, + tree: address_tree_pubkey, + }], + None, + ) + .await + .unwrap() + .value; + + // Pack tree infos into remaining accounts + let packed_tree_infos = rpc_result.pack_tree_infos(&mut remaining_accounts); + + // Get the packed address tree info + let address_tree_info = packed_tree_infos.address_trees[0]; + + // Get output state tree index + let output_state_tree_index = remaining_accounts.insert_or_get( + state_tree_queue.unwrap_or_else(|| rpc.get_random_state_tree_info().unwrap().queue), + ); + + // Get system accounts for the instruction + let (system_accounts, _, _) = remaining_accounts.to_account_metas(); + + // Create instruction data + let instruction_data = anchor_compressible::instruction::CreateGameSession { + session_id, + game_type: "Battle Royale".to_string(), + proof: rpc_result.proof, + compressed_address, + address_tree_info, + output_state_tree_index, + }; + + // Build the instruction + let instruction = Instruction { + program_id: *program_id, + accounts: [accounts.to_account_metas(None), system_accounts].concat(), + data: instruction_data.data(), + }; + + // Create and send transaction + let result = rpc + .create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer]) + .await; + + assert!(result.is_ok(), "Transaction should succeed"); + + // Verify the account is empty after compression + let game_session_account = rpc.get_account(*game_session_pda).await.unwrap(); + assert!( + game_session_account.is_some(), + "Account should exist after compression" + ); + + let account = game_session_account.unwrap(); + assert_eq!(account.lamports, 0, "Account lamports should be 0"); + assert!(account.data.is_empty(), "Account data should be empty"); + + let compressed_game_session = rpc + .get_compressed_account(compressed_address, None) + .await + .unwrap() + .value; + + assert_eq!(compressed_game_session.address, Some(compressed_address)); + assert!(compressed_game_session.data.is_some()); + + let buf = compressed_game_session.data.unwrap().data; + + let game_session = GameSession::deserialize(&mut &buf[..]).unwrap(); + + assert_eq!(game_session.session_id, session_id); + assert_eq!(game_session.game_type, "Battle Royale"); + assert_eq!(game_session.player, payer.pubkey()); + assert_eq!(game_session.score, 0); + assert!(game_session.compression_info.is_none()); +} + +#[allow(clippy::too_many_arguments)] +async fn decompress_multiple_pdas_with_ctoken( + rpc: &mut LightProgramTest, + payer: &Keypair, + program_id: &Pubkey, + user_record_pda: &Pubkey, + game_session_pda: &Pubkey, + session_id: u64, + expected_user_name: &str, + expected_game_type: &str, + expected_slot: u64, + compressed_token_account: light_client::indexer::CompressedTokenAccount, + native_token_account: Pubkey, +) { + let address_tree_pubkey = rpc.get_address_tree_v2().queue; + + // c pda USER_RECORD + 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; + + let user_account_data = c_user_pda.data.as_ref().unwrap(); + let c_user_record = UserRecord::deserialize(&mut &user_account_data.data[..]).unwrap(); + + // c pda GAME_SESSION + 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; + let game_account_data = c_game_pda.data.as_ref().unwrap(); + let c_game_session = GameSession::deserialize(&mut &game_account_data.data[..]).unwrap(); + + // Get validity proof for all three compressed accounts + let rpc_result = rpc + .get_validity_proof( + vec![ + c_user_pda.hash, + c_game_pda.hash, + compressed_token_account.clone().account.hash.clone(), + ], + vec![], + None, + ) + .await + .unwrap() + .value; + + let output_state_tree_info = rpc.get_random_state_tree_info().unwrap(); + + assert_eq!(compressed_token_account.token.owner, native_token_account); + + let instruction = + light_compressible_client::CompressibleInstruction::decompress_accounts_idempotent( + program_id, + &CompressibleInstruction::DECOMPRESS_ACCOUNTS_IDEMPOTENT_DISCRIMINATOR, + &payer.pubkey(), + &payer.pubkey(), + // must be same order as the compressed_accounts! + // &[*user_record_pda, *game_session_pda], + // &[native_token_account], + &[*user_record_pda, *game_session_pda, native_token_account], + &[ + // gets packed internally and never unpacked onchain: + ( + c_user_pda.clone(), + CompressedAccountVariant::UserRecord(c_user_record), + ), + ( + c_game_pda.clone(), + CompressedAccountVariant::GameSession(c_game_session), + ), + ( + compressed_token_account.clone().account, + CompressedAccountVariant::CompressibleTokenData( + CompressibleTokenDataWithVariant:: { + variant: CTokenAccountVariant::CTokenSigner, + token_data: compressed_token_account.clone().token, + }, + ), + ), + ], + rpc_result, + output_state_tree_info, + ) + .unwrap(); + + // Verify PDAs are uninitialized before decompression + 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"); + + // Verify UserRecord PDA is decompressed + 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 + ); + + // Verify GameSession PDA is decompressed + 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], + anchor_compressible::GameSession::DISCRIMINATOR, + "Game account anchor discriminator mismatch" + ); + + let decompressed_game_session = + anchor_compressible::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 + ); + + // Verify the native token account has the decompressed tokens + let token_account_data = rpc + .get_account(native_token_account) + .await + .unwrap() + .unwrap(); + // For now, just verify the account exists and has data + assert!( + !token_account_data.data.is_empty(), + "Token account should have data" + ); + assert_eq!(token_account_data.owner, COMPRESSED_TOKEN_PROGRAM_ID.into()); + + // Ensure all compressed accounts are now empty (closed) + let compressed_user_record_data = rpc + .get_compressed_account(c_user_pda.clone().address.clone().unwrap(), None) + .await + .unwrap() + .value; + let compressed_game_session_data = rpc + .get_compressed_account(c_game_pda.clone().address.clone().unwrap(), None) + .await + .unwrap() + .value; + rpc.get_compressed_account_by_hash(compressed_token_account.clone().account.hash.clone(), None) + .await + .expect_err("Compressed token account should not be found"); + + assert!( + compressed_user_record_data.data.unwrap().data.is_empty(), + "Compressed user record should be closed/empty after decompression" + ); + assert!( + compressed_game_session_data.data.unwrap().data.is_empty(), + "Compressed game session should be closed/empty after decompression" + ); +} + +#[allow(clippy::too_many_arguments)] +async fn decompress_multiple_pdas( + rpc: &mut LightProgramTest, + payer: &Keypair, + program_id: &Pubkey, + user_record_pda: &Pubkey, + game_session_pda: &Pubkey, + session_id: u64, + expected_user_name: &str, + expected_game_type: &str, + expected_slot: u64, +) { + let address_tree_pubkey = rpc.get_address_tree_v2().queue; + + // c pda USER_RECORD + 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; + + let user_account_data = c_user_pda.data.as_ref().unwrap(); + + let c_user_record = UserRecord::deserialize(&mut &user_account_data.data[..]).unwrap(); + + // c pda GAME_SESSION + 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; + let game_account_data = c_game_pda.data.as_ref().unwrap(); + + let c_game_session = GameSession::deserialize(&mut &game_account_data.data[..]).unwrap(); + + // Get validity proof for both compressed accounts + 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(); + + // Use the new SDK helper function with typed data + let instruction = + light_compressible_client::CompressibleInstruction::decompress_accounts_idempotent( + program_id, + &CompressibleInstruction::DECOMPRESS_ACCOUNTS_IDEMPOTENT_DISCRIMINATOR, + &payer.pubkey(), + &payer.pubkey(), // rent_payer can be the same as fee_payer + &[*user_record_pda, *game_session_pda], + &[ + ( + c_user_pda, + CompressedAccountVariant::UserRecord(c_user_record), + ), + ( + c_game_pda, + CompressedAccountVariant::GameSession(c_game_session), + ), + ], + rpc_result, + output_state_tree_info, + ) + .unwrap(); + + // Verify PDAs are uninitialized before decompression + let user_pda_account = rpc.get_account(*user_record_pda).await.unwrap(); + assert_eq!( + user_pda_account.as_ref().map(|a| a.data.len()).unwrap_or(0), + 0, + "User PDA account data len must be 0 before decompression" + ); + + let game_pda_account = rpc.get_account(*game_session_pda).await.unwrap(); + assert_eq!( + game_pda_account.as_ref().map(|a| a.data.len()).unwrap_or(0), + 0, + "Game PDA account data len must be 0 before decompression" + ); + + let cu = simulate_cu(rpc, payer, &instruction).await; + println!("decompress_multiple_pdas CU consumed: {}", cu); + + let result = rpc + .create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer]) + .await; + assert!(result.is_ok(), "Decompress transaction should succeed"); + + // Verify UserRecord PDA is decompressed + 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 + ); + + // Verify GameSession PDA is decompressed + 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], + anchor_compressible::GameSession::DISCRIMINATOR, + "Game account anchor discriminator mismatch" + ); + + let decompressed_game_session = + anchor_compressible::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 + ); + + // Verify compressed accounts exist and have correct data + let c_game_pda = rpc + .get_compressed_account(game_compressed_address, None) + .await + .unwrap() + .value; + + assert!(c_game_pda.data.is_some()); + assert_eq!(c_game_pda.data.unwrap().data.len(), 0); +} + +async fn create_user_record_and_game_session( + rpc: &mut LightProgramTest, + user: &Keypair, + program_id: &Pubkey, + config_pda: &Pubkey, + user_record_pda: &Pubkey, + game_session_pda: &Pubkey, + session_id: u64, +) -> (light_client::indexer::CompressedTokenAccount, Pubkey) { + let state_tree_info = rpc.get_random_state_tree_info().unwrap(); + + // Setup remaining accounts for Light Protocol + 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_small(system_config); + + // Get address tree info + let address_tree_pubkey = rpc.get_address_tree_v2().queue; + + // Create a mint signer for the compressed mint + let decimals = 6u8; + let mint_authority_keypair = Keypair::new(); + let mint_authority = mint_authority_keypair.pubkey(); + let freeze_authority = mint_authority; // Same as mint authority for this example + let mint_signer = Keypair::new(); + let compressed_mint_address = + derive_compressed_mint_address(&mint_signer.pubkey(), &address_tree_pubkey); + + // Find mint bump for the instruction + let (spl_mint, mint_bump) = find_spl_mint_address(&mint_signer.pubkey()); + // Create the instruction + let accounts = anchor_compressible::accounts::CreateUserRecordAndGameSession { + user: user.pubkey(), + user_record: *user_record_pda, + game_session: *game_session_pda, + mint_signer: mint_signer.pubkey(), + compressed_token_program: light_sdk_types::constants::C_TOKEN_PROGRAM_ID.into(), + system_program: solana_sdk::system_program::ID, + config: *config_pda, + rent_recipient: RENT_RECIPIENT, + mint_authority, + compress_token_program_cpi_authority: Pubkey::new_from_array(CPI_AUTHORITY_PDA), + }; + // Derive addresses for both compressed accounts + 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(), + ); + + // Get validity proof from RPC including mint address + 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); + + // Pack tree infos into remaining accounts + let packed_tree_infos = rpc_result.pack_tree_infos(&mut remaining_accounts); + + // Get the packed address tree info (all should use the same tree) + 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]; + + // Get system accounts for the instruction + let (system_accounts, _, _) = remaining_accounts.to_account_metas(); + + // Create instruction data + let instruction_data = anchor_compressible::instruction::CreateUserRecordAndGameSession { + account_data: anchor_compressible::AccountCreationData { + user_name: "Combined User".to_string(), + session_id, + game_type: "Combined Game".to_string(), + // Add mint metadata + 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: anchor_compressible::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, + // Add mint compression parameters + 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 { + base: BaseCompressedMint { + version: 1, + spl_mint: spl_mint.into(), + supply: 0, + decimals, + mint_authority: Some(mint_authority.into()), + freeze_authority: Some(freeze_authority.into()), + is_decompressed: false, + }, + extensions: None, + }, + }, + }, + }; + + // Build the instruction + let instruction = Instruction { + program_id: *program_id, + accounts: [accounts.to_account_metas(None), system_accounts].concat(), + data: instruction_data.data(), + }; + + // Create and send transaction + 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" + ); + + // Verify both accounts are empty after compression + let user_record_account = rpc.get_account(*user_record_pda).await.unwrap(); + assert!( + user_record_account.is_some(), + "User record account should exist after compression" + ); + let account = user_record_account.unwrap(); + assert_eq!( + account.lamports, 0, + "User record account lamports should be 0" + ); + assert!( + account.data.is_empty(), + "User record account data should be empty" + ); + + let game_session_account = rpc.get_account(*game_session_pda).await.unwrap(); + assert!( + game_session_account.is_some(), + "Game session account should exist after compression" + ); + let account = game_session_account.unwrap(); + assert_eq!( + account.lamports, 0, + "Game session account lamports should be 0" + ); + assert!( + account.data.is_empty(), + "Game session account data should be empty" + ); + + // Verify compressed accounts exist and have correct data + let compressed_user_record = rpc + .get_compressed_account(user_compressed_address, None) + .await + .unwrap() + .value; + + 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; + + 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); + + // SAME AS OWNER + let token_account_address = get_ctoken_signer_seeds( + &user.pubkey(), + &find_spl_mint_address(&mint_signer.pubkey()).0, + ) + .1; + + // Fetch the compressed token account that was created during the mint action + let compressed_token_accounts = rpc + .get_compressed_token_accounts_by_owner(&token_account_address, None, None) + .await + .unwrap() + .value; + + assert!( + !compressed_token_accounts.items.is_empty(), + "Should have at least one compressed token account" + ); + + // Get the first (and should be only) compressed token account + let compressed_token_account = compressed_token_accounts.items[0].clone(); + + (compressed_token_account, mint_signer.pubkey()) +} + +async fn compress_record( + rpc: &mut LightProgramTest, + payer: &Keypair, + program_id: &Pubkey, + user_record_pda: &Pubkey, + should_fail: bool, +) -> Result { + // Get the current decompressed user record data + 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" + ); + + // Setup remaining accounts for Light Protocol + let mut remaining_accounts = PackedAccounts::default(); + let system_config = SystemAccountMetaConfig::new(*program_id); + let _ = remaining_accounts.add_system_accounts_small(system_config); + + // Get address tree info + let address_tree_pubkey = rpc.get_address_tree_v2().queue; + + let address = derive_address( + &user_record_pda.to_bytes(), + &address_tree_pubkey.to_bytes(), + &program_id.to_bytes(), + ); + + let compressed_account = rpc + .get_compressed_account(address, None) + .await + .unwrap() + .value; + let compressed_address = compressed_account.address.unwrap(); + + // Get validity proof from RPC + 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, + anchor_compressible::instruction::CompressAccountsIdempotent::DISCRIMINATOR, + &payer.pubkey(), + &payer.pubkey(), + &RENT_RECIPIENT, // rent_recipient + &[*user_record_pda], + &[account], + vec![anchor_compressible::get_user_record_seeds(&payer.pubkey()).0], // compressed_account + rpc_result, // validity_proof_with_context + output_state_tree_info, // output_state_tree_info + ) + .unwrap(); + + if !should_fail { + let cu = simulate_cu(rpc, payer, &instruction).await; + println!("CompressRecord CU consumed: {}", cu); + } + + // Create and send transaction + 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"); + } + + // Verify the PDA account is now empty (compressed) + let user_pda_account = rpc.get_account(*user_record_pda).await.unwrap(); + assert!( + user_pda_account.is_some(), + "Account should exist after compression" + ); + let account = user_pda_account.unwrap(); + assert_eq!( + account.lamports, 0, + "Account lamports should be 0 after compression" + ); + assert!( + account.data.is_empty(), + "Account data should be empty after compression" + ); + + // Verify the compressed account exists + let compressed_user_record = rpc + .get_compressed_account(compressed_address, None) + .await + .unwrap() + .value; + + assert_eq!(compressed_user_record.address, Some(compressed_address)); + assert!(compressed_user_record.data.is_some()); + + let buf = compressed_user_record.data.unwrap().data; + let user_record: UserRecord = UserRecord::deserialize(&mut &buf[..]).unwrap(); + + assert_eq!(user_record.name, "Test User"); + assert_eq!(user_record.score, 11); + assert_eq!(user_record.owner, payer.pubkey()); + assert!(user_record.compression_info.is_none()); + Ok(result.unwrap()) +} + +async fn decompress_single_user_record( + rpc: &mut LightProgramTest, + payer: &Keypair, + program_id: &Pubkey, + user_record_pda: &Pubkey, + _user_record_bump: &u8, + expected_user_name: &str, + expected_slot: u64, +) { + let address_tree_pubkey = rpc.get_address_tree_v2().queue; + + // Get compressed user record + 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; + + let user_account_data = c_user_pda.data.as_ref().unwrap(); + let c_user_record = UserRecord::deserialize(&mut &user_account_data.data[..]).unwrap(); + + // Get validity proof for the compressed account + 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(); + // Use the new SDK helper function with typed data + let instruction = + light_compressible_client::CompressibleInstruction::decompress_accounts_idempotent( + program_id, + &CompressibleInstruction::DECOMPRESS_ACCOUNTS_IDEMPOTENT_DISCRIMINATOR, + &payer.pubkey(), + &payer.pubkey(), // rent_payer can be the same as fee_payer + &[*user_record_pda], + &[( + c_user_pda, + CompressedAccountVariant::UserRecord(c_user_record), + )], + rpc_result, + output_state_tree_info, + ) + .unwrap(); + + // Verify PDA is uninitialized before decompression + 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"); + + // Verify UserRecord PDA is decompressed + 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 + ); +} + +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, +) { + // Setup remaining accounts for Light Protocol + let mut remaining_accounts = PackedAccounts::default(); + let system_config = SystemAccountMetaConfig::new(*program_id); + let _ = remaining_accounts.add_system_accounts_small(system_config); + + // Get address tree info + let address_tree_pubkey = rpc.get_address_tree_v2().queue; + + // Create the instruction + let accounts = anchor_compressible::accounts::CreatePlaceholderRecord { + user: payer.pubkey(), + placeholder_record: *placeholder_record_pda, + system_program: solana_sdk::system_program::ID, + config: *config_pda, + rent_recipient: RENT_RECIPIENT, + }; + + // Derive a new address for the compressed account + let compressed_address = derive_address( + &placeholder_record_pda.to_bytes(), + &address_tree_pubkey.to_bytes(), + &program_id.to_bytes(), + ); + + // Get validity proof from RPC + let rpc_result = rpc + .get_validity_proof( + vec![], + vec![AddressWithTree { + address: compressed_address, + tree: address_tree_pubkey, + }], + None, + ) + .await + .unwrap() + .value; + + // Pack tree infos into remaining accounts + let packed_tree_infos = rpc_result.pack_tree_infos(&mut remaining_accounts); + + // Get the packed address tree info + let address_tree_info = packed_tree_infos.address_trees[0]; + + // Get output state tree index + let output_state_tree_index = + remaining_accounts.insert_or_get(rpc.get_random_state_tree_info().unwrap().queue); + + // Get system accounts for the instruction + let (system_accounts, _, _) = remaining_accounts.to_account_metas(); + + // Create instruction data + let instruction_data = anchor_compressible::instruction::CreatePlaceholderRecord { + placeholder_id, + name: name.to_string(), + proof: rpc_result.proof, + compressed_address, + address_tree_info, + output_state_tree_index, + }; + + // Build the instruction + let instruction = Instruction { + program_id: *program_id, + accounts: [accounts.to_account_metas(None), system_accounts].concat(), + data: instruction_data.data(), + }; + + let cu = simulate_cu(rpc, payer, &instruction).await; + println!("CreatePlaceholderRecord CU consumed: {}", cu); + + // Create and send transaction + let result = rpc + .create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer]) + .await; + + assert!( + result.is_ok(), + "CreatePlaceholderRecord transaction should succeed" + ); +} + +async fn compress_placeholder_record( + rpc: &mut LightProgramTest, + payer: &Keypair, + program_id: &Pubkey, + _config_pda: &Pubkey, + placeholder_record_pda: &Pubkey, + _placeholder_record_bump: &u8, + placeholder_id: u64, +) { + let address_tree_pubkey = rpc.get_address_tree_v2().queue; + + // Get compressed placeholder record address + let placeholder_compressed_address = derive_address( + &placeholder_record_pda.to_bytes(), + &address_tree_pubkey.to_bytes(), + &program_id.to_bytes(), + ); + + // Get the compressed account that already exists (empty) + let compressed_placeholder = rpc + .get_compressed_account(placeholder_compressed_address, None) + .await + .unwrap() + .value; + + // Get validity proof from RPC + let rpc_result = rpc + .get_validity_proof(vec![compressed_placeholder.hash], vec![], None) + .await + .unwrap() + .value; + + let placeholder_seeds = anchor_compressible::get_placeholder_record_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, + &anchor_compressible::instruction::CompressAccountsIdempotent::DISCRIMINATOR, + &payer.pubkey(), + &payer.pubkey(), + &RENT_RECIPIENT, + &[*placeholder_record_pda], + &[account], + vec![placeholder_seeds.0], + rpc_result, + output_state_tree_info, + ) + .unwrap(); + + let cu = simulate_cu(rpc, payer, &instruction).await; + println!("CompressPlaceholderRecord CU consumed: {}", cu); + + let result = rpc + .create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer]) + .await; + + assert!( + result.is_ok(), + "CompressPlaceholderRecord transaction should succeed: {:?}", + result + ); + + // Check if PDA account is closed (it may or may not be depending on the compression behavior) + let _account = rpc.get_account(*placeholder_record_pda).await.unwrap(); + + // Verify compressed account now has the data + let compressed_placeholder_after = rpc + .get_compressed_account(placeholder_compressed_address, None) + .await + .unwrap() + .value; + + 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.len() > 0, + "Compressed account should contain the PDA data" + ); +} + +async fn compress_placeholder_record_for_double_test( + rpc: &mut LightProgramTest, + payer: &Keypair, + program_id: &Pubkey, + placeholder_record_pda: &Pubkey, + placeholder_id: u64, + previous_account: Option, +) -> Result { + let address_tree_pubkey = rpc.get_address_tree_v2().queue; + + // Get compressed placeholder record address + let placeholder_compressed_address = derive_address( + &placeholder_record_pda.to_bytes(), + &address_tree_pubkey.to_bytes(), + &program_id.to_bytes(), + ); + + // Get the compressed account that exists (initially empty, later with data) + let compressed_placeholder = rpc + .get_compressed_account(placeholder_compressed_address, None) + .await + .unwrap() + .value; + + // Get validity proof from RPC + let rpc_result = rpc + .get_validity_proof(vec![compressed_placeholder.hash], vec![], None) + .await + .unwrap() + .value; + + let placeholder_seeds = anchor_compressible::get_placeholder_record_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, + &anchor_compressible::instruction::CompressAccountsIdempotent::DISCRIMINATOR, + &payer.pubkey(), + &payer.pubkey(), + &RENT_RECIPIENT, + &[*placeholder_record_pda], + &accounts_to_compress, + vec![placeholder_seeds.0], + rpc_result, + output_state_tree_info, + ) + .unwrap(); + + // Create and send transaction + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer]) + .await +} + +async fn decompress_single_game_session( + rpc: &mut LightProgramTest, + payer: &Keypair, + program_id: &Pubkey, + game_session_pda: &Pubkey, + _game_bump: &u8, + session_id: u64, + expected_game_type: &str, + expected_slot: u64, + expected_score: u64, +) { + let address_tree_pubkey = rpc.get_address_tree_v2().queue; + + // Get compressed game session + 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; + + let game_account_data = c_game_pda.data.as_ref().unwrap(); + let c_game_session = + anchor_compressible::GameSession::deserialize(&mut &game_account_data.data[..]).unwrap(); + + // Get validity proof for the compressed account + 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(); + + // Use the SDK helper function with typed data + let instruction = + light_compressible_client::CompressibleInstruction::decompress_accounts_idempotent( + program_id, + &CompressibleInstruction::DECOMPRESS_ACCOUNTS_IDEMPOTENT_DISCRIMINATOR, + &payer.pubkey(), + &payer.pubkey(), // rent_payer can be the same as fee_payer + &[*game_session_pda], + &[( + c_game_pda, + anchor_compressible::CompressedAccountVariant::GameSession(c_game_session), + )], + 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"); + + // Verify GameSession PDA is decompressed + 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], + anchor_compressible::GameSession::DISCRIMINATOR, + "Game account anchor discriminator mismatch" + ); + + let decompressed_game_session = + anchor_compressible::GameSession::try_deserialize(&mut &game_pda_data[..]).unwrap(); + assert_eq!(decompressed_game_session.session_id, session_id); + assert_eq!(decompressed_game_session.game_type, expected_game_type); + assert_eq!(decompressed_game_session.player, payer.pubkey()); + assert_eq!(decompressed_game_session.score, expected_score); + assert!(!decompressed_game_session + .compression_info + .as_ref() + .unwrap() + .is_compressed()); + assert_eq!( + decompressed_game_session + .compression_info + .as_ref() + .unwrap() + .last_written_slot(), + expected_slot + ); +} + +async fn compress_game_session_with_custom_data( + rpc: &mut LightProgramTest, + _payer: &Keypair, + _program_id: &Pubkey, + game_session_pda: &Pubkey, + _session_id: u64, +) { + let game_pda_account = rpc.get_account(*game_session_pda).await.unwrap().unwrap(); + let game_pda_data = game_pda_account.data; + let original_game_session = + anchor_compressible::GameSession::try_deserialize(&mut &game_pda_data[..]).unwrap(); + + // Test the custom compression trait directly + let custom_compressed_data = match original_game_session.compress_as() { + std::borrow::Cow::Borrowed(data) => data.clone(), // Should never happen since compression_info must be None + std::borrow::Cow::Owned(data) => data, // Use owned data directly + }; + + // Verify that the custom compression works as expected + assert_eq!( + custom_compressed_data.session_id, original_game_session.session_id, + "Session ID should be kept" + ); + assert_eq!( + custom_compressed_data.player, original_game_session.player, + "Player should be kept" + ); + assert_eq!( + custom_compressed_data.game_type, original_game_session.game_type, + "Game type should be kept" + ); + assert_eq!( + custom_compressed_data.start_time, 0, + "Start time should be RESET to 0" + ); + assert_eq!( + custom_compressed_data.end_time, None, + "End time should be RESET to None" + ); + assert_eq!( + custom_compressed_data.score, 0, + "Score should be RESET to 0" + ); +} + +#[tokio::test] +async fn test_double_compression_attack() { + let program_id = anchor_compressible::ID; + let config = ProgramTestConfig::new_v2(true, Some(vec![("anchor_compressible", 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); + + // Initialize compression config + let result = initialize_compression_config( + &mut rpc, + &payer, + &program_id, + &payer, + 100, + RENT_RECIPIENT, + vec![ADDRESS_SPACE[0]], + &CompressibleInstruction::INITIALIZE_COMPRESSION_CONFIG_DISCRIMINATOR, + None, + ) + .await; + assert!(result.is_ok(), "Initialize config should succeed"); + + // Create placeholder record + 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; + + // Verify the PDA exists and has data before first compression + 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" + ); + + // Verify empty compressed account was created + let address_tree_pubkey = rpc.get_address_tree_v2().queue; + let compressed_address = derive_address( + &placeholder_record_pda.to_bytes(), + &address_tree_pubkey.to_bytes(), + &program_id.to_bytes(), + ); + + let compressed_placeholder_before = rpc + .get_compressed_account(compressed_address, None) + .await + .unwrap() + .value; + + assert_eq!( + compressed_placeholder_before.address, + Some(compressed_address), + "Empty compressed account should exist" + ); + assert_eq!( + compressed_placeholder_before.data.unwrap().data.len(), + 0, + "Compressed account should be empty initially" + ); + + // Wait past compression delay + rpc.warp_to_slot(200).unwrap(); + + // First compression - should succeed and move data from PDA to compressed account + 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 + ); + + // Verify PDA is now empty/closed after first compression + let placeholder_pda_after_first = rpc.get_account(placeholder_record_pda).await.unwrap(); + if let Some(account) = placeholder_pda_after_first { + assert_eq!( + account.lamports, 0, + "PDA should have 0 lamports after first compression" + ); + assert!( + account.data.is_empty(), + "PDA should have no data after first compression" + ); + } + + // Verify compressed account now has the data + let compressed_placeholder_after_first = rpc + .get_compressed_account(compressed_address, None) + .await + .unwrap() + .value; + + 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" + ); + + // Second compression attempt - should succeed idempotently (skip already compressed account) + let second_compression_result = compress_placeholder_record_for_double_test( + &mut rpc, + &payer, + &program_id, + &placeholder_record_pda, + placeholder_id, + Some(account_before), + ) + .await; + + // This should succeed because the instruction is idempotent + assert!( + second_compression_result.is_ok(), + "Second compression should succeed idempotently: {:?}", + second_compression_result + ); + + // Verify state hasn't changed after second compression attempt + let placeholder_pda_after_second = rpc.get_account(placeholder_record_pda).await.unwrap(); + if let Some(account) = placeholder_pda_after_second { + assert_eq!( + account.lamports, 0, + "PDA should still have 0 lamports after second compression" + ); + assert!( + account.data.is_empty(), + "PDA should still have no data after second compression" + ); + } + + let compressed_placeholder_after_second = rpc + .get_compressed_account(compressed_address, None) + .await + .unwrap() + .value; + + // Verify compressed account data is unchanged + 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" + ); +} + +async fn compress_token_account_after_decompress( + rpc: &mut LightProgramTest, + user: &Keypair, + program_id: &Pubkey, + _config_pda: &Pubkey, + token_account_address: 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], +) { + // Verify the token account exists and has the expected data + 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) = + anchor_compressible::get_user_record_seeds(&user.pubkey()); + let (game_session_seeds, game_session_pubkey) = + anchor_compressible::get_game_session_seeds(session_id); + let (token_account_seeds, token_account_address) = + get_ctoken_signer_seeds(&user.pubkey(), &mint); + + let mut accounts: Vec = vec![]; + + 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(); + + accounts.push(user_record_account); + accounts.push(game_session_account); + accounts.push(token_account); // must come last. + + assert_eq!(*user_record_pda, user_record_pubkey); + assert_eq!(*game_session_pda, game_session_pubkey); + assert_eq!(token_account_address, token_account_address); + + 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; + let game_session: CompressedAccount = rpc + .get_compressed_account(compressed_game_session_address, None) + .await + .unwrap() + .value; + + 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, + &anchor_compressible::instruction::CompressAccountsIdempotent::DISCRIMINATOR, + &user.pubkey(), + &user.pubkey(), + &RENT_RECIPIENT, + &[ + user_record_pubkey, + game_session_pubkey, + token_account_address, + ], + &accounts, + vec![user_record_seeds, game_session_seeds, token_account_seeds], + proof_with_context, + random_tree_info, + ) + .unwrap(); + + // Send the transaction + let result = rpc + .create_and_send_transaction(&[instruction], &user.pubkey(), &[user]) + .await; + + assert!( + result.is_ok(), + "Compress token account transaction should succeed: {:?}", + result + ); + + // Verify the token account is now closed/empty + let token_account_after = rpc.get_account(token_account_address).await.unwrap(); + if let Some(account) = token_account_after { + assert_eq!( + account.lamports, 0, + "Token account should have 0 lamports after compression" + ); + assert!( + account.data.is_empty(), + "Token account should have no data after compression" + ); + } + + // Verify the compressed token account exists + let compressed_token_accounts = rpc + .get_compressed_token_accounts_by_owner(&token_account_address, None, None) + .await + .unwrap() + .value; + + assert!( + !compressed_token_accounts.items.is_empty(), + "Should have at least one compressed token account after compression" + ); + + let compressed_token = &compressed_token_accounts.items[0]; + assert_eq!( + compressed_token.token.mint, mint, + "Compressed token should have the same mint" + ); + assert_eq!( + compressed_token.token.owner, token_account_address, + "Compressed token owner should be the token account address" + ); + assert_eq!( + compressed_token.token.amount, amount, + "Compressed token should have the same amount" + ); + 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(); + + assert_eq!( + user_record_account.lamports, 0, + "User record account should be None" + ); + assert_eq!( + game_session_account.lamports, 0, + "Game session account should be None" + ); + assert_eq!(token_account.lamports, 0, "Token account should be None"); + assert!( + user_record_account.data.is_empty(), + "User record account should be empty" + ); + assert!( + game_session_account.data.is_empty(), + "Game session account should be empty" + ); + assert!( + token_account.data.is_empty(), + "Token account should be empty" + ); +} diff --git a/sdk-tests/anchor-compressible/tests/test_discriminator.rs b/sdk-tests/anchor-compressible/tests/test_discriminator.rs new file mode 100644 index 0000000000..b5fb4d20c1 --- /dev/null +++ b/sdk-tests/anchor-compressible/tests/test_discriminator.rs @@ -0,0 +1,18 @@ +#[test] +fn test_discriminator() { + use anchor_compressible::UserRecord; + use anchor_lang::Discriminator; + use light_sdk::LightDiscriminator; + + // anchor + let light_discriminator = UserRecord::DISCRIMINATOR; + println!("light discriminator: {:?}", light_discriminator); + + // ours (should be anchor compatible.) + let anchor_discriminator = UserRecord::LIGHT_DISCRIMINATOR; + + println!("Anchor discriminator: {:?}", anchor_discriminator); + println!("Match: {}", light_discriminator == anchor_discriminator); + + assert_eq!(light_discriminator, anchor_discriminator); +} diff --git a/sdk-tests/native-compressible/Cargo.toml b/sdk-tests/native-compressible/Cargo.toml new file mode 100644 index 0000000000..fa9d53630d --- /dev/null +++ b/sdk-tests/native-compressible/Cargo.toml @@ -0,0 +1,46 @@ +[package] +name = "native-compressible" +version = "1.0.0" +description = "Test program using generalized account compression" +repository = "https://github.com/Lightprotocol/light-protocol" +license = "Apache-2.0" +edition = "2021" + +[lib] +crate-type = ["cdylib", "lib"] +name = "native_compressible" +doctest = false + +[features] +no-entrypoint = [] +no-idl = [] +no-log-ix-name = [] +cpi = ["no-entrypoint"] +test-sbf = [] +default = [] + +[dependencies] +light-sdk = { workspace = true, default-features = false, features = ["borsh"] } +light-sdk-types = { workspace = true, default-features = false } +light-hasher = { workspace = true, features = ["solana"], default-features = false } +solana-program = { workspace = true } +light-macros = { workspace = true, features = ["solana"], default-features = false } +borsh = { workspace = true } +light-compressed-account = { workspace = true, features = ["solana"], default-features = false } +solana-clock = { workspace = true } +solana-sysvar = { workspace = true } + +[dev-dependencies] +light-program-test = { workspace = true, features = ["v2"], default-features = false } +light-client = { workspace = true } +light-compressible-client = { workspace = true } +tokio = { workspace = true } +solana-sdk = { workspace = true } + +[lints.rust.unexpected_cfgs] +level = "allow" +check-cfg = [ + 'cfg(target_os, values("solana"))', + 'cfg(feature, values("frozen-abi", "no-entrypoint"))', +] + diff --git a/sdk-tests/native-compressible/Xargo.toml b/sdk-tests/native-compressible/Xargo.toml new file mode 100644 index 0000000000..475fb71ed1 --- /dev/null +++ b/sdk-tests/native-compressible/Xargo.toml @@ -0,0 +1,2 @@ +[target.bpfel-unknown-unknown.dependencies.std] +features = [] diff --git a/sdk-tests/native-compressible/src/compress_dynamic_pda.rs b/sdk-tests/native-compressible/src/compress_dynamic_pda.rs new file mode 100644 index 0000000000..82f3c8d913 --- /dev/null +++ b/sdk-tests/native-compressible/src/compress_dynamic_pda.rs @@ -0,0 +1,85 @@ +use borsh::{BorshDeserialize, BorshSerialize}; +use light_sdk::{ + compressible::{compress_pda_native, CompressibleConfig}, + cpi::CpiAccountsSmall, + error::LightSdkError, + instruction::{account_meta::CompressedAccountMeta, ValidityProof}, +}; +use light_sdk_types::CpiAccountsConfig; +use solana_program::{account_info::AccountInfo, msg}; + +use crate::MyPdaAccount; + +/// Generic instruction data for compress account +/// This matches the expected format for compress account instructions +#[derive(BorshDeserialize, BorshSerialize)] +pub struct GenericCompressAccountInstruction { + pub proof: ValidityProof, + pub compressed_account_meta: CompressedAccountMeta, +} + +/// Compresses a PDA back into a compressed account +/// Anyone can call this after the timeout period has elapsed +pub fn compress_dynamic_pda( + accounts: &[AccountInfo], + instruction_data: &[u8], +) -> Result<(), LightSdkError> { + let mut instruction_data = instruction_data; + let instruction_data = GenericCompressAccountInstruction::deserialize(&mut instruction_data) + .map_err(|e| { + solana_program::msg!( + "Failed to deserialize GenericCompressAccountInstruction: {:?}", + e + ); + LightSdkError::Borsh + })?; + + let solana_account = &mut accounts[1].clone(); + let config_account = &accounts[2]; + let rent_recipient = &accounts[3]; + + msg!("solana_account?: {:?}", solana_account.key); + msg!("config_account?: {:?}", config_account.key); + msg!("rent_recipient?: {:?}", rent_recipient.key); + + // Load config + let config = CompressibleConfig::load_checked(config_account, &crate::ID)?; + + // CHECK: rent recipient from config + if rent_recipient.key != &config.rent_recipient { + solana_program::msg!( + "Rent recipient does not match config: {:?} != {:?}", + rent_recipient.key, + config.rent_recipient + ); + return Err(LightSdkError::ConstraintViolation); + } + + // Cpi accounts + let cpi_config = CpiAccountsConfig::new(crate::LIGHT_CPI_SIGNER); + let cpi_accounts = CpiAccountsSmall::new_with_config(&accounts[0], &accounts[4..], cpi_config); + + // Deserialize the PDA account data (skip the 8-byte discriminator) + // Use a scope to ensure the borrow is dropped before compression + let mut pda_data = { + let account_data = solana_account.data.borrow(); + msg!("pda account: {:?}", account_data); + + MyPdaAccount::deserialize(&mut &account_data[8..]).map_err(|e| { + solana_program::msg!("Failed to deserialize MyPdaAccount: {:?}", e); + LightSdkError::Borsh + })? + }; // account_data borrow is dropped here + + compress_pda_native::( + solana_account, + &mut pda_data, + &instruction_data.compressed_account_meta, + instruction_data.proof, + cpi_accounts, + rent_recipient, + &config.compression_delay, + )?; + + Ok(()) +} diff --git a/sdk-tests/native-compressible/src/compress_empty_compressed_pda.rs b/sdk-tests/native-compressible/src/compress_empty_compressed_pda.rs new file mode 100644 index 0000000000..6b3f389b81 --- /dev/null +++ b/sdk-tests/native-compressible/src/compress_empty_compressed_pda.rs @@ -0,0 +1,83 @@ +use borsh::{BorshDeserialize, BorshSerialize}; +use light_sdk::{ + compressible::{compress_pda_native, CompressibleConfig}, + cpi::CpiAccountsSmall, + error::LightSdkError, + instruction::{account_meta::CompressedAccountMeta, ValidityProof}, +}; +use light_sdk_types::CpiAccountsConfig; +use solana_program::{account_info::AccountInfo, msg}; + +use crate::MyPdaAccount; + +/// Generic instruction data for compress empty compressed PDA +/// This compresses a PDA that was created via create_empty_compressed_pda +#[derive(BorshDeserialize, BorshSerialize)] +pub struct CompressEmptyCompressedPdaInstruction { + pub proof: ValidityProof, + pub compressed_account_meta: CompressedAccountMeta, +} + +/// Compresses a PDA that was created with empty compressed account back into a compressed account +/// This is the second step after create_empty_compressed_pda +pub fn compress_empty_compressed_pda( + accounts: &[AccountInfo], + instruction_data: &[u8], +) -> Result<(), LightSdkError> { + let mut instruction_data = instruction_data; + let instruction_data = + CompressEmptyCompressedPdaInstruction::deserialize(&mut instruction_data).map_err(|e| { + solana_program::msg!( + "Failed to deserialize CompressEmptyCompressedPdaInstruction: {:?}", + e + ); + LightSdkError::Borsh + })?; + + let solana_account = &mut accounts[1].clone(); + let config_account = &accounts[2]; + let rent_recipient = &accounts[3]; + + // Load config + let config = CompressibleConfig::load_checked(config_account, &crate::ID)?; + + // CHECK: rent recipient from config + if rent_recipient.key != &config.rent_recipient { + solana_program::msg!( + "Rent recipient does not match config: {:?} != {:?}", + rent_recipient.key, + config.rent_recipient + ); + return Err(LightSdkError::ConstraintViolation); + } + + // Cpi accounts + let cpi_config = CpiAccountsConfig::new(crate::LIGHT_CPI_SIGNER); + let cpi_accounts = CpiAccountsSmall::new_with_config(&accounts[0], &accounts[4..], cpi_config); + + // Deserialize the PDA account data (skip the 8-byte discriminator) + // Use a scope to ensure the borrow is dropped before compression + let mut pda_data = { + let account_data = solana_account.data.borrow(); + msg!("pda account: {:?}", account_data); + + MyPdaAccount::deserialize(&mut &account_data[8..]).map_err(|e| { + solana_program::msg!("Failed to deserialize MyPdaAccount: {:?}", e); + LightSdkError::Borsh + })? + }; // account_data borrow is dropped here + + msg!("Compressing PDA that was created with empty compressed account"); + + compress_pda_native::( + solana_account, + &mut pda_data, + &instruction_data.compressed_account_meta, + instruction_data.proof, + cpi_accounts, + rent_recipient, + &config.compression_delay, + )?; + + Ok(()) +} diff --git a/sdk-tests/native-compressible/src/create_config.rs b/sdk-tests/native-compressible/src/create_config.rs new file mode 100644 index 0000000000..009bc3664f --- /dev/null +++ b/sdk-tests/native-compressible/src/create_config.rs @@ -0,0 +1,67 @@ +use borsh::{BorshDeserialize, BorshSerialize}; +use light_sdk::{ + compressible::process_initialize_compression_config_checked as sdk_process_initialize_compression_config_checked, + error::LightSdkError, +}; +use solana_program::{account_info::AccountInfo, msg, pubkey::Pubkey}; + +/// Creates a new compressible config PDA +pub fn process_initialize_compression_config_checked( + accounts: &[AccountInfo], + instruction_data: &[u8], +) -> Result<(), LightSdkError> { + let mut instruction_data = instruction_data; + msg!("instruction_data: {:?}", instruction_data.len()); + let instruction_data = InitializeCompressionConfigData::deserialize(&mut instruction_data) + .map_err(|err| { + msg!( + "InitializeCompressionConfigData::deserialize error: {:?}", + err + ); + LightSdkError::Borsh + })?; + + // Get accounts + let payer = &accounts[0]; + let config_account = &accounts[1]; + let program_data_account = &accounts[2]; + let update_authority = &accounts[3]; + let system_program = &accounts[4]; + + sdk_process_initialize_compression_config_checked( + config_account, + update_authority, + program_data_account, + &instruction_data.rent_recipient, + instruction_data.address_space, + instruction_data.compression_delay, + 0, // one global config for now, so bump is 0. + payer, + system_program, + &crate::ID, + )?; + + Ok(()) +} + +/// Generic instruction data for initialize config +/// Note: Real programs should use their specific instruction format +#[derive(BorshDeserialize, BorshSerialize)] +pub struct InitializeCompressionConfigData { + pub compression_delay: u32, + pub rent_recipient: Pubkey, + pub address_space: Vec, +} + +// Type alias for backward compatibility with tests +pub type CreateConfigInstructionData = InitializeCompressionConfigData; + +/// Generic instruction data for update config +/// Note: Real programs should use their specific instruction format +#[derive(BorshDeserialize, BorshSerialize)] +pub struct UpdateCompressionConfigData { + pub new_compression_delay: Option, + pub new_rent_recipient: Option, + pub new_address_space: Option>, + pub new_update_authority: Option, +} diff --git a/sdk-tests/native-compressible/src/create_dynamic_pda.rs b/sdk-tests/native-compressible/src/create_dynamic_pda.rs new file mode 100644 index 0000000000..a8768ce539 --- /dev/null +++ b/sdk-tests/native-compressible/src/create_dynamic_pda.rs @@ -0,0 +1,143 @@ +use borsh::{BorshDeserialize, BorshSerialize}; +use light_sdk::{ + compressible::{compress_account_on_init_native, CompressibleConfig, CompressionInfo}, + cpi::CpiAccountsSmall, + error::LightSdkError, + instruction::{PackedAddressTreeInfo, ValidityProof}, +}; +use solana_program::{ + account_info::AccountInfo, program::invoke_signed, pubkey::Pubkey, rent::Rent, + system_instruction, sysvar::Sysvar, +}; + +use crate::MyPdaAccount; + +/// INITS a PDA and compresses it into a new compressed account. +pub fn create_dynamic_pda( + accounts: &[AccountInfo], + instruction_data: &[u8], +) -> Result<(), LightSdkError> { + let mut instruction_data = instruction_data; + let instruction_data = CreateDynamicPdaInstructionData::deserialize(&mut instruction_data) + .map_err(|e| { + solana_program::msg!("Borsh deserialization error: {:?}", e); + LightSdkError::ProgramError(e.into()) + })?; + + let fee_payer = &accounts[0]; + // UNCHECKED: ...caller program checks this. + let solana_account = &accounts[1]; + let rent_recipient = &accounts[2]; + let config_account = &accounts[3]; + let system_program = &accounts[4]; + + // Load config + let config = CompressibleConfig::load_checked(config_account, &crate::ID)?; + + // CHECK: rent recipient from config + if rent_recipient.key != &config.rent_recipient { + solana_program::msg!( + "rent recipient mismatch {:?} != {:?}", + rent_recipient.key, + config.rent_recipient + ); + return Err(LightSdkError::ConstraintViolation); + } + + // Derive PDA with seeds and bump + // For this example, we'll use a simple seed pattern + let seed_data = b"dynamic_pda"; // You can customize this based on your needs + let (derived_pda, bump_seed) = Pubkey::find_program_address(&[seed_data], &crate::ID); + + // Verify the PDA matches what was passed in + if derived_pda != *solana_account.key { + solana_program::msg!( + "PDA derivation mismatch. derived_pda: {:?} != solana_account.key: {:?}", + derived_pda, + solana_account.key + ); + return Err(LightSdkError::ConstraintViolation); + } + + // Calculate space needed for MyPdaAccount + let account_space = std::mem::size_of::() + 8; // 8 bytes for discriminator + + // Calculate rent + let rent = Rent::get()?; + let rent_lamports = rent.minimum_balance(account_space); + + // Create the PDA account using system program + let create_account_ix = system_instruction::create_account( + fee_payer.key, + solana_account.key, + rent_lamports, + account_space as u64, + &crate::ID, + ); + + invoke_signed( + &create_account_ix, + &[ + fee_payer.clone(), + solana_account.clone(), + system_program.clone(), + ], + &[&[seed_data, &[bump_seed]]], + ) + .map_err(|e| { + solana_program::msg!("pda account create error: {:?}", e); + LightSdkError::ProgramError(e) + })?; + + // Initialize the PDA account data + let mut pda_account_data = MyPdaAccount { + compression_info: Some(CompressionInfo::new_decompressed()?), + data: [1; 31], // Initialize with default data + }; + + // Serialize the initial data into the account - use scope to ensure borrow is dropped + { + let mut account_data = solana_account.data.borrow_mut(); + pda_account_data + .serialize(&mut &mut account_data[..]) + .map_err(|e| { + solana_program::msg!("pda account serialization error: {:?}", e); + LightSdkError::ProgramError(e.into()) + })?; + } // account_data borrow is dropped here + + // Cpi accounts + let cpi_accounts_struct = + CpiAccountsSmall::new(fee_payer, &accounts[5..], crate::LIGHT_CPI_SIGNER); + + // the onchain PDA is the seed for the cPDA. this way devs don't have to + // change their onchain PDA checks. + let new_address_params = instruction_data + .address_tree_info + .into_new_address_params_packed(solana_account.key.to_bytes()); + + solana_program::msg!("pda account data: {:?}", pda_account_data); + + // Use the efficient native variant that accepts pre-deserialized data + compress_account_on_init_native::( + &mut solana_account.clone(), + &mut pda_account_data, + &instruction_data.compressed_address, + &new_address_params, + instruction_data.output_state_tree_index, + cpi_accounts_struct, + &config.address_space, + rent_recipient, + instruction_data.proof, + )?; + + Ok(()) +} + +#[derive(Clone, Debug, Default, BorshDeserialize, BorshSerialize)] +pub struct CreateDynamicPdaInstructionData { + pub proof: ValidityProof, + pub compressed_address: [u8; 32], + pub address_tree_info: PackedAddressTreeInfo, + pub output_state_tree_index: u8, +} diff --git a/sdk-tests/native-compressible/src/create_empty_compressed_pda.rs b/sdk-tests/native-compressible/src/create_empty_compressed_pda.rs new file mode 100644 index 0000000000..af5d44a2dc --- /dev/null +++ b/sdk-tests/native-compressible/src/create_empty_compressed_pda.rs @@ -0,0 +1,149 @@ +use borsh::{BorshDeserialize, BorshSerialize}; +use light_sdk::{ + compressible::{compress_empty_account_on_init_native, CompressibleConfig, CompressionInfo}, + cpi::CpiAccountsSmall, + error::LightSdkError, + instruction::{PackedAddressTreeInfo, ValidityProof}, +}; +use solana_program::{ + account_info::AccountInfo, program::invoke_signed, pubkey::Pubkey, rent::Rent, + system_instruction, sysvar::Sysvar, +}; + +use crate::MyPdaAccount; + +/// INITS a PDA and creates an EMPTY compressed account without closing the PDA. +/// The PDA remains intact with its data, and an empty compressed account is created. +pub fn create_empty_compressed_pda( + accounts: &[AccountInfo], + instruction_data: &[u8], +) -> Result<(), LightSdkError> { + let mut instruction_data = instruction_data; + let instruction_data = CreateEmptyCompressedPdaInstructionData::deserialize( + &mut instruction_data, + ) + .map_err(|e| { + solana_program::msg!("Borsh deserialization error: {:?}", e); + LightSdkError::ProgramError(e.into()) + })?; + + let fee_payer = &accounts[0]; + // UNCHECKED: ...caller program checks this. + let solana_account = &accounts[1]; + let config_account = &accounts[2]; + let system_program = &accounts[3]; + + // Load config + let config = CompressibleConfig::load_checked(config_account, &crate::ID)?; + + // Derive PDA with seeds and bump + // For this example, we'll use a simple seed pattern + let seed_data = b"empty_compressed_pda"; // Different seed from regular dynamic PDA + let (derived_pda, bump_seed) = Pubkey::find_program_address(&[seed_data], &crate::ID); + + // Verify the PDA matches what was passed in + if derived_pda != *solana_account.key { + solana_program::msg!( + "PDA derivation mismatch. derived_pda: {:?} != solana_account.key: {:?}", + derived_pda, + solana_account.key + ); + return Err(LightSdkError::ConstraintViolation); + } + + // Calculate space needed for MyPdaAccount + let account_space = std::mem::size_of::() + 8; // 8 bytes for discriminator + + // Calculate rent + let rent = Rent::get()?; + let rent_lamports = rent.minimum_balance(account_space); + + // Create the PDA account using system program + let create_account_ix = system_instruction::create_account( + fee_payer.key, + solana_account.key, + rent_lamports, + account_space as u64, + &crate::ID, + ); + + invoke_signed( + &create_account_ix, + &[ + fee_payer.clone(), + solana_account.clone(), + system_program.clone(), + ], + &[&[seed_data, &[bump_seed]]], + ) + .map_err(|e| { + solana_program::msg!("pda account create error: {:?}", e); + LightSdkError::ProgramError(e) + })?; + + // Initialize the PDA account data + let mut pda_account_data = MyPdaAccount { + compression_info: Some(CompressionInfo::new_decompressed()?), + data: [1; 31], // Initialize with same data as regular PDA (for consistency) + }; + + // Serialize the initial data into the account - use scope to ensure borrow is dropped + { + let mut account_data = solana_account.data.borrow_mut(); + pda_account_data + .serialize(&mut &mut account_data[..]) + .map_err(|e| { + solana_program::msg!("pda account serialization error: {:?}", e); + LightSdkError::ProgramError(e.into()) + })?; + } // account_data borrow is dropped here + + // Cpi accounts + let cpi_accounts_struct = + CpiAccountsSmall::new(fee_payer, &accounts[4..], crate::LIGHT_CPI_SIGNER); + + // the onchain PDA is the seed for the cPDA. this way devs don't have to + // change their onchain PDA checks. + let new_address_params = instruction_data + .address_tree_info + .into_new_address_params_packed(solana_account.key.to_bytes()); + + solana_program::msg!("pda account data: {:?}", pda_account_data); + solana_program::msg!("Creating EMPTY compressed account (PDA will remain intact)"); + + // Use the new empty compression function - key difference from regular compression + // Clone the account info to get mutability + let mut solana_account_mut = solana_account.clone(); + compress_empty_account_on_init_native::( + &mut solana_account_mut, + &mut pda_account_data, + &instruction_data.compressed_address, + &new_address_params, + instruction_data.output_state_tree_index, + cpi_accounts_struct, + &config.address_space, + instruction_data.proof, + )?; + + // Re-serialize the modified account data back to the on-chain account + // This ensures compression_info changes persist + { + let mut account_data = solana_account.data.borrow_mut(); + pda_account_data + .serialize(&mut &mut account_data[..]) + .map_err(|e| { + solana_program::msg!("pda account re-serialization error: {:?}", e); + LightSdkError::ProgramError(e.into()) + })?; + } + + Ok(()) +} + +#[derive(Clone, Debug, Default, BorshDeserialize, BorshSerialize)] +pub struct CreateEmptyCompressedPdaInstructionData { + pub proof: ValidityProof, + pub compressed_address: [u8; 32], + pub address_tree_info: PackedAddressTreeInfo, + pub output_state_tree_index: u8, +} diff --git a/sdk-tests/native-compressible/src/create_pda.rs b/sdk-tests/native-compressible/src/create_pda.rs new file mode 100644 index 0000000000..a2a6d9274c --- /dev/null +++ b/sdk-tests/native-compressible/src/create_pda.rs @@ -0,0 +1,94 @@ +use borsh::{BorshDeserialize, BorshSerialize}; +use light_sdk::{ + account::LightAccount, + cpi::{CpiAccountsConfig, CpiAccountsSmall, CpiInputs}, + error::LightSdkError, + instruction::{PackedAddressTreeInfo, ValidityProof}, + light_hasher::hash_to_field_size::hashv_to_bn254_field_size_be_const_array, +}; +use solana_program::{account_info::AccountInfo, msg}; + +use crate::{MyPdaAccount, ARRAY_LEN}; + +/// TODO: write test program with A8JgviaEAByMVLBhcebpDQ7NMuZpqBTBigC1b83imEsd (inconvenient program id) +/// CU usage: +/// - sdk pre system program cpi 10,942 CU +/// - total with V2 tree: 45,758 CU +pub fn create_pda( + accounts: &[AccountInfo], + instruction_data: &[u8], +) -> Result<(), LightSdkError> { + msg!("pre instruction_data"); + let mut instruction_data = instruction_data; + let instruction_data = CreatePdaInstructionData::deserialize(&mut instruction_data) + .map_err(|_| LightSdkError::Borsh)?; + msg!("pre config"); + let config = CpiAccountsConfig::new(crate::LIGHT_CPI_SIGNER); + let cpi_accounts = CpiAccountsSmall::new_with_config(&accounts[0], &accounts[1..], config); + + let address_tree_info = instruction_data.address_tree_info; + let (address, address_seed) = if BATCHED { + let address_seed = hashv_to_bn254_field_size_be_const_array::<3>(&[ + b"compressed", + instruction_data.data.as_slice(), + ]) + .unwrap(); + // to_bytes will go away as soon as we have a light_sdk::address::v2::derive_address + let address_tree_pubkey = address_tree_info + .get_tree_pubkey_small(&cpi_accounts)? + .to_bytes(); + let address = light_compressed_account::address::derive_address( + &address_seed, + &address_tree_pubkey, + &crate::ID.to_bytes(), + ); + (address, address_seed) + } else { + light_sdk::address::v1::derive_address( + &[b"compressed", instruction_data.data.as_slice()], + &address_tree_info.get_tree_pubkey_small(&cpi_accounts)?, + &crate::ID, + ) + }; + let new_address_params = address_tree_info.into_new_address_params_packed(address_seed); + let mut my_compressed_account = LightAccount::<'_, MyPdaAccount>::new_init( + &crate::ID, + Some(address), + instruction_data.output_merkle_tree_index, + ); + + my_compressed_account.data = instruction_data.data; + + let cpi_inputs = CpiInputs::new_with_address( + instruction_data.proof, + vec![my_compressed_account.to_account_info()?], + vec![new_address_params], + ); + cpi_inputs.invoke_light_system_program_small(cpi_accounts)?; + Ok(()) +} + +#[derive(Clone, Debug, LightHasher, LightDiscriminator, BorshDeserialize, BorshSerialize)] +pub struct MyCompressedAccount { + #[hash] + pub data: [u8; ARRAY_LEN], +} + +impl Default for MyCompressedAccount { + fn default() -> Self { + Self { + data: [0u8; ARRAY_LEN], + } + } +} + +#[derive(Clone, Debug, BorshDeserialize, BorshSerialize)] +#[repr(C)] +pub struct CreatePdaInstructionData { + pub proof: ValidityProof, + pub address_tree_info: PackedAddressTreeInfo, + pub output_merkle_tree_index: u8, + pub data: [u8; ARRAY_LEN], + pub system_accounts_offset: u8, + pub tree_accounts_offset: u8, +} diff --git a/sdk-tests/native-compressible/src/decompress_dynamic_pda.rs b/sdk-tests/native-compressible/src/decompress_dynamic_pda.rs new file mode 100644 index 0000000000..bd812eea10 --- /dev/null +++ b/sdk-tests/native-compressible/src/decompress_dynamic_pda.rs @@ -0,0 +1,181 @@ +use borsh::{BorshDeserialize, BorshSerialize}; +use light_sdk::{ + account::sha::LightAccount, + compressible::{prepare_accounts_for_decompress_idempotent, CompressibleConfig}, + cpi::{CpiAccountsSmall, CpiInputs}, + error::LightSdkError, + instruction::{account_meta::CompressedAccountMeta, ValidityProof}, +}; +use solana_program::{account_info::AccountInfo, msg}; + +use crate::MyPdaAccount; + +#[derive(Clone, Debug, BorshDeserialize, BorshSerialize)] +pub struct CompressedAccountData { + pub meta: CompressedAccountMeta, + /// Program-specific account variant enum + pub data: T, + /// PDA seeds (without bump) used to derive the PDA address + pub seeds: Vec>, +} + +#[derive(Clone, Debug, Default, BorshDeserialize, BorshSerialize)] +pub struct DecompressMultipleInstructionData { + pub proof: ValidityProof, + pub compressed_accounts: Vec>, + pub bumps: Vec, + pub system_accounts_offset: u8, +} +/// Example: Decompresses multiple compressed accounts into PDAs in a single transaction. +pub fn decompress_multiple_dynamic_pdas( + accounts: &[AccountInfo], + instruction_data: &[u8], +) -> Result<(), LightSdkError> { + let mut instruction_data = instruction_data; + let instruction_data = DecompressMultipleInstructionData::deserialize(&mut instruction_data) + .map_err(|e| { + solana_program::msg!( + "Failed to deserialize DecompressMultipleInstructionData: {:?}", + e + ); + LightSdkError::Borsh + })?; + + msg!("decompress_multiple_dynamic_pdas accounts: {:?}", accounts); + + // Account structure from CompressibleInstruction: + // [0] fee_payer (signer) + // [1] rent_payer (signer) + // [2] system_program + // [3..3+system_accounts_offset] PDA accounts + // [3+system_accounts_offset..] Light Protocol system accounts + + let fee_payer = &accounts[0]; + let rent_payer = &accounts[1]; + let config_account = &accounts[2]; + let config = CompressibleConfig::load_checked(config_account, &crate::ID)?; + + // PDA accounts start at index 3 and go for system_accounts_offset accounts + let pda_accounts_start = 3; + let pda_accounts_end = pda_accounts_start + instruction_data.system_accounts_offset as usize; + msg!("pda_accounts_start: {:?}", pda_accounts_start); + msg!("pda_accounts_end: {:?}", pda_accounts_end); + let solana_accounts = &accounts[pda_accounts_start..pda_accounts_end]; + msg!("solana_accounts: {:?}", solana_accounts); + + // Light Protocol system accounts start after PDA accounts + let system_accounts_start = pda_accounts_end; + let cpi_accounts = CpiAccountsSmall::new( + fee_payer, + &accounts[system_accounts_start..], + crate::LIGHT_CPI_SIGNER, + ); + + // Validate we have matching number of PDAs, compressed accounts, and bumps + if solana_accounts.len() != instruction_data.compressed_accounts.len() + || solana_accounts.len() != instruction_data.bumps.len() + { + return Err(LightSdkError::ConstraintViolation); + } + + // First pass: validate PDAs and collect data + let mut compressed_accounts = Vec::new(); + let mut pda_account_refs = Vec::new(); + let stored_bumps = instruction_data.bumps.clone(); // Store bumps to avoid borrowing issues + + for (i, compressed_account_data) in instruction_data.compressed_accounts.iter().enumerate() { + let compressed_account = LightAccount::<'_, MyPdaAccount>::new_mut( + &crate::ID, + &compressed_account_data.meta, + compressed_account_data.data.clone(), + )?; + + let bump = stored_bumps[i]; + + // Derive PDA for verification using the seeds from instruction data + let seeds_refs: Vec<&[u8]> = compressed_account_data + .seeds + .iter() + .map(|s| s.as_slice()) + .collect(); + let (derived_pda, expected_bump) = + solana_program::pubkey::Pubkey::find_program_address(&seeds_refs, &crate::ID); + + // Verify the PDA matches + if derived_pda != *solana_accounts[i].key { + msg!( + "derived_pda: {:?} does not match passed pda: {:?}", + derived_pda, + solana_accounts[i].key + ); + msg!("seeds used: {:?}", compressed_account_data.seeds); + return Err(LightSdkError::ConstraintViolation); + } + + // Verify the provided bump matches the expected bump + if bump != expected_bump { + msg!( + "provided bump: {:?}, expected bump: {:?}", + bump, + expected_bump + ); + return Err(LightSdkError::ConstraintViolation); + } + + compressed_accounts.push(compressed_account); + pda_account_refs.push(&solana_accounts[i]); + } + + // Second pass: build signer seeds with stable references using seeds from instruction data + let mut all_signer_seeds_storage = Vec::new(); + for (i, compressed_account_data) in instruction_data.compressed_accounts.iter().enumerate() { + // Use seeds from instruction data and append bump + let mut seeds_with_bump = compressed_account_data.seeds.clone(); + seeds_with_bump.push(vec![stored_bumps[i]]); + all_signer_seeds_storage.push(seeds_with_bump); + } + + // Convert to the format needed by the SDK + let signer_seeds_refs: Vec> = all_signer_seeds_storage + .iter() + .map(|seeds| seeds.iter().map(|s| s.as_slice()).collect()) + .collect(); + let signer_seeds_slices: Vec<&[&[u8]]> = signer_seeds_refs + .iter() + .map(|seeds| seeds.as_slice()) + .collect(); + + // For native-compressible, we'll use a hardcoded address space that matches the test setup + // This should match the address space used in tests + let address_space = config.address_space[0]; + + // Use prepare_accounts_for_decompress_idempotent directly and handle CPI manually + let compressed_infos = prepare_accounts_for_decompress_idempotent::( + &pda_account_refs, + compressed_accounts, + &signer_seeds_slices, + &cpi_accounts, + rent_payer, + address_space, + )?; + + if !compressed_infos.is_empty() { + let cpi_inputs = CpiInputs::new(instruction_data.proof, compressed_infos); + cpi_inputs.invoke_light_system_program_small(cpi_accounts)?; + } + + Ok(()) +} + +#[derive(Clone, Debug, Default, BorshDeserialize, BorshSerialize)] +pub struct DecompressToPdaInstructionData { + pub proof: ValidityProof, + pub compressed_account: MyCompressedAccount, + pub system_accounts_offset: u8, +} + +#[derive(Clone, Debug, Default, BorshDeserialize, BorshSerialize)] +pub struct MyCompressedAccount { + pub meta: CompressedAccountMeta, + pub data: MyPdaAccount, +} diff --git a/sdk-tests/native-compressible/src/lib.rs b/sdk-tests/native-compressible/src/lib.rs new file mode 100644 index 0000000000..2d32653627 --- /dev/null +++ b/sdk-tests/native-compressible/src/lib.rs @@ -0,0 +1,302 @@ +use borsh::{BorshDeserialize, BorshSerialize}; +use light_macros::pubkey; +use light_sdk::{ + account::Size, + compressible::{CompressionInfo, HasCompressionInfo}, + cpi::CpiSigner, + derive_light_cpi_signer, + error::LightSdkError, + sha::LightHasher, + LightDiscriminator, +}; +use solana_program::{ + account_info::AccountInfo, entrypoint, program_error::ProgramError, pubkey::Pubkey, +}; + +pub mod compress_dynamic_pda; +pub mod compress_empty_compressed_pda; +pub mod create_config; +pub mod create_dynamic_pda; +pub mod create_empty_compressed_pda; +pub mod create_pda; +pub mod decompress_dynamic_pda; +pub mod update_config; +pub mod update_pda; + +pub const ID: Pubkey = pubkey!("FNt7byTHev1k5x2cXZLBr8TdWiC3zoP5vcnZR4P682Uy"); +pub const LIGHT_CPI_SIGNER: CpiSigner = + derive_light_cpi_signer!("FNt7byTHev1k5x2cXZLBr8TdWiC3zoP5vcnZR4P682Uy"); + +entrypoint!(process_instruction); + +#[repr(u8)] +pub enum InstructionType { + CreatePdaBorsh = 0, + UpdatePdaBorsh = 1, + CompressDynamicPda = 2, + CreateDynamicPda = 3, + InitializeCompressionConfig = 4, + UpdateCompressionConfig = 5, + DecompressAccountsIdempotent = 6, + CreateEmptyCompressedPda = 7, + CompressEmptyCompressedPda = 8, +} + +impl TryFrom for InstructionType { + type Error = LightSdkError; + + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(InstructionType::CreatePdaBorsh), + 1 => Ok(InstructionType::UpdatePdaBorsh), + 2 => Ok(InstructionType::CompressDynamicPda), + 3 => Ok(InstructionType::CreateDynamicPda), + 4 => Ok(InstructionType::InitializeCompressionConfig), + 5 => Ok(InstructionType::UpdateCompressionConfig), + 6 => Ok(InstructionType::DecompressAccountsIdempotent), + 7 => Ok(InstructionType::CreateEmptyCompressedPda), + 8 => Ok(InstructionType::CompressEmptyCompressedPda), + + _ => panic!("Invalid instruction discriminator."), + } + } +} + +pub fn process_instruction( + _program_id: &Pubkey, + accounts: &[AccountInfo], + instruction_data: &[u8], +) -> Result<(), ProgramError> { + let discriminator = InstructionType::try_from(instruction_data[0]) + .map_err(|_| ProgramError::InvalidInstructionData)?; + + match discriminator { + InstructionType::CreatePdaBorsh => { + create_pda::create_pda::(accounts, &instruction_data[1..]) + } + InstructionType::UpdatePdaBorsh => { + update_pda::update_pda::(accounts, &instruction_data[1..]) + } + InstructionType::CompressDynamicPda => { + compress_dynamic_pda::compress_dynamic_pda(accounts, &instruction_data[1..]) + } + InstructionType::CreateDynamicPda => { + create_dynamic_pda::create_dynamic_pda(accounts, &instruction_data[1..]) + } + + InstructionType::InitializeCompressionConfig => { + create_config::process_initialize_compression_config_checked( + accounts, + &instruction_data[1..], + ) + } + InstructionType::UpdateCompressionConfig => { + update_config::process_update_config(accounts, &instruction_data[1..]) + } + InstructionType::DecompressAccountsIdempotent => { + decompress_dynamic_pda::decompress_multiple_dynamic_pdas( + accounts, + &instruction_data[1..], + ) + } + InstructionType::CreateEmptyCompressedPda => { + create_empty_compressed_pda::create_empty_compressed_pda( + accounts, + &instruction_data[1..], + ) + } + InstructionType::CompressEmptyCompressedPda => { + compress_empty_compressed_pda::compress_empty_compressed_pda( + accounts, + &instruction_data[1..], + ) + } + }?; + Ok(()) +} + +#[derive( + Clone, Debug, Default, LightHasher, LightDiscriminator, BorshDeserialize, BorshSerialize, +)] +pub struct MyPdaAccount { + #[skip] + pub compression_info: Option, + pub data: [u8; 31], +} + +// Implement the HasCompressionInfo trait +impl HasCompressionInfo for MyPdaAccount { + 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 MyPdaAccount { + fn size(&self) -> usize { + // compression_info is #[skip], so not serialized + Self::LIGHT_DISCRIMINATOR_SLICE.len() + 31 + 1 + 9 // discriminator + data: [u8; 31] + compression_info: Option + } +} + +#[cfg(test)] +mod test_sha_hasher { + use light_hasher::{to_byte_array::ToByteArray, DataHasher, Sha256}; + use light_sdk::sha::LightHasher; + + use super::*; + + #[derive( + Clone, Debug, Default, LightDiscriminator, BorshDeserialize, BorshSerialize, LightHasher, + )] + pub struct TestShaAccount { + #[skip] + pub compression_info: Option, + pub data: [u8; 31], + } + + #[test] + fn test_sha256_vs_poseidon_hashing() { + let account = MyPdaAccount { + compression_info: None, + data: [42u8; 31], + }; + + // Test Poseidon hashing (default) + let poseidon_hash = account.hash::().unwrap(); + + // Test SHA256 hashing + let sha256_hash = account.hash::().unwrap(); + + // They should be different + assert_ne!(poseidon_hash, sha256_hash); + + // Both should have first byte as 0 (field size truncated) or be different due to different hashing + println!("Poseidon hash: {:?}", poseidon_hash); + println!("SHA256 hash: {:?}", sha256_hash); + } + + #[test] + fn test_sha_hasher_derive_macro() { + let sha_account = TestShaAccount { + compression_info: None, + data: [99u8; 31], + }; + + // Test the to_byte_array implementation (which should use SHA256 internally) + let sha_byte_array = sha_account.to_byte_array().unwrap(); + + // Test DataHasher implementation with SHA256 + let sha_data_hash = sha_account.hash::().unwrap(); + + // Both should have first byte truncated to 0 for field size + assert_eq!(sha_byte_array[0], 0); + assert_eq!(sha_data_hash[0], 0); + + assert_eq!(sha_byte_array.len(), 32); + assert_eq!(sha_data_hash.len(), 32); + + println!("SHA account to_byte_array: {:?}", sha_byte_array); + println!("SHA account DataHasher: {:?}", sha_data_hash); + + // Test that this is different from Poseidon hashing + let poseidon_hash = sha_account.hash::().unwrap(); + // Poseidon hash should not have first byte truncated (ID=0) + assert_ne!(sha_byte_array, poseidon_hash); + assert_ne!(sha_data_hash, poseidon_hash); + + println!("Same account with Poseidon: {:?}", poseidon_hash); + } + + #[test] + fn test_large_struct_with_sha_hasher() { + // This demonstrates that SHA256 can handle arbitrary-sized data + // while Poseidon is limited to 12 fields in the current implementation + + use light_hasher::{Hasher, Sha256}; + + // Create a large struct that would exceed Poseidon's field limits + #[derive(Clone, Debug, Default, BorshDeserialize, BorshSerialize)] + struct LargeStruct { + pub field1: u64, + pub field2: u64, + pub field3: u64, + pub field4: u64, + pub field5: u64, + pub field6: u64, + pub field7: u64, + pub field8: u64, + pub field9: u64, + pub field10: u64, + pub field11: u64, + pub field12: u64, + pub field13: u64, + // Pubkeys that would require #[hash] attribute with Poseidon + pub owner: solana_program::pubkey::Pubkey, + pub authority: solana_program::pubkey::Pubkey, + } + + let large_account = LargeStruct { + field1: 1, + field2: 2, + field3: 3, + field4: 4, + field5: 5, + field6: 6, + field7: 7, + field8: 8, + field9: 9, + field10: 10, + field11: 11, + field12: 12, + field13: 13, + owner: solana_program::pubkey::Pubkey::new_unique(), + authority: solana_program::pubkey::Pubkey::new_unique(), + }; + + // Test that SHA256 can hash large data by serializing the whole struct + let serialized = large_account.try_to_vec().unwrap(); + println!("Serialized struct size: {} bytes", serialized.len()); + + // SHA256 can hash arbitrary amounts of data + let sha_hash = Sha256::hash(&serialized).unwrap(); + println!("SHA256 hash: {:?}", sha_hash); + + // Verify the hash is truncated properly (first byte should be 0 for field size) + // Note: Since SHA256::ID = 1 (not 0), the system program expects truncation + let mut expected_hash = sha_hash; + expected_hash[0] = 0; + + assert_eq!(sha_hash.len(), 32); + // For demonstration - in real usage, the truncation would be applied by the system + println!("SHA256 hash truncated: {:?}", expected_hash); + + // Show that this would be different from a smaller struct + let small_struct = MyPdaAccount { + compression_info: None, + data: [42u8; 31], + }; + + let small_serialized = small_struct.try_to_vec().unwrap(); + let small_hash = Sha256::hash(&small_serialized).unwrap(); + + // Different data should produce different hashes + assert_ne!(sha_hash, small_hash); + println!("Different struct produces different hash: {:?}", small_hash); + } +} diff --git a/sdk-tests/native-compressible/src/update_config.rs b/sdk-tests/native-compressible/src/update_config.rs new file mode 100644 index 0000000000..37b4caed13 --- /dev/null +++ b/sdk-tests/native-compressible/src/update_config.rs @@ -0,0 +1,37 @@ +use borsh::{BorshDeserialize, BorshSerialize}; +use light_sdk::{compressible::process_update_compression_config, error::LightSdkError}; +use solana_program::{account_info::AccountInfo, pubkey::Pubkey}; + +/// Updates an existing compressible config +pub fn process_update_config( + accounts: &[AccountInfo], + instruction_data: &[u8], +) -> Result<(), LightSdkError> { + let mut instruction_data = instruction_data; + let instruction_data = UpdateConfigInstructionData::deserialize(&mut instruction_data) + .map_err(|_| LightSdkError::Borsh)?; + + // Get accounts + let config_account = &accounts[0]; + let authority = &accounts[1]; + + process_update_compression_config( + config_account, + authority, + instruction_data.new_update_authority.as_ref(), + instruction_data.new_rent_recipient.as_ref(), + instruction_data.new_address_space, + instruction_data.new_compression_delay, + &crate::ID, + )?; + + Ok(()) +} + +#[derive(Clone, Debug, BorshDeserialize, BorshSerialize)] +pub struct UpdateConfigInstructionData { + pub new_update_authority: Option, + pub new_rent_recipient: Option, + pub new_address_space: Option>, + pub new_compression_delay: Option, +} diff --git a/sdk-tests/native-compressible/src/update_pda.rs b/sdk-tests/native-compressible/src/update_pda.rs new file mode 100644 index 0000000000..c7d7cd61be --- /dev/null +++ b/sdk-tests/native-compressible/src/update_pda.rs @@ -0,0 +1,81 @@ +use borsh::{BorshDeserialize, BorshSerialize}; +use light_sdk::{ + account::LightAccount, + cpi::{CpiAccountsConfig, CpiAccountsSmall, CpiInputs}, + error::LightSdkError, + instruction::{account_meta::CompressedAccountMeta, ValidityProof}, +}; +use solana_program::{account_info::AccountInfo, log::sol_log_compute_units}; + +use crate::MyPdaAccount; + +/// CU usage: +/// - sdk pre system program 9,183k CU +/// - total with V2 tree: 50,194 CU (proof by index) +/// - 51,609 +pub fn update_pda( + accounts: &[AccountInfo], + instruction_data: &[u8], +) -> Result<(), LightSdkError> { + sol_log_compute_units(); + let mut instruction_data = instruction_data; + let instruction_data = UpdatePdaInstructionData::deserialize(&mut instruction_data) + .map_err(|_| LightSdkError::Borsh)?; + + let mut my_compressed_account = LightAccount::<'_, MyPdaAccount>::new_mut( + &crate::ID, + &instruction_data.my_compressed_account.meta, + MyPdaAccount { + compression_info: None, + data: instruction_data.my_compressed_account.data, + }, + )?; + sol_log_compute_units(); + + my_compressed_account.data = instruction_data.new_data; + + let config = CpiAccountsConfig::new(crate::LIGHT_CPI_SIGNER); + sol_log_compute_units(); + let cpi_accounts = CpiAccountsSmall::new_with_config(&accounts[0], &accounts[1..], config); + sol_log_compute_units(); + let cpi_inputs = CpiInputs::new( + instruction_data.proof, + vec![my_compressed_account.to_account_info()?], + ); + sol_log_compute_units(); + cpi_inputs.invoke_light_system_program_small(cpi_accounts)?; + Ok(()) +} + +#[derive(Clone, Debug, BorshDeserialize, BorshSerialize)] +pub struct UpdatePdaInstructionData { + pub proof: ValidityProof, + pub my_compressed_account: UpdateMyCompressedAccount, + pub new_data: [u8; ARRAY_LEN], + pub system_accounts_offset: u8, +} +impl Default for UpdatePdaInstructionData { + fn default() -> Self { + Self { + new_data: [0u8; ARRAY_LEN], + my_compressed_account: UpdateMyCompressedAccount::default(), + system_accounts_offset: 0, + proof: ValidityProof::default(), + } + } +} + +#[derive(Clone, Debug, BorshDeserialize, BorshSerialize)] +pub struct UpdateMyCompressedAccount { + pub meta: CompressedAccountMeta, + pub data: [u8; ARRAY_LEN], +} + +impl Default for UpdateMyCompressedAccount { + fn default() -> Self { + Self { + meta: CompressedAccountMeta::default(), + data: [0u8; ARRAY_LEN], + } + } +} diff --git a/sdk-tests/native-compressible/tests/test_compressible_flow.rs b/sdk-tests/native-compressible/tests/test_compressible_flow.rs new file mode 100644 index 0000000000..f085b383fa --- /dev/null +++ b/sdk-tests/native-compressible/tests/test_compressible_flow.rs @@ -0,0 +1,571 @@ +#![cfg(feature = "test-sbf")] + +use core::panic; + +use borsh::{BorshDeserialize, BorshSerialize}; +use light_compressed_account::address::derive_address; +use light_compressible_client::CompressibleInstruction; +use light_program_test::{ + initialize_compression_config, + program_test::{LightProgramTest, TestRpc}, + setup_mock_program_data, AddressWithTree, Indexer, ProgramTestConfig, Rpc, +}; +use light_sdk::{ + compressible::CompressibleConfig, + instruction::{PackedAccounts, SystemAccountMetaConfig}, +}; +use native_compressible::{ + create_dynamic_pda::CreateDynamicPdaInstructionData, + create_empty_compressed_pda::CreateEmptyCompressedPdaInstructionData, InstructionType, + MyPdaAccount, +}; +use solana_sdk::{ + instruction::{AccountMeta, Instruction}, + pubkey::Pubkey, + signature::{Keypair, Signer}, +}; + +// Test constants +const RENT_RECIPIENT: Pubkey = + light_macros::pubkey!("CLEuMG7pzJX9xAuKCFzBP154uiG1GaNo4Fq7x6KAcAfG"); +const COMPRESSION_DELAY: u64 = 200; + +#[tokio::test] +async fn test_complete_compressible_flow() { + let config = ProgramTestConfig::new_v2( + true, + Some(vec![("native_compressible", native_compressible::ID)]), + ); + let mut rpc = LightProgramTest::new(config).await.unwrap(); + let payer = rpc.get_payer().insecure_clone(); + + let _config_pda = CompressibleConfig::derive_default_pda(&native_compressible::ID).0; + let _program_data_pda = setup_mock_program_data(&mut rpc, &payer, &native_compressible::ID); + + // Get address tree for the address space + let address_tree = rpc.get_address_tree_v2().queue; + + let result = initialize_compression_config( + &mut rpc, + &payer, + &native_compressible::ID, + &payer, + 200, + RENT_RECIPIENT, + vec![address_tree], + &[InstructionType::InitializeCompressionConfig as u8], + None, + ) + .await; + assert!(result.is_ok(), "Initialize config should succeed"); + + // 1. Create and compress account on init + let test_data = [1u8; 31]; + + let seeds: &[&[u8]] = &[b"dynamic_pda"]; + let (pda_pubkey, _bump) = Pubkey::find_program_address(seeds, &native_compressible::ID); + + let address_tree_pubkey = rpc.get_address_tree_v2().queue; + + let compressed_address = derive_address( + &pda_pubkey.to_bytes(), + &address_tree_pubkey.to_bytes(), + &native_compressible::ID.to_bytes(), + ); + + let pda_pubkey = create_and_compress_account(&mut rpc, &payer, test_data).await; + + // get account + let account = rpc.get_account(pda_pubkey).await.unwrap(); + assert!(account.is_some()); + assert_eq!(account.unwrap().lamports, 0); + + // get compressed account + let compressed_account = rpc.get_compressed_account(compressed_address, None).await; + assert!(compressed_account.is_ok()); + + // 2. Wait for compression delay to pass + rpc.warp_to_slot(COMPRESSION_DELAY + 1).unwrap(); + + // 3. Decompress the account + decompress_account(&mut rpc, &payer, &pda_pubkey, test_data).await; + + // get account + let account = rpc.get_account(pda_pubkey).await.unwrap(); + assert!(account.is_some()); + assert!(account.unwrap().lamports > 0); + // assert_eq!(account.unwrap().data.len(), 31); + + // 4. Verify PDA is decompressed + verify_decompressed_account(&mut rpc, &pda_pubkey, &compressed_address, test_data).await; + + // 5. Wait for compression delay to pass again + rpc.warp_to_slot(COMPRESSION_DELAY * 2 + 1).unwrap(); + + // 6. Compress the account again + compress_existing_account(&mut rpc, &payer, &pda_pubkey).await; + + // 7. Verify account is compressed again + verify_compressed_account(&mut rpc, &pda_pubkey).await; +} + +async fn create_and_compress_account( + rpc: &mut LightProgramTest, + payer: &Keypair, + _test_data: [u8; 31], +) -> Pubkey { + // Derive PDA + let seeds: &[&[u8]] = &[b"dynamic_pda"]; + let (pda_pubkey, _bump) = Pubkey::find_program_address(seeds, &native_compressible::ID); + + // Get address tree + let address_tree_pubkey = rpc.get_address_tree_v2().queue; + + // Derive compressed address + let compressed_address = derive_address( + &pda_pubkey.to_bytes(), + &address_tree_pubkey.to_bytes(), + &native_compressible::ID.to_bytes(), + ); + + // Get validity proof + let rpc_result = rpc + .get_validity_proof( + vec![], + vec![AddressWithTree { + address: compressed_address, + tree: address_tree_pubkey, + }], + None, + ) + .await + .unwrap() + .value; + + // Setup remaining accounts + let mut remaining_accounts = PackedAccounts::default(); + let system_config = SystemAccountMetaConfig::new(native_compressible::ID); + let _ = remaining_accounts.add_system_accounts_small(system_config); + + // Pack tree infos + let packed_tree_infos = rpc_result.pack_tree_infos(&mut remaining_accounts); + let address_tree_info = packed_tree_infos.address_trees[0]; + + // Get output state tree index + 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(); + + // Create instruction data for create_dynamic_pda + let instruction_data = CreateDynamicPdaInstructionData { + proof: rpc_result.proof, + compressed_address, + address_tree_info, + output_state_tree_index, + }; + + // Build instruction + let instruction = Instruction { + program_id: native_compressible::ID, + accounts: [ + vec![ + AccountMeta::new(payer.pubkey(), true), // fee_payer + AccountMeta::new(pda_pubkey, false), // solana_account + AccountMeta::new(RENT_RECIPIENT, false), // rent_recipient + AccountMeta::new_readonly( + CompressibleConfig::derive_default_pda(&native_compressible::ID).0, + false, + ), // config + AccountMeta::new_readonly(solana_sdk::system_program::ID, false), // system_program + ], + system_accounts, + ] + .concat(), + data: [ + &[InstructionType::CreateDynamicPda as u8][..], + &instruction_data.try_to_vec().unwrap()[..], + ] + .concat(), + }; + + let result = rpc + .create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer]) + .await; + + assert!( + result.is_ok(), + "Create and compress failed error: {:?}", + result.err() + ); + + pda_pubkey +} + +async fn decompress_account( + rpc: &mut LightProgramTest, + payer: &Keypair, + pda_pubkey: &Pubkey, + test_data: [u8; 31], +) { + // Get the compressed address + let address_tree_pubkey = rpc.get_address_tree_v2().queue; + let compressed_address = derive_address( + &pda_pubkey.to_bytes(), + &address_tree_pubkey.to_bytes(), + &native_compressible::ID.to_bytes(), + ); + + // Try to get the compressed account from the indexer + let compressed_account_result = rpc.get_compressed_account(compressed_address, None).await; + + if compressed_account_result.is_err() { + panic!("Could not get compressed account"); + } + + let compressed_account = compressed_account_result.unwrap().value; + + // Create MyPdaAccount from the test data + let my_pda_account = MyPdaAccount { + compression_info: None, // Will be set during decompression + data: test_data, + }; + + // Get validity proof + let rpc_result = rpc + .get_validity_proof(vec![compressed_account.hash], vec![], None) + .await + .unwrap() + .value; + + let instruction = CompressibleInstruction::decompress_accounts_idempotent( + &native_compressible::ID, + &[InstructionType::DecompressAccountsIdempotent as u8], // Use sdk-test's DecompressAccountsIdempotent discriminator + &payer.pubkey(), + &payer.pubkey(), + &[*pda_pubkey], + &[( + compressed_account.clone(), + my_pda_account.clone(), // MyPdaAccount implements required trait + vec![b"dynamic_pda".to_vec()], // PDA seeds without bump + )], + &[Pubkey::find_program_address(&[b"dynamic_pda"], &native_compressible::ID).1], // bump seed, must match the seeds used in create_dynamic_pda + rpc_result, + compressed_account.tree_info, + ) + .unwrap(); + + let result = rpc + .create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer]) + .await; + + assert!( + result.is_ok(), + "Decompress failed error: {:?}", + result.err() + ); +} + +async fn compress_existing_account( + rpc: &mut LightProgramTest, + payer: &Keypair, + pda_pubkey: &Pubkey, +) { + // Get the account data first + let account = rpc.get_account(*pda_pubkey).await.unwrap(); + if account.is_none() { + println!("PDA account not found, cannot compress"); + return; + } + + let account = account.unwrap(); + assert!(account.lamports > 0, "PDA account should have lamports"); + + // Get the compressed address + let address_tree_pubkey = rpc.get_address_tree_v2().queue; + let compressed_address = derive_address( + &pda_pubkey.to_bytes(), + &address_tree_pubkey.to_bytes(), + &native_compressible::ID.to_bytes(), + ); + + // Try to get the existing compressed account + let compressed_account_result = rpc.get_compressed_account(compressed_address, None).await; + + if compressed_account_result.is_err() { + panic!("Could not get compressed account"); + } + + let compressed_account = compressed_account_result.unwrap().value; + + // Get validity proof + let rpc_result = rpc + .get_validity_proof(vec![compressed_account.hash], vec![], None) + .await + .unwrap() + .value; + + let instruction = CompressibleInstruction::compress_account( + &native_compressible::ID, + &[InstructionType::CompressDynamicPda as u8], // Use sdk-test's CompressFromPda discriminator + &payer.pubkey(), + pda_pubkey, + &RENT_RECIPIENT, + &compressed_account, + rpc_result, + compressed_account.tree_info, + ) + .unwrap(); + + let result = rpc + .create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer]) + .await; + + assert!(result.is_ok(), "Compress failed error: {:?}", result.err()); +} + +async fn verify_decompressed_account( + rpc: &mut LightProgramTest, + pda_pubkey: &Pubkey, + compressed_address: &[u8; 32], + expected_data: [u8; 31], +) { + let account = rpc.get_account(*pda_pubkey).await.unwrap(); + + assert!( + account.is_some(), + "PDA account not found after decompression" + ); + + let account = account.unwrap(); + assert!( + account.data.len() > 8, + "PDA account not properly decompressed (empty data)" + ); + + // Try to deserialize the account data (skip the 8-byte discriminator) + let solana_account = MyPdaAccount::deserialize(&mut &account.data[8..]) + .expect("Could not deserialize PDA account data"); + assert!(solana_account.compression_info.is_some()); + assert_eq!(solana_account.data, expected_data); // data matches the expected data + assert!( + !solana_account + .compression_info + .as_ref() + .unwrap() + .is_compressed(), + "PDA account should not be compressed" + ); + // slot matches the slot of the last write + assert_eq!( + &solana_account.compression_info.unwrap().last_written_slot(), + &rpc.get_slot().await.unwrap() + ); + + let compressed_account = rpc.get_compressed_account(*compressed_address, None).await; + assert!(compressed_account.is_ok()); + let compressed_account = compressed_account.unwrap().value; + // After decompression, the compressed account data should be cleared + // This is a known behavior - commenting out for now to see if test passes + + assert!( + compressed_account.data.unwrap().data.as_slice().is_empty(), + "Compressed account data must be empty" + ); +} + +async fn verify_compressed_account(rpc: &mut LightProgramTest, pda_pubkey: &Pubkey) { + let account = rpc.get_account(*pda_pubkey).await.unwrap(); + + if let Some(account) = account { + assert_eq!( + account.lamports, 0, + "PDA account should have 0 lamports when compressed" + ); + assert!( + account.data.is_empty(), + "PDA account should have empty data when compressed" + ); + } else { + panic!("PDA account not found"); + } +} + +#[tokio::test] +async fn test_create_empty_compressed_account() { + let config = ProgramTestConfig::new_v2( + true, + Some(vec![("native_compressible", native_compressible::ID)]), + ); + let mut rpc = LightProgramTest::new(config).await.unwrap(); + let payer = rpc.get_payer().insecure_clone(); + + let _config_pda = CompressibleConfig::derive_default_pda(&native_compressible::ID).0; + let _program_data_pda = setup_mock_program_data(&mut rpc, &payer, &native_compressible::ID); + + // Get address tree for the address space + let address_tree = rpc.get_address_tree_v2().queue; + + let result = initialize_compression_config( + &mut rpc, + &payer, + &native_compressible::ID, + &payer, + 200, + RENT_RECIPIENT, + vec![address_tree], + &[InstructionType::InitializeCompressionConfig as u8], + None, + ) + .await; + assert!(result.is_ok(), "Initialize config should succeed"); + + // Test empty compression functionality + let test_data = [1u8; 31]; // Match what the PDA actually creates + + // 1. Create PDA and create empty compressed account (PDA should remain intact) + let pda_pubkey = create_empty_compressed_account(&mut rpc, &payer, test_data).await; + + // 2. Verify PDA still exists with data + let account = rpc.get_account(pda_pubkey).await.unwrap(); + assert!( + account.is_some(), + "PDA should still exist after empty compression" + ); + let account = account.unwrap(); + assert!(account.lamports > 0, "PDA should still have lamports"); + assert!(!account.data.is_empty(), "PDA should still have data"); + + // Try to deserialize the PDA data to verify it matches + let pda_data = MyPdaAccount::deserialize(&mut &account.data[8..]) + .expect("Could not deserialize PDA account data"); + assert_eq!(pda_data.data, test_data); + // Note: compression_info is marked with #[skip] so it will be None when deserialized + + // 3. Verify empty compressed account was created + let address_tree_pubkey = rpc.get_address_tree_v2().queue; + let compressed_address = derive_address( + &pda_pubkey.to_bytes(), + &address_tree_pubkey.to_bytes(), + &native_compressible::ID.to_bytes(), + ); + + let compressed_account = rpc.get_compressed_account(compressed_address, None).await; + assert!( + compressed_account.is_ok(), + "Compressed account should exist" + ); + let compressed_account = compressed_account.unwrap().value; + + // Key assertion: the compressed account should be empty + assert!( + compressed_account.data.is_none() || compressed_account.data.unwrap().data.is_empty(), + "Compressed account should be empty" + ); + + println!("✅ Empty compressed account test passed!"); + println!(" - PDA remains intact with data: {:?}", test_data); + println!( + " - Empty compressed account created at address: {:?}", + compressed_address + ); + println!(" - No account closure occurred"); + println!(" - Empty compressed account functionality working as intended"); + + // Note: The full compression cycle (empty → regular) is not implemented in this test + // due to complexities with compression_info handling in the native implementation. + + // The core empty compression functionality is working correctly. +} + +async fn create_empty_compressed_account( + rpc: &mut LightProgramTest, + payer: &Keypair, + _test_data: [u8; 31], +) -> Pubkey { + // Derive PDA with different seeds than regular PDA + let seeds: &[&[u8]] = &[b"empty_compressed_pda"]; + let (pda_pubkey, _bump) = Pubkey::find_program_address(seeds, &native_compressible::ID); + + // Get address tree + let address_tree_pubkey = rpc.get_address_tree_v2().queue; + + // Derive compressed address + let compressed_address = derive_address( + &pda_pubkey.to_bytes(), + &address_tree_pubkey.to_bytes(), + &native_compressible::ID.to_bytes(), + ); + + // Get validity proof + let rpc_result = rpc + .get_validity_proof( + vec![], + vec![AddressWithTree { + address: compressed_address, + tree: address_tree_pubkey, + }], + None, + ) + .await + .unwrap() + .value; + + // Setup remaining accounts + let mut remaining_accounts = PackedAccounts::default(); + let system_config = SystemAccountMetaConfig::new(native_compressible::ID); + let _ = remaining_accounts.add_system_accounts_small(system_config); + + // Pack tree infos + let packed_tree_infos = rpc_result.pack_tree_infos(&mut remaining_accounts); + let address_tree_info = packed_tree_infos.address_trees[0]; + + // Get output state tree index + 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(); + + // Create instruction data for create_empty_compressed_pda + let instruction_data = CreateEmptyCompressedPdaInstructionData { + proof: rpc_result.proof, + compressed_address, + address_tree_info, + output_state_tree_index, + }; + + // Build instruction + let instruction = Instruction { + program_id: native_compressible::ID, + accounts: [ + vec![ + AccountMeta::new(payer.pubkey(), true), // fee_payer + AccountMeta::new(pda_pubkey, false), // solana_account + AccountMeta::new_readonly( + CompressibleConfig::derive_default_pda(&native_compressible::ID).0, + false, + ), // config + AccountMeta::new_readonly(solana_sdk::system_program::ID, false), // system_program + ], + system_accounts, + ] + .concat(), + data: [ + &[InstructionType::CreateEmptyCompressedPda as u8][..], + &instruction_data.try_to_vec().unwrap()[..], + ] + .concat(), + }; + + let result = rpc + .create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer]) + .await; + + assert!( + result.is_ok(), + "Create empty compressed account failed error: {:?}", + result.err() + ); + + pda_pubkey +} diff --git a/sdk-tests/native-compressible/tests/test_config.rs b/sdk-tests/native-compressible/tests/test_config.rs new file mode 100644 index 0000000000..bdc0be31e1 --- /dev/null +++ b/sdk-tests/native-compressible/tests/test_config.rs @@ -0,0 +1,160 @@ +#![cfg(feature = "test-sbf")] + +use borsh::BorshSerialize; +use light_macros::pubkey; +use light_program_test::{program_test::LightProgramTest, ProgramTestConfig, Rpc}; +use light_sdk::compressible::CompressibleConfig; +use native_compressible::create_config::CreateConfigInstructionData; +use solana_sdk::{ + bpf_loader_upgradeable, + instruction::{AccountMeta, Instruction}, + pubkey::Pubkey, + signature::{Keypair, Signer}, +}; + +pub const ADDRESS_SPACE: Pubkey = pubkey!("CLEuMG7pzJX9xAuKCFzBP154uiG1GaNo4Fq7x6KAcAfG"); +pub const RENT_RECIPIENT: Pubkey = pubkey!("CLEuMG7pzJX9xAuKCFzBP154uiG1GaNo4Fq7x6KAcAfG"); + +#[tokio::test] +async fn test_create_and_update_config() { + let config = ProgramTestConfig::new_v2( + true, + Some(vec![("native_compressible", native_compressible::ID)]), + ); + let mut rpc = LightProgramTest::new(config).await.unwrap(); + let payer = rpc.get_payer().insecure_clone(); + + // Derive config PDA + let (config_pda, _) = CompressibleConfig::derive_pda(&native_compressible::ID, 0); + + // Derive program data account + let (program_data_pda, _) = Pubkey::find_program_address( + &[native_compressible::ID.as_ref()], + &bpf_loader_upgradeable::ID, + ); + + // Test create config + let create_ix_data = CreateConfigInstructionData { + rent_recipient: RENT_RECIPIENT, + address_space: vec![ADDRESS_SPACE], // Can add more for multi-address-space support + compression_delay: 100, + }; + + let create_ix = Instruction { + program_id: native_compressible::ID, + accounts: vec![ + AccountMeta::new(payer.pubkey(), true), + AccountMeta::new(config_pda, false), + AccountMeta::new_readonly(payer.pubkey(), true), // update_authority (signer) + AccountMeta::new_readonly(program_data_pda, false), // program data account + AccountMeta::new_readonly(solana_sdk::system_program::ID, false), + ], + data: [&[5u8][..], &create_ix_data.try_to_vec().unwrap()[..]].concat(), + }; + + // Note: This will fail in the test environment because the program data account + // doesn't exist in the test validator. In a real deployment, this would work. + let result = rpc + .create_and_send_transaction(&[create_ix], &payer.pubkey(), &[&payer]) + .await; + + // We expect this to fail in test environment + assert!( + result.is_err(), + "Should fail without proper program data account" + ); +} + +#[tokio::test] +async fn test_config_validation() { + let config = ProgramTestConfig::new_v2( + true, + Some(vec![("native_compressible", native_compressible::ID)]), + ); + let mut rpc = LightProgramTest::new(config).await.unwrap(); + let payer = rpc.get_payer().insecure_clone(); + let non_authority = Keypair::new(); + + // Derive PDAs + let (config_pda, _) = CompressibleConfig::derive_default_pda(&native_compressible::ID); + let (program_data_pda, _) = Pubkey::find_program_address( + &[native_compressible::ID.as_ref()], + &bpf_loader_upgradeable::ID, + ); + + // Try to create config with non-authority (should fail) + let create_ix_data = CreateConfigInstructionData { + rent_recipient: RENT_RECIPIENT, + address_space: vec![ADDRESS_SPACE], + compression_delay: 100, + }; + + let create_ix = Instruction { + program_id: native_compressible::ID, + accounts: vec![ + AccountMeta::new(payer.pubkey(), true), + AccountMeta::new(config_pda, false), + AccountMeta::new_readonly(non_authority.pubkey(), true), // wrong authority (signer) + AccountMeta::new_readonly(program_data_pda, false), + AccountMeta::new_readonly(solana_sdk::system_program::ID, false), + ], + data: [&[5u8][..], &create_ix_data.try_to_vec().unwrap()[..]].concat(), + }; + + // Fund the non-authority account + rpc.airdrop_lamports(&non_authority.pubkey(), 1_000_000_000) + .await + .unwrap(); + + let result = rpc + .create_and_send_transaction(&[create_ix], &payer.pubkey(), &[&payer, &non_authority]) + .await; + + assert!(result.is_err(), "Should fail with wrong authority"); +} + +#[tokio::test] +async fn test_config_creation_requires_signer() { + let config = ProgramTestConfig::new_v2( + true, + Some(vec![("native_compressible", native_compressible::ID)]), + ); + let mut rpc = LightProgramTest::new(config).await.unwrap(); + let payer = rpc.get_payer().insecure_clone(); + let non_signer = Keypair::new(); + + // Derive PDAs + let (config_pda, _) = CompressibleConfig::derive_default_pda(&native_compressible::ID); + let (program_data_pda, _) = Pubkey::find_program_address( + &[native_compressible::ID.as_ref()], + &bpf_loader_upgradeable::ID, + ); + + // Try to create config with non-signer as update authority (should fail) + let create_ix_data = CreateConfigInstructionData { + rent_recipient: RENT_RECIPIENT, + address_space: vec![ADDRESS_SPACE], + compression_delay: 100, + }; + + let create_ix = Instruction { + program_id: native_compressible::ID, + accounts: vec![ + AccountMeta::new(payer.pubkey(), true), + AccountMeta::new(config_pda, false), + AccountMeta::new_readonly(non_signer.pubkey(), false), // update_authority (NOT a signer) + AccountMeta::new_readonly(program_data_pda, false), + AccountMeta::new_readonly(solana_sdk::system_program::ID, false), + ], + data: [&[5u8][..], &create_ix_data.try_to_vec().unwrap()[..]].concat(), + }; + + let result = rpc + .create_and_send_transaction(&[create_ix], &payer.pubkey(), &[&payer]) + .await; + + assert!( + result.is_err(), + "Config creation without signer should fail" + ); +} diff --git a/sdk-tests/package.json b/sdk-tests/package.json new file mode 100644 index 0000000000..35b879ef57 --- /dev/null +++ b/sdk-tests/package.json @@ -0,0 +1,29 @@ +{ + "name": "@lightprotocol/sdk-tests", + "version": "0.1.0", + "license": "Apache-2.0", + "scripts": { + "build": "pnpm build-anchor-compressible && pnpm build-anchor-compressible-derived && pnpm build-native-compressible", + "build-anchor-compressible": "cd anchor-compressible/ && cargo build-sbf && cd ..", + "build-anchor-compressible-derived": "cd anchor-compressible-derived/ && cargo build-sbf && cd ..", + "build-native-compressible": "cd native-compressible/ && cargo build-sbf && cd ..", + "test": "RUSTFLAGS=\"-D warnings\" && pnpm test-anchor-compressible && pnpm test-anchor-compressible-derived && pnpm test-native-compressible", + "test-anchor-compressible": "cargo test-sbf -p anchor-compressible", + "test-anchor-compressible-derived": "cargo test-sbf -p anchor-compressible-derived", + "test-native-compressible": "cargo test-sbf -p native-compressible" + }, + "nx": { + "targets": { + "build": { + "outputs": [ + "{workspaceRoot}/target/deploy", + "{workspaceRoot}/target/idl", + "{workspaceRoot}/target/types" + ] + }, + "test": { + "outputs": [] + } + } + } +} diff --git a/sdk-tests/sdk-anchor-test/Anchor.toml b/sdk-tests/sdk-anchor-test/Anchor.toml index a443e6fb8c..0071604adb 100644 --- a/sdk-tests/sdk-anchor-test/Anchor.toml +++ b/sdk-tests/sdk-anchor-test/Anchor.toml @@ -5,7 +5,7 @@ seeds = false skip-lint = false [programs.localnet] -sdk_test = "2tzfijPBGbrR5PboyFUFKzfEoLTwdDSHUjANCw929wyt" +sdk-anchor-test = "2tzfijPBGbrR5PboyFUFKzfEoLTwdDSHUjANCw929wyt" [registry] url = "https://api.apr.dev" diff --git a/sdk-tests/sdk-native-test/Cargo.toml b/sdk-tests/sdk-native-test/Cargo.toml index 54b5978963..b0f7ddc005 100644 --- a/sdk-tests/sdk-native-test/Cargo.toml +++ b/sdk-tests/sdk-native-test/Cargo.toml @@ -9,6 +9,7 @@ edition = "2021" [lib] crate-type = ["cdylib", "lib"] name = "sdk_native_test" +doctest = false [features] no-entrypoint = [] @@ -19,17 +20,21 @@ test-sbf = [] default = [] [dependencies] -light-sdk = { workspace = true } -light-sdk-types = { workspace = true } -light-hasher = { workspace = true, features = ["solana"] } +light-sdk = { workspace = true, default-features = false, features = ["borsh"] } +light-sdk-types = { workspace = true, default-features = false } +light-hasher = { workspace = true, features = ["solana"], default-features = false } solana-program = { workspace = true } -light-macros = { workspace = true, features = ["solana"] } +light-macros = { workspace = true, features = ["solana"], default-features = false } borsh = { workspace = true } -light-compressed-account = { workspace = true, features = ["solana"] } +light-compressed-account = { workspace = true, features = ["solana"], default-features = false } light-zero-copy = { workspace = true } +solana-clock = { workspace = true } +solana-sysvar = { workspace = true } [dev-dependencies] -light-program-test = { workspace = true, features = ["devenv"] } +light-program-test = { workspace = true, features = ["v2"], default-features = false } +light-client = { workspace = true } +light-compressible-client = { workspace = true } tokio = { workspace = true } solana-sdk = { workspace = true } @@ -39,3 +44,4 @@ check-cfg = [ 'cfg(target_os, values("solana"))', 'cfg(feature, values("frozen-abi", "no-entrypoint"))', ] + diff --git a/sdk-tests/sdk-native-test/src/create_pda.rs b/sdk-tests/sdk-native-test/src/create_pda.rs index 27f3ba12a5..610d8d72b1 100644 --- a/sdk-tests/sdk-native-test/src/create_pda.rs +++ b/sdk-tests/sdk-native-test/src/create_pda.rs @@ -1,11 +1,12 @@ use borsh::{BorshDeserialize, BorshSerialize}; use light_sdk::{ account::LightAccount, - cpi::{CpiAccounts, CpiAccountsConfig, CpiInputs}, + cpi::{CpiAccountsConfig, CpiAccountsSmall, CpiInputs}, error::LightSdkError, instruction::{PackedAddressTreeInfo, ValidityProof}, light_hasher::hash_to_field_size::hashv_to_bn254_field_size_be_const_array, - LightDiscriminator, LightHasher, + sha::LightHasher, + LightDiscriminator, }; use solana_program::{account_info::AccountInfo, msg}; @@ -25,12 +26,7 @@ pub fn create_pda( .map_err(|_| LightSdkError::Borsh)?; msg!("pre config"); let config = CpiAccountsConfig::new(crate::LIGHT_CPI_SIGNER); - let cpi_accounts = CpiAccounts::try_new_with_config( - &accounts[0], - &accounts[instruction_data.system_accounts_offset as usize..], - config, - ) - .unwrap(); + let cpi_accounts = CpiAccountsSmall::new_with_config(&accounts[0], &accounts[1..], config); let address_tree_info = instruction_data.address_tree_info; let (address, address_seed) = if BATCHED { @@ -40,7 +36,9 @@ pub fn create_pda( ]) .unwrap(); // to_bytes will go away as soon as we have a light_sdk::address::v2::derive_address - let address_tree_pubkey = address_tree_info.get_tree_pubkey(&cpi_accounts)?.to_bytes(); + let address_tree_pubkey = address_tree_info + .get_tree_pubkey_small(&cpi_accounts)? + .to_bytes(); let address = light_compressed_account::address::derive_address( &address_seed, &address_tree_pubkey, @@ -50,7 +48,7 @@ pub fn create_pda( } else { light_sdk::address::v1::derive_address( &[b"compressed", instruction_data.data.as_slice()], - &address_tree_info.get_tree_pubkey(&cpi_accounts)?, + &address_tree_info.get_tree_pubkey_small(&cpi_accounts)?, &crate::ID, ) }; @@ -69,7 +67,7 @@ pub fn create_pda( vec![my_compressed_account.to_account_info()?], vec![new_address_params], ); - cpi_inputs.invoke_light_system_program(cpi_accounts)?; + cpi_inputs.invoke_light_system_program_small(cpi_accounts)?; Ok(()) } diff --git a/sdk-tests/sdk-native-test/src/update_pda.rs b/sdk-tests/sdk-native-test/src/update_pda.rs index 95d12e4fb7..2e8d4ef972 100644 --- a/sdk-tests/sdk-native-test/src/update_pda.rs +++ b/sdk-tests/sdk-native-test/src/update_pda.rs @@ -1,7 +1,7 @@ use borsh::{BorshDeserialize, BorshSerialize}; use light_sdk::{ account::LightAccount, - cpi::{CpiAccounts, CpiAccountsConfig, CpiInputs}, + cpi::{CpiAccountsConfig, CpiAccountsSmall, CpiInputs}, error::LightSdkError, instruction::{account_meta::CompressedAccountMeta, ValidityProof}, }; @@ -26,6 +26,7 @@ pub fn update_pda( &crate::ID, &instruction_data.my_compressed_account.meta, MyCompressedAccount { + // compression_info: None, data: instruction_data.my_compressed_account.data, }, )?; @@ -35,18 +36,14 @@ pub fn update_pda( let config = CpiAccountsConfig::new(crate::LIGHT_CPI_SIGNER); sol_log_compute_units(); - let cpi_accounts = CpiAccounts::try_new_with_config( - &accounts[0], - &accounts[instruction_data.system_accounts_offset as usize..], - config, - )?; + let cpi_accounts = CpiAccountsSmall::new_with_config(&accounts[0], &accounts[1..], config); sol_log_compute_units(); let cpi_inputs = CpiInputs::new( instruction_data.proof, vec![my_compressed_account.to_account_info()?], ); sol_log_compute_units(); - cpi_inputs.invoke_light_system_program(cpi_accounts)?; + cpi_inputs.invoke_light_system_program_small(cpi_accounts)?; Ok(()) } diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml index 8258907b2f..d27c605bfa 100644 --- a/xtask/Cargo.toml +++ b/xtask/Cargo.toml @@ -33,3 +33,4 @@ solana-client = { workspace = true } solana-transaction-status = { workspace = true } light-batched-merkle-tree = { workspace = true } light-registry = { workspace = true } +base64 = { workspace = true } \ No newline at end of file diff --git a/xtask/src/create_batch_state_tree.rs b/xtask/src/create_batch_state_tree.rs index 7afb0b411b..37b691765d 100644 --- a/xtask/src/create_batch_state_tree.rs +++ b/xtask/src/create_batch_state_tree.rs @@ -62,9 +62,6 @@ pub async fn create_batch_state_tree(options: Options) -> anyhow::Result<()> { let mt_keypair = Keypair::new(); let nfq_keypair = Keypair::new(); let cpi_keypair = Keypair::new(); - println!("new mt: {:?}", mt_keypair.pubkey()); - println!("new nfq: {:?}", nfq_keypair.pubkey()); - println!("new cpi: {:?}", cpi_keypair.pubkey()); write_keypair_file(&mt_keypair, format!("./target/mt-{}", mt_keypair.pubkey())).unwrap(); write_keypair_file( @@ -81,12 +78,12 @@ pub async fn create_batch_state_tree(options: Options) -> anyhow::Result<()> { nfq_keypairs.push(nfq_keypair); cpi_keypairs.push(cpi_keypair); } else { - let mt_keypair = read_keypair_file(options.mt_pubkey.unwrap()).unwrap(); - let nfq_keypair = read_keypair_file(options.nfq_pubkey.unwrap()).unwrap(); - let cpi_keypair = read_keypair_file(options.cpi_pubkey.unwrap()).unwrap(); - println!("read mt: {:?}", mt_keypair.pubkey()); - println!("read nfq: {:?}", nfq_keypair.pubkey()); - println!("read cpi: {:?}", cpi_keypair.pubkey()); + let mt_keypair = + read_keypair_file(format!("./target/mt-{}", options.mt_pubkey.unwrap())).unwrap(); + let nfq_keypair = + read_keypair_file(format!("./target/nfq-{}", options.nfq_pubkey.unwrap())).unwrap(); + let cpi_keypair = + read_keypair_file(format!("./target/cpi-{}", options.cpi_pubkey.unwrap())).unwrap(); mt_keypairs.push(mt_keypair); nfq_keypairs.push(nfq_keypair); cpi_keypairs.push(cpi_keypair); @@ -102,7 +99,6 @@ pub async fn create_batch_state_tree(options: Options) -> anyhow::Result<()> { read_keypair_file(keypair_path.clone()) .unwrap_or_else(|_| panic!("Keypair not found in default path {:?}", keypair_path)) }; - println!("read payer: {:?}", payer.pubkey()); let config = if let Some(config) = options.config { if config == "testnet" { diff --git a/xtask/src/new_deployment.rs b/xtask/src/new_deployment.rs index 14d13788e3..73fbcac825 100644 --- a/xtask/src/new_deployment.rs +++ b/xtask/src/new_deployment.rs @@ -310,6 +310,9 @@ pub fn new_testnet_setup() -> TestKeypairs { nullifier_queue_2: Keypair::new(), cpi_context_2: Keypair::new(), group_pda_seed: Keypair::new(), + batched_state_merkle_tree_2: Keypair::new(), + batched_output_queue_2: Keypair::new(), + batched_cpi_context_2: Keypair::new(), } }