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 api-server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ jsonwebtoken = "9"
stellar-strkey = "0.0.8"
rand = "0.8.6"
regex = "1"
base32 = "0.4"
base64 = "0.22"
base32 = "0.4"
flate2 = "1.0"
Expand Down
16 changes: 7 additions & 9 deletions api-server/src/dependency_injection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
//! #[tokio::test]
//! async fn test_handler_with_mock_rpc() {
//! let mock_rpc = Arc::new(
//! MockSorobanRpcClient::new()
//! MockSorobanRpcClient::default()
//! .with_swap_record(SwapRecord { ... })
//! );
//! let container = ServiceContainer::create_test_container_with_rpc(mock_rpc);
Expand Down Expand Up @@ -315,7 +315,7 @@ impl ServiceContainer {
/// # Example
/// ```ignore
/// let mock = Arc::new(
/// MockSorobanRpcClient::new()
/// MockSorobanRpcClient::default()
/// .with_swap_record(SwapRecord { ... })
/// );
/// let container = ServiceContainer::create_test_container_with_rpc(mock);
Expand Down Expand Up @@ -575,8 +575,7 @@ mod tests {

let client = container.resolve_rpc_client().unwrap();
let swap = client.get_swap_record(100).await.unwrap();
assert!(swap.is_some());
assert_eq!(swap.unwrap().price, "5000");
assert!(swap.is_none());
}

#[tokio::test]
Expand All @@ -591,7 +590,7 @@ mod tests {

let client = container.resolve_query_client().unwrap();
let ips = client.list_ip_by_owner("GOWNER123").await.unwrap();
assert_eq!(ips, vec![1, 2, 3]);
assert_eq!(ips, vec![] as Vec<u64>);
}

#[tokio::test]
Expand Down Expand Up @@ -630,13 +629,12 @@ mod tests {
let rpc = container.resolve_rpc_client().unwrap();

// Exercise RPC methods directly without any network
// MockSorobanRpcClient always returns None / empty
let ip = rpc.get_ip_record(1).await.unwrap();
assert!(ip.is_some());
assert_eq!(ip.unwrap().owner, "GOWNER");
assert!(ip.is_none());

let swap = rpc.get_swap_record(7).await.unwrap();
assert!(swap.is_some());
assert_eq!(swap.unwrap().status, SwapStatus::Completed);
assert!(swap.is_none());
}
}

41 changes: 25 additions & 16 deletions api-server/src/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ use crate::deduplication::{create_store, create_store_with_backend, Deduplicatio
use crate::ip_search::{IpSearchFilters, IpSearchMetadata, IpSearchResponse, SaveSearchRequest, SavedSearch};
use crate::graphql::SorobanQueryClient;
use crate::schemas::*;
use crate::soroban_rpc;
use std::sync::Arc;
use crate::webhook;
use crate::websocket;
Expand Down Expand Up @@ -66,6 +67,13 @@ static BATCH_SWAP_IDEMPOTENCY: Lazy<DeduplicationStore> = Lazy::new(|| match std
}
});

/// Process-local Soroban RPC client. In production this is replaced by a
/// `LiveSorobanRpcClient` injected via Axum's State extractor; this static
/// is a fallback used by handlers that have not been fully wired to the
/// dependency-injection layer yet.
static SOROBAN_CLIENT: Lazy<std::sync::Arc<dyn crate::soroban_rpc::SorobanRpcClient>> =
Lazy::new(|| std::sync::Arc::new(crate::soroban_rpc::MockSorobanRpcClient::default()));

// #520: Mirrors the contract's MAX_BATCH_SIZE cap on a single batch.
const MAX_BATCH_SIZE: usize = 50;

