use crate::config::NetworkConfig;
use std::fmt;
pub const MAINNET_PASSPHRASE: &str = "Public Global Stellar Network ; September 2015";
pub const TESTNET_PASSPHRASE: &str = "Test SDF Network ; September 2015";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MainnetSafetyError(pub String);
impl fmt::Display for MainnetSafetyError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl std::error::Error for MainnetSafetyError {}
fn rpc_host_is_mainnet(rpc_url: &str) -> bool {
let url = rpc_url.to_ascii_lowercase();
if !url.contains("stellar.org") {
return false;
}
if url.contains("testnet") || url.contains("futurenet") {
return false;
}
true
}
pub fn guard_mutating_network(
config: &NetworkConfig,
network_explicit: bool,
) -> Result<(), MainnetSafetyError> {
let passphrase_is_mainnet = config.passphrase == MAINNET_PASSPHRASE;
let rpc_is_mainnet = rpc_host_is_mainnet(&config.rpc_url);
if passphrase_is_mainnet && !network_explicit {
return Err(MainnetSafetyError(
"Refusing mutating operation on mainnet: the network was not explicitly selected. \
Pass --network-passphrase 'Public Global Stellar Network ; September 2015' \
(or use --network-profile) to target mainnet deliberately."
.to_string(),
));
}
if rpc_is_mainnet && !passphrase_is_mainnet {
return Err(MainnetSafetyError(
"Refusing mutating operation: the RPC URL targets mainnet but the network passphrase \
is not the mainnet passphrase (likely the testnet default). Set --network-passphrase \
'Public Global Stellar Network ; September 2015' to match the endpoint."
.to_string(),
));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::NetworkConfig;
fn testnet_default() -> NetworkConfig {
NetworkConfig::default()
}
fn mainnet_cfg() -> NetworkConfig {
NetworkConfig {
rpc_url: "https://soroban-rpc.stellar.org".to_string(),
passphrase: MAINNET_PASSPHRASE.to_string(),
timeout_secs: Some(15),
pool_max_idle_per_host: Some(100),
}
}
#[test]
fn testnet_default_is_allowed() {
let cfg = testnet_default();
assert!(guard_mutating_network(&cfg, false).is_ok());
assert!(guard_mutating_network(&cfg, true).is_ok());
}
#[test]
fn mainnet_explicit_passphrase_is_allowed() {
let cfg = mainnet_cfg();
assert!(guard_mutating_network(&cfg, true).is_ok());
}
#[test]
fn mainnet_passphrase_without_explicit_opt_in_is_refused() {
let cfg = mainnet_cfg();
let err = guard_mutating_network(&cfg, false).unwrap_err();
assert!(err.to_string().contains("not explicitly selected"));
}
#[test]
fn mainnet_rpc_with_testnet_passphrase_is_refused() {
let cfg = NetworkConfig {
rpc_url: "https://soroban-rpc.stellar.org".to_string(),
passphrase: TESTNET_PASSPHRASE.to_string(),
timeout_secs: Some(15),
pool_max_idle_per_host: Some(100),
};
let err = guard_mutating_network(&cfg, true).unwrap_err();
assert!(err.to_string().contains("not the mainnet passphrase"));
}
#[test]
fn mainnet_rpc_with_testnet_default_is_refused() {
let cfg = NetworkConfig {
rpc_url: "https://soroban-rpc.stellar.org".to_string(),
passphrase: TESTNET_PASSPHRASE.to_string(),
timeout_secs: Some(15),
pool_max_idle_per_host: Some(100),
};
let err = guard_mutating_network(&cfg, false).unwrap_err();
assert!(err.to_string().contains("not the mainnet passphrase"));
}
#[test]
fn unknown_rpc_host_is_not_treated_as_mainnet() {
let cfg = NetworkConfig {
rpc_url: "https://my-custom-rpc.example.com".to_string(),
passphrase: TESTNET_PASSPHRASE.to_string(),
timeout_secs: Some(15),
pool_max_idle_per_host: Some(100),
};
assert!(guard_mutating_network(&cfg, true).is_ok());
}
#[test]
fn testnet_rpc_url_is_not_mainnet() {
let cfg = NetworkConfig {
rpc_url: "https://soroban-testnet.stellar.org".to_string(),
passphrase: TESTNET_PASSPHRASE.to_string(),
timeout_secs: Some(15),
pool_max_idle_per_host: Some(100),
};
assert!(guard_mutating_network(&cfg, true).is_ok());
}
}