Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cli/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ test-ledger
/bin
/config.json
/src/utils/proverVersion.generated.ts
/src/utils/photonVersion.generated.ts
5 changes: 3 additions & 2 deletions cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,9 @@
"scripts": {
"postinstall": "[ -d ./bin ] && find ./bin -type f -exec chmod +x {} + || echo 'No bin directory found, skipping chmod'",
"sync-prover-version": "./scripts/syncProverVersion.sh",
"build": "shx rm -rf dist && pnpm sync-prover-version && pnpm tsc -p tsconfig.json && pnpm tsc -p tsconfig.test.json",
"build-release": "shx rm -rf dist && pnpm sync-prover-version && pnpm tsc -p tsconfig.json && pnpm tsc -p tsconfig.test.json",
"sync-photon-version": "./scripts/syncPhotonVersion.sh",
"build": "shx rm -rf dist && pnpm sync-prover-version && pnpm sync-photon-version && pnpm tsc -p tsconfig.json && pnpm tsc -p tsconfig.test.json",
"build-release": "shx rm -rf dist && pnpm sync-prover-version && pnpm sync-photon-version && pnpm tsc -p tsconfig.json && pnpm tsc -p tsconfig.test.json",
"format": "pnpm prettier --write \"src/**/*.{ts,js}\" \"test/**/*.{ts,js}\" -w",
"format:check": "pnpm prettier \"src/**/*{ts,js}\" \"test/**/*.{ts,js}\" --check",
"lint": "eslint .",
Expand Down
35 changes: 35 additions & 0 deletions cli/scripts/syncPhotonVersion.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#!/bin/bash
# Syncs the photon version and commit from the external/photon submodule to a TypeScript constant.
# This script is run as part of the CLI build process.

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CLI_DIR="$(dirname "$SCRIPT_DIR")"
REPO_ROOT="$(dirname "$CLI_DIR")"

PHOTON_DIR="$REPO_ROOT/external/photon"
OUTPUT_FILE="$CLI_DIR/src/utils/photonVersion.generated.ts"

if [ ! -d "$PHOTON_DIR" ]; then
echo "Error: photon submodule not found at $PHOTON_DIR"
echo " Run: git submodule update --init external/photon"
exit 1
fi

VERSION=$(grep '^version' "$PHOTON_DIR/Cargo.toml" | head -1 | sed 's/.*"\(.*\)".*/\1/')
COMMIT=$(git -C "$PHOTON_DIR" rev-parse HEAD 2>/dev/null)

if [ -z "$VERSION" ] || [ -z "$COMMIT" ]; then
echo "Error: Could not extract version or commit from photon submodule"
exit 1
fi

REPO="https://github.com/lightprotocol/photon.git"

cat > "$OUTPUT_FILE" << EOF
// Auto-generated from external/photon submodule - do not edit manually
export const PHOTON_VERSION = "$VERSION";
export const PHOTON_GIT_COMMIT = "$COMMIT";
export const PHOTON_GIT_REPO = "$REPO";
EOF

echo "Synced photon version $VERSION (commit $COMMIT) to $OUTPUT_FILE"
8 changes: 2 additions & 6 deletions cli/src/utils/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,8 @@ export const FORESTER_PROCESS_NAME = "forester";
export const SURFPOOL_VERSION = "1.0.1";
export const SURFPOOL_RELEASE_TAG = "v1.0.1-light";

export const PHOTON_VERSION = "0.51.2";

// Set these to override Photon requirements with a specific git commit:
export const USE_PHOTON_FROM_GIT = true; // If true, will show git install command instead of crates.io.
export const PHOTON_GIT_REPO = "https://github.com/lightprotocol/photon.git";
export const PHOTON_GIT_COMMIT = "32e9ae60926a0d614ad444b799d15c15c02f2ef7"; // If empty, will use main branch.
// PHOTON_VERSION, PHOTON_GIT_COMMIT, and PHOTON_GIT_REPO are auto-generated
// from the external/photon submodule at build time. See photonVersion.generated.ts.
export const LIGHT_PROTOCOL_PROGRAMS_DIR_ENV = "LIGHT_PROTOCOL_PROGRAMS_DIR";
export const BASE_PATH = "../../bin/";

Expand Down
13 changes: 3 additions & 10 deletions cli/src/utils/processPhotonIndexer.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import which from "which";
import { killProcess, spawnBinary, waitForServers } from "./process";
import { INDEXER_PROCESS_NAME } from "./constants";
import {
INDEXER_PROCESS_NAME,
PHOTON_VERSION,
USE_PHOTON_FROM_GIT,
PHOTON_GIT_REPO,
PHOTON_GIT_COMMIT,
} from "./constants";
} from "./photonVersion.generated";
import { exec } from "node:child_process";
import * as util from "node:util";
import { exit } from "node:process";
Expand All @@ -27,13 +26,7 @@ async function isExpectedPhotonVersion(
}

