diff --git a/.github/workflows/programs.yml b/.github/workflows/programs.yml index c84da7dfe3..9cce8303e0 100644 --- a/.github/workflows/programs.yml +++ b/.github/workflows/programs.yml @@ -7,7 +7,7 @@ on: - "program-tests/**" - "program-libs/**" - "prover/client/**" - - ".github/workflows/light-system-programs-tests.yml" + - ".github/workflows/programs.yml" pull_request: branches: - "*" @@ -16,7 +16,7 @@ on: - "program-tests/**" - "program-libs/**" - "prover/client/**" - - ".github/workflows/light-system-programs-tests.yml" + - ".github/workflows/programs.yml" types: - opened - synchronize @@ -24,6 +24,8 @@ on: - ready_for_review name: programs +permissions: + contents: read concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -31,7 +33,7 @@ concurrency: jobs: system-programs: - name: programs + name: ${{ matrix.test-group }} if: github.event.pull_request.draft == false runs-on: warp-ubuntu-latest-x64-4x timeout-minutes: 90 @@ -52,27 +54,16 @@ jobs: strategy: matrix: - include: - - program: account-compression-and-registry - sub-tests: '["cargo-test-sbf -p account-compression-test", "cargo-test-sbf -p registry-test"]' - - program: light-system-program-address - sub-tests: '["cargo-test-sbf -p system-test -- test_with_address", "cargo-test-sbf -p e2e-test", "cargo-test-sbf -p compressed-token-test --test light_token"]' - - program: light-system-program-compression - sub-tests: '["cargo-test-sbf -p system-test -- test_with_compression", "cargo-test-sbf -p system-test --test test_re_init_cpi_account"]' - - program: compressed-token-and-e2e - sub-tests: '["cargo test -p light-compressed-token", "cargo-test-sbf -p compressed-token-test --test v1", "cargo-test-sbf -p compressed-token-test --test mint"]' - - program: compressed-token-batched-tree - sub-tests: '["cargo-test-sbf -p compressed-token-test -- test_transfer_with_photon_and_batched_tree"]' - - program: system-cpi-test - sub-tests: - '["cargo-test-sbf -p system-cpi-test", "cargo test -p light-system-program-pinocchio", - "cargo-test-sbf -p system-cpi-v2-test -- --skip functional_ --skip event::parse", "cargo-test-sbf -p system-cpi-v2-test -- event::parse", - "cargo-test-sbf -p compressed-token-test --test transfer2" - ]' - - program: system-cpi-test-v2-functional-read-only - sub-tests: '["cargo-test-sbf -p system-cpi-v2-test -- functional_read_only"]' - - program: system-cpi-test-v2-functional-account-infos - sub-tests: '["cargo-test-sbf -p system-cpi-v2-test -- functional_account_infos"]' + test-group: + - account-compression-and-registry + - system-address + - system-compression + - compressed-token-and-e2e + - compressed-token-batched-tree + - system-cpi + - system-cpi-v2-functional-read-only + - system-cpi-v2-functional-account-infos + steps: - name: Checkout sources uses: actions/checkout@v6 @@ -87,34 +78,7 @@ jobs: run: | just cli build - - name: ${{ matrix.program }} + - name: Run tests + working-directory: program-tests run: | - - IFS=',' read -r -a sub_tests <<< "${{ join(fromJSON(matrix['sub-tests']), ', ') }}" - for subtest in "${sub_tests[@]}" - do - echo "$subtest" - - # Retry logic for flaky batched-tree test - if [[ "$subtest" == *"test_transfer_with_photon_and_batched_tree"* ]]; then - echo "Running flaky test with retry logic (max 3 attempts)..." - attempt=1 - max_attempts=3 - until RUSTFLAGS="-D warnings" eval "$subtest"; do - attempt=$((attempt + 1)) - if [ $attempt -gt $max_attempts ]; then - echo "Test failed after $max_attempts attempts" - exit 1 - fi - echo "Attempt $attempt/$max_attempts failed, retrying..." - sleep 5 - done - echo "Test passed on attempt $attempt" - else - RUSTFLAGS="-D warnings" eval "$subtest" - if [ "$subtest" == "cargo-test-sbf -p e2e-test" ]; then - just programs build-compressed-token-small - RUSTFLAGS="-D warnings" eval "$subtest -- --test test_10_all" - fi - fi - done + just ci-${{ matrix.test-group }} diff --git a/.mise.toml b/.mise.toml new file mode 100644 index 0000000000..c5492f4b9e --- /dev/null +++ b/.mise.toml @@ -0,0 +1,4 @@ +# Disable mise's Go management for this project. +# We use our own Go installation via devenv.sh. +[settings] +disable_tools = ["go"] diff --git a/Cargo.toml b/Cargo.toml index b708d9c74d..34a1e540a4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -140,6 +140,7 @@ pinocchio-pubkey = { version = "0.3.0" } pinocchio-system = { version = "0.3.0" } bs58 = "^0.5.1" sha2 = "0.10" +hex = "0.4" litesvm = "0.7" # Anchor anchor-lang = { version = "0.31.1" } diff --git a/cli/src/commands/test-validator/index.ts b/cli/src/commands/test-validator/index.ts index d668c90e20..b35398b97d 100644 --- a/cli/src/commands/test-validator/index.ts +++ b/cli/src/commands/test-validator/index.ts @@ -43,6 +43,22 @@ class SetupCommand extends Command { "Runs a test validator without starting a new prover service.", default: false, }), + forester: Flags.boolean({ + description: + "Start the forester service for auto-compression of compressible accounts.", + default: false, + }), + "forester-port": Flags.integer({ + description: "Port for the forester API server.", + required: false, + default: 8080, + }), + "compressible-pda-program": Flags.string({ + description: + "Compressible PDA programs to track. Format: 'program_id:discriminator_base58'. Can be specified multiple times.", + required: false, + multiple: true, + }), "skip-system-accounts": Flags.boolean({ description: "Runs a test validator without initialized light system accounts.", @@ -210,6 +226,7 @@ class SetupCommand extends Command { await stopTestEnv({ indexer: !flags["skip-indexer"], prover: !flags["skip-prover"], + forester: flags.forester, }); this.log("\nTest validator stopped successfully \x1b[32m✔\x1b[0m"); } else { @@ -262,6 +279,9 @@ class SetupCommand extends Command { indexerPort: flags["indexer-port"], proverPort: flags["prover-port"], prover: !flags["skip-prover"], + forester: flags.forester, + foresterPort: flags["forester-port"], + compressiblePdaPrograms: flags["compressible-pda-program"], skipSystemAccounts: flags["skip-system-accounts"], geyserConfig: flags["geyser-config"], validatorArgs: flags["validator-args"], diff --git a/cli/src/utils/constants.ts b/cli/src/utils/constants.ts index e3a9137737..e7943ce629 100644 --- a/cli/src/utils/constants.ts +++ b/cli/src/utils/constants.ts @@ -18,6 +18,7 @@ export const CARGO_GENERATE_TAG = "v0.18.4"; export const SOLANA_VALIDATOR_PROCESS_NAME = "solana-test-validator"; export const LIGHT_PROVER_PROCESS_NAME = "light-prover"; export const INDEXER_PROCESS_NAME = "photon"; +export const FORESTER_PROCESS_NAME = "forester"; export const PHOTON_VERSION = "0.51.2"; diff --git a/cli/src/utils/initTestEnv.ts b/cli/src/utils/initTestEnv.ts index c98c8c29bb..fde4ed82a6 100644 --- a/cli/src/utils/initTestEnv.ts +++ b/cli/src/utils/initTestEnv.ts @@ -19,6 +19,11 @@ import { } from "./process"; import { killProver, startProver } from "./processProverServer"; import { killIndexer, startIndexer } from "./processPhotonIndexer"; +import { + killForester, + startForester, + getPayerForForester, +} from "./processForester"; import { Connection, PublicKey } from "@solana/web3.js"; type Program = { id: string; name?: string; tag?: string; path?: string }; @@ -95,8 +100,10 @@ async function getProgramOwnedAccounts( export async function stopTestEnv(options: { indexer: boolean; prover: boolean; + forester?: boolean; }) { const processesToKill = [ + { name: "forester", condition: options.forester ?? false, killFunction: killForester }, { name: "photon", condition: options.indexer, killFunction: killIndexer }, { name: "prover", condition: options.prover, killFunction: killProver }, { @@ -129,9 +136,11 @@ export async function initTestEnv({ skipSystemAccounts, indexer = true, prover = true, + forester = false, rpcPort = 8899, indexerPort = 8784, proverPort = 3001, + foresterPort = 8080, gossipHost = "127.0.0.1", checkPhotonVersion = true, photonDatabaseUrl, @@ -141,6 +150,7 @@ export async function initTestEnv({ cloneNetwork, verbose, skipReset, + compressiblePdaPrograms, }: { additionalPrograms?: { address: string; path: string }[]; upgradeablePrograms?: { @@ -151,9 +161,11 @@ export async function initTestEnv({ skipSystemAccounts?: boolean; indexer: boolean; prover: boolean; + forester?: boolean; rpcPort?: number; indexerPort?: number; proverPort?: number; + foresterPort?: number; gossipHost?: string; checkPhotonVersion?: boolean; photonDatabaseUrl?: string; @@ -163,6 +175,7 @@ export async function initTestEnv({ cloneNetwork?: "devnet" | "mainnet"; verbose?: boolean; skipReset?: boolean; + compressiblePdaPrograms?: string[]; }) { // We cannot await this promise directly because it will hang the process startTestValidator({ @@ -209,6 +222,27 @@ export async function initTestEnv({ proverUrlForIndexer, ); } + + if (forester) { + if (!indexer || !prover) { + throw new Error("Forester requires both indexer and prover to be running"); + } + try { + const payer = getPayerForForester(); + await startForester({ + rpcUrl: `http://127.0.0.1:${rpcPort}`, + wsRpcUrl: `ws://127.0.0.1:${rpcPort + 1}`, + indexerUrl: `http://127.0.0.1:${indexerPort}`, + proverUrl: `http://127.0.0.1:${proverPort}`, + payer, + foresterPort, + compressiblePdaPrograms, + }); + } catch (error) { + console.error("Failed to start forester:", error); + throw error; + } + } } export async function initTestEnvIfNeeded({ diff --git a/cli/src/utils/processForester.ts b/cli/src/utils/processForester.ts new file mode 100644 index 0000000000..9bbc255e72 --- /dev/null +++ b/cli/src/utils/processForester.ts @@ -0,0 +1,107 @@ +import which from "which"; +import { killProcess, spawnBinary, waitForServers } from "./process"; +import { FORESTER_PROCESS_NAME } from "./constants"; +import { exec } from "node:child_process"; +import * as util from "node:util"; +import { exit } from "node:process"; +import * as fs from "fs"; +import * as path from "path"; + +const execAsync = util.promisify(exec); + +async function isForesterInstalled(): Promise { + try { + const resolvedOrNull = which.sync("forester", { nothrow: true }); + return resolvedOrNull !== null; + } catch (error) { + return false; + } +} + +function getForesterInstallMessage(): string { + return `\nForester not found. Please install it by running: "cargo install --git https://github.com/Lightprotocol/light-protocol forester --locked --force"`; +} + +export interface ForesterConfig { + rpcUrl: string; + wsRpcUrl: string; + indexerUrl: string; + proverUrl: string; + payer: string; + foresterPort: number; + compressiblePdaPrograms?: string[]; +} + +/** + * Starts the forester service for auto-compression of compressible accounts. + * + * @param config - Forester configuration + */ +export async function startForester(config: ForesterConfig) { + await killForester(); + + if (!(await isForesterInstalled())) { + console.log(getForesterInstallMessage()); + return exit(1); + } + + console.log("Starting forester..."); + + const args: string[] = [ + "start", + "--rpc-url", + config.rpcUrl, + "--ws-rpc-url", + config.wsRpcUrl, + "--indexer-url", + config.indexerUrl, + "--prover-url", + config.proverUrl, + "--payer", + config.payer, + "--api-server-port", + config.foresterPort.toString(), + "--enable-compressible", + ]; + + // Add compressible PDA programs if specified + if (config.compressiblePdaPrograms && config.compressiblePdaPrograms.length > 0) { + for (const program of config.compressiblePdaPrograms) { + args.push("--compressible-pda-program", program); + } + } + + spawnBinary(FORESTER_PROCESS_NAME, args); + await waitForServers([{ port: config.foresterPort, path: "/health" }]); + console.log("Forester started successfully!"); +} + +export async function killForester() { + await killProcess(FORESTER_PROCESS_NAME); +} + +/** + * Gets the payer keypair as a JSON array string for forester. + * Reads from ~/.config/solana/id.json or SOLANA_PAYER environment variable. + * + * @returns JSON array string of the keypair bytes + */ +export function getPayerForForester(): string { + // Check for SOLANA_PAYER environment variable first + if (process.env.SOLANA_PAYER) { + return process.env.SOLANA_PAYER; + } + + // Default to standard Solana keypair location + const homeDir = process.env.HOME || process.env.USERPROFILE || ""; + const keypairPath = path.join(homeDir, ".config", "solana", "id.json"); + + if (fs.existsSync(keypairPath)) { + const keypairData = fs.readFileSync(keypairPath, "utf-8"); + return keypairData.trim(); + } + + throw new Error( + "No payer keypair found. Set SOLANA_PAYER environment variable or create ~/.config/solana/id.json", + ); +} diff --git a/forester/Cargo.toml b/forester/Cargo.toml index 5b23dc2855..5ce45b8d55 100644 --- a/forester/Cargo.toml +++ b/forester/Cargo.toml @@ -44,7 +44,7 @@ futures = { workspace = true } thiserror = { workspace = true } borsh = { workspace = true } bs58 = { workspace = true } -hex = "0.4" +hex = { workspace = true } env_logger = { workspace = true } async-trait = { workspace = true } tracing = { workspace = true } diff --git a/forester/justfile b/forester/justfile index 430267c08f..1ae4223948 100644 --- a/forester/justfile +++ b/forester/justfile @@ -35,3 +35,7 @@ test-compressible-mint: build-compressible-test-deps test-compressible-ctoken: build-compressible-test-deps RUST_LOG=forester=debug,light_client=debug \ cargo test --package forester --test test_compressible_ctoken -- --nocapture + +# Test for indexer interface scenarios (creates test data for photon) +test-indexer-interface: build-test-deps + cargo test --package forester --test test_indexer_interface -- --nocapture diff --git a/forester/src/compressible/bootstrap_helpers.rs b/forester/src/compressible/bootstrap_helpers.rs index c358bacbfc..8ad43ec638 100644 --- a/forester/src/compressible/bootstrap_helpers.rs +++ b/forester/src/compressible/bootstrap_helpers.rs @@ -5,12 +5,18 @@ //! - Account field extraction from JSON responses //! - Standard and V2 API patterns -use std::time::Duration; +use std::{ + sync::{ + atomic::{AtomicBool, Ordering}, + Arc, + }, + time::Duration, +}; use serde_json::json; use solana_sdk::pubkey::Pubkey; -use tokio::time::timeout; -use tracing::debug; +use tokio::{sync::oneshot, time::timeout}; +use tracing::{debug, info}; use super::config::{DEFAULT_PAGE_SIZE, DEFAULT_PAGINATION_DELAY_MS}; use crate::Result; @@ -344,3 +350,127 @@ where Ok((page_count, total_fetched, total_inserted)) } + +/// Result of a bootstrap operation +#[derive(Debug, Clone)] +pub struct BootstrapResult { + /// Number of pages fetched (1 for standard API) + pub pages: usize, + /// Total number of accounts fetched from RPC + pub fetched: usize, + /// Number of accounts successfully inserted/processed + pub inserted: usize, +} + +/// High-level bootstrap runner that handles common scaffolding. +/// +/// This helper encapsulates: +/// - Shutdown flag setup and listener spawning +/// - HTTP client creation +/// - Automatic selection between standard and V2 APIs based on localhost detection +/// - Consistent logging with the provided label +/// +/// # Arguments +/// * `rpc_url` - The RPC endpoint URL +/// * `program_id` - The program ID to fetch accounts from +/// * `filters` - Optional memcmp/dataSize filters for the query +/// * `shutdown_rx` - Optional shutdown receiver for graceful cancellation +/// * `process_fn` - Closure called for each fetched account; returns true if successfully processed +/// * `label` - Label for log messages (e.g., "Mint", "CToken", "PDA") +/// +/// # Returns +/// A `BootstrapResult` containing page count, fetched count, and inserted count. +pub async fn run_bootstrap( + rpc_url: &str, + program_id: &Pubkey, + filters: Option>, + shutdown_rx: Option>, + process_fn: F, + label: &str, +) -> Result +where + F: FnMut(RawAccountData) -> bool, +{ + info!("Starting bootstrap of {} accounts", label); + + // Set up shutdown flag and listener task + let shutdown_flag = Arc::new(AtomicBool::new(false)); + + // Spawn shutdown listener and keep handle for cleanup + let shutdown_listener_handle = shutdown_rx.map(|rx| { + let shutdown_flag_clone = shutdown_flag.clone(); + tokio::spawn(async move { + let _ = rx.await; + shutdown_flag_clone.store(true, Ordering::SeqCst); + }) + }); + + let client = reqwest::Client::new(); + + info!( + "Bootstrapping {} accounts from program {}", + label, program_id + ); + + let result = if is_localhost(rpc_url) { + debug!("Detected localhost, using standard getProgramAccounts"); + let api_result = bootstrap_standard_api( + &client, + rpc_url, + program_id, + filters, + Some(&shutdown_flag), + process_fn, + ) + .await; + + // Abort shutdown listener before returning (success or error) + if let Some(handle) = shutdown_listener_handle { + handle.abort(); + } + + let (fetched, inserted) = api_result?; + + info!( + "{} bootstrap complete: {} fetched, {} inserted", + label, fetched, inserted + ); + + BootstrapResult { + pages: 1, + fetched, + inserted, + } + } else { + debug!("Using getProgramAccountsV2 with pagination"); + let api_result = bootstrap_v2_api( + &client, + rpc_url, + program_id, + filters, + Some(&shutdown_flag), + process_fn, + ) + .await; + + // Abort shutdown listener before returning (success or error) + if let Some(handle) = shutdown_listener_handle { + handle.abort(); + } + + let (pages, fetched, inserted) = api_result?; + + info!( + "{} bootstrap complete: {} pages, {} fetched, {} inserted", + label, pages, fetched, inserted + ); + + BootstrapResult { + pages, + fetched, + inserted, + } + }; + + Ok(result) +} diff --git a/forester/src/compressible/config.rs b/forester/src/compressible/config.rs index 46b65e35b9..14668317a2 100644 --- a/forester/src/compressible/config.rs +++ b/forester/src/compressible/config.rs @@ -34,7 +34,7 @@ pub const DEFAULT_PAGINATION_DELAY_MS: u64 = 100; /// Configuration for a compressible PDA program. /// -/// Can be specified via CLI (using `program_id:discriminator_base58` format) +/// Can be specified via CLI `--compressible-pda-program` (using `program_id:discriminator_base58` format) /// or via config file using the serialized struct format. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PdaProgramConfig { @@ -146,7 +146,7 @@ pub struct CompressibleConfig { #[serde(default = "default_max_concurrent_batches")] pub max_concurrent_batches: usize, /// Compressible PDA programs to track and compress. - /// Can be specified in config file or via CLI `--pda-program` flags. + /// Can be specified in config file or via CLI `--compressible-pda-program` flags. /// CLI values are merged with config file values. #[serde(default)] pub pda_programs: Vec, diff --git a/forester/src/compressible/ctoken/state.rs b/forester/src/compressible/ctoken/state.rs index eaf0272fac..5dbc5b9961 100644 --- a/forester/src/compressible/ctoken/state.rs +++ b/forester/src/compressible/ctoken/state.rs @@ -76,6 +76,7 @@ impl CTokenAccountTracker { /// Update tracker with an already-deserialized Token. /// Use this to avoid double deserialization when the Token is already available. + /// Skips mint accounts (only tracks actual token accounts). pub fn update_from_token( &self, pubkey: Pubkey, @@ -83,6 +84,12 @@ impl CTokenAccountTracker { lamports: u64, account_size: usize, ) -> Result<()> { + // Skip mint accounts - only track actual token accounts + if !ctoken.is_token_account() { + debug!("Skipping non-token account {}", pubkey); + return Ok(()); + } + let compressible_slot = match calculate_compressible_slot(&ctoken, lamports, account_size) { Ok(slot) => slot, Err(e) => { diff --git a/forester/src/compressible/mint/bootstrap.rs b/forester/src/compressible/mint/bootstrap.rs index b20aebfe9d..104c8dd00c 100644 --- a/forester/src/compressible/mint/bootstrap.rs +++ b/forester/src/compressible/mint/bootstrap.rs @@ -1,4 +1,4 @@ -use std::{sync::Arc, time::Duration}; +use std::sync::Arc; use tokio::sync::oneshot; use tracing::{debug, info}; @@ -6,9 +6,7 @@ use tracing::{debug, info}; use super::state::MintAccountTracker; use crate::{ compressible::{ - bootstrap_helpers::{ - bootstrap_standard_api, bootstrap_v2_api, is_localhost, RawAccountData, - }, + bootstrap_helpers::{run_bootstrap, RawAccountData}, config::{ACCOUNT_TYPE_OFFSET, MINT_ACCOUNT_TYPE_FILTER}, traits::CompressibleTracker, }, @@ -21,31 +19,18 @@ pub async fn bootstrap_mint_accounts( tracker: Arc, shutdown_rx: Option>, ) -> Result<()> { - info!("Starting bootstrap of decompressed Mint accounts"); - - // Set up shutdown flag - let shutdown_flag = Arc::new(std::sync::atomic::AtomicBool::new(false)); - - if let Some(rx) = shutdown_rx { - let shutdown_flag_clone = shutdown_flag.clone(); - tokio::spawn(async move { - let _ = rx.await; - shutdown_flag_clone.store(true, std::sync::atomic::Ordering::SeqCst); - }); - } - - let client = reqwest::Client::builder() - .timeout(Duration::from_secs(30)) - .build()?; - // Light Token Program ID let program_id = solana_sdk::pubkey::Pubkey::new_from_array(light_token_interface::LIGHT_TOKEN_PROGRAM_ID); - info!( - "Bootstrapping decompressed Mint accounts from program {}", - program_id - ); + // Filter for decompressed Mint accounts (account_type = 1) + let filters = Some(vec![serde_json::json!({ + "memcmp": { + "offset": ACCOUNT_TYPE_OFFSET, + "bytes": MINT_ACCOUNT_TYPE_FILTER, + "encoding": "base58" + } + })]); // Process function that updates tracker let process_account = |raw_data: RawAccountData| -> bool { @@ -58,50 +43,22 @@ pub async fn bootstrap_mint_accounts( true }; - // Filter for decompressed Mint accounts (account_type = 1) - let filters = Some(vec![serde_json::json!({ - "memcmp": { - "offset": ACCOUNT_TYPE_OFFSET, - "bytes": MINT_ACCOUNT_TYPE_FILTER, - "encoding": "base58" - } - })]); - - if is_localhost(&rpc_url) { - let (total_fetched, total_inserted) = bootstrap_standard_api( - &client, - &rpc_url, - &program_id, - filters, - Some(&shutdown_flag), - process_account, - ) - .await?; - - info!( - "Mint bootstrap complete: {} fetched, {} decompressed mints tracked", - total_fetched, total_inserted - ); - } else { - let (page_count, total_fetched, total_inserted) = bootstrap_v2_api( - &client, - &rpc_url, - &program_id, - filters, - Some(&shutdown_flag), - process_account, - ) - .await?; - - info!( - "Mint bootstrap finished: {} pages, {} fetched, {} decompressed mints tracked", - page_count, total_fetched, total_inserted - ); - } + let result = run_bootstrap( + &rpc_url, + &program_id, + filters, + shutdown_rx, + process_account, + "Mint", + ) + .await?; info!( - "Mint bootstrap finished: {} total mints tracked", - tracker.len() + "Mint bootstrap finished: {} total mints tracked (fetched: {}, inserted: {}, pages: {})", + tracker.len(), + result.fetched, + result.inserted, + result.pages ); Ok(()) diff --git a/forester/src/compressible/mint/state.rs b/forester/src/compressible/mint/state.rs index db19e6dd10..4ddebb4847 100644 --- a/forester/src/compressible/mint/state.rs +++ b/forester/src/compressible/mint/state.rs @@ -19,12 +19,13 @@ fn calculate_compressible_slot(mint: &Mint, lamports: u64, account_size: usize) let rent_exemption = get_rent_exemption_lamports(account_size as u64) .map_err(|e| anyhow::anyhow!("Failed to get rent exemption: {:?}", e))?; let compression_info = &mint.compression; + let config = &compression_info.rent_config; let last_funded_epoch = get_last_funded_epoch( account_size as u64, lamports, compression_info.last_claimed_slot, - &compression_info.rent_config, + config, rent_exemption, ); diff --git a/forester/src/compressible/pda/compressor.rs b/forester/src/compressible/pda/compressor.rs index 188057544a..98709294dc 100644 --- a/forester/src/compressible/pda/compressor.rs +++ b/forester/src/compressible/pda/compressor.rs @@ -105,6 +105,17 @@ impl PdaCompressor { ) })?; + /* + // Verify PDA derivation matches (mirrors LightConfig::load_checked) + let (expected_pda, _) = LightConfig::derive_pda(program_id, config.config_bump); + if expected_pda != config_pda { + return Err(anyhow::anyhow!( + "Config PDA derivation mismatch. Expected: {}. Found: {}", + expected_pda, + config_pda + )); + } + */ let rent_sponsor = config.rent_sponsor; let compression_authority = config.compression_authority; let address_tree = *config @@ -306,14 +317,17 @@ impl PdaCompressor { "Batched compress_accounts_idempotent tx confirmed: {}", signature ); + Ok(signature) } else { tracing::warn!( "compress_accounts_idempotent tx not confirmed: {} - accounts kept in tracker for retry", signature ); + Err(anyhow::anyhow!( + "Batch transaction not confirmed: {}", + signature + )) } - - Ok(signature) } /// Compress a single PDA account using cached config diff --git a/forester/tests/e2e_test.rs b/forester/tests/e2e_test.rs index 0f0387bb0c..f18f136f07 100644 --- a/forester/tests/e2e_test.rs +++ b/forester/tests/e2e_test.rs @@ -276,6 +276,7 @@ async fn e2e_test() { )], upgradeable_programs: vec![], limit_ledger_size: None, + validator_args: vec![], })) .await; spawn_prover().await; diff --git a/forester/tests/legacy/address_v2_test.rs b/forester/tests/legacy/address_v2_test.rs index aa3a821152..5dc063236a 100644 --- a/forester/tests/legacy/address_v2_test.rs +++ b/forester/tests/legacy/address_v2_test.rs @@ -62,6 +62,7 @@ async fn test_create_v2_address() { )], upgradeable_programs: vec![], limit_ledger_size: Some(500000), + validator_args: vec![], })) .await; diff --git a/forester/tests/legacy/batched_address_test.rs b/forester/tests/legacy/batched_address_test.rs index fe5fe363d0..72faee88fc 100644 --- a/forester/tests/legacy/batched_address_test.rs +++ b/forester/tests/legacy/batched_address_test.rs @@ -43,6 +43,7 @@ async fn test_address_batched() { )], upgradeable_programs: vec![], limit_ledger_size: None, + validator_args: vec![], })) .await; let tree_params = InitAddressTreeAccountsInstructionData::test_default(); diff --git a/forester/tests/legacy/batched_state_async_indexer_test.rs b/forester/tests/legacy/batched_state_async_indexer_test.rs index adc84a823c..b57dd3fce7 100644 --- a/forester/tests/legacy/batched_state_async_indexer_test.rs +++ b/forester/tests/legacy/batched_state_async_indexer_test.rs @@ -83,6 +83,7 @@ async fn test_state_indexer_async_batched() { sbf_programs: vec![], upgradeable_programs: vec![], limit_ledger_size: None, + validator_args: vec![], })) .await; spawn_prover().await; diff --git a/forester/tests/legacy/batched_state_indexer_test.rs b/forester/tests/legacy/batched_state_indexer_test.rs index 2b9600a7f8..dc1d69c12b 100644 --- a/forester/tests/legacy/batched_state_indexer_test.rs +++ b/forester/tests/legacy/batched_state_indexer_test.rs @@ -44,6 +44,7 @@ async fn test_state_indexer_batched() { sbf_programs: vec![], upgradeable_programs: vec![], limit_ledger_size: None, + validator_args: vec![], })) .await; diff --git a/forester/tests/legacy/batched_state_test.rs b/forester/tests/legacy/batched_state_test.rs index 3067ea3a3d..44790f59f0 100644 --- a/forester/tests/legacy/batched_state_test.rs +++ b/forester/tests/legacy/batched_state_test.rs @@ -48,6 +48,7 @@ async fn test_state_batched() { sbf_programs: vec![], upgradeable_programs: vec![], limit_ledger_size: None, + validator_args: vec![], })) .await; diff --git a/forester/tests/legacy/e2e_test.rs b/forester/tests/legacy/e2e_test.rs index 69dadc8b39..871b5c2f94 100644 --- a/forester/tests/legacy/e2e_test.rs +++ b/forester/tests/legacy/e2e_test.rs @@ -40,6 +40,7 @@ async fn test_epoch_monitor_with_2_foresters() { sbf_programs: vec![], upgradeable_programs: vec![], limit_ledger_size: None, + validator_args: vec![], })) .await; let forester_keypair1 = Keypair::new(); @@ -387,6 +388,7 @@ async fn test_epoch_double_registration() { sbf_programs: vec![], upgradeable_programs: vec![], limit_ledger_size: None, + validator_args: vec![], })) .await; diff --git a/forester/tests/legacy/e2e_v1_test.rs b/forester/tests/legacy/e2e_v1_test.rs index ffe207dbea..d8308bb3c5 100644 --- a/forester/tests/legacy/e2e_v1_test.rs +++ b/forester/tests/legacy/e2e_v1_test.rs @@ -41,6 +41,7 @@ async fn test_e2e_v1() { sbf_programs: vec![], upgradeable_programs: vec![], limit_ledger_size: None, + validator_args: vec![], })) .await; let forester_keypair1 = Keypair::new(); @@ -384,6 +385,7 @@ async fn test_epoch_double_registration() { sbf_programs: vec![], upgradeable_programs: vec![], limit_ledger_size: None, + validator_args: vec![], })) .await; diff --git a/forester/tests/test_batch_append_spent.rs b/forester/tests/test_batch_append_spent.rs index b923662ca5..058d8f1575 100644 --- a/forester/tests/test_batch_append_spent.rs +++ b/forester/tests/test_batch_append_spent.rs @@ -51,6 +51,7 @@ async fn test_batch_sequence() { sbf_programs: vec![], upgradeable_programs: vec![], limit_ledger_size: None, + validator_args: vec![], })) .await; diff --git a/forester/tests/test_compressible_ctoken.rs b/forester/tests/test_compressible_ctoken.rs index 4bd135b9b5..bee072e87a 100644 --- a/forester/tests/test_compressible_ctoken.rs +++ b/forester/tests/test_compressible_ctoken.rs @@ -90,22 +90,52 @@ async fn register_forester( // Calculate epoch info let current_slot = rpc.get_slot().await?; let current_epoch = protocol_config.get_current_epoch(current_slot); - println!("current_epoch {:?}", current_epoch); let phases = get_epoch_phases(&protocol_config, current_epoch); - let register_phase_start = phases.registration.start; - let active_phase_start = phases.active.start; - println!("phases {:?}", phases); - println!("current_slot {}", current_slot); + + println!( + "Current slot: {}, current_epoch: {}, phases: {:?}", + current_slot, current_epoch, phases + ); + + // Determine which epoch to register for: + // If we're already past the registration phase start, we might be in active phase + // and need to wait for the next epoch's registration + let (target_epoch, target_phases) = if current_slot >= phases.active.start { + // Already in active phase, register for next epoch + 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) + } else if current_slot >= phases.registration.start { + // In registration phase, register for current epoch + println!("In registration phase for epoch {}", current_epoch); + (current_epoch, phases) + } else { + // Before registration phase, wait for it + println!( + "Waiting for registration phase (starts at slot {})", + phases.registration.start + ); + (current_epoch, phases) + }; + + let register_phase_start = target_phases.registration.start; + let active_phase_start = target_phases.active.start; // Wait for registration phase while rpc.get_slot().await? < register_phase_start { sleep(Duration::from_millis(400)).await; } - // Register for epoch 0 - let epoch = 0u64; - let register_epoch_ix = - create_register_forester_epoch_pda_instruction(&forester_pubkey, &forester_pubkey, epoch); + // 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( @@ -116,23 +146,18 @@ async fn register_forester( ); rpc.process_transaction(tx).await?; - println!("Registered for epoch {}", epoch); - - println!( - "Waiting for active phase (current slot: {}, active phase starts at: {})...", - current_slot, active_phase_start - ); + println!("Registered for epoch {}", target_epoch); // Wait for active phase while rpc.get_slot().await? < active_phase_start { sleep(Duration::from_millis(400)).await; } - println!("Active phase reached"); + println!("Active phase reached for epoch {}", target_epoch); // Finalize registration let finalize_ix = - create_finalize_registration_instruction(&forester_pubkey, &forester_pubkey, epoch); + create_finalize_registration_instruction(&forester_pubkey, &forester_pubkey, target_epoch); let (blockhash, _) = rpc.get_latest_blockhash().await?; let tx = Transaction::new_signed_with_payer( @@ -160,10 +185,10 @@ async fn register_forester( use light_registry::protocol_config::state::EpochState; let epoch_struct = Epoch { - epoch, + epoch: target_epoch, epoch_pda: solana_sdk::pubkey::Pubkey::default(), forester_epoch_pda: solana_sdk::pubkey::Pubkey::default(), - phases, + phases: target_phases, state: EpochState::Active, merkle_trees: vec![], }; @@ -194,6 +219,7 @@ async fn test_compressible_ctoken_compression() { sbf_programs: vec![], upgradeable_programs: vec![], limit_ledger_size: None, + validator_args: vec![], }) .await; let mut rpc = LightClient::new(LightClientConfig::local()) @@ -365,6 +391,7 @@ async fn test_compressible_ctoken_bootstrap() { sbf_programs: vec![], upgradeable_programs: vec![], limit_ledger_size: None, + validator_args: vec![], }) .await; @@ -467,7 +494,7 @@ async fn run_bootstrap_test( }); if expected_count > 0 { - // Wait for bootstrap to find expected number of accounts (with timeout) + // Wait for bootstrap to find at least expected number of accounts (with timeout) let start = tokio::time::Instant::now(); let timeout = Duration::from_secs(60); @@ -479,12 +506,13 @@ async fn run_bootstrap_test( sleep(Duration::from_millis(500)).await; } - // Assert bootstrap picked up all accounts - assert_eq!( - tracker.len(), + // Assert bootstrap picked up at least the expected accounts + // (there may be more from previous tests sharing the validator) + assert!( + tracker.len() >= expected_count, + "Bootstrap should have found at least {} accounts, found {}", expected_count, - "Bootstrap should have found all {} accounts", - expected_count + tracker.len() ); } else { // Mainnet test: wait a bit for bootstrap to run @@ -498,14 +526,13 @@ async fn run_bootstrap_test( if let Some((expected_pubkeys, expected_mint)) = expected_data { // Verify specific accounts (localhost test) - // Verify all created accounts are in tracker + // Verify all created accounts are in tracker and have correct data for pubkey in &expected_pubkeys { - let found = accounts.iter().any(|acc| acc.pubkey == *pubkey); - assert!(found, "Bootstrap should have found account {}", pubkey); - } + let account_state = accounts + .iter() + .find(|acc| acc.pubkey == *pubkey) + .unwrap_or_else(|| panic!("Bootstrap should have found account {}", pubkey)); - // Verify account data is correct - for account_state in &accounts { println!( "Verifying account {}: mint={:?}, lamports={}", account_state.pubkey, account_state.account.mint, account_state.lamports diff --git a/forester/tests/test_compressible_mint.rs b/forester/tests/test_compressible_mint.rs index 9ace18f774..714f1da2be 100644 --- a/forester/tests/test_compressible_mint.rs +++ b/forester/tests/test_compressible_mint.rs @@ -23,13 +23,48 @@ use tokio::{ time::sleep, }; -/// Helper to create a compressed mint with decompression +/// Build an expected Mint for assertion comparison. +/// +/// Takes known values from test setup plus runtime values extracted from the on-chain account. +fn build_expected_mint( + mint_authority: &Pubkey, + decimals: u8, + mint_pda: &Pubkey, + mint_signer: &[u8; 32], + bump: u8, + version: u8, + compression: light_compressible::compression_info::CompressionInfo, +) -> Mint { + Mint { + base: BaseMint { + mint_authority: Some((*mint_authority).into()), + supply: 0, + decimals, + is_initialized: true, + freeze_authority: None, + }, + metadata: MintMetadata { + version, + mint_decompressed: true, + mint: (*mint_pda).into(), + mint_signer: *mint_signer, + bump, + }, + reserved: [0u8; 16], + account_type: ACCOUNT_TYPE_MINT, + compression, + extensions: None, + } +} + +/// Helper to create a compressed mint with decompression. +/// Returns (mint_pda, compression_address, mint_seed, bump). async fn create_decompressed_mint( rpc: &mut (impl Rpc + Indexer), payer: &Keypair, mint_authority: Pubkey, decimals: u8, -) -> (Pubkey, [u8; 32], Keypair) { +) -> (Pubkey, [u8; 32], Keypair, u8) { let mint_seed = Keypair::new(); let address_tree = rpc.get_address_tree_v2(); let output_queue = rpc.get_random_state_tree_info().unwrap().queue; @@ -84,7 +119,7 @@ async fn create_decompressed_mint( .await .expect("CreateMint should succeed"); - (mint_pda, compression_address, mint_seed) + (mint_pda, compression_address, mint_seed, bump) } /// Test that Mint bootstrap discovers decompressed mints @@ -106,6 +141,7 @@ async fn test_compressible_mint_bootstrap() { sbf_programs: vec![], upgradeable_programs: vec![], limit_ledger_size: None, + validator_args: vec![], }) .await; @@ -129,13 +165,13 @@ async fn test_compressible_mint_bootstrap() { .expect("Failed to wait for indexer"); // Create a decompressed mint - let (mint_pda, compression_address, mint_seed) = + let (mint_pda, compression_address, mint_seed, bump) = create_decompressed_mint(&mut rpc, &payer, payer.pubkey(), 9).await; println!("Created decompressed mint at: {}", mint_pda); println!("Compression address: {:?}", compression_address); - // Verify mint exists on-chain + // Verify mint exists on-chain and matches expected structure let mint_account = rpc.get_account(mint_pda).await.unwrap(); assert!(mint_account.is_some(), "Mint should exist after creation"); @@ -143,36 +179,18 @@ async fn test_compressible_mint_bootstrap() { let mint_data = mint_account.unwrap(); let mint = Mint::deserialize(&mut &mint_data.data[..]).expect("Failed to deserialize Mint"); - // Extract runtime-specific values from deserialized mint - let compression = mint.compression; - let metadata_version = mint.metadata.version; - - // Derive the bump from mint_seed - let (_, bump) = find_mint_address(&mint_seed.pubkey()); - - // Build expected Mint - let expected_mint = Mint { - base: BaseMint { - mint_authority: Some(payer.pubkey().to_bytes().into()), - supply: 0, - decimals: 9, - is_initialized: true, - freeze_authority: None, - }, - metadata: MintMetadata { - version: metadata_version, - mint_decompressed: true, - mint: mint_pda.to_bytes().into(), - mint_signer: mint_seed.pubkey().to_bytes(), - bump, - }, - reserved: [0u8; 16], - account_type: ACCOUNT_TYPE_MINT, - compression, - extensions: None, - }; + // Build expected mint using known values plus runtime compression info + let expected_mint = build_expected_mint( + &payer.pubkey(), + 9, + &mint_pda, + &mint_seed.pubkey().to_bytes(), + bump, + mint.metadata.version, + mint.compression, + ); - assert_eq!(mint, expected_mint, "Mint should match expected state"); + assert_eq!(mint, expected_mint, "Mint should match expected structure"); // Wait for indexer wait_for_indexer(&rpc) @@ -261,6 +279,7 @@ async fn test_compressible_mint_compression() { sbf_programs: vec![], upgradeable_programs: vec![], limit_ledger_size: None, + validator_args: vec![], }) .await; @@ -284,7 +303,7 @@ async fn test_compressible_mint_compression() { .expect("Failed to wait for indexer"); // Create a decompressed mint - let (mint_pda, compression_address, mint_seed) = + let (mint_pda, compression_address, mint_seed, bump) = create_decompressed_mint(&mut rpc, &payer, payer.pubkey(), 9).await; println!("Created decompressed mint at: {}", mint_pda); @@ -301,9 +320,6 @@ async fn test_compressible_mint_compression() { let compression = mint.compression; let metadata_version = mint.metadata.version; - // Derive the bump from mint_seed - let (_, bump) = find_mint_address(&mint_seed.pubkey()); - // Build expected Mint let expected_mint = Mint { base: BaseMint { @@ -364,55 +380,92 @@ async fn test_compressible_mint_compression() { let ready_accounts = tracker.get_ready_to_compress(current_slot); println!("Ready to compress: {} mints", ready_accounts.len()); - if !ready_accounts.is_empty() { - // Create compressor and compress - let compressor = - MintCompressor::new(rpc_pool.clone(), tracker.clone(), payer.insecure_clone()); - - println!("Compressing Mint..."); - let compress_result = compressor.compress_batch(&ready_accounts).await; - - let signature = compress_result.expect("Compression should succeed"); - println!("Compression transaction sent: {}", signature); - - // Wait for account to be closed - let start = tokio::time::Instant::now(); - let timeout = Duration::from_secs(30); - let mut account_closed = false; - - while start.elapsed() < timeout { - let mint_after = rpc - .get_account(mint_pda) - .await - .expect("Failed to query mint account"); - if mint_after.is_none() { - account_closed = true; - println!("Mint account closed successfully!"); - break; - } - sleep(Duration::from_millis(500)).await; - } + assert!( + !ready_accounts.is_empty(), + "Mint should be ready to compress with rent_payment=0" + ); + + // Create compressor and compress + let compressor = MintCompressor::new(rpc_pool.clone(), tracker.clone(), payer.insecure_clone()); - assert!( - account_closed, - "Mint account should be closed after compression" - ); + println!("Compressing Mint..."); + let compress_result = compressor.compress_batch(&ready_accounts).await; + + let signature = compress_result.expect("Compression should succeed"); + println!("Compression transaction sent: {}", signature); + + // Wait for account to be closed + let start = tokio::time::Instant::now(); + let timeout = Duration::from_secs(30); + let mut account_closed = false; - // Verify compressed mint still exists in the merkle tree - let compressed_after = rpc - .get_compressed_account(compression_address, None) + while start.elapsed() < timeout { + let mint_after = rpc + .get_account(mint_pda) .await - .unwrap() - .value; - assert!( - compressed_after.is_some(), - "Compressed mint should still exist after compression" - ); - - println!("Mint compression test completed successfully!"); - } else { - panic!("Mint should be ready to compress with rent_payment=0"); + .expect("Failed to query mint account"); + if mint_after.is_none() || mint_after.as_ref().map(|a| a.lamports) == Some(0) { + account_closed = true; + println!("Mint account closed successfully!"); + break; + } + sleep(Duration::from_millis(500)).await; } + + assert!( + account_closed, + "Mint account should be closed after compression" + ); + + wait_for_indexer(&rpc) + .await + .expect("Failed to wait for indexer"); + + // Verify compressed mint still exists in the merkle tree + let compressed_after = rpc + .get_compressed_account(compression_address, None) + .await + .unwrap() + .value; + assert!( + compressed_after.is_some(), + "Compressed mint should still exist after compression" + ); + + // Test Photon API: get_compressed_mint + println!("Testing Photon get_compressed_mint API..."); + let mint_response = rpc + .get_compressed_mint(compression_address, None) + .await + .expect("get_compressed_mint should succeed"); + + let compressed_mint = mint_response + .value + .expect("Compressed mint should be returned by get_compressed_mint"); + + assert_eq!(compressed_mint.mint.decimals, 9, "Decimals should match"); + assert_eq!( + compressed_mint.mint.mint_authority, + Some(payer.pubkey()), + "Mint authority should be payer" + ); + println!( + "Photon get_compressed_mint verified: decimals={}, supply={}", + compressed_mint.mint.decimals, compressed_mint.mint.supply + ); + + // Test Photon API: get_compressed_mint_by_pda + let mint_by_pda = rpc + .get_compressed_mint_by_pda(&mint_pda, None) + .await + .expect("get_compressed_mint_by_pda should succeed"); + assert!( + mint_by_pda.value.is_some(), + "Should find compressed mint by PDA" + ); + println!("Photon get_compressed_mint_by_pda verified!"); + + println!("Mint compression test completed successfully!"); } /// Test AccountSubscriber for Mint accounts @@ -436,6 +489,7 @@ async fn test_compressible_mint_subscription() { sbf_programs: vec![], upgradeable_programs: vec![], limit_ledger_size: None, + validator_args: vec![], }) .await; @@ -481,7 +535,7 @@ async fn test_compressible_mint_subscription() { sleep(Duration::from_secs(2)).await; // Create first decompressed mint (immediately compressible with rent_payment=0) - let (mint_pda_1, compression_address_1, _mint_seed_1) = + let (mint_pda_1, compression_address_1, _mint_seed_1, _bump_1) = create_decompressed_mint(&mut rpc, &payer, payer.pubkey(), 9).await; println!("Created first decompressed mint at: {}", mint_pda_1); @@ -508,7 +562,7 @@ async fn test_compressible_mint_subscription() { println!("Tracker detected first mint via subscription"); // Create second decompressed mint - let (mint_pda_2, _compression_address_2, _mint_seed_2) = + let (mint_pda_2, _compression_address_2, _mint_seed_2, _bump_2) = create_decompressed_mint(&mut rpc, &payer, payer.pubkey(), 6).await; println!("Created second decompressed mint at: {}", mint_pda_2); @@ -622,6 +676,94 @@ async fn test_compressible_mint_subscription() { "Compressed mint should still exist after compression" ); + wait_for_indexer(&rpc) + .await + .expect("Failed to wait for indexer"); + + // Test Photon API: get_compressed_mint by address + println!("Testing Photon get_compressed_mint API..."); + let mint_response = rpc + .get_compressed_mint(compression_address_1, None) + .await + .expect("get_compressed_mint should succeed"); + + let compressed_mint = mint_response + .value + .expect("Compressed mint should be returned by get_compressed_mint"); + + // Verify mint data matches what we created + assert_eq!( + compressed_mint.mint.decimals, 9, + "Decimals should match what we created" + ); + assert_eq!( + compressed_mint.mint.mint_authority, + Some(payer.pubkey()), + "Mint authority should be payer" + ); + assert!( + !compressed_mint.mint.mint_decompressed, + "Mint should NOT be marked as decompressed after compression" + ); + println!( + "get_compressed_mint verified: decimals={}, supply={}", + compressed_mint.mint.decimals, compressed_mint.mint.supply + ); + + // Test Photon API: get_compressed_mint_by_pda + println!("Testing Photon get_compressed_mint_by_pda API..."); + let mint_by_pda = rpc + .get_compressed_mint_by_pda(&mint_pda_1, None) + .await + .expect("get_compressed_mint_by_pda should succeed"); + + assert!( + mint_by_pda.value.is_some(), + "Compressed mint should be found by PDA" + ); + assert_eq!( + mint_by_pda.value.as_ref().unwrap().mint.decimals, + compressed_mint.mint.decimals, + "Mint found by PDA should match mint found by address" + ); + println!("get_compressed_mint_by_pda verified!"); + + // Test Photon API: get_compressed_mints_by_authority + println!("Testing Photon get_compressed_mints_by_authority API..."); + let mints_by_authority = rpc + .get_compressed_mints_by_authority( + &payer.pubkey(), + light_client::indexer::MintAuthorityType::Either, + None, + None, + ) + .await + .expect("get_compressed_mints_by_authority should succeed"); + + // We compressed mint_pda_1 (payer is authority), and mint_pda_2 is still decompressed + // So we should have exactly 1 compressed mint with payer as authority + assert!( + !mints_by_authority.value.items.is_empty(), + "Should find at least 1 compressed mint by authority" + ); + println!( + "get_compressed_mints_by_authority found {} mints for authority {}", + mints_by_authority.value.items.len(), + payer.pubkey() + ); + + // Verify the first mint in the list is the one we compressed + let found_mint = mints_by_authority + .value + .items + .iter() + .find(|m| m.account.address == Some(compression_address_1)); + assert!( + found_mint.is_some(), + "Should find the mint with compression_address_1 in authority query results" + ); + println!("Photon API tests completed successfully!"); + // Shutdown subscribers shutdown_tx .send(()) diff --git a/forester/tests/test_compressible_pda.rs b/forester/tests/test_compressible_pda.rs index 97d4095620..ae08b12ddb 100644 --- a/forester/tests/test_compressible_pda.rs +++ b/forester/tests/test_compressible_pda.rs @@ -16,7 +16,7 @@ use forester_utils::{ use light_client::{ indexer::Indexer, interface::{get_create_accounts_proof, CreateAccountsProofInput, InitializeRentFreeConfig}, - local_test_validator::{spawn_validator, LightValidatorConfig}, + local_test_validator::{spawn_validator, LightValidatorConfig, UpgradeableProgramConfig}, rpc::{LightClient, LightClientConfig, Rpc}, }; use light_compressed_account::address::derive_address; @@ -261,12 +261,13 @@ async fn test_compressible_pda_bootstrap() { enable_prover: true, wait_time: 60, sbf_programs: vec![], - upgradeable_programs: vec![( + upgradeable_programs: vec![UpgradeableProgramConfig::new( CSDK_TEST_PROGRAM_ID.to_string(), "../target/deploy/csdk_anchor_full_derived_test.so".to_string(), payer_pubkey_string(), )], limit_ledger_size: None, + validator_args: vec![], }) .await; @@ -452,12 +453,13 @@ async fn test_compressible_pda_compression() { enable_prover: true, wait_time: 60, sbf_programs: vec![], - upgradeable_programs: vec![( + upgradeable_programs: vec![UpgradeableProgramConfig::new( CSDK_TEST_PROGRAM_ID.to_string(), "../target/deploy/csdk_anchor_full_derived_test.so".to_string(), payer_pubkey_string(), )], limit_ledger_size: None, + validator_args: vec![], }) .await; @@ -687,12 +689,13 @@ async fn test_compressible_pda_subscription() { enable_prover: true, wait_time: 60, sbf_programs: vec![], - upgradeable_programs: vec![( + upgradeable_programs: vec![UpgradeableProgramConfig::new( CSDK_TEST_PROGRAM_ID.to_string(), "../target/deploy/csdk_anchor_full_derived_test.so".to_string(), payer_pubkey_string(), )], limit_ledger_size: None, + validator_args: vec![], }) .await; diff --git a/forester/tests/test_indexer_interface.rs b/forester/tests/test_indexer_interface.rs new file mode 100644 index 0000000000..d87b3a8b4a --- /dev/null +++ b/forester/tests/test_indexer_interface.rs @@ -0,0 +1,779 @@ +/// Test scenarios for indexer interface endpoints. +/// +/// This test creates various account types for testing the indexer's interface racing logic. +/// After running, use `cargo xtask export-photon-test-data --test-name indexer_interface` +/// to export transactions to the indexer's test snapshot directory. +/// +/// Scenarios covered: +/// 1. SPL Mint (on-chain) - standard mint for token operations +/// 2. Compressed token accounts (via mint_to) - for getTokenAccountInterface +/// 3. Registered v2 address in batched address tree - for address tree verification +/// 4. Decompressed mint (via CreateMint with rent_payment=0) - for getMintInterface (on-chain CMint) +/// 5. Fully compressed mint (CreateMint + CompressAndCloseMint) - for getMintInterface (compressed DB) +/// 6. Compressible token accounts - on-chain accounts that can be compressed +use std::{collections::HashMap, time::Duration}; + +use anchor_lang::Discriminator; +use borsh::BorshSerialize; +use create_address_test_program::create_invoke_cpi_instruction; +use forester_utils::utils::wait_for_indexer; +use light_client::{ + indexer::{photon_indexer::PhotonIndexer, AddressWithTree, ColdContext, Indexer}, + local_test_validator::{spawn_validator, LightValidatorConfig}, + rpc::{LightClient, LightClientConfig, Rpc}, +}; +use light_compressed_account::{ + address::derive_address, + instruction_data::{ + data::NewAddressParamsAssigned, with_readonly::InstructionDataInvokeCpiWithReadOnly, + }, +}; +use light_compressed_token::{ + process_mint::mint_sdk::create_mint_to_instruction, + process_transfer::transfer_sdk::to_account_metas, +}; +use light_test_utils::{ + actions::legacy::{ + create_compressible_token_account, + instructions::mint_action::{ + create_mint_action_instruction, MintActionParams, MintActionType, + }, + CreateCompressibleTokenAccountInputs, + }, + pack::pack_new_address_params_assigned, + spl::create_mint_helper_with_keypair, +}; +use light_token::instruction::{ + derive_mint_compressed_address, find_mint_address, CreateMint, CreateMintParams, +}; +use light_token_interface::state::TokenDataVersion; +use serial_test::serial; +use solana_sdk::{ + pubkey::Pubkey, + signature::{Keypair, Signature}, + signer::Signer, + transaction::Transaction, +}; +use tokio::time::sleep; + +const COMPUTE_BUDGET_LIMIT: u32 = 1_000_000; + +/// Helper to mint compressed tokens +async fn mint_compressed_tokens( + rpc: &mut R, + merkle_tree_pubkey: &Pubkey, + payer: &Keypair, + mint_pubkey: &Pubkey, + recipients: Vec, + amounts: Vec, +) -> Signature { + let mint_to_ix = create_mint_to_instruction( + &payer.pubkey(), + &payer.pubkey(), + mint_pubkey, + merkle_tree_pubkey, + amounts, + recipients, + None, + false, + 0, + ); + let instructions = vec![ + solana_sdk::compute_budget::ComputeBudgetInstruction::set_compute_unit_limit( + COMPUTE_BUDGET_LIMIT, + ), + mint_to_ix, + ]; + rpc.create_and_send_transaction(&instructions, &payer.pubkey(), &[payer]) + .await + .unwrap() +} + +/// Test that creates scenarios for Photon interface testing +/// +/// Run with: cargo test -p forester --test test_indexer_interface -- --nocapture +/// Then export: cargo xtask export-photon-test-data --test-name indexer_interface +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] +#[serial] +async fn test_indexer_interface_scenarios() { + // Start validator with indexer, prover, and create_address_test_program + spawn_validator(LightValidatorConfig { + enable_indexer: true, + enable_prover: true, + wait_time: 90, + sbf_programs: vec![( + "FNt7byTHev1k5x2cXZLBr8TdWiC3zoP5vcnZR4P682Uy".to_string(), + "../target/deploy/create_address_test_program.so".to_string(), + )], + upgradeable_programs: vec![], + limit_ledger_size: None, + validator_args: vec![], + }) + .await; + + let mut rpc = LightClient::new(LightClientConfig::local()) + .await + .expect("Failed to create LightClient"); + rpc.get_latest_active_state_trees() + .await + .expect("Failed to get state trees"); + + let payer = rpc.get_payer().insecure_clone(); + rpc.airdrop_lamports(&payer.pubkey(), 100_000_000_000) + .await + .expect("Failed to airdrop to payer"); + + // Give extra time for indexer to fully start + sleep(Duration::from_secs(5)).await; + + // Wait for indexer to be ready before making any requests + wait_for_indexer(&rpc) + .await + .expect("Failed to wait for indexer"); + + println!("\n========== PHOTON INTERFACE TEST ==========\n"); + println!("Payer: {}", payer.pubkey()); + + // ============ Scenario 1: Create SPL Mint ============ + println!("\n=== Creating SPL mint ==="); + + let mint_keypair = Keypair::new(); + let mint_pubkey = create_mint_helper_with_keypair(&mut rpc, &payer, &mint_keypair).await; + println!("SPL Mint: {}", mint_pubkey); + + // ============ Scenario 2: Create compressed token accounts ============ + println!("\n=== Creating compressed token accounts ==="); + + let bob = Keypair::new(); + let charlie = Keypair::new(); + + let state_tree_info = rpc.get_random_state_tree_info().unwrap(); + + // Mint compressed tokens to Bob and Charlie + let mint_sig = mint_compressed_tokens( + &mut rpc, + &state_tree_info.queue, + &payer, + &mint_pubkey, + vec![bob.pubkey(), charlie.pubkey()], + vec![1_000_000_000, 500_000_000], + ) + .await; + println!("Minted compressed tokens: {}", mint_sig); + println!("Bob pubkey: {}", bob.pubkey()); + println!("Charlie pubkey: {}", charlie.pubkey()); + + // Wait for indexer + sleep(Duration::from_secs(3)).await; + + // ============ Scenario 3: Register v2 Address (using create_address_test_program) ============ + println!("\n=== Registering v2 address in batched address tree ==="); + + // Use v2 (batched) address tree + let address_tree = rpc.get_address_tree_v2(); + + // Create a deterministic seed for the address + let address_seed: [u8; 32] = [42u8; 32]; + + // Derive address using v2 method (includes program ID) + let derived_address = derive_address( + &address_seed, + &address_tree.tree.to_bytes(), + &create_address_test_program::ID.to_bytes(), + ); + + println!("Derived v2 address: {:?}", derived_address); + + // Get validity proof for the new address + wait_for_indexer(&rpc).await.unwrap(); + let proof_result = rpc + .indexer() + .unwrap() + .get_validity_proof( + vec![], + vec![AddressWithTree { + address: derived_address, + tree: address_tree.tree, + }], + None, + ) + .await + .unwrap(); + + // Build new address params + let new_address_params = vec![NewAddressParamsAssigned { + seed: address_seed, + address_queue_pubkey: address_tree.tree.into(), // For batched trees, queue = tree + address_merkle_tree_pubkey: address_tree.tree.into(), + address_merkle_tree_root_index: proof_result.value.get_address_root_indices()[0], + assigned_account_index: None, + }]; + + // Pack the address params for the instruction + let mut remaining_accounts = HashMap::::new(); + let packed_new_address_params = + pack_new_address_params_assigned(&new_address_params, &mut remaining_accounts); + + // Build instruction data for create_address_test_program + let ix_data = InstructionDataInvokeCpiWithReadOnly { + mode: 0, + bump: 255, + with_cpi_context: false, + invoking_program_id: create_address_test_program::ID.into(), + proof: proof_result.value.proof.0, + new_address_params: packed_new_address_params, + is_compress: false, + compress_or_decompress_lamports: 0, + output_compressed_accounts: Default::default(), + input_compressed_accounts: Default::default(), + with_transaction_hash: true, + read_only_accounts: Vec::new(), + read_only_addresses: Vec::new(), + cpi_context: Default::default(), + }; + + let remaining_accounts_metas = to_account_metas(remaining_accounts); + + // Create the instruction using the test program + let instruction = create_invoke_cpi_instruction( + payer.pubkey(), + [ + light_system_program::instruction::InvokeCpiWithReadOnly::DISCRIMINATOR.to_vec(), + ix_data.try_to_vec().unwrap(), + ] + .concat(), + remaining_accounts_metas, + None, + ); + + let instructions = vec![ + solana_sdk::compute_budget::ComputeBudgetInstruction::set_compute_unit_limit( + COMPUTE_BUDGET_LIMIT, + ), + instruction, + ]; + let address_sig = rpc + .create_and_send_transaction(&instructions, &payer.pubkey(), &[&payer]) + .await + .unwrap(); + println!( + "Registered v2 address: {} (sig: {})", + hex::encode(derived_address), + address_sig + ); + + // ============ Scenario 4: Decompressed Mint (CreateMint with rent_payment=0) ============ + // This creates a compressed mint that is immediately decompressed to an on-chain CMint account. + // The compressed account only contains the 32-byte mint_pda reference (DECOMPRESSED_PDA_DISCRIMINATOR). + // Full mint data is on-chain in the CMint account owned by LIGHT_TOKEN_PROGRAM_ID. + println!("\n=== Creating decompressed mint (on-chain CMint) ==="); + + let decompressed_mint_seed = Keypair::new(); + let output_queue = rpc.get_random_state_tree_info().unwrap().queue; + + // Use v2 address tree for compressed mints + let mint_address_tree = rpc.get_address_tree_v2(); + + // Derive compression address for decompressed mint + let decompressed_mint_compression_address = + derive_mint_compressed_address(&decompressed_mint_seed.pubkey(), &mint_address_tree.tree); + + let (decompressed_mint_pda, decompressed_mint_bump) = + find_mint_address(&decompressed_mint_seed.pubkey()); + + // Get validity proof for the address + let rpc_result = rpc + .get_validity_proof( + vec![], + vec![AddressWithTree { + address: decompressed_mint_compression_address, + tree: mint_address_tree.tree, + }], + None, + ) + .await + .unwrap() + .value; + + // Create decompressed mint (CreateMint always creates both compressed + on-chain CMint) + let decompressed_mint_params = CreateMintParams { + decimals: 6, + address_merkle_tree_root_index: rpc_result.addresses[0].root_index, + mint_authority: payer.pubkey(), + proof: rpc_result.proof.0.unwrap(), + compression_address: decompressed_mint_compression_address, + mint: decompressed_mint_pda, + bump: decompressed_mint_bump, + freeze_authority: None, + extensions: None, + rent_payment: 0, // Immediately compressible + write_top_up: 0, + }; + + let create_decompressed_mint_builder = CreateMint::new( + decompressed_mint_params, + decompressed_mint_seed.pubkey(), + payer.pubkey(), + mint_address_tree.tree, + output_queue, + ); + let ix = create_decompressed_mint_builder.instruction().unwrap(); + + let (blockhash, _) = rpc.get_latest_blockhash().await.unwrap(); + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&payer.pubkey()), + &[&payer, &decompressed_mint_seed], + blockhash, + ); + let decompressed_mint_sig = rpc.process_transaction(tx).await.unwrap(); + println!( + "Created decompressed mint (CMint on-chain): {} (sig: {})", + decompressed_mint_pda, decompressed_mint_sig + ); + + // Wait for indexer to process + sleep(Duration::from_secs(3)).await; + + // ============ Scenario 5: Fully Compressed Mint (CreateMint + CompressAndCloseMint) ============ + // This creates a compressed mint and then compresses it, so full mint data is in the compressed DB. + // This is for testing getMintInterface cold path (no on-chain data needed). + println!("\n=== Creating fully compressed mint ==="); + + let compressed_mint_seed = Keypair::new(); + + // Derive compression address for fully compressed mint + let compressed_mint_compression_address = + derive_mint_compressed_address(&compressed_mint_seed.pubkey(), &mint_address_tree.tree); + + let (compressed_mint_pda, compressed_mint_bump) = + find_mint_address(&compressed_mint_seed.pubkey()); + + // Get validity proof for the new address + let rpc_result = rpc + .get_validity_proof( + vec![], + vec![AddressWithTree { + address: compressed_mint_compression_address, + tree: mint_address_tree.tree, + }], + None, + ) + .await + .unwrap() + .value; + + // Create compressed mint (will be decompressed initially) + let compressed_mint_params = CreateMintParams { + decimals: 9, + address_merkle_tree_root_index: rpc_result.addresses[0].root_index, + mint_authority: payer.pubkey(), + proof: rpc_result.proof.0.unwrap(), + compression_address: compressed_mint_compression_address, + mint: compressed_mint_pda, + bump: compressed_mint_bump, + freeze_authority: Some(payer.pubkey()), // Add freeze authority for variety + extensions: None, + rent_payment: 0, // Immediately compressible + write_top_up: 0, + }; + + let create_compressed_mint_builder = CreateMint::new( + compressed_mint_params, + compressed_mint_seed.pubkey(), + payer.pubkey(), + mint_address_tree.tree, + output_queue, + ); + let ix = create_compressed_mint_builder.instruction().unwrap(); + + let (blockhash, _) = rpc.get_latest_blockhash().await.unwrap(); + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&payer.pubkey()), + &[&payer, &compressed_mint_seed], + blockhash, + ); + let create_mint_sig = rpc.process_transaction(tx).await.unwrap(); + println!( + "Created mint (step 1/2): {} (sig: {})", + compressed_mint_pda, create_mint_sig + ); + + // Wait for indexer to process the CreateMint + sleep(Duration::from_secs(3)).await; + wait_for_indexer(&rpc).await.unwrap(); + + // Now compress and close the mint to make it fully compressed + println!("Compressing mint via CompressAndCloseMint..."); + + let compress_params = MintActionParams { + compressed_mint_address: compressed_mint_compression_address, + mint_seed: compressed_mint_seed.pubkey(), + authority: payer.pubkey(), + payer: payer.pubkey(), + actions: vec![MintActionType::CompressAndCloseMint { idempotent: false }], + new_mint: None, + }; + + let compress_ix = create_mint_action_instruction(&mut rpc, compress_params) + .await + .expect("Failed to create CompressAndCloseMint instruction"); + + let (blockhash, _) = rpc.get_latest_blockhash().await.unwrap(); + let tx = Transaction::new_signed_with_payer( + &[compress_ix], + Some(&payer.pubkey()), + &[&payer], + blockhash, + ); + let compress_mint_sig = rpc.process_transaction(tx).await.unwrap(); + println!( + "Compressed mint (step 2/2): {} (sig: {})", + compressed_mint_pda, compress_mint_sig + ); + + // Wait for indexer to process + sleep(Duration::from_secs(3)).await; + + // ============ Scenario 6: Compressible Token Account ============ + println!("\n=== Creating compressible token account ==="); + + let compressible_owner = Keypair::new(); + rpc.airdrop_lamports(&compressible_owner.pubkey(), 1_000_000_000) + .await + .expect("Failed to airdrop to compressible owner"); + + let compressible_token_account = create_compressible_token_account( + &mut rpc, + CreateCompressibleTokenAccountInputs { + owner: compressible_owner.pubkey(), + mint: mint_pubkey, + num_prepaid_epochs: 2, + payer: &payer, + token_account_keypair: None, + lamports_per_write: Some(100), + token_account_version: TokenDataVersion::ShaFlat, + }, + ) + .await + .expect("Failed to create compressible token account"); + println!( + "Created compressible token account: {}", + compressible_token_account + ); + println!("Compressible owner: {}", compressible_owner.pubkey()); + + // ============ Summary ============ + println!("\n========== ADDRESSES SUMMARY ==========\n"); + println!("SPL Mint: {}", mint_pubkey); + println!("Registered v2 Address: {}", hex::encode(derived_address)); + println!( + "Decompressed Mint PDA (on-chain CMint): {}", + decompressed_mint_pda + ); + println!( + "Decompressed Mint Address: {:?}", + decompressed_mint_compression_address + ); + println!( + "Fully Compressed Mint PDA (in compressed DB): {}", + compressed_mint_pda + ); + println!( + "Fully Compressed Mint Address: {:?}", + compressed_mint_compression_address + ); + println!("Bob (compressed token holder): {}", bob.pubkey()); + println!("Charlie (compressed token holder): {}", charlie.pubkey()); + println!("Compressible owner: {}", compressible_owner.pubkey()); + println!("Compressible token account: {}", compressible_token_account); + + // ============ Test Interface Endpoints ============ + println!("\n========== TESTING INTERFACE ENDPOINTS ==========\n"); + + // Create PhotonIndexer to test the interface endpoints + let photon_indexer = PhotonIndexer::new("http://localhost:8784".to_string(), None); + + // Wait for indexer to sync + sleep(Duration::from_secs(3)).await; + wait_for_indexer(&rpc).await.unwrap(); + + // ============ Test 1: getMintInterface with decompressed mint (on-chain CMint) ============ + println!("Test 1: getMintInterface with decompressed mint (on-chain CMint)..."); + let decompressed_mint_interface = photon_indexer + .get_mint_interface(&decompressed_mint_pda, None) + .await + .expect("getMintInterface should not error for decompressed mint") + .value + .expect("Decompressed mint should be found"); + + assert!( + decompressed_mint_interface.account.is_hot(), + "Decompressed mint should be hot (on-chain)" + ); + assert!( + decompressed_mint_interface.account.cold.is_none(), + "On-chain mint should not have cold context" + ); + assert_eq!( + decompressed_mint_interface.account.key, decompressed_mint_pda, + "Key should match the queried address" + ); + assert!( + decompressed_mint_interface.account.account.lamports > 0, + "On-chain mint should have lamports > 0" + ); + assert_eq!( + decompressed_mint_interface.mint_data.decimals, 6, + "Decompressed mint decimals should be 6" + ); + assert_eq!( + decompressed_mint_interface.mint_data.mint_pda, decompressed_mint_pda, + "Mint PDA should match the queried address" + ); + println!(" PASSED: Decompressed mint resolved from on-chain with correct data"); + + // ============ Test 2: getMintInterface with fully compressed mint (compressed DB) ============ + println!("\nTest 2: getMintInterface with fully compressed mint (compressed DB)..."); + let compressed_mint_interface = photon_indexer + .get_mint_interface(&compressed_mint_pda, None) + .await + .expect("getMintInterface should not error for compressed mint") + .value + .expect("Compressed mint should be found"); + + assert!( + compressed_mint_interface.account.is_cold(), + "Fully compressed mint should be cold (from compressed DB)" + ); + assert!( + compressed_mint_interface.account.cold.is_some(), + "Compressed mint should have cold context" + ); + // Verify cold context is the Mint variant + assert!( + matches!( + compressed_mint_interface.account.cold, + Some(ColdContext::Mint { .. }) + ), + "Cold context should be the Mint variant" + ); + assert_eq!( + compressed_mint_interface.account.key, compressed_mint_pda, + "Key should match the queried address" + ); + assert_eq!( + compressed_mint_interface.mint_data.decimals, 9, + "Compressed mint decimals should be 9" + ); + assert_eq!( + compressed_mint_interface.mint_data.freeze_authority, + Some(payer.pubkey()), + "Compressed mint freeze authority should match" + ); + assert_eq!( + compressed_mint_interface.mint_data.mint_pda, compressed_mint_pda, + "Mint PDA should match the queried address" + ); + println!(" PASSED: Compressed mint resolved from DB with correct data"); + + // ============ Test 3: getAccountInterface with compressible token account (on-chain) ============ + println!("\nTest 3: getAccountInterface with compressible token account (on-chain)..."); + let compressible_account_interface = photon_indexer + .get_account_interface(&compressible_token_account, None) + .await + .expect("getAccountInterface should not error for compressible account") + .value + .expect("Compressible token account should be found"); + + assert!( + compressible_account_interface.is_hot(), + "Compressible account should be hot (on-chain)" + ); + assert!( + compressible_account_interface.cold.is_none(), + "On-chain account should not have cold context" + ); + assert_eq!( + compressible_account_interface.key, compressible_token_account, + "Key should match the queried address" + ); + assert!( + compressible_account_interface.account.lamports > 0, + "On-chain account should have lamports > 0" + ); + println!(" PASSED: Compressible account resolved from on-chain"); + + // ============ Test 4: getTokenAccountInterface with compressible token account (on-chain) ============ + println!("\nTest 4: getTokenAccountInterface with compressible token account (on-chain)..."); + let compressible_token_interface = photon_indexer + .get_token_account_interface(&compressible_token_account, None) + .await + .expect("getTokenAccountInterface should not error") + .value + .expect("Compressible token account should be found via token interface"); + + assert!( + compressible_token_interface.account.is_hot(), + "Token account should be hot (on-chain)" + ); + assert!( + compressible_token_interface.account.cold.is_none(), + "On-chain token account should not have cold context" + ); + assert_eq!( + compressible_token_interface.account.key, compressible_token_account, + "Token account key should match" + ); + assert_eq!( + compressible_token_interface.token.mint, mint_pubkey, + "Token mint should match SPL mint" + ); + assert_eq!( + compressible_token_interface.token.owner, + compressible_owner.pubkey(), + "Token owner should match compressible owner" + ); + println!(" PASSED: Token account interface resolved with correct token data"); + + // ============ Test 5: getMultipleAccountInterfaces batch lookup ============ + println!("\nTest 5: getMultipleAccountInterfaces batch lookup..."); + let batch_addresses = vec![&decompressed_mint_pda, &compressible_token_account]; + + let batch_response = photon_indexer + .get_multiple_account_interfaces(batch_addresses.clone(), None) + .await + .expect("getMultipleAccountInterfaces should not error"); + + assert_eq!( + batch_response.value.len(), + 2, + "Batch response should have exactly 2 results" + ); + + // First result: decompressed mint + let batch_mint = batch_response.value[0] + .as_ref() + .expect("Decompressed mint should be found in batch"); + assert!(batch_mint.is_hot(), "Batch mint should be hot (on-chain)"); + assert_eq!( + batch_mint.key, decompressed_mint_pda, + "Batch mint key should match" + ); + assert!( + batch_mint.account.lamports > 0, + "Batch mint should have lamports > 0" + ); + + // Second result: compressible token account + let batch_token = batch_response.value[1] + .as_ref() + .expect("Compressible account should be found in batch"); + assert!( + batch_token.is_hot(), + "Batch token account should be hot (on-chain)" + ); + assert_eq!( + batch_token.key, compressible_token_account, + "Batch token account key should match" + ); + assert!( + batch_token.account.lamports > 0, + "Batch token account should have lamports > 0" + ); + println!(" PASSED: Batch lookup returned correct results"); + + // ============ Test 6: Consistency between getMintInterface and getAccountInterface ============ + println!("\nTest 6: Consistency between getMintInterface and getAccountInterface..."); + let mint_via_mint = photon_indexer + .get_mint_interface(&decompressed_mint_pda, None) + .await + .expect("getMintInterface should succeed") + .value + .expect("Mint should be found via getMintInterface"); + + let mint_via_account = photon_indexer + .get_account_interface(&decompressed_mint_pda, None) + .await + .expect("getAccountInterface should succeed") + .value + .expect("Mint should be found via getAccountInterface"); + + assert_eq!( + mint_via_mint.account.key, mint_via_account.key, + "Keys should match between interfaces" + ); + assert_eq!( + mint_via_mint.account.account.lamports, mint_via_account.account.lamports, + "Lamports should match between interfaces" + ); + assert_eq!( + mint_via_mint.account.cold.is_none(), + mint_via_account.cold.is_none(), + "Hot/cold status should match between interfaces" + ); + assert_eq!( + mint_via_mint.account.account.data, mint_via_account.account.data, + "Data should match between interfaces" + ); + assert_eq!( + mint_via_mint.account.account.owner, mint_via_account.account.owner, + "Owner should match between interfaces" + ); + println!(" PASSED: Consistency verified between getMintInterface and getAccountInterface"); + + // ============ Test 7: Verify fully compressed mint via getAccountInterface returns None ============ + // Fully compressed mints (after CompressAndCloseMint) have full mint data in the compressed DB. + // Their address column contains the compression_address, not the mint_pda. + // Since they don't have the [255; 8] discriminator, onchain_pubkey is not set. + // Therefore getAccountInterface by mint_pda should return None (use getMintInterface instead). + println!("\nTest 7: getAccountInterface with fully compressed mint PDA..."); + let compressed_via_account = photon_indexer + .get_account_interface(&compressed_mint_pda, None) + .await + .expect("getAccountInterface should not error"); + + assert!( + compressed_via_account.value.is_none(), + "Fully compressed mint should NOT be found via getAccountInterface (use getMintInterface)" + ); + println!(" PASSED: Fully compressed mint correctly returns None via getAccountInterface"); + + // ============ Test 8: Verify decompressed mint found via getAccountInterface (generic linking) ============ + // Decompressed mints have discriminator [255; 8] + 32-byte mint_pda in data. + // The generic linking feature extracts this as onchain_pubkey during ingestion. + // Therefore getAccountInterface(mint_pda) should find it via onchain_pubkey column. + println!("\nTest 8: getAccountInterface with decompressed mint PDA (generic linking)..."); + let decompressed_via_account = photon_indexer + .get_account_interface(&decompressed_mint_pda, None) + .await + .expect("getAccountInterface should not error"); + + let decompressed_account = decompressed_via_account + .value + .expect("Decompressed mint should be found via getAccountInterface (generic linking)"); + + // The decompressed mint should be found from on-chain (CMint account exists) + assert!( + decompressed_account.is_hot(), + "Decompressed mint via getAccountInterface should be hot (on-chain)" + ); + assert!( + decompressed_account.cold.is_none(), + "Decompressed mint via getAccountInterface should not have cold context" + ); + assert_eq!( + decompressed_account.key, decompressed_mint_pda, + "Key should match the queried mint PDA" + ); + assert!( + decompressed_account.account.lamports > 0, + "Decompressed mint should have lamports > 0" + ); + println!(" PASSED: Decompressed mint found via getAccountInterface with generic linking"); + + println!("\n========== ALL TESTS PASSED =========="); + println!("\nTo export transactions, run:"); + println!("cargo xtask export-photon-test-data --test-name indexer_interface"); +} diff --git a/js/stateless.js/tests/unit/version.test.ts b/js/stateless.js/tests/unit/version.test.ts index 97db06c8be..a0cc7100aa 100644 --- a/js/stateless.js/tests/unit/version.test.ts +++ b/js/stateless.js/tests/unit/version.test.ts @@ -20,8 +20,9 @@ describe('Version System', () => { }); it('should respect LIGHT_PROTOCOL_VERSION environment variable', () => { + // Default is V2 when no env var is set (see constants.ts line 31) const expectedVersion = - process.env.LIGHT_PROTOCOL_VERSION || VERSION.V1; + process.env.LIGHT_PROTOCOL_VERSION || VERSION.V2; expect(featureFlags.version).toBe(expectedVersion); }); diff --git a/justfile b/justfile index e3624721d5..325ac4c250 100644 --- a/justfile +++ b/justfile @@ -36,7 +36,7 @@ lint: lint-rust js::lint lint-rust: cargo +nightly fmt --all -- --check - cargo clippy --workspace --all-features -- -D warnings + cargo clippy --workspace --all-features --tests -- -D warnings format: cargo +nightly fmt --all diff --git a/program-libs/CLAUDE.md b/program-libs/CLAUDE.md index 81359b5151..3803b58e81 100644 --- a/program-libs/CLAUDE.md +++ b/program-libs/CLAUDE.md @@ -63,6 +63,7 @@ Some crates depend on external Light Protocol crates not in program-libs: ## Testing Unit tests run with `cargo test`: + ```bash cargo test -p light-hasher --all-features cargo test -p light-compressed-account --all-features diff --git a/program-tests/compressed-token-test/tests/v1.rs b/program-tests/compressed-token-test/tests/v1.rs index 9c4f55d2af..3b8b493c16 100644 --- a/program-tests/compressed-token-test/tests/v1.rs +++ b/program-tests/compressed-token-test/tests/v1.rs @@ -4892,6 +4892,7 @@ async fn test_transfer_with_photon_and_batched_tree() { sbf_programs: vec![], upgradeable_programs: vec![], limit_ledger_size: None, + validator_args: vec![], }) .await; diff --git a/program-tests/justfile b/program-tests/justfile index 18454e4823..b63c5fcdaa 100644 --- a/program-tests/justfile +++ b/program-tests/justfile @@ -6,10 +6,126 @@ default: build: cd create-address-test-program && cargo build-sbf -test: build +# === Full test suite (mirrors CI) === + +test: build test-account-compression test-registry test-system test-system-cpi test-system-cpi-v2 test-compressed-token test-e2e + +# === Individual test packages === + +test-account-compression: RUSTFLAGS="-D warnings" cargo test-sbf -p account-compression-test + +test-registry: RUSTFLAGS="-D warnings" cargo test-sbf -p registry-test - RUSTFLAGS="-D warnings" cargo test-sbf -p system-test + +# System program tests +test-system: test-system-address test-system-compression test-system-re-init + +test-system-address: + RUSTFLAGS="-D warnings" cargo test-sbf -p system-test -- test_with_address + +test-system-compression: + RUSTFLAGS="-D warnings" cargo test-sbf -p system-test -- test_with_compression + +test-system-re-init: + RUSTFLAGS="-D warnings" cargo test-sbf -p system-test --test test_re_init_cpi_account + +# System CPI tests (v1) +test-system-cpi: RUSTFLAGS="-D warnings" cargo test-sbf -p system-cpi-test - RUSTFLAGS="-D warnings" cargo test-sbf -p compressed-token-test + +# System CPI tests (v2) +test-system-cpi-v2: test-system-cpi-v2-main test-system-cpi-v2-event-parse test-system-cpi-v2-functional + +test-system-cpi-v2-main: + RUSTFLAGS="-D warnings" cargo test-sbf -p system-cpi-v2-test -- --skip functional_ --skip event::parse + +test-system-cpi-v2-event-parse: + RUSTFLAGS="-D warnings" cargo test-sbf -p system-cpi-v2-test -- event::parse + +test-system-cpi-v2-functional: test-system-cpi-v2-functional-read-only test-system-cpi-v2-functional-account-infos + +test-system-cpi-v2-functional-read-only: + RUSTFLAGS="-D warnings" cargo test-sbf -p system-cpi-v2-test -- functional_read_only + +test-system-cpi-v2-functional-account-infos: + RUSTFLAGS="-D warnings" cargo test-sbf -p system-cpi-v2-test -- functional_account_infos + +# Compressed token tests +test-compressed-token: test-compressed-token-unit test-compressed-token-v1 test-compressed-token-mint test-compressed-token-light-token test-compressed-token-transfer2 + +test-compressed-token-unit: + RUSTFLAGS="-D warnings" cargo test -p light-compressed-token + +test-compressed-token-v1: + RUSTFLAGS="-D warnings" cargo test-sbf -p compressed-token-test --test v1 + +test-compressed-token-mint: + RUSTFLAGS="-D warnings" cargo test-sbf -p compressed-token-test --test mint + +test-compressed-token-light-token: + RUSTFLAGS="-D warnings" cargo test-sbf -p compressed-token-test --test light_token + +test-compressed-token-transfer2: + RUSTFLAGS="-D warnings" cargo test-sbf -p compressed-token-test --test transfer2 + +# Compressed token batched tree test (flaky, may need retries) +test-compressed-token-batched-tree: + RUSTFLAGS="-D warnings" cargo test-sbf -p compressed-token-test -- test_transfer_with_photon_and_batched_tree + +# E2E tests +test-e2e: RUSTFLAGS="-D warnings" cargo test-sbf -p e2e-test + +# E2E extended tests (requires building compressed-token-small first) +test-e2e-extended: build-compressed-token-small + RUSTFLAGS="-D warnings" cargo test-sbf -p e2e-test -- --test test_10_all + +# Pinocchio unit tests +test-pinocchio: + RUSTFLAGS="-D warnings" cargo test -p light-system-program-pinocchio + +# === Build targets === + +build-compressed-token-small: + pnpm --filter @lightprotocol/programs run build-compressed-token-small + +# === CI-equivalent grouped tests === + +# Matches CI: account-compression-and-registry +ci-account-compression-and-registry: test-account-compression test-registry + +# Matches CI: light-system-program-address +ci-system-address: test-system-address test-e2e test-e2e-extended test-compressed-token-light-token + +# Matches CI: light-system-program-compression +ci-system-compression: test-system-compression test-system-re-init + +# Matches CI: compressed-token-and-e2e +ci-compressed-token-and-e2e: test-compressed-token-unit test-compressed-token-v1 test-compressed-token-mint + +# Matches CI: compressed-token-batched-tree (with retry for flaky test) +ci-compressed-token-batched-tree: + #!/usr/bin/env bash + set -euo pipefail + attempt=1 + max_attempts=3 + until RUSTFLAGS="-D warnings" cargo test-sbf -p compressed-token-test -- test_transfer_with_photon_and_batched_tree; do + attempt=$((attempt + 1)) + if [ $attempt -gt $max_attempts ]; then + echo "Test failed after $max_attempts attempts" + exit 1 + fi + echo "Attempt $attempt/$max_attempts failed, retrying in 5s..." + sleep 5 + done + echo "Test passed on attempt $attempt" + +# Matches CI: system-cpi-test +ci-system-cpi: test-system-cpi test-pinocchio test-system-cpi-v2-main test-system-cpi-v2-event-parse test-compressed-token-transfer2 + +# Matches CI: system-cpi-test-v2-functional-read-only +ci-system-cpi-v2-functional-read-only: test-system-cpi-v2-functional-read-only + +# Matches CI: system-cpi-test-v2-functional-account-infos +ci-system-cpi-v2-functional-account-infos: test-system-cpi-v2-functional-account-infos diff --git a/program-tests/system-cpi-v2-test/tests/event.rs b/program-tests/system-cpi-v2-test/tests/event.rs index 9ed2aae062..f482b0a7a1 100644 --- a/program-tests/system-cpi-v2-test/tests/event.rs +++ b/program-tests/system-cpi-v2-test/tests/event.rs @@ -539,6 +539,7 @@ async fn generate_photon_test_data_multiple_events() { )], upgradeable_programs: vec![], limit_ledger_size: None, + validator_args: vec![], }) .await; diff --git a/scripts/devenv.sh b/scripts/devenv.sh index 605bce12b7..a656a8de25 100755 --- a/scripts/devenv.sh +++ b/scripts/devenv.sh @@ -74,8 +74,11 @@ if [ -z "${CI:-}" ]; then alias light="${LIGHT_PROTOCOL_TOPLEVEL}/cli/test_bin/run" fi -# Define GOROOT for Go. export GOROOT="${LIGHT_PROTOCOL_TOPLEVEL}/.local/go" +export GOTOOLCHAIN=local +unset GOBIN +# Disable mise entirely to prevent its hooks from overriding our paths. +export MISE_DISABLED=1 # Ensure Rust binaries are in PATH PATH="${CARGO_HOME}/bin:${PATH}" diff --git a/scripts/devenv/versions.sh b/scripts/devenv/versions.sh index c578cacb05..f46121af4b 100755 --- a/scripts/devenv/versions.sh +++ b/scripts/devenv/versions.sh @@ -13,7 +13,7 @@ export SOLANA_VERSION="2.2.15" export ANCHOR_VERSION="0.31.1" export JQ_VERSION="1.8.0" export PHOTON_VERSION="0.51.2" -export PHOTON_COMMIT="83b46c9aef58a134edef2eb8e506c1bc6604e876" +export PHOTON_COMMIT="ab3583e9cc43389a780fd1165781820e2f748a87" export REDIS_VERSION="8.0.1" export ANCHOR_TAG="anchor-v${ANCHOR_VERSION}" diff --git a/sdk-libs/client/src/indexer/base58.rs b/sdk-libs/client/src/indexer/base58.rs index a2b66a123f..46b3953aa3 100644 --- a/sdk-libs/client/src/indexer/base58.rs +++ b/sdk-libs/client/src/indexer/base58.rs @@ -38,10 +38,13 @@ pub fn decode_base58_to_fixed_array(input: &str) -> Result<[u8; let mut buffer = [0u8; N]; let decoded_len = bs58::decode(input) .onto(&mut buffer) - .map_err(|_| IndexerError::InvalidResponseData)?; + .map_err(|e| IndexerError::base58_decode_error("base58", e))?; if decoded_len != N { - return Err(IndexerError::InvalidResponseData); + return Err(IndexerError::base58_decode_error( + "base58", + format!("expected {} bytes, got {}", N, decoded_len), + )); } Ok(buffer) diff --git a/sdk-libs/client/src/indexer/indexer_trait.rs b/sdk-libs/client/src/indexer/indexer_trait.rs index b051ab3c1d..2c8ecd7ae2 100644 --- a/sdk-libs/client/src/indexer/indexer_trait.rs +++ b/sdk-libs/client/src/indexer/indexer_trait.rs @@ -4,12 +4,13 @@ use solana_pubkey::Pubkey; use super::{ response::{Items, ItemsWithCursor, Response}, types::{ - CompressedAccount, CompressedTokenAccount, OwnerBalance, QueueInfoResult, + CompressedAccount, CompressedMint, CompressedTokenAccount, OwnerBalance, QueueInfoResult, SignatureWithMetadata, TokenBalance, ValidityProofWithContext, }, Address, AddressWithTree, GetCompressedAccountsByOwnerConfig, - GetCompressedTokenAccountsByOwnerOrDelegateOptions, Hash, IndexerError, IndexerRpcConfig, - MerkleProof, NewAddressProofWithContext, PaginatedOptions, QueueElementsV2Options, RetryConfig, + GetCompressedMintsByAuthorityOptions, GetCompressedTokenAccountsByOwnerOrDelegateOptions, Hash, + IndexerError, IndexerRpcConfig, MerkleProof, MintAuthorityType, NewAddressProofWithContext, + PaginatedOptions, QueueElementsV2Options, RetryConfig, }; use crate::indexer::QueueElementsResult; // TODO: remove all references in input types. @@ -193,4 +194,27 @@ pub trait Indexer: std::marker::Send + std::marker::Sync { merkle_tree_pubkey: [u8; 32], config: Option, ) -> Result>, IndexerError>; + + /// Returns the compressed mint with the given address. + async fn get_compressed_mint( + &self, + address: Address, + config: Option, + ) -> Result>, IndexerError>; + + /// Returns the compressed mint with the given PDA (decompressed account address). + async fn get_compressed_mint_by_pda( + &self, + mint_pda: &Pubkey, + config: Option, + ) -> Result>, IndexerError>; + + /// Returns compressed mints controlled by the given authority. + async fn get_compressed_mints_by_authority( + &self, + authority: &Pubkey, + authority_type: MintAuthorityType, + options: Option, + config: Option, + ) -> Result>, IndexerError>; } diff --git a/sdk-libs/client/src/indexer/mod.rs b/sdk-libs/client/src/indexer/mod.rs index fa03606dfe..d9fa33c0e8 100644 --- a/sdk-libs/client/src/indexer/mod.rs +++ b/sdk-libs/client/src/indexer/mod.rs @@ -14,12 +14,13 @@ pub use error::IndexerError; pub use indexer_trait::Indexer; pub use response::{Context, Items, ItemsWithCursor, Response}; pub use types::{ - AccountProofInputs, Address, AddressMerkleTreeAccounts, AddressProofInputs, AddressQueueData, - AddressWithTree, CompressedAccount, CompressedTokenAccount, Hash, InputQueueData, MerkleProof, - MerkleProofWithContext, NewAddressProofWithContext, NextTreeInfo, OutputQueueData, - OwnerBalance, ProofOfLeaf, QueueElementsResult, QueueInfo, QueueInfoResult, RootIndex, - SignatureWithMetadata, StateMerkleTreeAccounts, StateQueueData, TokenBalance, TreeInfo, - ValidityProofWithContext, + AccountInterface, AccountProofInputs, Address, AddressMerkleTreeAccounts, AddressProofInputs, + AddressQueueData, AddressWithTree, ColdContext, ColdData, CompressedAccount, CompressedMint, + CompressedTokenAccount, Hash, InputQueueData, InterfaceTreeInfo, MerkleProof, + MerkleProofWithContext, MintData, MintInterface, NewAddressProofWithContext, NextTreeInfo, + OutputQueueData, OwnerBalance, ProofOfLeaf, QueueElementsResult, QueueInfo, QueueInfoResult, + RootIndex, SignatureWithMetadata, SolanaAccountData, StateMerkleTreeAccounts, StateQueueData, + TokenAccountInterface, TokenBalance, TreeInfo, ValidityProofWithContext, }; mod options; pub use options::*; diff --git a/sdk-libs/client/src/indexer/options.rs b/sdk-libs/client/src/indexer/options.rs index dbbf699fb5..25738a8ff4 100644 --- a/sdk-libs/client/src/indexer/options.rs +++ b/sdk-libs/client/src/indexer/options.rs @@ -2,7 +2,7 @@ use photon_api::models::{FilterSelector, Memcmp}; use solana_account_decoder_client_types::UiDataSliceConfig; use solana_pubkey::Pubkey; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Default)] pub struct GetCompressedTokenAccountsByOwnerOrDelegateOptions { pub mint: Option, pub cursor: Option, @@ -112,3 +112,53 @@ impl QueueElementsV2Options { self } } + +/// Authority type for querying compressed mints. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum MintAuthorityType { + MintAuthority, + FreezeAuthority, + Either, +} + +/// Options for fetching compressed mints by authority. +#[derive(Debug, Clone)] +pub struct GetCompressedMintsByAuthorityOptions { + /// Cursor for pagination + pub cursor: Option, + /// Maximum number of results to return + pub limit: Option, + /// Authority type filter. Defaults to `MintAuthorityType::Either` (both mint and freeze authorities). + pub authority_type: MintAuthorityType, +} + +impl Default for GetCompressedMintsByAuthorityOptions { + fn default() -> Self { + Self { + cursor: None, + limit: None, + authority_type: MintAuthorityType::Either, + } + } +} + +impl GetCompressedMintsByAuthorityOptions { + pub fn new() -> Self { + Self::default() + } + + pub fn with_cursor(mut self, cursor: String) -> Self { + self.cursor = Some(cursor); + self + } + + pub fn with_limit(mut self, limit: u16) -> Self { + self.limit = Some(limit); + self + } + + pub fn with_authority_type(mut self, authority_type: MintAuthorityType) -> Self { + self.authority_type = authority_type; + self + } +} diff --git a/sdk-libs/client/src/indexer/photon_indexer.rs b/sdk-libs/client/src/indexer/photon_indexer.rs index a220c16554..e815de8da8 100644 --- a/sdk-libs/client/src/indexer/photon_indexer.rs +++ b/sdk-libs/client/src/indexer/photon_indexer.rs @@ -10,15 +10,17 @@ use solana_pubkey::Pubkey; use tracing::{error, trace, warn}; use super::types::{ - CompressedAccount, CompressedTokenAccount, OwnerBalance, SignatureWithMetadata, TokenBalance, + AccountInterface, CompressedAccount, CompressedMint, CompressedTokenAccount, MintInterface, + OwnerBalance, SignatureWithMetadata, TokenAccountInterface, TokenBalance, }; use crate::indexer::{ base58::Base58Conversions, config::RetryConfig, response::{Context, Items, ItemsWithCursor, Response}, Address, AddressWithTree, GetCompressedAccountsByOwnerConfig, - GetCompressedTokenAccountsByOwnerOrDelegateOptions, Hash, Indexer, IndexerError, - IndexerRpcConfig, MerkleProof, NewAddressProofWithContext, PaginatedOptions, + GetCompressedMintsByAuthorityOptions, GetCompressedTokenAccountsByOwnerOrDelegateOptions, Hash, + Indexer, IndexerError, IndexerRpcConfig, MerkleProof, MintAuthorityType, + NewAddressProofWithContext, PaginatedOptions, }; // Tests are in program-tests/client-test/tests/light-client.rs @@ -1777,4 +1779,397 @@ impl Indexer for PhotonIndexer { todo!(); } } + + async fn get_compressed_mint( + &self, + address: Address, + config: Option, + ) -> Result>, IndexerError> { + let config = config.unwrap_or_default(); + self.retry(config.retry_config, || async { + let request = photon_api::models::GetCompressedMintPostRequest::new( + photon_api::models::GetCompressedMintPostRequestParams::with_address( + bs58::encode(address).into_string(), + ), + ); + + let result = photon_api::apis::default_api::get_compressed_mint_post( + &self.configuration, + request, + ) + .await?; + + let api_response = Self::extract_result_with_error_check( + "get_compressed_mint", + result.error, + result.result.map(|r| *r), + )?; + + if api_response.context.slot < config.slot { + return Err(IndexerError::IndexerNotSyncedToSlot); + } + + let mint = match api_response.value { + Some(boxed) => Some(CompressedMint::try_from(&*boxed)?), + None => None, + }; + + Ok(Response { + context: Context { + slot: api_response.context.slot, + }, + value: mint, + }) + }) + .await + } + + async fn get_compressed_mint_by_pda( + &self, + mint_pda: &Pubkey, + config: Option, + ) -> Result>, IndexerError> { + let config = config.unwrap_or_default(); + self.retry(config.retry_config, || async { + let request = photon_api::models::GetCompressedMintPostRequest::new( + photon_api::models::GetCompressedMintPostRequestParams::with_mint_pda( + mint_pda.to_string(), + ), + ); + + let result = photon_api::apis::default_api::get_compressed_mint_post( + &self.configuration, + request, + ) + .await?; + + let api_response = Self::extract_result_with_error_check( + "get_compressed_mint_by_pda", + result.error, + result.result.map(|r| *r), + )?; + + if api_response.context.slot < config.slot { + return Err(IndexerError::IndexerNotSyncedToSlot); + } + + let mint = match api_response.value { + Some(boxed) => Some(CompressedMint::try_from(&*boxed)?), + None => None, + }; + + Ok(Response { + context: Context { + slot: api_response.context.slot, + }, + value: mint, + }) + }) + .await + } + + async fn get_compressed_mints_by_authority( + &self, + authority: &Pubkey, + authority_type: MintAuthorityType, + options: Option, + config: Option, + ) -> Result>, IndexerError> { + let config = config.unwrap_or_default(); + self.retry(config.retry_config, || async { + let api_authority_type = match authority_type { + MintAuthorityType::MintAuthority => { + photon_api::models::AuthorityType::MintAuthority + } + MintAuthorityType::FreezeAuthority => { + photon_api::models::AuthorityType::FreezeAuthority + } + MintAuthorityType::Either => photon_api::models::AuthorityType::Both, + }; + + let request = photon_api::models::GetCompressedMintsByAuthorityPostRequest::new( + photon_api::models::GetCompressedMintsByAuthorityPostRequestParams { + authority: authority.to_string(), + authority_type: api_authority_type, + cursor: options.as_ref().and_then(|o| o.cursor.clone()), + limit: options.as_ref().and_then(|o| o.limit), + }, + ); + + let result = photon_api::apis::default_api::get_compressed_mints_by_authority_post( + &self.configuration, + request, + ) + .await?; + + let api_response = Self::extract_result_with_error_check( + "get_compressed_mints_by_authority", + result.error, + result.result.map(|r| *r), + )?; + + if api_response.context.slot < config.slot { + return Err(IndexerError::IndexerNotSyncedToSlot); + } + + let mints: Result, _> = api_response + .value + .items + .iter() + .map(CompressedMint::try_from) + .collect(); + + let cursor = api_response.value.cursor; + + Ok(Response { + context: Context { + slot: api_response.context.slot, + }, + value: ItemsWithCursor { + items: mints?, + cursor, + }, + }) + }) + .await + } +} + +// ============ Interface Methods ============ +// These methods use the Interface endpoints that race hot (on-chain) and cold (compressed) lookups + +impl PhotonIndexer { + /// Get account data from either on-chain or compressed sources. + /// Races both lookups and returns the result with the higher slot. + pub async fn get_account_interface( + &self, + address: &Pubkey, + config: Option, + ) -> Result>, IndexerError> { + let config = config.unwrap_or_default(); + self.retry(config.retry_config, || async { + let request = photon_api::models::GetAccountInterfacePostRequest::new( + photon_api::models::GetAccountInterfacePostRequestParams::new(address.to_string()), + ); + + let result = photon_api::apis::default_api::get_account_interface_post( + &self.configuration, + request, + ) + .await?; + + let api_response = Self::extract_result_with_error_check( + "get_account_interface", + result.error, + result.result.map(|r| *r), + )?; + + if api_response.context.slot < config.slot { + return Err(IndexerError::IndexerNotSyncedToSlot); + } + + let account = match api_response.value { + Some(boxed) => Some(AccountInterface::try_from(boxed.as_ref())?), + None => None, + }; + + Ok(Response { + context: Context { + slot: api_response.context.slot, + }, + value: account, + }) + }) + .await + } + + /// Get token account data from either on-chain or compressed sources. + /// Races both lookups and returns the result with the higher slot. + pub async fn get_token_account_interface( + &self, + address: &Pubkey, + config: Option, + ) -> Result>, IndexerError> { + let config = config.unwrap_or_default(); + self.retry(config.retry_config, || async { + let request = photon_api::models::GetTokenAccountInterfacePostRequest::new( + photon_api::models::GetTokenAccountInterfacePostRequestParams::new( + address.to_string(), + ), + ); + + let result = photon_api::apis::default_api::get_token_account_interface_post( + &self.configuration, + request, + ) + .await?; + + let api_response = Self::extract_result_with_error_check( + "get_token_account_interface", + result.error, + result.result.map(|r| *r), + )?; + + if api_response.context.slot < config.slot { + return Err(IndexerError::IndexerNotSyncedToSlot); + } + + let account = match api_response.value { + Some(boxed) => Some(TokenAccountInterface::try_from(boxed.as_ref())?), + None => None, + }; + + Ok(Response { + context: Context { + slot: api_response.context.slot, + }, + value: account, + }) + }) + .await + } + + /// Get Associated Token Account data from either on-chain or compressed sources. + /// Derives the Light Protocol ATA address from owner+mint, then races hot/cold lookups. + pub async fn get_ata_interface( + &self, + owner: &Pubkey, + mint: &Pubkey, + config: Option, + ) -> Result>, IndexerError> { + let config = config.unwrap_or_default(); + self.retry(config.retry_config, || async { + let request = photon_api::models::GetAtaInterfacePostRequest::new( + photon_api::models::GetAtaInterfacePostRequestParams::new( + owner.to_string(), + mint.to_string(), + ), + ); + + let result = + photon_api::apis::default_api::get_ata_interface_post(&self.configuration, request) + .await?; + + let api_response = Self::extract_result_with_error_check( + "get_ata_interface", + result.error, + result.result.map(|r| *r), + )?; + + if api_response.context.slot < config.slot { + return Err(IndexerError::IndexerNotSyncedToSlot); + } + + let account = match api_response.value { + Some(boxed) => Some(TokenAccountInterface::try_from(boxed.as_ref())?), + None => None, + }; + + Ok(Response { + context: Context { + slot: api_response.context.slot, + }, + value: account, + }) + }) + .await + } + + /// Get mint data from either on-chain or compressed sources. + /// Races both lookups and returns the result with the higher slot. + pub async fn get_mint_interface( + &self, + address: &Pubkey, + config: Option, + ) -> Result>, IndexerError> { + let config = config.unwrap_or_default(); + self.retry(config.retry_config, || async { + let request = photon_api::models::GetMintInterfacePostRequest::new( + photon_api::models::GetMintInterfacePostRequestParams::new(address.to_string()), + ); + + let result = photon_api::apis::default_api::get_mint_interface_post( + &self.configuration, + request, + ) + .await?; + + let api_response = Self::extract_result_with_error_check( + "get_mint_interface", + result.error, + result.result.map(|r| *r), + )?; + + if api_response.context.slot < config.slot { + return Err(IndexerError::IndexerNotSyncedToSlot); + } + + let mint = match api_response.value { + Some(boxed) => Some(MintInterface::try_from(boxed.as_ref())?), + None => None, + }; + + Ok(Response { + context: Context { + slot: api_response.context.slot, + }, + value: mint, + }) + }) + .await + } + + /// Get multiple account interfaces in a batch. + /// Returns a vector where each element corresponds to an input address. + pub async fn get_multiple_account_interfaces( + &self, + addresses: Vec<&Pubkey>, + config: Option, + ) -> Result>>, IndexerError> { + let config = config.unwrap_or_default(); + self.retry(config.retry_config, || async { + let address_strings: Vec = + addresses.iter().map(|addr| addr.to_string()).collect(); + + let request = photon_api::models::GetMultipleAccountInterfacesPostRequest::new( + photon_api::models::GetMultipleAccountInterfacesPostRequestParams::new( + address_strings, + ), + ); + + let result = photon_api::apis::default_api::get_multiple_account_interfaces_post( + &self.configuration, + request, + ) + .await?; + + let api_response = Self::extract_result_with_error_check( + "get_multiple_account_interfaces", + result.error, + result.result.map(|r| *r), + )?; + + if api_response.context.slot < config.slot { + return Err(IndexerError::IndexerNotSyncedToSlot); + } + + let accounts: Result>, IndexerError> = api_response + .value + .into_iter() + .map(|maybe_acc| { + maybe_acc + .map(|ai| AccountInterface::try_from(&ai)) + .transpose() + }) + .collect(); + + Ok(Response { + context: Context { + slot: api_response.context.slot, + }, + value: accounts?, + }) + }) + .await + } } diff --git a/sdk-libs/client/src/indexer/types.rs b/sdk-libs/client/src/indexer/types.rs index 3f653db274..48cad89ea5 100644 --- a/sdk-libs/client/src/indexer/types.rs +++ b/sdk-libs/client/src/indexer/types.rs @@ -11,8 +11,13 @@ use light_indexed_merkle_tree::array::IndexedElement; use light_sdk::instruction::{ PackedAccounts, PackedAddressTreeInfo, PackedStateTreeInfo, ValidityProof, }; -use light_token::compat::{AccountState, TokenData}; -use light_token_interface::state::ExtensionStruct; +use light_token::{ + compat::{AccountState, TokenData}, + instruction::find_mint_address, +}; +use light_token_interface::state::{ + BaseMint, CompressionInfo, ExtensionStruct, Mint as LightMint, MintMetadata, ACCOUNT_TYPE_MINT, +}; use num_bigint::BigUint; use solana_pubkey::Pubkey; use tracing::warn; @@ -408,17 +413,17 @@ impl ValidityProofWithContext { .compressed_proof .a .try_into() - .map_err(|_| IndexerError::InvalidResponseData)?, + .map_err(|_| IndexerError::decode_error("proof.a", "invalid length"))?, b: value .compressed_proof .b .try_into() - .map_err(|_| IndexerError::InvalidResponseData)?, + .map_err(|_| IndexerError::decode_error("proof.b", "invalid length"))?, c: value .compressed_proof .c .try_into() - .map_err(|_| IndexerError::InvalidResponseData)?, + .map_err(|_| IndexerError::decode_error("proof.c", "invalid length"))?, })); // Convert account data from V1 flat arrays to V2 structured format @@ -428,7 +433,13 @@ impl ValidityProofWithContext { Pubkey::new_from_array(decode_base58_to_fixed_array(&value.merkle_trees[i])?); let tree_info = super::tree_info::QUEUE_TREE_MAPPING .get(&value.merkle_trees[i]) - .ok_or(IndexerError::InvalidResponseData)?; + .ok_or(IndexerError::MissingResult { + context: "conversion".into(), + message: format!( + "tree not found in QUEUE_TREE_MAPPING: {}", + &value.merkle_trees[i] + ), + })?; Ok(AccountProofInputs { hash: decode_base58_to_fixed_array(&value.leaves[i])?, @@ -455,7 +466,10 @@ impl ValidityProofWithContext { )?); let tree_info = super::tree_info::QUEUE_TREE_MAPPING .get(&value.merkle_trees[i]) - .ok_or(IndexerError::InvalidResponseData)?; + .ok_or(IndexerError::MissingResult { + context: "conversion".into(), + message: "expected value was None".into(), + })?; Ok(AddressProofInputs { address: decode_base58_to_fixed_array(&value.leaves[i])?, // Address is in leaves @@ -490,15 +504,15 @@ impl ValidityProofWithContext { a: proof .a .try_into() - .map_err(|_| IndexerError::InvalidResponseData)?, + .map_err(|_| IndexerError::decode_error("proof.a", "invalid length"))?, b: proof .b .try_into() - .map_err(|_| IndexerError::InvalidResponseData)?, + .map_err(|_| IndexerError::decode_error("proof.b", "invalid length"))?, c: proof .c .try_into() - .map_err(|_| IndexerError::InvalidResponseData)?, + .map_err(|_| IndexerError::decode_error("proof.c", "invalid length"))?, })) } else { ValidityProof::new(None) @@ -659,7 +673,7 @@ impl TryFrom for CompressedAccount { fn try_from(account: CompressedAccountWithMerkleContext) -> Result { let hash = account .hash() - .map_err(|_| IndexerError::InvalidResponseData)?; + .map_err(|e| IndexerError::decode_error("data", e))?; // Breaks light-program-test let tree_info = QUEUE_TREE_MAPPING.get( &Pubkey::new_from_array(account.merkle_context.merkle_tree_pubkey.to_bytes()) @@ -721,7 +735,7 @@ impl TryFrom<&photon_api::models::AccountV2> for CompressedAccount { Ok::, IndexerError>(Some(CompressedAccountData { discriminator: data.discriminator.to_le_bytes(), data: base64::decode_config(&data.data, base64::STANDARD_NO_PAD) - .map_err(|_| IndexerError::InvalidResponseData)?, + .map_err(|e| IndexerError::decode_error("data", e))?, data_hash: decode_base58_to_fixed_array(&data.data_hash)?, })) } else { @@ -776,7 +790,7 @@ impl TryFrom<&photon_api::models::Account> for CompressedAccount { Ok::, IndexerError>(Some(CompressedAccountData { discriminator: data.discriminator.to_le_bytes(), data: base64::decode_config(&data.data, base64::STANDARD_NO_PAD) - .map_err(|_| IndexerError::InvalidResponseData)?, + .map_err(|e| IndexerError::decode_error("data", e))?, data_hash: decode_base58_to_fixed_array(&data.data_hash)?, })) } else { @@ -794,9 +808,13 @@ impl TryFrom<&photon_api::models::Account> for CompressedAccount { let lamports = account.lamports; let leaf_index = account.leaf_index; - let tree_info = QUEUE_TREE_MAPPING - .get(&account.tree) - .ok_or(IndexerError::InvalidResponseData)?; + let tree_info = + QUEUE_TREE_MAPPING + .get(&account.tree) + .ok_or(IndexerError::MissingResult { + context: "conversion".into(), + message: "expected value was None".into(), + })?; let tree_info = TreeInfo { cpi_context: tree_info.cpi_context, @@ -886,9 +904,9 @@ impl TryFrom<&photon_api::models::TokenAccount> for CompressedTokenAccount { .as_ref() .map(|tlv| { let bytes = base64::decode_config(tlv, base64::STANDARD_NO_PAD) - .map_err(|_| IndexerError::InvalidResponseData)?; + .map_err(|e| IndexerError::decode_error("tlv", e))?; Vec::::deserialize(&mut bytes.as_slice()) - .map_err(|_| IndexerError::InvalidResponseData) + .map_err(|e| IndexerError::decode_error("extensions", e)) }) .transpose()?, }; @@ -927,9 +945,9 @@ impl TryFrom<&photon_api::models::TokenAccountV2> for CompressedTokenAccount { .as_ref() .map(|tlv| { let bytes = base64::decode_config(tlv, base64::STANDARD_NO_PAD) - .map_err(|_| IndexerError::InvalidResponseData)?; + .map_err(|e| IndexerError::decode_error("tlv", e))?; Vec::::deserialize(&mut bytes.as_slice()) - .map_err(|_| IndexerError::InvalidResponseData) + .map_err(|e| IndexerError::decode_error("extensions", e)) }) .transpose()?, }; @@ -1037,3 +1055,385 @@ impl TryFrom<&photon_api::models::OwnerBalance> for OwnerBalance { }) } } + +/// Mint-specific data for compressed mints +#[derive(Clone, Default, Debug, PartialEq)] +pub struct MintData { + /// The PDA (decompressed account address) for this mint + pub mint_pda: Pubkey, + /// The signer/seed used for PDA derivation + pub mint_signer: [u8; 32], + /// Authority that can mint new tokens + pub mint_authority: Option, + /// Authority that can freeze accounts + pub freeze_authority: Option, + /// Total supply of tokens + pub supply: u64, + /// Number of decimals + pub decimals: u8, + /// Version of the mint + pub version: u8, + /// Whether the mint has been decompressed + pub mint_decompressed: bool, + /// Serialized extensions (decoded bytes; base64 decoded in `TryFrom`) + pub extensions: Option>, +} + +impl TryFrom<&photon_api::models::MintData> for MintData { + type Error = IndexerError; + + fn try_from(mint_data: &photon_api::models::MintData) -> Result { + Ok(MintData { + mint_pda: Pubkey::new_from_array(decode_base58_to_fixed_array(&mint_data.mint_pda)?), + mint_signer: decode_base58_to_fixed_array(&mint_data.mint_signer)?, + mint_authority: mint_data + .mint_authority + .as_ref() + .map(|a| decode_base58_to_fixed_array(a).map(Pubkey::new_from_array)) + .transpose()?, + freeze_authority: mint_data + .freeze_authority + .as_ref() + .map(|a| decode_base58_to_fixed_array(a).map(Pubkey::new_from_array)) + .transpose()?, + supply: mint_data.supply, + decimals: mint_data.decimals, + version: mint_data.version, + mint_decompressed: mint_data.mint_decompressed, + extensions: mint_data + .extensions + .as_ref() + .map(|ext| { + base64::decode_config(ext, base64::STANDARD_NO_PAD) + .map_err(|e| IndexerError::decode_error("extensions", e)) + }) + .transpose()?, + }) + } +} + +impl MintData { + /// Convert to `light_token_interface::state::Mint`. + /// + /// This reconstructs the full Mint struct from the indexed data. + /// Note: `CompressionInfo` is defaulted since it's not stored by the indexer. + pub fn to_light_mint(&self) -> Result { + // Derive bump from mint_signer + let mint_signer_pubkey = Pubkey::new_from_array(self.mint_signer); + let (derived_pda, bump) = find_mint_address(&mint_signer_pubkey); + + // Verify derived PDA matches stored mint_pda (fail fast on mismatch) + if derived_pda != self.mint_pda { + return Err(IndexerError::DataDecodeError { + field: "mint_pda".to_string(), + message: format!( + "Derived mint PDA {} (bump={}) does not match stored mint_pda {}", + derived_pda, bump, self.mint_pda + ), + }); + } + + // Parse extensions if present + let extensions = self + .extensions + .as_ref() + .map(|ext_bytes| { + Vec::::deserialize(&mut ext_bytes.as_slice()) + .map_err(|e| IndexerError::decode_error("extensions", e)) + }) + .transpose()?; + + Ok(LightMint { + base: BaseMint { + mint_authority: self + .mint_authority + .map(|p| light_compressed_account::Pubkey::new_from_array(p.to_bytes())), + supply: self.supply, + decimals: self.decimals, + is_initialized: true, // Always true for indexed mints + freeze_authority: self + .freeze_authority + .map(|p| light_compressed_account::Pubkey::new_from_array(p.to_bytes())), + }, + metadata: MintMetadata { + version: self.version, + mint_decompressed: self.mint_decompressed, + mint: light_compressed_account::Pubkey::new_from_array(self.mint_pda.to_bytes()), + mint_signer: self.mint_signer, + bump, + }, + reserved: [0u8; 16], + account_type: ACCOUNT_TYPE_MINT, + compression: CompressionInfo::default(), // Not stored by indexer + extensions, + }) + } +} + +/// A compressed mint with its account data +#[derive(Clone, Default, Debug, PartialEq)] +pub struct CompressedMint { + /// Mint-specific data (mint_pda, authorities, supply, decimals, etc.) + pub mint: MintData, + /// General account information (address, hash, lamports, merkle context, etc.) + pub account: CompressedAccount, +} + +impl TryFrom<&photon_api::models::CompressedMint> for CompressedMint { + type Error = IndexerError; + + fn try_from(compressed_mint: &photon_api::models::CompressedMint) -> Result { + let account = CompressedAccount::try_from(compressed_mint.account.as_ref())?; + let mint = MintData::try_from(compressed_mint.mint_data.as_ref())?; + + Ok(CompressedMint { mint, account }) + } +} + +// ============ Interface Types ============ +// These types are used by the Interface endpoints that race hot (on-chain) and cold (compressed) lookups + +/// Standard Solana account fields +#[derive(Clone, Debug, PartialEq)] +pub struct SolanaAccountData { + pub lamports: u64, + pub data: Vec, + pub owner: Pubkey, + pub executable: bool, + pub rent_epoch: u64, + pub space: u64, +} + +/// Merkle tree info for compressed accounts +#[derive(Clone, Debug, PartialEq)] +pub struct InterfaceTreeInfo { + pub tree: Pubkey, + pub seq: Option, +} + +/// Structured compressed account data (discriminator separated) +#[derive(Clone, Debug, PartialEq)] +pub struct ColdData { + pub discriminator: [u8; 8], + pub data: Vec, +} + +/// Compressed account context — present when account is in compressed state +#[derive(Clone, Debug, PartialEq)] +pub enum ColdContext { + Account { + hash: [u8; 32], + leaf_index: u64, + tree_info: InterfaceTreeInfo, + data: ColdData, + }, + Token { + hash: [u8; 32], + leaf_index: u64, + tree_info: InterfaceTreeInfo, + data: ColdData, + }, + Mint { + hash: [u8; 32], + leaf_index: u64, + tree_info: InterfaceTreeInfo, + data: ColdData, + }, +} + +/// Decode tree info from photon_api format +fn decode_tree_info( + tree_info: &photon_api::models::InterfaceTreeInfo, +) -> Result { + Ok(InterfaceTreeInfo { + tree: Pubkey::new_from_array(decode_base58_to_fixed_array(&tree_info.tree)?), + seq: tree_info.seq, + }) +} + +/// Decode cold data from photon_api format +fn decode_cold_data(data: &photon_api::models::ColdData) -> Result { + Ok(ColdData { + discriminator: data.discriminator, + data: base64::decode_config(&data.data, base64::STANDARD_NO_PAD) + .map_err(|e| IndexerError::decode_error("data", e))?, + }) +} + +/// Helper to convert photon_api ColdContext to client ColdContext +fn convert_cold_context( + cold: &photon_api::models::ColdContext, +) -> Result { + match cold { + photon_api::models::ColdContext::Account { + hash, + leaf_index, + tree_info, + data, + } => Ok(ColdContext::Account { + hash: decode_base58_to_fixed_array(hash)?, + leaf_index: *leaf_index, + tree_info: decode_tree_info(tree_info)?, + data: decode_cold_data(data)?, + }), + photon_api::models::ColdContext::Token { + hash, + leaf_index, + tree_info, + data, + } => Ok(ColdContext::Token { + hash: decode_base58_to_fixed_array(hash)?, + leaf_index: *leaf_index, + tree_info: decode_tree_info(tree_info)?, + data: decode_cold_data(data)?, + }), + photon_api::models::ColdContext::Mint { + hash, + leaf_index, + tree_info, + data, + } => Ok(ColdContext::Mint { + hash: decode_base58_to_fixed_array(hash)?, + leaf_index: *leaf_index, + tree_info: decode_tree_info(tree_info)?, + data: decode_cold_data(data)?, + }), + } +} + +/// Unified account interface — works for both on-chain and compressed accounts +#[derive(Clone, Debug, PartialEq)] +pub struct AccountInterface { + /// The on-chain Solana pubkey + pub key: Pubkey, + /// Standard Solana account fields + pub account: SolanaAccountData, + /// Compressed context — None if on-chain, Some if compressed + pub cold: Option, +} + +impl AccountInterface { + /// Returns true if this account is on-chain (hot) + pub fn is_hot(&self) -> bool { + self.cold.is_none() + } + + /// Returns true if this account is compressed (cold) + pub fn is_cold(&self) -> bool { + self.cold.is_some() + } +} + +/// Helper to convert photon_api AccountInterface to client AccountInterface +fn convert_account_interface( + ai: &photon_api::models::AccountInterface, +) -> Result { + let cold = ai.cold.as_ref().map(convert_cold_context).transpose()?; + + let data = base64::decode_config(&ai.account.data, base64::STANDARD_NO_PAD) + .map_err(|e| IndexerError::decode_error("account.data", e))?; + + Ok(AccountInterface { + key: Pubkey::new_from_array(decode_base58_to_fixed_array(&ai.key)?), + account: SolanaAccountData { + lamports: ai.account.lamports, + data, + owner: Pubkey::new_from_array(decode_base58_to_fixed_array(&ai.account.owner)?), + executable: ai.account.executable, + rent_epoch: ai.account.rent_epoch, + space: ai.account.space, + }, + cold, + }) +} + +impl TryFrom<&photon_api::models::AccountInterface> for AccountInterface { + type Error = IndexerError; + + fn try_from(ai: &photon_api::models::AccountInterface) -> Result { + convert_account_interface(ai) + } +} + +impl TryFrom<&photon_api::models::InterfaceResult> for AccountInterface { + type Error = IndexerError; + + fn try_from(ir: &photon_api::models::InterfaceResult) -> Result { + match ir { + photon_api::models::InterfaceResult::Account(ai) => AccountInterface::try_from(ai), + photon_api::models::InterfaceResult::Token(tai) => { + AccountInterface::try_from(&tai.account) + } + photon_api::models::InterfaceResult::Mint(mi) => { + AccountInterface::try_from(&mi.account) + } + } + } +} + +/// Token account interface with parsed token data +#[derive(Clone, Debug, PartialEq)] +pub struct TokenAccountInterface { + /// Base account interface data + pub account: AccountInterface, + /// Parsed token data (same as CompressedTokenAccount.token) + pub token: TokenData, +} + +impl TryFrom<&photon_api::models::TokenAccountInterface> for TokenAccountInterface { + type Error = IndexerError; + + fn try_from(tai: &photon_api::models::TokenAccountInterface) -> Result { + let account = convert_account_interface(&tai.account)?; + + // Parse token data - same pattern as CompressedTokenAccount + let token = TokenData { + mint: Pubkey::new_from_array(decode_base58_to_fixed_array(&tai.token_data.mint)?), + owner: Pubkey::new_from_array(decode_base58_to_fixed_array(&tai.token_data.owner)?), + amount: tai.token_data.amount, + delegate: tai + .token_data + .delegate + .as_ref() + .map(|d| decode_base58_to_fixed_array(d).map(Pubkey::new_from_array)) + .transpose()?, + state: match tai.token_data.state { + photon_api::models::AccountState::Initialized => AccountState::Initialized, + photon_api::models::AccountState::Frozen => AccountState::Frozen, + }, + tlv: tai + .token_data + .tlv + .as_ref() + .map(|tlv| { + let bytes = base64::decode_config(tlv, base64::STANDARD_NO_PAD) + .map_err(|e| IndexerError::decode_error("tlv", e))?; + Vec::::deserialize(&mut bytes.as_slice()) + .map_err(|e| IndexerError::decode_error("extensions", e)) + }) + .transpose()?, + }; + + Ok(TokenAccountInterface { account, token }) + } +} + +/// Mint account interface with parsed mint data +#[derive(Clone, Debug, PartialEq)] +pub struct MintInterface { + /// Base account interface data + pub account: AccountInterface, + /// Parsed mint data + pub mint_data: MintData, +} + +impl TryFrom<&photon_api::models::MintInterface> for MintInterface { + type Error = IndexerError; + + fn try_from(mi: &photon_api::models::MintInterface) -> Result { + let account = convert_account_interface(&mi.account)?; + let mint_data = MintData::try_from(&mi.mint_data)?; + + Ok(MintInterface { account, mint_data }) + } +} diff --git a/sdk-libs/client/src/interface/account_interface.rs b/sdk-libs/client/src/interface/account_interface.rs index 4c04469b7f..8c96e84d13 100644 --- a/sdk-libs/client/src/interface/account_interface.rs +++ b/sdk-libs/client/src/interface/account_interface.rs @@ -43,7 +43,7 @@ pub enum AccountInterfaceError { /// Uses standard `solana_account::Account` for raw data. /// For hot accounts: actual on-chain bytes. /// For cold accounts: synthetic bytes from cold data. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Default)] pub struct AccountInterface { /// The account's public key. pub key: Pubkey, @@ -150,6 +150,7 @@ impl AccountInterface { match &self.cold { Some(ColdContext::Account(c)) => Some(c.hash), Some(ColdContext::Token(c)) => Some(c.account.hash), + Some(ColdContext::Mint(c)) => Some(c.hash), None => None, } } @@ -159,6 +160,7 @@ impl AccountInterface { match &self.cold { Some(ColdContext::Account(c)) => Some(&c.tree_info), Some(ColdContext::Token(c)) => Some(&c.account.tree_info), + Some(ColdContext::Mint(c)) => Some(&c.tree_info), None => None, } } @@ -168,14 +170,16 @@ impl AccountInterface { match &self.cold { Some(ColdContext::Account(c)) => Some(c.leaf_index), Some(ColdContext::Token(c)) => Some(c.account.leaf_index), + Some(ColdContext::Mint(c)) => Some(c.leaf_index), None => None, } } - /// Get as CompressedAccount if cold account type. + /// Get as CompressedAccount if cold account or mint type. pub fn as_compressed_account(&self) -> Option<&CompressedAccount> { match &self.cold { Some(ColdContext::Account(c)) => Some(c), + Some(ColdContext::Mint(c)) => Some(c), _ => None, } } @@ -191,7 +195,7 @@ impl AccountInterface { /// Try to parse as Mint. Returns None if not a mint or parse fails. pub fn as_mint(&self) -> Option { match &self.cold { - Some(ColdContext::Account(ca)) => { + Some(ColdContext::Mint(ca)) => { let data = ca.data.as_ref()?; borsh::BorshDeserialize::deserialize(&mut data.data.as_slice()).ok() } @@ -218,7 +222,7 @@ impl AccountInterface { /// /// For ATAs: `parsed.owner` is the wallet owner (set from fetch params). /// For program-owned: `parsed.owner` is the PDA. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Default)] pub struct TokenAccountInterface { /// The token account's public key. pub key: Pubkey, @@ -390,3 +394,13 @@ impl TokenAccountInterface { self.ata_bump().is_some() } } + +impl From for AccountInterface { + fn from(tai: TokenAccountInterface) -> Self { + Self { + key: tai.key, + account: tai.account, + cold: tai.cold, + } + } +} diff --git a/sdk-libs/client/src/interface/account_interface_ext.rs b/sdk-libs/client/src/interface/account_interface_ext.rs deleted file mode 100644 index d6ae2237a5..0000000000 --- a/sdk-libs/client/src/interface/account_interface_ext.rs +++ /dev/null @@ -1,288 +0,0 @@ -use async_trait::async_trait; -use borsh::BorshDeserialize as _; -use light_compressed_account::address::derive_address; -use light_token::instruction::derive_token_ata; -use light_token_interface::{state::Mint, MINT_ADDRESS_TREE}; -use solana_pubkey::Pubkey; - -use super::{AccountInterface, AccountToFetch, MintInterface, MintState, TokenAccountInterface}; -use crate::{ - indexer::{GetCompressedTokenAccountsByOwnerOrDelegateOptions, Indexer}, - rpc::{Rpc, RpcError}, -}; - -fn indexer_err(e: impl std::fmt::Display) -> RpcError { - RpcError::CustomError(format!("IndexerError: {}", e)) -} - -/// Extension trait for fetching account interfaces (unified hot/cold handling). -#[async_trait] -pub trait AccountInterfaceExt: Rpc + Indexer { - /// Fetch MintInterface for a mint account. - /// - /// Use this instead of get_account + unpack_mint. - async fn get_mint_interface(&self, address: &Pubkey) -> Result; - - /// Fetch AccountInterface for an account. - /// - /// Use this instead of get_account. - async fn get_account_interface( - &self, - address: &Pubkey, - program_id: &Pubkey, - ) -> Result; - - /// Fetch TokenAccountInterface for a token account. - /// - /// Use this instead of get_token_account. - async fn get_token_account_interface( - &self, - address: &Pubkey, - ) -> Result; - - /// Fetch TokenAccountInterface for an associated token account. - /// - /// Use this for all ATAs. - async fn get_ata_interface( - &self, - owner: &Pubkey, - mint: &Pubkey, - ) -> Result; - - /// Fetch multiple accounts with automatic type dispatch. - /// - /// Use this instead of get_multiple_accounts. - async fn get_multiple_account_interfaces( - &self, - accounts: &[AccountToFetch], - ) -> Result, RpcError>; -} - -// TODO: move all these to native RPC methods with single roundtrip. -#[async_trait] -impl AccountInterfaceExt for T { - async fn get_mint_interface(&self, address: &Pubkey) -> Result { - let address_tree = Pubkey::new_from_array(MINT_ADDRESS_TREE); - let compressed_address = derive_address( - &address.to_bytes(), - &address_tree.to_bytes(), - &light_token_interface::LIGHT_TOKEN_PROGRAM_ID, - ); - - // Hot - if let Some(account) = self.get_account(*address).await? { - if account.lamports > 0 { - return Ok(MintInterface { - mint: *address, - address_tree, - compressed_address, - state: MintState::Hot { account }, - }); - } - } - - // Cold - let result = self - .get_compressed_account(compressed_address, None) - .await - .map_err(indexer_err)?; - - if let Some(compressed) = result.value { - if let Some(data) = compressed.data.as_ref() { - if !data.data.is_empty() { - let mint_data = Mint::try_from_slice(&data.data) - .map_err(|e| RpcError::CustomError(format!("mint parse error: {}", e)))?; - return Ok(MintInterface { - mint: *address, - address_tree, - compressed_address, - state: MintState::Cold { - compressed, - mint_data, - }, - }); - } - } - } - - Ok(MintInterface { - mint: *address, - address_tree, - compressed_address, - state: MintState::None, - }) - } - - async fn get_account_interface( - &self, - address: &Pubkey, - program_id: &Pubkey, - ) -> Result { - let address_tree = self.get_address_tree_v2().tree; - let compressed_address = derive_address( - &address.to_bytes(), - &address_tree.to_bytes(), - &program_id.to_bytes(), - ); - - // Hot - if let Some(account) = self.get_account(*address).await? { - if account.lamports > 0 { - return Ok(AccountInterface::hot(*address, account)); - } - } - - // Cold - let result = self - .get_compressed_account(compressed_address, None) - .await - .map_err(indexer_err)?; - - if let Some(compressed) = result.value { - if compressed.data.as_ref().is_some_and(|d| !d.data.is_empty()) { - return Ok(AccountInterface::cold(*address, compressed, *program_id)); - } - } - - // Doesn't exist. - let account = solana_account::Account { - lamports: 0, - data: vec![], - owner: *program_id, - executable: false, - rent_epoch: 0, - }; - Ok(AccountInterface::hot(*address, account)) - } - - async fn get_token_account_interface( - &self, - address: &Pubkey, - ) -> Result { - use light_sdk::constants::LIGHT_TOKEN_PROGRAM_ID; - - // Hot - if let Some(account) = self.get_account(*address).await? { - if account.lamports > 0 { - return TokenAccountInterface::hot(*address, account) - .map_err(|e| RpcError::CustomError(format!("parse error: {}", e))); - } - } - - // Cold (program-owned tokens: address = owner) - let result = self - .get_compressed_token_accounts_by_owner(address, None, None) - .await - .map_err(indexer_err)?; - - if let Some(compressed) = result.value.items.into_iter().next() { - return Ok(TokenAccountInterface::cold( - *address, - compressed, - *address, // owner = hot address - LIGHT_TOKEN_PROGRAM_ID.into(), - )); - } - - Err(RpcError::CustomError(format!( - "token account not found: {}", - address - ))) - } - - async fn get_ata_interface( - &self, - owner: &Pubkey, - mint: &Pubkey, - ) -> Result { - use light_sdk::constants::LIGHT_TOKEN_PROGRAM_ID; - - let (ata, _bump) = derive_token_ata(owner, mint); - - // Hot - if let Some(account) = self.get_account(ata).await? { - if account.lamports > 0 { - return TokenAccountInterface::hot(ata, account) - .map_err(|e| RpcError::CustomError(format!("parse error: {}", e))); - } - } - - // Cold (ATA query by address) - let options = Some(GetCompressedTokenAccountsByOwnerOrDelegateOptions::new( - Some(*mint), - )); - let result = self - .get_compressed_token_accounts_by_owner(&ata, options, None) - .await - .map_err(indexer_err)?; - - if let Some(compressed) = result.value.items.into_iter().next() { - return Ok(TokenAccountInterface::cold( - ata, - compressed, - *owner, // owner_override = wallet owner - LIGHT_TOKEN_PROGRAM_ID.into(), - )); - } - - Err(RpcError::CustomError(format!( - "ATA not found: owner={} mint={}", - owner, mint - ))) - } - - async fn get_multiple_account_interfaces( - &self, - accounts: &[AccountToFetch], - ) -> Result, RpcError> { - // TODO: concurrent with futures - let mut result = Vec::with_capacity(accounts.len()); - - for account in accounts { - let iface = match account { - AccountToFetch::Pda { - address, - program_id, - } => self.get_account_interface(address, program_id).await?, - AccountToFetch::Token { address } => { - let token_iface = self.get_token_account_interface(address).await?; - AccountInterface { - key: token_iface.key, - account: token_iface.account, - cold: token_iface.cold, - } - } - AccountToFetch::Ata { wallet_owner, mint } => { - let token_iface = self.get_ata_interface(wallet_owner, mint).await?; - AccountInterface { - key: token_iface.key, - account: token_iface.account, - cold: token_iface.cold, - } - } - AccountToFetch::Mint { address } => { - let mint_iface = self.get_mint_interface(address).await?; - match mint_iface.state { - MintState::Hot { account } => AccountInterface { - key: mint_iface.mint, - account, - cold: None, - }, - MintState::Cold { compressed, .. } => { - let owner = compressed.owner; - AccountInterface::cold(mint_iface.mint, compressed, owner) - } - MintState::None => AccountInterface { - key: mint_iface.mint, - account: Default::default(), - cold: None, - }, - } - } - }; - result.push(iface); - } - - Ok(result) - } -} diff --git a/sdk-libs/client/src/interface/decompress_mint.rs b/sdk-libs/client/src/interface/decompress_mint.rs index d877c1ce71..db09536a46 100644 --- a/sdk-libs/client/src/interface/decompress_mint.rs +++ b/sdk-libs/client/src/interface/decompress_mint.rs @@ -13,6 +13,7 @@ use solana_instruction::Instruction; use solana_pubkey::Pubkey; use thiserror::Error; +use super::{AccountInterface, ColdContext}; use crate::indexer::{CompressedAccount, Indexer, ValidityProofWithContext}; /// Error type for mint load operations. @@ -38,7 +39,7 @@ pub enum DecompressMintError { } /// Mint state: hot (on-chain), cold (compressed), or none. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Default)] #[allow(clippy::large_enum_variant)] pub enum MintState { /// On-chain. @@ -49,11 +50,12 @@ pub enum MintState { mint_data: Mint, }, /// Doesn't exist. + #[default] None, } /// Mint interface for hot/cold handling. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Default)] pub struct MintInterface { pub mint: Pubkey, pub address_tree: Pubkey, @@ -97,6 +99,51 @@ impl MintInterface { } } +impl From for AccountInterface { + fn from(mi: MintInterface) -> Self { + match mi.state { + MintState::Hot { account } => Self { + key: mi.mint, + account, + cold: None, + }, + MintState::Cold { + compressed, + mint_data: _, + } => { + let data = compressed + .data + .as_ref() + .map(|d| { + let mut buf = d.discriminator.to_vec(); + buf.extend_from_slice(&d.data); + buf + }) + .unwrap_or_default(); + + Self { + key: mi.mint, + account: Account { + lamports: compressed.lamports, + data, + owner: Pubkey::new_from_array( + light_token_interface::LIGHT_TOKEN_PROGRAM_ID, + ), + executable: false, + rent_epoch: 0, + }, + cold: Some(ColdContext::Mint(compressed)), + } + } + MintState::None => Self { + key: mi.mint, + account: Account::default(), + cold: None, + }, + } + } +} + pub const DEFAULT_RENT_PAYMENT: u8 = 2; pub const DEFAULT_WRITE_TOP_UP: u32 = 0; diff --git a/sdk-libs/client/src/interface/light_program_interface.rs b/sdk-libs/client/src/interface/light_program_interface.rs index 3817140a23..42d7cff8c9 100644 --- a/sdk-libs/client/src/interface/light_program_interface.rs +++ b/sdk-libs/client/src/interface/light_program_interface.rs @@ -22,6 +22,9 @@ pub enum AccountToFetch { Pda { address: Pubkey, program_id: Pubkey }, /// Token account (program-owned) - uses `get_token_account_interface(address)` Token { address: Pubkey }, + /// Token account by owner and mint - uses `get_compressed_token_accounts_by_owner` with mint filter + /// This is for program-owned token accounts (like vaults) where the address can't be looked up directly + TokenByOwnerMint { owner: Pubkey, mint: Pubkey }, /// ATA - uses `get_ata_interface(wallet_owner, mint)` Ata { wallet_owner: Pubkey, mint: Pubkey }, /// Light mint - uses `get_mint_interface(address)` @@ -40,6 +43,13 @@ impl AccountToFetch { Self::Token { address } } + /// Create a token fetch request by owner and mint. + /// Use this for program-owned token accounts (like vaults) where the on-chain PDA + /// address isn't preserved in the compressed representation. + pub fn token_by_owner_mint(owner: Pubkey, mint: Pubkey) -> Self { + Self::TokenByOwnerMint { owner, mint } + } + pub fn ata(wallet_owner: Pubkey, mint: Pubkey) -> Self { Self::Ata { wallet_owner, mint } } @@ -48,11 +58,14 @@ impl AccountToFetch { Self::Mint { address } } + /// Returns the primary pubkey for this fetch request. + /// For `TokenByOwnerMint`, returns the owner since there's no single address. #[must_use] pub fn pubkey(&self) -> Pubkey { match self { Self::Pda { address, .. } => *address, Self::Token { address } => *address, + Self::TokenByOwnerMint { owner, .. } => *owner, Self::Ata { wallet_owner, mint } => derive_token_ata(wallet_owner, mint).0, Self::Mint { address } => *address, } @@ -61,15 +74,18 @@ impl AccountToFetch { /// Context for cold accounts. /// -/// Two variants based on data structure, not account type: -/// - `Account` - PDA +/// Three variants based on data structure: +/// - `Account` - Generic PDA /// - `Token` - Token account -#[derive(Clone, Debug)] +/// - `Mint` - Compressed mint +#[derive(Clone, Debug, PartialEq)] pub enum ColdContext { - /// PDA + /// Generic PDA Account(CompressedAccount), /// Token account Token(CompressedTokenAccount), + /// Compressed mint + Mint(CompressedAccount), } /// Specification for a program-owned PDA with typed variant. @@ -130,6 +146,7 @@ impl PdaSpec { match &self.interface.cold { Some(ColdContext::Account(c)) => Some(c), Some(ColdContext::Token(c)) => Some(&c.account), + Some(ColdContext::Mint(c)) => Some(c), None => None, } } diff --git a/sdk-libs/client/src/interface/mod.rs b/sdk-libs/client/src/interface/mod.rs index b8847c6e98..e7b2f15c90 100644 --- a/sdk-libs/client/src/interface/mod.rs +++ b/sdk-libs/client/src/interface/mod.rs @@ -1,7 +1,6 @@ //! Client utilities for hot/cold account handling. pub mod account_interface; -pub mod account_interface_ext; pub mod create_accounts_proof; pub mod decompress_mint; pub mod initialize_config; @@ -12,7 +11,6 @@ pub mod pack; pub mod tx_size; pub use account_interface::{AccountInterface, AccountInterfaceError, TokenAccountInterface}; -pub use account_interface_ext::AccountInterfaceExt; pub use create_accounts_proof::{ get_create_accounts_proof, CreateAccountsProofError, CreateAccountsProofInput, CreateAccountsProofResult, diff --git a/sdk-libs/client/src/local_test_validator.rs b/sdk-libs/client/src/local_test_validator.rs index b0b7dfbcbc..d2165ed403 100644 --- a/sdk-libs/client/src/local_test_validator.rs +++ b/sdk-libs/client/src/local_test_validator.rs @@ -2,6 +2,27 @@ use std::process::{Command, Stdio}; use light_prover_client::helpers::get_project_root; +/// Configuration for an upgradeable program to deploy to the validator. +#[derive(Debug, Clone)] +pub struct UpgradeableProgramConfig { + /// The program ID (public key) of the program + pub program_id: String, + /// Path to the compiled program (.so file) + pub program_path: String, + /// The upgrade authority for the program + pub upgrade_authority: String, +} + +impl UpgradeableProgramConfig { + pub fn new(program_id: String, program_path: String, upgrade_authority: String) -> Self { + Self { + program_id, + program_path, + upgrade_authority, + } + } +} + #[derive(Debug)] pub struct LightValidatorConfig { pub enable_indexer: bool, @@ -9,10 +30,12 @@ pub struct LightValidatorConfig { pub wait_time: u64, /// Non-upgradeable programs: (program_id, program_path) pub sbf_programs: Vec<(String, String)>, - /// Upgradeable programs: (program_id, program_path, upgrade_authority) - /// Use this when the program needs a valid upgrade authority (e.g., for compression config) - pub upgradeable_programs: Vec<(String, String, String)>, + /// Upgradeable programs to deploy with a valid upgrade authority. + /// Use this when the program needs a valid upgrade authority (e.g., for compression config). + pub upgradeable_programs: Vec, pub limit_ledger_size: Option, + /// Additional arguments to pass to the validator (e.g., "--account
") + pub validator_args: Vec, } impl Default for LightValidatorConfig { @@ -24,6 +47,7 @@ impl Default for LightValidatorConfig { sbf_programs: vec![], upgradeable_programs: vec![], limit_ledger_size: None, + validator_args: vec![], } } } @@ -50,7 +74,9 @@ pub async fn spawn_validator(config: LightValidatorConfig) { for upgradeable_program in config.upgradeable_programs.iter() { path.push_str(&format!( " --upgradeable-program {} {} {}", - upgradeable_program.0, upgradeable_program.1, upgradeable_program.2 + upgradeable_program.program_id, + upgradeable_program.program_path, + upgradeable_program.upgrade_authority )); } @@ -58,6 +84,10 @@ pub async fn spawn_validator(config: LightValidatorConfig) { path.push_str(" --skip-prover"); } + for arg in config.validator_args.iter() { + path.push_str(&format!(" {}", arg)); + } + println!("Starting validator with command: {}", path); let child = Command::new("sh") diff --git a/sdk-libs/client/src/rpc/client.rs b/sdk-libs/client/src/rpc/client.rs index 09dabfa7cb..5c00f32f7c 100644 --- a/sdk-libs/client/src/rpc/client.rs +++ b/sdk-libs/client/src/rpc/client.rs @@ -31,7 +31,12 @@ use tracing::warn; use super::LightClientConfig; use crate::{ - indexer::{photon_indexer::PhotonIndexer, Indexer, TreeInfo}, + indexer::{ + photon_indexer::PhotonIndexer, AccountInterface as IndexerAccountInterface, Indexer, + IndexerRpcConfig, MintInterface as IndexerMintInterface, Response, + TokenAccountInterface as IndexerTokenAccountInterface, TreeInfo, + }, + interface::{AccountInterface, MintInterface, MintState, TokenAccountInterface}, rpc::{ errors::RpcError, get_light_state_tree_infos::{ @@ -432,6 +437,297 @@ impl LightClient { } } +// Conversion helpers from indexer types to interface types + +fn convert_account_interface( + indexer_ai: IndexerAccountInterface, +) -> Result { + use light_compressed_account::compressed_account::CompressedAccountData; + + use crate::indexer::{ColdContext as IndexerColdContext, CompressedAccount}; + + let account = Account { + lamports: indexer_ai.account.lamports, + data: indexer_ai.account.data, + owner: indexer_ai.account.owner, + executable: indexer_ai.account.executable, + rent_epoch: indexer_ai.account.rent_epoch, + }; + + match indexer_ai.cold { + None => Ok(AccountInterface::hot(indexer_ai.key, account)), + Some(IndexerColdContext::Account { + hash, + leaf_index, + tree_info, + data, + }) => { + let compressed = CompressedAccount { + address: None, + data: Some(CompressedAccountData { + discriminator: data.discriminator, + data: data.data, + data_hash: [0u8; 32], // Computed on demand if needed + }), + hash, + lamports: indexer_ai.account.lamports, + leaf_index: leaf_index as u32, + owner: indexer_ai.account.owner, + prove_by_index: false, + seq: tree_info.seq, + slot_created: 0, + tree_info: TreeInfo { + tree: tree_info.tree, + queue: tree_info.tree, // TODO: proper queue mapping (requires indexer changes) + cpi_context: None, + next_tree_info: None, + tree_type: TreeType::StateV1, // TODO: proper tree_type mapping (requires indexer changes) + }, + }; + Ok(AccountInterface::cold( + indexer_ai.key, + compressed, + indexer_ai.account.owner, + )) + } + Some(IndexerColdContext::Token { + hash, + leaf_index, + tree_info, + data, + }) => { + use light_token::compat::TokenData; + + use crate::indexer::CompressedTokenAccount; + + // Parse token data from the cold data - propagate errors instead of using default + let token_data: TokenData = + borsh::BorshDeserialize::deserialize(&mut data.data.as_slice()).map_err(|e| { + RpcError::CustomError(format!("Failed to deserialize token data: {}", e)) + })?; + + let wallet_owner = token_data.owner; + + let compressed_account = CompressedAccount { + address: None, + data: Some(CompressedAccountData { + discriminator: data.discriminator, + data: data.data, + data_hash: [0u8; 32], + }), + hash, + lamports: indexer_ai.account.lamports, + leaf_index: leaf_index as u32, + owner: indexer_ai.account.owner, + prove_by_index: false, + seq: tree_info.seq, + slot_created: 0, + tree_info: TreeInfo { + tree: tree_info.tree, + queue: tree_info.tree, // TODO: proper queue mapping (requires indexer changes) + cpi_context: None, + next_tree_info: None, + tree_type: TreeType::StateV1, // TODO: proper tree_type mapping (requires indexer changes) + }, + }; + let compressed_token = CompressedTokenAccount { + token: token_data, + account: compressed_account.clone(), + }; + Ok(AccountInterface::cold_token( + indexer_ai.key, + compressed_token, + wallet_owner, + )) + } + Some(IndexerColdContext::Mint { + hash, + leaf_index, + tree_info, + data, + }) => { + let compressed = CompressedAccount { + address: None, + data: Some(CompressedAccountData { + discriminator: data.discriminator, + data: data.data, + data_hash: [0u8; 32], + }), + hash, + lamports: indexer_ai.account.lamports, + leaf_index: leaf_index as u32, + owner: indexer_ai.account.owner, + prove_by_index: false, + seq: tree_info.seq, + slot_created: 0, + tree_info: TreeInfo { + tree: tree_info.tree, + queue: tree_info.tree, // TODO: proper queue mapping (requires indexer changes) + cpi_context: None, + next_tree_info: None, + tree_type: TreeType::StateV1, // TODO: proper tree_type mapping (requires indexer changes) + }, + }; + Ok(AccountInterface::cold( + indexer_ai.key, + compressed, + indexer_ai.account.owner, + )) + } + } +} + +fn convert_token_account_interface( + indexer_tai: IndexerTokenAccountInterface, +) -> Result { + use light_compressed_account::compressed_account::CompressedAccountData; + + use crate::indexer::{ + ColdContext as IndexerColdContext, CompressedAccount, CompressedTokenAccount, + }; + + let account = Account { + lamports: indexer_tai.account.account.lamports, + data: indexer_tai.account.account.data.clone(), + owner: indexer_tai.account.account.owner, + executable: indexer_tai.account.account.executable, + rent_epoch: indexer_tai.account.account.rent_epoch, + }; + + match indexer_tai.account.cold { + None => TokenAccountInterface::hot(indexer_tai.account.key, account) + .map_err(|e| RpcError::CustomError(format!("parse error: {}", e))), + Some(IndexerColdContext::Token { + hash, + leaf_index, + tree_info, + data, + }) => { + let compressed_account = CompressedAccount { + address: None, + data: Some(CompressedAccountData { + discriminator: data.discriminator, + data: data.data, + data_hash: [0u8; 32], + }), + hash, + lamports: indexer_tai.account.account.lamports, + leaf_index: leaf_index as u32, + owner: indexer_tai.account.account.owner, + prove_by_index: false, + seq: tree_info.seq, + slot_created: 0, + tree_info: TreeInfo { + tree: tree_info.tree, + queue: tree_info.tree, + cpi_context: None, + next_tree_info: None, + tree_type: TreeType::StateV1, + }, + }; + // Extract token owner before moving token into CompressedTokenAccount + let token_owner = indexer_tai.token.owner; + let compressed_token = CompressedTokenAccount { + token: indexer_tai.token, + account: compressed_account, + }; + Ok(TokenAccountInterface::cold( + indexer_tai.account.key, + compressed_token, + token_owner, // owner_override: use token owner, not account key + indexer_tai.account.account.owner, + )) + } + _ => Err(RpcError::CustomError( + "unexpected cold context type for token account".into(), + )), + } +} + +fn convert_mint_interface(indexer_mi: IndexerMintInterface) -> Result { + use light_compressed_account::{ + address::derive_address, compressed_account::CompressedAccountData, + }; + use light_token_interface::{state::Mint, MINT_ADDRESS_TREE}; + + use crate::indexer::{ColdContext as IndexerColdContext, CompressedAccount}; + + let address_tree = Pubkey::new_from_array(MINT_ADDRESS_TREE); + let compressed_address = derive_address( + &indexer_mi.account.key.to_bytes(), + &address_tree.to_bytes(), + &light_token_interface::LIGHT_TOKEN_PROGRAM_ID, + ); + + let account = Account { + lamports: indexer_mi.account.account.lamports, + data: indexer_mi.account.account.data.clone(), + owner: indexer_mi.account.account.owner, + executable: indexer_mi.account.account.executable, + rent_epoch: indexer_mi.account.account.rent_epoch, + }; + + match indexer_mi.account.cold { + None => Ok(MintInterface { + mint: indexer_mi.account.key, + address_tree, + compressed_address, + state: MintState::Hot { account }, + }), + Some(IndexerColdContext::Mint { + hash, + leaf_index, + tree_info, + data, + }) + | Some(IndexerColdContext::Account { + hash, + leaf_index, + tree_info, + data, + }) => { + let mint_data = Mint::try_from_slice(&data.data) + .map_err(|e| RpcError::CustomError(format!("mint parse error: {}", e)))?; + + let compressed = CompressedAccount { + address: None, + data: Some(CompressedAccountData { + discriminator: data.discriminator, + data: data.data, + data_hash: [0u8; 32], + }), + hash, + lamports: indexer_mi.account.account.lamports, + leaf_index: leaf_index as u32, + owner: indexer_mi.account.account.owner, + prove_by_index: false, + seq: tree_info.seq, + slot_created: 0, + tree_info: TreeInfo { + tree: tree_info.tree, + queue: tree_info.tree, + cpi_context: None, + next_tree_info: None, + tree_type: TreeType::StateV1, + }, + }; + + Ok(MintInterface { + mint: indexer_mi.account.key, + address_tree, + compressed_address, + state: MintState::Cold { + compressed, + mint_data, + }, + }) + } + _ => Err(RpcError::CustomError( + "unexpected cold context type for mint".into(), + )), + } +} + #[async_trait] impl Rpc for LightClient { async fn new(config: LightClientConfig) -> Result @@ -899,6 +1195,183 @@ impl Rpc for LightClient { tree_type: TreeType::AddressV2, } } + + async fn get_account_interface( + &self, + address: &Pubkey, + config: Option, + ) -> Result>, RpcError> { + let indexer = self + .indexer + .as_ref() + .ok_or(RpcError::IndexerNotInitialized)?; + let resp = indexer + .get_account_interface(address, config) + .await + .map_err(|e| RpcError::CustomError(format!("Indexer error: {e}")))?; + + let value = resp.value.map(convert_account_interface).transpose()?; + Ok(Response { + context: resp.context, + value, + }) + } + + async fn get_token_account_interface( + &self, + address: &Pubkey, + config: Option, + ) -> Result>, RpcError> { + let indexer = self + .indexer + .as_ref() + .ok_or(RpcError::IndexerNotInitialized)?; + let resp = indexer + .get_token_account_interface(address, config) + .await + .map_err(|e| RpcError::CustomError(format!("Indexer error: {e}")))?; + + let value = match resp.value { + Some(tai) => Some(convert_token_account_interface(tai)?), + None => None, + }; + + Ok(Response { + context: resp.context, + value, + }) + } + + async fn get_ata_interface( + &self, + owner: &Pubkey, + mint: &Pubkey, + config: Option, + ) -> Result>, RpcError> { + let indexer = self + .indexer + .as_ref() + .ok_or(RpcError::IndexerNotInitialized)?; + let resp = indexer + .get_ata_interface(owner, mint, config) + .await + .map_err(|e| RpcError::CustomError(format!("Indexer error: {e}")))?; + + let value = match resp.value { + Some(tai) => Some(convert_token_account_interface(tai)?), + None => None, + }; + + Ok(Response { + context: resp.context, + value, + }) + } + + async fn get_mint_interface( + &self, + address: &Pubkey, + config: Option, + ) -> Result>, RpcError> { + let indexer = self + .indexer + .as_ref() + .ok_or(RpcError::IndexerNotInitialized)?; + let resp = indexer + .get_mint_interface(address, config) + .await + .map_err(|e| RpcError::CustomError(format!("Indexer error: {e}")))?; + + let value = match resp.value { + Some(mi) => Some(convert_mint_interface(mi)?), + None => None, + }; + + Ok(Response { + context: resp.context, + value, + }) + } + + async fn get_token_account_by_owner_mint( + &self, + owner: &Pubkey, + mint: &Pubkey, + config: Option, + ) -> Result>, RpcError> { + use crate::indexer::GetCompressedTokenAccountsByOwnerOrDelegateOptions; + + let indexer = self + .indexer + .as_ref() + .ok_or(RpcError::IndexerNotInitialized)?; + + let options = Some(GetCompressedTokenAccountsByOwnerOrDelegateOptions { + mint: Some(*mint), + ..Default::default() + }); + + let resp = indexer + .get_compressed_token_accounts_by_owner(owner, options, config) + .await + .map_err(|e| RpcError::CustomError(format!("Indexer error: {e}")))?; + + let items = resp.value.items; + if items.len() > 1 { + return Err(RpcError::CustomError(format!( + "Ambiguous lookup: found {} compressed token accounts for owner {} and mint {}. \ + Use get_compressed_token_accounts_by_owner for multiple accounts.", + items.len(), + owner, + mint + ))); + } + + let value = items.into_iter().next().map(|token_acc| { + let key = token_acc + .account + .address + .map(Pubkey::new_from_array) + .unwrap_or(*owner); + TokenAccountInterface::cold( + key, + token_acc, + *owner, + light_sdk::constants::LIGHT_TOKEN_PROGRAM_ID.into(), + ) + }); + + Ok(Response { + context: resp.context, + value, + }) + } + + async fn get_multiple_account_interfaces( + &self, + addresses: Vec<&Pubkey>, + config: Option, + ) -> Result>>, RpcError> { + let indexer = self + .indexer + .as_ref() + .ok_or(RpcError::IndexerNotInitialized)?; + let resp = indexer + .get_multiple_account_interfaces(addresses, config) + .await + .map_err(|e| RpcError::CustomError(format!("Indexer error: {e}")))?; + + let value: Result>, RpcError> = resp + .value + .into_iter() + .map(|opt| opt.map(convert_account_interface).transpose()) + .collect(); + + Ok(Response { + context: resp.context, + value: value?, + }) + } } impl MerkleTreeExt for LightClient {} diff --git a/sdk-libs/client/src/rpc/indexer.rs b/sdk-libs/client/src/rpc/indexer.rs index 55c6b069e0..0901bddb70 100644 --- a/sdk-libs/client/src/rpc/indexer.rs +++ b/sdk-libs/client/src/rpc/indexer.rs @@ -3,9 +3,10 @@ use solana_pubkey::Pubkey; use super::LightClient; use crate::indexer::{ - Address, AddressWithTree, CompressedAccount, CompressedTokenAccount, - GetCompressedAccountsByOwnerConfig, GetCompressedTokenAccountsByOwnerOrDelegateOptions, Hash, - Indexer, IndexerError, IndexerRpcConfig, Items, ItemsWithCursor, MerkleProof, + Address, AddressWithTree, CompressedAccount, CompressedMint, CompressedTokenAccount, + GetCompressedAccountsByOwnerConfig, GetCompressedMintsByAuthorityOptions, + GetCompressedTokenAccountsByOwnerOrDelegateOptions, Hash, Indexer, IndexerError, + IndexerRpcConfig, Items, ItemsWithCursor, MerkleProof, MintAuthorityType, NewAddressProofWithContext, OwnerBalance, PaginatedOptions, QueueElementsResult, QueueElementsV2Options, QueueInfoResult, Response, RetryConfig, SignatureWithMetadata, TokenBalance, ValidityProofWithContext, @@ -316,4 +317,45 @@ impl Indexer for LightClient { .get_indexer_health(config) .await?) } + + async fn get_compressed_mint( + &self, + address: Address, + config: Option, + ) -> Result>, IndexerError> { + Ok(self + .indexer + .as_ref() + .ok_or(IndexerError::NotInitialized)? + .get_compressed_mint(address, config) + .await?) + } + + async fn get_compressed_mint_by_pda( + &self, + mint_pda: &Pubkey, + config: Option, + ) -> Result>, IndexerError> { + Ok(self + .indexer + .as_ref() + .ok_or(IndexerError::NotInitialized)? + .get_compressed_mint_by_pda(mint_pda, config) + .await?) + } + + async fn get_compressed_mints_by_authority( + &self, + authority: &Pubkey, + authority_type: MintAuthorityType, + options: Option, + config: Option, + ) -> Result>, IndexerError> { + Ok(self + .indexer + .as_ref() + .ok_or(IndexerError::NotInitialized)? + .get_compressed_mints_by_authority(authority, authority_type, options, config) + .await?) + } } diff --git a/sdk-libs/client/src/rpc/rpc_trait.rs b/sdk-libs/client/src/rpc/rpc_trait.rs index 104c32d51e..56df386749 100644 --- a/sdk-libs/client/src/rpc/rpc_trait.rs +++ b/sdk-libs/client/src/rpc/rpc_trait.rs @@ -18,7 +18,8 @@ use solana_transaction_status_client_types::TransactionStatus; use super::client::RpcUrl; use crate::{ - indexer::{Indexer, TreeInfo}, + indexer::{Indexer, IndexerRpcConfig, Response, TreeInfo}, + interface::{AccountInterface, AccountToFetch, MintInterface, TokenAccountInterface}, rpc::errors::RpcError, }; @@ -234,4 +235,128 @@ pub trait Rpc: Send + Sync + Debug + 'static { fn get_address_tree_v1(&self) -> TreeInfo; fn get_address_tree_v2(&self) -> TreeInfo; + + // ============ Interface Methods ============ + // These race hot (on-chain) and cold (compressed) lookups in the indexer. + + /// Get account data from either on-chain or compressed sources. + /// + /// Looks up by on-chain Solana pubkey. For cold accounts, searches by + /// onchain_pubkey stored in the compressed account data. + async fn get_account_interface( + &self, + address: &Pubkey, + config: Option, + ) -> Result>, RpcError>; + + /// Get token account data from either on-chain or compressed sources. + async fn get_token_account_interface( + &self, + address: &Pubkey, + config: Option, + ) -> Result>, RpcError>; + + /// Get ATA data from either on-chain or compressed sources. + async fn get_ata_interface( + &self, + owner: &Pubkey, + mint: &Pubkey, + config: Option, + ) -> Result>, RpcError>; + + /// Get mint data from either on-chain or compressed sources. + async fn get_mint_interface( + &self, + address: &Pubkey, + config: Option, + ) -> Result>, RpcError>; + + /// Get a token account interface by owner and mint. + async fn get_token_account_by_owner_mint( + &self, + owner: &Pubkey, + mint: &Pubkey, + config: Option, + ) -> Result>, RpcError>; + + /// Get multiple account interfaces in a batch. + async fn get_multiple_account_interfaces( + &self, + addresses: Vec<&Pubkey>, + config: Option, + ) -> Result>>, RpcError>; + + /// Fetch multiple accounts using `AccountToFetch` descriptors. + /// + /// Routes each account to the correct method based on its variant: + /// - `Pda` -> `get_account_interface` + /// - `Token` -> `get_token_account_interface` + /// - `Ata` -> `get_ata_interface` + /// - `Mint` -> `get_mint_interface` + async fn fetch_accounts( + &self, + accounts: &[AccountToFetch], + config: Option, + ) -> Result, RpcError> { + let mut results = Vec::with_capacity(accounts.len()); + for account in accounts { + let interface = match account { + AccountToFetch::Pda { address, .. } => self + .get_account_interface(address, config.clone()) + .await? + .value + .ok_or_else(|| { + RpcError::CustomError(format!("PDA account not found: {}", address)) + })?, + AccountToFetch::Token { address } => { + let tai = self + .get_token_account_interface(address, config.clone()) + .await? + .value + .ok_or_else(|| { + RpcError::CustomError(format!("Token account not found: {}", address)) + })?; + tai.into() + } + AccountToFetch::TokenByOwnerMint { owner, mint } => { + let tai = self + .get_token_account_by_owner_mint(owner, mint, config.clone()) + .await? + .value + .ok_or_else(|| { + RpcError::CustomError(format!( + "Token account not found for owner {} mint {}", + owner, mint + )) + })?; + tai.into() + } + AccountToFetch::Ata { wallet_owner, mint } => { + let tai = self + .get_ata_interface(wallet_owner, mint, config.clone()) + .await? + .value + .ok_or_else(|| { + RpcError::CustomError(format!( + "ATA not found for owner {} mint {}", + wallet_owner, mint + )) + })?; + tai.into() + } + AccountToFetch::Mint { address } => { + let mi = self + .get_mint_interface(address, config.clone()) + .await? + .value + .ok_or_else(|| { + RpcError::CustomError(format!("Mint not found: {}", address)) + })?; + mi.into() + } + }; + results.push(interface); + } + Ok(results) + } } diff --git a/sdk-libs/photon-api/src/apis/default_api.rs b/sdk-libs/photon-api/src/apis/default_api.rs index d0dd52fa51..797e4a3473 100644 --- a/sdk-libs/photon-api/src/apis/default_api.rs +++ b/sdk-libs/photon-api/src/apis/default_api.rs @@ -349,6 +349,69 @@ pub enum GetValidityProofV2PostError { UnknownValue(serde_json::Value), } +/// struct for typed errors of method [`get_compressed_mint_post`] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum GetCompressedMintPostError { + Status429(models::GetBatchAddressUpdateInfoPost429Response), + Status500(models::GetBatchAddressUpdateInfoPost429Response), + UnknownValue(serde_json::Value), +} + +/// struct for typed errors of method [`get_compressed_mints_by_authority_post`] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum GetCompressedMintsByAuthorityPostError { + Status429(models::GetBatchAddressUpdateInfoPost429Response), + Status500(models::GetBatchAddressUpdateInfoPost429Response), + UnknownValue(serde_json::Value), +} + +/// struct for typed errors of method [`get_account_interface_post`] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum GetAccountInterfacePostError { + Status429(models::GetBatchAddressUpdateInfoPost429Response), + Status500(models::GetBatchAddressUpdateInfoPost429Response), + UnknownValue(serde_json::Value), +} + +/// struct for typed errors of method [`get_token_account_interface_post`] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum GetTokenAccountInterfacePostError { + Status429(models::GetBatchAddressUpdateInfoPost429Response), + Status500(models::GetBatchAddressUpdateInfoPost429Response), + UnknownValue(serde_json::Value), +} + +/// struct for typed errors of method [`get_ata_interface_post`] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum GetAtaInterfacePostError { + Status429(models::GetBatchAddressUpdateInfoPost429Response), + Status500(models::GetBatchAddressUpdateInfoPost429Response), + UnknownValue(serde_json::Value), +} + +/// struct for typed errors of method [`get_mint_interface_post`] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum GetMintInterfacePostError { + Status429(models::GetBatchAddressUpdateInfoPost429Response), + Status500(models::GetBatchAddressUpdateInfoPost429Response), + UnknownValue(serde_json::Value), +} + +/// struct for typed errors of method [`get_multiple_account_interfaces_post`] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum GetMultipleAccountInterfacesPostError { + Status429(models::GetBatchAddressUpdateInfoPost429Response), + Status500(models::GetBatchAddressUpdateInfoPost429Response), + UnknownValue(serde_json::Value), +} + pub async fn get_batch_address_update_info_post( configuration: &configuration::Configuration, get_batch_address_update_info_post_request: models::GetBatchAddressUpdateInfoPostRequest, @@ -1997,6 +2060,297 @@ pub async fn get_validity_proof_v2_post( } } +pub async fn get_compressed_mint_post( + configuration: &configuration::Configuration, + get_compressed_mint_post_request: models::GetCompressedMintPostRequest, +) -> Result> { + let local_var_configuration = configuration; + + let local_var_client = &local_var_configuration.client; + + let local_var_uri_str = format!("{}/getCompressedMint", local_var_configuration.base_path); + let local_var_uri_str = append_api_key(local_var_configuration, &local_var_uri_str); + let mut local_var_req_builder = + local_var_client.request(reqwest::Method::POST, local_var_uri_str.as_str()); + + if let Some(ref local_var_user_agent) = local_var_configuration.user_agent { + local_var_req_builder = + local_var_req_builder.header(reqwest::header::USER_AGENT, local_var_user_agent.clone()); + } + local_var_req_builder = local_var_req_builder.json(&get_compressed_mint_post_request); + + let local_var_req = local_var_req_builder.build()?; + let local_var_resp = local_var_client.execute(local_var_req).await?; + + let local_var_status = local_var_resp.status(); + let local_var_content = local_var_resp.text().await?; + + if !local_var_status.is_client_error() && !local_var_status.is_server_error() { + serde_json::from_str(&local_var_content).map_err(Error::from) + } else { + let local_var_entity: Option = + serde_json::from_str(&local_var_content).ok(); + let local_var_error = ResponseContent { + status: local_var_status, + content: local_var_content, + entity: local_var_entity, + }; + Err(Error::ResponseError(local_var_error)) + } +} + +pub async fn get_compressed_mints_by_authority_post( + configuration: &configuration::Configuration, + get_compressed_mints_by_authority_post_request: models::GetCompressedMintsByAuthorityPostRequest, +) -> Result< + models::GetCompressedMintsByAuthorityPost200Response, + Error, +> { + let local_var_configuration = configuration; + + let local_var_client = &local_var_configuration.client; + + let local_var_uri_str = format!( + "{}/getCompressedMintsByAuthority", + local_var_configuration.base_path + ); + let local_var_uri_str = append_api_key(local_var_configuration, &local_var_uri_str); + let mut local_var_req_builder = + local_var_client.request(reqwest::Method::POST, local_var_uri_str.as_str()); + + if let Some(ref local_var_user_agent) = local_var_configuration.user_agent { + local_var_req_builder = + local_var_req_builder.header(reqwest::header::USER_AGENT, local_var_user_agent.clone()); + } + local_var_req_builder = + local_var_req_builder.json(&get_compressed_mints_by_authority_post_request); + + let local_var_req = local_var_req_builder.build()?; + let local_var_resp = local_var_client.execute(local_var_req).await?; + + let local_var_status = local_var_resp.status(); + let local_var_content = local_var_resp.text().await?; + + if !local_var_status.is_client_error() && !local_var_status.is_server_error() { + serde_json::from_str(&local_var_content).map_err(Error::from) + } else { + let local_var_entity: Option = + serde_json::from_str(&local_var_content).ok(); + let local_var_error = ResponseContent { + status: local_var_status, + content: local_var_content, + entity: local_var_entity, + }; + Err(Error::ResponseError(local_var_error)) + } +} + +pub async fn get_account_interface_post( + configuration: &configuration::Configuration, + get_account_interface_post_request: models::GetAccountInterfacePostRequest, +) -> Result> { + let local_var_configuration = configuration; + + let local_var_client = &local_var_configuration.client; + + let local_var_uri_str = format!("{}/getAccountInterface", local_var_configuration.base_path); + let local_var_uri_str = append_api_key(local_var_configuration, &local_var_uri_str); + let mut local_var_req_builder = + local_var_client.request(reqwest::Method::POST, local_var_uri_str.as_str()); + + if let Some(ref local_var_user_agent) = local_var_configuration.user_agent { + local_var_req_builder = + local_var_req_builder.header(reqwest::header::USER_AGENT, local_var_user_agent.clone()); + } + local_var_req_builder = local_var_req_builder.json(&get_account_interface_post_request); + + let local_var_req = local_var_req_builder.build()?; + let local_var_resp = local_var_client.execute(local_var_req).await?; + + let local_var_status = local_var_resp.status(); + let local_var_content = local_var_resp.text().await?; + + if !local_var_status.is_client_error() && !local_var_status.is_server_error() { + serde_json::from_str(&local_var_content).map_err(Error::from) + } else { + let local_var_entity: Option = + serde_json::from_str(&local_var_content).ok(); + let local_var_error = ResponseContent { + status: local_var_status, + content: local_var_content, + entity: local_var_entity, + }; + Err(Error::ResponseError(local_var_error)) + } +} + +pub async fn get_token_account_interface_post( + configuration: &configuration::Configuration, + get_token_account_interface_post_request: models::GetTokenAccountInterfacePostRequest, +) -> Result> +{ + let local_var_configuration = configuration; + + let local_var_client = &local_var_configuration.client; + + let local_var_uri_str = format!( + "{}/getTokenAccountInterface", + local_var_configuration.base_path + ); + let local_var_uri_str = append_api_key(local_var_configuration, &local_var_uri_str); + let mut local_var_req_builder = + local_var_client.request(reqwest::Method::POST, local_var_uri_str.as_str()); + + if let Some(ref local_var_user_agent) = local_var_configuration.user_agent { + local_var_req_builder = + local_var_req_builder.header(reqwest::header::USER_AGENT, local_var_user_agent.clone()); + } + local_var_req_builder = local_var_req_builder.json(&get_token_account_interface_post_request); + + let local_var_req = local_var_req_builder.build()?; + let local_var_resp = local_var_client.execute(local_var_req).await?; + + let local_var_status = local_var_resp.status(); + let local_var_content = local_var_resp.text().await?; + + if !local_var_status.is_client_error() && !local_var_status.is_server_error() { + serde_json::from_str(&local_var_content).map_err(Error::from) + } else { + let local_var_entity: Option = + serde_json::from_str(&local_var_content).ok(); + let local_var_error = ResponseContent { + status: local_var_status, + content: local_var_content, + entity: local_var_entity, + }; + Err(Error::ResponseError(local_var_error)) + } +} + +pub async fn get_ata_interface_post( + configuration: &configuration::Configuration, + get_ata_interface_post_request: models::GetAtaInterfacePostRequest, +) -> Result> { + let local_var_configuration = configuration; + + let local_var_client = &local_var_configuration.client; + + let local_var_uri_str = format!("{}/getAtaInterface", local_var_configuration.base_path); + let local_var_uri_str = append_api_key(local_var_configuration, &local_var_uri_str); + let mut local_var_req_builder = + local_var_client.request(reqwest::Method::POST, local_var_uri_str.as_str()); + + if let Some(ref local_var_user_agent) = local_var_configuration.user_agent { + local_var_req_builder = + local_var_req_builder.header(reqwest::header::USER_AGENT, local_var_user_agent.clone()); + } + local_var_req_builder = local_var_req_builder.json(&get_ata_interface_post_request); + + let local_var_req = local_var_req_builder.build()?; + let local_var_resp = local_var_client.execute(local_var_req).await?; + + let local_var_status = local_var_resp.status(); + let local_var_content = local_var_resp.text().await?; + + if !local_var_status.is_client_error() && !local_var_status.is_server_error() { + serde_json::from_str(&local_var_content).map_err(Error::from) + } else { + let local_var_entity: Option = + serde_json::from_str(&local_var_content).ok(); + let local_var_error = ResponseContent { + status: local_var_status, + content: local_var_content, + entity: local_var_entity, + }; + Err(Error::ResponseError(local_var_error)) + } +} + +pub async fn get_mint_interface_post( + configuration: &configuration::Configuration, + get_mint_interface_post_request: models::GetMintInterfacePostRequest, +) -> Result> { + let local_var_configuration = configuration; + + let local_var_client = &local_var_configuration.client; + + let local_var_uri_str = format!("{}/getMintInterface", local_var_configuration.base_path); + let local_var_uri_str = append_api_key(local_var_configuration, &local_var_uri_str); + let mut local_var_req_builder = + local_var_client.request(reqwest::Method::POST, local_var_uri_str.as_str()); + + if let Some(ref local_var_user_agent) = local_var_configuration.user_agent { + local_var_req_builder = + local_var_req_builder.header(reqwest::header::USER_AGENT, local_var_user_agent.clone()); + } + local_var_req_builder = local_var_req_builder.json(&get_mint_interface_post_request); + + let local_var_req = local_var_req_builder.build()?; + let local_var_resp = local_var_client.execute(local_var_req).await?; + + let local_var_status = local_var_resp.status(); + let local_var_content = local_var_resp.text().await?; + + if !local_var_status.is_client_error() && !local_var_status.is_server_error() { + serde_json::from_str(&local_var_content).map_err(Error::from) + } else { + let local_var_entity: Option = + serde_json::from_str(&local_var_content).ok(); + let local_var_error = ResponseContent { + status: local_var_status, + content: local_var_content, + entity: local_var_entity, + }; + Err(Error::ResponseError(local_var_error)) + } +} + +pub async fn get_multiple_account_interfaces_post( + configuration: &configuration::Configuration, + get_multiple_account_interfaces_post_request: models::GetMultipleAccountInterfacesPostRequest, +) -> Result< + models::GetMultipleAccountInterfacesPost200Response, + Error, +> { + let local_var_configuration = configuration; + + let local_var_client = &local_var_configuration.client; + + let local_var_uri_str = format!( + "{}/getMultipleAccountInterfaces", + local_var_configuration.base_path + ); + let local_var_uri_str = append_api_key(local_var_configuration, &local_var_uri_str); + let mut local_var_req_builder = + local_var_client.request(reqwest::Method::POST, local_var_uri_str.as_str()); + + if let Some(ref local_var_user_agent) = local_var_configuration.user_agent { + local_var_req_builder = + local_var_req_builder.header(reqwest::header::USER_AGENT, local_var_user_agent.clone()); + } + local_var_req_builder = + local_var_req_builder.json(&get_multiple_account_interfaces_post_request); + + let local_var_req = local_var_req_builder.build()?; + let local_var_resp = local_var_client.execute(local_var_req).await?; + + let local_var_status = local_var_resp.status(); + let local_var_content = local_var_resp.text().await?; + + if !local_var_status.is_client_error() && !local_var_status.is_server_error() { + serde_json::from_str(&local_var_content).map_err(Error::from) + } else { + let local_var_entity: Option = + serde_json::from_str(&local_var_content).ok(); + let local_var_error = ResponseContent { + status: local_var_status, + content: local_var_content, + entity: local_var_entity, + }; + Err(Error::ResponseError(local_var_error)) + } +} + fn append_api_key(configuration: &Configuration, uri_str: &str) -> String { let mut uri_str = uri_str.to_string(); if let Some(ref api_key) = configuration.api_key { diff --git a/sdk-libs/photon-api/src/models/_get_account_interface_post_200_response.rs b/sdk-libs/photon-api/src/models/_get_account_interface_post_200_response.rs new file mode 100644 index 0000000000..1dc4692b4f --- /dev/null +++ b/sdk-libs/photon-api/src/models/_get_account_interface_post_200_response.rs @@ -0,0 +1,35 @@ +/* + * photon-indexer + * + * Solana indexer for general compression + * + * The version of the OpenAPI document: 0.50.0 + * + */ + +use crate::models; + +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct GetAccountInterfacePost200Response { + #[serde(rename = "error", skip_serializing_if = "Option::is_none")] + pub error: Option>, + /// An ID to identify the response. + #[serde(rename = "id")] + pub id: String, + /// The version of the JSON-RPC protocol. + #[serde(rename = "jsonrpc")] + pub jsonrpc: String, + #[serde(rename = "result", skip_serializing_if = "Option::is_none")] + pub result: Option>, +} + +impl GetAccountInterfacePost200Response { + pub fn new(id: String, jsonrpc: String) -> Self { + Self { + error: None, + id, + jsonrpc, + result: None, + } + } +} diff --git a/sdk-libs/photon-api/src/models/_get_account_interface_post_200_response_result.rs b/sdk-libs/photon-api/src/models/_get_account_interface_post_200_response_result.rs new file mode 100644 index 0000000000..e59fae1529 --- /dev/null +++ b/sdk-libs/photon-api/src/models/_get_account_interface_post_200_response_result.rs @@ -0,0 +1,27 @@ +/* + * photon-indexer + * + * Solana indexer for general compression + * + * The version of the OpenAPI document: 0.50.0 + * + */ + +use crate::models; + +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct GetAccountInterfacePost200ResponseResult { + #[serde(rename = "context")] + pub context: Box, + #[serde(rename = "value", skip_serializing_if = "Option::is_none")] + pub value: Option>, +} + +impl GetAccountInterfacePost200ResponseResult { + pub fn new(context: models::Context) -> Self { + Self { + context: Box::new(context), + value: None, + } + } +} diff --git a/sdk-libs/photon-api/src/models/_get_account_interface_post_request.rs b/sdk-libs/photon-api/src/models/_get_account_interface_post_request.rs new file mode 100644 index 0000000000..dd4cc0e843 --- /dev/null +++ b/sdk-libs/photon-api/src/models/_get_account_interface_post_request.rs @@ -0,0 +1,36 @@ +/* + * photon-indexer + * + * Solana indexer for general compression + * + * The version of the OpenAPI document: 0.50.0 + * + */ + +use crate::models; + +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct GetAccountInterfacePostRequest { + /// An ID to identify the request. + #[serde(rename = "id")] + pub id: String, + /// The version of the JSON-RPC protocol. + #[serde(rename = "jsonrpc")] + pub jsonrpc: String, + /// The name of the method to invoke. + #[serde(rename = "method")] + pub method: String, + #[serde(rename = "params")] + pub params: Box, +} + +impl GetAccountInterfacePostRequest { + pub fn new(params: models::GetAccountInterfacePostRequestParams) -> Self { + Self { + id: "test-id".to_string(), + jsonrpc: "2.0".to_string(), + method: "getAccountInterface".to_string(), + params: Box::new(params), + } + } +} diff --git a/sdk-libs/photon-api/src/models/_get_account_interface_post_request_params.rs b/sdk-libs/photon-api/src/models/_get_account_interface_post_request_params.rs new file mode 100644 index 0000000000..d3b6238808 --- /dev/null +++ b/sdk-libs/photon-api/src/models/_get_account_interface_post_request_params.rs @@ -0,0 +1,22 @@ +/* + * photon-indexer + * + * Solana indexer for general compression + * + * The version of the OpenAPI document: 0.50.0 + * + */ + +/// GetAccountInterfacePostRequestParams : Request parameters for getAccountInterface +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct GetAccountInterfacePostRequestParams { + /// Account address to look up + #[serde(rename = "address")] + pub address: String, +} + +impl GetAccountInterfacePostRequestParams { + pub fn new(address: String) -> Self { + Self { address } + } +} diff --git a/sdk-libs/photon-api/src/models/_get_ata_interface_post_200_response.rs b/sdk-libs/photon-api/src/models/_get_ata_interface_post_200_response.rs new file mode 100644 index 0000000000..5c7709912a --- /dev/null +++ b/sdk-libs/photon-api/src/models/_get_ata_interface_post_200_response.rs @@ -0,0 +1,35 @@ +/* + * photon-indexer + * + * Solana indexer for general compression + * + * The version of the OpenAPI document: 0.50.0 + * + */ + +use crate::models; + +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct GetAtaInterfacePost200Response { + #[serde(rename = "error", skip_serializing_if = "Option::is_none")] + pub error: Option>, + /// An ID to identify the response. + #[serde(rename = "id")] + pub id: String, + /// The version of the JSON-RPC protocol. + #[serde(rename = "jsonrpc")] + pub jsonrpc: String, + #[serde(rename = "result", skip_serializing_if = "Option::is_none")] + pub result: Option>, +} + +impl GetAtaInterfacePost200Response { + pub fn new(id: String, jsonrpc: String) -> Self { + Self { + error: None, + id, + jsonrpc, + result: None, + } + } +} diff --git a/sdk-libs/photon-api/src/models/_get_ata_interface_post_200_response_result.rs b/sdk-libs/photon-api/src/models/_get_ata_interface_post_200_response_result.rs new file mode 100644 index 0000000000..f477f54d09 --- /dev/null +++ b/sdk-libs/photon-api/src/models/_get_ata_interface_post_200_response_result.rs @@ -0,0 +1,27 @@ +/* + * photon-indexer + * + * Solana indexer for general compression + * + * The version of the OpenAPI document: 0.50.0 + * + */ + +use crate::models; + +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct GetAtaInterfacePost200ResponseResult { + #[serde(rename = "context")] + pub context: Box, + #[serde(rename = "value", skip_serializing_if = "Option::is_none")] + pub value: Option>, +} + +impl GetAtaInterfacePost200ResponseResult { + pub fn new(context: models::Context) -> Self { + Self { + context: Box::new(context), + value: None, + } + } +} diff --git a/sdk-libs/photon-api/src/models/_get_ata_interface_post_request.rs b/sdk-libs/photon-api/src/models/_get_ata_interface_post_request.rs new file mode 100644 index 0000000000..81c36f3981 --- /dev/null +++ b/sdk-libs/photon-api/src/models/_get_ata_interface_post_request.rs @@ -0,0 +1,36 @@ +/* + * photon-indexer + * + * Solana indexer for general compression + * + * The version of the OpenAPI document: 0.50.0 + * + */ + +use crate::models; + +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct GetAtaInterfacePostRequest { + /// An ID to identify the request. + #[serde(rename = "id")] + pub id: String, + /// The version of the JSON-RPC protocol. + #[serde(rename = "jsonrpc")] + pub jsonrpc: String, + /// The name of the method to invoke. + #[serde(rename = "method")] + pub method: String, + #[serde(rename = "params")] + pub params: Box, +} + +impl GetAtaInterfacePostRequest { + pub fn new(params: models::GetAtaInterfacePostRequestParams) -> Self { + Self { + id: "test-id".to_string(), + jsonrpc: "2.0".to_string(), + method: "getAtaInterface".to_string(), + params: Box::new(params), + } + } +} diff --git a/sdk-libs/photon-api/src/models/_get_ata_interface_post_request_params.rs b/sdk-libs/photon-api/src/models/_get_ata_interface_post_request_params.rs new file mode 100644 index 0000000000..ea336353c5 --- /dev/null +++ b/sdk-libs/photon-api/src/models/_get_ata_interface_post_request_params.rs @@ -0,0 +1,25 @@ +/* + * photon-indexer + * + * Solana indexer for general compression + * + * The version of the OpenAPI document: 0.50.0 + * + */ + +/// GetAtaInterfacePostRequestParams : Request parameters for getAtaInterface +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct GetAtaInterfacePostRequestParams { + /// Owner address + #[serde(rename = "owner")] + pub owner: String, + /// Mint address + #[serde(rename = "mint")] + pub mint: String, +} + +impl GetAtaInterfacePostRequestParams { + pub fn new(owner: String, mint: String) -> Self { + Self { owner, mint } + } +} diff --git a/sdk-libs/photon-api/src/models/_get_compressed_mint_post_200_response.rs b/sdk-libs/photon-api/src/models/_get_compressed_mint_post_200_response.rs new file mode 100644 index 0000000000..d21727e4c4 --- /dev/null +++ b/sdk-libs/photon-api/src/models/_get_compressed_mint_post_200_response.rs @@ -0,0 +1,62 @@ +/* + * photon-indexer + * + * Solana indexer for general compression + * + * The version of the OpenAPI document: 0.50.0 + * + * Generated by: https://openapi-generator.tech + */ + +use crate::models; + +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct GetCompressedMintPost200Response { + #[serde(rename = "error", skip_serializing_if = "Option::is_none")] + pub error: Option>, + /// An ID to identify the response. + #[serde(rename = "id")] + pub id: Id, + /// The version of the JSON-RPC protocol. + #[serde(rename = "jsonrpc")] + pub jsonrpc: Jsonrpc, + #[serde(rename = "result", skip_serializing_if = "Option::is_none")] + pub result: Option>, +} + +impl GetCompressedMintPost200Response { + pub fn new(id: Id, jsonrpc: Jsonrpc) -> GetCompressedMintPost200Response { + GetCompressedMintPost200Response { + error: None, + id, + jsonrpc, + result: None, + } + } +} + +/// An ID to identify the response. +#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)] +pub enum Id { + #[serde(rename = "test-account")] + TestAccount, +} + +impl Default for Id { + fn default() -> Id { + Self::TestAccount + } +} + +/// The version of the JSON-RPC protocol. +#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)] +pub enum Jsonrpc { + #[serde(rename = "2.0")] + Variant2Period0, +} + +impl Default for Jsonrpc { + fn default() -> Jsonrpc { + Self::Variant2Period0 + } +} diff --git a/sdk-libs/photon-api/src/models/_get_compressed_mint_post_200_response_result.rs b/sdk-libs/photon-api/src/models/_get_compressed_mint_post_200_response_result.rs new file mode 100644 index 0000000000..d7a36cfe23 --- /dev/null +++ b/sdk-libs/photon-api/src/models/_get_compressed_mint_post_200_response_result.rs @@ -0,0 +1,28 @@ +/* + * photon-indexer + * + * Solana indexer for general compression + * + * The version of the OpenAPI document: 0.50.0 + * + * Generated by: https://openapi-generator.tech + */ + +use crate::models; + +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct GetCompressedMintPost200ResponseResult { + #[serde(rename = "context")] + pub context: Box, + #[serde(rename = "value", skip_serializing_if = "Option::is_none")] + pub value: Option>, +} + +impl GetCompressedMintPost200ResponseResult { + pub fn new(context: models::Context) -> GetCompressedMintPost200ResponseResult { + GetCompressedMintPost200ResponseResult { + context: Box::new(context), + value: None, + } + } +} diff --git a/sdk-libs/photon-api/src/models/_get_compressed_mint_post_request.rs b/sdk-libs/photon-api/src/models/_get_compressed_mint_post_request.rs new file mode 100644 index 0000000000..e51b7eb533 --- /dev/null +++ b/sdk-libs/photon-api/src/models/_get_compressed_mint_post_request.rs @@ -0,0 +1,76 @@ +/* + * photon-indexer + * + * Solana indexer for general compression + * + * The version of the OpenAPI document: 0.50.0 + * + * Generated by: https://openapi-generator.tech + */ + +use crate::models; + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct GetCompressedMintPostRequest { + /// An ID to identify the request. + #[serde(rename = "id")] + pub id: Id, + /// The version of the JSON-RPC protocol. + #[serde(rename = "jsonrpc")] + pub jsonrpc: Jsonrpc, + /// The name of the method to invoke. + #[serde(rename = "method")] + pub method: Method, + #[serde(rename = "params")] + pub params: Box, +} + +impl GetCompressedMintPostRequest { + pub fn new(params: models::GetCompressedMintPostRequestParams) -> GetCompressedMintPostRequest { + GetCompressedMintPostRequest { + id: Id::default(), + jsonrpc: Jsonrpc::default(), + method: Method::default(), + params: Box::new(params), + } + } +} + +/// An ID to identify the request. +#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)] +pub enum Id { + #[serde(rename = "test-account")] + TestAccount, +} + +impl Default for Id { + fn default() -> Id { + Self::TestAccount + } +} + +/// The version of the JSON-RPC protocol. +#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)] +pub enum Jsonrpc { + #[serde(rename = "2.0")] + Variant2Period0, +} + +impl Default for Jsonrpc { + fn default() -> Jsonrpc { + Self::Variant2Period0 + } +} + +/// The name of the method to invoke. +#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)] +pub enum Method { + #[serde(rename = "getCompressedMint")] + GetCompressedMint, +} + +impl Default for Method { + fn default() -> Method { + Self::GetCompressedMint + } +} diff --git a/sdk-libs/photon-api/src/models/_get_compressed_mint_post_request_params.rs b/sdk-libs/photon-api/src/models/_get_compressed_mint_post_request_params.rs new file mode 100644 index 0000000000..b040026fb2 --- /dev/null +++ b/sdk-libs/photon-api/src/models/_get_compressed_mint_post_request_params.rs @@ -0,0 +1,39 @@ +/* + * photon-indexer + * + * Solana indexer for general compression + * + * The version of the OpenAPI document: 0.50.0 + * + * Generated by: https://openapi-generator.tech + */ + +/// GetCompressedMintPostRequestParams : Request for compressed mint data. +/// Exactly one of `address` or `mint_pda` must be set. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct GetCompressedMintPostRequestParams { + /// A Solana public key represented as a base58 string (compressed address). + #[serde(rename = "address", default, skip_serializing_if = "Option::is_none")] + pub address: Option, + /// A Solana public key represented as a base58 string (mint PDA). + #[serde(rename = "mintPda", default, skip_serializing_if = "Option::is_none")] + pub mint_pda: Option, +} + +impl GetCompressedMintPostRequestParams { + /// Create params to query by compressed address. + pub fn with_address(address: String) -> Self { + Self { + address: Some(address), + mint_pda: None, + } + } + + /// Create params to query by mint PDA. + pub fn with_mint_pda(mint_pda: String) -> Self { + Self { + address: None, + mint_pda: Some(mint_pda), + } + } +} diff --git a/sdk-libs/photon-api/src/models/_get_compressed_mints_by_authority_post_200_response.rs b/sdk-libs/photon-api/src/models/_get_compressed_mints_by_authority_post_200_response.rs new file mode 100644 index 0000000000..1e45fec6f8 --- /dev/null +++ b/sdk-libs/photon-api/src/models/_get_compressed_mints_by_authority_post_200_response.rs @@ -0,0 +1,62 @@ +/* + * photon-indexer + * + * Solana indexer for general compression + * + * The version of the OpenAPI document: 0.50.0 + * + * Generated by: https://openapi-generator.tech + */ + +use crate::models; + +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct GetCompressedMintsByAuthorityPost200Response { + #[serde(rename = "error", skip_serializing_if = "Option::is_none")] + pub error: Option>, + /// An ID to identify the response. + #[serde(rename = "id")] + pub id: Id, + /// The version of the JSON-RPC protocol. + #[serde(rename = "jsonrpc")] + pub jsonrpc: Jsonrpc, + #[serde(rename = "result", skip_serializing_if = "Option::is_none")] + pub result: Option>, +} + +impl GetCompressedMintsByAuthorityPost200Response { + pub fn new(id: Id, jsonrpc: Jsonrpc) -> GetCompressedMintsByAuthorityPost200Response { + GetCompressedMintsByAuthorityPost200Response { + error: None, + id, + jsonrpc, + result: None, + } + } +} + +/// An ID to identify the response. +#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)] +pub enum Id { + #[serde(rename = "test-account")] + TestAccount, +} + +impl Default for Id { + fn default() -> Id { + Self::TestAccount + } +} + +/// The version of the JSON-RPC protocol. +#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)] +pub enum Jsonrpc { + #[serde(rename = "2.0")] + Variant2Period0, +} + +impl Default for Jsonrpc { + fn default() -> Jsonrpc { + Self::Variant2Period0 + } +} diff --git a/sdk-libs/photon-api/src/models/_get_compressed_mints_by_authority_post_200_response_result.rs b/sdk-libs/photon-api/src/models/_get_compressed_mints_by_authority_post_200_response_result.rs new file mode 100644 index 0000000000..9414bb15e6 --- /dev/null +++ b/sdk-libs/photon-api/src/models/_get_compressed_mints_by_authority_post_200_response_result.rs @@ -0,0 +1,31 @@ +/* + * photon-indexer + * + * Solana indexer for general compression + * + * The version of the OpenAPI document: 0.50.0 + * + * Generated by: https://openapi-generator.tech + */ + +use crate::models; + +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct GetCompressedMintsByAuthorityPost200ResponseResult { + #[serde(rename = "context")] + pub context: Box, + #[serde(rename = "value")] + pub value: Box, +} + +impl GetCompressedMintsByAuthorityPost200ResponseResult { + pub fn new( + context: models::Context, + value: models::CompressedMintList, + ) -> GetCompressedMintsByAuthorityPost200ResponseResult { + GetCompressedMintsByAuthorityPost200ResponseResult { + context: Box::new(context), + value: Box::new(value), + } + } +} diff --git a/sdk-libs/photon-api/src/models/_get_compressed_mints_by_authority_post_request.rs b/sdk-libs/photon-api/src/models/_get_compressed_mints_by_authority_post_request.rs new file mode 100644 index 0000000000..b3eaecd939 --- /dev/null +++ b/sdk-libs/photon-api/src/models/_get_compressed_mints_by_authority_post_request.rs @@ -0,0 +1,78 @@ +/* + * photon-indexer + * + * Solana indexer for general compression + * + * The version of the OpenAPI document: 0.50.0 + * + * Generated by: https://openapi-generator.tech + */ + +use crate::models; + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct GetCompressedMintsByAuthorityPostRequest { + /// An ID to identify the request. + #[serde(rename = "id")] + pub id: Id, + /// The version of the JSON-RPC protocol. + #[serde(rename = "jsonrpc")] + pub jsonrpc: Jsonrpc, + /// The name of the method to invoke. + #[serde(rename = "method")] + pub method: Method, + #[serde(rename = "params")] + pub params: Box, +} + +impl GetCompressedMintsByAuthorityPostRequest { + pub fn new( + params: models::GetCompressedMintsByAuthorityPostRequestParams, + ) -> GetCompressedMintsByAuthorityPostRequest { + GetCompressedMintsByAuthorityPostRequest { + id: Id::default(), + jsonrpc: Jsonrpc::default(), + method: Method::default(), + params: Box::new(params), + } + } +} + +/// An ID to identify the request. +#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)] +pub enum Id { + #[serde(rename = "test-account")] + TestAccount, +} + +impl Default for Id { + fn default() -> Id { + Self::TestAccount + } +} + +/// The version of the JSON-RPC protocol. +#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)] +pub enum Jsonrpc { + #[serde(rename = "2.0")] + Variant2Period0, +} + +impl Default for Jsonrpc { + fn default() -> Jsonrpc { + Self::Variant2Period0 + } +} + +/// The name of the method to invoke. +#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)] +pub enum Method { + #[serde(rename = "getCompressedMintsByAuthority")] + GetCompressedMintsByAuthority, +} + +impl Default for Method { + fn default() -> Method { + Self::GetCompressedMintsByAuthority + } +} diff --git a/sdk-libs/photon-api/src/models/_get_compressed_mints_by_authority_post_request_params.rs b/sdk-libs/photon-api/src/models/_get_compressed_mints_by_authority_post_request_params.rs new file mode 100644 index 0000000000..ffd1ec0b5e --- /dev/null +++ b/sdk-libs/photon-api/src/models/_get_compressed_mints_by_authority_post_request_params.rs @@ -0,0 +1,56 @@ +/* + * photon-indexer + * + * Solana indexer for general compression + * + * The version of the OpenAPI document: 0.50.0 + * + * Generated by: https://openapi-generator.tech + */ + +/// GetCompressedMintsByAuthorityPostRequestParams : Request for compressed mints by authority +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct GetCompressedMintsByAuthorityPostRequestParams { + /// A Solana public key represented as a base58 string. + #[serde(rename = "authority")] + pub authority: String, + /// Type of authority to filter by: mintAuthority, freezeAuthority, or both. + #[serde(rename = "authorityType")] + pub authority_type: AuthorityType, + /// A base 58 encoded string. + #[serde(rename = "cursor", default, skip_serializing_if = "Option::is_none")] + pub cursor: Option, + #[serde(rename = "limit", default, skip_serializing_if = "Option::is_none")] + pub limit: Option, +} + +impl GetCompressedMintsByAuthorityPostRequestParams { + pub fn new( + authority: String, + authority_type: AuthorityType, + ) -> GetCompressedMintsByAuthorityPostRequestParams { + GetCompressedMintsByAuthorityPostRequestParams { + authority, + authority_type, + cursor: None, + limit: None, + } + } +} + +/// Type of authority to filter by. +#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)] +pub enum AuthorityType { + #[serde(rename = "mintAuthority")] + MintAuthority, + #[serde(rename = "freezeAuthority")] + FreezeAuthority, + #[serde(rename = "both")] + Both, +} + +impl Default for AuthorityType { + fn default() -> AuthorityType { + Self::Both + } +} diff --git a/sdk-libs/photon-api/src/models/_get_mint_interface_post_200_response.rs b/sdk-libs/photon-api/src/models/_get_mint_interface_post_200_response.rs new file mode 100644 index 0000000000..162277b4a0 --- /dev/null +++ b/sdk-libs/photon-api/src/models/_get_mint_interface_post_200_response.rs @@ -0,0 +1,35 @@ +/* + * photon-indexer + * + * Solana indexer for general compression + * + * The version of the OpenAPI document: 0.50.0 + * + */ + +use crate::models; + +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct GetMintInterfacePost200Response { + #[serde(rename = "error", skip_serializing_if = "Option::is_none")] + pub error: Option>, + /// An ID to identify the response. + #[serde(rename = "id")] + pub id: String, + /// The version of the JSON-RPC protocol. + #[serde(rename = "jsonrpc")] + pub jsonrpc: String, + #[serde(rename = "result", skip_serializing_if = "Option::is_none")] + pub result: Option>, +} + +impl GetMintInterfacePost200Response { + pub fn new(id: String, jsonrpc: String) -> Self { + Self { + error: None, + id, + jsonrpc, + result: None, + } + } +} diff --git a/sdk-libs/photon-api/src/models/_get_mint_interface_post_200_response_result.rs b/sdk-libs/photon-api/src/models/_get_mint_interface_post_200_response_result.rs new file mode 100644 index 0000000000..2aa3042ef1 --- /dev/null +++ b/sdk-libs/photon-api/src/models/_get_mint_interface_post_200_response_result.rs @@ -0,0 +1,27 @@ +/* + * photon-indexer + * + * Solana indexer for general compression + * + * The version of the OpenAPI document: 0.50.0 + * + */ + +use crate::models; + +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct GetMintInterfacePost200ResponseResult { + #[serde(rename = "context")] + pub context: Box, + #[serde(rename = "value", skip_serializing_if = "Option::is_none")] + pub value: Option>, +} + +impl GetMintInterfacePost200ResponseResult { + pub fn new(context: models::Context) -> Self { + Self { + context: Box::new(context), + value: None, + } + } +} diff --git a/sdk-libs/photon-api/src/models/_get_mint_interface_post_request.rs b/sdk-libs/photon-api/src/models/_get_mint_interface_post_request.rs new file mode 100644 index 0000000000..b0aedd8cd6 --- /dev/null +++ b/sdk-libs/photon-api/src/models/_get_mint_interface_post_request.rs @@ -0,0 +1,36 @@ +/* + * photon-indexer + * + * Solana indexer for general compression + * + * The version of the OpenAPI document: 0.50.0 + * + */ + +use crate::models; + +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct GetMintInterfacePostRequest { + /// An ID to identify the request. + #[serde(rename = "id")] + pub id: String, + /// The version of the JSON-RPC protocol. + #[serde(rename = "jsonrpc")] + pub jsonrpc: String, + /// The name of the method to invoke. + #[serde(rename = "method")] + pub method: String, + #[serde(rename = "params")] + pub params: Box, +} + +impl GetMintInterfacePostRequest { + pub fn new(params: models::GetMintInterfacePostRequestParams) -> Self { + Self { + id: "test-id".to_string(), + jsonrpc: "2.0".to_string(), + method: "getMintInterface".to_string(), + params: Box::new(params), + } + } +} diff --git a/sdk-libs/photon-api/src/models/_get_mint_interface_post_request_params.rs b/sdk-libs/photon-api/src/models/_get_mint_interface_post_request_params.rs new file mode 100644 index 0000000000..1e38fdbd4a --- /dev/null +++ b/sdk-libs/photon-api/src/models/_get_mint_interface_post_request_params.rs @@ -0,0 +1,22 @@ +/* + * photon-indexer + * + * Solana indexer for general compression + * + * The version of the OpenAPI document: 0.50.0 + * + */ + +/// GetMintInterfacePostRequestParams : Request parameters for getMintInterface +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct GetMintInterfacePostRequestParams { + /// Mint address to look up + #[serde(rename = "address")] + pub address: String, +} + +impl GetMintInterfacePostRequestParams { + pub fn new(address: String) -> Self { + Self { address } + } +} diff --git a/sdk-libs/photon-api/src/models/_get_multiple_account_interfaces_post_200_response.rs b/sdk-libs/photon-api/src/models/_get_multiple_account_interfaces_post_200_response.rs new file mode 100644 index 0000000000..d46d97e6e7 --- /dev/null +++ b/sdk-libs/photon-api/src/models/_get_multiple_account_interfaces_post_200_response.rs @@ -0,0 +1,35 @@ +/* + * photon-indexer + * + * Solana indexer for general compression + * + * The version of the OpenAPI document: 0.50.0 + * + */ + +use crate::models; + +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct GetMultipleAccountInterfacesPost200Response { + #[serde(rename = "error", skip_serializing_if = "Option::is_none")] + pub error: Option>, + /// An ID to identify the response. + #[serde(rename = "id")] + pub id: String, + /// The version of the JSON-RPC protocol. + #[serde(rename = "jsonrpc")] + pub jsonrpc: String, + #[serde(rename = "result", skip_serializing_if = "Option::is_none")] + pub result: Option>, +} + +impl GetMultipleAccountInterfacesPost200Response { + pub fn new(id: String, jsonrpc: String) -> Self { + Self { + error: None, + id, + jsonrpc, + result: None, + } + } +} diff --git a/sdk-libs/photon-api/src/models/_get_multiple_account_interfaces_post_200_response_result.rs b/sdk-libs/photon-api/src/models/_get_multiple_account_interfaces_post_200_response_result.rs new file mode 100644 index 0000000000..31085786eb --- /dev/null +++ b/sdk-libs/photon-api/src/models/_get_multiple_account_interfaces_post_200_response_result.rs @@ -0,0 +1,28 @@ +/* + * photon-indexer + * + * Solana indexer for general compression + * + * The version of the OpenAPI document: 0.50.0 + * + */ + +use crate::models; + +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct GetMultipleAccountInterfacesPost200ResponseResult { + #[serde(rename = "context")] + pub context: Box, + /// List of typed results (Some for found accounts, None for not found) + #[serde(rename = "value")] + pub value: Vec>, +} + +impl GetMultipleAccountInterfacesPost200ResponseResult { + pub fn new(context: models::Context, value: Vec>) -> Self { + Self { + context: Box::new(context), + value, + } + } +} diff --git a/sdk-libs/photon-api/src/models/_get_multiple_account_interfaces_post_request.rs b/sdk-libs/photon-api/src/models/_get_multiple_account_interfaces_post_request.rs new file mode 100644 index 0000000000..fb9308c13b --- /dev/null +++ b/sdk-libs/photon-api/src/models/_get_multiple_account_interfaces_post_request.rs @@ -0,0 +1,36 @@ +/* + * photon-indexer + * + * Solana indexer for general compression + * + * The version of the OpenAPI document: 0.50.0 + * + */ + +use crate::models; + +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct GetMultipleAccountInterfacesPostRequest { + /// An ID to identify the request. + #[serde(rename = "id")] + pub id: String, + /// The version of the JSON-RPC protocol. + #[serde(rename = "jsonrpc")] + pub jsonrpc: String, + /// The name of the method to invoke. + #[serde(rename = "method")] + pub method: String, + #[serde(rename = "params")] + pub params: Box, +} + +impl GetMultipleAccountInterfacesPostRequest { + pub fn new(params: models::GetMultipleAccountInterfacesPostRequestParams) -> Self { + Self { + id: "test-id".to_string(), + jsonrpc: "2.0".to_string(), + method: "getMultipleAccountInterfaces".to_string(), + params: Box::new(params), + } + } +} diff --git a/sdk-libs/photon-api/src/models/_get_multiple_account_interfaces_post_request_params.rs b/sdk-libs/photon-api/src/models/_get_multiple_account_interfaces_post_request_params.rs new file mode 100644 index 0000000000..c20d8b1e42 --- /dev/null +++ b/sdk-libs/photon-api/src/models/_get_multiple_account_interfaces_post_request_params.rs @@ -0,0 +1,22 @@ +/* + * photon-indexer + * + * Solana indexer for general compression + * + * The version of the OpenAPI document: 0.50.0 + * + */ + +/// GetMultipleAccountInterfacesPostRequestParams : Request parameters for getMultipleAccountInterfaces +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct GetMultipleAccountInterfacesPostRequestParams { + /// List of account addresses to look up (max 100) + #[serde(rename = "addresses")] + pub addresses: Vec, +} + +impl GetMultipleAccountInterfacesPostRequestParams { + pub fn new(addresses: Vec) -> Self { + Self { addresses } + } +} diff --git a/sdk-libs/photon-api/src/models/_get_token_account_interface_post_200_response.rs b/sdk-libs/photon-api/src/models/_get_token_account_interface_post_200_response.rs new file mode 100644 index 0000000000..7bd42eb7f7 --- /dev/null +++ b/sdk-libs/photon-api/src/models/_get_token_account_interface_post_200_response.rs @@ -0,0 +1,35 @@ +/* + * photon-indexer + * + * Solana indexer for general compression + * + * The version of the OpenAPI document: 0.50.0 + * + */ + +use crate::models; + +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct GetTokenAccountInterfacePost200Response { + #[serde(rename = "error", skip_serializing_if = "Option::is_none")] + pub error: Option>, + /// An ID to identify the response. + #[serde(rename = "id")] + pub id: String, + /// The version of the JSON-RPC protocol. + #[serde(rename = "jsonrpc")] + pub jsonrpc: String, + #[serde(rename = "result", skip_serializing_if = "Option::is_none")] + pub result: Option>, +} + +impl GetTokenAccountInterfacePost200Response { + pub fn new(id: String, jsonrpc: String) -> Self { + Self { + error: None, + id, + jsonrpc, + result: None, + } + } +} diff --git a/sdk-libs/photon-api/src/models/_get_token_account_interface_post_200_response_result.rs b/sdk-libs/photon-api/src/models/_get_token_account_interface_post_200_response_result.rs new file mode 100644 index 0000000000..dadd19b281 --- /dev/null +++ b/sdk-libs/photon-api/src/models/_get_token_account_interface_post_200_response_result.rs @@ -0,0 +1,27 @@ +/* + * photon-indexer + * + * Solana indexer for general compression + * + * The version of the OpenAPI document: 0.50.0 + * + */ + +use crate::models; + +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct GetTokenAccountInterfacePost200ResponseResult { + #[serde(rename = "context")] + pub context: Box, + #[serde(rename = "value", skip_serializing_if = "Option::is_none")] + pub value: Option>, +} + +impl GetTokenAccountInterfacePost200ResponseResult { + pub fn new(context: models::Context) -> Self { + Self { + context: Box::new(context), + value: None, + } + } +} diff --git a/sdk-libs/photon-api/src/models/_get_token_account_interface_post_request.rs b/sdk-libs/photon-api/src/models/_get_token_account_interface_post_request.rs new file mode 100644 index 0000000000..8255ac11f7 --- /dev/null +++ b/sdk-libs/photon-api/src/models/_get_token_account_interface_post_request.rs @@ -0,0 +1,36 @@ +/* + * photon-indexer + * + * Solana indexer for general compression + * + * The version of the OpenAPI document: 0.50.0 + * + */ + +use crate::models; + +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct GetTokenAccountInterfacePostRequest { + /// An ID to identify the request. + #[serde(rename = "id")] + pub id: String, + /// The version of the JSON-RPC protocol. + #[serde(rename = "jsonrpc")] + pub jsonrpc: String, + /// The name of the method to invoke. + #[serde(rename = "method")] + pub method: String, + #[serde(rename = "params")] + pub params: Box, +} + +impl GetTokenAccountInterfacePostRequest { + pub fn new(params: models::GetTokenAccountInterfacePostRequestParams) -> Self { + Self { + id: "test-id".to_string(), + jsonrpc: "2.0".to_string(), + method: "getTokenAccountInterface".to_string(), + params: Box::new(params), + } + } +} diff --git a/sdk-libs/photon-api/src/models/_get_token_account_interface_post_request_params.rs b/sdk-libs/photon-api/src/models/_get_token_account_interface_post_request_params.rs new file mode 100644 index 0000000000..60f583e6af --- /dev/null +++ b/sdk-libs/photon-api/src/models/_get_token_account_interface_post_request_params.rs @@ -0,0 +1,22 @@ +/* + * photon-indexer + * + * Solana indexer for general compression + * + * The version of the OpenAPI document: 0.50.0 + * + */ + +/// GetTokenAccountInterfacePostRequestParams : Request parameters for getTokenAccountInterface +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct GetTokenAccountInterfacePostRequestParams { + /// Token account address to look up + #[serde(rename = "address")] + pub address: String, +} + +impl GetTokenAccountInterfacePostRequestParams { + pub fn new(address: String) -> Self { + Self { address } + } +} diff --git a/sdk-libs/photon-api/src/models/account_interface.rs b/sdk-libs/photon-api/src/models/account_interface.rs new file mode 100644 index 0000000000..efae2f6498 --- /dev/null +++ b/sdk-libs/photon-api/src/models/account_interface.rs @@ -0,0 +1,35 @@ +/* + * photon-indexer + * + * Solana indexer for general compression + * + * The version of the OpenAPI document: 0.51.0 + * + */ + +use crate::models; + +/// AccountInterface : Unified account interface — works for both on-chain and compressed accounts +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AccountInterface { + /// The on-chain Solana pubkey + #[serde(rename = "key")] + pub key: String, + /// Standard Solana account fields + #[serde(rename = "account")] + pub account: models::SolanaAccountData, + /// Compressed context — null if on-chain, present if compressed + #[serde(rename = "cold", skip_serializing_if = "Option::is_none")] + pub cold: Option, +} + +impl AccountInterface { + pub fn new(key: String, account: models::SolanaAccountData) -> Self { + Self { + key, + account, + cold: None, + } + } +} diff --git a/sdk-libs/photon-api/src/models/cold_context.rs b/sdk-libs/photon-api/src/models/cold_context.rs new file mode 100644 index 0000000000..5b8c3faa38 --- /dev/null +++ b/sdk-libs/photon-api/src/models/cold_context.rs @@ -0,0 +1,46 @@ +/* + * photon-indexer + * + * Solana indexer for general compression + * + * The version of the OpenAPI document: 0.51.0 + * + */ + +use crate::models; + +/// ColdContext : Compressed account context — present when account is in compressed state +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(tag = "type", rename_all = "camelCase")] +pub enum ColdContext { + /// Generic compressed account + #[serde(rename = "account")] + Account { + hash: String, + #[serde(rename = "leafIndex")] + leaf_index: u64, + #[serde(rename = "treeInfo")] + tree_info: models::InterfaceTreeInfo, + data: models::ColdData, + }, + /// Compressed token account + #[serde(rename = "token")] + Token { + hash: String, + #[serde(rename = "leafIndex")] + leaf_index: u64, + #[serde(rename = "treeInfo")] + tree_info: models::InterfaceTreeInfo, + data: models::ColdData, + }, + /// Compressed mint account + #[serde(rename = "mint")] + Mint { + hash: String, + #[serde(rename = "leafIndex")] + leaf_index: u64, + #[serde(rename = "treeInfo")] + tree_info: models::InterfaceTreeInfo, + data: models::ColdData, + }, +} diff --git a/sdk-libs/photon-api/src/models/cold_data.rs b/sdk-libs/photon-api/src/models/cold_data.rs new file mode 100644 index 0000000000..318e7939f5 --- /dev/null +++ b/sdk-libs/photon-api/src/models/cold_data.rs @@ -0,0 +1,27 @@ +/* + * photon-indexer + * + * Solana indexer for general compression + * + * The version of the OpenAPI document: 0.51.0 + * + */ + +/// ColdData : Structured compressed account data (discriminator separated) +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ColdData { + /// First 8 bytes of the account data (discriminator) + pub discriminator: [u8; 8], + /// Remaining account data after discriminator, base64 encoded + pub data: String, +} + +impl ColdData { + pub fn new(discriminator: [u8; 8], data: String) -> Self { + Self { + discriminator, + data, + } + } +} diff --git a/sdk-libs/photon-api/src/models/compressed_context.rs b/sdk-libs/photon-api/src/models/compressed_context.rs new file mode 100644 index 0000000000..e664500503 --- /dev/null +++ b/sdk-libs/photon-api/src/models/compressed_context.rs @@ -0,0 +1,40 @@ +/* + * photon-indexer + * + * Solana indexer for general compression + * + * The version of the OpenAPI document: 0.50.0 + * + */ + +/// CompressedContext : Context information for compressed accounts +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct CompressedContext { + /// The hash of the compressed account (leaf hash in Merkle tree) + #[serde(rename = "hash")] + pub hash: String, + /// The Merkle tree address + #[serde(rename = "tree")] + pub tree: String, + /// The leaf index in the Merkle tree + #[serde(rename = "leafIndex")] + pub leaf_index: u64, + /// Sequence number (None if in output queue, Some once inserted into Merkle tree) + #[serde(rename = "seq", skip_serializing_if = "Option::is_none")] + pub seq: Option, + /// Whether the account can be proven by index (in output queue) + #[serde(rename = "proveByIndex")] + pub prove_by_index: bool, +} + +impl CompressedContext { + pub fn new(hash: String, tree: String, leaf_index: u64, prove_by_index: bool) -> Self { + Self { + hash, + tree, + leaf_index, + seq: None, + prove_by_index, + } + } +} diff --git a/sdk-libs/photon-api/src/models/compressed_mint.rs b/sdk-libs/photon-api/src/models/compressed_mint.rs new file mode 100644 index 0000000000..54552a62f1 --- /dev/null +++ b/sdk-libs/photon-api/src/models/compressed_mint.rs @@ -0,0 +1,28 @@ +/* + * photon-indexer + * + * Solana indexer for general compression + * + * The version of the OpenAPI document: 0.50.0 + * + * Generated by: https://openapi-generator.tech + */ + +use crate::models; + +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct CompressedMint { + #[serde(rename = "account")] + pub account: Box, + #[serde(rename = "mintData")] + pub mint_data: Box, +} + +impl CompressedMint { + pub fn new(account: models::AccountV2, mint_data: models::MintData) -> CompressedMint { + CompressedMint { + account: Box::new(account), + mint_data: Box::new(mint_data), + } + } +} diff --git a/sdk-libs/photon-api/src/models/compressed_mint_list.rs b/sdk-libs/photon-api/src/models/compressed_mint_list.rs new file mode 100644 index 0000000000..ffb9710d62 --- /dev/null +++ b/sdk-libs/photon-api/src/models/compressed_mint_list.rs @@ -0,0 +1,30 @@ +/* + * photon-indexer + * + * Solana indexer for general compression + * + * The version of the OpenAPI document: 0.50.0 + * + * Generated by: https://openapi-generator.tech + */ + +use serde::{Deserialize, Serialize}; + +use crate::models; + +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct CompressedMintList { + #[serde(rename = "items")] + pub items: Vec, + #[serde(rename = "cursor", skip_serializing_if = "Option::is_none")] + pub cursor: Option, +} + +impl CompressedMintList { + pub fn new(items: Vec) -> CompressedMintList { + CompressedMintList { + items, + cursor: None, + } + } +} diff --git a/sdk-libs/photon-api/src/models/interface_result.rs b/sdk-libs/photon-api/src/models/interface_result.rs new file mode 100644 index 0000000000..a861594207 --- /dev/null +++ b/sdk-libs/photon-api/src/models/interface_result.rs @@ -0,0 +1,31 @@ +/* + * photon-indexer + * + * Solana indexer for general compression + * + * The version of the OpenAPI document: 0.50.0 + * + */ + +use crate::models; + +/// InterfaceResult : Heterogeneous result type for batch lookups +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(tag = "type")] +pub enum InterfaceResult { + /// Generic account result + #[serde(rename = "account")] + Account(models::AccountInterface), + /// Token account result with parsed token data + #[serde(rename = "token")] + Token(models::TokenAccountInterface), + /// Mint account result with parsed mint data + #[serde(rename = "mint")] + Mint(models::MintInterface), +} + +impl Default for InterfaceResult { + fn default() -> Self { + Self::Account(models::AccountInterface::default()) + } +} diff --git a/sdk-libs/photon-api/src/models/mint_data.rs b/sdk-libs/photon-api/src/models/mint_data.rs new file mode 100644 index 0000000000..5c1029353f --- /dev/null +++ b/sdk-libs/photon-api/src/models/mint_data.rs @@ -0,0 +1,61 @@ +/* + * photon-indexer + * + * Solana indexer for general compression + * + * The version of the OpenAPI document: 0.50.0 + * + * Generated by: https://openapi-generator.tech + */ + +/// MintData : Compressed mint account data + +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct MintData { + /// A Solana public key represented as a base58 string. + #[serde(rename = "mintPda")] + pub mint_pda: String, + /// A Solana public key represented as a base58 string. + #[serde(rename = "mintSigner")] + pub mint_signer: String, + /// A Solana public key represented as a base58 string. + #[serde(rename = "mintAuthority", skip_serializing_if = "Option::is_none")] + pub mint_authority: Option, + /// A Solana public key represented as a base58 string. + #[serde(rename = "freezeAuthority", skip_serializing_if = "Option::is_none")] + pub freeze_authority: Option, + #[serde(rename = "supply")] + pub supply: u64, + #[serde(rename = "decimals")] + pub decimals: u8, + #[serde(rename = "version")] + pub version: u8, + #[serde(rename = "mintDecompressed")] + pub mint_decompressed: bool, + /// A base 64 encoded string. + #[serde(rename = "extensions", skip_serializing_if = "Option::is_none")] + pub extensions: Option, +} + +impl MintData { + pub fn new( + mint_pda: String, + mint_signer: String, + supply: u64, + decimals: u8, + version: u8, + mint_decompressed: bool, + ) -> MintData { + MintData { + mint_pda, + mint_signer, + mint_authority: None, + freeze_authority: None, + supply, + decimals, + version, + mint_decompressed, + extensions: None, + } + } +} diff --git a/sdk-libs/photon-api/src/models/mint_interface.rs b/sdk-libs/photon-api/src/models/mint_interface.rs new file mode 100644 index 0000000000..d842de2edd --- /dev/null +++ b/sdk-libs/photon-api/src/models/mint_interface.rs @@ -0,0 +1,27 @@ +/* + * photon-indexer + * + * Solana indexer for general compression + * + * The version of the OpenAPI document: 0.50.0 + * + */ + +use crate::models; + +/// MintInterface : Mint account interface with parsed mint data +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct MintInterface { + /// Base account interface data (flattened) + #[serde(flatten)] + pub account: models::AccountInterface, + /// Parsed mint data + #[serde(rename = "mintData")] + pub mint_data: models::MintData, +} + +impl MintInterface { + pub fn new(account: models::AccountInterface, mint_data: models::MintData) -> Self { + Self { account, mint_data } + } +} diff --git a/sdk-libs/photon-api/src/models/mod.rs b/sdk-libs/photon-api/src/models/mod.rs index 115861e99e..e65c2df490 100644 --- a/sdk-libs/photon-api/src/models/mod.rs +++ b/sdk-libs/photon-api/src/models/mod.rs @@ -342,3 +342,89 @@ pub mod node; pub use self::node::Node; pub mod queue_request; pub use self::queue_request::QueueRequest; +pub mod mint_data; +pub use self::mint_data::MintData; +pub mod compressed_mint; +pub use self::compressed_mint::CompressedMint; +pub mod compressed_mint_list; +pub use self::compressed_mint_list::CompressedMintList; +pub mod _get_compressed_mint_post_request_params; +pub use self::_get_compressed_mint_post_request_params::GetCompressedMintPostRequestParams; +pub mod _get_compressed_mint_post_request; +pub use self::_get_compressed_mint_post_request::GetCompressedMintPostRequest; +pub mod _get_compressed_mint_post_200_response_result; +pub use self::_get_compressed_mint_post_200_response_result::GetCompressedMintPost200ResponseResult; +pub mod _get_compressed_mint_post_200_response; +pub use self::_get_compressed_mint_post_200_response::GetCompressedMintPost200Response; +pub mod _get_compressed_mints_by_authority_post_request_params; +pub use self::_get_compressed_mints_by_authority_post_request_params::{ + AuthorityType, GetCompressedMintsByAuthorityPostRequestParams, +}; +pub mod _get_compressed_mints_by_authority_post_request; +pub use self::_get_compressed_mints_by_authority_post_request::GetCompressedMintsByAuthorityPostRequest; +pub mod _get_compressed_mints_by_authority_post_200_response_result; +pub use self::_get_compressed_mints_by_authority_post_200_response_result::GetCompressedMintsByAuthorityPost200ResponseResult; +pub mod _get_compressed_mints_by_authority_post_200_response; +pub use self::_get_compressed_mints_by_authority_post_200_response::GetCompressedMintsByAuthorityPost200Response; +// Interface types +pub mod solana_account_data; +pub use self::solana_account_data::SolanaAccountData; +pub mod cold_data; +pub use self::cold_data::ColdData; +pub mod tree_info; +pub use self::tree_info::TreeInfo as InterfaceTreeInfo; +pub mod cold_context; +pub use self::cold_context::ColdContext; +pub mod account_interface; +pub use self::account_interface::AccountInterface; +pub mod token_account_interface; +pub use self::token_account_interface::TokenAccountInterface; +pub mod mint_interface; +pub use self::mint_interface::MintInterface; +pub mod interface_result; +pub use self::interface_result::InterfaceResult; +// getAccountInterface +pub mod _get_account_interface_post_request_params; +pub use self::_get_account_interface_post_request_params::GetAccountInterfacePostRequestParams; +pub mod _get_account_interface_post_request; +pub use self::_get_account_interface_post_request::GetAccountInterfacePostRequest; +pub mod _get_account_interface_post_200_response_result; +pub use self::_get_account_interface_post_200_response_result::GetAccountInterfacePost200ResponseResult; +pub mod _get_account_interface_post_200_response; +pub use self::_get_account_interface_post_200_response::GetAccountInterfacePost200Response; +// getTokenAccountInterface +pub mod _get_token_account_interface_post_request_params; +pub use self::_get_token_account_interface_post_request_params::GetTokenAccountInterfacePostRequestParams; +pub mod _get_token_account_interface_post_request; +pub use self::_get_token_account_interface_post_request::GetTokenAccountInterfacePostRequest; +pub mod _get_token_account_interface_post_200_response_result; +pub use self::_get_token_account_interface_post_200_response_result::GetTokenAccountInterfacePost200ResponseResult; +pub mod _get_token_account_interface_post_200_response; +pub use self::_get_token_account_interface_post_200_response::GetTokenAccountInterfacePost200Response; +// getAtaInterface +pub mod _get_ata_interface_post_request_params; +pub use self::_get_ata_interface_post_request_params::GetAtaInterfacePostRequestParams; +pub mod _get_ata_interface_post_request; +pub use self::_get_ata_interface_post_request::GetAtaInterfacePostRequest; +pub mod _get_ata_interface_post_200_response_result; +pub use self::_get_ata_interface_post_200_response_result::GetAtaInterfacePost200ResponseResult; +pub mod _get_ata_interface_post_200_response; +pub use self::_get_ata_interface_post_200_response::GetAtaInterfacePost200Response; +// getMintInterface +pub mod _get_mint_interface_post_request_params; +pub use self::_get_mint_interface_post_request_params::GetMintInterfacePostRequestParams; +pub mod _get_mint_interface_post_request; +pub use self::_get_mint_interface_post_request::GetMintInterfacePostRequest; +pub mod _get_mint_interface_post_200_response_result; +pub use self::_get_mint_interface_post_200_response_result::GetMintInterfacePost200ResponseResult; +pub mod _get_mint_interface_post_200_response; +pub use self::_get_mint_interface_post_200_response::GetMintInterfacePost200Response; +// getMultipleAccountInterfaces +pub mod _get_multiple_account_interfaces_post_request_params; +pub use self::_get_multiple_account_interfaces_post_request_params::GetMultipleAccountInterfacesPostRequestParams; +pub mod _get_multiple_account_interfaces_post_request; +pub use self::_get_multiple_account_interfaces_post_request::GetMultipleAccountInterfacesPostRequest; +pub mod _get_multiple_account_interfaces_post_200_response_result; +pub use self::_get_multiple_account_interfaces_post_200_response_result::GetMultipleAccountInterfacesPost200ResponseResult; +pub mod _get_multiple_account_interfaces_post_200_response; +pub use self::_get_multiple_account_interfaces_post_200_response::GetMultipleAccountInterfacesPost200Response; diff --git a/sdk-libs/photon-api/src/models/resolved_from.rs b/sdk-libs/photon-api/src/models/resolved_from.rs new file mode 100644 index 0000000000..c79935af7e --- /dev/null +++ b/sdk-libs/photon-api/src/models/resolved_from.rs @@ -0,0 +1,23 @@ +/* + * photon-indexer + * + * Solana indexer for general compression + * + * The version of the OpenAPI document: 0.50.0 + * + */ + +/// ResolvedFrom : Indicates the source of the resolved account data +#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)] +pub enum ResolvedFrom { + #[serde(rename = "onchain")] + Onchain, + #[serde(rename = "compressed")] + Compressed, +} + +impl Default for ResolvedFrom { + fn default() -> Self { + Self::Onchain + } +} diff --git a/sdk-libs/photon-api/src/models/solana_account_data.rs b/sdk-libs/photon-api/src/models/solana_account_data.rs new file mode 100644 index 0000000000..5d4142b840 --- /dev/null +++ b/sdk-libs/photon-api/src/models/solana_account_data.rs @@ -0,0 +1,40 @@ +/* + * photon-indexer + * + * Solana indexer for general compression + * + * The version of the OpenAPI document: 0.51.0 + * + */ + +/// SolanaAccountData : Standard Solana account fields (matches getAccountInfo shape) +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SolanaAccountData { + pub lamports: u64, + pub data: String, + pub owner: String, + pub executable: bool, + pub rent_epoch: u64, + pub space: u64, +} + +impl SolanaAccountData { + pub fn new( + lamports: u64, + data: String, + owner: String, + executable: bool, + rent_epoch: u64, + space: u64, + ) -> Self { + Self { + lamports, + data, + owner, + executable, + rent_epoch, + space, + } + } +} diff --git a/sdk-libs/photon-api/src/models/token_account_interface.rs b/sdk-libs/photon-api/src/models/token_account_interface.rs new file mode 100644 index 0000000000..3e3080c100 --- /dev/null +++ b/sdk-libs/photon-api/src/models/token_account_interface.rs @@ -0,0 +1,30 @@ +/* + * photon-indexer + * + * Solana indexer for general compression + * + * The version of the OpenAPI document: 0.50.0 + * + */ + +use crate::models; + +/// TokenAccountInterface : Token account interface with parsed token data +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct TokenAccountInterface { + /// Base account interface data (flattened) + #[serde(flatten)] + pub account: models::AccountInterface, + /// Parsed token account data + #[serde(rename = "tokenData")] + pub token_data: models::TokenData, +} + +impl TokenAccountInterface { + pub fn new(account: models::AccountInterface, token_data: models::TokenData) -> Self { + Self { + account, + token_data, + } + } +} diff --git a/sdk-libs/photon-api/src/models/tree_info.rs b/sdk-libs/photon-api/src/models/tree_info.rs new file mode 100644 index 0000000000..31984566ae --- /dev/null +++ b/sdk-libs/photon-api/src/models/tree_info.rs @@ -0,0 +1,23 @@ +/* + * photon-indexer + * + * Solana indexer for general compression + * + * The version of the OpenAPI document: 0.51.0 + * + */ + +/// TreeInfo : Merkle tree info for compressed accounts +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TreeInfo { + pub tree: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub seq: Option, +} + +impl TreeInfo { + pub fn new(tree: String) -> Self { + Self { tree, seq: None } + } +} diff --git a/sdk-libs/program-test/src/indexer/test_indexer.rs b/sdk-libs/program-test/src/indexer/test_indexer.rs index a0691279b7..a4ca21729c 100644 --- a/sdk-libs/program-test/src/indexer/test_indexer.rs +++ b/sdk-libs/program-test/src/indexer/test_indexer.rs @@ -1,4 +1,4 @@ -use std::{fmt::Debug, time::Duration}; +use std::{collections::HashMap, fmt::Debug, time::Duration}; #[cfg(feature = "devenv")] use account_compression::{ @@ -13,6 +13,7 @@ pub(crate) const STATE_MERKLE_TREE_ROOTS: u64 = 2400; pub(crate) const DEFAULT_BATCH_STATE_TREE_HEIGHT: usize = 32; pub(crate) const DEFAULT_BATCH_ADDRESS_TREE_HEIGHT: usize = 40; pub(crate) const DEFAULT_BATCH_ROOT_HISTORY_LEN: usize = 200; + use async_trait::async_trait; use borsh::BorshDeserialize; #[cfg(feature = "devenv")] @@ -23,9 +24,10 @@ use light_client::{ fee::FeeConfig, indexer::{ AccountProofInputs, Address, AddressMerkleTreeAccounts, AddressProofInputs, - AddressWithTree, CompressedAccount, CompressedTokenAccount, Context, - GetCompressedAccountsByOwnerConfig, GetCompressedTokenAccountsByOwnerOrDelegateOptions, - Indexer, IndexerError, IndexerRpcConfig, Items, ItemsWithCursor, MerkleProof, + AddressWithTree, CompressedAccount, CompressedMint, CompressedTokenAccount, Context, + GetCompressedAccountsByOwnerConfig, GetCompressedMintsByAuthorityOptions, + GetCompressedTokenAccountsByOwnerOrDelegateOptions, Indexer, IndexerError, + IndexerRpcConfig, Items, ItemsWithCursor, MerkleProof, MintAuthorityType, NewAddressProofWithContext, OwnerBalance, PaginatedOptions, QueueElementsResult, QueueElementsV2Options, Response, RetryConfig, RootIndex, SignatureWithMetadata, StateMerkleTreeAccounts, TokenBalance, ValidityProofWithContext, @@ -38,6 +40,9 @@ use light_compressed_account::{ tx_hash::create_tx_hash, TreeType, }; +/// Discriminator for compressible accounts that store onchain_pubkey in the first 32 bytes of data. +/// Re-exported from light_compressible for convenience. +pub use light_compressible::DECOMPRESSED_PDA_DISCRIMINATOR; use light_event::event::PublicTransactionEvent; use light_hasher::{bigint::bigint_to_be_bytes_array, Poseidon}; use light_merkle_tree_reference::MerkleTree; @@ -102,6 +107,8 @@ pub struct TestIndexer { pub token_compressed_accounts: Vec, pub token_nullified_compressed_accounts: Vec, pub events: Vec, + /// Index mapping onchain_pubkey to compressed account index. + pub onchain_pubkey_index: HashMap<[u8; 32], usize>, } impl Clone for TestIndexer { @@ -116,6 +123,7 @@ impl Clone for TestIndexer { token_compressed_accounts: self.token_compressed_accounts.clone(), token_nullified_compressed_accounts: self.token_nullified_compressed_accounts.clone(), events: self.events.clone(), + onchain_pubkey_index: self.onchain_pubkey_index.clone(), } } } @@ -992,7 +1000,33 @@ impl Indexer for TestIndexer { } async fn get_indexer_health(&self, _config: Option) -> Result { - todo!("get_indexer_health not implemented") + Ok(true) // Test indexer is always healthy + } + + async fn get_compressed_mint( + &self, + _address: Address, + _config: Option, + ) -> Result>, IndexerError> { + todo!("get_compressed_mint not implemented") + } + + async fn get_compressed_mint_by_pda( + &self, + _mint_pda: &Pubkey, + _config: Option, + ) -> Result>, IndexerError> { + todo!("get_compressed_mint_by_pda not implemented") + } + + async fn get_compressed_mints_by_authority( + &self, + _authority: &Pubkey, + _authority_type: MintAuthorityType, + _options: Option, + _config: Option, + ) -> Result>, IndexerError> { + todo!("get_compressed_mints_by_authority not implemented") } } @@ -1345,9 +1379,153 @@ impl TestIndexer { token_compressed_accounts: vec![], token_nullified_compressed_accounts: vec![], group_pda, + onchain_pubkey_index: HashMap::new(), + } + } + + /// Extract onchain_pubkey from compressed account data if it has the decompressed discriminator. + /// Compressible accounts store the on-chain PDA pubkey in the first 32 bytes of data. + fn extract_onchain_pubkey_from_data( + data: Option<&light_compressed_account::compressed_account::CompressedAccountData>, + ) -> Option<[u8; 32]> { + let data = data?; + // Check discriminator matches DECOMPRESSED_PDA_DISCRIMINATOR + if data.discriminator == DECOMPRESSED_PDA_DISCRIMINATOR && data.data.len() >= 32 { + // onchain_pubkey is stored in the first 32 bytes of data (after discriminator) + data.data[..32].try_into().ok() + } else { + None } } + /// Find a compressed account by its on-chain pubkey. + /// This mirrors Photon's lookup by onchain_pubkey column. + pub fn find_compressed_account_by_onchain_pubkey( + &self, + onchain_pubkey: &[u8; 32], + ) -> Option<&CompressedAccountWithMerkleContext> { + let matches: Vec<_> = self + .compressed_accounts + .iter() + .filter(|acc| { + Self::extract_onchain_pubkey_from_data(acc.compressed_account.data.as_ref()) + .as_ref() + == Some(onchain_pubkey) + }) + .collect(); + + debug_assert!( + matches.len() <= 1, + "find_compressed_account_by_onchain_pubkey: found {} matches, expected at most 1", + matches.len() + ); + + matches.into_iter().next() + } + + /// Find multiple compressed accounts by their on-chain pubkeys. + pub fn find_multiple_compressed_accounts_by_onchain_pubkeys( + &self, + onchain_pubkeys: &[[u8; 32]], + ) -> Vec> { + onchain_pubkeys + .iter() + .map(|pubkey| self.find_compressed_account_by_onchain_pubkey(pubkey)) + .collect() + } + + /// Find a token compressed account by its on-chain pubkey. + pub fn find_token_account_by_onchain_pubkey( + &self, + onchain_pubkey: &[u8; 32], + ) -> Option<&TokenDataWithMerkleContext> { + let matches: Vec<_> = self + .token_compressed_accounts + .iter() + .filter(|acc| { + Self::extract_onchain_pubkey_from_data( + acc.compressed_account.compressed_account.data.as_ref(), + ) + .as_ref() + == Some(onchain_pubkey) + }) + .collect(); + + debug_assert!( + matches.len() <= 1, + "find_token_account_by_onchain_pubkey: found {} matches, expected at most 1", + matches.len() + ); + + matches.into_iter().next() + } + + /// Find a compressed account by its PDA pubkey + pub fn find_compressed_account_by_pda_seed( + &self, + pda_pubkey: &[u8; 32], + ) -> Option<&CompressedAccountWithMerkleContext> { + // Try each address tree to find an account whose address matches + for address_tree in &self.address_merkle_trees { + let tree_pubkey = address_tree.accounts.merkle_tree.to_bytes(); + + // For each compressed account with an address, check if it was derived from this seed + for acc in &self.compressed_accounts { + if let Some(address) = acc.compressed_account.address { + // Try deriving with this tree and the account's owner as program_id + let owner_bytes = acc.compressed_account.owner.to_bytes(); + let derived = light_compressed_account::address::derive_address( + pda_pubkey, + &tree_pubkey, + &owner_bytes, + ); + + if derived == address { + return Some(acc); + } + } + } + } + None + } + + /// Find a token compressed account by its PDA pubkey + pub fn find_token_account_by_pda_seed( + &self, + pda_pubkey: &[u8; 32], + ) -> Option<&TokenDataWithMerkleContext> { + // Try each address tree to find an account whose address matches + for address_tree in &self.address_merkle_trees { + let tree_pubkey = address_tree.accounts.merkle_tree.to_bytes(); + + // For each token compressed account with an address, check if it was derived from this seed + for acc in &self.token_compressed_accounts { + if let Some(address) = acc.compressed_account.compressed_account.address { + // Try deriving with this tree and the account's owner as program_id + let owner_bytes = acc.compressed_account.compressed_account.owner.to_bytes(); + let derived = light_compressed_account::address::derive_address( + pda_pubkey, + &tree_pubkey, + &owner_bytes, + ); + + if derived == address { + return Some(acc); + } + } + } + } + None + } + + /// Get the sequence number for a state merkle tree by its pubkey. + pub fn get_state_tree_seq(&self, tree_pubkey: &Pubkey) -> Option { + self.state_merkle_trees + .iter() + .find(|tree| tree.accounts.merkle_tree == *tree_pubkey) + .map(|tree| tree.merkle_tree.sequence_number as u64) + } + pub fn add_address_merkle_tree_bundle( address_merkle_tree_accounts: AddressMerkleTreeAccounts, // TODO: add config here diff --git a/sdk-libs/program-test/src/program_test/indexer.rs b/sdk-libs/program-test/src/program_test/indexer.rs index a1a80113ce..5178e533db 100644 --- a/sdk-libs/program-test/src/program_test/indexer.rs +++ b/sdk-libs/program-test/src/program_test/indexer.rs @@ -1,8 +1,9 @@ use async_trait::async_trait; use light_client::indexer::{ - Address, AddressWithTree, CompressedAccount, CompressedTokenAccount, - GetCompressedAccountsByOwnerConfig, GetCompressedTokenAccountsByOwnerOrDelegateOptions, Hash, - Indexer, IndexerError, IndexerRpcConfig, Items, ItemsWithCursor, MerkleProof, + Address, AddressWithTree, CompressedAccount, CompressedMint, CompressedTokenAccount, + GetCompressedAccountsByOwnerConfig, GetCompressedMintsByAuthorityOptions, + GetCompressedTokenAccountsByOwnerOrDelegateOptions, Hash, Indexer, IndexerError, + IndexerRpcConfig, Items, ItemsWithCursor, MerkleProof, MintAuthorityType, NewAddressProofWithContext, OwnerBalance, PaginatedOptions, QueueElementsResult, QueueElementsV2Options, Response, RetryConfig, SignatureWithMetadata, TokenBalance, ValidityProofWithContext, @@ -313,4 +314,45 @@ impl Indexer for LightProgramTest { .get_indexer_health(config) .await?) } + + async fn get_compressed_mint( + &self, + address: Address, + config: Option, + ) -> Result>, IndexerError> { + Ok(self + .indexer + .as_ref() + .ok_or(IndexerError::NotInitialized)? + .get_compressed_mint(address, config) + .await?) + } + + async fn get_compressed_mint_by_pda( + &self, + mint_pda: &Pubkey, + config: Option, + ) -> Result>, IndexerError> { + Ok(self + .indexer + .as_ref() + .ok_or(IndexerError::NotInitialized)? + .get_compressed_mint_by_pda(mint_pda, config) + .await?) + } + + async fn get_compressed_mints_by_authority( + &self, + authority: &Pubkey, + authority_type: MintAuthorityType, + options: Option, + config: Option, + ) -> Result>, IndexerError> { + Ok(self + .indexer + .as_ref() + .ok_or(IndexerError::NotInitialized)? + .get_compressed_mints_by_authority(authority, authority_type, options, config) + .await?) + } } diff --git a/sdk-libs/program-test/src/program_test/rpc.rs b/sdk-libs/program-test/src/program_test/rpc.rs index a2f5d6981d..2aded8111e 100644 --- a/sdk-libs/program-test/src/program_test/rpc.rs +++ b/sdk-libs/program-test/src/program_test/rpc.rs @@ -4,7 +4,8 @@ use anchor_lang::pubkey; use async_trait::async_trait; use borsh::BorshDeserialize; use light_client::{ - indexer::{Indexer, TreeInfo}, + indexer::{CompressedAccount, CompressedTokenAccount, Context, Indexer, Response, TreeInfo}, + interface::{AccountInterface, MintInterface, MintState, TokenAccountInterface}, rpc::{LightClientConfig, Rpc, RpcError}, }; use light_compressed_account::TreeType; @@ -366,6 +367,444 @@ impl Rpc for LightProgramTest { "create_and_send_versioned_transaction is unimplemented for LightProgramTest" ); } + + async fn get_account_interface( + &self, + address: &Pubkey, + _config: Option, + ) -> Result>, RpcError> { + let slot = self.context.get_sysvar::().slot; + + // Hot: check on-chain first + if let Some(account) = self.context.get_account(address) { + if account.lamports > 0 { + return Ok(Response { + context: Context { slot }, + value: Some(AccountInterface::hot(*address, account)), + }); + } + } + + // Cold: check TestIndexer by onchain pubkey (mirrors Photon behavior) + if let Some(indexer) = self.indexer.as_ref() { + // First try: lookup by onchain_pubkey (for accounts with DECOMPRESSED_PDA_DISCRIMINATOR) + if let Some(compressed_with_ctx) = + indexer.find_compressed_account_by_onchain_pubkey(&address.to_bytes()) + { + let owner: Pubkey = compressed_with_ctx.compressed_account.owner.into(); + let compressed: CompressedAccount = compressed_with_ctx.clone().try_into().map_err( + |e| { + RpcError::CustomError(format!( + "CompressedAccountWithMerkleContext conversion failed for address {}: {:?}", + address, e + )) + }, + )?; + + return Ok(Response { + context: Context { slot }, + value: Some(AccountInterface::cold(*address, compressed, owner)), + }); + } + + // Second try: lookup by PDA seed (for accounts whose address was derived from this pubkey) + if let Some(compressed_with_ctx) = + indexer.find_compressed_account_by_pda_seed(&address.to_bytes()) + { + let owner: Pubkey = compressed_with_ctx.compressed_account.owner.into(); + let compressed: CompressedAccount = compressed_with_ctx.clone().try_into().map_err( + |e| { + RpcError::CustomError(format!( + "CompressedAccountWithMerkleContext conversion failed for PDA seed {}: {:?}", + address, e + )) + }, + )?; + + return Ok(Response { + context: Context { slot }, + value: Some(AccountInterface::cold(*address, compressed, owner)), + }); + } + } + + Ok(Response { + context: Context { slot }, + value: None, + }) + } + + async fn get_token_account_interface( + &self, + address: &Pubkey, + _config: Option, + ) -> Result>, RpcError> { + use light_sdk::constants::LIGHT_TOKEN_PROGRAM_ID; + + let light_token_program_id: Pubkey = LIGHT_TOKEN_PROGRAM_ID.into(); + let slot = self.context.get_sysvar::().slot; + + // Hot: check on-chain first (must be owned by LIGHT_TOKEN_PROGRAM_ID) + if let Some(account) = self.context.get_account(address) { + if account.lamports > 0 && account.owner == light_token_program_id { + match TokenAccountInterface::hot(*address, account) { + Ok(iface) => { + return Ok(Response { + context: Context { slot }, + value: Some(iface), + }); + } + Err(_) => { + // Fall through to cold lookup if parsing failed + } + } + } + } + + // Cold: check TestIndexer by onchain_pubkey, PDA seed, or token_data.owner + if let Some(indexer) = self.indexer.as_ref() { + // First try: lookup by onchain_pubkey (for accounts with DECOMPRESSED_PDA_DISCRIMINATOR) + let token_acc = indexer + .find_token_account_by_onchain_pubkey(&address.to_bytes()) + .or_else(|| { + // Second try: lookup by PDA seed (for accounts whose address was derived from this pubkey) + indexer.find_token_account_by_pda_seed(&address.to_bytes()) + }); + + if let Some(token_acc) = token_acc { + // Convert to CompressedTokenAccount + let compressed_account: CompressedAccount = token_acc + .compressed_account + .clone() + .try_into() + .map_err(|e| RpcError::CustomError(format!("conversion error: {:?}", e)))?; + + let compressed_token = CompressedTokenAccount { + token: token_acc.token_data.clone(), + account: compressed_account, + }; + + return Ok(Response { + context: Context { slot }, + value: Some(TokenAccountInterface::cold( + *address, + compressed_token, + *address, // owner = hot address for program-owned tokens + light_token_program_id, + )), + }); + } + + // Third try: lookup by token_data.owner (for tokens where owner == address) + let result = indexer + .get_compressed_token_accounts_by_owner(address, None, None) + .await + .map_err(|e| RpcError::CustomError(format!("indexer error: {}", e)))?; + + let items = result.value.items; + if items.len() > 1 { + return Err(RpcError::CustomError(format!( + "Ambiguous lookup: found {} compressed token accounts for address {}. \ + Use get_compressed_token_accounts_by_owner for multiple accounts.", + items.len(), + address + ))); + } + + if let Some(token_acc) = items.into_iter().next() { + let key = token_acc + .account + .address + .map(Pubkey::new_from_array) + .unwrap_or(*address); + return Ok(Response { + context: Context { slot }, + value: Some(TokenAccountInterface::cold( + key, + token_acc, + *address, // owner = hot address for program-owned tokens + light_token_program_id, + )), + }); + } + } + + Ok(Response { + context: Context { slot }, + value: None, + }) + } + + async fn get_ata_interface( + &self, + owner: &Pubkey, + mint: &Pubkey, + _config: Option, + ) -> Result>, RpcError> { + use light_client::indexer::GetCompressedTokenAccountsByOwnerOrDelegateOptions; + use light_sdk::constants::LIGHT_TOKEN_PROGRAM_ID; + use light_token::instruction::derive_token_ata; + + let (ata, _bump) = derive_token_ata(owner, mint); + let light_token_program_id: Pubkey = LIGHT_TOKEN_PROGRAM_ID.into(); + let slot = self.context.get_sysvar::().slot; + + // First try: on-chain (hot) lookup + // We handle this directly instead of using get_token_account_interface + // because we need to control owner_override for ata_bump() to work + if let Some(account) = self.context.get_account(&ata) { + if account.lamports > 0 && account.owner == light_token_program_id { + match TokenAccountInterface::hot(ata, account) { + Ok(iface) => { + return Ok(Response { + context: Context { slot }, + value: Some(iface), + }); + } + Err(_) => { + // Fall through to cold lookup if parsing failed + } + } + } + } + + // Cold: search compressed tokens by ata_pubkey + mint + // In Light Protocol, token_data.owner is the token account pubkey (ATA), not wallet owner + // But we need to pass the wallet owner for TokenAccountInterface::cold so ata_bump() works + if let Some(indexer) = self.indexer.as_ref() { + let options = Some(GetCompressedTokenAccountsByOwnerOrDelegateOptions { + mint: Some(*mint), + ..Default::default() + }); + let result = indexer + .get_compressed_token_accounts_by_owner(&ata, options, None) + .await + .map_err(|e| RpcError::CustomError(format!("indexer error: {}", e)))?; + + let items = result.value.items; + if items.len() > 1 { + return Err(RpcError::CustomError(format!( + "Ambiguous lookup: found {} compressed token accounts for ATA {} (owner: {}, mint: {}). \ + Use get_compressed_token_accounts_by_owner for multiple accounts.", + items.len(), + ata, + owner, + mint + ))); + } + + if let Some(token_acc) = items.into_iter().next() { + return Ok(Response { + context: Context { slot }, + value: Some(TokenAccountInterface::cold( + ata, // key = ATA pubkey (derived, so we use it directly) + token_acc, + *owner, // owner_override = wallet owner (for ata_bump() to work) + light_token_program_id, + )), + }); + } + } + + Ok(Response { + context: Context { slot }, + value: None, + }) + } + + async fn get_token_account_by_owner_mint( + &self, + owner: &Pubkey, + mint: &Pubkey, + _config: Option, + ) -> Result>, RpcError> { + use light_client::indexer::GetCompressedTokenAccountsByOwnerOrDelegateOptions; + use light_sdk::constants::LIGHT_TOKEN_PROGRAM_ID; + + let light_token_program_id: Pubkey = LIGHT_TOKEN_PROGRAM_ID.into(); + let slot = self.context.get_sysvar::().slot; + + // Search in compressed token accounts by owner with mint filter + if let Some(indexer) = self.indexer.as_ref() { + let options = Some(GetCompressedTokenAccountsByOwnerOrDelegateOptions { + mint: Some(*mint), + ..Default::default() + }); + let result = indexer + .get_compressed_token_accounts_by_owner(owner, options, None) + .await + .map_err(|e| RpcError::CustomError(format!("indexer error: {}", e)))?; + + let items = result.value.items; + if items.len() > 1 { + return Err(RpcError::CustomError(format!( + "Ambiguous lookup: found {} compressed token accounts for owner {} and mint {}. \ + Use get_compressed_token_accounts_by_owner for multiple accounts.", + items.len(), + owner, + mint + ))); + } + + if let Some(token_acc) = items.into_iter().next() { + let key = token_acc + .account + .address + .map(Pubkey::new_from_array) + .unwrap_or(*owner); + return Ok(Response { + context: Context { slot }, + value: Some(TokenAccountInterface::cold( + key, + token_acc, + *owner, + light_token_program_id, + )), + }); + } + } + + Ok(Response { + context: Context { slot }, + value: None, + }) + } + + async fn get_mint_interface( + &self, + address: &Pubkey, + _config: Option, + ) -> Result>, RpcError> { + use borsh::BorshDeserialize as _; + use light_compressed_account::address::derive_address; + use light_token_interface::{state::Mint, MINT_ADDRESS_TREE}; + + let slot = self.context.get_sysvar::().slot; + let address_tree = Pubkey::new_from_array(MINT_ADDRESS_TREE); + let light_token_program_id: Pubkey = + Pubkey::new_from_array(light_token_interface::LIGHT_TOKEN_PROGRAM_ID); + let compressed_address = derive_address( + &address.to_bytes(), + &address_tree.to_bytes(), + &light_token_interface::LIGHT_TOKEN_PROGRAM_ID, + ); + + if let Some(account) = self.context.get_account(address) { + if account.lamports > 0 && account.owner == light_token_program_id { + return Ok(Response { + context: Context { slot }, + value: Some(MintInterface { + mint: *address, + address_tree, + compressed_address, + state: MintState::Hot { account }, + }), + }); + } + } + + // Cold: check indexer by compressed address + if let Some(indexer) = self.indexer.as_ref() { + let result = indexer + .get_compressed_account(compressed_address, None) + .await + .map_err(|e| RpcError::CustomError(format!("indexer error: {}", e)))?; + + if let Some(compressed) = result.value { + if let Some(data) = compressed.data.as_ref() { + if !data.data.is_empty() { + let mint_data = Mint::try_from_slice(&data.data).map_err(|e| { + RpcError::CustomError(format!("mint parse error: {}", e)) + })?; + return Ok(Response { + context: Context { slot }, + value: Some(MintInterface { + mint: *address, + address_tree, + compressed_address, + state: MintState::Cold { + compressed, + mint_data, + }, + }), + }); + } + } + } + } + + // Not found + Ok(Response { + context: Context { slot }, + value: None, + }) + } + + async fn get_multiple_account_interfaces( + &self, + addresses: Vec<&Pubkey>, + _config: Option, + ) -> Result>>, RpcError> { + let slot = self.context.get_sysvar::().slot; + let mut results: Vec> = vec![None; addresses.len()]; + + // Batch fetch on-chain accounts (hot path) + let owned_addresses: Vec = addresses.iter().map(|a| **a).collect(); + let on_chain_accounts: Vec> = owned_addresses + .iter() + .map(|addr| self.context.get_account(addr)) + .collect(); + + // Track which addresses still need cold lookup + let mut cold_lookup_indices: Vec = Vec::new(); + let mut cold_lookup_pubkeys: Vec<[u8; 32]> = Vec::new(); + + for (i, (address, maybe_account)) in addresses + .iter() + .zip(on_chain_accounts.into_iter()) + .enumerate() + { + if let Some(account) = maybe_account { + if account.lamports > 0 { + results[i] = Some(AccountInterface::hot(**address, account)); + continue; + } + } + // Not found on-chain or has 0 lamports, need cold lookup + cold_lookup_indices.push(i); + cold_lookup_pubkeys.push(address.to_bytes()); + } + + // Batch lookup cold accounts from TestIndexer + if !cold_lookup_pubkeys.is_empty() { + if let Some(indexer) = self.indexer.as_ref() { + let cold_results = indexer + .find_multiple_compressed_accounts_by_onchain_pubkeys(&cold_lookup_pubkeys); + + for (lookup_idx, maybe_compressed) in cold_results.into_iter().enumerate() { + let original_idx = cold_lookup_indices[lookup_idx]; + if let Some(compressed_with_ctx) = maybe_compressed { + let owner: Pubkey = compressed_with_ctx.compressed_account.owner.into(); + let compressed: CompressedAccount = + compressed_with_ctx.clone().try_into().map_err(|e| { + RpcError::CustomError(format!("conversion error: {:?}", e)) + })?; + + results[original_idx] = Some(AccountInterface::cold( + *addresses[original_idx], + compressed, + owner, + )); + } + } + } + } + + Ok(Response { + context: Context { slot }, + value: results, + }) + } } impl LightProgramTest { diff --git a/sdk-tests/client-test/tests/light_client.rs b/sdk-tests/client-test/tests/light_client.rs index 22d799c888..5f6bfb45f7 100644 --- a/sdk-tests/client-test/tests/light_client.rs +++ b/sdk-tests/client-test/tests/light_client.rs @@ -55,6 +55,7 @@ async fn test_all_endpoints() { sbf_programs: vec![], upgradeable_programs: vec![], limit_ledger_size: None, + validator_args: vec![], }; spawn_validator(config).await; diff --git a/sdk-tests/csdk-anchor-full-derived-test-sdk/src/lib.rs b/sdk-tests/csdk-anchor-full-derived-test-sdk/src/lib.rs index 5cbe8e3521..d9d0a608b9 100644 --- a/sdk-tests/csdk-anchor-full-derived-test-sdk/src/lib.rs +++ b/sdk-tests/csdk-anchor-full-derived-test-sdk/src/lib.rs @@ -33,6 +33,11 @@ pub type MintInterfaceMap = HashMap, kind: AccountKind) -> Self { Self { pubkey, kind } } + + fn token_by_owner_mint(owner: Pubkey, mint: Pubkey) -> Self { + Self { + pubkey: None, // No direct pubkey for owner+mint lookup + kind: AccountKind::TokenByOwnerMint { owner, mint }, + } + } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -229,6 +241,9 @@ impl AmmSdk { let compressed_account = match &account.cold { Some(ColdContext::Token(ct)) => ct.account.clone(), Some(ColdContext::Account(ca)) => ca.clone(), + Some(ColdContext::Mint(_)) => { + return Err(AmmSdkError::MissingField("unexpected Mint cold context")) + } None => return Err(AmmSdkError::MissingField("cold_context")), }; AccountInterface { @@ -290,20 +305,29 @@ impl AmmSdk { } fn account_requirements(&self, ix: &AmmInstruction) -> Vec { + let vault_0_req = match (self.token_0_vault, self.token_0_mint) { + (Some(owner), Some(mint)) => AccountRequirement::token_by_owner_mint(owner, mint), + _ => AccountRequirement::new(self.token_0_vault, AccountKind::Token), + }; + let vault_1_req = match (self.token_1_vault, self.token_1_mint) { + (Some(owner), Some(mint)) => AccountRequirement::token_by_owner_mint(owner, mint), + _ => AccountRequirement::new(self.token_1_vault, AccountKind::Token), + }; + match ix { AmmInstruction::Swap => { vec![ AccountRequirement::new(self.pool_state_pubkey, AccountKind::Pda), - AccountRequirement::new(self.token_0_vault, AccountKind::Token), - AccountRequirement::new(self.token_1_vault, AccountKind::Token), + vault_0_req, + vault_1_req, AccountRequirement::new(self.observation_key, AccountKind::Pda), ] } AmmInstruction::Deposit | AmmInstruction::Withdraw => { vec![ AccountRequirement::new(self.pool_state_pubkey, AccountKind::Pda), - AccountRequirement::new(self.token_0_vault, AccountKind::Token), - AccountRequirement::new(self.token_1_vault, AccountKind::Token), + vault_0_req, + vault_1_req, AccountRequirement::new(self.observation_key, AccountKind::Pda), AccountRequirement::new(self.lp_mint, AccountKind::Mint), ] @@ -339,12 +363,15 @@ impl LightProgramInterface for AmmSdk { fn get_accounts_to_update(&self, ix: &Self::Instruction) -> Vec { self.account_requirements(ix) .into_iter() - .filter_map(|req| { - req.pubkey.map(|pubkey| match req.kind { - AccountKind::Pda => AccountToFetch::pda(pubkey, PROGRAM_ID), - AccountKind::Token => AccountToFetch::token(pubkey), - AccountKind::Mint => AccountToFetch::mint(pubkey), - }) + .filter_map(|req| match req.kind { + AccountKind::Pda => req + .pubkey + .map(|pubkey| AccountToFetch::pda(pubkey, PROGRAM_ID)), + AccountKind::Token => req.pubkey.map(AccountToFetch::token), + AccountKind::TokenByOwnerMint { owner, mint } => { + Some(AccountToFetch::token_by_owner_mint(owner, mint)) + } + AccountKind::Mint => req.pubkey.map(AccountToFetch::mint), }) .collect() } @@ -381,6 +408,11 @@ impl LightProgramInterface for AmmSdk { } } } + AccountKind::TokenByOwnerMint { owner, mint: _ } => { + if let Some(spec) = self.program_owned_specs.get(&owner) { + specs.push(AccountSpec::Pda(spec.clone())); + } + } AccountKind::Mint => { if let Some(mint_pubkey) = req.pubkey { if let Some(spec) = self.mint_specs.get(&mint_pubkey) { diff --git a/sdk-tests/csdk-anchor-full-derived-test-sdk/tests/trait_tests.rs b/sdk-tests/csdk-anchor-full-derived-test-sdk/tests/trait_tests.rs index 3a9c8332e2..c5fa516125 100644 --- a/sdk-tests/csdk-anchor-full-derived-test-sdk/tests/trait_tests.rs +++ b/sdk-tests/csdk-anchor-full-derived-test-sdk/tests/trait_tests.rs @@ -495,11 +495,12 @@ fn test_get_accounts_to_update_categories() { let sdk = AmmSdk::new(); let typed = sdk.get_accounts_to_update(&AmmInstruction::Deposit); - // All should be one of Pda, Token, Ata, or Mint + // All should be one of Pda, Token, TokenByOwnerMint, Ata, or Mint for acc in &typed { match acc { AccountToFetch::Pda { .. } => {} AccountToFetch::Token { .. } => {} + AccountToFetch::TokenByOwnerMint { .. } => {} AccountToFetch::Ata { .. } => {} AccountToFetch::Mint { .. } => {} } diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/amm_stress_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/amm_stress_test.rs index c89d9c1b3c..ec6d59d6e8 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/tests/amm_stress_test.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/amm_stress_test.rs @@ -19,8 +19,8 @@ use light_batched_merkle_tree::{ initialize_state_tree::InitStateTreeAccountsInstructionData, }; use light_client::interface::{ - create_load_instructions, get_create_accounts_proof, AccountInterfaceExt, - CreateAccountsProofInput, InitializeRentFreeConfig, LightProgramInterface, + create_load_instructions, get_create_accounts_proof, CreateAccountsProofInput, + InitializeRentFreeConfig, LightProgramInterface, }; use light_compressible::rent::SLOTS_PER_EPOCH; use light_program_test::{ @@ -489,9 +489,11 @@ async fn refresh_cache(rpc: &mut LightProgramTest, pdas: &AmmPdas) -> CachedStat async fn decompress_all(ctx: &mut AmmTestContext, pdas: &AmmPdas) { let pool_interface = ctx .rpc - .get_account_interface(&pdas.pool_state, &ctx.program_id) + .get_account_interface(&pdas.pool_state, None) .await - .expect("failed to get pool_state"); + .expect("failed to get pool_state") + .value + .expect("pool_state should exist"); assert!(pool_interface.is_cold(), "pool_state should be cold"); let mut sdk = AmmSdk::from_keyed_accounts(&[pool_interface]) @@ -501,9 +503,9 @@ async fn decompress_all(ctx: &mut AmmTestContext, pdas: &AmmPdas) { let keyed_accounts = ctx .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"); @@ -512,31 +514,39 @@ async fn decompress_all(ctx: &mut AmmTestContext, pdas: &AmmPdas) { let creator_lp_interface = ctx .rpc - .get_ata_interface(&ctx.creator.pubkey(), &pdas.lp_mint) + .get_ata_interface(&ctx.creator.pubkey(), &pdas.lp_mint, None) .await - .expect("failed to get creator_lp_token"); + .expect("failed to get creator_lp_token") + .value + .expect("creator_lp_token should exist"); // Creator's token_0 and token_1 ATAs also get compressed during epoch warp let creator_token_0_interface = ctx .rpc - .get_ata_interface(&ctx.creator.pubkey(), &ctx.token_0_mint) + .get_ata_interface(&ctx.creator.pubkey(), &ctx.token_0_mint, None) .await - .expect("failed to get creator_token_0"); + .expect("failed to get creator_token_0") + .value + .expect("creator_token_0 should exist"); let creator_token_1_interface = ctx .rpc - .get_ata_interface(&ctx.creator.pubkey(), &ctx.token_1_mint) + .get_ata_interface(&ctx.creator.pubkey(), &ctx.token_1_mint, None) .await - .expect("failed to get creator_token_1"); + .expect("failed to get creator_token_1") + .value + .expect("creator_token_1 should exist"); // Underlying mints also get compressed -- convert MintInterface to AccountInterface use light_client::interface::{AccountInterface, AccountSpec, MintState}; let mint_0_iface = ctx .rpc - .get_mint_interface(&ctx.token_0_mint) + .get_mint_interface(&ctx.token_0_mint, None) .await - .expect("failed to get token_0_mint"); + .expect("failed to get token_0_mint") + .value + .expect("token_0_mint should exist"); let mint_0_account_iface = match mint_0_iface.state { MintState::Hot { account } => AccountInterface { key: mint_0_iface.mint, @@ -556,9 +566,11 @@ async fn decompress_all(ctx: &mut AmmTestContext, pdas: &AmmPdas) { let mint_1_iface = ctx .rpc - .get_mint_interface(&ctx.token_1_mint) + .get_mint_interface(&ctx.token_1_mint, None) .await - .expect("failed to get token_1_mint"); + .expect("failed to get token_1_mint") + .value + .expect("token_1_mint should exist"); let mint_1_account_iface = match mint_1_iface.state { MintState::Hot { account } => AccountInterface { key: mint_1_iface.mint, diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/amm_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/amm_test.rs index 844022ac64..2fa278c9d7 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/tests/amm_test.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/amm_test.rs @@ -17,8 +17,8 @@ use csdk_anchor_full_derived_test::amm_test::{ // SDK for AmmSdk-based approach use csdk_anchor_full_derived_test_sdk::{AmmInstruction, AmmSdk}; use light_client::interface::{ - create_load_instructions, get_create_accounts_proof, AccountInterfaceExt, - CreateAccountsProofInput, InitializeRentFreeConfig, LightProgramInterface, + create_load_instructions, get_create_accounts_proof, CreateAccountsProofInput, + InitializeRentFreeConfig, LightProgramInterface, }; use light_compressible::rent::SLOTS_PER_EPOCH; use light_program_test::{ @@ -631,9 +631,11 @@ async fn test_amm_full_lifecycle() { let pool_interface = ctx .rpc - .get_account_interface(&pdas.pool_state, &ctx.program_id) + .get_account_interface(&pdas.pool_state, None) .await - .expect("failed to get pool_state"); + .expect("failed to get pool_state") + .value + .expect("pool_state should exist"); assert!(pool_interface.is_cold(), "pool_state should be cold"); // Create Program Interface SDK. @@ -644,9 +646,9 @@ async fn test_amm_full_lifecycle() { let keyed_accounts = ctx .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"); @@ -655,9 +657,11 @@ async fn test_amm_full_lifecycle() { let creator_lp_interface = ctx .rpc - .get_ata_interface(&ctx.creator.pubkey(), &pdas.lp_mint) + .get_ata_interface(&ctx.creator.pubkey(), &pdas.lp_mint, None) .await - .expect("failed to get creator_lp_token"); + .expect("failed to get creator_lp_token") + .value + .expect("creator_lp_token should exist"); // add ata use light_client::interface::AccountSpec; diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/basic_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/basic_test.rs index ba5cc3128c..16ae2d6464 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/tests/basic_test.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/basic_test.rs @@ -2,8 +2,7 @@ mod shared; use anchor_lang::{InstructionData, ToAccountMetas}; use light_client::interface::{ - get_create_accounts_proof, AccountInterfaceExt, CreateAccountsProofInput, - InitializeRentFreeConfig, + get_create_accounts_proof, CreateAccountsProofInput, InitializeRentFreeConfig, }; use light_compressible::{rent::SLOTS_PER_EPOCH, DECOMPRESSED_PDA_DISCRIMINATOR}; use light_program_test::{ @@ -360,21 +359,27 @@ async fn test_create_pdas_and_mint_auto() { // Fetch unified interfaces (hot/cold transparent) let user_interface = rpc - .get_account_interface(&user_record_pda, &program_id) + .get_account_interface(&user_record_pda, None) .await - .expect("failed to get user"); + .expect("failed to get user") + .value + .expect("user should exist"); assert!(user_interface.is_cold(), "UserRecord should be cold"); let game_interface = rpc - .get_account_interface(&game_session_pda, &program_id) + .get_account_interface(&game_session_pda, None) .await - .expect("failed to get game"); + .expect("failed to get game") + .value + .expect("game should exist"); assert!(game_interface.is_cold(), "GameSession should be cold"); let vault_interface = rpc - .get_token_account_interface(&vault_pda) + .get_token_account_interface(&vault_pda, None) .await - .expect("failed to get vault"); + .expect("failed to get vault") + .value + .expect("vault should exist"); assert!(vault_interface.is_cold(), "Vault should be cold"); assert_eq!(vault_interface.amount(), vault_mint_amount); @@ -427,9 +432,11 @@ async fn test_create_pdas_and_mint_auto() { // get_ata_interface: fetches ATA with unified handling using standard SPL types let ata_interface = rpc - .get_ata_interface(&payer.pubkey(), &mint_pda) + .get_ata_interface(&payer.pubkey(), &mint_pda, None) .await - .expect("get_ata_interface should succeed"); + .expect("get_ata_interface should succeed") + .value + .expect("ATA should exist"); assert!(ata_interface.is_cold(), "ATA should be cold after warp"); assert_eq!(ata_interface.amount(), user_ata_mint_amount); assert_eq!(ata_interface.mint(), mint_pda); @@ -441,26 +448,15 @@ async fn test_create_pdas_and_mint_auto() { // Fetch mint interface let mint_interface = rpc - .get_mint_interface(&mint_pda) + .get_mint_interface(&mint_pda, None) .await - .expect("get_mint_interface should succeed"); + .expect("get_mint_interface should succeed") + .value + .expect("Mint should exist"); assert!(mint_interface.is_cold(), "Mint should be cold after warp"); // Convert MintInterface to AccountInterface for use in AccountSpec - let (compressed, _mint_data) = mint_interface - .compressed() - .expect("cold mint must have compressed data"); - let mint_account_interface = AccountInterface { - key: mint_pda, - account: solana_account::Account { - lamports: 0, - data: vec![], - owner: light_token::instruction::LIGHT_TOKEN_PROGRAM_ID, - executable: false, - rent_epoch: 0, - }, - cold: Some(ColdContext::Account(compressed.clone())), - }; + let mint_account_interface: AccountInterface = mint_interface.into(); // Build AccountSpec slice for all accounts let specs: Vec> = vec![ diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/d10_token_accounts_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/d10_token_accounts_test.rs index cb80c58243..e505025eeb 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/tests/d10_token_accounts_test.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/d10_token_accounts_test.rs @@ -10,9 +10,7 @@ use csdk_anchor_full_derived_test::d10_token_accounts::{ D10SingleAtaMarkonlyParams, D10SingleAtaParams, D10SingleVaultParams, D10_SINGLE_VAULT_AUTH_SEED, D10_SINGLE_VAULT_SEED, }; -use light_client::interface::{ - get_create_accounts_proof, AccountInterfaceExt, InitializeRentFreeConfig, -}; +use light_client::interface::{get_create_accounts_proof, InitializeRentFreeConfig}; use light_program_test::{ program_test::{setup_mock_program_data, LightProgramTest}, ProgramTestConfig, Rpc, @@ -537,9 +535,11 @@ async fn test_d10_single_ata_markonly_lifecycle() { // ATAs use get_ata_interface which fetches the compressed token data let ata_interface = ctx .rpc - .get_ata_interface(&ata_owner, &mint) + .get_ata_interface(&ata_owner, &mint, None) .await - .expect("get_ata_interface should succeed"); + .expect("get_ata_interface should succeed") + .value + .expect("ata interface should exist"); assert!( ata_interface.is_cold(), "ATA should be cold after compression" diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/d11_zero_copy_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/d11_zero_copy_test.rs index 80497c3ff1..8a7991eb63 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/tests/d11_zero_copy_test.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/d11_zero_copy_test.rs @@ -42,8 +42,8 @@ use csdk_anchor_full_derived_test::d11_zero_copy::{ D11_ZC_VAULT_SEED, }; use light_client::interface::{ - create_load_instructions, get_create_accounts_proof, AccountInterfaceExt, AccountSpec, - CreateAccountsProofInput, InitializeRentFreeConfig, PdaSpec, + create_load_instructions, get_create_accounts_proof, AccountSpec, CreateAccountsProofInput, + InitializeRentFreeConfig, PdaSpec, }; use light_compressed_account::address::derive_address; use light_compressible::rent::SLOTS_PER_EPOCH; @@ -256,9 +256,11 @@ async fn test_d11_zc_with_vault() { // PHASE 4: Decompress account let account_interface = ctx .rpc - .get_account_interface(&zc_pda, &ctx.program_id) + .get_account_interface(&zc_pda, None) .await - .expect("failed to get account interface"); + .expect("failed to get account interface") + .value + .expect("account interface should exist"); assert!( account_interface.is_cold(), "Account should be cold (compressed)" @@ -426,9 +428,11 @@ async fn test_d11_zc_with_ata() { // PHASE 4: Decompress account let account_interface = ctx .rpc - .get_account_interface(&zc_pda, &ctx.program_id) + .get_account_interface(&zc_pda, None) .await - .expect("failed to get account interface"); + .expect("failed to get account interface") + .value + .expect("account interface should exist"); assert!( account_interface.is_cold(), "Account should be cold (compressed)" @@ -584,9 +588,11 @@ async fn test_d11_multiple_zc() { // PHASE 4: Decompress first account let account_interface_1 = ctx .rpc - .get_account_interface(&zc_pda_1, &ctx.program_id) + .get_account_interface(&zc_pda_1, None) .await - .expect("failed to get account interface 1"); + .expect("failed to get account interface 1") + .value + .expect("account interface 1 should exist"); assert!(account_interface_1.is_cold(), "Account 1 should be cold"); let variant_1: LightAccountVariant = @@ -614,9 +620,11 @@ async fn test_d11_multiple_zc() { // Decompress second account let account_interface_2 = ctx .rpc - .get_account_interface(&zc_pda_2, &ctx.program_id) + .get_account_interface(&zc_pda_2, None) .await - .expect("failed to get account interface 2"); + .expect("failed to get account interface 2") + .value + .expect("account interface 2 should exist"); assert!(account_interface_2.is_cold(), "Account 2 should be cold"); let variant_2: LightAccountVariant = @@ -785,9 +793,11 @@ async fn test_d11_mixed_zc_borsh() { // PHASE 4: Decompress zero-copy account let account_interface_zc = ctx .rpc - .get_account_interface(&zc_pda, &ctx.program_id) + .get_account_interface(&zc_pda, None) .await - .expect("failed to get zc account interface"); + .expect("failed to get zc account interface") + .value + .expect("zc account interface should exist"); let variant_zc: LightAccountVariant = csdk_anchor_full_derived_test::csdk_anchor_full_derived_test::ZcMixedRecordSeeds { owner } @@ -814,9 +824,11 @@ async fn test_d11_mixed_zc_borsh() { // Decompress borsh account let account_interface_borsh = ctx .rpc - .get_account_interface(&borsh_pda, &ctx.program_id) + .get_account_interface(&borsh_pda, None) .await - .expect("failed to get borsh account interface"); + .expect("failed to get borsh account interface") + .value + .expect("borsh account interface should exist"); let variant_borsh: LightAccountVariant = csdk_anchor_full_derived_test::csdk_anchor_full_derived_test::BorshRecordSeeds { owner } @@ -975,9 +987,11 @@ async fn test_d11_zc_with_ctx_seeds() { // PHASE 4: Decompress account let account_interface = ctx .rpc - .get_account_interface(&zc_pda, &ctx.program_id) + .get_account_interface(&zc_pda, None) .await - .expect("failed to get account interface"); + .expect("failed to get account interface") + .value + .expect("account interface should exist"); assert!( account_interface.is_cold(), "Account should be cold (compressed)" @@ -1118,9 +1132,11 @@ async fn test_d11_zc_with_params_seeds() { // PHASE 4: Decompress account let account_interface = ctx .rpc - .get_account_interface(&zc_pda, &ctx.program_id) + .get_account_interface(&zc_pda, None) .await - .expect("failed to get account interface"); + .expect("failed to get account interface") + .value + .expect("account interface should exist"); assert!( account_interface.is_cold(), "Account should be cold (compressed)" @@ -1292,9 +1308,11 @@ async fn test_d11_zc_with_mint_to() { // PHASE 4: Decompress account let account_interface = ctx .rpc - .get_account_interface(&zc_pda, &ctx.program_id) + .get_account_interface(&zc_pda, None) .await - .expect("failed to get account interface"); + .expect("failed to get account interface") + .value + .expect("account interface should exist"); assert!( account_interface.is_cold(), "Account should be cold (compressed)" diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/failing_tests.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/failing_tests.rs index 05e8ab04b7..28fda8af8d 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/tests/failing_tests.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/failing_tests.rs @@ -20,8 +20,8 @@ use csdk_anchor_full_derived_test::{ }, }; use light_client::interface::{ - create_load_instructions, get_create_accounts_proof, AccountInterfaceExt, AccountSpec, - CreateAccountsProofInput, InitializeRentFreeConfig, PdaSpec, + create_load_instructions, get_create_accounts_proof, AccountSpec, CreateAccountsProofInput, + InitializeRentFreeConfig, PdaSpec, }; use light_compressible::rent::SLOTS_PER_EPOCH; use light_program_test::{ @@ -176,9 +176,11 @@ async fn test_pda_wrong_rent_sponsor() { // Get account interface let account_interface = ctx .rpc - .get_account_interface(&zc_pda, &ctx.program_id) + .get_account_interface(&zc_pda, None) .await - .expect("failed to get account interface"); + .expect("failed to get account interface") + .value + .expect("account interface should exist"); // Build valid variant let variant: LightAccountVariant = @@ -222,9 +224,11 @@ async fn test_pda_double_decompress_is_noop() { // Get account interface let account_interface = ctx .rpc - .get_account_interface(&zc_pda, &ctx.program_id) + .get_account_interface(&zc_pda, None) .await - .expect("failed to get account interface"); + .expect("failed to get account interface") + .value + .expect("account interface should exist"); let variant: LightAccountVariant = csdk_anchor_full_derived_test::csdk_anchor_full_derived_test::ZcVaultRecordSeeds { owner } @@ -259,9 +263,11 @@ async fn test_pda_double_decompress_is_noop() { // Since the account is now hot, create_load_instructions will return empty let account_interface_2 = ctx .rpc - .get_account_interface(&zc_pda, &ctx.program_id) + .get_account_interface(&zc_pda, None) .await - .expect("failed to get account interface"); + .expect("failed to get account interface") + .value + .expect("account interface should exist"); // Account should be hot now assert!( @@ -305,9 +311,11 @@ async fn test_pda_wrong_config() { // Get account interface let account_interface = ctx .rpc - .get_account_interface(&zc_pda, &ctx.program_id) + .get_account_interface(&zc_pda, None) .await - .expect("failed to get account interface"); + .expect("failed to get account interface") + .value + .expect("account interface should exist"); let variant: LightAccountVariant = csdk_anchor_full_derived_test::csdk_anchor_full_derived_test::ZcVaultRecordSeeds { owner } @@ -353,9 +361,11 @@ async fn test_system_accounts_offset_out_of_bounds() { let account_interface = ctx .rpc - .get_account_interface(&zc_pda, &ctx.program_id) + .get_account_interface(&zc_pda, None) .await - .expect("failed to get account interface"); + .expect("failed to get account interface") + .value + .expect("account interface should exist"); let variant: LightAccountVariant = csdk_anchor_full_derived_test::csdk_anchor_full_derived_test::ZcVaultRecordSeeds { owner } @@ -397,9 +407,11 @@ async fn test_token_accounts_offset_invalid() { let account_interface = ctx .rpc - .get_account_interface(&zc_pda, &ctx.program_id) + .get_account_interface(&zc_pda, None) .await - .expect("failed to get account interface"); + .expect("failed to get account interface") + .value + .expect("account interface should exist"); let variant: LightAccountVariant = csdk_anchor_full_derived_test::csdk_anchor_full_derived_test::ZcVaultRecordSeeds { owner } @@ -446,9 +458,11 @@ async fn test_missing_system_accounts() { let account_interface = ctx .rpc - .get_account_interface(&zc_pda, &ctx.program_id) + .get_account_interface(&zc_pda, None) .await - .expect("failed to get account interface"); + .expect("failed to get account interface") + .value + .expect("account interface should exist"); let variant: LightAccountVariant = csdk_anchor_full_derived_test::csdk_anchor_full_derived_test::ZcVaultRecordSeeds { owner } @@ -489,9 +503,11 @@ async fn test_pda_account_mismatch() { let account_interface = ctx .rpc - .get_account_interface(&zc_pda, &ctx.program_id) + .get_account_interface(&zc_pda, None) .await - .expect("failed to get account interface"); + .expect("failed to get account interface") + .value + .expect("account interface should exist"); let variant: LightAccountVariant = csdk_anchor_full_derived_test::csdk_anchor_full_derived_test::ZcVaultRecordSeeds { owner } @@ -532,9 +548,11 @@ async fn test_fee_payer_not_signer() { let account_interface = ctx .rpc - .get_account_interface(&zc_pda, &ctx.program_id) + .get_account_interface(&zc_pda, None) .await - .expect("failed to get account interface"); + .expect("failed to get account interface") + .value + .expect("account interface should exist"); let variant: LightAccountVariant = csdk_anchor_full_derived_test::csdk_anchor_full_derived_test::ZcVaultRecordSeeds { owner } diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/integration_tests.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/integration_tests.rs index 0363047387..d1b8d82e0f 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/tests/integration_tests.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/integration_tests.rs @@ -10,8 +10,8 @@ mod shared; use anchor_lang::{AnchorDeserialize, InstructionData, ToAccountMetas}; use csdk_anchor_full_derived_test::csdk_anchor_full_derived_test::LightAccountVariant; use light_client::interface::{ - create_load_instructions, get_create_accounts_proof, AccountInterfaceExt, AccountSpec, - CreateAccountsProofInput, InitializeRentFreeConfig, PdaSpec, + create_load_instructions, get_create_accounts_proof, AccountSpec, CreateAccountsProofInput, + InitializeRentFreeConfig, PdaSpec, }; use light_compressible::rent::SLOTS_PER_EPOCH; use light_program_test::{ @@ -124,9 +124,11 @@ impl TestContext { // Get account interface let account_interface = self .rpc - .get_account_interface(pda, &self.program_id) + .get_account_interface(pda, None) .await - .expect("failed to get account interface"); + .expect("failed to get account interface") + .value + .expect("account should exist"); assert!( account_interface.is_cold(), "Account should be cold after compression" @@ -186,9 +188,11 @@ impl TestContext { // Get account interface let account_interface = self .rpc - .get_account_interface(pda, &self.program_id) + .get_account_interface(pda, None) .await - .expect("failed to get account interface"); + .expect("failed to get account interface") + .value + .expect("account interface should exist"); assert!( account_interface.is_cold(), "Account should be cold after compression" @@ -238,9 +242,11 @@ impl TestContext { // Fetch token account interface let vault_interface = self .rpc - .get_token_account_interface(vault_pda) + .get_token_account_interface(vault_pda, None) .await - .expect("get_token_account_interface should succeed"); + .expect("get_token_account_interface should succeed") + .value + .expect("token account interface should exist"); assert!(vault_interface.is_cold(), "Token vault should be cold"); // Deserialize token data @@ -597,8 +603,10 @@ async fn test_d8_multi_rentfree() { // Decompress first account let interface1 = ctx .rpc - .get_account_interface(&pda1, &ctx.program_id) + .get_account_interface(&pda1, None) .await + .unwrap() + .value .unwrap(); let variant1 = D8MultiRecord1Seeds { owner, id1 } .into_variant(&interface1.account.data[8..]) @@ -620,8 +628,10 @@ async fn test_d8_multi_rentfree() { // Decompress second account let interface2 = ctx .rpc - .get_account_interface(&pda2, &ctx.program_id) + .get_account_interface(&pda2, None) .await + .unwrap() + .value .unwrap(); let variant2 = D8MultiRecord2Seeds { owner, id2 } .into_variant(&interface2.account.data[8..]) @@ -736,8 +746,10 @@ async fn test_d8_all() { // Decompress first account (single type) let interface_single = ctx .rpc - .get_account_interface(&pda_single, &ctx.program_id) + .get_account_interface(&pda_single, None) .await + .unwrap() + .value .unwrap(); let variant_single = D8AllSingleSeeds { owner } .into_variant(&interface_single.account.data[8..]) @@ -759,8 +771,10 @@ async fn test_d8_all() { // Decompress second account (multi type) let interface_multi = ctx .rpc - .get_account_interface(&pda_multi, &ctx.program_id) + .get_account_interface(&pda_multi, None) .await + .unwrap() + .value .unwrap(); let variant_multi = D8AllMultiSeeds { owner } .into_variant(&interface_multi.account.data[8..]) @@ -1485,8 +1499,10 @@ async fn test_d9_all() { ) { let interface = ctx .rpc - .get_account_interface(pda, &ctx.program_id) + .get_account_interface(pda, None) .await + .unwrap() + .value .unwrap(); let variant = seeds.into_variant(&interface.account.data[8..]).unwrap(); let spec = PdaSpec::new(interface.clone(), variant, ctx.program_id); @@ -1608,9 +1624,11 @@ async fn test_d8_pda_only_full_lifecycle() { // PHASE 3: Decompress account let account_interface = ctx .rpc - .get_account_interface(&pda, &ctx.program_id) + .get_account_interface(&pda, None) .await - .expect("failed to get account interface"); + .expect("failed to get account interface") + .value + .expect("account should exist"); assert!(account_interface.is_cold(), "Account should be cold"); let variant = D8PdaOnlyRecordSeeds { owner } diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/mint/metadata_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/mint/metadata_test.rs index 18273f21df..47894ba5e1 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/tests/mint/metadata_test.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/mint/metadata_test.rs @@ -5,8 +5,7 @@ mod shared; use anchor_lang::{InstructionData, ToAccountMetas}; use light_client::interface::{ - decompress_mint::decompress_mint, get_create_accounts_proof, AccountInterfaceExt, - CreateAccountsProofInput, + decompress_mint::decompress_mint, get_create_accounts_proof, CreateAccountsProofInput, }; use light_compressible::{rent::SLOTS_PER_EPOCH, DECOMPRESSED_PDA_DISCRIMINATOR}; use light_program_test::{program_test::TestRpc, Indexer, Rpc}; @@ -227,9 +226,11 @@ async fn test_create_mint_with_metadata() { // Fetch mint interface (unified hot/cold handling) // Note: pass the mint PDA (cmint_pda), not the mint signer seed let mint_interface = rpc - .get_mint_interface(&cmint_pda) + .get_mint_interface(&cmint_pda, None) .await - .expect("get_mint_interface should succeed"); + .expect("get_mint_interface should succeed") + .value + .expect("mint interface should exist"); assert!(mint_interface.is_cold(), "Mint should be cold after warp"); // Create decompression instruction using decompress_mint helper diff --git a/sdk-tests/justfile b/sdk-tests/justfile index 27c1d0a969..9cbf9ae952 100644 --- a/sdk-tests/justfile +++ b/sdk-tests/justfile @@ -10,3 +10,4 @@ test: RUSTFLAGS="-D warnings" cargo test-sbf -p sdk-native-test RUSTFLAGS="-D warnings" cargo test-sbf -p sdk-anchor-test RUSTFLAGS="-D warnings" cargo test-sbf -p sdk-token-test + RUSTFLAGS="-D warnings" cargo test-sbf -p csdk-anchor-full-derived-test diff --git a/sdk-tests/manual-test/tests/account_loader.rs b/sdk-tests/manual-test/tests/account_loader.rs index bcb345011c..501217b161 100644 --- a/sdk-tests/manual-test/tests/account_loader.rs +++ b/sdk-tests/manual-test/tests/account_loader.rs @@ -7,8 +7,8 @@ mod shared; use anchor_lang::{Discriminator, InstructionData, ToAccountMetas}; use light_client::interface::{ - create_load_instructions, get_create_accounts_proof, AccountInterfaceExt, AccountSpec, - CreateAccountsProofInput, PdaSpec, + create_load_instructions, get_create_accounts_proof, AccountSpec, CreateAccountsProofInput, + PdaSpec, }; use light_compressible::rent::SLOTS_PER_EPOCH; use light_program_test::{program_test::TestRpc, Indexer, Rpc}; @@ -118,9 +118,11 @@ async fn test_zero_copy_create_compress_decompress() { // PHASE 4: Decompress account let account_interface = rpc - .get_account_interface(&record_pda, &program_id) + .get_account_interface(&record_pda, None) .await - .expect("failed to get account interface"); + .expect("failed to get account interface") + .value + .expect("account interface should exist"); assert!( account_interface.is_cold(), "Account should be cold (compressed)" diff --git a/sdk-tests/manual-test/tests/test.rs b/sdk-tests/manual-test/tests/test.rs index 6bf6314a94..fe94962b81 100644 --- a/sdk-tests/manual-test/tests/test.rs +++ b/sdk-tests/manual-test/tests/test.rs @@ -6,8 +6,8 @@ mod shared; use anchor_lang::{InstructionData, ToAccountMetas}; use light_client::interface::{ - create_load_instructions, get_create_accounts_proof, AccountInterfaceExt, AccountSpec, - CreateAccountsProofInput, PdaSpec, + create_load_instructions, get_create_accounts_proof, AccountSpec, CreateAccountsProofInput, + PdaSpec, }; use light_compressible::rent::SLOTS_PER_EPOCH; use light_program_test::{program_test::TestRpc, Indexer, Rpc}; @@ -116,9 +116,11 @@ async fn test_create_compress_decompress() { // PHASE 4: Decompress account let account_interface = rpc - .get_account_interface(&record_pda, &program_id) + .get_account_interface(&record_pda, None) .await - .expect("failed to get account interface"); + .expect("failed to get account interface") + .value + .expect("account interface should exist"); assert!( account_interface.is_cold(), "Account should be cold (compressed)" diff --git a/sdk-tests/single-account-loader-test/tests/test.rs b/sdk-tests/single-account-loader-test/tests/test.rs index bea7332de5..ea1314c0de 100644 --- a/sdk-tests/single-account-loader-test/tests/test.rs +++ b/sdk-tests/single-account-loader-test/tests/test.rs @@ -2,8 +2,8 @@ use anchor_lang::{InstructionData, ToAccountMetas}; use light_client::interface::{ - create_load_instructions, get_create_accounts_proof, AccountInterfaceExt, AccountSpec, - CreateAccountsProofInput, InitializeRentFreeConfig, PdaSpec, + create_load_instructions, get_create_accounts_proof, AccountSpec, CreateAccountsProofInput, + InitializeRentFreeConfig, PdaSpec, }; use light_compressible::rent::SLOTS_PER_EPOCH; use light_program_test::{ @@ -240,9 +240,11 @@ async fn test_zero_copy_record_full_lifecycle() { // PHASE 4: Decompress account let account_interface = rpc - .get_account_interface(&record_pda, &program_id) + .get_account_interface(&record_pda, None) .await - .expect("failed to get account interface"); + .expect("failed to get account interface") + .value + .expect("account interface should exist"); assert!( account_interface.is_cold(), "Account should be cold (compressed)"