config.rs 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695
  1. use std::path::PathBuf;
  2. use bitcoin::hashes::{sha256, Hash};
  3. use cdk::nuts::{CurrencyUnit, PublicKey};
  4. use cdk::Amount;
  5. use cdk_axum::cache;
  6. use cdk_common::common::QuoteTTL;
  7. use config::{Config, ConfigError, File};
  8. use serde::{Deserialize, Serialize};
  9. #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
  10. #[serde(rename_all = "lowercase")]
  11. pub enum LoggingOutput {
  12. /// Log to stderr only
  13. Stderr,
  14. /// Log to file only
  15. File,
  16. /// Log to both stderr and file (default)
  17. #[default]
  18. Both,
  19. }
  20. impl std::str::FromStr for LoggingOutput {
  21. type Err = String;
  22. fn from_str(s: &str) -> Result<Self, Self::Err> {
  23. match s.to_lowercase().as_str() {
  24. "stderr" => Ok(LoggingOutput::Stderr),
  25. "file" => Ok(LoggingOutput::File),
  26. "both" => Ok(LoggingOutput::Both),
  27. _ => Err(format!(
  28. "Unknown logging output: {s}. Valid options: stdout, file, both"
  29. )),
  30. }
  31. }
  32. }
  33. #[derive(Debug, Clone, Serialize, Deserialize, Default)]
  34. pub struct LoggingConfig {
  35. /// Where to output logs: stdout, file, or both
  36. #[serde(default)]
  37. pub output: LoggingOutput,
  38. /// Log level for console output (when stdout or both)
  39. pub console_level: Option<String>,
  40. /// Log level for file output (when file or both)
  41. pub file_level: Option<String>,
  42. }
  43. #[derive(Clone, Serialize, Deserialize)]
  44. pub struct Info {
  45. pub url: String,
  46. pub listen_host: String,
  47. pub listen_port: u16,
  48. /// Overrides mnemonic
  49. pub seed: Option<String>,
  50. pub mnemonic: Option<String>,
  51. pub signatory_url: Option<String>,
  52. pub signatory_certs: Option<String>,
  53. pub input_fee_ppk: Option<u64>,
  54. pub http_cache: cache::Config,
  55. /// Logging configuration
  56. #[serde(default)]
  57. pub logging: LoggingConfig,
  58. /// When this is set to true, the mint exposes a Swagger UI for it's API at
  59. /// `[listen_host]:[listen_port]/swagger-ui`
  60. ///
  61. /// This requires `mintd` was built with the `swagger` feature flag.
  62. pub enable_swagger_ui: Option<bool>,
  63. /// Optional persisted quote TTL values (seconds) to initialize the database with
  64. /// when RPC is disabled or on first-run when RPC is enabled.
  65. /// If not provided, defaults are used.
  66. #[serde(skip_serializing_if = "Option::is_none")]
  67. pub quote_ttl: Option<QuoteTTL>,
  68. }
  69. impl Default for Info {
  70. fn default() -> Self {
  71. Info {
  72. url: String::new(),
  73. listen_host: "127.0.0.1".to_string(),
  74. listen_port: 8091, // Default to port 8091 instead of 0
  75. seed: None,
  76. mnemonic: None,
  77. signatory_url: None,
  78. signatory_certs: None,
  79. input_fee_ppk: None,
  80. http_cache: cache::Config::default(),
  81. enable_swagger_ui: None,
  82. logging: LoggingConfig::default(),
  83. quote_ttl: None,
  84. }
  85. }
  86. }
  87. impl std::fmt::Debug for Info {
  88. fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
  89. // Use a fallback approach that won't panic
  90. let mnemonic_display: String = {
  91. if let Some(mnemonic) = self.mnemonic.as_ref() {
  92. let hash = sha256::Hash::hash(mnemonic.as_bytes());
  93. format!("<hashed: {hash}>")
  94. } else {
  95. format!("<url: {}>", self.signatory_url.clone().unwrap_or_default())
  96. }
  97. };
  98. f.debug_struct("Info")
  99. .field("url", &self.url)
  100. .field("listen_host", &self.listen_host)
  101. .field("listen_port", &self.listen_port)
  102. .field("mnemonic", &mnemonic_display)
  103. .field("input_fee_ppk", &self.input_fee_ppk)
  104. .field("http_cache", &self.http_cache)
  105. .field("logging", &self.logging)
  106. .field("enable_swagger_ui", &self.enable_swagger_ui)
  107. .finish()
  108. }
  109. }
  110. #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
  111. #[serde(rename_all = "lowercase")]
  112. pub enum LnBackend {
  113. #[default]
  114. None,
  115. #[cfg(feature = "cln")]
  116. Cln,
  117. #[cfg(feature = "lnbits")]
  118. LNbits,
  119. #[cfg(feature = "fakewallet")]
  120. FakeWallet,
  121. #[cfg(feature = "lnd")]
  122. Lnd,
  123. #[cfg(feature = "ldk-node")]
  124. LdkNode,
  125. #[cfg(feature = "grpc-processor")]
  126. GrpcProcessor,
  127. }
  128. impl std::str::FromStr for LnBackend {
  129. type Err = String;
  130. fn from_str(s: &str) -> Result<Self, Self::Err> {
  131. match s.to_lowercase().as_str() {
  132. #[cfg(feature = "cln")]
  133. "cln" => Ok(LnBackend::Cln),
  134. #[cfg(feature = "lnbits")]
  135. "lnbits" => Ok(LnBackend::LNbits),
  136. #[cfg(feature = "fakewallet")]
  137. "fakewallet" => Ok(LnBackend::FakeWallet),
  138. #[cfg(feature = "lnd")]
  139. "lnd" => Ok(LnBackend::Lnd),
  140. #[cfg(feature = "ldk-node")]
  141. "ldk-node" | "ldknode" => Ok(LnBackend::LdkNode),
  142. #[cfg(feature = "grpc-processor")]
  143. "grpcprocessor" => Ok(LnBackend::GrpcProcessor),
  144. _ => Err(format!("Unknown Lightning backend: {s}")),
  145. }
  146. }
  147. }
  148. #[derive(Debug, Clone, Serialize, Deserialize)]
  149. pub struct Ln {
  150. pub ln_backend: LnBackend,
  151. pub invoice_description: Option<String>,
  152. pub min_mint: Amount,
  153. pub max_mint: Amount,
  154. pub min_melt: Amount,
  155. pub max_melt: Amount,
  156. }
  157. impl Default for Ln {
  158. fn default() -> Self {
  159. Ln {
  160. ln_backend: LnBackend::default(),
  161. invoice_description: None,
  162. min_mint: 1.into(),
  163. max_mint: 500_000.into(),
  164. min_melt: 1.into(),
  165. max_melt: 500_000.into(),
  166. }
  167. }
  168. }
  169. #[cfg(feature = "lnbits")]
  170. #[derive(Debug, Clone, Serialize, Deserialize, Default)]
  171. pub struct LNbits {
  172. pub admin_api_key: String,
  173. pub invoice_api_key: String,
  174. pub lnbits_api: String,
  175. pub fee_percent: f32,
  176. pub reserve_fee_min: Amount,
  177. }
  178. #[cfg(feature = "cln")]
  179. #[derive(Debug, Clone, Serialize, Deserialize, Default)]
  180. pub struct Cln {
  181. pub rpc_path: PathBuf,
  182. #[serde(default)]
  183. pub bolt12: bool,
  184. pub fee_percent: f32,
  185. pub reserve_fee_min: Amount,
  186. }
  187. #[cfg(feature = "lnd")]
  188. #[derive(Debug, Clone, Serialize, Deserialize, Default)]
  189. pub struct Lnd {
  190. pub address: String,
  191. pub cert_file: PathBuf,
  192. pub macaroon_file: PathBuf,
  193. pub fee_percent: f32,
  194. pub reserve_fee_min: Amount,
  195. }
  196. #[cfg(feature = "ldk-node")]
  197. #[derive(Debug, Clone, Serialize, Deserialize)]
  198. pub struct LdkNode {
  199. /// Fee percentage (e.g., 0.02 for 2%)
  200. #[serde(default = "default_ldk_fee_percent")]
  201. pub fee_percent: f32,
  202. /// Minimum reserve fee
  203. #[serde(default = "default_ldk_reserve_fee_min")]
  204. pub reserve_fee_min: Amount,
  205. /// Bitcoin network (mainnet, testnet, signet, regtest)
  206. pub bitcoin_network: Option<String>,
  207. /// Chain source type (esplora or bitcoinrpc)
  208. pub chain_source_type: Option<String>,
  209. /// Esplora URL (when chain_source_type = "esplora")
  210. pub esplora_url: Option<String>,
  211. /// Bitcoin RPC configuration (when chain_source_type = "bitcoinrpc")
  212. pub bitcoind_rpc_host: Option<String>,
  213. pub bitcoind_rpc_port: Option<u16>,
  214. pub bitcoind_rpc_user: Option<String>,
  215. pub bitcoind_rpc_password: Option<String>,
  216. /// Storage directory path
  217. pub storage_dir_path: Option<String>,
  218. /// LDK node listening host
  219. pub ldk_node_host: Option<String>,
  220. /// LDK node listening port
  221. pub ldk_node_port: Option<u16>,
  222. /// Gossip source type (p2p or rgs)
  223. pub gossip_source_type: Option<String>,
  224. /// Rapid Gossip Sync URL (when gossip_source_type = "rgs")
  225. pub rgs_url: Option<String>,
  226. /// Webserver host (defaults to 127.0.0.1)
  227. #[serde(default = "default_webserver_host")]
  228. pub webserver_host: Option<String>,
  229. /// Webserver port
  230. #[serde(default = "default_webserver_port")]
  231. pub webserver_port: Option<u16>,
  232. }
  233. #[cfg(feature = "ldk-node")]
  234. impl Default for LdkNode {
  235. fn default() -> Self {
  236. Self {
  237. fee_percent: default_ldk_fee_percent(),
  238. reserve_fee_min: default_ldk_reserve_fee_min(),
  239. bitcoin_network: None,
  240. chain_source_type: None,
  241. esplora_url: None,
  242. bitcoind_rpc_host: None,
  243. bitcoind_rpc_port: None,
  244. bitcoind_rpc_user: None,
  245. bitcoind_rpc_password: None,
  246. storage_dir_path: None,
  247. ldk_node_host: None,
  248. ldk_node_port: None,
  249. gossip_source_type: None,
  250. rgs_url: None,
  251. webserver_host: default_webserver_host(),
  252. webserver_port: default_webserver_port(),
  253. }
  254. }
  255. }
  256. #[cfg(feature = "ldk-node")]
  257. fn default_ldk_fee_percent() -> f32 {
  258. 0.04
  259. }
  260. #[cfg(feature = "ldk-node")]
  261. fn default_ldk_reserve_fee_min() -> Amount {
  262. 4.into()
  263. }
  264. #[cfg(feature = "ldk-node")]
  265. fn default_webserver_host() -> Option<String> {
  266. Some("127.0.0.1".to_string())
  267. }
  268. #[cfg(feature = "ldk-node")]
  269. fn default_webserver_port() -> Option<u16> {
  270. Some(8091)
  271. }
  272. #[cfg(feature = "fakewallet")]
  273. #[derive(Debug, Clone, Serialize, Deserialize)]
  274. pub struct FakeWallet {
  275. pub supported_units: Vec<CurrencyUnit>,
  276. pub fee_percent: f32,
  277. pub reserve_fee_min: Amount,
  278. #[serde(default = "default_min_delay_time")]
  279. pub min_delay_time: u64,
  280. #[serde(default = "default_max_delay_time")]
  281. pub max_delay_time: u64,
  282. }
  283. #[cfg(feature = "fakewallet")]
  284. impl Default for FakeWallet {
  285. fn default() -> Self {
  286. Self {
  287. supported_units: vec![CurrencyUnit::Sat],
  288. fee_percent: 0.02,
  289. reserve_fee_min: 2.into(),
  290. min_delay_time: 1,
  291. max_delay_time: 3,
  292. }
  293. }
  294. }
  295. // Helper functions to provide default values
  296. #[cfg(feature = "fakewallet")]
  297. fn default_min_delay_time() -> u64 {
  298. 1
  299. }
  300. #[cfg(feature = "fakewallet")]
  301. fn default_max_delay_time() -> u64 {
  302. 3
  303. }
  304. #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
  305. pub struct GrpcProcessor {
  306. pub supported_units: Vec<CurrencyUnit>,
  307. pub addr: String,
  308. pub port: u16,
  309. pub tls_dir: Option<PathBuf>,
  310. }
  311. #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
  312. #[serde(rename_all = "lowercase")]
  313. pub enum DatabaseEngine {
  314. #[default]
  315. Sqlite,
  316. Postgres,
  317. }
  318. impl std::str::FromStr for DatabaseEngine {
  319. type Err = String;
  320. fn from_str(s: &str) -> Result<Self, Self::Err> {
  321. match s.to_lowercase().as_str() {
  322. "sqlite" => Ok(DatabaseEngine::Sqlite),
  323. "postgres" => Ok(DatabaseEngine::Postgres),
  324. _ => Err(format!("Unknown database engine: {s}")),
  325. }
  326. }
  327. }
  328. #[derive(Debug, Clone, Serialize, Deserialize, Default)]
  329. pub struct Database {
  330. pub engine: DatabaseEngine,
  331. pub postgres: Option<PostgresConfig>,
  332. }
  333. #[derive(Debug, Clone, Serialize, Deserialize, Default)]
  334. pub struct AuthDatabase {
  335. pub postgres: Option<PostgresAuthConfig>,
  336. }
  337. #[derive(Debug, Clone, Serialize, Deserialize)]
  338. pub struct PostgresAuthConfig {
  339. pub url: String,
  340. pub tls_mode: Option<String>,
  341. pub max_connections: Option<usize>,
  342. pub connection_timeout_seconds: Option<u64>,
  343. }
  344. impl Default for PostgresAuthConfig {
  345. fn default() -> Self {
  346. Self {
  347. url: String::new(),
  348. tls_mode: Some("disable".to_string()),
  349. max_connections: Some(20),
  350. connection_timeout_seconds: Some(10),
  351. }
  352. }
  353. }
  354. #[derive(Debug, Clone, Serialize, Deserialize)]
  355. pub struct PostgresConfig {
  356. pub url: String,
  357. pub tls_mode: Option<String>,
  358. pub max_connections: Option<usize>,
  359. pub connection_timeout_seconds: Option<u64>,
  360. }
  361. impl Default for PostgresConfig {
  362. fn default() -> Self {
  363. Self {
  364. url: String::new(),
  365. tls_mode: Some("disable".to_string()),
  366. max_connections: Some(20),
  367. connection_timeout_seconds: Some(10),
  368. }
  369. }
  370. }
  371. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
  372. #[serde(rename_all = "lowercase")]
  373. pub enum AuthType {
  374. Clear,
  375. Blind,
  376. #[default]
  377. None,
  378. }
  379. impl std::str::FromStr for AuthType {
  380. type Err = String;
  381. fn from_str(s: &str) -> Result<Self, Self::Err> {
  382. match s.to_lowercase().as_str() {
  383. "clear" => Ok(AuthType::Clear),
  384. "blind" => Ok(AuthType::Blind),
  385. "none" => Ok(AuthType::None),
  386. _ => Err(format!("Unknown auth type: {s}")),
  387. }
  388. }
  389. }
  390. #[derive(Debug, Clone, Default, Serialize, Deserialize)]
  391. pub struct Auth {
  392. #[serde(default)]
  393. pub auth_enabled: bool,
  394. pub openid_discovery: String,
  395. pub openid_client_id: String,
  396. pub mint_max_bat: u64,
  397. #[serde(default = "default_blind")]
  398. pub mint: AuthType,
  399. #[serde(default)]
  400. pub get_mint_quote: AuthType,
  401. #[serde(default)]
  402. pub check_mint_quote: AuthType,
  403. #[serde(default)]
  404. pub melt: AuthType,
  405. #[serde(default)]
  406. pub get_melt_quote: AuthType,
  407. #[serde(default)]
  408. pub check_melt_quote: AuthType,
  409. #[serde(default = "default_blind")]
  410. pub swap: AuthType,
  411. #[serde(default = "default_blind")]
  412. pub restore: AuthType,
  413. #[serde(default)]
  414. pub check_proof_state: AuthType,
  415. /// Enable WebSocket authentication support
  416. #[serde(default = "default_blind")]
  417. pub websocket_auth: AuthType,
  418. }
  419. fn default_blind() -> AuthType {
  420. AuthType::Blind
  421. }
  422. /// CDK settings, derived from `config.toml`
  423. #[derive(Debug, Clone, Serialize, Deserialize, Default)]
  424. pub struct Settings {
  425. pub info: Info,
  426. pub mint_info: MintInfo,
  427. pub ln: Ln,
  428. #[cfg(feature = "cln")]
  429. pub cln: Option<Cln>,
  430. #[cfg(feature = "lnbits")]
  431. pub lnbits: Option<LNbits>,
  432. #[cfg(feature = "lnd")]
  433. pub lnd: Option<Lnd>,
  434. #[cfg(feature = "ldk-node")]
  435. pub ldk_node: Option<LdkNode>,
  436. #[cfg(feature = "fakewallet")]
  437. pub fake_wallet: Option<FakeWallet>,
  438. pub grpc_processor: Option<GrpcProcessor>,
  439. pub database: Database,
  440. #[cfg(feature = "auth")]
  441. pub auth_database: Option<AuthDatabase>,
  442. #[cfg(feature = "management-rpc")]
  443. pub mint_management_rpc: Option<MintManagementRpc>,
  444. pub auth: Option<Auth>,
  445. #[cfg(feature = "prometheus")]
  446. pub prometheus: Option<Prometheus>,
  447. }
  448. #[derive(Debug, Clone, Serialize, Deserialize, Default)]
  449. #[cfg(feature = "prometheus")]
  450. pub struct Prometheus {
  451. pub enabled: bool,
  452. pub address: Option<String>,
  453. pub port: Option<u16>,
  454. }
  455. #[derive(Debug, Clone, Serialize, Deserialize, Default)]
  456. pub struct MintInfo {
  457. /// name of the mint and should be recognizable
  458. pub name: String,
  459. /// hex pubkey of the mint
  460. pub pubkey: Option<PublicKey>,
  461. /// short description of the mint
  462. pub description: String,
  463. /// long description
  464. pub description_long: Option<String>,
  465. /// url to the mint icon
  466. pub icon_url: Option<String>,
  467. /// message of the day that the wallet must display to the user
  468. pub motd: Option<String>,
  469. /// Nostr publickey
  470. pub contact_nostr_public_key: Option<String>,
  471. /// Contact email
  472. pub contact_email: Option<String>,
  473. /// URL to the terms of service
  474. pub tos_url: Option<String>,
  475. }
  476. #[cfg(feature = "management-rpc")]
  477. #[derive(Debug, Clone, Serialize, Deserialize, Default)]
  478. pub struct MintManagementRpc {
  479. /// When this is set to `true` the mint use the config file for the initial set up on first start.
  480. /// Changes to the `[mint_info]` after this **MUST** be made via the RPC changes to the config file or env vars will be ignored.
  481. pub enabled: bool,
  482. pub address: Option<String>,
  483. pub port: Option<u16>,
  484. pub tls_dir_path: Option<PathBuf>,
  485. }
  486. impl Settings {
  487. #[must_use]
  488. pub fn new<P>(config_file_name: Option<P>) -> Self
  489. where
  490. P: Into<PathBuf>,
  491. {
  492. let default_settings = Self::default();
  493. // attempt to construct settings with file
  494. let from_file = Self::new_from_default(&default_settings, config_file_name);
  495. match from_file {
  496. Ok(f) => f,
  497. Err(e) => {
  498. tracing::error!(
  499. "Error reading config file, falling back to defaults. Error: {e:?}"
  500. );
  501. default_settings
  502. }
  503. }
  504. }
  505. fn new_from_default<P>(
  506. default: &Settings,
  507. config_file_name: Option<P>,
  508. ) -> Result<Self, ConfigError>
  509. where
  510. P: Into<PathBuf>,
  511. {
  512. let mut default_config_file_name = home::home_dir()
  513. .ok_or(ConfigError::NotFound("Config Path".to_string()))?
  514. .join("cashu-rs-mint");
  515. default_config_file_name.push("config.toml");
  516. let config: String = match config_file_name {
  517. Some(value) => value.into().to_string_lossy().to_string(),
  518. None => default_config_file_name.to_string_lossy().to_string(),
  519. };
  520. let builder = Config::builder();
  521. let config: Config = builder
  522. // use defaults
  523. .add_source(Config::try_from(default)?)
  524. // override with file contents
  525. .add_source(File::with_name(&config))
  526. .build()?;
  527. let settings: Settings = config.try_deserialize()?;
  528. match settings.ln.ln_backend {
  529. LnBackend::None => panic!("Ln backend must be set"),
  530. #[cfg(feature = "cln")]
  531. LnBackend::Cln => assert!(
  532. settings.cln.is_some(),
  533. "CLN backend requires a valid config."
  534. ),
  535. #[cfg(feature = "lnbits")]
  536. LnBackend::LNbits => assert!(
  537. settings.lnbits.is_some(),
  538. "LNbits backend requires a valid config"
  539. ),
  540. #[cfg(feature = "lnd")]
  541. LnBackend::Lnd => {
  542. assert!(
  543. settings.lnd.is_some(),
  544. "LND backend requires a valid config."
  545. )
  546. }
  547. #[cfg(feature = "ldk-node")]
  548. LnBackend::LdkNode => {
  549. assert!(
  550. settings.ldk_node.is_some(),
  551. "LDK Node backend requires a valid config."
  552. )
  553. }
  554. #[cfg(feature = "fakewallet")]
  555. LnBackend::FakeWallet => assert!(
  556. settings.fake_wallet.is_some(),
  557. "FakeWallet backend requires a valid config."
  558. ),
  559. #[cfg(feature = "grpc-processor")]
  560. LnBackend::GrpcProcessor => {
  561. assert!(
  562. settings.grpc_processor.is_some(),
  563. "GRPC backend requires a valid config."
  564. )
  565. }
  566. }
  567. Ok(settings)
  568. }
  569. }
  570. #[cfg(test)]
  571. mod tests {
  572. use super::*;
  573. #[test]
  574. fn test_info_debug_impl() {
  575. // Create a sample Info struct with test data
  576. let info = Info {
  577. url: "http://example.com".to_string(),
  578. listen_host: "127.0.0.1".to_string(),
  579. listen_port: 8080,
  580. mnemonic: Some("test secret mnemonic phrase".to_string()),
  581. input_fee_ppk: Some(100),
  582. ..Default::default()
  583. };
  584. // Convert the Info struct to a debug string
  585. let debug_output = format!("{info:?}");
  586. // Verify the debug output contains expected fields
  587. assert!(debug_output.contains("url: \"http://example.com\""));
  588. assert!(debug_output.contains("listen_host: \"127.0.0.1\""));
  589. assert!(debug_output.contains("listen_port: 8080"));
  590. // The mnemonic should be hashed, not displayed in plaintext
  591. assert!(!debug_output.contains("test secret mnemonic phrase"));
  592. assert!(debug_output.contains("<hashed: "));
  593. assert!(debug_output.contains("input_fee_ppk: Some(100)"));
  594. }
  595. #[test]
  596. fn test_info_debug_with_empty_mnemonic() {
  597. // Test with an empty mnemonic to ensure it doesn't panic
  598. let info = Info {
  599. url: "http://example.com".to_string(),
  600. listen_host: "127.0.0.1".to_string(),
  601. listen_port: 8080,
  602. mnemonic: Some("".to_string()), // Empty mnemonic
  603. enable_swagger_ui: Some(false),
  604. ..Default::default()
  605. };
  606. // This should not panic
  607. let debug_output = format!("{:?}", info);
  608. // The empty mnemonic should still be hashed
  609. assert!(debug_output.contains("<hashed: "));
  610. }
  611. #[test]
  612. fn test_info_debug_with_special_chars() {
  613. // Test with a mnemonic containing special characters
  614. let info = Info {
  615. url: "http://example.com".to_string(),
  616. listen_host: "127.0.0.1".to_string(),
  617. listen_port: 8080,
  618. mnemonic: Some("特殊字符 !@#$%^&*()".to_string()), // Special characters
  619. ..Default::default()
  620. };
  621. // This should not panic
  622. let debug_output = format!("{:?}", info);
  623. // The mnemonic with special chars should be hashed
  624. assert!(!debug_output.contains("特殊字符 !@#$%^&*()"));
  625. assert!(debug_output.contains("<hashed: "));
  626. }
  627. }