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
15 changes: 15 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[workspace]
members = [
"crates/lumenqraph-core",
"crates/lumenqraph-indexer",
"crates/lumenqraph-api",
"crates/lumenqraph-webhooks",
"crates/lumenqraph-mcp",
]

[workspace.dependencies]
tokio = { version = "1.0", features = ["full"] }
log = "0.4"
env_logger = "0.10"
thiserror = "1.0"
serde_json = "1.0"
10 changes: 10 additions & 0 deletions crates/lumenqraph-api/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[package]
name = "lumenqraph-api"
version = "0.1.0"
edition = "2021"

[dependencies]
lumenqraph-core = { path = "../lumenqraph-core" }
tokio = { version = "1.0", features = ["full"] }
log = "0.4"
env_logger = "0.10"
68 changes: 68 additions & 0 deletions crates/lumenqraph-api/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
use std::env;
use log::{info, error};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
env_logger::init();

info!("Starting Lumenqraph API service");

// Validate contract IDs at startup
let contract_ids = env::var("CONTRACT_IDS")
.map_err(|_| "Missing CONTRACT_IDS environment variable")?
.split(',')
.map(|id| id.trim().to_string())
.collect::<Vec<_>>();

if let Err(e) = lumenqraph_core::validate_contract_ids(&contract_ids) {
error!("Invalid contract IDs at startup: {}", e);
std::process::exit(1);
}

info!("Contract IDs validation passed: {} contracts configured", contract_ids.len());

// Start API server (placeholder implementation)
info!("API service started successfully");

// Keep the service running
tokio::signal::ctrl_c().await?;
info!("Shutting down API service");

Ok(())
}

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

#[tokio::test]
async fn test_startup_validation_success() {
env::set_var("CONTRACT_IDS", "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA,CBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB");

let contract_ids = env::var("CONTRACT_IDS")
.unwrap()
.split(',')
.map(|id| id.trim().to_string())
.collect::<Vec<_>>();

assert!(lumenqraph_core::validate_contract_ids(&contract_ids).is_ok());

env::remove_var("CONTRACT_IDS");
}

#[tokio::test]
async fn test_startup_validation_failure() {
env::set_var("CONTRACT_IDS", "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA,INVALID_ID");

let contract_ids = env::var("CONTRACT_IDS")
.unwrap()
.split(',')
.map(|id| id.trim().to_string())
.collect::<Vec<_>>();

assert!(lumenqraph_core::validate_contract_ids(&contract_ids).is_err());

env::remove_var("CONTRACT_IDS");
}
}
6 changes: 6 additions & 0 deletions crates/lumenqraph-core/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[package]
name = "lumenqraph-core"
version = "0.1.0"
edition = "2021"

[dependencies]
3 changes: 3 additions & 0 deletions crates/lumenqraph-core/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
pub mod validation;

pub use validation::{is_valid_contract_id, validate_contract_ids};
58 changes: 58 additions & 0 deletions crates/lumenqraph-core/src/validation.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/// Validates that a string is a valid Stellar contract ID (C-strkey format)
pub fn is_valid_contract_id(contract_id: &str) -> bool {
// Basic validation: C-strkey format
// Contract IDs start with 'C' and are 56 characters long
// Full validation would include StrKey decoding and checksum verification
contract_id.starts_with('C') && contract_id.len() == 56 && contract_id.chars().all(|c| c.is_ascii_alphanumeric())
}

/// Validates a list of contract IDs and returns the first invalid one, if any
pub fn validate_contract_ids(contract_ids: &[String]) -> Result<(), String> {
for contract_id in contract_ids {
if !is_valid_contract_id(contract_id) {
return Err(format!("Invalid contract ID: {}", contract_id));
}
}
Ok(())
}

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

#[test]
fn test_valid_contract_id() {
let valid_id = "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
assert!(is_valid_contract_id(valid_id));
}

#[test]
fn test_invalid_contract_id_wrong_prefix() {
let invalid_id = "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
assert!(!is_valid_contract_id(invalid_id));
}

#[test]
fn test_invalid_contract_id_wrong_length() {
let invalid_id = "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
assert!(!is_valid_contract_id(invalid_id));
}

#[test]
fn test_validate_contract_ids_success() {
let contract_ids = vec![
"CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA".to_string(),
"CBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB".to_string(),
];
assert!(validate_contract_ids(&contract_ids).is_ok());
}

