Skip to content

Commit 84f2c46

Browse files
authored
Merge pull request #382 from yinkscss/fix/343-test-data-management
[343] Test Data Management
2 parents fbabbf2 + dbf9073 commit 84f2c46

41 files changed

Lines changed: 268 additions & 5859 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Cargo.toml

Lines changed: 4 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,4 @@
1-
[package]
2-
name = "stellar-escrow-indexer"
3-
version = "0.1.0"
4-
edition = "2021"
5-
6-
[dependencies]
7-
tokio = { version = "1.0", features = ["full"] }
8-
axum = { version = "0.7", features = ["ws", "json", "multipart"] }
9-
tower = "0.4"
10-
tower-http = { version = "0.5", features = ["cors", "fs"] }
11-
serde = { version = "1.0", features = ["derive"] }
12-
serde_json = "1.0"
13-
sqlx = { version = "0.7", features = ["runtime-tokio", "postgres", "chrono", "uuid"] }
14-
chrono = { version = "0.4", features = ["serde"] }
15-
uuid = { version = "1.0", features = ["v4", "serde"] }
16-
futures = "0.3"
17-
anyhow = "1.0"
18-
thiserror = "1.0"
19-
tracing = "0.1"
20-
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
21-
config = "0.14"
22-
clap = { version = "4.0", features = ["derive"] }
23-
reqwest = { version = "0.11", features = ["json"] }
24-
async-stream = "0.3"
25-
tokio-stream = "0.1"
26-
dashmap = "5.5"
27-
toml = "0.8"
28-
tokio-util = { version = "0.7", features = ["io"] }
29-
mime = "0.3"
30-
image = { version = "0.24", default-features = false, features = ["jpeg", "png", "webp"] }
31-
sha2 = "0.10"
32-
hex = "0.4"
33-
bytes = "1.0"
34-
smartcore = { version = "0.3", features = ["serde"] }
35-
redis = { version = "0.25", features = ["tokio-comp", "connection-manager"] }
36-
37-
[dev-dependencies]
38-
wiremock = "0.6"
1+
[workspace]
2+
# `contract` is built via `cargo build --manifest-path contract/Cargo.toml` (Soroban toolchain).
3+
members = ["indexer", "ui", "mobile-sdk"]
4+
resolver = "2"

DISASTER_RECOVERY.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
## 3. Backup System
3030

3131
### Automated Daily Backup
32-
The `backup` Docker service runs `scripts/backup.sh` daily at 02:00 UTC.
32+
The `backup` Docker service runs `scripts/backup.sh` daily at 02:00 UTC. The script prints `BACKUP_LOCATION=<path>` on success for automation; the indexer `BackupService` records runs and exposes `GET /backup/status`, `GET /backup/history`, and `GET /backup/recovery-plan` for operators.
3333