function getPhotonInstallMessage(): string {
if (USE_PHOTON_FROM_GIT && PHOTON_GIT_COMMIT) {
return `\nPhoton indexer ${PHOTON_VERSION} (commit ${PHOTON_GIT_COMMIT}) not found. Please install it by running: "cargo install --git ${PHOTON_GIT_REPO} --rev ${PHOTON_GIT_COMMIT} --locked --force"`;
} else if (USE_PHOTON_FROM_GIT) {
return `\nPhoton indexer ${PHOTON_VERSION} not found. Please install it by running: "cargo install --git ${PHOTON_GIT_REPO} --locked --force"`;
} else {
return `\nPhoton indexer ${PHOTON_VERSION} not found. Please install it by running: "cargo install photon-indexer --version ${PHOTON_VERSION} --locked --force"`;
}
return `\nPhoton indexer ${PHOTON_VERSION} (commit ${PHOTON_GIT_COMMIT}) not found. Please install it by running: "cargo install --git ${PHOTON_GIT_REPO} --rev ${PHOTON_GIT_COMMIT} --locked --force"`;
}

export async function startIndexer(
Expand Down
2 changes: 1 addition & 1 deletion external/photon
Submodule photon updated 30 files
+54 −31 Cargo.lock
+14 −11 Cargo.toml
+3 −55 src/api/api.rs
+0 −1 src/api/method/get_validity_proof/prover/structs.rs
+0 −67 src/api/method/interface/get_account_interfaces.rs
+0 −481 src/api/method/interface/get_ata_interface.rs
+42 −88 src/api/method/interface/get_multiple_account_interfaces.rs
+0 −389 src/api/method/interface/get_token_account_interface.rs
+0 −67 src/api/method/interface/get_token_account_interfaces.rs
+0 −8 src/api/method/interface/mod.rs
+545 −675 src/api/method/interface/racing.rs
+10 −291 src/api/method/interface/types.rs
+0 −39 src/api/rpc_server.rs
+1 −0 src/common/mod.rs
+18 −0 src/common/token_layout.rs
+4 −1 src/common/typedefs/account/mod.rs
+5 −1 src/common/typedefs/account/v2.rs
+240 −1 src/common/typedefs/token_data.rs
+2 −2 src/ingester/parser/indexer_events.rs
+2 −2 src/ingester/parser/tx_event_parser.rs
+116 −12 src/ingester/persist/mod.rs
+86 −22 src/migration/migrations/standard/m20260201_000001_add_discriminator_blob.rs
+0 −27 src/migration/migrations/standard/m20260203_000001_drop_mints_table.rs
+131 −0 src/migration/migrations/standard/m20260210_000001_backfill_mint_onchain_pubkey.rs
+56 −0 src/migration/migrations/standard/m20260210_000002_add_ata_owner_index.rs
+4 −2 src/migration/migrations/standard/mod.rs
+24 −9 src/monitor/mod.rs
+2 −14 src/openapi/mod.rs
+8 −624 src/openapi/specs/api.yaml
+22 −66 tests/integration_tests/interface_tests.rs
22 changes: 16 additions & 6 deletions scripts/devenv/install-photon.sh
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,23 @@ sed_inplace() {
}

install_photon() {
local expected_version="${PHOTON_VERSION}"
local expected_commit="${PHOTON_COMMIT}"
local photon_path="${REPO_ROOT}/external/photon"

# Ensure photon submodule is initialized and up to date
echo "Updating photon submodule..."
cd "${REPO_ROOT}"
git submodule update --init --recursive external/photon
cd "${SCRIPT_DIR}"

# Derive version and commit from the actual submodule state (after init)
local expected_version
expected_version=$(grep '^version' "${photon_path}/Cargo.toml" | head -1 | sed 's/.*"\(.*\)".*/\1/')
local expected_commit
expected_commit=$(git -C "${photon_path}" rev-parse HEAD)
local install_marker="photon:${expected_version}:${expected_commit}"

# Validate required variables
if [ -z "${expected_version}" ] || [ -z "${expected_commit}" ]; then
echo "ERROR: PHOTON_VERSION or PHOTON_COMMIT not set in versions.sh"
echo "ERROR: Could not derive version or commit from external/photon submodule."
exit 1
fi

Expand All @@ -45,8 +55,8 @@ install_photon() {
sed_inplace "/^photon:/d" "$INSTALL_LOG" 2>/dev/null || true
sed_inplace "/^photon$/d" "$INSTALL_LOG" 2>/dev/null || true

echo "Installing Photon indexer ${expected_version} (commit ${expected_commit})..."
RUSTFLAGS="-A dead-code" cargo install --git https://github.com/helius-labs/photon.git --rev ${expected_commit} --locked --force
echo "Installing Photon indexer ${expected_version} (commit ${expected_commit}) from submodule..."
RUSTFLAGS="-A dead-code" cargo install --path "${photon_path}" --locked --force

# Verify installation succeeded
if [ ! -f "${PREFIX}/cargo/bin/photon" ]; then
Expand Down
4 changes: 2 additions & 2 deletions scripts/devenv/versions.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@ REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || echo "${PWD}")"
export RUST_VERSION=$(grep 'channel' "${REPO_ROOT}/rust-toolchain.toml" | sed 's/.*"\(.*\)".*/\1/' | cut -d'.' -f1,2)
export GO_VERSION=$(grep '^go ' "${REPO_ROOT}/prover/server/go.mod" | awk '{print $2}')
export PNPM_VERSION=$(grep 'packageManager' "${REPO_ROOT}/package.json" | sed 's/.*pnpm@\([^"]*\).*/\1/')
export PHOTON_COMMIT=$(git -C "${REPO_ROOT}" rev-parse HEAD:external/photon 2>/dev/null || echo "unknown")
export PHOTON_VERSION=$(grep '^version' "${REPO_ROOT}/external/photon/Cargo.toml" 2>/dev/null | head -1 | sed 's/.*"\(.*\)".*/\1/')

# Versions to bump manually (edit below)
export NODE_VERSION="22.16.0"
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="301153a04c3232413198098a0a6725207ce36298"
export REDIS_VERSION="8.0.1"

export ANCHOR_TAG="anchor-v${ANCHOR_VERSION}"
Expand Down
116 changes: 38 additions & 78 deletions sdk-libs/client/src/indexer/photon_indexer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1716,11 +1716,8 @@ impl Indexer for PhotonIndexer {
}

// ============ Interface Methods ============
// These methods use the Interface endpoints that race hot (on-chain) and cold (compressed) lookups

// 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,
Expand Down Expand Up @@ -1764,99 +1761,46 @@ impl PhotonIndexer {
.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<IndexerRpcConfig>,
) -> Result<Response<Option<TokenAccountInterface>>, IndexerError> {
let config = config.unwrap_or_default();
self.retry(config.retry_config, || async {
let params = photon_api::types::PostGetTokenAccountInterfaceBodyParams {
address: photon_api::types::SerializablePubkey(address.to_string()),
};
let request =
photon_api::apis::default_api::make_get_token_account_interface_body(params);

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,
)?;

if api_response.context.slot < config.slot {
return Err(IndexerError::IndexerNotSyncedToSlot);
let response = self.get_account_interface(address, config).await?;
let value = match response.value {
Some(ai) => {
let token = parse_token_data_from_indexer_account(&ai)?;
Some(TokenAccountInterface { account: ai, token })
}

let account = match api_response.value {
Some(ref tai) => Some(TokenAccountInterface::try_from(tai)?),
None => None,
};

Ok(Response {
context: Context {
slot: api_response.context.slot,
},
value: account,
})
None => None,
};
Ok(Response {
context: response.context,
value,
})
.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_associated_token_account_interface(
&self,
owner: &Pubkey,
mint: &Pubkey,
config: Option<IndexerRpcConfig>,
) -> Result<Response<Option<TokenAccountInterface>>, IndexerError> {
let config = config.unwrap_or_default();
self.retry(config.retry_config, || async {
let params = photon_api::types::PostGetAtaInterfaceBodyParams {
owner: photon_api::types::SerializablePubkey(owner.to_string()),
mint: photon_api::types::SerializablePubkey(mint.to_string()),
};
let request = photon_api::apis::default_api::make_get_ata_interface_body(params);

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_associated_token_account_interface",
result.error,
result.result,
)?;

if api_response.context.slot < config.slot {
return Err(IndexerError::IndexerNotSyncedToSlot);
let ata_address = light_token::instruction::get_associated_token_address(owner, mint);
let response = self.get_account_interface(&ata_address, config).await?;
let value = match response.value {
Some(ai) => {
let token = parse_token_data_from_indexer_account(&ai)?;
Some(TokenAccountInterface { account: ai, token })
}

let account = match api_response.value {
Some(ref tai) => Some(TokenAccountInterface::try_from(tai)?),
None => None,
};

Ok(Response {
context: Context {
slot: api_response.context.slot,
},
value: account,
})
None => None,
};
Ok(Response {
context: response.context,
value,
})
.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>,
Expand Down Expand Up @@ -1909,3 +1853,19 @@ impl PhotonIndexer {
.await
}
}

/// Parse token data from an indexer AccountInterface.
/// For compressed (cold) accounts: borsh-deserializes TokenData from the cold data bytes.
/// For on-chain (hot) accounts: returns default TokenData (downstream conversion re-parses from SPL layout).
fn parse_token_data_from_indexer_account(
ai: &AccountInterface,
) -> Result<light_token::compat::TokenData, IndexerError> {
match &ai.cold {
Some(cold) => borsh::BorshDeserialize::deserialize(&mut cold.data.data.as_slice())
.map_err(|e| IndexerError::decode_error("token_data", e)),
None => {
// Hot account — downstream will re-parse from SPL account data directly
Ok(light_token::compat::TokenData::default())
}
}
}
Loading
Loading