Skip to content
Open
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
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,17 @@ cargo run -p waveflow-api
- [Implementation Roadmap](docs/ROADMAP.md)
- [Core loop walkthrough](docs/core-loop.md)

## Production configuration

Set `SOROBAN_RPC_URL` and `NETWORK_PASSPHRASE` together when deploying the gateway:

| Network | `SOROBAN_RPC_URL` | `NETWORK_PASSPHRASE` |
|---------|-------------------|----------------------|
| Testnet | `https://soroban-testnet.stellar.org` | `Test SDF Network ; September 2015` |
| Mainnet | Your mainnet Soroban RPC endpoint | `Public Global Stellar Network ; September 2015` |

The gateway validates obvious testnet/mainnet mismatches at startup so production does not silently sign transactions for the wrong Stellar network.

## License

MIT
64 changes: 62 additions & 2 deletions crates/shared/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ use std::env;

use crate::error::{WaveFlowError, WaveFlowResult};

pub const TESTNET_NETWORK_PASSPHRASE: &str = "Test SDF Network ; September 2015";
pub const PUBLIC_NETWORK_PASSPHRASE: &str = "Public Global Stellar Network ; September 2015";

#[derive(Debug, Clone)]
pub struct AppConfig {
pub database_url: String,
Expand All @@ -24,8 +27,9 @@ impl AppConfig {
.map_err(|_| WaveFlowError::Config("GITHUB_WEBHOOK_SECRET is required".into()))?;
let soroban_rpc_url = env::var("SOROBAN_RPC_URL")
.unwrap_or_else(|_| "https://soroban-testnet.stellar.org".into());
let network_passphrase = env::var("NETWORK_PASSPHRASE")
.unwrap_or_else(|_| "Test SDF Network ; September 2015".into());
let network_passphrase =
env::var("NETWORK_PASSPHRASE").unwrap_or_else(|_| TESTNET_NETWORK_PASSPHRASE.into());
validate_rpc_network_pair(&soroban_rpc_url, &network_passphrase)?;
let escrow_contract_id = env::var("ESCROW_CONTRACT_ID").ok();
let gateway_secret_key = env::var("GATEWAY_SECRET_KEY").ok();
let api_admin_keys = env::var("API_ADMIN_KEYS")
Expand Down Expand Up @@ -59,6 +63,27 @@ impl AppConfig {
}
}

fn validate_rpc_network_pair(
soroban_rpc_url: &str,
network_passphrase: &str,
) -> WaveFlowResult<()> {
let lower_url = soroban_rpc_url.to_ascii_lowercase();

if lower_url.contains("mainnet") && network_passphrase != PUBLIC_NETWORK_PASSPHRASE {
return Err(WaveFlowError::Config(format!(
"NETWORK_PASSPHRASE must be `{PUBLIC_NETWORK_PASSPHRASE}` when SOROBAN_RPC_URL points to mainnet"
)));
}

if lower_url.contains("testnet") && network_passphrase != TESTNET_NETWORK_PASSPHRASE {
return Err(WaveFlowError::Config(format!(
"NETWORK_PASSPHRASE must be `{TESTNET_NETWORK_PASSPHRASE}` when SOROBAN_RPC_URL points to testnet"
)));
}

Ok(())
}

#[cfg(test)]
mod tests {
use super::*;
Expand All @@ -67,13 +92,48 @@ mod tests {
fn parses_admin_keys_from_comma_list() {
std::env::set_var("DATABASE_URL", "postgres://localhost/waveflow");
std::env::set_var("GITHUB_WEBHOOK_SECRET", "secret");
std::env::set_var("SOROBAN_RPC_URL", "https://soroban-testnet.stellar.org");
std::env::set_var("NETWORK_PASSPHRASE", TESTNET_NETWORK_PASSPHRASE);
std::env::set_var("API_ADMIN_KEYS", "key-a, key-b");

let cfg = AppConfig::from_env().expect("config");
assert_eq!(cfg.api_admin_keys, vec!["key-a", "key-b"]);

std::env::remove_var("DATABASE_URL");
std::env::remove_var("GITHUB_WEBHOOK_SECRET");
std::env::remove_var("SOROBAN_RPC_URL");
std::env::remove_var("NETWORK_PASSPHRASE");
std::env::remove_var("API_ADMIN_KEYS");
}

#[test]
fn rejects_mainnet_rpc_with_testnet_passphrase() {
let err = validate_rpc_network_pair(
"https://soroban-mainnet.stellar.org",
TESTNET_NETWORK_PASSPHRASE,
)
.expect_err("mainnet RPC requires public passphrase");

assert!(err.to_string().contains(PUBLIC_NETWORK_PASSPHRASE));
}

#[test]
fn rejects_testnet_rpc_with_public_passphrase() {
let err = validate_rpc_network_pair(
"https://soroban-testnet.stellar.org",
PUBLIC_NETWORK_PASSPHRASE,
)
.expect_err("testnet RPC requires testnet passphrase");

assert!(err.to_string().contains(TESTNET_NETWORK_PASSPHRASE));
}

#[test]
fn accepts_matching_mainnet_rpc_and_passphrase() {
validate_rpc_network_pair(
"https://soroban-mainnet.stellar.org",
PUBLIC_NETWORK_PASSPHRASE,
)
.expect("mainnet RPC with public passphrase");
}
}
2 changes: 1 addition & 1 deletion docs/configuration/env-vars.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Loaded by `crates/shared/src/config.rs` via `AppConfig::from_env()`.
| Variable | Default | Description |
|----------|---------|-------------|
| `SOROBAN_RPC_URL` | `https://soroban-testnet.stellar.org` | RPC endpoint |
| `NETWORK_PASSPHRASE` | Testnet passphrase | Network identifier |
| `NETWORK_PASSPHRASE` | `Test SDF Network ; September 2015` | Network identifier. Use `Public Global Stellar Network ; September 2015` with mainnet RPC. |
| `ESCROW_CONTRACT_ID` | none | Deployed contract ID (required for live attestation) |
| `GATEWAY_SECRET_KEY` | none | Stellar secret key for gateway signing |

Expand Down
9 changes: 8 additions & 1 deletion docs/deployment/render.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,14 @@ Both services receive `DATABASE_URL` from the Render Postgres attachment.

## Soroban

Default blueprint sets `SOROBAN_RPC_URL` to testnet. Override for mainnet when ready.
Default blueprint sets `SOROBAN_RPC_URL` and `NETWORK_PASSPHRASE` to testnet:

| Network | `SOROBAN_RPC_URL` | `NETWORK_PASSPHRASE` |
|---------|-------------------|----------------------|
| Testnet | `https://soroban-testnet.stellar.org` | `Test SDF Network ; September 2015` |
| Mainnet | Your mainnet Soroban RPC endpoint | `Public Global Stellar Network ; September 2015` |

Override both values together when moving the gateway to mainnet. The gateway rejects obvious testnet/mainnet RPC and passphrase mismatches during startup.

## Local parity

Expand Down
2 changes: 2 additions & 0 deletions render.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ services:
sync: false
- key: SOROBAN_RPC_URL
value: https://soroban-testnet.stellar.org
- key: NETWORK_PASSPHRASE
value: Test SDF Network ; September 2015
- key: ESCROW_CONTRACT_ID
sync: false
- key: GATEWAY_SECRET_KEY
Expand Down