clementine_tx_sender/
config.rs

1//! TxSender standalone configuration.
2
3use crate::MempoolConfig;
4use bitcoin::secp256k1::SecretKey;
5use bitcoin::Network;
6use clementine_config::tx_sender::TxSenderLimits;
7use clementine_errors::BridgeError;
8use secrecy::SecretString;
9use std::str::FromStr;
10
11const DEFAULT_POLL_DELAY_MS: u64 = 30_000;
12
13#[derive(Clone, Debug)]
14pub struct TxSenderPostgresConfig {
15    pub host: String,
16    pub port: u16,
17    pub user: SecretString,
18    pub password: SecretString,
19    pub dbname: String,
20}
21
22#[derive(Clone, Debug)]
23pub struct TxSenderBitcoinRpcConfig {
24    pub url: String,
25    pub user: SecretString,
26    pub password: SecretString,
27}
28
29#[derive(Clone, Debug)]
30pub struct TxSenderJsonRpcConfig {
31    /// Bind address for the JSON-RPC server. Restricted to 127.0.0.1 or 0.0.0.0.
32    pub bind: String,
33    /// TCP port for the JSON-RPC server.
34    pub port: u16,
35}
36
37/// Configuration for running the tx-sender service standalone.
38#[derive(Clone, Debug)]
39pub struct TxSenderConfig {
40    pub network: Network,
41    /// Taproot signing key used by tx-sender.
42    ///
43    /// In clementine_core usage this is derived from `BridgeConfig.secret_key`.
44    /// In standalone usage it is sourced from env `SECRET_KEY`.
45    pub secret_key: SecretKey,
46    /// Optional Citrea DA blob signing key.
47    ///
48    /// If not provided, tx-sender falls back to `secret_key` for Citrea blob signing.
49    pub private_da_key: Option<SecretKey>,
50    pub postgres: TxSenderPostgresConfig,
51    pub bitcoin_rpc: TxSenderBitcoinRpcConfig,
52    pub mempool: MempoolConfig,
53    pub limits: TxSenderLimits,
54    /// How many confirmations are required before tx-sender treats an observation as final.
55    ///
56    /// The chain tip has 1 confirmation. Minimum value should be 1.
57    pub finality_depth: u32,
58
59    /// Poll delay for the txsender loop if txsender is used as standalone, in milliseconds.
60    ///
61    /// If not provided, defaults to 30 seconds.
62    pub poll_delay_ms: u64,
63
64    /// Whether to use unsafe utxos for funding new txs. An utxo is unsafe it belongs to a tx with at least one non wallet input, if it belongs to a tx that was rbf replaced.
65    pub include_unsafe: bool,
66
67    /// Optional JSON-RPC configuration, will not be used if json-rpc feature is not .
68    pub jsonrpc: Option<TxSenderJsonRpcConfig>,
69}
70
71fn env_required(name: &'static str) -> Result<String, BridgeError> {
72    std::env::var(name).map_err(|e| BridgeError::EnvVarNotSet(e, name))
73}
74
75fn env_optional(name: &'static str) -> Option<String> {
76    std::env::var(name).ok()
77}
78
79fn env_parse_required<T: std::str::FromStr>(name: &'static str) -> Result<T, BridgeError>
80where
81    <T as std::str::FromStr>::Err: std::fmt::Debug,
82{
83    env_required(name)?
84        .parse::<T>()
85        .map_err(|e| BridgeError::EnvVarMalformed(name, format!("{e:?}")))
86}
87
88fn env_parse_optional<T: std::str::FromStr>(name: &'static str) -> Result<Option<T>, BridgeError>
89where
90    <T as std::str::FromStr>::Err: std::fmt::Debug,
91{
92    let Some(v) = env_optional(name) else {
93        return Ok(None);
94    };
95    v.parse::<T>()
96        .map(Some)
97        .map_err(|e| BridgeError::EnvVarMalformed(name, format!("{e:?}")))
98}
99
100fn env_parse_optional_or<T: std::str::FromStr>(
101    name: &'static str,
102    default: T,
103) -> Result<T, BridgeError>
104where
105    <T as std::str::FromStr>::Err: std::fmt::Debug,
106{
107    Ok(env_parse_optional::<T>(name)?.unwrap_or(default))
108}
109
110impl TxSenderConfig {
111    pub fn from_env() -> Result<Self, BridgeError> {
112        let network_str = env_required("NETWORK")?;
113        let network = Network::from_str(&network_str)
114            .map_err(|e| BridgeError::EnvVarMalformed("NETWORK", format!("{e:?}")))?;
115
116        let secret_key_str = env_required("SECRET_KEY")?;
117        let secret_key = SecretKey::from_str(&secret_key_str)
118            .map_err(|e| BridgeError::EnvVarMalformed("SECRET_KEY", format!("{e:?}")))?;
119
120        let private_da_key =
121            match env_optional("PRIVATE_DA_KEY") {
122                Some(value) => Some(SecretKey::from_str(&value).map_err(|e| {
123                    BridgeError::EnvVarMalformed("PRIVATE_DA_KEY", format!("{e:?}"))
124                })?),
125                None => None,
126            };
127
128        let postgres = TxSenderPostgresConfig {
129            host: env_required("DB_HOST")?,
130            port: env_parse_required::<u16>("DB_PORT")?,
131            user: env_required("DB_USER")?.into(),
132            password: env_required("DB_PASSWORD")?.into(),
133            dbname: env_required("DB_NAME")?,
134        };
135
136        let bitcoin_rpc = TxSenderBitcoinRpcConfig {
137            url: env_required("BITCOIN_RPC_URL")?,
138            user: env_required("BITCOIN_RPC_USER")?.into(),
139            password: env_required("BITCOIN_RPC_PASSWORD")?.into(),
140        };
141
142        let mempool = MempoolConfig {
143            host: env_optional("MEMPOOL_API_HOST"),
144            endpoint: env_optional("MEMPOOL_API_ENDPOINT"),
145        };
146
147        // Keep limits in sync with existing `TX_SENDER_*` env vars used by core.
148        // Missing values use defaults; malformed values fail standalone startup.
149        let defaults = TxSenderLimits::default();
150        let limits = TxSenderLimits {
151            fee_rate_hard_cap: env_parse_optional_or::<u64>(
152                "TX_SENDER_FEE_RATE_HARD_CAP",
153                defaults.fee_rate_hard_cap,
154            )?,
155            mempool_fee_rate_multiplier: env_parse_optional_or::<u64>(
156                "TX_SENDER_MEMPOOL_FEE_RATE_MULTIPLIER",
157                defaults.mempool_fee_rate_multiplier,
158            )?,
159            mempool_fee_rate_offset_sat_kvb: env_parse_optional_or::<u64>(
160                "TX_SENDER_MEMPOOL_FEE_RATE_OFFSET_SAT_KVB",
161                defaults.mempool_fee_rate_offset_sat_kvb,
162            )?,
163            cpfp_fee_payer_bump_wait_time_seconds: env_parse_optional_or::<u64>(
164                "TX_SENDER_CPFP_FEE_PAYER_BUMP_WAIT_TIME_SECONDS",
165                defaults.cpfp_fee_payer_bump_wait_time_seconds,
166            )?,
167            fee_bump_after_blocks: env_parse_optional_or::<u32>(
168                "TX_SENDER_FEE_BUMP_AFTER_BLOCKS",
169                defaults.fee_bump_after_blocks,
170            )?,
171            min_bump_kvb: env_parse_optional_or::<u64>(
172                "TX_SENDER_MIN_BUMP_KVB",
173                defaults.min_bump_kvb,
174            )?,
175        };
176
177        let finality_depth = env_parse_required::<u32>("TX_SENDER_FINALITY_DEPTH")?;
178
179        let poll_delay_ms =
180            env_parse_optional::<u64>("TX_SENDER_POLL_DELAY_MS")?.unwrap_or(DEFAULT_POLL_DELAY_MS);
181        if poll_delay_ms == 0 {
182            return Err(BridgeError::EnvVarMalformed(
183                "TX_SENDER_POLL_DELAY_MS",
184                "poll_delay_ms must be >= 1".to_string(),
185            ));
186        }
187
188        let include_unsafe = env_parse_required::<bool>("TX_SENDER_INCLUDE_UNSAFE")?;
189
190        if finality_depth < 1 {
191            return Err(BridgeError::EnvVarMalformed(
192                "TX_SENDER_FINALITY_DEPTH",
193                "finality depth must be >= 1".to_string(),
194            ));
195        }
196
197        #[cfg(feature = "json-rpc")]
198        let jsonrpc = {
199            let port = env_parse_optional::<u16>("TX_SENDER_JSONRPC_PORT")?;
200            port.map(|port| {
201                let bind = env_optional("TX_SENDER_JSONRPC_BIND")
202                    .unwrap_or_else(|| "127.0.0.1".to_string());
203                if bind != "127.0.0.1" && bind != "0.0.0.0" {
204                    return Err(BridgeError::EnvVarMalformed(
205                        "TX_SENDER_JSONRPC_BIND",
206                        "bind must be either 127.0.0.1 or 0.0.0.0".to_string(),
207                    ));
208                }
209                Ok(TxSenderJsonRpcConfig { bind, port })
210            })
211            .transpose()?
212        };
213
214        #[cfg(not(feature = "json-rpc"))]
215        let jsonrpc = None;
216
217        Ok(Self {
218            network,
219            secret_key,
220            private_da_key,
221            postgres,
222            bitcoin_rpc,
223            mempool,
224            limits,
225            finality_depth,
226            poll_delay_ms,
227            include_unsafe,
228            jsonrpc,
229        })
230    }
231}