Skip to main content

stratum_apps/
tp_type.rs

1use crate::key_utils::Secp256k1PublicKey;
2use std::path::PathBuf;
3
4#[cfg(feature = "bitcoin-core-sv2")]
5use crate::config_helpers::opt_path_from_toml;
6
7#[cfg(feature = "bitcoin-core-sv2")]
8use bitcoin_core_sv2::common::BitcoinCoreVersion;
9
10/// Bitcoin network for determining node.sock location
11#[derive(Clone, Debug, serde::Deserialize)]
12#[serde(rename_all = "lowercase")]
13pub enum BitcoinNetwork {
14    Mainnet,
15    Testnet4,
16    Signet,
17    Regtest,
18}
19
20#[cfg(feature = "bitcoin-core-sv2")]
21fn deserialize_bitcoin_core_version<'de, D>(deserializer: D) -> Result<BitcoinCoreVersion, D::Error>
22where
23    D: serde::Deserializer<'de>,
24{
25    let major = <u8 as serde::Deserialize>::deserialize(deserializer)?;
26    BitcoinCoreVersion::try_from(major).map_err(|unsupported| {
27        serde::de::Error::custom(format!(
28            "unsupported Bitcoin Core IPC version: {unsupported}. expected 30 or 31"
29        ))
30    })
31}
32
33impl BitcoinNetwork {
34    /// Returns the subdirectory name for this network.
35    /// Mainnet uses the root data directory.
36    fn subdir(&self) -> Option<&'static str> {
37        match self {
38            BitcoinNetwork::Mainnet => None,
39            BitcoinNetwork::Testnet4 => Some("testnet4"),
40            BitcoinNetwork::Signet => Some("signet"),
41            BitcoinNetwork::Regtest => Some("regtest"),
42        }
43    }
44}
45
46/// Returns the default Bitcoin Core data directory for the current OS.
47fn default_bitcoin_data_dir() -> Option<PathBuf> {
48    #[cfg(target_os = "linux")]
49    {
50        dirs::home_dir().map(|h| h.join(".bitcoin"))
51    }
52    #[cfg(target_os = "macos")]
53    {
54        dirs::home_dir().map(|h| h.join("Library/Application Support/Bitcoin"))
55    }
56    #[cfg(not(any(target_os = "linux", target_os = "macos")))]
57    {
58        None
59    }
60}
61
62/// Resolves the IPC socket path from network and optional data_dir.
63/// Constructs path from network + optional data_dir (or OS default).
64///
65/// Returns `None` if data_dir cannot be determined (neither provided nor OS default available).
66pub fn resolve_ipc_socket_path(
67    network: &BitcoinNetwork,
68    data_dir: Option<PathBuf>,
69) -> Option<PathBuf> {
70    let base_dir = data_dir.or_else(default_bitcoin_data_dir)?;
71
72    Some(match network.subdir() {
73        Some(subdir) => base_dir.join(subdir).join("node.sock"),
74        None => base_dir.join("node.sock"),
75    })
76}
77
78/// Which type of Template Provider will be used,
79/// along with the relevant config parameters for each.
80#[derive(Clone, Debug, serde::Deserialize)]
81pub enum TemplateProviderType {
82    Sv2Tp {
83        address: String,
84        public_key: Option<Secp256k1PublicKey>,
85    },
86    #[cfg(feature = "bitcoin-core-sv2")]
87    BitcoinCoreIpc {
88        /// Bitcoin Core IPC schema major version.
89        #[serde(deserialize_with = "deserialize_bitcoin_core_version")]
90        version: BitcoinCoreVersion,
91        /// Network for determining socket path subdirectory.
92        network: BitcoinNetwork,
93        /// Custom Bitcoin data directory. Uses OS default if not set.
94        #[serde(default, deserialize_with = "opt_path_from_toml")]
95        data_dir: Option<PathBuf>,
96        fee_threshold: u64,
97        min_interval: u8,
98    },
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104
105    #[test]
106    fn network_with_data_dir_mainnet() {
107        let result =
108            resolve_ipc_socket_path(&BitcoinNetwork::Mainnet, Some(PathBuf::from("/data")));
109        assert_eq!(result, Some(PathBuf::from("/data/node.sock")));
110    }
111
112    #[test]
113    fn network_with_data_dir_regtest() {
114        let result =
115            resolve_ipc_socket_path(&BitcoinNetwork::Regtest, Some(PathBuf::from("/data")));
116        assert_eq!(result, Some(PathBuf::from("/data/regtest/node.sock")));
117    }
118
119    #[test]
120    fn network_with_data_dir_signet() {
121        let result = resolve_ipc_socket_path(&BitcoinNetwork::Signet, Some(PathBuf::from("/data")));
122        assert_eq!(result, Some(PathBuf::from("/data/signet/node.sock")));
123    }
124
125    #[test]
126    fn network_with_data_dir_testnet4() {
127        let result =
128            resolve_ipc_socket_path(&BitcoinNetwork::Testnet4, Some(PathBuf::from("/data")));
129        assert_eq!(result, Some(PathBuf::from("/data/testnet4/node.sock")));
130    }
131
132    #[test]
133    fn missing_data_dir_uses_os_default() {
134        // This test verifies behavior when data_dir is None
135        // Result depends on OS - will be Some on Linux/macOS, None on unsupported OS
136        let result = resolve_ipc_socket_path(&BitcoinNetwork::Regtest, None);
137        #[cfg(any(target_os = "linux", target_os = "macos"))]
138        assert!(result.is_some());
139        #[cfg(not(any(target_os = "linux", target_os = "macos")))]
140        assert!(result.is_none());
141    }
142
143    #[cfg(feature = "bitcoin-core-sv2")]
144    #[test]
145    fn bitcoin_core_version_accepts_30_and_31() {
146        assert!(matches!(
147            BitcoinCoreVersion::try_from(30),
148            Ok(BitcoinCoreVersion::V30X)
149        ));
150        assert!(matches!(
151            BitcoinCoreVersion::try_from(31),
152            Ok(BitcoinCoreVersion::V31X)
153        ));
154    }
155
156    #[cfg(feature = "bitcoin-core-sv2")]
157    #[test]
158    fn bitcoin_core_version_rejects_unsupported_values() {
159        assert!(BitcoinCoreVersion::try_from(29).is_err());
160        assert!(BitcoinCoreVersion::try_from(32).is_err());
161    }
162}