diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..65e7762 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,298 @@ +name: CI + +on: + push: + branches: [main, master] + pull_request: + branches: [main, master] + types: [opened, synchronize, reopened, ready_for_review] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + CARGO_TERM_COLOR: always + # Light protocol commit that cp-swap-reference depends on + LIGHT_PROTOCOL_COMMIT: "4fa7d8941d698930b1dcfb618cc4901f95320b5c" + # Versions from light-protocol at that commit + SOLANA_VERSION: "2.2.15" + RUST_VERSION: "1.93" + NODE_VERSION: "22" + PNPM_VERSION: "9.15.4" + PHOTON_VERSION: "0.51.2" + PHOTON_COMMIT: "2e32a4e48f95d68a4d99b9384a763f4a4157fe85" + SBF_OUT_DIR: target/deploy + +jobs: + lint: + name: Lint + if: github.event.pull_request.draft == false + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install just + uses: extractions/setup-just@v2 + + - name: Setup Rust + uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: ${{ env.RUST_VERSION }} + components: rustfmt, clippy + + - name: Run lint + run: just lint + + build: + name: Build + if: github.event.pull_request.draft == false + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install just + uses: extractions/setup-just@v2 + + - name: Setup Rust + uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: ${{ env.RUST_VERSION }} + + - name: Cache Solana CLI + id: cache-solana + uses: actions/cache@v4 + with: + path: ~/.local/share/solana/install + key: ${{ runner.os }}-solana-${{ env.SOLANA_VERSION }} + + - name: Install Solana CLI + if: steps.cache-solana.outputs.cache-hit != 'true' + run: | + sh -c "$(curl -sSfL https://release.anza.xyz/v${{ env.SOLANA_VERSION }}/install)" + + - name: Add Solana to PATH + run: echo "$HOME/.local/share/solana/install/active_release/bin" >> $GITHUB_PATH + + - name: Cache program build + id: cache-program + uses: actions/cache@v4 + with: + path: target/deploy/*.so + key: ${{ runner.os }}-program-${{ hashFiles('programs/**/*.rs', 'programs/**/Cargo.toml', 'Cargo.lock') }} + + - name: Build program + if: steps.cache-program.outputs.cache-hit != 'true' + run: just build + + - name: Upload program artifact + uses: actions/upload-artifact@v4 + with: + name: program-so + path: target/deploy/*.so + retention-days: 1 + + test-local: + name: Local Tests + needs: build + if: github.event.pull_request.draft == false + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install just + uses: extractions/setup-just@v2 + + - name: Setup Rust + uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: ${{ env.RUST_VERSION }} + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: ${{ env.PNPM_VERSION }} + + - name: Cache Solana CLI + id: cache-solana + uses: actions/cache@v4 + with: + path: ~/.local/share/solana/install + key: ${{ runner.os }}-solana-${{ env.SOLANA_VERSION }} + + - name: Install Solana CLI + if: steps.cache-solana.outputs.cache-hit != 'true' + run: | + sh -c "$(curl -sSfL https://release.anza.xyz/v${{ env.SOLANA_VERSION }}/install)" + + - name: Add Solana to PATH + run: echo "$HOME/.local/share/solana/install/active_release/bin" >> $GITHUB_PATH + + - name: Cache light-protocol + id: cache-light-protocol + uses: actions/cache@v4 + with: + path: | + light-protocol + light-protocol/node_modules + light-protocol/target/deploy/*.so + key: ${{ runner.os }}-light-protocol-${{ env.LIGHT_PROTOCOL_COMMIT }} + + - name: Clone and build light-protocol + if: steps.cache-light-protocol.outputs.cache-hit != 'true' + run: | + git clone https://github.com/Lightprotocol/light-protocol.git + cd light-protocol + git checkout ${{ env.LIGHT_PROTOCOL_COMMIT }} + + just install + just programs build + just cli build + + - name: Install light CLI globally + run: | + # Install the CLI globally so find_light_bin() can discover it + cd light-protocol/cli && npm install -g . + # Verify the CLI is accessible + which light + + - name: Download program artifact + uses: actions/download-artifact@v4 + with: + name: program-so + path: target/deploy + + - name: Run local tests + run: just test-local + + test-integration: + name: Integration Tests + needs: build + if: github.event.pull_request.draft == false + runs-on: ubuntu-latest + timeout-minutes: 45 + + 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 + + steps: + - uses: actions/checkout@v4 + + - name: Install just + uses: extractions/setup-just@v2 + + - name: Setup Rust + uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: ${{ env.RUST_VERSION }} + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: ${{ env.PNPM_VERSION }} + + - name: Cache Solana CLI + id: cache-solana + uses: actions/cache@v4 + with: + path: ~/.local/share/solana/install + key: ${{ runner.os }}-solana-${{ env.SOLANA_VERSION }} + + - name: Install Solana CLI + if: steps.cache-solana.outputs.cache-hit != 'true' + run: | + sh -c "$(curl -sSfL https://release.anza.xyz/v${{ env.SOLANA_VERSION }}/install)" + + - name: Add Solana to PATH + run: echo "$HOME/.local/share/solana/install/active_release/bin" >> $GITHUB_PATH + + - name: Cache Photon + id: cache-photon + uses: actions/cache@v4 + with: + path: ~/.cargo/bin/photon + key: ${{ runner.os }}-photon-${{ env.PHOTON_VERSION }}-${{ env.PHOTON_COMMIT }} + + - name: Install Photon from git + if: steps.cache-photon.outputs.cache-hit != 'true' + run: | + echo "Installing Photon ${{ env.PHOTON_VERSION }} (commit ${{ env.PHOTON_COMMIT }})..." + RUSTFLAGS="-A dead-code" cargo install --git https://github.com/helius-labs/photon.git --rev ${{ env.PHOTON_COMMIT }} --locked --force + + - name: Cache light-protocol + id: cache-light-protocol + uses: actions/cache@v4 + with: + path: | + light-protocol + light-protocol/node_modules + light-protocol/target/deploy/*.so + key: ${{ runner.os }}-light-protocol-${{ env.LIGHT_PROTOCOL_COMMIT }} + + - name: Clone and build light-protocol + if: steps.cache-light-protocol.outputs.cache-hit != 'true' + run: | + git clone https://github.com/Lightprotocol/light-protocol.git + cd light-protocol + git checkout ${{ env.LIGHT_PROTOCOL_COMMIT }} + + # Use light-protocol's justfile commands + just install + just programs build + just cli build + + - name: Install light CLI globally + run: | + # Install the CLI globally so find_light_bin() can discover it + cd light-protocol/cli && npm install -g . + # Verify the CLI is accessible + which light + + - name: Download program artifact + uses: actions/download-artifact@v4 + with: + name: program-so + path: target/deploy + + - name: Generate Solana keypair + run: | + mkdir -p ~/.config/solana + solana-keygen new --no-bip39-passphrase -o ~/.config/solana/id.json + + - name: Start light test-validator + run: | + cd light-protocol + pnpm --filter=@lightprotocol/zk-compression-cli test-validator & + # Wait for validator to be ready + sleep 45 + solana cluster-version || (echo "Validator not ready" && exit 1) + + - name: Run integration tests + run: just test-integration + + - name: Display logs on failure + if: failure() + run: | + echo "=== Displaying test-ledger logs ===" + find . -path "*/test-ledger/*.log" -type f -exec echo "=== {} ===" \; -exec tail -100 {} \; || echo "No logs found" diff --git a/.gitignore b/.gitignore index 508a755..2bd394a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ - +.surfpool .anchor .DS_Store target diff --git a/Cargo.lock b/Cargo.lock index 785203f..e79693d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -12,6 +12,30 @@ dependencies = [ "regex", ] +[[package]] +name = "account-compression" +version = "2.0.0" +source = "git+https://github.com/Lightprotocol/light-protocol?rev=e705f24cede040a2452090d8dcf8ae4a9ab1e6aa#e705f24cede040a2452090d8dcf8ae4a9ab1e6aa" +dependencies = [ + "aligned-sized", + "anchor-lang", + "bytemuck", + "light-account-checks", + "light-batched-merkle-tree", + "light-bounded-vec", + "light-compressed-account", + "light-concurrent-merkle-tree", + "light-hash-set", + "light-hasher", + "light-indexed-merkle-tree", + "light-merkle-tree-metadata", + "light-zero-copy", + "num-bigint 0.4.6", + "solana-sdk", + "solana-security-txt", + "zerocopy", +] + [[package]] name = "adler2" version = "2.0.1" @@ -126,8 +150,7 @@ dependencies = [ [[package]] name = "aligned-sized" version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48a526ec4434d531d488af59fe866f36b310fe8906691c75dffa664450a3800a" +source = "git+https://github.com/Lightprotocol/light-protocol?rev=e705f24cede040a2452090d8dcf8ae4a9ab1e6aa#e705f24cede040a2452090d8dcf8ae4a9ab1e6aa" dependencies = [ "proc-macro2", "quote", @@ -688,6 +711,18 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "bb8" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89aabfae550a5c44b43ab941844ffcd2e993cb6900b342debf59e9ea74acdb8" +dependencies = [ + "async-trait", + "futures-util", + "parking_lot", + "tokio", +] + [[package]] name = "bincode" version = "1.3.3" @@ -893,9 +928,9 @@ checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e" [[package]] name = "bytemuck" -version = "1.24.0" +version = "1.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" dependencies = [ "bytemuck_derive", ] @@ -935,9 +970,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.53" +version = "1.2.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "755d2fce177175ffca841e9a06afdb2c4ab0f593d53b4dee48147dfaade85932" +checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29" dependencies = [ "find-msvc-tools", "jobserver", @@ -1243,6 +1278,20 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "dashmap" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + [[package]] name = "deranged" version = "0.5.5" @@ -1505,9 +1554,9 @@ checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" [[package]] name = "find-msvc-tools" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "five8" @@ -1570,6 +1619,42 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" +[[package]] +name = "forester-utils" +version = "2.0.0" +source = "git+https://github.com/Lightprotocol/light-protocol?rev=e705f24cede040a2452090d8dcf8ae4a9ab1e6aa#e705f24cede040a2452090d8dcf8ae4a9ab1e6aa" +dependencies = [ + "account-compression", + "anchor-lang", + "async-trait", + "bb8", + "borsh 0.10.4", + "governor", + "light-account-checks", + "light-batched-merkle-tree", + "light-client", + "light-compressed-account", + "light-compressed-token-sdk", + "light-compressible", + "light-concurrent-merkle-tree", + "light-hash-set", + "light-hasher", + "light-indexed-merkle-tree", + "light-merkle-tree-reference", + "light-prover-client", + "light-registry", + "light-sdk", + "light-sparse-merkle-tree", + "light-token-interface", + "num-traits", + "solana-instruction", + "solana-pubkey", + "solana-sdk", + "thiserror 2.0.18", + "tokio", + "tracing", +] + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -1656,6 +1741,12 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +[[package]] +name = "futures-timer" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" + [[package]] name = "futures-util" version = "0.3.31" @@ -1734,6 +1825,29 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "governor" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be93b4ec2e4710b04d9264c0c7350cdd62a8c20e5e4ac732552ebb8f0debe8eb" +dependencies = [ + "cfg-if", + "dashmap", + "futures-sink", + "futures-timer", + "futures-util", + "getrandom 0.3.4", + "no-std-compat", + "nonzero_ext", + "parking_lot", + "portable-atomic", + "quanta", + "rand 0.9.2", + "smallvec", + "spinning_top", + "web-time", +] + [[package]] name = "groth16-solana" version = "0.2.0" @@ -1811,6 +1925,12 @@ dependencies = [ "ahash", ] +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + [[package]] name = "hashbrown" version = "0.15.2" @@ -2085,7 +2205,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.1", + "socket2 0.6.2", "system-configuration 0.6.1", "tokio", "tower-service", @@ -2095,9 +2215,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.64" +version = "0.1.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -2444,9 +2564,8 @@ dependencies = [ [[package]] name = "light-account-checks" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0785da22cd4a7667583141ca56c790a5c8afa2b22ad2a08204d78881035524e8" +version = "0.7.0" +source = "git+https://github.com/Lightprotocol/light-protocol?rev=e705f24cede040a2452090d8dcf8ae4a9ab1e6aa#e705f24cede040a2452090d8dcf8ae4a9ab1e6aa" dependencies = [ "solana-account-info", "solana-msg", @@ -2474,29 +2593,27 @@ dependencies = [ [[package]] name = "light-array-map" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "859dc5b406a8bf0b114f686e6f2e36d0e939bad6f579492a520d309b52fde1f8" +version = "0.2.0" +source = "git+https://github.com/Lightprotocol/light-protocol?rev=e705f24cede040a2452090d8dcf8ae4a9ab1e6aa#e705f24cede040a2452090d8dcf8ae4a9ab1e6aa" dependencies = [ "tinyvec", ] [[package]] name = "light-batched-merkle-tree" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13cb8bc778065ee71d1990fdc94112e35dc63a5e387a323284a49f40d123d8e0" +version = "0.9.0" +source = "git+https://github.com/Lightprotocol/light-protocol?rev=e705f24cede040a2452090d8dcf8ae4a9ab1e6aa#e705f24cede040a2452090d8dcf8ae4a9ab1e6aa" dependencies = [ "aligned-sized", "borsh 0.10.4", "light-account-checks", "light-bloom-filter", - "light-compressed-account 0.8.0", + "light-compressed-account", "light-hasher", "light-macros", "light-merkle-tree-metadata", "light-verifier", - "light-zero-copy 0.6.0", + "light-zero-copy", "solana-account-info", "solana-msg", "solana-program-error", @@ -2508,9 +2625,8 @@ dependencies = [ [[package]] name = "light-bloom-filter" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8a609e3c9179f0ae8488cc70c5413c86dfd97dad7ad85fee2ad8da2d0a11e61" +version = "0.6.0" +source = "git+https://github.com/Lightprotocol/light-protocol?rev=e705f24cede040a2452090d8dcf8ae4a9ab1e6aa#e705f24cede040a2452090d8dcf8ae4a9ab1e6aa" dependencies = [ "bitvec", "num-bigint 0.4.6", @@ -2533,9 +2649,8 @@ dependencies = [ [[package]] name = "light-client" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b1f3cd013364dbe5c45a9e9a8faee1af30dccb600cd56a41e296ed8d5684768" +version = "0.19.0" +source = "git+https://github.com/Lightprotocol/light-protocol?rev=e705f24cede040a2452090d8dcf8ae4a9ab1e6aa#e705f24cede040a2452090d8dcf8ae4a9ab1e6aa" dependencies = [ "anchor-lang", "async-trait", @@ -2544,7 +2659,8 @@ dependencies = [ "bs58", "futures", "lazy_static", - "light-compressed-account 0.8.0", + "light-compressed-account", + "light-compressed-token-sdk", "light-compressible", "light-concurrent-merkle-tree", "light-event", @@ -2559,6 +2675,8 @@ dependencies = [ "num-bigint 0.4.6", "photon-api", "rand 0.8.5", + "reqwest 0.12.28", + "serde_json", "smallvec", "solana-account", "solana-account-decoder-client-types", @@ -2588,61 +2706,68 @@ dependencies = [ [[package]] name = "light-compressed-account" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "058df2733fa6a3e4bda6f162a6c5d41f10fc8c6f6ddb992af1de76b60214e4a6" +version = "0.9.0" +source = "git+https://github.com/Lightprotocol/light-protocol?rev=e705f24cede040a2452090d8dcf8ae4a9ab1e6aa#e705f24cede040a2452090d8dcf8ae4a9ab1e6aa" dependencies = [ + "anchor-lang", "borsh 0.10.4", + "bytemuck", "light-hasher", "light-macros", + "light-poseidon 0.3.0", "light-program-profiler", - "light-zero-copy 0.5.0", + "light-zero-copy", + "solana-msg", + "solana-program-error", + "solana-pubkey", "thiserror 2.0.18", "tinyvec", "zerocopy", ] [[package]] -name = "light-compressed-account" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "768ae5a56d8c9cf315d132b3faa5b067f95b3d6a294c579e82f8f0e0bf29c7cc" +name = "light-compressed-token-sdk" +version = "0.1.0" +source = "git+https://github.com/Lightprotocol/light-protocol?rev=e705f24cede040a2452090d8dcf8ae4a9ab1e6aa#e705f24cede040a2452090d8dcf8ae4a9ab1e6aa" dependencies = [ "anchor-lang", + "arrayvec", "borsh 0.10.4", - "bytemuck", - "light-hasher", - "light-macros", - "light-poseidon 0.3.0", + "light-account-checks", + "light-compressed-account", "light-program-profiler", - "light-zero-copy 0.6.0", + "light-sdk", + "light-sdk-types", + "light-token-interface", + "light-token-types", + "light-zero-copy", + "solana-account-info", + "solana-cpi", + "solana-instruction", "solana-msg", "solana-program-error", "solana-pubkey", "thiserror 2.0.18", - "tinyvec", - "zerocopy", ] [[package]] name = "light-compressible" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eff0f0065beb8d16df587b3ea17082e11dea3f67c98813b4bcc061eecd94561f" +version = "0.4.0" +source = "git+https://github.com/Lightprotocol/light-protocol?rev=e705f24cede040a2452090d8dcf8ae4a9ab1e6aa#e705f24cede040a2452090d8dcf8ae4a9ab1e6aa" dependencies = [ "aligned-sized", "anchor-lang", "borsh 0.10.4", "bytemuck", "light-account-checks", - "light-compressed-account 0.8.0", + "light-compressed-account", "light-hasher", "light-macros", "light-program-profiler", - "light-sdk-types", - "light-zero-copy 0.6.0", + "light-zero-copy", "pinocchio-pubkey", "solana-pubkey", + "solana-rent", "thiserror 2.0.18", "zerocopy", ] @@ -2650,8 +2775,7 @@ dependencies = [ [[package]] name = "light-concurrent-merkle-tree" version = "5.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db96f47253a0907aaa46dac15cecb27b5510130e48da0b36690dcd2e99a6d558" +source = "git+https://github.com/Lightprotocol/light-protocol?rev=e705f24cede040a2452090d8dcf8ae4a9ab1e6aa#e705f24cede040a2452090d8dcf8ae4a9ab1e6aa" dependencies = [ "borsh 0.10.4", "light-bounded-vec", @@ -2663,22 +2787,33 @@ dependencies = [ [[package]] name = "light-event" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1674c9d85b32a9e8abb90cccdee18e35ae29daa1126fdb81a8a28c0a54802096" +version = "0.4.0" +source = "git+https://github.com/Lightprotocol/light-protocol?rev=e705f24cede040a2452090d8dcf8ae4a9ab1e6aa#e705f24cede040a2452090d8dcf8ae4a9ab1e6aa" dependencies = [ "borsh 0.10.4", - "light-compressed-account 0.8.0", + "light-compressed-account", "light-hasher", - "light-zero-copy 0.6.0", + "light-token-interface", + "light-zero-copy", + "thiserror 2.0.18", +] + +[[package]] +name = "light-hash-set" +version = "4.0.0" +source = "git+https://github.com/Lightprotocol/light-protocol?rev=e705f24cede040a2452090d8dcf8ae4a9ab1e6aa#e705f24cede040a2452090d8dcf8ae4a9ab1e6aa" +dependencies = [ + "light-hasher", + "num-bigint 0.4.6", + "num-traits", + "solana-program-error", "thiserror 2.0.18", ] [[package]] name = "light-hasher" version = "5.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c822662e6e109bac0e132a43fd52a4ef684811245a794e048cf9cda001e934c8" +source = "git+https://github.com/Lightprotocol/light-protocol?rev=e705f24cede040a2452090d8dcf8ae4a9ab1e6aa#e705f24cede040a2452090d8dcf8ae4a9ab1e6aa" dependencies = [ "ark-bn254 0.5.0", "ark-ff 0.5.0", @@ -2695,8 +2830,7 @@ dependencies = [ [[package]] name = "light-indexed-array" version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f14f984030d86b6f07bd8f5ae04e2c40fcd0c3bdfcc7a291fff1ed59c9e6554" +source = "git+https://github.com/Lightprotocol/light-protocol?rev=e705f24cede040a2452090d8dcf8ae4a9ab1e6aa#e705f24cede040a2452090d8dcf8ae4a9ab1e6aa" dependencies = [ "light-hasher", "num-bigint 0.4.6", @@ -2707,8 +2841,7 @@ dependencies = [ [[package]] name = "light-indexed-merkle-tree" version = "5.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0824755289075f28de2820fc7d4ec4e6b9e99d404e033c07338b91cce8c71fb8" +source = "git+https://github.com/Lightprotocol/light-protocol?rev=e705f24cede040a2452090d8dcf8ae4a9ab1e6aa#e705f24cede040a2452090d8dcf8ae4a9ab1e6aa" dependencies = [ "light-bounded-vec", "light-concurrent-merkle-tree", @@ -2720,11 +2853,42 @@ dependencies = [ "thiserror 2.0.18", ] +[[package]] +name = "light-instruction-decoder" +version = "0.2.0" +source = "git+https://github.com/Lightprotocol/light-protocol?rev=e705f24cede040a2452090d8dcf8ae4a9ab1e6aa#e705f24cede040a2452090d8dcf8ae4a9ab1e6aa" +dependencies = [ + "borsh 0.10.4", + "bs58", + "light-compressed-account", + "light-instruction-decoder-derive", + "light-sdk-types", + "light-token-interface", + "serde", + "solana-instruction", + "solana-pubkey", + "solana-signature", + "tabled", +] + +[[package]] +name = "light-instruction-decoder-derive" +version = "0.2.0" +source = "git+https://github.com/Lightprotocol/light-protocol?rev=e705f24cede040a2452090d8dcf8ae4a9ab1e6aa#e705f24cede040a2452090d8dcf8ae4a9ab1e6aa" +dependencies = [ + "bs58", + "darling", + "heck 0.5.0", + "proc-macro2", + "quote", + "sha2 0.10.9", + "syn 2.0.114", +] + [[package]] name = "light-macros" version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "179ac51cadc1d0ca047b4d6265a7cc245ca3affc16a20a2749585aa6464d39c2" +source = "git+https://github.com/Lightprotocol/light-protocol?rev=e705f24cede040a2452090d8dcf8ae4a9ab1e6aa#e705f24cede040a2452090d8dcf8ae4a9ab1e6aa" dependencies = [ "bs58", "proc-macro2", @@ -2735,14 +2899,13 @@ dependencies = [ [[package]] name = "light-merkle-tree-metadata" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17d08edcc194eef61b0f499934ce398122d54ac57505d44480e5f079a4220566" +version = "0.9.0" +source = "git+https://github.com/Lightprotocol/light-protocol?rev=e705f24cede040a2452090d8dcf8ae4a9ab1e6aa#e705f24cede040a2452090d8dcf8ae4a9ab1e6aa" dependencies = [ "anchor-lang", "borsh 0.10.4", "bytemuck", - "light-compressed-account 0.8.0", + "light-compressed-account", "solana-msg", "solana-program-error", "solana-sysvar", @@ -2753,8 +2916,7 @@ dependencies = [ [[package]] name = "light-merkle-tree-reference" version = "4.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8d480f62ca32b38a6231bbc5310d693f91d6b5bdcc18bb13c2d9aab7a1c90e8" +source = "git+https://github.com/Lightprotocol/light-protocol?rev=e705f24cede040a2452090d8dcf8ae4a9ab1e6aa#e705f24cede040a2452090d8dcf8ae4a9ab1e6aa" dependencies = [ "light-hasher", "light-indexed-array", @@ -2809,9 +2971,8 @@ dependencies = [ [[package]] name = "light-program-test" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a981dfbc19c529543ab1dd8d100319b89aac053b81415a681d1474c986218307" +version = "0.19.0" +source = "git+https://github.com/Lightprotocol/light-protocol?rev=e705f24cede040a2452090d8dcf8ae4a9ab1e6aa#e705f24cede040a2452090d8dcf8ae4a9ab1e6aa" dependencies = [ "anchor-lang", "async-trait", @@ -2820,13 +2981,16 @@ dependencies = [ "bs58", "bytemuck", "chrono", + "light-account-checks", "light-client", - "light-compressed-account 0.8.0", + "light-compressed-account", + "light-compressed-token-sdk", "light-compressible", "light-event", "light-hasher", "light-indexed-array", "light-indexed-merkle-tree", + "light-instruction-decoder", "light-merkle-tree-metadata", "light-merkle-tree-reference", "light-prover-client", @@ -2834,7 +2998,7 @@ dependencies = [ "light-sdk-types", "light-token", "light-token-interface", - "light-zero-copy 0.6.0", + "light-zero-copy", "litesvm", "log", "num-bigint 0.4.6", @@ -2861,14 +3025,13 @@ dependencies = [ [[package]] name = "light-prover-client" -version = "5.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75d8c9b8b6e9d445b9ef27467da592ee231e614282c3c0bd2f30f567eb904845" +version = "6.0.0" +source = "git+https://github.com/Lightprotocol/light-protocol?rev=e705f24cede040a2452090d8dcf8ae4a9ab1e6aa#e705f24cede040a2452090d8dcf8ae4a9ab1e6aa" dependencies = [ "ark-bn254 0.5.0", "ark-serialize 0.5.0", "ark-std 0.5.0", - "light-compressed-account 0.7.0", + "light-compressed-account", "light-hasher", "light-indexed-array", "light-sparse-merkle-tree", @@ -2883,23 +3046,51 @@ dependencies = [ "tracing", ] +[[package]] +name = "light-registry" +version = "2.1.0" +source = "git+https://github.com/Lightprotocol/light-protocol?rev=e705f24cede040a2452090d8dcf8ae4a9ab1e6aa#e705f24cede040a2452090d8dcf8ae4a9ab1e6aa" +dependencies = [ + "account-compression", + "aligned-sized", + "anchor-lang", + "borsh 0.10.4", + "light-account-checks", + "light-batched-merkle-tree", + "light-compressible", + "light-macros", + "light-merkle-tree-metadata", + "light-program-profiler", + "light-system-program-anchor", + "light-token-interface", + "light-zero-copy", + "solana-account-info", + "solana-instruction", + "solana-pubkey", + "solana-sdk", + "solana-security-txt", + "spl-pod", +] + [[package]] name = "light-sdk" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dece106ebd0897bd23a12bad040e0999d93b54447d0473739f91b1f83b1d331" +version = "0.19.0" +source = "git+https://github.com/Lightprotocol/light-protocol?rev=e705f24cede040a2452090d8dcf8ae4a9ab1e6aa#e705f24cede040a2452090d8dcf8ae4a9ab1e6aa" dependencies = [ "anchor-lang", "bincode", "borsh 0.10.4", + "bytemuck", "light-account-checks", - "light-compressed-account 0.8.0", + "light-compressed-account", "light-compressible", "light-hasher", "light-macros", + "light-program-profiler", "light-sdk-macros", "light-sdk-types", - "light-zero-copy 0.6.0", + "light-token-interface", + "light-zero-copy", "num-bigint 0.4.6", "solana-account-info", "solana-clock", @@ -2917,9 +3108,8 @@ dependencies = [ [[package]] name = "light-sdk-macros" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45d91992fa08093b1a274b3baed1d8368de794cc2645f9942718e5fe47a27dc2" +version = "0.19.0" +source = "git+https://github.com/Lightprotocol/light-protocol?rev=e705f24cede040a2452090d8dcf8ae4a9ab1e6aa#e705f24cede040a2452090d8dcf8ae4a9ab1e6aa" dependencies = [ "darling", "light-hasher", @@ -2932,14 +3122,13 @@ dependencies = [ [[package]] name = "light-sdk-types" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b765f0a39428a137b8d449fa60ba147194cdbff08aa0add598c6047fff2cb7d2" +version = "0.19.0" +source = "git+https://github.com/Lightprotocol/light-protocol?rev=e705f24cede040a2452090d8dcf8ae4a9ab1e6aa#e705f24cede040a2452090d8dcf8ae4a9ab1e6aa" dependencies = [ "anchor-lang", "borsh 0.10.4", "light-account-checks", - "light-compressed-account 0.8.0", + "light-compressed-account", "light-hasher", "light-macros", "solana-msg", @@ -2949,8 +3138,7 @@ dependencies = [ [[package]] name = "light-sparse-merkle-tree" version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4251e79b6c63f4946572dcfd7623680ad0f9e0efe1a761a944733333c5645063" +source = "git+https://github.com/Lightprotocol/light-protocol?rev=e705f24cede040a2452090d8dcf8ae4a9ab1e6aa#e705f24cede040a2452090d8dcf8ae4a9ab1e6aa" dependencies = [ "light-hasher", "light-indexed-array", @@ -2959,18 +3147,31 @@ dependencies = [ "thiserror 2.0.18", ] +[[package]] +name = "light-system-program-anchor" +version = "2.0.0" +source = "git+https://github.com/Lightprotocol/light-protocol?rev=e705f24cede040a2452090d8dcf8ae4a9ab1e6aa#e705f24cede040a2452090d8dcf8ae4a9ab1e6aa" +dependencies = [ + "account-compression", + "aligned-sized", + "anchor-lang", + "light-compressed-account", + "light-zero-copy", + "zerocopy", +] + [[package]] name = "light-token" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62907a12a9801200e5f4c03bb7f2dbdd9aa679223a959167c456a06005291d79" +version = "0.4.0" +source = "git+https://github.com/Lightprotocol/light-protocol?rev=e705f24cede040a2452090d8dcf8ae4a9ab1e6aa#e705f24cede040a2452090d8dcf8ae4a9ab1e6aa" dependencies = [ "anchor-lang", "arrayvec", "borsh 0.10.4", "light-account-checks", "light-batched-merkle-tree", - "light-compressed-account 0.8.0", + "light-compressed-account", + "light-compressed-token-sdk", "light-compressible", "light-macros", "light-program-profiler", @@ -2979,7 +3180,7 @@ dependencies = [ "light-sdk-types", "light-token-interface", "light-token-types", - "light-zero-copy 0.6.0", + "light-zero-copy", "solana-account-info", "solana-cpi", "solana-instruction", @@ -2992,21 +3193,20 @@ dependencies = [ [[package]] name = "light-token-interface" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fb19b8e268a0154a8e13b3a8f6f43fa4928643e2de102d98a90b2af21f482ba" +version = "0.3.0" +source = "git+https://github.com/Lightprotocol/light-protocol?rev=e705f24cede040a2452090d8dcf8ae4a9ab1e6aa#e705f24cede040a2452090d8dcf8ae4a9ab1e6aa" dependencies = [ "aligned-sized", "anchor-lang", "borsh 0.10.4", "bytemuck", "light-array-map", - "light-compressed-account 0.8.0", + "light-compressed-account", "light-compressible", "light-hasher", "light-macros", "light-program-profiler", - "light-zero-copy 0.6.0", + "light-zero-copy", "pinocchio", "pinocchio-pubkey", "solana-account-info", @@ -3020,14 +3220,13 @@ dependencies = [ [[package]] name = "light-token-types" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "278dddbf18d104f1225c480ca6d7b8710e1f9ff4104f24be70c522ecb6ed1dfc" +version = "0.4.0" +source = "git+https://github.com/Lightprotocol/light-protocol?rev=e705f24cede040a2452090d8dcf8ae4a9ab1e6aa#e705f24cede040a2452090d8dcf8ae4a9ab1e6aa" dependencies = [ "anchor-lang", "borsh 0.10.4", "light-account-checks", - "light-compressed-account 0.8.0", + "light-compressed-account", "light-macros", "light-sdk-types", "solana-msg", @@ -3036,53 +3235,28 @@ dependencies = [ [[package]] name = "light-verifier" -version = "7.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f35f47736be493b60d8b56ef0c8e94afd6a99efafebb257f62b0b545e9aacab" +version = "8.0.0" +source = "git+https://github.com/Lightprotocol/light-protocol?rev=e705f24cede040a2452090d8dcf8ae4a9ab1e6aa#e705f24cede040a2452090d8dcf8ae4a9ab1e6aa" dependencies = [ "groth16-solana", - "light-compressed-account 0.8.0", + "light-compressed-account", "thiserror 2.0.18", ] -[[package]] -name = "light-zero-copy" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a8862f463792fd60ae8f5dc418150c16213e302e19d54fba0694cf8515be5ff" -dependencies = [ - "light-zero-copy-derive 0.5.0", - "zerocopy", -] - [[package]] name = "light-zero-copy" version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5621fb515e14af46148699c0b65334aabe230a1d2cbd06736ccc7a408c8a4af" +source = "git+https://github.com/Lightprotocol/light-protocol?rev=e705f24cede040a2452090d8dcf8ae4a9ab1e6aa#e705f24cede040a2452090d8dcf8ae4a9ab1e6aa" dependencies = [ - "light-zero-copy-derive 0.6.0", + "light-zero-copy-derive", "solana-program-error", "zerocopy", ] -[[package]] -name = "light-zero-copy-derive" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8af086d52100b3cab1f2993b146adc7a69fa6aaa878ae4c19514c77c50304379" -dependencies = [ - "lazy_static", - "proc-macro2", - "quote", - "syn 2.0.114", -] - [[package]] name = "light-zero-copy-derive" version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41c46425e5c7ab5203ff5c86ae2615b169cca55f9283f5f60f5dd74143be6934" +source = "git+https://github.com/Lightprotocol/light-protocol?rev=e705f24cede040a2452090d8dcf8ae4a9ab1e6aa#e705f24cede040a2452090d8dcf8ae4a9ab1e6aa" dependencies = [ "lazy_static", "proc-macro2", @@ -3276,6 +3450,18 @@ dependencies = [ "tempfile", ] +[[package]] +name = "no-std-compat" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c" + +[[package]] +name = "nonzero_ext" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21" + [[package]] name = "num" version = "0.2.1" @@ -3324,9 +3510,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" [[package]] name = "num-derive" @@ -3454,9 +3640,9 @@ checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "openssl-src" -version = "300.5.4+3.5.4" +version = "300.5.5+3.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a507b3792995dae9b0df8a1c1e3771e8418b7c2d9f0baeba32e6fe8b06c7cb72" +checksum = "3f1787d533e03597a7934fd0a765f0d28e94ecc5fb7789f8053b1e699a56f709" dependencies = [ "cc", ] @@ -3560,8 +3746,7 @@ dependencies = [ [[package]] name = "photon-api" version = "0.54.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e572dba0c255f5b8176f15b9e849330d915a8927804f7f9702d5bbbc70e4a1ad" +source = "git+https://github.com/Lightprotocol/light-protocol?rev=e705f24cede040a2452090d8dcf8ae4a9ab1e6aa#e705f24cede040a2452090d8dcf8ae4a9ab1e6aa" dependencies = [ "reqwest 0.12.28", "serde", @@ -3641,9 +3826,9 @@ dependencies = [ [[package]] name = "portable-atomic" -version = "1.13.0" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f89776e4d69bb58bc6993e99ffa1d11f228b839984854c7daeb5d37f87cbe950" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "potential_utf" @@ -3711,9 +3896,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.105" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] @@ -3757,6 +3942,21 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "quanta" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3ab5a9d756f0d97bdc89019bd2e4ea098cf9cde50ee7564dde6b81ccc8f06c7" +dependencies = [ + "crossbeam-utils", + "libc", + "once_cell", + "raw-cpuid", + "wasi 0.11.1+wasi-snapshot-preview1", + "web-sys", + "winapi", +] + [[package]] name = "quick-error" version = "1.2.3" @@ -3787,7 +3987,7 @@ dependencies = [ "quinn-udp", "rustc-hash", "rustls 0.23.36", - "socket2 0.6.1", + "socket2 0.6.2", "thiserror 2.0.18", "tokio", "tracing", @@ -3824,16 +4024,16 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.6.1", + "socket2 0.6.2", "tracing", "windows-sys 0.60.2", ] [[package]] name = "quote" -version = "1.0.43" +version = "1.0.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" dependencies = [ "proc-macro2", ] @@ -3959,28 +4159,46 @@ dependencies = [ "rand_core 0.9.5", ] +[[package]] +name = "raw-cpuid" +version = "11.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186" +dependencies = [ + "bitflags 2.10.0", +] + [[package]] name = "raydium-cp-swap" version = "0.2.0" dependencies = [ "anchor-lang", "arrayref", + "base64 0.22.1", "bincode", "blake3", + "bs58", "bytemuck", + "forester-utils", "light-anchor-spl", "light-client", "light-hasher", "light-program-test", + "light-registry", "light-sdk", "light-token", "proptest", "quickcheck", "rand 0.9.2", + "reqwest 0.12.28", + "serde_json", "solana-account-info", + "solana-commitment-config", + "solana-compute-budget-interface", "solana-cpi", "solana-instruction", "solana-keypair", + "solana-message", "solana-msg", "solana-program", "solana-program-error", @@ -4340,9 +4558,9 @@ dependencies = [ [[package]] name = "schemars" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54e910108742c57a770f492731f99be216a52fadd361b06c8fb59d74ccc267d2" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" dependencies = [ "dyn-clone", "ref-cast", @@ -4490,7 +4708,7 @@ dependencies = [ "indexmap 1.9.3", "indexmap 2.13.0", "schemars 0.9.0", - "schemars 1.2.0", + "schemars 1.2.1", "serde_core", "serde_json", "serde_with_macros", @@ -4604,9 +4822,9 @@ checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" [[package]] name = "slab" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" @@ -4626,9 +4844,9 @@ dependencies = [ [[package]] name = "socket2" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" dependencies = [ "libc", "windows-sys 0.60.2", @@ -6892,6 +7110,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "spinning_top" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d96d2d1d716fb500937168cc09353ffdc7a012be8475ac7308e1bdf0e3923300" +dependencies = [ + "lock_api", +] + [[package]] name = "spl-associated-token-account" version = "6.0.0" @@ -7871,9 +8098,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.45" +version = "0.3.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9e442fc33d7fdb45aa9bfeb312c095964abdf596f7567261062b2a7107aaabd" +checksum = "9da98b7d9b7dad93488a84b8248efc35352b0b2657397d4167e7ad67e5d535e5" dependencies = [ "deranged", "itoa", @@ -7886,15 +8113,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b36ee98fd31ec7426d599183e8fe26932a8dc1fb76ddb6214d05493377d34ca" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" [[package]] name = "time-macros" -version = "0.2.25" +version = "0.2.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71e552d1249bf61ac2a52db88179fd0673def1e1ad8243a00d9ec9ed71fee3dd" +checksum = "78cc610bac2dcee56805c99642447d4c5dbde4d01f752ffea0199aee1f601dc4" dependencies = [ "num-conv", "time-core", @@ -7937,7 +8164,7 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.6.1", + "socket2 0.6.2", "tokio-macros", "windows-sys 0.61.2", ] @@ -8323,9 +8550,9 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "uuid" -version = "1.19.0" +version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" +checksum = "ee48d38b119b0cd71fe4141b30f5ba9c7c5d9f4e7a3a8b4a674e4b6ef789976f" dependencies = [ "getrandom 0.3.4", "js-sys", @@ -8887,18 +9114,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.33" +version = "0.8.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "668f5168d10b9ee831de31933dc111a459c97ec93225beb307aed970d1372dfd" +checksum = "7456cf00f0685ad319c5b1693f291a650eaf345e941d082fc4e03df8a03996ac" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.33" +version = "0.8.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c7962b26b0a8685668b671ee4b54d007a67d4eaf05fda79ac0ecf41e32270f1" +checksum = "1328722bbf2115db7e19d69ebcc15e795719e2d66b60827c6a69a117365e37a0" dependencies = [ "proc-macro2", "quote", @@ -8981,9 +9208,9 @@ dependencies = [ [[package]] name = "zmij" -version = "1.0.16" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfcd145825aace48cff44a8844de64bf75feec3080e0aa5cdbde72961ae51a65" +checksum = "3ff05f8caa9038894637571ae6b9e29466c1f4f829d26c9b28f869a29cbe3445" [[package]] name = "zstd" diff --git a/justfile b/justfile new file mode 100644 index 0000000..351d82f --- /dev/null +++ b/justfile @@ -0,0 +1,122 @@ +# CP-Swap Reference Implementation +set dotenv-load + +# Use absolute path for SBF_OUT_DIR so tests can find program binaries +root_dir := `pwd` +export SBF_OUT_DIR := root_dir / "target/deploy" + +default: + @just --list + +# === Build === + +# Build the SBF program +build: + cargo build-sbf + +# Build native (for tests) +build-native: + cargo build --package raydium-cp-swap + +# === Test === + +# Run all tests (build + local + integration) +test-all: build test-local test-integration + +# Run local tests only (no external validator needed) +test: build test-local + +# Run local tests (LightProgramTest, no external validator) +test-local: + cargo test --test local_test -- --nocapture + +# Run functional tests (requires light test-validator) +test-functional: + cargo test --test functional_test -- --nocapture --test-threads=1 + +# Run program tests (requires light test-validator with forester) +test-program: + cargo test --test program_test -- --nocapture --test-threads=1 + +# Run all integration tests (starts/stops light test-validator automatically) +test-integration: start-validator-background + -cargo test --test functional_test -- --nocapture --test-threads=1 + -cargo test --test program_test -- --nocapture --test-threads=1 + just stop-validator + +# === Lint & Format === + +# Check formatting and run clippy +lint: + cargo fmt --all -- --check + cargo clippy --workspace --all-features --tests -- -D warnings + +# Format code +format: + cargo fmt --all + +# === Clean === + +# Clean build artifacts +clean: + find . -type d -name "test-ledger" -exec rm -rf {} + 2>/dev/null || true + cargo clean + +# === Info === + +# Show version info +info: + @echo "Solana: $(solana --version)" + @echo "Rust: $(rustc --version)" + @echo "Anchor: $(anchor --version)" + +# === Development === + +# Payer pubkey for upgradeable program deployment +payer_pubkey := "ALA2cnz41Wa2v2EYUdkYHsg7VnKsbH1j7secM5aiP8k" +accounts_dir := root_dir / "programs/cp-swap/tests/accounts" + +# Start light test-validator in foreground (for manual testing) +start-validator: _stop-validator _clean-ledger + @echo "Starting light test-validator with cp-swap program (foreground)..." + light test-validator \ + --limit-ledger-size 50000000 \ + --upgradeable-program CPMMoo8L3F4NbTegBCKVNunggL7H1ZpdTHKxQB5qKP1C target/deploy/raydium_cp_swap.so "{{payer_pubkey}}" \ + --account-dir "{{accounts_dir}}" + +# Start light test-validator in background with cp-swap program +start-validator-background: _stop-validator _clean-ledger + @echo "Starting light test-validator with cp-swap program..." + light test-validator \ + --limit-ledger-size 50000000 \ + --upgradeable-program CPMMoo8L3F4NbTegBCKVNunggL7H1ZpdTHKxQB5qKP1C target/deploy/raydium_cp_swap.so "{{payer_pubkey}}" \ + --account-dir "{{accounts_dir}}" & + @echo "Waiting for validator to start..." + @for i in $(seq 1 120); do \ + if curl -s -X POST http://localhost:8899 -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1,"method":"getHealth"}' 2>/dev/null | grep -q '"result"'; then \ + echo "Validator is ready!"; \ + exit 0; \ + fi; \ + sleep 1; \ + done; \ + echo "Validator failed to start within 120 seconds"; \ + exit 1 + +# Stop light test-validator +stop-validator: + light test-validator --stop 2>/dev/null || true + +# Internal: stop any existing validator +_stop-validator: + @echo "Stopping any existing validator..." + @light test-validator --stop 2>/dev/null || true + @sleep 2 + +# Internal: clean old ledger +_clean-ledger: + @echo "Cleaning old ledger..." + @rm -rf test-ledger + +# Watch and rebuild on changes +watch: + cargo watch -x "build-sbf" diff --git a/programs/cp-swap/Cargo.toml b/programs/cp-swap/Cargo.toml index 7ba71c3..aade667 100644 --- a/programs/cp-swap/Cargo.toml +++ b/programs/cp-swap/Cargo.toml @@ -19,11 +19,11 @@ enable-log = [] devnet = [] client = [] anchor-debug = [] -idl-build = ["anchor-lang/idl-build", "light-sdk/idl-build", "light-token/idl-build", "light-anchor-spl/idl-build"] +idl-build = ["anchor-lang/idl-build", "light-sdk/idl-build", "light-token/idl-build", "light-anchor-spl/idl-build", "light-anchor-spl/token_2022"] test-sbf = [] [dependencies] -anchor-lang = { version = "=0.31.1", features = ["init-if-needed", "idl-build"] } +anchor-lang = { version = "=0.31.1", features = ["init-if-needed"] } spl-token-2022 = { version = "7.0.0", features = ["no-entrypoint"] } spl-math = { version = "0.3", features = ["no-entrypoint"] } uint = "0.10.0" @@ -32,10 +32,10 @@ bytemuck = { version = "1.4.0", features = ["derive", "min_const_generics"] } arrayref = { version = "0.3.6" } blake3 = { workspace = true } -light-sdk = { version = "0.18.0", features = ["anchor", "anchor-discriminator", "idl-build", "cpi-context"] } -light-token = { version = "0.3.0", features = ["anchor", "idl-build"] } -light-hasher = "5" -light-anchor-spl = { version = "0.31.1", features = ["idl-build", "memo"] } +light-sdk = { git = "https://github.com/Lightprotocol/light-protocol", rev = "e705f24cede040a2452090d8dcf8ae4a9ab1e6aa", features = ["anchor", "cpi-context"] } +light-token = { git = "https://github.com/Lightprotocol/light-protocol", rev = "e705f24cede040a2452090d8dcf8ae4a9ab1e6aa", features = ["anchor"] } +light-hasher = { git = "https://github.com/Lightprotocol/light-protocol", rev = "e705f24cede040a2452090d8dcf8ae4a9ab1e6aa" } +light-anchor-spl = { version = "0.31.1", features = ["memo", "token_2022"] } solana-account-info = "2.3" solana-program = "2.2" solana-pubkey = "2.2" @@ -48,15 +48,24 @@ quickcheck = "1.0.3" proptest = "1.0" rand = "0.9.0" -light-program-test = { version = "0.18.0" } -light-client = { version = "0.18.0" } +light-program-test = { git = "https://github.com/Lightprotocol/light-protocol", rev = "e705f24cede040a2452090d8dcf8ae4a9ab1e6aa" } +light-client = { git = "https://github.com/Lightprotocol/light-protocol", rev = "e705f24cede040a2452090d8dcf8ae4a9ab1e6aa" } +light-registry = { git = "https://github.com/Lightprotocol/light-protocol", rev = "e705f24cede040a2452090d8dcf8ae4a9ab1e6aa" } +forester-utils = { git = "https://github.com/Lightprotocol/light-protocol", rev = "e705f24cede040a2452090d8dcf8ae4a9ab1e6aa" } tokio = { version = "1", features = ["full"] } spl-token = "7.0.0" +solana-commitment-config = { version = "2.2" } +solana-compute-budget-interface = { version = "2.2" } solana-keypair = { version = "2.2" } solana-signer = { version = "2.2" } solana-instruction = { version = "2.2" } +solana-message = { version = "2.2" } solana-sdk = { version = "2.3" } bincode = "1.3" +base64 = "0.22" +serde_json = "1.0" +bs58 = "0.5" +reqwest = { version = "0.12", features = ["json"] } [profile.release] diff --git a/programs/cp-swap/src/instructions/deposit.rs b/programs/cp-swap/src/instructions/deposit.rs index 814038b..d48dd5f 100644 --- a/programs/cp-swap/src/instructions/deposit.rs +++ b/programs/cp-swap/src/instructions/deposit.rs @@ -6,8 +6,8 @@ use crate::utils::token::*; use anchor_lang::prelude::*; use light_anchor_spl::token::Token; use light_anchor_spl::token_interface::Token2022; +use light_anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface}; use light_token::instruction::MintToCpi; -use light_anchor_spl::token_interface::{TokenAccount, Mint,TokenInterface}; #[derive(Accounts)] pub struct Deposit<'info> { @@ -213,6 +213,7 @@ pub fn deposit( authority: ctx.accounts.authority.to_account_info(), system_program: ctx.accounts.system_program.to_account_info(), max_top_up: None, + fee_payer: None, } .invoke_signed(&[&[crate::AUTH_SEED.as_bytes(), &[pool_state.auth_bump]]])?; pool_state.recent_epoch = Clock::get()?.epoch; diff --git a/programs/cp-swap/src/instructions/initialize.rs b/programs/cp-swap/src/instructions/initialize.rs index 111bfbc..ad3ff9b 100644 --- a/programs/cp-swap/src/instructions/initialize.rs +++ b/programs/cp-swap/src/instructions/initialize.rs @@ -17,8 +17,8 @@ use light_sdk::interface::CreateAccountsProof; use light_token::anchor::LightAccounts; use light_token::{ instruction::{ - CreateTokenAccountCpi, CreateTokenAtaCpi, MintToCpi, COMPRESSIBLE_CONFIG_V1, - RENT_SPONSOR as LIGHT_TOKEN_RENT_SPONSOR, + CreateTokenAccountCpi, CreateTokenAtaCpi, MintToCpi, LIGHT_TOKEN_CONFIG, + LIGHT_TOKEN_RENT_SPONSOR, }, utils::get_token_account_balance, }; @@ -82,12 +82,14 @@ pub struct Initialize<'info> { pub lp_mint_signer: UncheckedAccount<'info>, #[account(mut)] - #[light_account(init, mint, - mint_signer = lp_mint_signer, - authority = authority, - decimals = 9, - mint_seeds = &[LP_MINT_SIGNER_SEED, self.pool_state.to_account_info().key.as_ref(), &[params.lp_mint_signer_bump]], - authority_seeds = &[crate::AUTH_SEED.as_bytes(), &[params.authority_bump]] + #[light_account(init, + mint::signer = lp_mint_signer, + mint::authority = authority, + mint::decimals = 9, + mint::seeds = &[LP_MINT_SIGNER_SEED, self.pool_state.to_account_info().key.as_ref()], + mint::bump = params.lp_mint_signer_bump, + mint::authority_seeds = &[crate::AUTH_SEED.as_bytes()], + mint::authority_bump = params.authority_bump )] pub lp_mint: UncheckedAccount<'info>, @@ -117,7 +119,10 @@ pub struct Initialize<'info> { ], bump, )] - #[light_account(token, authority = [crate::AUTH_SEED.as_bytes()])] + #[light_account( + token::seeds = [POOL_VAULT_SEED.as_bytes(), self.pool_state.key().as_ref(), self.token_0_mint.key().as_ref()], + token::owner_seeds = [crate::AUTH_SEED.as_bytes()] + )] pub token_0_vault: UncheckedAccount<'info>, #[account( @@ -129,7 +134,10 @@ pub struct Initialize<'info> { ], bump, )] - #[light_account(token, authority = [crate::AUTH_SEED.as_bytes()])] + #[light_account( + token::seeds = [POOL_VAULT_SEED.as_bytes(), self.pool_state.key().as_ref(), self.token_1_mint.key().as_ref()], + token::owner_seeds = [crate::AUTH_SEED.as_bytes()] + )] pub token_1_vault: UncheckedAccount<'info>, #[account( @@ -154,8 +162,12 @@ pub struct Initialize<'info> { pub compression_config: AccountInfo<'info>, - #[account(address = COMPRESSIBLE_CONFIG_V1)] - pub light_token_compressible_config: AccountInfo<'info>, + #[account(address = LIGHT_TOKEN_CONFIG)] + pub light_token_config: AccountInfo<'info>, + + /// CHECK: PDA rent sponsor for reimbursement + #[account(mut)] + pub pda_rent_sponsor: AccountInfo<'info>, #[account(mut, address = LIGHT_TOKEN_RENT_SPONSOR)] pub light_token_rent_sponsor: AccountInfo<'info>, @@ -199,7 +211,7 @@ pub fn initialize<'info>( owner: ctx.accounts.authority.key(), } .rent_free( - ctx.accounts.light_token_compressible_config.to_account_info(), + ctx.accounts.light_token_config.to_account_info(), ctx.accounts.light_token_rent_sponsor.to_account_info(), ctx.accounts.system_program.to_account_info(), &crate::ID, @@ -219,7 +231,7 @@ pub fn initialize<'info>( owner: ctx.accounts.authority.key(), } .rent_free( - ctx.accounts.light_token_compressible_config.to_account_info(), + ctx.accounts.light_token_config.to_account_info(), ctx.accounts.light_token_rent_sponsor.to_account_info(), ctx.accounts.system_program.to_account_info(), &crate::ID, @@ -272,7 +284,7 @@ pub fn initialize<'info>( &system_instruction::transfer( ctx.accounts.creator.key, &ctx.accounts.create_pool_fee.key(), - u64::from(ctx.accounts.amm_config.create_pool_fee), + ctx.accounts.amm_config.create_pool_fee, ), &[ ctx.accounts.creator.to_account_info(), @@ -332,7 +344,7 @@ pub fn initialize<'info>( } .idempotent() .rent_free( - ctx.accounts.light_token_compressible_config.to_account_info(), + ctx.accounts.light_token_config.to_account_info(), ctx.accounts.light_token_rent_sponsor.to_account_info(), ctx.accounts.system_program.to_account_info(), ) @@ -346,6 +358,7 @@ pub fn initialize<'info>( authority: ctx.accounts.authority.to_account_info(), system_program: ctx.accounts.system_program.to_account_info(), max_top_up: None, + fee_payer: None, } .invoke_signed(&[&[crate::AUTH_SEED.as_bytes(), &[ctx.bumps.authority]]])?; diff --git a/programs/cp-swap/src/instructions/withdraw.rs b/programs/cp-swap/src/instructions/withdraw.rs index 484d08b..e903dff 100644 --- a/programs/cp-swap/src/instructions/withdraw.rs +++ b/programs/cp-swap/src/instructions/withdraw.rs @@ -187,7 +187,9 @@ pub fn withdraw( mint: ctx.accounts.lp_mint.to_account_info(), amount: lp_token_amount, authority: ctx.accounts.owner.to_account_info(), + system_program: ctx.accounts.system_program.to_account_info(), max_top_up: None, + fee_payer: None, } .invoke()?; diff --git a/programs/cp-swap/src/lib.rs b/programs/cp-swap/src/lib.rs index 2d80571..fff1f3e 100644 --- a/programs/cp-swap/src/lib.rs +++ b/programs/cp-swap/src/lib.rs @@ -1,4 +1,15 @@ #![allow(deprecated)] +#![allow(clippy::redundant_static_lifetimes)] +#![allow(clippy::let_and_return)] +#![allow(clippy::useless_conversion)] +#![allow(clippy::needless_question_mark)] +#![allow(clippy::needless_bool)] +#![allow(clippy::unnecessary_cast)] +#![allow(clippy::suspicious_doc_comments)] +#![allow(clippy::manual_div_ceil)] +#![allow(clippy::bool_assert_comparison)] +#![allow(clippy::reversed_empty_ranges)] +#![allow(clippy::collapsible_if)] pub mod curve; pub mod error; @@ -49,10 +60,7 @@ pub mod admin { use super::{pubkey, Pubkey}; #[cfg(feature = "devnet")] pub const ID: Pubkey = pubkey!("adMCyoCgfkg7bQiJ9aBJ59H3BXLY3r5LNLfPpQfMzBe"); - #[cfg(all(not(feature = "devnet"), not(feature = "test-sbf")))] - pub const ID: Pubkey = pubkey!("AKnL4NNf3DGWZJS6cPknBuEGnVsV4A4m5tgebLHaRSZ9"); - // Test admin - pubkey derived from Keypair::from_seed(&[1u8; 32]) - #[cfg(feature = "test-sbf")] + #[cfg(not(feature = "devnet"))] pub const ID: Pubkey = pubkey!("AKnL4NNf3DGWZJS6cPknBuEGnVsV4A4m5tgebLHaRSZ9"); } diff --git a/programs/cp-swap/src/states/oracle.rs b/programs/cp-swap/src/states/oracle.rs index 2b4e798..3c36c15 100644 --- a/programs/cp-swap/src/states/oracle.rs +++ b/programs/cp-swap/src/states/oracle.rs @@ -1,6 +1,7 @@ use anchor_lang::prelude::*; -use light_sdk::LightDiscriminator; -use light_token::anchor::{CompressionInfo, LightAccount}; +use light_token::anchor::{ + CompressionInfo, LightAccount, LightDiscriminatorTrait as LightDiscriminator, +}; #[cfg(test)] use std::time::{SystemTime, UNIX_EPOCH}; @@ -19,7 +20,7 @@ pub struct Observation { #[derive(Default, Debug, InitSpace, LightAccount)] #[account] pub struct ObservationState { - pub compression_info: Option, + pub compression_info: CompressionInfo, pub initialized: bool, pub observation_index: u16, pub pool_id: Pubkey, diff --git a/programs/cp-swap/src/states/pool.rs b/programs/cp-swap/src/states/pool.rs index b23db7b..497540e 100644 --- a/programs/cp-swap/src/states/pool.rs +++ b/programs/cp-swap/src/states/pool.rs @@ -1,7 +1,8 @@ use anchor_lang::prelude::*; use light_anchor_spl::token_interface::Mint; -use light_sdk::LightDiscriminator; -use light_token::anchor::{CompressionInfo, LightAccount}; +use light_token::anchor::{ + CompressionInfo, LightAccount, LightDiscriminatorTrait as LightDiscriminator, +}; use std::ops::{BitAnd, BitOr, BitXor}; pub const POOL_SEED: &str = "pool"; @@ -26,7 +27,7 @@ pub enum PoolStatusBitFlag { #[account] #[repr(C)] pub struct PoolState { - pub compression_info: Option, + pub compression_info: CompressionInfo, pub amm_config: Pubkey, pub pool_creator: Pubkey, pub token_0_vault: Pubkey, @@ -53,6 +54,7 @@ pub struct PoolState { } impl PoolState { + #[allow(clippy::too_many_arguments)] pub fn initialize( &mut self, auth_bump: u8, @@ -105,6 +107,7 @@ impl PoolState { } } + /// Get status by bit, if it is `noraml` status, return true pub fn get_status_by_bit(&self, bit: PoolStatusBitIndex) -> bool { let status = u8::from(1) << (bit as u8); self.status.bitand(status) == 0 @@ -140,7 +143,7 @@ pub mod pool_test { #[test] fn get_set_status_by_bit() { let mut pool_state = PoolState::default(); - pool_state.set_status(4); + pool_state.set_status(4); // 0000100 assert_eq!( pool_state.get_status_by_bit(PoolStatusBitIndex::Swap), false @@ -154,24 +157,28 @@ pub mod pool_test { true ); + // disable -> disable, nothing to change pool_state.set_status_by_bit(PoolStatusBitIndex::Swap, PoolStatusBitFlag::Disable); assert_eq!( pool_state.get_status_by_bit(PoolStatusBitIndex::Swap), false ); + // disable -> enable pool_state.set_status_by_bit(PoolStatusBitIndex::Swap, PoolStatusBitFlag::Enable); assert_eq!(pool_state.get_status_by_bit(PoolStatusBitIndex::Swap), true); + // enable -> enable, nothing to change pool_state.set_status_by_bit(PoolStatusBitIndex::Swap, PoolStatusBitFlag::Enable); assert_eq!(pool_state.get_status_by_bit(PoolStatusBitIndex::Swap), true); + // enable -> disable pool_state.set_status_by_bit(PoolStatusBitIndex::Swap, PoolStatusBitFlag::Disable); assert_eq!( pool_state.get_status_by_bit(PoolStatusBitIndex::Swap), false ); - pool_state.set_status(5); + pool_state.set_status(5); // 0000101 assert_eq!( pool_state.get_status_by_bit(PoolStatusBitIndex::Swap), false @@ -185,7 +192,7 @@ pub mod pool_test { true ); - pool_state.set_status(7); + pool_state.set_status(7); // 0000111 assert_eq!( pool_state.get_status_by_bit(PoolStatusBitIndex::Swap), false diff --git a/programs/cp-swap/src/utils/math.rs b/programs/cp-swap/src/utils/math.rs index 19210ba..3024180 100644 --- a/programs/cp-swap/src/utils/math.rs +++ b/programs/cp-swap/src/utils/math.rs @@ -2,6 +2,7 @@ ///! U128 is more efficient that u128 ///! https://github.com/solana-labs/solana/issues/19549 use uint::construct_uint; + construct_uint! { pub struct U128(2); } diff --git a/programs/cp-swap/src/utils/token.rs b/programs/cp-swap/src/utils/token.rs index 1849b79..8258da5 100644 --- a/programs/cp-swap/src/utils/token.rs +++ b/programs/cp-swap/src/utils/token.rs @@ -50,6 +50,7 @@ const MINT_WHITELIST: [&'static str; 4] = [ "2b1kV6DkPAnxd5ixfnxCpjxmKwqjjaYmCZfHsFu24GXo", ]; +#[allow(clippy::too_many_arguments)] pub fn transfer_from_user_to_pool_vault<'a>( authority: AccountInfo<'a>, from: AccountInfo<'a>, @@ -79,12 +80,12 @@ pub fn transfer_from_user_to_pool_vault<'a>( light_token_cpi_authority, system_program, ) - .invoke() - .map_err(|e| anchor_lang::prelude::ProgramError::from(e))?; + .invoke()?; Ok(()) } +#[allow(clippy::too_many_arguments)] pub fn transfer_from_pool_vault_to_user<'a>( authority: AccountInfo<'a>, from_vault: AccountInfo<'a>, @@ -115,8 +116,7 @@ pub fn transfer_from_pool_vault_to_user<'a>( light_token_cpi_authority, system_program, ) - .invoke_signed(signer_seeds) - .map_err(|e| anchor_lang::prelude::ProgramError::from(e))?; + .invoke_signed(signer_seeds)?; Ok(()) } diff --git a/programs/cp-swap/tests/accounts/DNXgeM9EiiaAbaWvwjHj9fQQLAX5ZsfHyvmYUNRAdNC8.json b/programs/cp-swap/tests/accounts/DNXgeM9EiiaAbaWvwjHj9fQQLAX5ZsfHyvmYUNRAdNC8.json new file mode 100644 index 0000000..894e169 --- /dev/null +++ b/programs/cp-swap/tests/accounts/DNXgeM9EiiaAbaWvwjHj9fQQLAX5ZsfHyvmYUNRAdNC8.json @@ -0,0 +1,10 @@ +{ + "pubkey": "DNXgeM9EiiaAbaWvwjHj9fQQLAX5ZsfHyvmYUNRAdNC8", + "account": { + "lamports": 2039280, + "data": ["BpuIV/6rgYT7aH9jRhjANdrEOdwa6ztVmKDwAAAAAAECY+L7WEJcIRnY07lwy9TuaZBIebD9aqhznpq8Pv+mUQDKmjsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEAAADwHR8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", "base64"], + "owner": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", + "executable": false, + "rentEpoch": 0 + } +} diff --git a/programs/cp-swap/tests/functional_test.rs b/programs/cp-swap/tests/functional_test.rs index 9b6218c..35cdedc 100644 --- a/programs/cp-swap/tests/functional_test.rs +++ b/programs/cp-swap/tests/functional_test.rs @@ -1,9 +1,7 @@ /// Functional integration test for cp-swap program. -/// Tests pool initialization with light-program-test framework. - -use light_client::interface::AccountInterfaceExt; -use light_program_test::program_test::TestRpc; -use light_program_test::Rpc; +/// Tests pool initialization with light test-validator and photon indexer. +use light_client::rpc::Rpc; +use solana_compute_budget_interface::ComputeBudgetInstruction; use solana_keypair::Keypair; use solana_signer::Signer; @@ -11,7 +9,7 @@ mod helpers; mod program; use helpers::*; -#[tokio::test] +#[tokio::test(flavor = "multi_thread")] async fn test_full_lifecycle() { let program_id = raydium_cp_swap::ID; @@ -44,7 +42,7 @@ async fn test_full_lifecycle() { assert_amm_config_created(&mut env.rpc, amm_config).await; // Setup create pool fee account - setup_create_pool_fee_account(&mut env.rpc, &env.payer.pubkey()); + setup_create_pool_fee_account(&mut env.rpc, &env.payer, &env.payer.pubkey()).await; // Derive PDAs let pdas = derive_amm_pdas( @@ -76,8 +74,21 @@ async fn test_full_lifecycle() { 0, // open_time = 0 (immediate) ); + // Create Address Lookup Table with all accounts from the initialize instruction + // This reduces transaction size by referencing accounts via 1-byte indices + let lut_addresses = extract_lut_addresses(&proof_result.remaining_accounts); + let lut = create_address_lookup_table(&mut env.rpc, &env.payer, lut_addresses).await; + + // Add compute budget instruction - Initialize requires more than default 200k CU + let compute_budget_ix = ComputeBudgetInstruction::set_compute_unit_limit(1_400_000); + env.rpc - .create_and_send_transaction(&[init_instruction], &creator.pubkey(), &[&creator]) + .create_and_send_versioned_transaction( + &[compute_budget_ix, init_instruction], + &creator.pubkey(), + &[&creator], + &[lut], + ) .await .expect("Initialize should succeed"); @@ -86,7 +97,10 @@ async fn test_full_lifecycle() { // Check initial LP token balance (should have received initial LP tokens from initialize) let lp_balance_after_init = get_token_balance(&mut env.rpc, pdas.creator_lp_token).await; println!("LP balance after init: {}", lp_balance_after_init); - assert!(lp_balance_after_init > 0, "Should have received LP tokens from initialization"); + assert!( + lp_balance_after_init > 0, + "Should have received LP tokens from initialization" + ); // ======================================================================== // Deposit @@ -132,7 +146,7 @@ async fn test_full_lifecycle() { // Swap (token_0 -> token_1) // ======================================================================== // Warp time forward so pool is open for swaps (open_time = block_timestamp + 1) - env.rpc.warp_to_slot(100).unwrap(); + env.rpc.warp_to_slot(100).await.unwrap(); let token_0_balance_before = get_token_balance(&mut env.rpc, tokens.creator_token_0).await; let token_1_balance_before = get_token_balance(&mut env.rpc, tokens.creator_token_1).await; @@ -219,10 +233,10 @@ async fn test_full_lifecycle() { } /// Test SDK initialization from fetched accounts and account requirements. -#[tokio::test] +#[tokio::test(flavor = "multi_thread")] async fn test_sdk_from_keyed_accounts() { - use program::{CpSwapSdk, CpSwapInstruction}; use light_client::interface::LightProgramInterface; + use program::{CpSwapInstruction, CpSwapSdk}; let program_id = raydium_cp_swap::ID; @@ -230,7 +244,8 @@ async fn test_sdk_from_keyed_accounts() { let mut setup = setup_pool_environment(program_id, 2).await; // Initialize pool first (SDK requires actual account data) - let proof_result = get_pool_create_accounts_proof(&setup.env.rpc, &program_id, &setup.pdas).await; + let proof_result = + get_pool_create_accounts_proof(&setup.env.rpc, &program_id, &setup.pdas).await; let init_ix = build_initialize_instruction( program_id, setup.creator.pubkey(), @@ -243,16 +258,36 @@ async fn test_sdk_from_keyed_accounts() { 100_000, 0, ); - setup.env.rpc - .create_and_send_transaction(&[init_ix], &setup.creator.pubkey(), &[&setup.creator]) + + // Create Address Lookup Table for the initialize transaction + let lut_addresses = extract_lut_addresses(&proof_result.remaining_accounts); + let lut = + create_address_lookup_table(&mut setup.env.rpc, &setup.env.payer, lut_addresses).await; + + // Add compute budget instruction + let compute_budget_ix = ComputeBudgetInstruction::set_compute_unit_limit(1_400_000); + + setup + .env + .rpc + .create_and_send_versioned_transaction( + &[compute_budget_ix, init_ix], + &setup.creator.pubkey(), + &[&setup.creator], + &[lut], + ) .await .expect("Initialize should succeed"); // Fetch pool state account - let pool_interface = setup.env.rpc - .get_account_interface(&setup.pdas.pool_state, &program_id) + let pool_interface = setup + .env + .rpc + .get_account_interface(&setup.pdas.pool_state, None) .await - .expect("get_account_interface should succeed"); + .expect("get_account_interface should succeed") + .value + .expect("pool account should exist"); // Create SDK from fetched account let sdk = CpSwapSdk::from_keyed_accounts(&[pool_interface]) @@ -270,13 +305,25 @@ async fn test_sdk_from_keyed_accounts() { // Check account requirements for each instruction type let swap_accounts = sdk.get_accounts_to_update(&CpSwapInstruction::Swap); - assert_eq!(swap_accounts.len(), 6, "Swap needs 6 accounts: pool, observation, vault0, vault1, mint0, mint1"); + assert_eq!( + swap_accounts.len(), + 6, + "Swap needs 6 accounts: pool, observation, vault0, vault1, mint0, mint1" + ); let deposit_accounts = sdk.get_accounts_to_update(&CpSwapInstruction::Deposit); - assert_eq!(deposit_accounts.len(), 7, "Deposit needs 7 accounts: pool, observation, vault0, vault1, lp_mint, mint0, mint1"); + assert_eq!( + deposit_accounts.len(), + 7, + "Deposit needs 7 accounts: pool, observation, vault0, vault1, lp_mint, mint0, mint1" + ); let withdraw_accounts = sdk.get_accounts_to_update(&CpSwapInstruction::Withdraw); - assert_eq!(withdraw_accounts.len(), 7, "Withdraw needs 7 accounts: pool, observation, vault0, vault1, lp_mint, mint0, mint1"); + assert_eq!( + withdraw_accounts.len(), + 7, + "Withdraw needs 7 accounts: pool, observation, vault0, vault1, lp_mint, mint0, mint1" + ); // Verify program_id method assert_eq!(sdk.program_id(), program_id); diff --git a/programs/cp-swap/tests/helpers.rs b/programs/cp-swap/tests/helpers.rs index b0e4b32..78cb74d 100644 --- a/programs/cp-swap/tests/helpers.rs +++ b/programs/cp-swap/tests/helpers.rs @@ -1,50 +1,77 @@ -#![allow(dead_code)] +#![allow(dead_code, clippy::too_many_arguments, clippy::useless_vec)] /// Functional integration test for cp-swap program. -/// Tests pool initialization with light-program-test framework. - +/// Tests pool initialization with light test-validator and photon indexer. use anchor_lang::{InstructionData, ToAccountMetas}; -use light_client::interface::{ - get_create_accounts_proof, CreateAccountsProofInput, CreateAccountsProofResult, - InitializeRentFreeConfig, +use forester_utils::forester_epoch::get_epoch_phases; +use light_anchor_spl::memo::spl_memo; +use light_client::{ + indexer::Indexer, + interface::{ + get_create_accounts_proof, instructions::build_compress_accounts_idempotent, + instructions::COMPRESS_ACCOUNTS_IDEMPOTENT_DISCRIMINATOR, CreateAccountsProofInput, + CreateAccountsProofResult, InitializeRentFreeConfig, LightConfig, + }, + rpc::{ + lut::{instruction as lut_instruction, load_lookup_table}, + LightClient, LightClientConfig, Rpc, + }, }; -use solana_pubkey::pubkey; -use light_program_test::{ - program_test::{setup_mock_program_data, LightProgramTest, TestRpc}, - Indexer, ProgramTestConfig, Rpc, +use light_program_test::accounts::test_keypairs::PAYER_KEYPAIR; +use light_registry::{ + protocol_config::state::ProtocolConfigPda, + sdk::{ + create_finalize_registration_instruction, create_register_forester_epoch_pda_instruction, + create_register_forester_instruction, + }, + utils::{get_forester_pda, get_protocol_config_pda_address}, + ForesterConfig as RegistryForesterConfig, }; +use light_sdk::compressed_account::derive_address; +use light_sdk::light_account_checks::discriminator::DISCRIMINATOR_LEN; +use light_sdk::LightDiscriminator; use light_token::{ constants::CPI_AUTHORITY_PDA, + constants::LIGHT_TOKEN_PROGRAM_ID, instruction::{ find_mint_address, get_associated_token_address_and_bump, CreateAssociatedTokenAccount, - CreateMint, CreateMintParams, MintTo, COMPRESSIBLE_CONFIG_V1, - RENT_SPONSOR as LIGHT_TOKEN_RENT_SPONSOR, + CreateMint, CreateMintParams, MintTo, LIGHT_TOKEN_CONFIG, LIGHT_TOKEN_RENT_SPONSOR, }, - constants::LIGHT_TOKEN_PROGRAM_ID, }; use raydium_cp_swap::{ instructions::initialize::LP_MINT_SIGNER_SEED, - states::{AMM_CONFIG_SEED, OBSERVATION_SEED, POOL_SEED, POOL_VAULT_SEED}, + program_rent_sponsor, + states::{ + ObservationState, PoolState, AMM_CONFIG_SEED, OBSERVATION_SEED, POOL_SEED, POOL_VAULT_SEED, + }, InitializeParams, AUTH_SEED, }; -use solana_instruction::Instruction; +use solana_commitment_config::CommitmentConfig; +use solana_instruction::{AccountMeta, Instruction}; use solana_keypair::Keypair; +use solana_message::AddressLookupTableAccount; use solana_pubkey::Pubkey; -use solana_signer::Signer; use solana_sdk::{program_pack::Pack, signature::SeedDerivable}; -use light_anchor_spl::memo::spl_memo; -use spl_token_2022; +use solana_signer::Signer; +use tokio::sync::OnceCell; +static VALIDATOR_INIT: OnceCell<()> = OnceCell::const_new(); +static VALIDATOR_WITH_FORESTER_INIT: OnceCell<()> = OnceCell::const_new(); +/// BPF Loader Upgradeable program ID +const BPF_LOADER_UPGRADEABLE: Pubkey = + solana_pubkey::pubkey!("BPFLoaderUpgradeab1e11111111111111111111111"); // ============================================================================ // Constants // ============================================================================ -const RENT_SPONSOR: Pubkey = pubkey!("CLEuMG7pzJX9xAuKCFzBP154uiG1GaNo4Fq7x6KAcAfG"); +fn rent_sponsor() -> Pubkey { + program_rent_sponsor() +} pub fn light_token_program_id() -> Pubkey { - Pubkey::from(LIGHT_TOKEN_PROGRAM_ID) + LIGHT_TOKEN_PROGRAM_ID } // ============================================================================ @@ -67,7 +94,7 @@ pub struct AmmPdas { /// Test environment setup result. pub struct TestEnv { - pub rpc: LightProgramTest, + pub rpc: LightClient, pub payer: Keypair, pub config_pda: Pubkey, } @@ -86,29 +113,600 @@ pub struct TokenSetup { // Setup Functions // ============================================================================ -/// Initialize the test environment with LightProgramTest and compression config. +/// Get the payer keypair used for test-validator deployment. +/// This must match the upgrade authority used when spawning the validator. +pub fn get_payer_keypair() -> Keypair { + Keypair::try_from(PAYER_KEYPAIR.as_ref()).expect("Invalid PAYER_KEYPAIR") +} + +/// Create the pool fee receiver account data file for preloading. +/// Returns the path to the JSON file. +fn create_pool_fee_receiver_account_file() -> String { + use base64::{engine::general_purpose::STANDARD, Engine}; + use serde_json::json; + use std::fs; + use std::path::PathBuf; + + let wsol_mint = spl_token::native_mint::id(); + let payer = get_payer_keypair(); + + // Create a wrapped SOL token account structure + // Token account layout: mint (32) + owner (32) + amount (8) + delegate (36) + state (1) + ... + let mut data = vec![0u8; spl_token::state::Account::LEN]; + + // Set mint (first 32 bytes) + data[0..32].copy_from_slice(wsol_mint.as_ref()); + // Set owner (next 32 bytes) + data[32..64].copy_from_slice(payer.pubkey().as_ref()); + // Set amount (next 8 bytes) - 1 SOL in lamports + let amount: u64 = 1_000_000_000; + data[64..72].copy_from_slice(&amount.to_le_bytes()); + // Set delegate option to None (4 bytes = 0) + data[72..76].copy_from_slice(&[0u8; 4]); + // Skip delegate pubkey (32 bytes) + // Set state to Initialized (1 byte = 1) + data[108] = 1; + // Rest is zeros (is_native, delegated_amount, close_authority) + + // Create JSON in the format expected by solana-test-validator --account + let account_json = json!({ + "pubkey": raydium_cp_swap::create_pool_fee_receiver::ID.to_string(), + "account": { + "lamports": 2_000_000_000u64, // 2 SOL for rent + balance + "data": [STANDARD.encode(&data), "base64"], + "owner": spl_token::id().to_string(), + "executable": false, + "rentEpoch": 0 + } + }); + + // Write to temp file + let tmp_dir = std::env::temp_dir(); + let file_path: PathBuf = tmp_dir.join("pool_fee_receiver.json"); + fs::write( + &file_path, + serde_json::to_string_pretty(&account_json).unwrap(), + ) + .expect("Failed to write pool fee receiver account file"); + + file_path.to_string_lossy().to_string() +} + +/// Check if validator is already running by testing RPC endpoint. +async fn is_validator_running() -> bool { + use reqwest::Client; + let client = Client::new(); + match client + .post("http://localhost:8899") + .header("Content-Type", "application/json") + .body(r#"{"jsonrpc":"2.0","id":1,"method":"getHealth"}"#) + .send() + .await + { + Ok(resp) => resp + .text() + .await + .map(|t| t.contains("\"ok\"")) + .unwrap_or(false), + Err(_) => false, + } +} + +/// Spawn the test-validator with cp-swap program deployed. +/// This is called once per test run via Once. +/// If validator is already running (e.g., started by justfile), skip spawning. +async fn ensure_validator_running(program_id: Pubkey) { + use std::process::{Command, Stdio}; + + // Check if validator is already running + if is_validator_running().await { + println!("Validator already running, skipping spawn"); + return; + } + + // Stop any existing validator first + println!("Stopping any existing validator..."); + let _ = Command::new("light") + .args(["test-validator", "--stop"]) + .output(); + + // Give it a moment to clean up + tokio::time::sleep(tokio::time::Duration::from_secs(2)).await; + + // Clean up old ledger to ensure fresh state + let _ = std::fs::remove_dir_all("test-ledger"); + + // Get the path to the compiled program (use absolute path) + // Look up from cwd to find the target/deploy directory + let program_path = std::env::var("SBF_OUT_DIR") + .map(|dir| format!("{}/raydium_cp_swap.so", dir)) + .unwrap_or_else(|_| { + let cwd = std::env::current_dir().expect("Failed to get current directory"); + // Try multiple possible locations + let candidates = vec![ + cwd.join("target/deploy/raydium_cp_swap.so"), + cwd.join("../../target/deploy/raydium_cp_swap.so"), // From programs/cp-swap + cwd.parent() + .unwrap() + .join("target/deploy/raydium_cp_swap.so"), + cwd.parent() + .unwrap() + .parent() + .unwrap() + .join("target/deploy/raydium_cp_swap.so"), + ]; + candidates + .iter() + .find(|p| p.exists()) + .expect("Could not find raydium_cp_swap.so - run `cargo build-sbf` first") + .canonicalize() + .expect("Failed to canonicalize program path") + .to_string_lossy() + .to_string() + }); + + let payer = get_payer_keypair(); + + // Find the accounts directory containing the fee receiver account JSON + let cwd = std::env::current_dir().expect("Failed to get current directory"); + let accounts_dir = cwd + .join("programs/cp-swap/tests/accounts") + .canonicalize() + .unwrap_or_else(|_| cwd.join("tests/accounts")); + + // Build the command using the light CLI from PATH + // Use --account-dir to load the fee receiver account + let cmd = format!( + "light test-validator \ + --limit-ledger-size 50000000 \ + --upgradeable-program {} {} {} \ + --account-dir {}", + program_id, + program_path, + payer.pubkey(), + accounts_dir.display() + ); + + println!("Starting validator with command: {}", cmd); + + let child = Command::new("sh") + .arg("-c") + .arg(&cmd) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .spawn() + .expect("Failed to start validator process"); + + // Detach the process + std::mem::drop(child); + + // Wait for validator to start + tokio::time::sleep(tokio::time::Duration::from_secs(60)).await; +} + +/// Spawn the test-validator with cp-swap program deployed AND forester for auto-compression. +/// This is for tests that need to verify compression behavior. +#[allow(dead_code)] +async fn ensure_validator_running_with_forester(program_id: Pubkey) { + use std::process::{Command, Stdio}; + + // Check if validator is already running + if is_validator_running().await { + println!("Validator already running, skipping spawn"); + return; + } + + // Stop any existing validator first + println!("Stopping any existing validator..."); + let _ = Command::new("light") + .args(["test-validator", "--stop"]) + .output(); + + // Give it a moment to clean up + tokio::time::sleep(tokio::time::Duration::from_secs(2)).await; + + // Clean up old ledger to ensure fresh state + let _ = std::fs::remove_dir_all("test-ledger"); + + // Get the path to the compiled program (use absolute path) + let program_path = std::env::var("SBF_OUT_DIR") + .map(|dir| format!("{}/raydium_cp_swap.so", dir)) + .unwrap_or_else(|_| { + let cwd = std::env::current_dir().expect("Failed to get current directory"); + let candidates = [ + cwd.join("target/deploy/raydium_cp_swap.so"), + cwd.join("../../target/deploy/raydium_cp_swap.so"), + cwd.parent() + .unwrap() + .join("target/deploy/raydium_cp_swap.so"), + cwd.parent() + .unwrap() + .parent() + .unwrap() + .join("target/deploy/raydium_cp_swap.so"), + ]; + candidates + .iter() + .find(|p| p.exists()) + .expect("Could not find raydium_cp_swap.so - run `cargo build-sbf` first") + .canonicalize() + .expect("Failed to canonicalize program path") + .to_string_lossy() + .to_string() + }); + + let payer = get_payer_keypair(); + + // Find the accounts directory containing the fee receiver account JSON + let cwd = std::env::current_dir().expect("Failed to get current directory"); + let accounts_dir = cwd + .join("programs/cp-swap/tests/accounts") + .canonicalize() + .unwrap_or_else(|_| cwd.join("tests/accounts")); + + // Get discriminators from the actual types via LightDiscriminator trait + let pool_state_disc_b58 = bs58::encode(PoolState::LIGHT_DISCRIMINATOR).into_string(); + let observation_state_disc_b58 = + bs58::encode(ObservationState::LIGHT_DISCRIMINATOR).into_string(); + + // Build the command with forester enabled + // Format for compressible-pda-program: 'program_id:discriminator_base58' + // Note: We use a very short --slots-per-epoch (32) so accounts become compressible quickly. + // With max_funded_epochs=2 in RentConfig::default(), accounts become compressible after 2 epochs. + // Using 32 slots/epoch means accounts become compressible after ~64 slots (~26 seconds at 400ms/slot). + let slots_per_epoch = 32; + // Note: Use --no-use-surfpool for forester mode because forester requires solana-test-validator + let cmd = format!( + "light test-validator \ + --limit-ledger-size 50000000 \ + --upgradeable-program {} {} {} \ + --forester \ + --no-use-surfpool \ + --compressible-pda-program {}:{} \ + --compressible-pda-program {}:{} \ + --account-dir {} \ + --validator-args '--slots-per-epoch {}'", + program_id, + program_path, + payer.pubkey(), + program_id, + pool_state_disc_b58, + program_id, + observation_state_disc_b58, + accounts_dir.display(), + slots_per_epoch + ); + + println!("Starting validator with forester: {}", cmd); + + let child = Command::new("sh") + .arg("-c") + .arg(&cmd) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .spawn() + .expect("Failed to start validator process"); + + // Detach the process + std::mem::drop(child); + + // Wait for validator to start (forester takes longer to start) + tokio::time::sleep(tokio::time::Duration::from_secs(90)).await; +} + +/// Register the forester with the Light Registry protocol. +/// This is required for the forester to perform compression operations. +async fn register_forester(rpc: &mut LightClient) -> Result> { + use solana_sdk::transaction::Transaction; + use std::time::Duration; + + let forester_keypair = get_payer_keypair(); + let forester_pubkey = forester_keypair.pubkey(); + + // Governance authority is the same as PAYER_KEYPAIR in tests + let governance_authority = get_payer_keypair(); + let governance_pubkey = governance_authority.pubkey(); + + // Fund governance authority if needed + let gov_balance = rpc.get_balance(&governance_pubkey).await.unwrap_or(0); + if gov_balance < 10_000_000_000 { + println!( + "Funding governance authority {} with 10 SOL", + governance_pubkey + ); + rpc.airdrop_lamports(&governance_pubkey, 10_000_000_000 - gov_balance) + .await?; + tokio::time::sleep(Duration::from_millis(500)).await; + } + + // Fund forester + let forester_balance = rpc.get_balance(&forester_pubkey).await.unwrap_or(0); + if forester_balance < 10_000_000_000 { + println!("Funding forester {} with 10 SOL", forester_pubkey); + rpc.airdrop_lamports(&forester_pubkey, 10_000_000_000 - forester_balance) + .await?; + tokio::time::sleep(Duration::from_millis(500)).await; + } + + // Get protocol config + let protocol_config_pda_address = get_protocol_config_pda_address().0; + let protocol_config = rpc + .get_anchor_account::(&protocol_config_pda_address) + .await? + .ok_or("Protocol config not found")? + .config; + + // Check if forester is already registered + let (forester_pda, _) = get_forester_pda(&forester_pubkey); + let existing_forester = rpc.get_account(forester_pda).await.ok().flatten(); + + if existing_forester.is_none() { + // Register base forester + let register_ix = create_register_forester_instruction( + &governance_pubkey, + &governance_pubkey, + &forester_pubkey, + RegistryForesterConfig::default(), + ); + + let (blockhash, _) = rpc.get_latest_blockhash().await?; + let tx = Transaction::new_signed_with_payer( + &[register_ix], + Some(&governance_pubkey), + &[&governance_authority], + blockhash, + ); + rpc.process_transaction(tx).await?; + println!("Registered base forester: {}", forester_pda); + } else { + println!("Forester already registered: {}", forester_pda); + } + + // Determine which epoch to register for + let current_slot = rpc.get_slot().await?; + let current_epoch = protocol_config.get_current_epoch(current_slot); + let phases = get_epoch_phases(&protocol_config, current_epoch); + + println!( + "Current slot: {}, current_epoch: {}, phases: {:?}", + current_slot, current_epoch, phases + ); + + let (target_epoch, register_phase_start, active_phase_start) = + if current_slot >= phases.active.start { + let next_epoch = current_epoch + 1; + let next_phases = get_epoch_phases(&protocol_config, next_epoch); + println!( + "Already in active phase, registering for next epoch {}, phases: {:?}", + next_epoch, next_phases + ); + ( + next_epoch, + next_phases.registration.start, + next_phases.active.start, + ) + } else if current_slot >= phases.registration.start { + println!("In registration phase for epoch {}", current_epoch); + ( + current_epoch, + phases.registration.start, + phases.active.start, + ) + } else { + println!( + "Waiting for registration phase (starts at slot {})", + phases.registration.start + ); + ( + current_epoch, + phases.registration.start, + phases.active.start, + ) + }; + + // Wait for registration phase + while rpc.get_slot().await? < register_phase_start { + tokio::time::sleep(Duration::from_millis(400)).await; + } + + // Register for the target epoch + let register_epoch_ix = create_register_forester_epoch_pda_instruction( + &forester_pubkey, + &forester_pubkey, + target_epoch, + ); + + let (blockhash, _) = rpc.get_latest_blockhash().await?; + let tx = Transaction::new_signed_with_payer( + &[register_epoch_ix], + Some(&forester_pubkey), + &[&forester_keypair], + blockhash, + ); + rpc.process_transaction(tx).await?; + println!("Registered for epoch {}", target_epoch); + + // Wait for active phase + while rpc.get_slot().await? < active_phase_start { + tokio::time::sleep(Duration::from_millis(400)).await; + } + println!("Active phase reached for epoch {}", target_epoch); + + // Finalize registration + let finalize_ix = + create_finalize_registration_instruction(&forester_pubkey, &forester_pubkey, target_epoch); + + let (blockhash, _) = rpc.get_latest_blockhash().await?; + let tx = Transaction::new_signed_with_payer( + &[finalize_ix], + Some(&forester_pubkey), + &[&forester_keypair], + blockhash, + ); + rpc.process_transaction(tx).await?; + println!("Finalized forester registration for epoch {}", target_epoch); + + Ok(forester_keypair) +} + +/// Wait for the indexer to be synced with the RPC. +pub async fn wait_for_indexer(rpc: &LightClient) -> Result<(), String> { + let max_attempts = 120; // Increase max attempts for slower startup + for attempt in 0..max_attempts { + // First check if RPC is responding + let rpc_slot = match rpc.get_slot().await { + Ok(slot) => slot, + Err(e) => { + if attempt % 10 == 0 { + println!("Waiting for RPC... attempt {}: {}", attempt, e); + } + tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; + continue; + } + }; + + // Then check indexer + let indexer = match rpc.indexer() { + Ok(i) => i, + Err(e) => { + if attempt % 10 == 0 { + println!( + "Waiting for indexer connection... attempt {}: {}", + attempt, e + ); + } + tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; + continue; + } + }; + + let indexer_slot = match indexer.get_indexer_slot(None).await { + Ok(slot) => slot, + Err(e) => { + if attempt % 10 == 0 { + println!("Waiting for indexer slot... attempt {}: {}", attempt, e); + } + tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; + continue; + } + }; + + if indexer_slot >= rpc_slot.saturating_sub(5) { + println!( + "Indexer synced! RPC slot: {}, Indexer slot: {}", + rpc_slot, indexer_slot + ); + return Ok(()); + } + + if attempt % 10 == 0 { + println!( + "Waiting for indexer to sync... RPC slot: {}, Indexer slot: {}", + rpc_slot, indexer_slot + ); + } + tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; + } + Err("Indexer did not sync in time".to_string()) +} + +/// Initialize the test environment with test-validator and photon indexer. pub async fn setup_test_environment(program_id: Pubkey) -> TestEnv { - let mut config = - ProgramTestConfig::new_v2(true, Some(vec![("raydium_cp_swap", program_id)])); - config = config.with_light_protocol_events(); + setup_test_environment_inner(program_id, false).await +} + +/// Initialize the test environment with test-validator, photon indexer, AND forester. +/// Use this for tests that need auto-compression. +pub async fn setup_test_environment_with_forester(program_id: Pubkey) -> TestEnv { + setup_test_environment_inner(program_id, true).await +} - let mut rpc = LightProgramTest::new(config).await.unwrap(); - let payer = rpc.get_payer().insecure_clone(); +/// Internal helper for test environment setup. +async fn setup_test_environment_inner(program_id: Pubkey, with_forester: bool) -> TestEnv { + // Ensure validator is running (only spawns once per variant) + if with_forester { + VALIDATOR_WITH_FORESTER_INIT + .get_or_init(|| async { + ensure_validator_running_with_forester(program_id).await; + }) + .await; + } else { + VALIDATOR_INIT + .get_or_init(|| async { + ensure_validator_running(program_id).await; + }) + .await; + } + + // Connect to the running validator + let config = LightClientConfig::local(); + let mut rpc = ::new(config) + .await + .expect("Failed to connect to validator"); + + // Wait for indexer to be synced before proceeding + wait_for_indexer(&rpc).await.expect("Indexer should sync"); + + // Fetch state trees from the validator (populates the internal cache) + rpc.get_latest_active_state_trees() + .await + .expect("Failed to fetch state trees"); + + // Use the payer keypair that was used as upgrade authority + // Note: This is also the forester's payer keypair (from ~/.config/solana/id.json) + let payer = get_payer_keypair(); + + // Fund the payer with enough SOL for both tests AND forester operations + // Forester needs SOL to call compress_accounts_idempotent + rpc.airdrop_lamports(&payer.pubkey(), 100_000_000_000) + .await + .expect("Airdrop to payer should succeed"); + + // Register forester with the Light Registry protocol + if with_forester { + println!("Forester logs available at: test-ledger/forester.log"); + println!("Forester payer (same as test payer): {}", payer.pubkey()); - let program_data_pda = setup_mock_program_data(&mut rpc, &payer, &program_id); + // Register forester so it can perform compression operations + register_forester(&mut rpc) + .await + .expect("Forester registration should succeed"); + } + + // Derive program_data_pda from BPF loader + let (program_data_pda, _) = + Pubkey::find_program_address(&[program_id.as_ref()], &BPF_LOADER_UPGRADEABLE); let (init_config_ix, config_pda) = InitializeRentFreeConfig::new( &program_id, &payer.pubkey(), &program_data_pda, - RENT_SPONSOR, + rent_sponsor(), payer.pubkey(), ) .build(); - rpc.create_and_send_transaction(&[init_config_ix], &payer.pubkey(), &[&payer]) + // Check if config already exists (idempotent initialization for test reruns) + let config_exists = rpc.get_account(config_pda).await.ok().flatten().is_some(); + if !config_exists { + rpc.create_and_send_transaction(&[init_config_ix], &payer.pubkey(), &[&payer]) + .await + .expect("Initialize config should succeed"); + } else { + println!("Config already initialized at {}", config_pda); + } + + // Fund the rent sponsor PDA so it can pay for rent reimbursements + rpc.airdrop_lamports(&rent_sponsor(), 1_000_000_000) + .await + .expect("Airdrop to rent sponsor should succeed"); + + // Wait for indexer to sync + wait_for_indexer(&rpc) .await - .expect("Initialize config should succeed"); + .expect("Failed to wait for indexer"); TestEnv { rpc, @@ -119,7 +717,7 @@ pub async fn setup_test_environment(program_id: Pubkey) -> TestEnv { /// Create a compressed mint with ATAs for recipients. pub async fn setup_create_mint( - rpc: &mut LightProgramTest, + rpc: &mut LightClient, payer: &Keypair, mint_authority: Pubkey, decimals: u8, @@ -202,6 +800,7 @@ pub async fn setup_create_mint( amount: *amount, authority: mint_authority, max_top_up: None, + fee_payer: None, } .instruction() .unwrap(); @@ -217,7 +816,7 @@ pub async fn setup_create_mint( /// Create token mints and fund creator with initial balances. pub async fn setup_token_mints( - rpc: &mut LightProgramTest, + rpc: &mut LightClient, payer: &Keypair, creator: &Pubkey, initial_balance: u64, @@ -275,7 +874,7 @@ pub fn get_admin_keypair() -> Keypair { } pub async fn create_amm_config( - rpc: &mut LightProgramTest, + rpc: &mut LightClient, payer: &Keypair, admin: &Keypair, program_id: Pubkey, @@ -286,6 +885,18 @@ pub async fn create_amm_config( &program_id, ); + // Check if already exists (idempotent for test reruns with persisted ledger) + if rpc + .get_account(amm_config_pda) + .await + .ok() + .flatten() + .is_some() + { + println!("AmmConfig already exists at {}", amm_config_pda); + return amm_config_pda; + } + let create_config_accounts = raydium_cp_swap::accounts::CreateAmmConfig { owner: admin.pubkey(), amm_config: amm_config_pda, @@ -314,32 +925,28 @@ pub async fn create_amm_config( } /// Setup the create_pool_fee account (wrapped SOL token account). -pub fn setup_create_pool_fee_account(rpc: &mut LightProgramTest, owner: &Pubkey) { +/// This account is preloaded via validator_args when spawning the validator. +/// This function now just verifies the account exists. +pub async fn setup_create_pool_fee_account( + rpc: &mut LightClient, + _payer: &Keypair, + _owner: &Pubkey, +) { let create_pool_fee_receiver = raydium_cp_swap::create_pool_fee_receiver::ID; - let wsol_mint = spl_token::native_mint::id(); - let mut fee_receiver_data = vec![0u8; spl_token::state::Account::LEN]; - let fee_account = spl_token::state::Account { - mint: wsol_mint, - owner: *owner, - amount: 0, - delegate: solana_sdk::program_option::COption::None, - state: spl_token::state::AccountState::Initialized, - is_native: solana_sdk::program_option::COption::Some(0), - delegated_amount: 0, - close_authority: solana_sdk::program_option::COption::None, - }; - spl_token::state::Account::pack(fee_account, &mut fee_receiver_data).unwrap(); - - rpc.set_account( - create_pool_fee_receiver, - solana_sdk::account::Account { - lamports: 1_000_000_000, - data: fee_receiver_data, - owner: spl_token::id(), - executable: false, - rent_epoch: 0, - }, + // Check if account already exists (should be preloaded by validator) + if let Ok(Some(_)) = rpc.get_account(create_pool_fee_receiver).await { + println!( + "Pool fee receiver account exists at {}", + create_pool_fee_receiver + ); + return; + } + + // Account was not preloaded - this is a problem + panic!( + "Pool fee receiver account at {} was not preloaded. Check validator_args configuration.", + create_pool_fee_receiver ); } @@ -420,7 +1027,7 @@ pub fn derive_amm_pdas( /// Get the create accounts proof for pool initialization. pub async fn get_pool_create_accounts_proof( - rpc: &LightProgramTest, + rpc: &LightClient, program_id: &Pubkey, pdas: &AmmPdas, ) -> CreateAccountsProofResult { @@ -437,6 +1044,66 @@ pub async fn get_pool_create_accounts_proof( .unwrap() } +// ============================================================================ +// Address Lookup Table Helpers +// ============================================================================ + +/// Create an Address Lookup Table containing the given addresses. +/// Returns the LUT address and the AddressLookupTableAccount. +pub async fn create_address_lookup_table( + rpc: &mut LightClient, + payer: &Keypair, + addresses: Vec, +) -> AddressLookupTableAccount { + // Wait a moment and then get a recent slot for LUT derivation + // This helps ensure we get a slot that's actually in the SlotHashes sysvar + tokio::time::sleep(tokio::time::Duration::from_millis(1000)).await; + + // Get slot with finalized commitment from the underlying client + let recent_slot = rpc + .client + .get_slot_with_commitment(CommitmentConfig::finalized()) + .expect("Failed to get finalized slot"); + + println!("Creating LUT with recent_slot: {}", recent_slot); + + // Create the lookup table + let (create_ix, lut_address) = + lut_instruction::create_lookup_table(payer.pubkey(), payer.pubkey(), recent_slot); + + rpc.create_and_send_transaction(&[create_ix], &payer.pubkey(), &[payer]) + .await + .expect("Failed to create lookup table"); + + // Wait a bit for the table to be created + tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; + + // Extend the lookup table with addresses (max 30 addresses per extend) + for chunk in addresses.chunks(30) { + let extend_ix = lut_instruction::extend_lookup_table( + lut_address, + payer.pubkey(), + Some(payer.pubkey()), + chunk.to_vec(), + ); + + rpc.create_and_send_transaction(&[extend_ix], &payer.pubkey(), &[payer]) + .await + .expect("Failed to extend lookup table"); + + // Wait for extension to be processed + tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; + } + + // Load and return the lookup table + load_lookup_table(&rpc.client, &lut_address).expect("Failed to load lookup table") +} + +/// Extract unique pubkeys from remaining accounts for LUT creation. +pub fn extract_lut_addresses(remaining_accounts: &[AccountMeta]) -> Vec { + remaining_accounts.iter().map(|acc| acc.pubkey).collect() +} + /// Build the Withdraw instruction. pub fn build_withdraw_instruction( program_id: Pubkey, @@ -632,8 +1299,9 @@ pub fn build_initialize_instruction( system_program: solana_sdk::system_program::ID, rent: solana_sdk::sysvar::rent::ID, compression_config: config_pda, - light_token_compressible_config: Pubkey::from(COMPRESSIBLE_CONFIG_V1), - light_token_rent_sponsor: Pubkey::from(LIGHT_TOKEN_RENT_SPONSOR), + light_token_config: LIGHT_TOKEN_CONFIG, + pda_rent_sponsor: raydium_cp_swap::program_rent_sponsor(), + light_token_rent_sponsor: LIGHT_TOKEN_RENT_SPONSOR, light_token_program: light_token_program_id(), light_token_cpi_authority: CPI_AUTHORITY_PDA, }; @@ -659,7 +1327,7 @@ pub fn build_initialize_instruction( /// Get the balance of a token account. /// Supports both SPL Token and Light Token accounts. -pub async fn get_token_balance(rpc: &mut LightProgramTest, account: Pubkey) -> u64 { +pub async fn get_token_balance(rpc: &mut LightClient, account: Pubkey) -> u64 { let account_data = rpc.get_account(account).await.unwrap(); if let Some(account) = account_data { // Token account layout: mint (32) + owner (32) + amount (8) @@ -677,7 +1345,7 @@ pub async fn get_token_balance(rpc: &mut LightProgramTest, account: Pubkey) -> u } /// Assert that an account exists on-chain. -pub async fn assert_onchain_exists(rpc: &mut LightProgramTest, pda: &Pubkey) { +pub async fn assert_onchain_exists(rpc: &mut LightClient, pda: &Pubkey) { assert!( rpc.get_account(*pda).await.unwrap().is_some(), "Account {} should exist on-chain", @@ -686,7 +1354,7 @@ pub async fn assert_onchain_exists(rpc: &mut LightProgramTest, pda: &Pubkey) { } /// Assert that an account is closed (doesn't exist or has 0 lamports). -pub async fn assert_onchain_closed(rpc: &mut LightProgramTest, pda: &Pubkey) { +pub async fn assert_onchain_closed(rpc: &mut LightClient, pda: &Pubkey) { let acc = rpc.get_account(*pda).await.unwrap(); assert!( acc.is_none() || acc.unwrap().lamports == 0, @@ -696,7 +1364,11 @@ pub async fn assert_onchain_closed(rpc: &mut LightProgramTest, pda: &Pubkey) { } /// Assert all pool accounts exist on-chain (hot or decompressed state). -pub async fn assert_pool_accounts_exist(rpc: &mut LightProgramTest, pdas: &AmmPdas, tokens: &TokenSetup) { +pub async fn assert_pool_accounts_exist( + rpc: &mut LightClient, + pdas: &AmmPdas, + tokens: &TokenSetup, +) { assert_onchain_exists(rpc, &pdas.pool_state).await; assert_onchain_exists(rpc, &pdas.observation_state).await; assert_onchain_exists(rpc, &pdas.lp_mint).await; @@ -708,7 +1380,11 @@ pub async fn assert_pool_accounts_exist(rpc: &mut LightProgramTest, pdas: &AmmPd } /// Assert all pool accounts are compressed (closed on-chain). -pub async fn assert_pool_accounts_compressed(rpc: &mut LightProgramTest, pdas: &AmmPdas, tokens: &TokenSetup) { +pub async fn assert_pool_accounts_compressed( + rpc: &mut LightClient, + pdas: &AmmPdas, + tokens: &TokenSetup, +) { assert_onchain_closed(rpc, &pdas.pool_state).await; assert_onchain_closed(rpc, &pdas.observation_state).await; assert_onchain_closed(rpc, &pdas.lp_mint).await; @@ -720,7 +1396,7 @@ pub async fn assert_pool_accounts_compressed(rpc: &mut LightProgramTest, pdas: & } /// Verify that the pool was initialized correctly. -pub async fn assert_pool_initialized(rpc: &mut LightProgramTest, pdas: &AmmPdas) { +pub async fn assert_pool_initialized(rpc: &mut LightClient, pdas: &AmmPdas) { let pool_account = rpc.get_account(pdas.pool_state).await.unwrap(); assert!(pool_account.is_some(), "Pool state should exist"); @@ -733,7 +1409,7 @@ pub async fn assert_pool_initialized(rpc: &mut LightProgramTest, pdas: &AmmPdas) /// Assert that deposit succeeded by checking LP token balance increased. pub async fn assert_deposit_succeeded( - rpc: &mut LightProgramTest, + rpc: &mut LightClient, owner_lp_token: Pubkey, lp_balance_before: u64, expected_lp_increase: u64, @@ -752,7 +1428,7 @@ pub async fn assert_deposit_succeeded( /// Assert that swap succeeded by checking balances changed correctly. pub async fn assert_swap_succeeded( - rpc: &mut LightProgramTest, + rpc: &mut LightClient, input_account: Pubkey, output_account: Pubkey, input_balance_before: u64, @@ -784,7 +1460,7 @@ pub async fn assert_swap_succeeded( /// Assert that withdraw succeeded by checking LP token balance decreased. pub async fn assert_withdraw_succeeded( - rpc: &mut LightProgramTest, + rpc: &mut LightClient, owner_lp_token: Pubkey, lp_balance_before: u64, expected_lp_decrease: u64, @@ -799,11 +1475,105 @@ pub async fn assert_withdraw_succeeded( } /// Verify that the AMM config was created. -pub async fn assert_amm_config_created(rpc: &mut LightProgramTest, amm_config: Pubkey) { +pub async fn assert_amm_config_created(rpc: &mut LightClient, amm_config: Pubkey) { let account = rpc.get_account(amm_config).await.unwrap(); assert!(account.is_some(), "AmmConfig account should exist"); } +// ============================================================================ +// Manual Compression Helpers +// ============================================================================ + +/// Manually compress a PDA account using compress_accounts_idempotent instruction. +/// This bypasses the forester timing check and compresses the account immediately. +/// Useful for testing when you can't wait for the forester's epoch-based timing. +pub async fn compress_pda_account( + rpc: &mut LightClient, + payer: &Keypair, + program_id: &Pubkey, + pda_pubkey: &Pubkey, + config_pda: &Pubkey, +) -> Result<(), Box> { + use anchor_lang::AnchorDeserialize; + + // Get the LightConfig to find rent_sponsor and address_tree + let cfg_acc = rpc + .get_account(*config_pda) + .await? + .ok_or("Config account not found")?; + let cfg = LightConfig::deserialize(&mut &cfg_acc.data[DISCRIMINATOR_LEN..]) + .map_err(|e| format!("Failed to deserialize config: {:?}", e))?; + + let rent_sponsor = cfg.rent_sponsor; + let compression_authority = payer.pubkey(); + let address_tree = cfg.address_space[0]; + + // Derive the compressed address + let compressed_address = derive_address( + &pda_pubkey.to_bytes(), + &address_tree.to_bytes(), + &program_id.to_bytes(), + ); + + // Get the compressed account from indexer + let compressed_account = rpc + .get_compressed_account(compressed_address, None) + .await? + .value + .ok_or_else(|| format!("Compressed account not found for PDA {}", pda_pubkey))?; + + // Get validity proof + let proof_with_context = rpc + .get_validity_proof(vec![compressed_account.hash], vec![], None) + .await? + .value; + + // Build program metas for compress_accounts_idempotent + let program_metas = vec![ + AccountMeta::new(payer.pubkey(), true), // fee_payer + AccountMeta::new_readonly(*config_pda, false), // config + AccountMeta::new(rent_sponsor, false), // rent_sponsor + AccountMeta::new_readonly(compression_authority, false), // compression_authority + ]; + + // Build compress instruction + let ix = build_compress_accounts_idempotent( + program_id, + &COMPRESS_ACCOUNTS_IDEMPOTENT_DISCRIMINATOR, + &[*pda_pubkey], + &program_metas, + proof_with_context, + )?; + + // Send transaction + rpc.create_and_send_transaction(&[ix], &payer.pubkey(), &[payer]) + .await?; + + println!("Compressed PDA {} successfully", pda_pubkey); + Ok(()) +} + +/// Compress multiple PDA accounts for the cp-swap pool. +/// This manually compresses pool_state and observation_state. +pub async fn compress_pool_pda_accounts( + rpc: &mut LightClient, + payer: &Keypair, + program_id: &Pubkey, + pdas: &AmmPdas, + config_pda: &Pubkey, +) -> Result<(), Box> { + // Compress pool_state + compress_pda_account(rpc, payer, program_id, &pdas.pool_state, config_pda).await?; + + // Wait for indexer to sync + tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; + + // Compress observation_state + compress_pda_account(rpc, payer, program_id, &pdas.observation_state, config_pda).await?; + + Ok(()) +} + // ============================================================================ // Unified Setup Functions for SDK-based Tests // ============================================================================ @@ -819,7 +1589,28 @@ pub struct PoolSetup { /// Setup a complete pool environment in a single call. pub async fn setup_pool_environment(program_id: Pubkey, amm_config_index: u16) -> PoolSetup { - let mut env = setup_test_environment(program_id).await; + setup_pool_environment_inner(program_id, amm_config_index, false).await +} + +/// Setup a complete pool environment with forester for auto-compression tests. +pub async fn setup_pool_environment_with_forester( + program_id: Pubkey, + amm_config_index: u16, +) -> PoolSetup { + setup_pool_environment_inner(program_id, amm_config_index, true).await +} + +/// Internal helper for pool environment setup. +async fn setup_pool_environment_inner( + program_id: Pubkey, + amm_config_index: u16, + with_forester: bool, +) -> PoolSetup { + let mut env = if with_forester { + setup_test_environment_with_forester(program_id).await + } else { + setup_test_environment(program_id).await + }; let creator = Keypair::new(); env.rpc @@ -834,10 +1625,18 @@ pub async fn setup_pool_environment(program_id: Pubkey, amm_config_index: u16) - .unwrap(); let initial_balance = 1_000_000; - let tokens = setup_token_mints(&mut env.rpc, &env.payer, &creator.pubkey(), initial_balance).await; + let tokens = + setup_token_mints(&mut env.rpc, &env.payer, &creator.pubkey(), initial_balance).await; - let amm_config = create_amm_config(&mut env.rpc, &env.payer, &admin, program_id, amm_config_index).await; - setup_create_pool_fee_account(&mut env.rpc, &env.payer.pubkey()); + let amm_config = create_amm_config( + &mut env.rpc, + &env.payer, + &admin, + program_id, + amm_config_index, + ) + .await; + setup_create_pool_fee_account(&mut env.rpc, &env.payer, &env.payer.pubkey()).await; let pdas = derive_amm_pdas( &program_id, diff --git a/programs/cp-swap/tests/helpers_local.rs b/programs/cp-swap/tests/helpers_local.rs new file mode 100644 index 0000000..4f6dd53 --- /dev/null +++ b/programs/cp-swap/tests/helpers_local.rs @@ -0,0 +1,801 @@ +//! Helpers for local tests using LightProgramTest (LiteSVM-based, no external validator). +//! These tests run faster but require less infrastructure. + +#![allow(dead_code, deprecated, clippy::too_many_arguments)] + +use anchor_lang::{InstructionData, ToAccountMetas}; +use light_anchor_spl::memo::spl_memo; +use light_client::{ + indexer::AddressWithTree, + interface::{ + get_create_accounts_proof, CreateAccountsProofInput, CreateAccountsProofResult, + InitializeRentFreeConfig, + }, +}; +use light_program_test::{ + program_test::{LightProgramTest, TestRpc}, + Indexer, ProgramTestConfig, Rpc, +}; +use light_token::{ + constants::CPI_AUTHORITY_PDA, + constants::LIGHT_TOKEN_PROGRAM_ID, + instruction::{ + find_mint_address, get_associated_token_address_and_bump, CreateAssociatedTokenAccount, + CreateMint, CreateMintParams, MintTo, LIGHT_TOKEN_CONFIG, LIGHT_TOKEN_RENT_SPONSOR, + }, +}; +use raydium_cp_swap::{ + instructions::initialize::LP_MINT_SIGNER_SEED, + program_rent_sponsor, + states::{AMM_CONFIG_SEED, OBSERVATION_SEED, POOL_SEED, POOL_VAULT_SEED}, + InitializeParams, AUTH_SEED, +}; +use solana_instruction::Instruction; +use solana_keypair::Keypair; +use solana_pubkey::Pubkey; +use solana_sdk::{bpf_loader_upgradeable, program_pack::Pack, signature::SeedDerivable}; +use solana_signer::Signer; + +// ============================================================================ +// Constants +// ============================================================================ + +fn rent_sponsor() -> Pubkey { + program_rent_sponsor() +} + +pub fn light_token_program_id() -> Pubkey { + LIGHT_TOKEN_PROGRAM_ID +} + +// ============================================================================ +// Types +// ============================================================================ + +/// PDAs for the AMM pool. +pub struct AmmPdas { + pub pool_state: Pubkey, + pub observation_state: Pubkey, + pub authority: Pubkey, + pub authority_bump: u8, + pub token_0_vault: Pubkey, + pub token_1_vault: Pubkey, + pub lp_mint_signer: Pubkey, + pub lp_mint_signer_bump: u8, + pub lp_mint: Pubkey, + pub creator_lp_token: Pubkey, + pub creator_lp_token_bump: u8, +} + +/// Test environment setup result. +pub struct TestEnv { + pub rpc: LightProgramTest, + pub payer: Keypair, + pub config_pda: Pubkey, +} + +/// Token mints and creator accounts for the pool. +pub struct TokenSetup { + pub token_0_mint: Pubkey, + pub token_1_mint: Pubkey, + pub token_0_mint_signer: Pubkey, + pub token_1_mint_signer: Pubkey, + pub creator_token_0: Pubkey, + pub creator_token_1: Pubkey, +} + +// ============================================================================ +// Setup Functions +// ============================================================================ + +/// Initialize the test environment with LightProgramTest and compression config. +pub async fn setup_test_environment(program_id: Pubkey) -> TestEnv { + let config = ProgramTestConfig::new_v2( + true, // with_prover + Some(vec![("raydium_cp_swap", program_id)]), + ); + + let mut rpc = LightProgramTest::new(config).await.unwrap(); + let payer = rpc.payer.insecure_clone(); + + // Setup mock program data for the BPF Loader + let program_data_pda = setup_mock_program_data(&mut rpc, &payer, &program_id); + + let (init_config_ix, config_pda) = InitializeRentFreeConfig::new( + &program_id, + &payer.pubkey(), + &program_data_pda, + rent_sponsor(), + payer.pubkey(), + ) + .build(); + + rpc.create_and_send_transaction(&[init_config_ix], &payer.pubkey(), &[&payer]) + .await + .expect("Initialize config should succeed"); + + // Fund the rent sponsor PDA + rpc.airdrop_lamports(&rent_sponsor(), 1_000_000_000) + .await + .expect("Airdrop to rent sponsor should succeed"); + + TestEnv { + rpc, + payer, + config_pda, + } +} + +/// Setup mock program data PDA for the BPF Loader. +pub fn setup_mock_program_data( + rpc: &mut LightProgramTest, + payer: &Keypair, + program_id: &Pubkey, +) -> Pubkey { + let (program_data_pda, _) = + Pubkey::find_program_address(&[program_id.as_ref()], &bpf_loader_upgradeable::ID); + + // Create mock program data with upgrade authority set to payer + // ProgramData account layout: + // - discriminator: u32 (4 bytes) - must be 3 for ProgramData + // - slot: u64 (8 bytes) + // - upgrade_authority_address: Option (1 + 32 bytes) + 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(payer.pubkey().as_ref()); // Authority pubkey + + rpc.set_account( + program_data_pda, + solana_sdk::account::Account { + lamports: 1_000_000_000, + data, + owner: bpf_loader_upgradeable::ID, + executable: false, + rent_epoch: 0, + }, + ); + + program_data_pda +} + +/// Create a compressed mint with ATAs for recipients. +pub async fn setup_create_mint( + rpc: &mut LightProgramTest, + payer: &Keypair, + mint_authority: Pubkey, + decimals: u8, + recipients: Vec<(u64, Pubkey)>, +) -> (Pubkey, Vec, Keypair) { + let mint_seed = Keypair::new(); + let address_tree = rpc.get_address_tree_v2(); + let output_queue = rpc.get_random_state_tree_info().unwrap().queue; + + let compression_address = light_token::instruction::derive_mint_compressed_address( + &mint_seed.pubkey(), + &address_tree.tree, + ); + + let (mint, bump) = find_mint_address(&mint_seed.pubkey()); + + let rpc_result = rpc + .get_validity_proof( + vec![], + vec![AddressWithTree { + address: compression_address, + tree: address_tree.tree, + }], + None, + ) + .await + .unwrap() + .value; + + let params = CreateMintParams { + decimals, + address_merkle_tree_root_index: rpc_result.addresses[0].root_index, + mint_authority, + proof: rpc_result.proof.0.unwrap(), + compression_address, + mint, + bump, + freeze_authority: None, + extensions: None, + rent_payment: 16, + write_top_up: 766, + }; + + let create_mint_builder = CreateMint::new( + params, + mint_seed.pubkey(), + payer.pubkey(), + address_tree.tree, + output_queue, + ); + let instruction = create_mint_builder.instruction().unwrap(); + + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer, &mint_seed]) + .await + .unwrap(); + + if recipients.is_empty() { + return (mint, vec![], mint_seed); + } + + let mut ata_pubkeys = Vec::with_capacity(recipients.len()); + + for (_amount, owner) in &recipients { + let (ata_address, _bump) = get_associated_token_address_and_bump(owner, &mint); + ata_pubkeys.push(ata_address); + + let create_ata = CreateAssociatedTokenAccount::new(payer.pubkey(), *owner, mint); + let ata_instruction = create_ata.instruction().unwrap(); + + rpc.create_and_send_transaction(&[ata_instruction], &payer.pubkey(), &[payer]) + .await + .unwrap(); + } + + for (idx, (amount, _)) in recipients.iter().enumerate() { + if *amount > 0 { + let mint_instruction = MintTo { + mint, + destination: ata_pubkeys[idx], + amount: *amount, + authority: mint_authority, + max_top_up: None, + fee_payer: None, + } + .instruction() + .unwrap(); + + rpc.create_and_send_transaction(&[mint_instruction], &payer.pubkey(), &[payer]) + .await + .unwrap(); + } + } + + (mint, ata_pubkeys, mint_seed) +} + +/// Create token mints and fund creator with initial balances. +pub async fn setup_token_mints( + rpc: &mut LightProgramTest, + payer: &Keypair, + creator: &Pubkey, + initial_balance: u64, +) -> TokenSetup { + let (mint_a, ata_pubkeys_a, mint_seed_a) = setup_create_mint( + rpc, + payer, + payer.pubkey(), + 9, + vec![(initial_balance, *creator)], + ) + .await; + + let (mint_b, ata_pubkeys_b, mint_seed_b) = setup_create_mint( + rpc, + payer, + payer.pubkey(), + 9, + vec![(initial_balance, *creator)], + ) + .await; + + // Ensure proper ordering: token_0_mint < token_1_mint + if mint_a < mint_b { + TokenSetup { + token_0_mint: mint_a, + token_1_mint: mint_b, + token_0_mint_signer: mint_seed_a.pubkey(), + token_1_mint_signer: mint_seed_b.pubkey(), + creator_token_0: ata_pubkeys_a[0], + creator_token_1: ata_pubkeys_b[0], + } + } else { + TokenSetup { + token_0_mint: mint_b, + token_1_mint: mint_a, + token_0_mint_signer: mint_seed_b.pubkey(), + token_1_mint_signer: mint_seed_a.pubkey(), + creator_token_0: ata_pubkeys_b[0], + creator_token_1: ata_pubkeys_a[0], + } + } +} + +// ============================================================================ +// AMM Config Functions +// ============================================================================ + +/// Get the admin keypair for testing. +pub fn get_admin_keypair() -> Keypair { + Keypair::from_seed(&[1u8; 32]).unwrap() +} + +pub async fn create_amm_config( + rpc: &mut LightProgramTest, + payer: &Keypair, + admin: &Keypair, + program_id: Pubkey, + index: u16, +) -> Pubkey { + let (amm_config_pda, _) = Pubkey::find_program_address( + &[AMM_CONFIG_SEED.as_bytes(), &index.to_be_bytes()], + &program_id, + ); + + let create_config_accounts = raydium_cp_swap::accounts::CreateAmmConfig { + owner: admin.pubkey(), + amm_config: amm_config_pda, + system_program: solana_sdk::system_program::ID, + }; + + let create_config_data = raydium_cp_swap::instruction::CreateAmmConfig { + index, + trade_fee_rate: 2500, + protocol_fee_rate: 1000, + fund_fee_rate: 500, + create_pool_fee: 0, + }; + + let create_config_ix = Instruction { + program_id, + accounts: create_config_accounts.to_account_metas(None), + data: create_config_data.data(), + }; + + rpc.create_and_send_transaction(&[create_config_ix], &payer.pubkey(), &[payer, admin]) + .await + .expect("Create AmmConfig should succeed"); + + amm_config_pda +} + +/// Setup the create_pool_fee account (wrapped SOL token account). +pub fn setup_create_pool_fee_account(rpc: &mut LightProgramTest, owner: &Pubkey) { + let create_pool_fee_receiver = raydium_cp_swap::create_pool_fee_receiver::ID; + let wsol_mint = spl_token::native_mint::id(); + + let mut fee_receiver_data = vec![0u8; spl_token::state::Account::LEN]; + let fee_account = spl_token::state::Account { + mint: wsol_mint, + owner: *owner, + amount: 0, + delegate: solana_sdk::program_option::COption::None, + state: spl_token::state::AccountState::Initialized, + is_native: solana_sdk::program_option::COption::Some(0), + delegated_amount: 0, + close_authority: solana_sdk::program_option::COption::None, + }; + spl_token::state::Account::pack(fee_account, &mut fee_receiver_data).unwrap(); + + rpc.set_account( + create_pool_fee_receiver, + solana_sdk::account::Account { + lamports: 1_000_000_000, + data: fee_receiver_data, + owner: spl_token::id(), + executable: false, + rent_epoch: 0, + }, + ); +} + +// ============================================================================ +// PDA Derivation +// ============================================================================ + +/// Derive all AMM PDAs for the pool. +pub fn derive_amm_pdas( + program_id: &Pubkey, + amm_config: &Pubkey, + token_0_mint: &Pubkey, + token_1_mint: &Pubkey, + creator: &Pubkey, +) -> AmmPdas { + let (pool_state, _) = Pubkey::find_program_address( + &[ + POOL_SEED.as_bytes(), + amm_config.as_ref(), + token_0_mint.as_ref(), + token_1_mint.as_ref(), + ], + program_id, + ); + + let (authority, authority_bump) = + Pubkey::find_program_address(&[AUTH_SEED.as_bytes()], program_id); + + let (observation_state, _) = Pubkey::find_program_address( + &[OBSERVATION_SEED.as_bytes(), pool_state.as_ref()], + program_id, + ); + + let (token_0_vault, _) = Pubkey::find_program_address( + &[ + POOL_VAULT_SEED.as_bytes(), + pool_state.as_ref(), + token_0_mint.as_ref(), + ], + program_id, + ); + + let (token_1_vault, _) = Pubkey::find_program_address( + &[ + POOL_VAULT_SEED.as_bytes(), + pool_state.as_ref(), + token_1_mint.as_ref(), + ], + program_id, + ); + + let (lp_mint_signer, lp_mint_signer_bump) = + Pubkey::find_program_address(&[LP_MINT_SIGNER_SEED, pool_state.as_ref()], program_id); + + let (lp_mint, _) = find_mint_address(&lp_mint_signer); + + let (creator_lp_token, creator_lp_token_bump) = + get_associated_token_address_and_bump(creator, &lp_mint); + + AmmPdas { + pool_state, + observation_state, + authority, + authority_bump, + token_0_vault, + token_1_vault, + lp_mint_signer, + lp_mint_signer_bump, + lp_mint, + creator_lp_token, + creator_lp_token_bump, + } +} + +// ============================================================================ +// Instruction Building +// ============================================================================ + +/// Get the create accounts proof for pool initialization. +pub async fn get_pool_create_accounts_proof( + rpc: &LightProgramTest, + program_id: &Pubkey, + pdas: &AmmPdas, +) -> CreateAccountsProofResult { + get_create_accounts_proof( + rpc, + program_id, + vec![ + CreateAccountsProofInput::pda(pdas.pool_state), + CreateAccountsProofInput::pda(pdas.observation_state), + CreateAccountsProofInput::mint(pdas.lp_mint_signer), + ], + ) + .await + .unwrap() +} + +/// Build the Withdraw instruction. +pub fn build_withdraw_instruction( + program_id: Pubkey, + owner: Pubkey, + pdas: &AmmPdas, + tokens: &TokenSetup, + owner_token_0: Pubkey, + owner_token_1: Pubkey, + lp_token_amount: u64, + minimum_token_0_amount: u64, + minimum_token_1_amount: u64, +) -> Instruction { + let accounts = raydium_cp_swap::accounts::Withdraw { + owner, + authority: pdas.authority, + pool_state: pdas.pool_state, + owner_lp_token: pdas.creator_lp_token, + token_0_account: owner_token_0, + token_1_account: owner_token_1, + token_0_vault: pdas.token_0_vault, + token_1_vault: pdas.token_1_vault, + token_program: spl_token::id(), + token_program_2022: spl_token_2022::id(), + vault_0_mint: tokens.token_0_mint, + vault_1_mint: tokens.token_1_mint, + lp_mint: pdas.lp_mint, + memo_program: spl_memo::id(), + system_program: solana_sdk::system_program::ID, + light_token_cpi_authority: CPI_AUTHORITY_PDA, + light_token_program: light_token_program_id(), + }; + + let instruction_data = raydium_cp_swap::instruction::Withdraw { + lp_token_amount, + minimum_token_0_amount, + minimum_token_1_amount, + }; + + Instruction { + program_id, + accounts: accounts.to_account_metas(None), + data: instruction_data.data(), + } +} + +/// Build the Swap instruction. +pub fn build_swap_instruction( + program_id: Pubkey, + payer: Pubkey, + amm_config: Pubkey, + pdas: &AmmPdas, + tokens: &TokenSetup, + input_token_account: Pubkey, + output_token_account: Pubkey, + is_token_0_input: bool, + amount_in: u64, + minimum_amount_out: u64, +) -> Instruction { + let (input_vault, output_vault, input_mint, output_mint) = if is_token_0_input { + ( + pdas.token_0_vault, + pdas.token_1_vault, + tokens.token_0_mint, + tokens.token_1_mint, + ) + } else { + ( + pdas.token_1_vault, + pdas.token_0_vault, + tokens.token_1_mint, + tokens.token_0_mint, + ) + }; + + let accounts = raydium_cp_swap::accounts::Swap { + payer, + authority: pdas.authority, + amm_config, + pool_state: pdas.pool_state, + input_token_account, + output_token_account, + input_vault, + output_vault, + input_token_program: light_token_program_id(), + output_token_program: light_token_program_id(), + input_token_mint: input_mint, + output_token_mint: output_mint, + observation_state: pdas.observation_state, + light_token_program: light_token_program_id(), + system_program: solana_sdk::system_program::ID, + light_token_cpi_authority: CPI_AUTHORITY_PDA, + }; + + let instruction_data = raydium_cp_swap::instruction::SwapBaseInput { + amount_in, + minimum_amount_out, + }; + + Instruction { + program_id, + accounts: accounts.to_account_metas(None), + data: instruction_data.data(), + } +} + +/// Build the Deposit instruction. +pub fn build_deposit_instruction( + program_id: Pubkey, + owner: Pubkey, + pdas: &AmmPdas, + tokens: &TokenSetup, + owner_token_0: Pubkey, + owner_token_1: Pubkey, + lp_token_amount: u64, + maximum_token_0_amount: u64, + maximum_token_1_amount: u64, +) -> Instruction { + let accounts = raydium_cp_swap::accounts::Deposit { + owner, + authority: pdas.authority, + pool_state: pdas.pool_state, + owner_lp_token: pdas.creator_lp_token, + token_0_account: owner_token_0, + token_1_account: owner_token_1, + token_0_vault: pdas.token_0_vault, + token_1_vault: pdas.token_1_vault, + token_program: spl_token::id(), + token_program_2022: spl_token_2022::id(), + light_token_program: light_token_program_id(), + vault_0_mint: tokens.token_0_mint, + vault_1_mint: tokens.token_1_mint, + lp_mint: pdas.lp_mint, + system_program: solana_sdk::system_program::ID, + light_token_cpi_authority: CPI_AUTHORITY_PDA, + }; + + let instruction_data = raydium_cp_swap::instruction::Deposit { + lp_token_amount, + maximum_token_0_amount, + maximum_token_1_amount, + }; + + Instruction { + program_id, + accounts: accounts.to_account_metas(None), + data: instruction_data.data(), + } +} + +/// Build the Initialize instruction. +pub fn build_initialize_instruction( + program_id: Pubkey, + creator: Pubkey, + amm_config: Pubkey, + pdas: &AmmPdas, + tokens: &TokenSetup, + config_pda: Pubkey, + proof_result: &CreateAccountsProofResult, + init_amount_0: u64, + init_amount_1: u64, + open_time: u64, +) -> Instruction { + let init_params = InitializeParams { + init_amount_0, + init_amount_1, + open_time, + create_accounts_proof: proof_result.create_accounts_proof.clone(), + lp_mint_signer_bump: pdas.lp_mint_signer_bump, + creator_lp_token_bump: pdas.creator_lp_token_bump, + authority_bump: pdas.authority_bump, + }; + + let accounts = raydium_cp_swap::accounts::Initialize { + creator, + amm_config, + authority: pdas.authority, + pool_state: pdas.pool_state, + token_0_mint: tokens.token_0_mint, + token_1_mint: tokens.token_1_mint, + lp_mint_signer: pdas.lp_mint_signer, + lp_mint: pdas.lp_mint, + creator_token_0: tokens.creator_token_0, + creator_token_1: tokens.creator_token_1, + creator_lp_token: pdas.creator_lp_token, + token_0_vault: pdas.token_0_vault, + token_1_vault: pdas.token_1_vault, + observation_state: pdas.observation_state, + create_pool_fee: raydium_cp_swap::create_pool_fee_receiver::ID, + token_program: spl_token::id(), + token_0_program: light_token_program_id(), + token_1_program: light_token_program_id(), + associated_token_program: light_anchor_spl::associated_token::ID, + system_program: solana_sdk::system_program::ID, + rent: solana_sdk::sysvar::rent::ID, + compression_config: config_pda, + light_token_config: LIGHT_TOKEN_CONFIG, + pda_rent_sponsor: raydium_cp_swap::program_rent_sponsor(), + light_token_rent_sponsor: LIGHT_TOKEN_RENT_SPONSOR, + light_token_program: light_token_program_id(), + light_token_cpi_authority: CPI_AUTHORITY_PDA, + }; + + let instruction_data = raydium_cp_swap::instruction::Initialize { + params: init_params, + }; + + Instruction { + program_id, + accounts: [ + accounts.to_account_metas(None), + proof_result.remaining_accounts.clone(), + ] + .concat(), + data: instruction_data.data(), + } +} + +// ============================================================================ +// Assertions +// ============================================================================ + +/// Get the balance of a token account. +pub async fn get_token_balance(rpc: &mut LightProgramTest, account: Pubkey) -> u64 { + let account_data = rpc.get_account(account).await.unwrap(); + if let Some(account) = account_data { + const AMOUNT_OFFSET: usize = 64; + if account.data.len() >= AMOUNT_OFFSET + 8 { + let amount_bytes = &account.data[AMOUNT_OFFSET..AMOUNT_OFFSET + 8]; + u64::from_le_bytes(amount_bytes.try_into().unwrap()) + } else { + 0 + } + } else { + 0 + } +} + +/// Verify that the pool was initialized correctly. +pub async fn assert_pool_initialized(rpc: &mut LightProgramTest, pdas: &AmmPdas) { + let pool_account = rpc.get_account(pdas.pool_state).await.unwrap(); + assert!(pool_account.is_some(), "Pool state should exist"); + + let observation_account = rpc.get_account(pdas.observation_state).await.unwrap(); + assert!( + observation_account.is_some(), + "Observation state should exist" + ); +} + +/// Assert that deposit succeeded by checking LP token balance increased. +pub async fn assert_deposit_succeeded( + rpc: &mut LightProgramTest, + owner_lp_token: Pubkey, + lp_balance_before: u64, + expected_lp_increase: u64, +) { + let lp_balance_after = get_token_balance(rpc, owner_lp_token).await; + let actual_increase = lp_balance_after.saturating_sub(lp_balance_before); + assert!( + actual_increase >= expected_lp_increase, + "LP token balance should increase by at least {}. Before: {}, After: {}, Actual increase: {}", + expected_lp_increase, + lp_balance_before, + lp_balance_after, + actual_increase + ); +} + +/// Assert that swap succeeded by checking balances changed correctly. +pub async fn assert_swap_succeeded( + rpc: &mut LightProgramTest, + input_account: Pubkey, + output_account: Pubkey, + input_balance_before: u64, + output_balance_before: u64, + expected_input_decrease: u64, + min_output_increase: u64, +) { + let input_balance_after = get_token_balance(rpc, input_account).await; + let output_balance_after = get_token_balance(rpc, output_account).await; + + let actual_input_decrease = input_balance_before.saturating_sub(input_balance_after); + let actual_output_increase = output_balance_after.saturating_sub(output_balance_before); + + assert_eq!( + actual_input_decrease, expected_input_decrease, + "Input token balance should decrease by {}. Before: {}, After: {}", + expected_input_decrease, input_balance_before, input_balance_after + ); + + assert!( + actual_output_increase >= min_output_increase, + "Output token balance should increase by at least {}. Before: {}, After: {}, Actual: {}", + min_output_increase, + output_balance_before, + output_balance_after, + actual_output_increase + ); +} + +/// Assert that withdraw succeeded by checking LP token balance decreased. +pub async fn assert_withdraw_succeeded( + rpc: &mut LightProgramTest, + owner_lp_token: Pubkey, + lp_balance_before: u64, + expected_lp_decrease: u64, +) { + let lp_balance_after = get_token_balance(rpc, owner_lp_token).await; + let actual_decrease = lp_balance_before.saturating_sub(lp_balance_after); + assert_eq!( + actual_decrease, expected_lp_decrease, + "LP token balance should decrease by {}. Before: {}, After: {}", + expected_lp_decrease, lp_balance_before, lp_balance_after + ); +} + +/// Verify that the AMM config was created. +pub async fn assert_amm_config_created(rpc: &mut LightProgramTest, amm_config: Pubkey) { + let account = rpc.get_account(amm_config).await.unwrap(); + assert!(account.is_some(), "AmmConfig account should exist"); +} diff --git a/programs/cp-swap/tests/local_test.rs b/programs/cp-swap/tests/local_test.rs new file mode 100644 index 0000000..1e0ac10 --- /dev/null +++ b/programs/cp-swap/tests/local_test.rs @@ -0,0 +1,233 @@ +//! Local integration tests for cp-swap program using LightProgramTest. +//! These tests run with LiteSVM (no external validator required). + +use light_client::rpc::Rpc; +use solana_keypair::Keypair; +use solana_signer::Signer; + +mod helpers_local; +use helpers_local::*; + +/// Test the full pool lifecycle: Initialize -> Deposit -> Swap -> Withdraw +#[tokio::test] +async fn test_full_lifecycle_local() { + let program_id = raydium_cp_swap::ID; + + // ======================================================================== + // Setup + // ======================================================================== + let mut env = setup_test_environment(program_id).await; + + // Create and fund creator + let creator = Keypair::new(); + env.rpc + .airdrop_lamports(&creator.pubkey(), 100_000_000_000) + .await + .unwrap(); + + // Get admin keypair and fund it + let admin = get_admin_keypair(); + env.rpc + .airdrop_lamports(&admin.pubkey(), 10_000_000_000) + .await + .unwrap(); + + // Setup token mints with initial balance for lifecycle operations + let initial_balance = 1_000_000; + let tokens = + setup_token_mints(&mut env.rpc, &env.payer, &creator.pubkey(), initial_balance).await; + + // Create AMM config + let amm_config = create_amm_config(&mut env.rpc, &env.payer, &admin, program_id, 1).await; + assert_amm_config_created(&mut env.rpc, amm_config).await; + + // Setup create pool fee account + setup_create_pool_fee_account(&mut env.rpc, &env.payer.pubkey()); + + // Derive PDAs + let pdas = derive_amm_pdas( + &program_id, + &amm_config, + &tokens.token_0_mint, + &tokens.token_1_mint, + &creator.pubkey(), + ); + + // ======================================================================== + // Initialize Pool + // ======================================================================== + let proof_result = get_pool_create_accounts_proof(&env.rpc, &program_id, &pdas).await; + + let init_amount_0 = 100_000; + let init_amount_1 = 100_000; + + let init_instruction = build_initialize_instruction( + program_id, + creator.pubkey(), + amm_config, + &pdas, + &tokens, + env.config_pda, + &proof_result, + init_amount_0, + init_amount_1, + 0, // open_time = 0 (immediate) + ); + + env.rpc + .create_and_send_transaction(&[init_instruction], &creator.pubkey(), &[&creator]) + .await + .expect("Initialize should succeed"); + + assert_pool_initialized(&mut env.rpc, &pdas).await; + + // Check initial LP token balance + let lp_balance_after_init = get_token_balance(&mut env.rpc, pdas.creator_lp_token).await; + println!("LP balance after init: {}", lp_balance_after_init); + assert!( + lp_balance_after_init > 0, + "Should have received LP tokens from initialization" + ); + + // ======================================================================== + // Deposit + // ======================================================================== + let lp_balance_before_deposit = get_token_balance(&mut env.rpc, pdas.creator_lp_token).await; + + let deposit_lp_amount = 500; + let max_token_0 = 10_000; + let max_token_1 = 10_000; + + let deposit_instruction = build_deposit_instruction( + program_id, + creator.pubkey(), + &pdas, + &tokens, + tokens.creator_token_0, + tokens.creator_token_1, + deposit_lp_amount, + max_token_0, + max_token_1, + ); + + env.rpc + .create_and_send_transaction(&[deposit_instruction], &creator.pubkey(), &[&creator]) + .await + .expect("Deposit should succeed"); + + assert_deposit_succeeded( + &mut env.rpc, + pdas.creator_lp_token, + lp_balance_before_deposit, + deposit_lp_amount, + ) + .await; + + println!( + "Deposit succeeded. LP balance: {}", + get_token_balance(&mut env.rpc, pdas.creator_lp_token).await + ); + + // ======================================================================== + // Swap (token_0 -> token_1) + // ======================================================================== + let token_0_balance_before = get_token_balance(&mut env.rpc, tokens.creator_token_0).await; + let token_1_balance_before = get_token_balance(&mut env.rpc, tokens.creator_token_1).await; + + let swap_amount_in = 100; + let min_amount_out = 1; + + let swap_instruction = build_swap_instruction( + program_id, + creator.pubkey(), + amm_config, + &pdas, + &tokens, + tokens.creator_token_0, + tokens.creator_token_1, + true, + swap_amount_in, + min_amount_out, + ); + + env.rpc + .create_and_send_transaction(&[swap_instruction], &creator.pubkey(), &[&creator]) + .await + .expect("Swap should succeed"); + + assert_swap_succeeded( + &mut env.rpc, + tokens.creator_token_0, + tokens.creator_token_1, + token_0_balance_before, + token_1_balance_before, + swap_amount_in, + min_amount_out, + ) + .await; + + println!( + "Swap succeeded. Token 0 balance: {}, Token 1 balance: {}", + get_token_balance(&mut env.rpc, tokens.creator_token_0).await, + get_token_balance(&mut env.rpc, tokens.creator_token_1).await + ); + + // ======================================================================== + // Withdraw + // ======================================================================== + let lp_balance_before_withdraw = get_token_balance(&mut env.rpc, pdas.creator_lp_token).await; + let withdraw_lp_amount = lp_balance_before_withdraw / 2; + + let withdraw_instruction = build_withdraw_instruction( + program_id, + creator.pubkey(), + &pdas, + &tokens, + tokens.creator_token_0, + tokens.creator_token_1, + withdraw_lp_amount, + 0, + 0, + ); + + env.rpc + .create_and_send_transaction(&[withdraw_instruction], &creator.pubkey(), &[&creator]) + .await + .expect("Withdraw should succeed"); + + assert_withdraw_succeeded( + &mut env.rpc, + pdas.creator_lp_token, + lp_balance_before_withdraw, + withdraw_lp_amount, + ) + .await; + + println!( + "Withdraw succeeded. LP balance: {}, Token 0 balance: {}, Token 1 balance: {}", + get_token_balance(&mut env.rpc, pdas.creator_lp_token).await, + get_token_balance(&mut env.rpc, tokens.creator_token_0).await, + get_token_balance(&mut env.rpc, tokens.creator_token_1).await + ); + + println!("Full lifecycle test completed successfully!"); +} + +/// Test AMM config creation +#[tokio::test] +async fn test_amm_config_local() { + let program_id = raydium_cp_swap::ID; + + let mut env = setup_test_environment(program_id).await; + + let admin = get_admin_keypair(); + env.rpc + .airdrop_lamports(&admin.pubkey(), 10_000_000_000) + .await + .unwrap(); + + let amm_config = create_amm_config(&mut env.rpc, &env.payer, &admin, program_id, 0).await; + assert_amm_config_created(&mut env.rpc, amm_config).await; + + println!("AMM config created at: {}", amm_config); +} diff --git a/programs/cp-swap/tests/program.rs b/programs/cp-swap/tests/program.rs index 3af4711..45d628b 100644 --- a/programs/cp-swap/tests/program.rs +++ b/programs/cp-swap/tests/program.rs @@ -6,17 +6,17 @@ /// - Parsing pool accounts from AccountInterface /// - Tracking account state (hot/cold) /// - Building AccountSpec for load instructions - use anchor_lang::AnchorDeserialize; use light_client::interface::{ AccountInterface, AccountSpec, AccountToFetch, ColdContext, LightProgramInterface, PdaSpec, TokenAccountInterface, }; +use light_sdk::interface::token::{Token, TokenDataWithSeeds}; use light_sdk::LightDiscriminator; -use light_token::compat::{CTokenData, TokenData}; use raydium_cp_swap::instructions::initialize::LP_MINT_SIGNER_SEED; +use raydium_cp_swap::raydium_cp_swap::{LightAccountVariant, Token0VaultSeeds, Token1VaultSeeds}; +use raydium_cp_swap::raydium_cp_swap::{ObservationStateSeeds, PoolStateSeeds}; use raydium_cp_swap::{ - raydium_cp_swap::{LightAccountVariant, TokenAccountVariant}, states::{ObservationState, PoolState}, AUTH_SEED, }; @@ -144,10 +144,12 @@ impl CpSwapSdk { // Create PdaSpec with variant let variant = LightAccountVariant::PoolState { + seeds: PoolStateSeeds { + amm_config: pool_state.amm_config, + token_0_mint: pool_state.token_0_mint, + token_1_mint: pool_state.token_1_mint, + }, data: pool_state.clone(), - amm_config: pool_state.amm_config, - token_0_mint: pool_state.token_0_mint, - token_1_mint: pool_state.token_1_mint, }; let spec = PdaSpec::new(interface, variant, PROGRAM_ID); self.pda_specs.insert(pool_pubkey, spec); @@ -156,7 +158,10 @@ impl CpSwapSdk { } /// Parse observation state from AccountInterface. - fn parse_observation_state(&mut self, interface: AccountInterface) -> Result<(), CpSwapSdkError> { + fn parse_observation_state( + &mut self, + interface: AccountInterface, + ) -> Result<(), CpSwapSdkError> { let pool_state = self .pool_state_pubkey .ok_or(CpSwapSdkError::PoolStateNotParsed)?; @@ -173,8 +178,8 @@ impl CpSwapSdk { .map_err(|e| CpSwapSdkError::ParseError(e.to_string()))?; let variant = LightAccountVariant::ObservationState { + seeds: ObservationStateSeeds { pool_state }, data: obs_state, - pool_state, }; let spec = PdaSpec::new(interface, variant, PROGRAM_ID); self.pda_specs.insert(obs_pubkey, spec); @@ -182,72 +187,60 @@ impl CpSwapSdk { Ok(()) } - /// Store token vault interface. - /// Vaults are program-owned PDAs, so we convert them to PdaSpec with CTokenData variant. - pub fn set_token_vault(&mut self, interface: TokenAccountInterface, is_vault_0: bool) { + /// Store token vault interface and create PdaSpec with token vault variant. + pub fn set_token_vault( + &mut self, + interface: TokenAccountInterface, + is_vault_0: bool, + ) -> Result<(), CpSwapSdkError> { + let pool_state = self + .pool_state_pubkey + .ok_or(CpSwapSdkError::PoolStateNotParsed)?; + let key = interface.key; - let pool_state = self.pool_state_pubkey.expect("pool_state must be set before vaults"); - let mint = if is_vault_0 { - self.token_0_mint.expect("token_0_mint must be set") + if is_vault_0 { + self.token_0_vault = Some(key); } else { - self.token_1_mint.expect("token_1_mint must be set") - }; + self.token_1_vault = Some(key); + } - // Build TokenData from TokenAccountInterface - let token_data = TokenData { - mint: interface.mint(), - owner: interface.owner(), - amount: interface.amount(), - delegate: if interface.parsed.delegate.option == [1, 0, 0, 0] { - Some(Pubkey::from(interface.parsed.delegate.value)) - } else { - None - }, - state: light_token::compat::AccountState::Initialized, - tlv: None, - }; + let token: Token = Token::deserialize(&mut &interface.account.data[..]) + .map_err(|e| CpSwapSdkError::ParseError(e.to_string()))?; - // Build variant based on which vault this is let variant = if is_vault_0 { - LightAccountVariant::CTokenData(CTokenData { - variant: TokenAccountVariant::Token0Vault { + let token_0_mint = self + .token_0_mint + .ok_or(CpSwapSdkError::MissingField("token_0_mint"))?; + LightAccountVariant::Token0Vault(TokenDataWithSeeds { + seeds: Token0VaultSeeds { pool_state, - token_0_mint: mint, + token_0_mint, }, - token_data, + token_data: token, }) } else { - LightAccountVariant::CTokenData(CTokenData { - variant: TokenAccountVariant::Token1Vault { + let token_1_mint = self + .token_1_mint + .ok_or(CpSwapSdkError::MissingField("token_1_mint"))?; + LightAccountVariant::Token1Vault(TokenDataWithSeeds { + seeds: Token1VaultSeeds { pool_state, - token_1_mint: mint, + token_1_mint, }, - token_data, + token_data: token, }) }; - // Convert TokenAccountInterface to AccountInterface for PdaSpec - // For cold vaults, we need to convert ColdContext::Token to ColdContext::Account - let cold = if let Some(ColdContext::Token(ct)) = &interface.cold { - Some(ColdContext::Account(ct.account.clone())) - } else { - None - }; - let account_interface = AccountInterface { - key, - account: interface.account.clone(), - cold, + key: interface.key, + account: interface.account, + cold: interface.cold, }; let spec = PdaSpec::new(account_interface, variant, PROGRAM_ID); - self.pda_specs.insert(key, spec); - if is_vault_0 { - self.token_0_vault = Some(key); - } else { - self.token_1_vault = Some(key); - } + + Ok(()) } /// Store LP mint interface. @@ -267,32 +260,30 @@ impl CpSwapSdk { .pool_state_pubkey .ok_or(CpSwapSdkError::PoolStateNotParsed)?; - // Deserialize token data properly - let token_data = TokenData::deserialize(&mut &account.data()[..]) + let token: Token = Token::deserialize(&mut &account.data()[..]) .map_err(|e| CpSwapSdkError::ParseError(e.to_string()))?; - // Build variant based on which vault this is let variant = if is_vault_0 { let token_0_mint = self .token_0_mint .ok_or(CpSwapSdkError::MissingField("token_0_mint"))?; - LightAccountVariant::CTokenData(CTokenData { - variant: TokenAccountVariant::Token0Vault { + LightAccountVariant::Token0Vault(TokenDataWithSeeds { + seeds: Token0VaultSeeds { pool_state, token_0_mint, }, - token_data, + token_data: token, }) } else { let token_1_mint = self .token_1_mint .ok_or(CpSwapSdkError::MissingField("token_1_mint"))?; - LightAccountVariant::CTokenData(CTokenData { - variant: TokenAccountVariant::Token1Vault { + LightAccountVariant::Token1Vault(TokenDataWithSeeds { + seeds: Token1VaultSeeds { pool_state, token_1_mint, }, - token_data, + token_data: token, }) }; @@ -302,6 +293,9 @@ impl CpSwapSdk { let compressed_account = match &account.cold { Some(ColdContext::Token(ct)) => ct.account.clone(), Some(ColdContext::Account(ca)) => ca.clone(), + Some(ColdContext::Mint(_)) => { + return Err(CpSwapSdkError::MissingField("unexpected mint cold context")) + } None => return Err(CpSwapSdkError::MissingField("cold_context")), }; AccountInterface { @@ -313,6 +307,7 @@ impl CpSwapSdk { account.clone() }; + // Decompression goes to PROGRAM_ID (cp-swap), not interface.account.owner (SPL/Light Token) let spec = PdaSpec::new(interface, variant, PROGRAM_ID); self.pda_specs.insert(account.key, spec); @@ -369,35 +364,35 @@ impl CpSwapSdk { pub fn is_pool_state_cold(&self) -> bool { self.pool_state_pubkey .and_then(|k| self.pda_specs.get(&k)) - .map_or(false, |s| s.is_cold()) + .is_some_and(|s| s.is_cold()) } /// Check if observation state is cold. pub fn is_observation_cold(&self) -> bool { self.observation_key .and_then(|k| self.pda_specs.get(&k)) - .map_or(false, |s| s.is_cold()) + .is_some_and(|s| s.is_cold()) } /// Check if token 0 vault is cold. pub fn is_vault_0_cold(&self) -> bool { self.token_0_vault .and_then(|k| self.pda_specs.get(&k)) - .map_or(false, |s| s.is_cold()) + .is_some_and(|s| s.is_cold()) } /// Check if token 1 vault is cold. pub fn is_vault_1_cold(&self) -> bool { self.token_1_vault .and_then(|k| self.pda_specs.get(&k)) - .map_or(false, |s| s.is_cold()) + .is_some_and(|s| s.is_cold()) } /// Check if LP mint is cold. pub fn is_lp_mint_cold(&self) -> bool { self.lp_mint .and_then(|k| self.mint_specs.get(&k)) - .map_or(false, |s| s.is_cold()) + .is_some_and(|s| s.is_cold()) } /// Get pool state pubkey. @@ -418,11 +413,22 @@ impl LightProgramInterface for CpSwapSdk { fn from_keyed_accounts(accounts: &[AccountInterface]) -> Result { let mut sdk = Self::new(); + // Debug: print expected discriminator + println!( + "DEBUG: Expected PoolState::LIGHT_DISCRIMINATOR = {:?}", + PoolState::LIGHT_DISCRIMINATOR + ); + // First pass: find and parse pool state for account in accounts { let data = account.data(); if data.len() >= 8 { let discriminator: [u8; 8] = data[..8].try_into().unwrap_or_default(); + println!( + "DEBUG: Account discriminator = {:?}, matches = {}", + discriminator, + discriminator == PoolState::LIGHT_DISCRIMINATOR + ); if discriminator == PoolState::LIGHT_DISCRIMINATOR { sdk.parse_pool_state(account.clone())?; break; diff --git a/programs/cp-swap/tests/program_test.rs b/programs/cp-swap/tests/program_test.rs index 78a5fda..faac80a 100644 --- a/programs/cp-swap/tests/program_test.rs +++ b/programs/cp-swap/tests/program_test.rs @@ -1,11 +1,9 @@ +#![allow(clippy::cloned_ref_to_slice_refs)] /// Clean integration test for cp-swap using CpSwapSdk. -/// Tests the full lifecycle: Initialize -> Warp -> Compress -> Load -> Execute Operations - -use light_client::interface::{ - create_load_instructions, AccountInterfaceExt, AccountSpec, LightProgramInterface, -}; -use light_program_test::program_test::TestRpc; -use light_program_test::Rpc; +/// Tests the full lifecycle: Initialize -> Wait -> Compress -> Load -> Execute Operations +use light_client::interface::{create_load_instructions, AccountSpec, LightProgramInterface}; +use light_client::rpc::Rpc; +use solana_compute_budget_interface::ComputeBudgetInstruction; use solana_instruction::Instruction; use solana_sdk::transaction::Transaction; use solana_signer::Signer; @@ -19,15 +17,21 @@ use program::{CpSwapInstruction, CpSwapSdk}; fn log_transaction_size(name: &str, ixs: &[Instruction]) { let tx = Transaction::new_with_payer(ixs, None); let serialized = bincode::serialize(&tx).expect("Failed to serialize transaction"); - println!("{}: {} bytes ({} instructions)", name, serialized.len(), ixs.len()); + println!( + "{}: {} bytes ({} instructions)", + name, + serialized.len(), + ixs.len() + ); } -#[tokio::test] +#[tokio::test(flavor = "multi_thread")] async fn test_sdk_lifecycle() { let program_id = raydium_cp_swap::ID; // ==================== PHASE 1: Setup & Initialize Pool ==================== - let mut setup = setup_pool_environment(program_id, 10).await; + // Use forester-enabled environment for auto-compression + let mut setup = setup_pool_environment_with_forester(program_id, 10).await; let proof_result = get_pool_create_accounts_proof(&setup.env.rpc, &program_id, &setup.pdas).await; @@ -44,39 +48,108 @@ async fn test_sdk_lifecycle() { 0, ); log_transaction_size("Initialize transaction", &[init_ix.clone()]); + + // Create Address Lookup Table for the initialize transaction + let lut_addresses = extract_lut_addresses(&proof_result.remaining_accounts); + let lut = + create_address_lookup_table(&mut setup.env.rpc, &setup.env.payer, lut_addresses).await; + + // Add compute budget instruction + let compute_budget_ix = ComputeBudgetInstruction::set_compute_unit_limit(1_400_000); + setup .env .rpc - .create_and_send_transaction(&[init_ix], &setup.creator.pubkey(), &[&setup.creator]) + .create_and_send_versioned_transaction( + &[compute_budget_ix, init_ix], + &setup.creator.pubkey(), + &[&setup.creator], + &[lut], + ) .await .unwrap(); // ==================== PHASE 2: Verify Hot Accounts Exist ==================== assert_pool_accounts_exist(&mut setup.env.rpc, &setup.pdas, &setup.tokens).await; - // ==================== PHASE 3: Warp to Trigger Compression ==================== - setup - .env - .rpc - .warp_epoch_forward(30) + // ==================== PHASE 3: Manual Compression ==================== + // The forester's compression timing is based on SLOTS_PER_EPOCH (13500) which is + // hardcoded in light-compressible crate. We can't change this constant at runtime, + // so instead we manually compress the PDA accounts using compress_accounts_idempotent. + // This simulates what the forester would do after rent expires. + println!("Manually compressing pool PDA accounts..."); + + // Wait a moment for indexer to sync compressed account states + tokio::time::sleep(tokio::time::Duration::from_secs(2)).await; + + compress_pool_pda_accounts( + &mut setup.env.rpc, + &setup.env.payer, + &program_id, + &setup.pdas, + &setup.env.config_pda, + ) + .await + .expect("Manual compression should succeed"); + + // Wait for indexer to process the compression + println!("Waiting for indexer to process compression..."); + tokio::time::sleep(tokio::time::Duration::from_secs(5)).await; + wait_for_indexer(&setup.env.rpc) .await - .unwrap(); + .expect("Indexer should sync after compression"); - // ==================== PHASE 4: Assert All Accounts Are Compressed ==================== - assert_pool_accounts_compressed(&mut setup.env.rpc, &setup.pdas, &setup.tokens).await; + // ==================== PHASE 4: Assert PDA Accounts Are Compressed ==================== + // Note: Only pool_state and observation_state are compressed by our helper. + // Other accounts (mints, vaults, ATAs) are handled differently. + assert_onchain_closed(&mut setup.env.rpc, &setup.pdas.pool_state).await; + assert_onchain_closed(&mut setup.env.rpc, &setup.pdas.observation_state).await; + println!("Pool accounts verified as closed on-chain"); // ==================== PHASE 5: Create SDK from Compressed State ==================== + // Now that Photon supports looking up fully compressed accounts by their PDA pubkey, + // we can use get_account_interface directly. + println!("Fetching compressed pool state via get_account_interface..."); + let pool_interface = setup .env .rpc - .get_account_interface(&setup.pdas.pool_state, &program_id) + .get_account_interface(&setup.pdas.pool_state, None) .await - .expect("pool should be compressed"); + .expect("get_account_interface should succeed") + .value + .expect("pool_state should be found via get_account_interface"); + + let data = pool_interface.data(); + println!( + "Found pool_state via get_account_interface: key={}, is_cold={}, data_len={}, discriminator={:?}", + pool_interface.key, + pool_interface.is_cold(), + data.len(), + if data.len() >= 8 { &data[..8] } else { data } + ); assert!( pool_interface.is_cold(), - "pool_state should be cold after warp" + "pool_state should be cold after compression" ); + // Debug: check if discriminator matches PoolState::LIGHT_DISCRIMINATOR + if data.len() >= 8 { + let disc: [u8; 8] = data[..8].try_into().unwrap(); + println!("Discriminator bytes: {:?}", disc); + // PoolState::LIGHT_DISCRIMINATOR is [0, 236, 227, 245, 215, 195, 222, 70] from program.rs + println!("Expected PoolState::LIGHT_DISCRIMINATOR: [0, 236, 227, 245, 215, 195, 222, 70]"); + } + + // Debug: print the account info + println!("About to call from_keyed_accounts with {} accounts", 1); + println!("Account key: {}", pool_interface.key); + println!("Account data len: {}", pool_interface.data().len()); + if pool_interface.data().len() >= 8 { + let disc: [u8; 8] = pool_interface.data()[..8].try_into().unwrap(); + println!("Account discriminator from data(): {:?}", disc); + } + let mut sdk = CpSwapSdk::from_keyed_accounts(&[pool_interface]) .expect("from_keyed_accounts should succeed"); @@ -85,9 +158,9 @@ async fn test_sdk_lifecycle() { let keyed_accounts = setup .env .rpc - .get_multiple_account_interfaces(&accounts_to_fetch) + .fetch_accounts(&accounts_to_fetch, None) .await - .expect("get_multiple_account_interfaces should succeed"); + .expect("fetch_accounts should succeed"); sdk.update(&keyed_accounts) .expect("sdk.update should succeed"); @@ -95,37 +168,57 @@ async fn test_sdk_lifecycle() { // ==================== PHASE 7: Build Specs for Load ==================== let mut all_specs = sdk.get_specs_for_instruction(&CpSwapInstruction::Deposit); - // Fetch creator's ATAs (compressed) and add to specs + // Fetch creator's ATAs and add to specs + // These are Light Token accounts (on-chain), so they should return as hot let creator_lp_ata_interface = setup .env .rpc - .get_ata_interface(&setup.creator.pubkey(), &setup.pdas.lp_mint) + .get_ata_interface(&setup.creator.pubkey(), &setup.pdas.lp_mint, None) .await - .expect("get_ata_interface for creator_lp_token should succeed"); + .expect("get_ata_interface for creator_lp_token should succeed") + .value + .expect("creator_lp_token should exist"); all_specs.push(AccountSpec::Ata(creator_lp_ata_interface)); let creator_token_0_interface = setup .env .rpc - .get_ata_interface(&setup.creator.pubkey(), &setup.tokens.token_0_mint) + .get_ata_interface(&setup.creator.pubkey(), &setup.tokens.token_0_mint, None) .await - .expect("get_ata_interface for creator_token_0 should succeed"); + .expect("get_ata_interface for creator_token_0 should succeed") + .value + .expect("creator_token_0 should exist"); all_specs.push(AccountSpec::Ata(creator_token_0_interface)); let creator_token_1_interface = setup .env .rpc - .get_ata_interface(&setup.creator.pubkey(), &setup.tokens.token_1_mint) + .get_ata_interface(&setup.creator.pubkey(), &setup.tokens.token_1_mint, None) .await - .expect("get_ata_interface for creator_token_1 should succeed"); + .expect("get_ata_interface for creator_token_1 should succeed") + .value + .expect("creator_token_1 should exist"); all_specs.push(AccountSpec::Ata(creator_token_1_interface)); // ==================== PHASE 8: Create Load Instructions ==================== + // Debug: print tree info from the specs + for spec in &all_specs { + if let AccountSpec::Pda(pda_spec) = spec { + if let Some(compressed) = pda_spec.compressed() { + println!( + "DEBUG: PDA spec tree_info: tree={}, queue={}, tree_type={:?}", + compressed.tree_info.tree, + compressed.tree_info.queue, + compressed.tree_info.tree_type + ); + } + } + } + let all_load_ixs = create_load_instructions( &all_specs, setup.env.payer.pubkey(), setup.env.config_pda, - setup.env.payer.pubkey(), &setup.env.rpc, ) .await @@ -133,14 +226,40 @@ async fn test_sdk_lifecycle() { // ==================== PHASE 9: Execute Load ==================== log_transaction_size("Load transaction", &all_load_ixs); + + // Debug: Print all required signers for the instructions + println!("DEBUG: Required signers in instructions:"); + let mut required_signers = std::collections::HashSet::new(); + for ix in &all_load_ixs { + for meta in &ix.accounts { + if meta.is_signer { + required_signers.insert(meta.pubkey); + } + } + } + for signer in &required_signers { + println!(" Required: {}", signer); + } + println!("DEBUG: Provided signers:"); + println!(" payer: {}", setup.env.payer.pubkey()); + println!(" creator: {}", setup.creator.pubkey()); + + // Execute load transaction + // Note: ATAs are returned as hot (on-chain) since Light Token accounts exist on-chain. + // Only the PDA decompress instructions are generated for cold PDAs. + // Use only payer if that's the only required signer + let signers = if required_signers.contains(&setup.creator.pubkey()) { + println!("DEBUG: Adding creator to signers"); + vec![&setup.env.payer, &setup.creator] + } else { + println!("DEBUG: Only payer needed"); + vec![&setup.env.payer] + }; + setup .env .rpc - .create_and_send_transaction( - &all_load_ixs, - &setup.env.payer.pubkey(), - &[&setup.env.payer, &setup.creator], - ) + .create_and_send_transaction(&all_load_ixs, &setup.env.payer.pubkey(), &signers) .await .expect("Load should succeed");