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
27#[derive(Debug, Clone)]
29pub struct ArcadeRuntime {
30 pub url: String,
32 pub callback_token: String,
34 pub public_callback_url: Option<String>,
36}
37
38pub fn arcade_mode_enabled() -> bool {
40 if let Ok(mode) = std::env::var("ARC_MODE") {
41 if mode.eq_ignore_ascii_case("arcade") {
42 return true;
43 }
44 }
45 if let Ok(v) = std::env::var("ARCADE") {
46 let v = v.trim();
47 return v == "1" || v.eq_ignore_ascii_case("true") || v.eq_ignore_ascii_case("yes");
48 }
49 false
50}
51
52pub fn arcade_runtime(db_path: &str) -> Result<Option<ArcadeRuntime>> {
58 if !arcade_mode_enabled() {
59 return Ok(None);
60 }
61 let url = std::env::var("ARC_URL").unwrap_or_else(|_| ARCADE_V2_MAINNET.to_string());
62 let callback_token = resolve_callback_token(db_path)?;
63 let public_callback_url = std::env::var("PUBLIC_CALLBACK_URL")
64 .ok()
65 .filter(|s| !s.is_empty());
66 Ok(Some(ArcadeRuntime {
67 url,
68 callback_token,
69 public_callback_url,
70 }))
71}
72
73pub fn resolve_callback_token(db_path: &str) -> Result<String> {
79 if let Ok(tok) = std::env::var("CALLBACK_TOKEN") {
80 let tok = tok.trim().to_string();
81 if !tok.is_empty() {
82 return Ok(tok);
83 }
84 }
85
86 let token_path = callback_token_path(db_path);
87 if token_path.exists() {
88 let tok = std::fs::read_to_string(&token_path)
89 .with_context(|| format!("reading {}", token_path.display()))?
90 .trim()
91 .to_string();
92 if !tok.is_empty() {
93 return Ok(tok);
94 }
95 }
96
97 let tok: String = bsv_sdk::primitives::PrivateKey::random()
100 .to_hex()
101 .chars()
102 .take(32)
103 .collect();
104
105 let mut opts = std::fs::OpenOptions::new();
106 opts.write(true).create_new(true);
107 #[cfg(unix)]
108 {
109 use std::os::unix::fs::OpenOptionsExt;
110 opts.mode(0o600);
111 }
112 let mut f = opts
113 .open(&token_path)
114 .with_context(|| format!("creating {}", token_path.display()))?;
115 f.write_all(tok.as_bytes())?;
116 tracing::info!(path = %token_path.display(), "generated per-wallet callback token");
117 Ok(tok)
118}
119
120pub fn callback_token_path(db_path: &str) -> PathBuf {
123 PathBuf::from(format!("{}.callback-token", db_path))
124}
125
126pub fn services_options_from_env(chain: Chain, db_path: &str) -> Result<ServicesOptions> {
131 let mut opts = match chain {
132 Chain::Main => ServicesOptions::mainnet(),
133 Chain::Test => ServicesOptions::testnet(),
134 };
135
136 if let Ok(url) = std::env::var("CHAINTRACKS_URL") {
137 if !url.is_empty() {
138 opts = opts.with_chaintracks_url(url);
139 }
140 }
141
142 let mut arc_config: Option<ArcConfig> = None;
149 if let Ok(key) = std::env::var("TAAL_API_KEY") {
150 if !key.is_empty() {
151 arc_config = Some(ArcConfig::with_api_key(key));
152 }
153 }
154 if let Ok(key) = std::env::var("MAIN_TAAL_API_KEY") {
155 if !key.is_empty() {
156 let mut headers = std::collections::HashMap::new();
157 headers.insert("Authorization".to_string(), key);
158 let mut cfg = arc_config.unwrap_or_default();
159 cfg.headers = Some(headers);
160 arc_config = Some(cfg);
161 }
162 }
163
164 if let Some(runtime) = arcade_runtime(db_path)? {
165 let arcade_config = ArcadeConfig {
169 callback_token: Some(runtime.callback_token.clone()),
170 callback_url: runtime.public_callback_url.clone(),
171 ..Default::default()
172 };
173 opts.arc_config = arc_config;
174 opts = opts.with_arcade(runtime.url, Some(arcade_config));
175 } else {
176 let arc_url = std::env::var("ARC_URL")
178 .ok()
179 .filter(|s| !s.is_empty())
180 .unwrap_or_else(|| opts.arc_url.clone());
181 opts = opts.with_arc(arc_url, arc_config);
182 }
183
184 Ok(opts)
185}
186
187#[cfg(test)]
188mod tests {
189 use super::*;
190
191 #[test]
192 fn callback_token_path_is_next_to_db() {
193 let p = callback_token_path("/tmp/wallet.db");
194 assert_eq!(p, PathBuf::from("/tmp/wallet.db.callback-token"));
195 }
196
197 #[test]
198 fn generated_token_is_32_hex_and_persisted() {
199 let dir = tempfile::tempdir().unwrap();
200 let db = dir.path().join("w.db");
201 let db = db.to_str().unwrap();
202
203 std::env::remove_var("CALLBACK_TOKEN");
205 let tok = resolve_callback_token(db).unwrap();
206 assert_eq!(tok.len(), 32);
207 assert!(tok.chars().all(|c| c.is_ascii_hexdigit()));
208
209 let tok2 = resolve_callback_token(db).unwrap();
211 assert_eq!(tok, tok2);
212
213 assert!(callback_token_path(db).exists());
215 }
216}