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
4 changes: 2 additions & 2 deletions contract/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ edition = "2021"
axum = "0.7"
tokio = { version = "1.35", features = ["full"] }
tower = "0.4"
tower-http = { version = "0.5", features = ["trace", "cors"] }
tower-http = { version = "0.5", features = ["trace", "cors", "request-id"] }
url = "2"
futures = "0.3"

Expand Down Expand Up @@ -36,7 +36,7 @@ prometheus = "0.13"

# Logging
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }

# Error handling
thiserror = "1.0"
Expand Down
22 changes: 18 additions & 4 deletions contract/src/cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,42 +6,46 @@ use std::sync::Arc;
use tokio::sync::RwLock;
use tracing::warn;

#[derive(Clone)]
pub enum CacheBackend {
Redis(RedisCache),
InMemory(InMemoryCache),
}

impl CacheBackend {
#[tracing::instrument(name = "cache.check_connection", skip(self))]
pub async fn check_connection(&self) -> bool {
match self {
Self::Redis(c) => c.check_connection().await,
Self::InMemory(c) => c.check_connection().await,
}
}

/// Closes the underlying connection cleanly. Called during graceful
/// shutdown, after in-flight requests have finished draining.
#[tracing::instrument(name = "cache.close", skip(self))]
pub async fn close(&self) {
match self {
Self::Redis(c) => c.close().await,
Self::InMemory(c) => c.close().await,
}
}

#[tracing::instrument(name = "cache.get_raw", skip(self), fields(key = %key))]
pub async fn get_raw(&self, key: &str) -> Result<Option<String>> {
match self {
Self::Redis(c) => c.get_raw(key).await,
Self::InMemory(c) => c.get_raw(key).await,
}
}

#[tracing::instrument(name = "cache.set_raw", skip(self, value), fields(key = %key, ttl = ttl))]
pub async fn set_raw(&self, key: &str, value: &str, ttl: u64) -> Result<()> {
match self {
Self::Redis(c) => c.set_raw(key, value, ttl).await,
Self::InMemory(c) => c.set_raw(key, value, ttl).await,
}
}

#[tracing::instrument(name = "cache.get", skip(self), fields(key = %key))]
pub async fn get<T>(&self, key: &str) -> Result<Option<T>>
where
T: for<'de> Deserialize<'de>,
Expand All @@ -52,6 +56,7 @@ impl CacheBackend {
}
}

#[tracing::instrument(name = "cache.set", skip(self, value), fields(key = %key, ttl = ttl))]
pub async fn set<T>(&self, key: &str, value: &T, ttl: u64) -> Result<()>
where
T: Serialize,
Expand All @@ -60,6 +65,7 @@ impl CacheBackend {
self.set_raw(key, &serialized, ttl).await
}

#[tracing::instrument(name = "cache.delete", skip(self), fields(key = %key))]
pub async fn delete(&self, key: &str) -> Result<()> {
match self {
Self::Redis(c) => c.delete(key).await,
Expand All @@ -68,6 +74,7 @@ impl CacheBackend {
}
}

#[derive(Clone)]
pub struct RedisCache {
connection: ConnectionManager,
}
Expand Down Expand Up @@ -114,6 +121,7 @@ impl RedisCache {
}
}

