Skip to content
Open
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
2,029 changes: 1,695 additions & 334 deletions Cargo.lock

Large diffs are not rendered by default.

21 changes: 15 additions & 6 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -84,16 +84,25 @@ solana-bn254 = "3.1.2"
solana-commitment-config = "3.0.0"

solana-transaction-status = "3.0.8"
solana-program-option = "3.0.0"
solana-program-pack = "3.0.0"
spl-token-interface = "2.0.0"

light-zero-copy = { version = "0.5",default-features = false}


light-zero-copy = { version = "0.6", default-features = false}
light-concurrent-merkle-tree = {version = "4", default-features = false }
light-batched-merkle-tree = { version = "0.6", default-features = false}
light-merkle-tree-metadata = { version = "0.6", default-features = false }
light-compressed-account = { version = "0.6", default-features = false }
light-batched-merkle-tree = { version = "0.10", default-features = false}
light-merkle-tree-metadata = { version = "0.10", default-features = false }
light-compressed-account = { version = "0.10.1", default-features = false }
light-hasher = { version = "5", features = ["poseidon", "keccak", "sha256"], default-features = false}
light-poseidon = "0.3.0"
light-poseidon = "0.4.0"
light-indexed-merkle-tree = { version = "4", default-features = false }
light-event = "0.1"
light-event = "0.21"

light-token-interface = "0.4"
light-sdk-types = "0.20"


