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
3 changes: 3 additions & 0 deletions CONTRIBUTING_BACKEND.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,3 +123,6 @@ The backend follows a modular structure:
- **Security**: Never commit sensitive information (like real database credentials) to the repository.

Thank you for contributing to PrediFi! 🚀


// Comment to start working on task
13 changes: 13 additions & 0 deletions backend/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion backend/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ chrono = { version = "0.4", features = ["serde"] }
tower-http = { version = "0.6.8", features = ["cors"] }
dotenvy = "0.15"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] }
tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt", "json"] }
sqlx = { version = "0.8", default-features = false, features = [
"runtime-tokio-rustls",
"postgres",
Expand Down
5 changes: 5 additions & 0 deletions backend/migrations/005_add_index_pools_created_at.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
-- Migration: add index to created_at for pools
--
-- Optimizes sorting by creation time.

CREATE INDEX IF NOT EXISTS idx_pools_created_at ON pools (created_at);
139 changes: 109 additions & 30 deletions backend/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ pub struct Config {
pub rpc_timeout_secs: u64,
/// Tracing log level passed to `RUST_LOG` / `EnvFilter` (default `"info"`).
pub log_level: String,
/// Runtime environment, e.g. "production", "development", "test" (default "development").
pub app_env: String,
/// Protocol treasury fee in basis points (default `300` = 3 %).
pub treasury_fee_bps: u32,
/// Referral share of the protocol fee in basis points (default `5000` = 50 % of the fee).
Expand Down Expand Up @@ -84,8 +86,16 @@ impl Config {
let host = get_string(vars, "PREDIFI_APP_HOST", DEFAULT_HOST);
let port = get_u16(vars, "PREDIFI_APP_PORT", DEFAULT_PORT)?;
let database_url = get_string(vars, "PREDIFI_DATABASE_URL", DEFAULT_DATABASE_URL);
let db_max_connections = get_u32(vars, "PREDIFI_DB_MAX_CONNECTIONS", DEFAULT_DB_MAX_CONNECTIONS)?;
let db_min_connections = get_u32(vars, "PREDIFI_DB_MIN_CONNECTIONS", DEFAULT_DB_MIN_CONNECTIONS)?;
let db_max_connections = get_u32(
vars,
"PREDIFI_DB_MAX_CONNECTIONS",
DEFAULT_DB_MAX_CONNECTIONS,
)?;
let db_min_connections = get_u32(
vars,
"PREDIFI_DB_MIN_CONNECTIONS",
DEFAULT_DB_MIN_CONNECTIONS,
)?;
let db_acquire_timeout_secs = get_u64(
vars,
"PREDIFI_DB_ACQUIRE_TIMEOUT_SECS",
Expand All @@ -107,6 +117,7 @@ impl Config {
DEFAULT_RPC_TIMEOUT_SECS,
)?;
let log_level = get_string(vars, "RUST_LOG", DEFAULT_LOG_LEVEL);
let app_env = get_string(vars, "PREDIFI_APP_ENV", "development");
let treasury_fee_bps = get_u32(vars, "PREDIFI_TREASURY_FEE_BPS", DEFAULT_TREASURY_FEE_BPS)?;
let referral_fee_bps = get_u32(vars, "PREDIFI_REFERRAL_FEE_BPS", DEFAULT_REFERRAL_FEE_BPS)?;
let stellar_rpc_url = get_string(vars, "PREDIFI_STELLAR_RPC_URL", DEFAULT_STELLAR_RPC_URL);
Expand Down Expand Up @@ -137,6 +148,7 @@ impl Config {
rpc_health_retry_count,
rpc_timeout_secs,
log_level,
app_env,
treasury_fee_bps,
referral_fee_bps,
stellar_rpc_url,
Expand Down Expand Up @@ -169,15 +181,13 @@ impl Config {
rpc_health_retry_count: 3,
rpc_timeout_secs: 10,
log_level: String::from("debug"),
app_env: String::from("test"),
treasury_fee_bps: DEFAULT_TREASURY_FEE_BPS,
referral_fee_bps: DEFAULT_REFERRAL_FEE_BPS,
stellar_rpc_url: String::from(DEFAULT_STELLAR_RPC_URL),
sentry_dsn: None,
redis_url: String::from(DEFAULT_REDIS_URL),
cors_allowed_origins: DEFAULT_CORS_ORIGINS
.iter()
.map(|s| s.to_string())
.collect(),
cors_allowed_origins: DEFAULT_CORS_ORIGINS.iter().map(|s| s.to_string()).collect(),
}
}
}
Expand Down Expand Up @@ -249,10 +259,7 @@ fn parse_cors_origins(vars: &HashMap<String, String>) -> Result<Vec<String>, Con
let raw = match vars.get("PREDIFI_CORS_ALLOWED_ORIGINS") {
Some(v) => v.clone(),
None => {
return Ok(DEFAULT_CORS_ORIGINS
.iter()
.map(|s| s.to_string())
.collect());
return Ok(DEFAULT_CORS_ORIGINS.iter().map(|s| s.to_string()).collect());
}
};

Expand Down Expand Up @@ -287,7 +294,10 @@ fn validate_cors_origin(origin: &str) -> Result<(), ConfigError> {
if origin == "*" || origin.eq_ignore_ascii_case("null") {
return Err(ConfigError::InvalidValue {
key: "PREDIFI_CORS_ALLOWED_ORIGINS",
reason: format!("'{}' is not a valid origin — wildcards and 'null' are not permitted", origin),
reason: format!(
"'{}' is not a valid origin — wildcards and 'null' are not permitted",
origin
),
});
}

Expand Down Expand Up @@ -362,7 +372,10 @@ fn validate_cors_origin(origin: &str) -> Result<(), ConfigError> {
if host.is_empty() {
return Err(ConfigError::InvalidValue {
key: "PREDIFI_CORS_ALLOWED_ORIGINS",
reason: format!("'{}' is not a valid origin — host must not be empty", origin),
reason: format!(
"'{}' is not a valid origin — host must not be empty",
origin
),
});
}

Expand Down Expand Up @@ -475,7 +488,10 @@ mod tests {

#[test]
fn config_rejects_non_numeric_port() {
let vars = HashMap::from([(String::from("PREDIFI_APP_PORT"), String::from("not-a-number"))]);
let vars = HashMap::from([(
String::from("PREDIFI_APP_PORT"),
String::from("not-a-number"),
)]);
let error = Config::from_map(&vars).expect_err("port must be numeric");

assert!(
Expand All @@ -493,8 +509,14 @@ mod tests {
#[test]
fn config_rejects_min_connections_larger_than_max() {
let vars = HashMap::from([
(String::from("PREDIFI_DB_MIN_CONNECTIONS"), String::from("20")),
(String::from("PREDIFI_DB_MAX_CONNECTIONS"), String::from("10")),
(
String::from("PREDIFI_DB_MIN_CONNECTIONS"),
String::from("20"),
),
(
String::from("PREDIFI_DB_MAX_CONNECTIONS"),
String::from("10"),
),
]);
let error = Config::from_map(&vars).expect_err("min > max must be rejected");

Expand Down Expand Up @@ -574,7 +596,13 @@ mod tests {
)]);
let error = Config::from_map(&vars).expect_err("wildcard must be rejected");
assert!(
matches!(error, ConfigError::InvalidValue { key: "PREDIFI_CORS_ALLOWED_ORIGINS", .. }),
matches!(
error,
ConfigError::InvalidValue {
key: "PREDIFI_CORS_ALLOWED_ORIGINS",
..
}
),
"unexpected error: {error}"
);
}
Expand All @@ -587,7 +615,13 @@ mod tests {
)]);
let error = Config::from_map(&vars).expect_err("null origin must be rejected");
assert!(
matches!(error, ConfigError::InvalidValue { key: "PREDIFI_CORS_ALLOWED_ORIGINS", .. }),
matches!(
error,
ConfigError::InvalidValue {
key: "PREDIFI_CORS_ALLOWED_ORIGINS",
..
}
),
"unexpected error: {error}"
);
}
Expand All @@ -600,7 +634,13 @@ mod tests {
)]);
let error = Config::from_map(&vars).expect_err("origin without scheme must be rejected");
assert!(
matches!(error, ConfigError::InvalidValue { key: "PREDIFI_CORS_ALLOWED_ORIGINS", .. }),
matches!(
error,
ConfigError::InvalidValue {
key: "PREDIFI_CORS_ALLOWED_ORIGINS",
..
}
),
"unexpected error: {error}"
);
}
Expand All @@ -613,7 +653,13 @@ mod tests {
)]);
let error = Config::from_map(&vars).expect_err("ftp scheme must be rejected");
assert!(
matches!(error, ConfigError::InvalidValue { key: "PREDIFI_CORS_ALLOWED_ORIGINS", .. }),
matches!(
error,
ConfigError::InvalidValue {
key: "PREDIFI_CORS_ALLOWED_ORIGINS",
..
}
),
"unexpected error: {error}"
);
}
Expand All @@ -626,7 +672,13 @@ mod tests {
)]);
let error = Config::from_map(&vars).expect_err("origin with path must be rejected");
assert!(
matches!(error, ConfigError::InvalidValue { key: "PREDIFI_CORS_ALLOWED_ORIGINS", .. }),
matches!(
error,
ConfigError::InvalidValue {
key: "PREDIFI_CORS_ALLOWED_ORIGINS",
..
}
),
"unexpected error: {error}"
);
}
Expand All @@ -639,7 +691,13 @@ mod tests {
)]);
let error = Config::from_map(&vars).expect_err("origin with query string must be rejected");
assert!(
matches!(error, ConfigError::InvalidValue { key: "PREDIFI_CORS_ALLOWED_ORIGINS", .. }),
matches!(
error,
ConfigError::InvalidValue {
key: "PREDIFI_CORS_ALLOWED_ORIGINS",
..
}
),
"unexpected error: {error}"
);
}
Expand All @@ -652,7 +710,13 @@ mod tests {
)]);
let error = Config::from_map(&vars).expect_err("origin with fragment must be rejected");
assert!(
matches!(error, ConfigError::InvalidValue { key: "PREDIFI_CORS_ALLOWED_ORIGINS", .. }),
matches!(
error,
ConfigError::InvalidValue {
key: "PREDIFI_CORS_ALLOWED_ORIGINS",
..
}
),
"unexpected error: {error}"
);
}
Expand All @@ -665,7 +729,13 @@ mod tests {
)]);
let error = Config::from_map(&vars).expect_err("port > 65535 must be rejected");
assert!(
matches!(error, ConfigError::InvalidValue { key: "PREDIFI_CORS_ALLOWED_ORIGINS", .. }),
matches!(
error,
ConfigError::InvalidValue {
key: "PREDIFI_CORS_ALLOWED_ORIGINS",
..
}
),
"unexpected error: {error}"
);
}
Expand All @@ -678,7 +748,13 @@ mod tests {
)]);
let error = Config::from_map(&vars).expect_err("empty origin list must be rejected");
assert!(
matches!(error, ConfigError::InvalidValue { key: "PREDIFI_CORS_ALLOWED_ORIGINS", .. }),
matches!(
error,
ConfigError::InvalidValue {
key: "PREDIFI_CORS_ALLOWED_ORIGINS",
..
}
),
"unexpected error: {error}"
);
}
Expand Down Expand Up @@ -737,10 +813,7 @@ mod tests {
/// `InvalidNumber` display message includes the variable name and bad value.
#[test]
fn invalid_number_display_includes_key_and_value() {
let vars = HashMap::from([(
String::from("PREDIFI_APP_PORT"),
String::from("not-a-port"),
)]);
let vars = HashMap::from([(String::from("PREDIFI_APP_PORT"), String::from("not-a-port"))]);
let error = Config::from_map(&vars).expect_err("non-numeric port must fail");
let msg = error.to_string();
assert!(
Expand All @@ -757,8 +830,14 @@ mod tests {
#[test]
fn invalid_value_display_includes_key_and_reason() {
let vars = HashMap::from([
(String::from("PREDIFI_DB_MIN_CONNECTIONS"), String::from("50")),
(String::from("PREDIFI_DB_MAX_CONNECTIONS"), String::from("10")),
(
String::from("PREDIFI_DB_MIN_CONNECTIONS"),
String::from("50"),
),
(
String::from("PREDIFI_DB_MAX_CONNECTIONS"),
String::from("10"),
),
]);
let error = Config::from_map(&vars).expect_err("min > max must fail");
let msg = error.to_string();
Expand Down
Loading
Loading