bsv_wallet_cli/
services_env.rs1use anyhow::{Context, Result};
20use bsv_wallet_toolbox::{
21 services::{ArcadeConfig, ARCADE_V2_MAINNET},
22 ArcConfig, Chain, ServicesOptions,
23};
24use std::io::Write;
25use std::path::PathBuf;
26
27pub const DEFAULT_MAINNET_CHAINTRACKS_URL: &str = "https://mainnet-chaintracks.babbage.systems";
32pub const DEFAULT_TESTNET_CHAINTRACKS_URL: &str = "https://testnet-chaintracks.babbage.systems";
34
35pub fn chaintracks_url_for(chain: Chain, configured: Option<&str>) -> Option<String> {
43 match configured.map(str::trim) {
44 Some(v) if v.eq_ignore_ascii_case("off") => None,
45 Some(v) if !v.is_empty() => Some(v.to_string()),
46 _ => Some(
47 match chain {
48 Chain::Main => DEFAULT_MAINNET_CHAINTRACKS_URL,
49 Chain::Test => DEFAULT_TESTNET_CHAINTRACKS_URL,
50 }
51 .to_string(),
52 ),
53 }
54}
55
56#[derive(Debug, Clone)]
58pub struct ArcadeRuntime {
59 pub url: String,
61 pub callback_token: String,
63 pub public_callback_url: Option<String>,
65}
66
67pub fn arcade_mode_enabled() -> bool {
69 if let Ok(mode) = std::env::var("ARC_MODE") {
70 if mode.eq_ignore_ascii_case("arcade") {
71 return true;
72 }
73 }
74 if let Ok(v) = std::env::var("ARCADE") {
75 let v = v.trim();
76 return v == "1" || v.eq_ignore_ascii_case("true") || v.eq_ignore_ascii_case("yes");
77 }
78 false
79}
80
81pub fn arcade_runtime(db_path: &str) -> Result<Option<ArcadeRuntime>> {
87 if !arcade_mode_enabled() {
88 return Ok(None);
89 }
90 let url = std::env::var("ARC_URL").unwrap_or_else(|_| ARCADE_V2_MAINNET.to_string());
91 let callback_token = resolve_callback_token(db_path)?;
92 let public_callback_url = std::env::var("PUBLIC_CALLBACK_URL")
93 .ok()
94 .filter(|s| !s.is_empty());
95 Ok(Some(ArcadeRuntime {
96 url,
97 callback_token,
98 public_callback_url,
99 }))
100}
101
102pub fn resolve_callback_token(db_path: &str) -> Result<String> {
108 if let Ok(tok) = std::env::var("CALLBACK_TOKEN") {
109 let tok = tok.trim().to_string();
110 if !tok.is_empty() {
111 return Ok(tok);
112 }
113 }
114
115 let token_path = callback_token_path(db_path);
116 if token_path.exists() {
117 let tok = std::fs::read_to_string(&token_path)
118 .with_context(|| format!("reading {}", token_path.display()))?
119 .trim()
120 .to_string();
121 if !tok.is_empty() {
122 return Ok(tok);
123 }
124 }
125
126 let tok: String = bsv_sdk::primitives::PrivateKey::random()
129 .to_hex()
130 .chars()
131 .take(32)
132 .collect();
133
134 let mut opts = std::fs::OpenOptions::new();
135 opts.write(true).create_new(true);
136 #[cfg(unix)]
137 {
138 use std::os::unix::fs::OpenOptionsExt;
139 opts.mode(0o600);
140 }
141 let mut f = opts
142 .open(&token_path)
143 .with_context(|| format!("creating {}", token_path.display()))?;
144 f.write_all(tok.as_bytes())?;
145 tracing::info!(path = %token_path.display(), "generated per-wallet callback token");
146 Ok(tok)
147}
148
149pub fn callback_token_path(db_path: &str) -> PathBuf {
152 PathBuf::from(format!("{}.callback-token", db_path))
153}
154
155pub fn services_options_from_env(chain: Chain, db_path: &str) -> Result<ServicesOptions> {
160 let mut opts = match chain {
161 Chain::Main => ServicesOptions::mainnet(),
162 Chain::Test => ServicesOptions::testnet(),
163 };
164
165 let configured = std::env::var("CHAINTRACKS_URL").ok();
166 match chaintracks_url_for(chain, configured.as_deref()) {
167 Some(url) => opts = opts.with_chaintracks_url(url),
168 None => tracing::warn!(
169 "CHAINTRACKS_URL=off: merkle proofs will be stored without header validation"
170 ),
171 }
172
173 let mut arc_config: Option<ArcConfig> = None;
180 if let Ok(key) = std::env::var("TAAL_API_KEY") {
181 if !key.is_empty() {
182 arc_config = Some(ArcConfig::with_api_key(key));
183 }
184 }
185 if let Ok(key) = std::env::var("MAIN_TAAL_API_KEY") {
186 if !key.is_empty() {
187 let mut headers = std::collections::HashMap::new();
188 headers.insert("Authorization".to_string(), key);
189 let mut cfg = arc_config.unwrap_or_default();
190 cfg.headers = Some(headers);
191 arc_config = Some(cfg);
192 }
193 }
194
195 if let Some(runtime) = arcade_runtime(db_path)? {
196 let arcade_config = ArcadeConfig {
200 callback_token: Some(runtime.callback_token.clone()),
201 callback_url: runtime.public_callback_url.clone(),
202 ..Default::default()
203 };
204 opts.arc_config = arc_config;
205 opts = opts.with_arcade(runtime.url, Some(arcade_config));
206 } else {
207 let arc_url = std::env::var("ARC_URL")
209 .ok()
210 .filter(|s| !s.is_empty())
211 .unwrap_or_else(|| opts.arc_url.clone());
212 opts = opts.with_arc(arc_url, arc_config);
213 }
214
215 Ok(opts)
216}
217
218#[cfg(test)]
219mod tests {
220 use super::*;
221
222 #[test]
223 fn chaintracks_defaults_to_the_public_instance_for_the_chain() {
224 assert_eq!(
225 chaintracks_url_for(Chain::Main, None).as_deref(),
226 Some(DEFAULT_MAINNET_CHAINTRACKS_URL)
227 );
228 assert_eq!(
229 chaintracks_url_for(Chain::Test, Some("")).as_deref(),
230 Some(DEFAULT_TESTNET_CHAINTRACKS_URL)
231 );
232 assert_eq!(
233 chaintracks_url_for(Chain::Main, Some(" ")).as_deref(),
234 Some(DEFAULT_MAINNET_CHAINTRACKS_URL)
235 );
236 }
237
238 #[test]
239 fn chaintracks_honours_an_explicit_url() {
240 assert_eq!(
241 chaintracks_url_for(Chain::Main, Some("https://ct.example/v1")).as_deref(),
242 Some("https://ct.example/v1")
243 );
244 }
245
246 #[test]
247 fn chaintracks_off_disables_validation_on_purpose() {
248 assert_eq!(chaintracks_url_for(Chain::Main, Some("off")), None);
249 assert_eq!(chaintracks_url_for(Chain::Test, Some("OFF")), None);
250 }
251
252 #[test]
253 fn callback_token_path_is_next_to_db() {
254 let p = callback_token_path("/tmp/wallet.db");
255 assert_eq!(p, PathBuf::from("/tmp/wallet.db.callback-token"));
256 }
257
258 #[test]
259 fn generated_token_is_32_hex_and_persisted() {
260 let dir = tempfile::tempdir().unwrap();
261 let db = dir.path().join("w.db");
262 let db = db.to_str().unwrap();
263
264 std::env::remove_var("CALLBACK_TOKEN");
266 let tok = resolve_callback_token(db).unwrap();
267 assert_eq!(tok.len(), 32);
268 assert!(tok.chars().all(|c| c.is_ascii_hexdigit()));
269
270 let tok2 = resolve_callback_token(db).unwrap();
272 assert_eq!(tok, tok2);
273
274 assert!(callback_token_path(db).exists());
276 }
277}