3434
```bash
3535
# Manual backup trigger

indexer/Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ edition = "2021"
66
[dependencies]
77
tokio = { version = "1.0", features = ["full"] }
88
axum = { version = "0.7", features = ["ws", "json", "multipart"] }
9-
tower = "0.4"
9+
tower = { version = "0.4", features = ["util"] }
1010
tower-http = { version = "0.5", features = ["cors", "fs"] }
1111
serde = { version = "1.0", features = ["derive"] }
1212
serde_json = "1.0"
@@ -33,7 +33,7 @@ hex = "0.4"
3333
bytes = "1.0"
3434
smartcore = { version = "0.3", features = ["serde"] }
3535
hmac = "0.12"
36-
redis = { version = "0.24", features = ["aio", "tokio-comp"] }
36+
redis = { version = "0.24", features = ["aio", "tokio-comp", "connection-manager"] }
3737

3838
[dev-dependencies]
3939
wiremock = "0.6"

indexer/src/compliance_service/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use crate::database::Database;
88
use crate::models::Event;
99
use aml::{AmlScreener, AmlResult};
1010
use kyc::{KycProvider, KycResult, KycStatus};
11+
use reporting::ComplianceReport;
1112
use serde::{Deserialize, Serialize};
1213
use std::sync::Arc;
1314
use chrono::{DateTime, Utc};

indexer/src/config.rs

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,19 @@ fn default_schema_version() -> u32 {
105105
1
106106
}
107107

108+
/// Non-secret subset of `Config` for operators and deployment validation (issue #313).
109+
#[derive(Debug, Clone, Serialize, Deserialize)]
110+
pub struct PublicConfigSnapshot {
111+
pub meta: MetaConfig,
112+
pub server: ServerConfig,
113+
pub stellar_network: String,
114+
pub stellar_horizon_url: String,
115+
pub stellar_contract_configured: bool,
116+
pub cache_redis_configured: bool,
117+
pub backup_interval_hours: u64,
118+
pub gateway_instance_count: usize,
119+
}
120+
108121
#[derive(Debug, Clone, Serialize, Deserialize)]
109122
pub struct ServerConfig {
110123
pub port: u16,
@@ -476,6 +489,20 @@ impl Config {
476489
Err(ConfigValidationError(errors))
477490
}
478491
}
492+
493+
/// Safe, privacy-preserving view for `/config/public` (no API keys, DB URLs, or webhook secrets).
494+
pub fn public_snapshot(&self) -> PublicConfigSnapshot {
495+
PublicConfigSnapshot {
496+
meta: self.meta.clone(),
497+
server: self.server.clone(),
498+
stellar_network: self.stellar.network.clone(),
499+
stellar_horizon_url: self.stellar.horizon_url.clone(),
500+
stellar_contract_configured: !self.stellar.contract_id.is_empty(),
501+
cache_redis_configured: !self.cache.redis_url.is_empty(),
502+
backup_interval_hours: self.backup.interval_hours,
503+
gateway_instance_count: self.gateway.service_instances.len(),
504+
}
505+
}
479506
}
480507

481508
impl Default for Config {
@@ -493,7 +520,7 @@ impl Default for Config {
493520
database: DatabaseConfig {
494521
url: "postgres://indexer:password@localhost/stellar_escrow".to_string(),
495522
max_connections: 10,
496-
connect_timeout_seconds: 30,
523+
min_connections: 2,
497524
},
498525
stellar: StellarConfig {
499526
network: "testnet".to_string(),
@@ -509,11 +536,11 @@ impl Default for Config {
509536
whitelist: vec![],
510537
blacklist: vec![],
511538
},
539+
auth: AuthConfig::default(),
512540
storage: StorageConfig {
513541
base_dir: "./uploads".to_string(),
514542
max_file_size_mb: 10,
515543
},
516-
notification: NotificationConfig::default(),
517544
notification: NotificationConfig {
518545
email_api_url: "https://api.sendgrid.com".to_string(),
519546
email_api_key: String::new(),
@@ -526,9 +553,13 @@ impl Default for Config {
526553
push_project_id: String::new(),
527554
push_server_key: String::new(),
528555
},
556+
cache: CacheConfig::default(),
529557
gateway: GatewayConfig::default(),
530-
531558
integration: IntegrationConfig::default(),
559+
compliance: ComplianceConfig::default(),
560+
monitoring: MonitoringConfig::default(),
561+
analytics: AnalyticsConfig::default(),
562+
backup: BackupConfig::default(),
532563
}
533564
}
534565
}

indexer/src/database.rs

Lines changed: 65 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -337,50 +337,41 @@ impl Database {
337337
}
338338

339339
pub async fn count_events(&self, query: &EventQuery) -> Result<i64, AppError> {
340-
let mut sql = "SELECT COUNT(*) FROM events WHERE 1=1".to_string();
341-
let mut bindings: Vec<String> = vec![];
342-
343-
if let Some(event_type) = &query.event_type {
344-
sql.push_str(&format!(" AND event_type = ${}", bindings.len() + 1));
345-
bindings.push(event_type.clone());
340+
let mut b = sqlx::QueryBuilder::new("SELECT COUNT(*) FROM events WHERE 1=1");
341+
if let Some(ref event_type) = query.event_type {
342+
b.push(" AND event_type = ");
343+
b.push_bind(event_type);
346344
}
347-
if let Some(category) = &query.category {
348-
sql.push_str(&format!(" AND category = ${}", bindings.len() + 1));
349-
bindings.push(category.clone());
345+
if let Some(ref category) = query.category {
346+
b.push(" AND category = ");
347+
b.push_bind(category);
350348
}
351-
if let Some(contract_id) = &query.contract_id {
352-
sql.push_str(&format!(" AND contract_id = ${}", bindings.len() + 1));
353-
bindings.push(contract_id.clone());
349+
if let Some(ref contract_id) = query.contract_id {
350+
b.push(" AND contract_id = ");
351+
b.push_bind(contract_id);
354352
}
355353
if let Some(trade_id) = query.trade_id {
356-
sql.push_str(&format!(" AND data->>'trade_id' = ${}", bindings.len() + 1));
357-
bindings.push(trade_id.to_string());
354+
b.push(" AND data->>'trade_id' = ");
355+
b.push_bind(trade_id.to_string());
358356
}
359357
if let Some(from_ledger) = query.from_ledger {
360-
sql.push_str(&format!(" AND ledger >= ${}", bindings.len() + 1));
361-
bindings.push(from_ledger.to_string());
358+
b.push(" AND ledger >= ");
359+
b.push_bind(from_ledger);
362360
}
363361
if let Some(to_ledger) = query.to_ledger {
364-
sql.push_str(&format!(" AND ledger <= ${}", bindings.len() + 1));
365-
bindings.push(to_ledger.to_string());
366-
}
367-
368-
let mut q = sqlx::query(&sql);
369-
for b in &bindings {
370-
q = q.bind(b);
362+
b.push(" AND ledger <= ");
363+
b.push_bind(to_ledger);
371364
}
372-
// Timestamp filters bound separately (DateTime type)
373365
if let Some(from_time) = query.from_time {
374-
sql.push_str(&format!(" AND timestamp >= ${}", bindings.len() + 1));
375-
q = q.bind(from_time);
366+
b.push(" AND timestamp >= ");
367+
b.push_bind(from_time);
376368
}
377369
if let Some(to_time) = query.to_time {
378-
sql.push_str(&format!(" AND timestamp <= ${}", bindings.len() + 1));
379-
q = q.bind(to_time);
370+
b.push(" AND timestamp <= ");
371+
b.push_bind(to_time);
380372
}
381-
382-
let row = q.fetch_one(&self.pool).await?;
383-
Ok(row.get::<i64, _>(0))
373+
let total: i64 = b.build_query_scalar().fetch_one(&self.pool).await?;
374+
Ok(total)
384375
}
385376

386377
pub async fn get_events_in_range(
@@ -1230,6 +1221,12 @@ impl Database {
12301221
.bind(status_code as i16)
12311222
.bind(duration_ms as i64)
12321223
.bind(is_error)
1224+
.execute(&self.pool)
1225+
.await?;
1226+
Ok(())
1227+
}
1228+
1229+
// -----------------------------------------------------------------------
12331230
// Integration service
12341231
// -----------------------------------------------------------------------
12351232

@@ -1281,6 +1278,7 @@ impl Database {
12811278
Ok(())
12821279
}
12831280

1281+
pub async fn get_integration_deliveries(
12841282
/// Query the hourly APM rollup materialized view for the last `hours` hours.
12851283
pub async fn get_perf_hourly_rollup(
12861284
&self,
@@ -1566,6 +1564,42 @@ impl Database {
15661564
.bind(ep.active)
15671565
.bind(ep.created_at)
15681566
.bind(ep.failure_count as i32)
1567+
.execute(&self.pool)
1568+
.await?;
1569+
Ok(())
1570+
}
1571+
1572+
pub async fn deactivate_webhook_endpoint(&self, id: Uuid) -> Result<(), anyhow::Error> {
1573+
sqlx::query("UPDATE webhook_endpoints SET active = false WHERE id = $1")
1574+
.bind(id)
1575+
.execute(&self.pool)
1576+
.await?;
1577+
Ok(())
1578+
}
1579+
1580+
pub async fn insert_webhook_delivery(
1581+
&self,
1582+
record: &crate::webhook_service::WebhookDeliveryRecord,
1583+
) -> Result<(), AppError> {
1584+
sqlx::query(
1585+
"INSERT INTO webhook_deliveries (id, endpoint_id, event_type, payload, status_code, success, attempt, error, delivered_at, duration_ms) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10)"
1586+
)
1587+
.bind(record.id)
1588+
.bind(record.endpoint_id)
1589+
.bind(&record.event_type)
1590+
.bind(&record.payload)
1591+
.bind(record.status_code.map(|c| c as i32))
1592+
.bind(record.success)
1593+
.bind(record.attempt as i32)
1594+
.bind(&record.error)
1595+
.bind(record.delivered_at)
1596+
.bind(record.duration_ms as i64)
1597+
.execute(&self.pool)
1598+
.await?;
1599+
Ok(())
1600+
}
1601+
}
1602+
15691603
// Compliance Operations
15701604
// =============================================================================
15711605

@@ -1668,34 +1702,6 @@ impl Database {
16681702
Ok(())
16691703
}
16701704

1671-
pub async fn deactivate_webhook_endpoint(&self, id: Uuid) -> Result<(), anyhow::Error> {
1672-
sqlx::query("UPDATE webhook_endpoints SET active = false WHERE id = $1")
1673-
.bind(id)
1674-
.execute(&self.pool)
1675-
.await?;
1676-
Ok(())
1677-
}
1678-
1679-
pub async fn insert_webhook_delivery(
1680-
&self,
1681-
record: &crate::webhook_service::WebhookDeliveryRecord,
1682-
) -> Result<(), AppError> {
1683-
sqlx::query(
1684-
"INSERT INTO webhook_deliveries (id, endpoint_id, event_type, payload, status_code, success, attempt, error, delivered_at, duration_ms) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10)"
1685-
)
1686-
.bind(record.id)
1687-
.bind(record.endpoint_id)
1688-
.bind(&record.event_type)
1689-
.bind(&record.payload)
1690-
.bind(record.status_code.map(|c| c as i32))
1691-
.bind(record.success)
1692-
.bind(record.attempt as i32)
1693-
.bind(&record.error)
1694-
.bind(record.delivered_at)
1695-
.bind(record.duration_ms as i64)
1696-
.execute(&self.pool)
1697-
.await?;
1698-
Ok(())
16991705
fn row_to_compliance_check(
17001706
&self,
17011707
row: &sqlx::postgres::PgRow,

indexer/src/error.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ pub enum AppError {
5656
#[error("Storage error: {0}")]
5757
Storage(String),
5858

59+
#[error("Internal error: {0}")]
60+
Internal(String),
5961
#[error("Conflict: {0}")]
6062
Conflict(String),
6163

@@ -132,6 +134,11 @@ impl IntoResponse for AppError {
132134
"STORAGE_ERROR",
133135
"Storage error",
134136
),
137+
AppError::Internal(_) => (
138+
StatusCode::INTERNAL_SERVER_ERROR,
139+
"INTERNAL_ERROR",
140+
"Internal error",
141+
),
135142
AppError::Conflict(_) => (StatusCode::CONFLICT, "CONFLICT", "Resource already exists"),
136143
AppError::BadRequest(_) => (StatusCode::BAD_REQUEST, "BAD_REQUEST", "Bad request"),
137144
};

0 commit comments

Comments
 (0)