#[derive(Clone)]
pub struct InMemoryCache {
store: Arc<RwLock<HashMap<String, String>>>,
}
Expand Down Expand Up @@ -182,7 +190,10 @@ mod tests {
async fn test_overwrite_value() {
let cache = in_memory_backend();
cache.set("key1", &"first".to_string(), 3600).await.unwrap();
cache.set("key1", &"second".to_string(), 3600).await.unwrap();
cache
.set("key1", &"second".to_string(), 3600)
.await
.unwrap();
let result: Option<String> = cache.get("key1").await.unwrap();
assert_eq!(result, Some("second".to_string()));
}
Expand Down Expand Up @@ -264,7 +275,10 @@ mod tests {
#[tokio::test]
async fn test_cache_backend_enum_dispatch() {
let cache = in_memory_backend();
cache.set("enum_key", &"enum_value".to_string(), 3600).await.unwrap();
cache
.set("enum_key", &"enum_value".to_string(), 3600)
.await
.unwrap();
let result: Option<String> = cache.get("enum_key").await.unwrap();
assert_eq!(result, Some("enum_value".to_string()));
}
Expand Down
21 changes: 21 additions & 0 deletions contract/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,23 @@ use std::env;
use thiserror::Error;
use url::Url;

/// Deployment environment. Controls log output format: JSON in production,
/// human-readable in development. Unrecognized values default to development.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Environment {
Development,
Production,
}

impl Environment {
fn from_env_str(value: &str) -> Self {
match value.trim().to_ascii_lowercase().as_str() {
"production" | "prod" => Self::Production,
_ => Self::Development,
}
}
}

#[derive(Debug, Clone)]
pub struct AppConfig {
pub port: u16,
Expand All @@ -13,6 +30,7 @@ pub struct AppConfig {
pub rate_limit_burst: u32,
pub stellar_max_retries: u32,
pub log_level: String,
pub environment: Environment,
pub webhook_urls: Vec<String>,
pub webhook_secret: Option<String>,
pub cache_verification_ttl: u64,
Expand Down Expand Up @@ -40,6 +58,7 @@ impl AppConfig {
get_env_or_default("STELLAR_HORIZON_URL", "https://horizon-testnet.stellar.org");
let redis_url = get_env_or_default("REDIS_URL", "redis://127.0.0.1:6379");
let log_level = get_env_or_default("LOG_LEVEL", "info");
let environment = Environment::from_env_str(&get_env_or_default("APP_ENV", "development"));
let webhook_urls_raw = get_env_or_default("WEBHOOK_URLS", "");

let stellar_secret_key = match env::var("STELLAR_SECRET_KEY") {
Expand Down Expand Up @@ -177,6 +196,7 @@ impl AppConfig {
rate_limit_burst,
stellar_max_retries,
log_level,
environment,
webhook_urls,
webhook_secret,
cache_verification_ttl,
Expand All @@ -202,6 +222,7 @@ mod tests {
"RATE_LIMIT_BURST",
"STELLAR_MAX_RETRIES",
"LOG_LEVEL",
"APP_ENV",
"WEBHOOK_URLS",
"WEBHOOK_SECRET",
"CACHE_VERIFICATION_TTL",
Expand Down
6 changes: 1 addition & 5 deletions contract/src/handlers/health.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
use axum::{
extract::State,
response::IntoResponse,
Json,
};
use axum::{extract::State, response::IntoResponse, Json};

use crate::types::{AppState, HealthResponse};

Expand Down
5 changes: 2 additions & 3 deletions contract/src/handlers/submit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@ use axum::{
use tracing::{info, warn};

use crate::hash_validator::HashValidator;
use crate::stellar::derive_account_id;
use crate::types::{AppState, SubmitRequest, SubmitResponse};
use crate::types::{map_validation_error, AppState, SubmitRequest, SubmitResponse};

/// POST /submit — anchor a document hash to Stellar using a ManageData operation.
///
Expand All @@ -22,7 +21,7 @@ pub async fn submit_document(
) -> Response {
let normalized_hash = HashValidator::normalize(&req.document_hash);
if let Err(err) = HashValidator::validate_sha256(&normalized_hash) {
let (status, body) = super::verify::map_validation_error_inline(err);
let (status, body) = map_validation_error(err);
return (status, Json(body)).into_response();
}

Expand Down
2 changes: 1 addition & 1 deletion contract/src/handlers/transfer.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use axum::{
extract::{Path, State},
http::StatusCode,
response::{IntoResponse, Response},
response::IntoResponse,
Json,
};
use chrono::{NaiveDate, Utc};
Expand Down
2 changes: 1 addition & 1 deletion contract/src/handlers/verify.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use crate::hash_validator::{HashValidator, ValidationError as HashValidationErro
use crate::stellar::derive_account_id;
use crate::types::{
map_validation_error, AppState, BatchVerifyItem, BatchVerifyRequest, BatchVerifyResponse,
HistoryResponse, VerifyRequest, VerifyResponse, ValidationErrorResponse,
HistoryResponse, ValidationErrorResponse, VerifyRequest, VerifyResponse,
};

// Verify document by POST
Expand Down
Loading
Loading