#[test]
fn test_validate_contract_ids_failure() {
let contract_ids = vec![
"CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA".to_string(),
"GBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB".to_string(), // Invalid G-strkey
];
assert!(validate_contract_ids(&contract_ids).is_err());
}
}
16 changes: 16 additions & 0 deletions crates/lumenqraph-indexer/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
[package]
name = "lumenqraph-indexer"
version = "0.1.0"
edition = "2021"

[dependencies]
lumenqraph-core = { path = "../lumenqraph-core" }
tokio = { version = "1.0", features = ["full"] }
log = "0.4"
env_logger = "0.10"
thiserror = "1.0"
prometheus = "0.13"
lazy_static = "1.4"

[dev-dependencies]
tokio-test = "0.4"
84 changes: 84 additions & 0 deletions crates/lumenqraph-indexer/src/config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
use std::env;
use std::time::Duration;

#[derive(Debug, Clone)]
pub struct Config {
pub max_consecutive_errors: u32,
pub circuit_breaker_interval: Duration,
pub contract_ids: Vec<String>,
pub rpc_url: String,
pub polling_interval: Duration,
pub max_backoff: Duration,
}

impl Config {
pub fn from_env() -> Result<Self, ConfigError> {
let max_consecutive_errors = env::var("MAX_CONSECUTIVE_ERRORS")
.unwrap_or_else(|_| "20".to_string())
.parse()
.map_err(ConfigError::InvalidMaxConsecutiveErrors)?;

let circuit_breaker_interval_secs = env::var("CIRCUIT_BREAKER_INTERVAL_SECS")
.unwrap_or_else(|_| "300".to_string()) // 5 minutes default
.parse::<u64>()
.map_err(ConfigError::InvalidCircuitBreakerInterval)?;

let contract_ids = env::var("CONTRACT_IDS")
.map_err(|_| ConfigError::MissingContractIds)?
.split(',')
.map(|id| id.trim().to_string())
.collect::<Vec<_>>();

// Validate contract IDs using the core validation
lumenqraph_core::validate_contract_ids(&contract_ids)
.map_err(|e| ConfigError::InvalidContractId(e))?;

let rpc_url = env::var("RPC_URL")
.map_err(|_| ConfigError::MissingRpcUrl)?;

let polling_interval_secs = env::var("POLLING_INTERVAL_SECS")
.unwrap_or_else(|_| "5".to_string())
.parse::<u64>()
.map_err(ConfigError::InvalidPollingInterval)?;

let max_backoff_secs = env::var("MAX_BACKOFF_SECS")
.unwrap_or_else(|_| "60".to_string())
.parse::<u64>()
.map_err(ConfigError::InvalidMaxBackoff)?;

Ok(Config {
max_consecutive_errors,
circuit_breaker_interval: Duration::from_secs(circuit_breaker_interval_secs),
contract_ids,
rpc_url,
polling_interval: Duration::from_secs(polling_interval_secs),
max_backoff: Duration::from_secs(max_backoff_secs),
})
}
}



#[derive(Debug, thiserror::Error)]
pub enum ConfigError {
#[error("Invalid MAX_CONSECUTIVE_ERRORS: {0}")]
InvalidMaxConsecutiveErrors(#[source] std::num::ParseIntError),

#[error("Invalid CIRCUIT_BREAKER_INTERVAL_SECS: {0}")]
InvalidCircuitBreakerInterval(#[source] std::num::ParseIntError),

#[error("Missing CONTRACT_IDS environment variable")]
MissingContractIds,

#[error("Invalid contract ID: {0}")]
InvalidContractId(String),

#[error("Missing RPC_URL environment variable")]
MissingRpcUrl,

#[error("Invalid POLLING_INTERVAL_SECS: {0}")]
InvalidPollingInterval(#[source] std::num::ParseIntError),

#[error("Invalid MAX_BACKOFF_SECS: {0}")]
InvalidMaxBackoff(#[source] std::num::ParseIntError),
}
6 changes: 6 additions & 0 deletions crates/lumenqraph-indexer/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
pub mod config;
pub mod poller;
pub mod validation;

pub use config::{Config, ConfigError};
pub use poller::{Poller, PollerError};
Loading