sqlx = { version = "0.6.2", features = [
"macros",
Expand Down
50 changes: 48 additions & 2 deletions src/api/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,11 @@ use crate::api::method::get_validity_proof::{
GetValidityProofRequestDocumentation, GetValidityProofRequestV2, GetValidityProofResponse,
GetValidityProofResponseV2,
};
use crate::api::method::interface::{
get_account_interface, get_multiple_account_interfaces, GetAccountInterfaceRequest,
GetAccountInterfaceResponse, GetMultipleAccountInterfacesRequest,
GetMultipleAccountInterfacesResponse,
};
use crate::api::method::utils::{
AccountBalanceResponse, GetLatestSignaturesRequest, GetNonPaginatedSignaturesResponse,
GetNonPaginatedSignaturesResponseWithError, GetPaginatedSignaturesResponse, HashRequest,
Expand All @@ -101,18 +106,21 @@ pub struct PhotonApi {
db_conn: Arc<DatabaseConnection>,
rpc_client: Arc<RpcClient>,
prover_url: String,
prover_api_key: Option<String>,
}

impl PhotonApi {
pub fn new(
db_conn: Arc<DatabaseConnection>,
rpc_client: Arc<RpcClient>,
prover_url: String,
prover_api_key: Option<String>,
) -> Self {
Self {
db_conn,
rpc_client,
prover_url,
prover_api_key,
}
}
}
Expand Down Expand Up @@ -363,14 +371,26 @@ impl PhotonApi {
&self,
request: GetValidityProofRequest,
) -> Result<GetValidityProofResponse, PhotonApiError> {
get_validity_proof(self.db_conn.as_ref(), &self.prover_url, request).await
get_validity_proof(
self.db_conn.as_ref(),
&self.prover_url,
self.prover_api_key.as_deref(),
request,
)
.await
}

pub async fn get_validity_proof_v2(
&self,
request: GetValidityProofRequestV2,
) -> Result<GetValidityProofResponseV2, PhotonApiError> {
get_validity_proof_v2(self.db_conn.as_ref(), &self.prover_url, request).await
get_validity_proof_v2(
self.db_conn.as_ref(),
&self.prover_url,
self.prover_api_key.as_deref(),
request,
)
.await
}

pub async fn get_latest_compression_signatures(
Expand All @@ -387,6 +407,21 @@ impl PhotonApi {
get_latest_non_voting_signatures(self.db_conn.as_ref(), request).await
}

// Interface endpoints - race hot (on-chain) and cold (compressed) lookups
pub async fn get_account_interface(
&self,
request: GetAccountInterfaceRequest,
) -> Result<GetAccountInterfaceResponse, PhotonApiError> {
get_account_interface(&self.db_conn, &self.rpc_client, request).await
}

pub async fn get_multiple_account_interfaces(
&self,
request: GetMultipleAccountInterfacesRequest,
) -> Result<GetMultipleAccountInterfacesResponse, PhotonApiError> {
get_multiple_account_interfaces(&self.db_conn, &self.rpc_client, request).await
}

pub fn method_api_specs() -> Vec<OpenApiSpec> {
vec![
OpenApiSpec {
Expand Down Expand Up @@ -576,6 +611,17 @@ impl PhotonApi {
request: None,
response: UnsignedInteger::schema().1,
},
// Interface endpoints
OpenApiSpec {
name: "getAccountInterface".to_string(),
request: Some(GetAccountInterfaceRequest::schema().1),
response: GetAccountInterfaceResponse::schema().1,
},
OpenApiSpec {
name: "getMultipleAccountInterfaces".to_string(),
request: Some(GetMultipleAccountInterfacesRequest::schema().1),
response: GetMultipleAccountInterfacesResponse::schema().1,
},
]
}
}
2 changes: 1 addition & 1 deletion src/api/method/get_compressed_accounts_by_owner/v1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ pub async fn get_compressed_accounts_by_owner(
query_builder.build_base_query(conn, &request)?;

let columns = format!(
"hash, {}, data_hash, address, owner, tree, leaf_index, seq, slot_created, spent, prev_spent, lamports, discriminator, queue, in_output_queue, nullifier_queue_index, nullified_in_tree, nullifier, tx_hash, tree_type",
"hash, {}, data_hash, address, onchain_pubkey, owner, tree, leaf_index, seq, slot_created, spent, prev_spent, lamports, discriminator, discriminator_v2, queue, in_output_queue, nullifier_queue_index, nullified_in_tree, nullifier, tx_hash, tree_type",
query_builder.data_column
);

Expand Down
2 changes: 1 addition & 1 deletion src/api/method/get_compressed_accounts_by_owner/v2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ pub async fn get_compressed_accounts_by_owner_v2(
query_builder.build_base_query(conn, &request)?;

let columns = format!(
"hash, {}, data_hash, address, owner, tree, queue, in_output_queue, nullifier_queue_index, tx_hash, nullifier, leaf_index, seq, slot_created, spent, prev_spent, lamports, discriminator, nullified_in_tree, tree_type",
"hash, {}, data_hash, address, onchain_pubkey, owner, tree, queue, in_output_queue, nullifier_queue_index, tx_hash, nullifier, leaf_index, seq, slot_created, spent, prev_spent, lamports, discriminator, discriminator_v2, nullified_in_tree, tree_type",
query_builder.data_column
);

Expand Down
3 changes: 1 addition & 2 deletions src/api/method/get_compressed_token_balances_by_owner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,7 @@ pub async fn get_compressed_token_balances_by_owner(
// Only use mint > cursor_mint if we're not filtering by a specific mint
// If filtering by a specific mint, the cursor should be ignored or we'd get no results
if mint.is_none() {
filter =
filter.and(token_owner_balances::Column::Mint.gt::<Vec<u8>>(cursor_mint.into()));
filter = filter.and(token_owner_balances::Column::Mint.gt::<Vec<u8>>(cursor_mint));
}
// If a specific mint is provided, we can't paginate within that mint
// because there's only one record per owner-mint combination in token_owner_balances
Expand Down
4 changes: 1 addition & 3 deletions src/api/method/get_multiple_new_address_proofs.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
use light_compressed_account::TreeType;
use light_sdk_types::constants::ADDRESS_TREE_V1;
use sea_orm::{
ConnectionTrait, DatabaseConnection, DatabaseTransaction, Statement, TransactionTrait,
};
use serde::{Deserialize, Serialize};
use solana_pubkey::{pubkey, Pubkey};
use utoipa::ToSchema;

use crate::api::error::PhotonApiError;
Expand All @@ -17,8 +17,6 @@ use std::collections::HashMap;

pub const MAX_ADDRESSES: usize = 1000;

pub const ADDRESS_TREE_V1: Pubkey = pubkey!("amt1Ayt45jfbdw5YSo7iz6WZxUmnZsQTYXy82hVwyC2");

#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, PartialEq, Eq)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
#[allow(non_snake_case)]
Expand Down
82 changes: 55 additions & 27 deletions src/api/method/get_queue_elements.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ use utoipa::ToSchema;
const MAX_QUEUE_ELEMENTS: u16 = 30_000;
const MAX_QUEUE_ELEMENTS_SQLITE: u16 = 500;

const MAX_SQL_PARAMS: usize = 30_000;

/// Encode tree node position as a single u64
/// Format: [level: u8][position: 56 bits]
/// Level 0 = leaves, Level tree_height-1 = root
Expand Down Expand Up @@ -236,13 +238,26 @@ pub async fn get_queue_elements(
let (nodes, initial_root, root_seq) =
merge_state_queue_proofs(&output_proof_data, &input_proof_data)?;

Some(StateQueueData {
nodes,
initial_root,
root_seq,
output_queue,
input_queue,
})
let has_output_data = output_queue
.as_ref()
.map(|oq| !oq.leaf_indices.is_empty())
.unwrap_or(false);
let has_input_data = input_queue
.as_ref()
.map(|iq| !iq.leaf_indices.is_empty())
.unwrap_or(false);

if has_output_data || has_input_data {
Some(StateQueueData {
nodes,
initial_root,
root_seq,
output_queue,
input_queue,
})
} else {
None
}
} else {
None
};
Expand Down Expand Up @@ -821,12 +836,16 @@ async fn fetch_address_queue_v2(
let mut current_hash = hashed_leaf;

for (level, sibling_hash) in proof.proof.iter().enumerate() {
let sibling_pos = if pos % 2 == 0 { pos + 1 } else { pos - 1 };
let sibling_pos = if pos.is_multiple_of(2) {
pos + 1
} else {
pos - 1
};

let sibling_idx = encode_node_index(level as u8, sibling_pos, tree_info.height as u8);
nodes_map.insert(sibling_idx, sibling_hash.clone());

let parent_hash = if pos % 2 == 0 {
let parent_hash = if pos.is_multiple_of(2) {
Poseidon::hashv(&[&current_hash.0, &sibling_hash.0])
} else {
Poseidon::hashv(&[&sibling_hash.0, &current_hash.0])
Expand Down Expand Up @@ -982,7 +1001,11 @@ fn deduplicate_nodes_from_refs(

// Walk up the proof path, storing sibling hashes and path node hashes from DB
for (level, sibling_hash) in proof_ctx.proof.iter().enumerate() {
let sibling_pos = if pos % 2 == 0 { pos + 1 } else { pos - 1 };
let sibling_pos = if pos.is_multiple_of(2) {
pos + 1
} else {
pos - 1
};

// Store the sibling (from proof)
let sibling_idx = encode_node_index(level as u8, sibling_pos, tree_height);
Expand Down Expand Up @@ -1052,24 +1075,29 @@ async fn fetch_path_nodes_from_db(
return Ok(HashMap::new());
}

let path_nodes = state_trees::Entity::find()
.filter(
state_trees::Column::Tree
.eq(tree_bytes)
.and(state_trees::Column::NodeIdx.is_in(all_path_indices)),
)
.all(tx)
.await
.map_err(|e| {
PhotonApiError::UnexpectedError(format!("Failed to fetch path nodes: {}", e))
})?;

let mut result = HashMap::new();
for node in path_nodes {
let hash = Hash::try_from(node.hash).map_err(|e| {
PhotonApiError::UnexpectedError(format!("Invalid hash in path node: {}", e))
})?;
result.insert(node.node_idx, hash);

let tree_bytes_ref = tree_bytes.clone();

for chunk in all_path_indices.chunks(MAX_SQL_PARAMS) {
let path_nodes = state_trees::Entity::find()
.filter(
state_trees::Column::Tree
.eq(tree_bytes_ref.clone())
.and(state_trees::Column::NodeIdx.is_in(chunk.to_vec())),
)
.all(tx)
.await
.map_err(|e| {
PhotonApiError::UnexpectedError(format!("Failed to fetch path nodes: {}", e))
})?;

for node in path_nodes {
let hash = Hash::try_from(node.hash).map_err(|e| {
PhotonApiError::UnexpectedError(format!("Invalid hash in path node: {}", e))
})?;
result.insert(node.node_idx, hash);
}
}

Ok(result)
Expand Down
10 changes: 6 additions & 4 deletions src/api/method/get_transaction_with_compression_info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@ pub async fn get_transaction_helper(
conn: &DatabaseConnection,
signature: SerializableSignature,
txn: EncodedConfirmedTransactionWithStatusMeta,
rpc_client: &RpcClient,
) -> Result<GetTransactionResponse, PhotonApiError> {
// Ignore if tx failed or meta is missed
let meta = txn.transaction.meta.as_ref();
Expand All @@ -223,7 +224,7 @@ pub async fn get_transaction_helper(
let tx_info: TransactionInfo = clone_tx(&txn).try_into().map_err(|_e| {
PhotonApiError::UnexpectedError(format!("Failed to convert transaction {}", signature.0))
})?;
let status_update = parse_transaction(conn, &tx_info, slot)
let status_update = parse_transaction(conn, &tx_info, slot, rpc_client)
.await
.map_err(|_e| {
PhotonApiError::UnexpectedError(format!("Failed to parse transaction {}", signature.0))
Expand Down Expand Up @@ -276,7 +277,7 @@ pub async fn get_transaction_with_compression_info(
request: GetTransactionRequest,
) -> Result<GetTransactionResponse, PhotonApiError> {
let txn = fetch_transaction_from_rpc(rpc_client, &request.signature).await?;
get_transaction_helper(conn, request.signature, txn).await
get_transaction_helper(conn, request.signature, txn, rpc_client).await
}

fn parse_optional_token_data_v2(
Expand Down Expand Up @@ -339,6 +340,7 @@ pub async fn get_transaction_helper_v2(
conn: &DatabaseConnection,
signature: SerializableSignature,
txn: EncodedConfirmedTransactionWithStatusMeta,
rpc_client: &RpcClient,
) -> Result<GetTransactionResponseV2, PhotonApiError> {
// Ignore if tx failed or meta is missed
let meta = txn.transaction.meta.as_ref();
Expand All @@ -353,7 +355,7 @@ pub async fn get_transaction_helper_v2(
PhotonApiError::UnexpectedError(format!("Failed to convert transaction {}", signature.0))
})?;

let status_update = parse_transaction(conn, &tx_info, slot)
let status_update = parse_transaction(conn, &tx_info, slot, rpc_client)
.await
.map_err(|_e| {
PhotonApiError::UnexpectedError(format!("Failed to parse transaction {}", signature.0))
Expand Down Expand Up @@ -395,5 +397,5 @@ pub async fn get_transaction_with_compression_info_v2(
request: GetTransactionRequest,
) -> Result<GetTransactionResponseV2, PhotonApiError> {
let txn = fetch_transaction_from_rpc(rpc_client, &request.signature).await?;
get_transaction_helper_v2(conn, request.signature, txn).await
get_transaction_helper_v2(conn, request.signature, txn, rpc_client).await
}
Loading