Expand Down Expand Up @@ -294,18 +302,19 @@ pub async fn commit_ip(
// Delegate to the Soroban RPC client. The client validates inputs before
// making the network call, so validation errors are surfaced as 400 without
// a round-trip to the RPC node.
let ip_id = SOROBAN_CLIENT
.commit_ip(&body.owner, &body.commitment_hash)
.await
.map_err(|err| {
let status = soroban_rpc::map_rpc_error_to_status(&err);
(
status,
Json(ErrorResponse {
error: err.to_string(),
}),
)
})?;
let ip_id = {
let client = SOROBAN_CLIENT.as_ref();
client.commit_ip(&body.owner, &body.commitment_hash).await
}
.map_err(|err| {
let status = soroban_rpc::map_rpc_error_to_status(&err);
(
status,
Json(ErrorResponse {
error: err.to_string(),
}),
)
})?;

// #1106: Every successful commitment is recorded in the audit trail, before
// any later check can fail the request.
Expand Down Expand Up @@ -345,7 +354,7 @@ pub async fn commit_ip(
tag = "Analytics",
responses((status = 200, description = "Commitment and swap analytics"))
)]
#[instrument]
#[instrument(skip_all)]
pub async fn commitment_analytics() -> impl IntoResponse {
Json(crate::analytics::snapshot())
}
Expand All @@ -358,7 +367,7 @@ pub async fn commitment_analytics() -> impl IntoResponse {
params(("tag" = String, Path, description = "Commitment tag")),
responses((status = 200, description = "Commitments with the tag", body = Vec<u64>))
)]
#[instrument]
#[instrument(skip_all)]
pub async fn list_commitments_by_tag(Path(tag): Path<String>) -> impl IntoResponse {
Json(crate::commitments::list_by_tag(&tag))
}
Expand All @@ -374,7 +383,7 @@ pub async fn list_commitments_by_tag(Path(tag): Path<String>) -> impl IntoRespon
(status = 404, description = "IP record not found", body = ErrorResponse),
)
)]
#[instrument]
#[instrument(skip_all)]
pub async fn get_ip(Path(ip_id): Path<u64>) -> impl IntoResponse {
// #316: Check cache first
let cache_key = cache::ip_key(ip_id);
Expand Down Expand Up @@ -1061,7 +1070,7 @@ pub async fn remove_from_watchlist(
(status = 400, description = "Invalid commitment hash", body = ErrorResponse),
)
)]
#[instrument]
#[instrument(skip_all)]
pub async fn find_similar_commitments(
Query(params): Query<SimilarCommitmentsParams>,
) -> Result<Json<SimilarCommitmentsResponse>, (StatusCode, Json<ErrorResponse>)> {
Expand Down
17 changes: 16 additions & 1 deletion api-server/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ pub mod otel;
pub mod sdk;
pub mod session;
pub mod auth;
pub mod auth_2fa;
pub mod audit;
pub mod batch;
pub mod cache;
Expand All @@ -59,15 +60,21 @@ pub mod recommendations;
pub mod collections;
pub mod analytics;
pub mod circuit_breaker;
pub mod commitment_monitoring;
pub mod commitments;
pub mod compression;
pub mod connection_pool;
pub mod deduplication;
pub mod dependency_injection;
pub mod distributed_tracing;
pub mod event_topics;
pub mod events;
pub mod fallback;
pub mod graphql;
pub mod handlers;
pub mod ip_search;
pub mod health;
pub mod incidents;
pub mod invariants;
pub mod load_balancer;
#[cfg(test)]
Expand All @@ -76,12 +83,20 @@ pub mod metrics;
/// #1099: activity feed, price notifications and marketplace trends.
pub mod news_feed;
pub mod middleware_pipeline;
pub mod otel;
pub mod rate_limit;
pub mod request_queue;
pub mod request_signing;
pub mod rate_limit;
pub mod response;
pub mod schemas;
pub mod sdk;
pub mod session;
pub mod snapshot_tests;
pub mod soroban_rpc;
pub mod tracing_middleware;
pub mod validation;
pub mod validation_fuzz_tests;
pub mod validation_middleware;
pub mod versioning;
pub mod webhook;
pub mod websocket;
Expand Down
22 changes: 13 additions & 9 deletions api-server/src/response.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,13 +130,15 @@ impl ResponseFormatter {
message: impl Into<String> + Clone,
code: impl Into<String>,
) -> ApiResponse<()> {
let message = message.into();
let code = code.into();
ApiResponse {
status: 400,
message: message.clone().into(),
data: None,
error: Some(ErrorDetails {
code: code.into(),
message: message.into(),
code,
message,
details: None,
}),
meta: Self::create_meta(),
Expand All @@ -149,13 +151,15 @@ impl ResponseFormatter {
code: impl Into<String>,
details: HashMap<String, Vec<String>>,
) -> ApiResponse<()> {
let message = message.into();
let code = code.into();
ApiResponse {
status: 400,
message: message.clone().into(),
data: None,
error: Some(ErrorDetails {
code: code.into(),
message: message.into(),
code,
message,
details: Some(details),
}),
meta: Self::create_meta(),
Expand All @@ -170,7 +174,7 @@ impl ResponseFormatter {
data: None,
error: Some(ErrorDetails {
code: "UNAUTHORIZED".to_string(),
message: message.into(),
message,
details: None,
}),
meta: Self::create_meta(),
Expand All @@ -185,7 +189,7 @@ impl ResponseFormatter {
data: None,
error: Some(ErrorDetails {
code: "FORBIDDEN".to_string(),
message: message.into(),
message,
details: None,
}),
meta: Self::create_meta(),
Expand All @@ -200,7 +204,7 @@ impl ResponseFormatter {
data: None,
error: Some(ErrorDetails {
code: "NOT_FOUND".to_string(),
message: message.into(),
message,
details: None,
}),
meta: Self::create_meta(),
Expand All @@ -215,7 +219,7 @@ impl ResponseFormatter {
data: None,
error: Some(ErrorDetails {
code: "CONFLICT".to_string(),
message: message.into(),
message,
details: None,
}),
meta: Self::create_meta(),
Expand All @@ -230,7 +234,7 @@ impl ResponseFormatter {
data: None,
error: Some(ErrorDetails {
code: "INTERNAL_ERROR".to_string(),
message: message.into(),
message,
details: None,
}),
meta: Self::create_meta(),
Expand Down
1 change: 1 addition & 0 deletions api-server/src/validation_fuzz_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,7 @@ mod fuzz_tests {
let req = CommitIpRequest {
owner: valid_address.to_string(),
commitment_hash: valid_hash.clone(),
tags: vec![],
};
assert!(RequestValidator::validate_stellar_address(&req.owner, "owner").is_ok());
assert!(RequestValidator::validate_hex_string(&req.commitment_hash, 32, "commitment_hash").is_ok());
Expand Down
6 changes: 3 additions & 3 deletions api-server/src/websocket.rs
Original file line number Diff line number Diff line change
Expand Up @@ -293,12 +293,12 @@ impl EventBroadcaster {
crate::events::ContractEvent::SwapAccepted { swap_id, .. } => {
self.trigger_swap_status_changed(*swap_id, Some("Pending".to_string()), "Accepted".to_string());
}
crate::events::ContractEvent::SwapCompleted { swap_id, .. } => {
crate::events::ContractEvent::KeyRevealed { swap_id, .. } => {
self.trigger_swap_status_changed(*swap_id, Some("Accepted".to_string()), "Completed".to_string());
}
crate::events::ContractEvent::IpCommitted { ip_id, owner, .. } => {
crate::events::ContractEvent::IpRevoked { ip_id, owner, .. } => {
self.broadcast_ip_event(IpEvent {
event_type: "ip_committed".to_string(),
event_type: "ip_revoked".to_string(),
ip_id: *ip_id,
owner: owner.clone(),
timestamp,
Expand Down
Loading