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
31 changes: 31 additions & 0 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 Down Expand Up @@ -402,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 @@ -591,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,
},
]
}
}
22 changes: 22 additions & 0 deletions src/api/method/interface/get_account_interface.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
use sea_orm::DatabaseConnection;
use solana_client::nonblocking::rpc_client::RpcClient;

use crate::api::error::PhotonApiError;
use crate::common::typedefs::context::Context;

use super::racing::race_hot_cold;
use super::types::{GetAccountInterfaceRequest, GetAccountInterfaceResponse};

/// 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(
conn: &DatabaseConnection,
rpc_client: &RpcClient,
request: GetAccountInterfaceRequest,
) -> Result<GetAccountInterfaceResponse, PhotonApiError> {
let context = Context::extract(conn).await?;

let value = race_hot_cold(rpc_client, conn, &request.address, None).await?;

Ok(GetAccountInterfaceResponse { context, value })
}
107 changes: 107 additions & 0 deletions src/api/method/interface/get_multiple_account_interfaces.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
use sea_orm::DatabaseConnection;
use solana_client::nonblocking::rpc_client::RpcClient;
use tokio::sync::Semaphore;

use crate::api::error::PhotonApiError;
use crate::common::typedefs::context::Context;
use crate::common::typedefs::serializable_pubkey::SerializablePubkey;

use super::racing::{get_distinct_owners_with_addresses, race_hot_cold};
use super::types::{
AccountInterface, GetMultipleAccountInterfacesRequest, GetMultipleAccountInterfacesResponse,
MAX_BATCH_SIZE,
};

/// Maximum concurrent hot+cold lookups per batch request.
const MAX_CONCURRENT_LOOKUPS: usize = 20;

/// Get multiple account data from either on-chain or compressed sources.
/// Returns one unified AccountInterface shape for every input pubkey.
pub async fn get_multiple_account_interfaces(
conn: &DatabaseConnection,
rpc_client: &RpcClient,
request: GetMultipleAccountInterfacesRequest,
) -> Result<GetMultipleAccountInterfacesResponse, PhotonApiError> {
if request.addresses.len() > MAX_BATCH_SIZE {
return Err(PhotonApiError::ValidationError(format!(
"Batch size {} exceeds maximum of {}",
request.addresses.len(),
MAX_BATCH_SIZE
)));
}

if request.addresses.is_empty() {
return Err(PhotonApiError::ValidationError(
"At least one address must be provided".to_string(),
));
}

let context = Context::extract(conn).await?;

let distinct_owners = get_distinct_owners_with_addresses(conn)
.await
.map_err(PhotonApiError::DatabaseError)?;

let semaphore = Semaphore::new(MAX_CONCURRENT_LOOKUPS);
let futures: Vec<_> = request
.addresses
.iter()
.map(|address| async {
let _permit = semaphore.acquire().await.unwrap();
race_hot_cold(rpc_client, conn, address, Some(&distinct_owners)).await
})
.collect();

let results = futures::future::join_all(futures).await;

let value = collect_batch_results(&request.addresses, results)?;

Ok(GetMultipleAccountInterfacesResponse { context, value })
}

fn collect_batch_results(
addresses: &[SerializablePubkey],
results: Vec<Result<Option<AccountInterface>, PhotonApiError>>,
) -> Result<Vec<Option<AccountInterface>>, PhotonApiError> {
let mut value = Vec::with_capacity(results.len());
for (i, result) in results.into_iter().enumerate() {
match result {
// Includes Ok(None): account not found is returned as None.
Ok(account) => value.push(account),
// Only actual lookup failures abort the entire batch call.
Err(e) => {
log::error!(
"Failed to fetch interface for address {:?} (index {}): {:?}",
addresses.get(i),
i,
e
);
return Err(e);
}
}
}
Ok(value)
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn collect_batch_results_keeps_none_for_not_found_accounts() {
let addresses = vec![SerializablePubkey::default(), SerializablePubkey::default()];
let results = vec![Ok(None), Ok(None)];

let value = collect_batch_results(&addresses, results).expect("expected success");
assert_eq!(value, vec![None, None]);
}

#[test]
fn collect_batch_results_returns_error_for_actual_failure() {
let addresses = vec![SerializablePubkey::default()];
let results = vec![Err(PhotonApiError::UnexpectedError("boom".to_string()))];

let err = collect_batch_results(&addresses, results).expect_err("expected error");
assert_eq!(err, PhotonApiError::UnexpectedError("boom".to_string()));
}
}
8 changes: 8 additions & 0 deletions src/api/method/interface/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
pub mod get_account_interface;
pub mod get_multiple_account_interfaces;
pub mod racing;
pub mod types;

pub use get_account_interface::get_account_interface;
pub use get_multiple_account_interfaces::get_multiple_account_interfaces;
pub use types::*;
Loading