sdkt_core/
network_safety.rs1use crate::config::NetworkConfig;
18use std::fmt;
19
20pub const MAINNET_PASSPHRASE: &str = "Public Global Stellar Network ; September 2015";
22
23pub const TESTNET_PASSPHRASE: &str = "Test SDF Network ; September 2015";
25
26#[derive(Debug, Clone, PartialEq, Eq)]
28pub struct MainnetSafetyError(pub String);
29
30impl fmt::Display for MainnetSafetyError {
31 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
32 f.write_str(&self.0)
33 }
34}
35
36impl std::error::Error for MainnetSafetyError {}
37
38fn rpc_host_is_mainnet(rpc_url: &str) -> bool {
45 let url = rpc_url.to_ascii_lowercase();
46 if !url.contains("stellar.org") {
47 return false;
48 }
49 if url.contains("testnet") || url.contains("futurenet") {
50 return false;
51 }
52 true
53}
54
55pub fn guard_mutating_network(
74 config: &NetworkConfig,
75 network_explicit: bool,
76) -> Result<(), MainnetSafetyError> {
77 let passphrase_is_mainnet = config.passphrase == MAINNET_PASSPHRASE;
78 let rpc_is_mainnet = rpc_host_is_mainnet(&config.rpc_url);
79
80 if passphrase_is_mainnet && !network_explicit {
81 return Err(MainnetSafetyError(
82 "Refusing mutating operation on mainnet: the network was not explicitly selected. \
83 Pass --network-passphrase 'Public Global Stellar Network ; September 2015' \
84 (or use --network-profile) to target mainnet deliberately."
85 .to_string(),
86 ));
87 }
88
89 if rpc_is_mainnet && !passphrase_is_mainnet {
90 return Err(MainnetSafetyError(
91 "Refusing mutating operation: the RPC URL targets mainnet but the network passphrase \
92 is not the mainnet passphrase (likely the testnet default). Set --network-passphrase \
93 'Public Global Stellar Network ; September 2015' to match the endpoint."
94 .to_string(),
95 ));
96 }
97
98 Ok(())
99}
100
101#[cfg(test)]
102mod tests {
103 use super::*;
104 use crate::config::NetworkConfig;
105
106 fn testnet_default() -> NetworkConfig {
107 NetworkConfig::default()
108 }
109
110 fn mainnet_cfg() -> NetworkConfig {
111 NetworkConfig {
112 rpc_url: "https://soroban-rpc.stellar.org".to_string(),
113 passphrase: MAINNET_PASSPHRASE.to_string(),
114 timeout_secs: Some(15),
115 pool_max_idle_per_host: Some(100),
116 }
117 }
118
119 #[test]
120 fn testnet_default_is_allowed() {
121 let cfg = testnet_default();
122 assert!(guard_mutating_network(&cfg, false).is_ok());
123 assert!(guard_mutating_network(&cfg, true).is_ok());
124 }
125
126 #[test]
127 fn mainnet_explicit_passphrase_is_allowed() {
128 let cfg = mainnet_cfg();
129 assert!(guard_mutating_network(&cfg, true).is_ok());
131 }
132
133 #[test]
134 fn mainnet_passphrase_without_explicit_opt_in_is_refused() {
135 let cfg = mainnet_cfg();
136 let err = guard_mutating_network(&cfg, false).unwrap_err();
137 assert!(err.to_string().contains("not explicitly selected"));
138 }
139
140 #[test]
141 fn mainnet_rpc_with_testnet_passphrase_is_refused() {
142 let cfg = NetworkConfig {
143 rpc_url: "https://soroban-rpc.stellar.org".to_string(),
144 passphrase: TESTNET_PASSPHRASE.to_string(),
145 timeout_secs: Some(15),
146 pool_max_idle_per_host: Some(100),
147 };
148 let err = guard_mutating_network(&cfg, true).unwrap_err();
150 assert!(err.to_string().contains("not the mainnet passphrase"));
151 }
152
153 #[test]
154 fn mainnet_rpc_with_testnet_default_is_refused() {
155 let cfg = NetworkConfig {
157 rpc_url: "https://soroban-rpc.stellar.org".to_string(),
158 passphrase: TESTNET_PASSPHRASE.to_string(),
159 timeout_secs: Some(15),
160 pool_max_idle_per_host: Some(100),
161 };
162 let err = guard_mutating_network(&cfg, false).unwrap_err();
163 assert!(err.to_string().contains("not the mainnet passphrase"));
164 }
165
166 #[test]
167 fn unknown_rpc_host_is_not_treated_as_mainnet() {
168 let cfg = NetworkConfig {
169 rpc_url: "https://my-custom-rpc.example.com".to_string(),
170 passphrase: TESTNET_PASSPHRASE.to_string(),
171 timeout_secs: Some(15),
172 pool_max_idle_per_host: Some(100),
173 };
174 assert!(guard_mutating_network(&cfg, true).is_ok());
175 }
176
177 #[test]
178 fn testnet_rpc_url_is_not_mainnet() {
179 let cfg = NetworkConfig {
180 rpc_url: "https://soroban-testnet.stellar.org".to_string(),
181 passphrase: TESTNET_PASSPHRASE.to_string(),
182 timeout_secs: Some(15),
183 pool_max_idle_per_host: Some(100),
184 };
185 assert!(guard_mutating_network(&cfg, true).is_ok());
186 }
187}