bark/config.rs
1
2use std::fmt;
3use std::path::{Path, PathBuf};
4
5use anyhow::Context;
6use bitcoin::{FeeRate, Network};
7
8use bitcoin_ext::{BlockDelta, BlockHeight};
9
10use crate::chain::ChainSourceSpec;
11use crate::secret::Secret;
12
13
14/// Networks bark can be used on
15#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
16pub enum BarkNetwork {
17 /// Bitcoin's mainnet
18 Mainnet,
19 /// The official Bitcoin Core signet
20 Signet,
21 /// Mutinynet
22 Mutinynet,
23 /// Any regtest network
24 Regtest,
25}
26
27impl BarkNetwork {
28 /// Map to the [Network] types
29 pub fn as_bitcoin(&self) -> Network {
30 match self {
31 Self::Mainnet => Network::Bitcoin,
32 Self::Signet => Network::Signet,
33 Self::Mutinynet => Network::Signet,
34 Self::Regtest => Network::Regtest,
35 }
36 }
37}
38
39impl fmt::Display for BarkNetwork {
40 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
41 match self {
42 Self::Mainnet => f.write_str("mainnet"),
43 Self::Signet => f.write_str("signet"),
44 Self::Mutinynet => f.write_str("mutinynet"),
45 Self::Regtest => f.write_str("regtest"),
46 }
47 }
48}
49
50impl fmt::Debug for BarkNetwork {
51 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
52 fmt::Display::fmt(self, f)
53 }
54}
55
56/// Consecutive unused seed-derived VTXO key indices we tolerate before
57/// concluding a VTXO isn't ours.
58///
59/// The default for [Config::vtxo_key_gap_limit].
60pub const DEFAULT_VTXO_KEY_GAP_LIMIT: u32 = 250;
61
62/// The largest gap limit a VTXO key scan will accept.
63///
64/// A scan for a key that isn't ours runs the limit to its end, deriving a
65/// keypair per index and holding the unmatched ones, so an unbounded limit is
66/// unbounded work. The worst real case seen so far needed 10,000.
67pub const MAX_VTXO_KEY_GAP_LIMIT: u32 = 100_000;
68
69/// Configuration of the Bark wallet.
70///
71/// - [Config::esplora_address] or [Config::bitcoind_address] must be provided.
72/// - If you use [Config::bitcoind_address], you must also provide:
73/// - [Config::bitcoind_cookiefile] or
74/// - [Config::bitcoind_user] and [Config::bitcoind_pass]
75/// - Other optional fields can be omitted.
76///
77/// # Example
78/// Configure the wallet using defaults, then override endpoints for public signet:
79///
80/// ```rust
81/// use bark::Config;
82///
83/// let cfg = Config {
84/// server_address: "https://ark.signet.2nd.dev".into(),
85/// esplora_address: Some("https://esplora.signet.2nd.dev".into()),
86/// ..Config::network_default(bitcoin::Network::Bitcoin)
87/// };
88/// // cfg now has all other fields from the default configuration
89/// ```
90#[derive(Debug, Clone, Serialize, Deserialize)]
91pub struct Config {
92 /// The address of your ark server.
93 pub server_address: String,
94
95 /// An access token used to access a private server.
96 #[deprecated(
97 since = "0.2.4",
98 note = "access tokens are not enforced by the server; this field will be removed",
99 )]
100 pub server_access_token: Option<String>,
101
102 /// Client identifier sent on every RPC to the Ark server (as the
103 /// `x-user-agent` header) so server-side telemetry can attribute traffic
104 /// per implementation.
105 ///
106 /// Defaults to `bark/<version>` when unset. Integrators embedding bark
107 /// (FFI bindings, WASM wallets, custom apps) should set their own value,
108 /// e.g. `"aqua/1.4.2"`.
109 ///
110 /// Format: `<name>/<version>`. The name must be 1-32 chars of lowercase
111 /// ASCII alphanumeric, `-`, or `_`. Anything else (uppercase, missing
112 /// slash, invalid chars, too long) gets the RPC rejected by the server
113 /// with `invalid_argument`.
114 pub user_agent: Option<String>,
115
116 /// The address of the Esplora HTTP REST server to use.
117 ///
118 /// Either this or the `bitcoind_address` field has to be provided.
119 pub esplora_address: Option<String>,
120
121 /// The address of the bitcoind RPC server to use.
122 ///
123 /// Either this or the `esplora_address` field has to be provided.
124 /// Either `bitcoind_cookiefile` or `bitcoind_user` and `bitcoind_pass` has to be provided.
125 /// The node must run with `txindex=1`; the wallet refuses to start otherwise.
126 pub bitcoind_address: Option<String>,
127
128 /// The path to the bitcoind rpc cookie file.
129 ///
130 /// Only used with `bitcoind_address`.
131 pub bitcoind_cookiefile: Option<PathBuf>,
132
133 /// The bitcoind RPC username.
134 ///
135 /// Only used with `bitcoind_address`.
136 pub bitcoind_user: Option<String>,
137
138 /// The bitcoind RPC password.
139 ///
140 /// Only used with `bitcoind_address`.
141 ///
142 /// The [Secret] wrapper keeps the password out of debug logs.
143 pub bitcoind_pass: Option<Secret<String>>,
144
145 /// The ZMQ endpoint of the bitcoind node (e.g. `tcp://127.0.0.1:28332`),
146 /// used to get notified of new blocks.
147 ///
148 /// Only used with `bitcoind_address`. When unset, the chain tip is
149 /// polled instead.
150 pub bitcoind_zmq_address: Option<String>,
151
152 /// The number of blocks before expiration to refresh vtxos.
153 ///
154 /// Default value: 144 (24h) for mainnet, 12 for testnets
155 pub vtxo_refresh_expiry_threshold: BlockDelta,
156
157 /// An upper limit of the number of blocks we expect to need to
158 /// safely exit the vtxos.
159 ///
160 /// Default value: 12
161 pub vtxo_exit_margin: BlockDelta,
162
163 /// The number of blocks to claim a HTLC-recv VTXO.
164 ///
165 /// Default value: 18
166 pub htlc_recv_claim_delta: BlockDelta,
167
168 /// Maximum number of retry attempts when claiming a Lightning receive
169 /// against the server fails. After this budget is exhausted, the HTLC-recv
170 /// VTXOs will be exited on-chain.
171 ///
172 /// Default value: 5
173 pub lightning_receive_claim_retries: u8,
174
175 /// Optional SOCKS5 proxy URL for network traffic.
176 ///
177 /// The proxy is automatically bypassed for localhost addresses
178 /// (127.0.0.1, localhost, ::1), so a local bitcoind works without
179 /// extra configuration.
180 ///
181 /// Use `socks5h://` to resolve DNS through the proxy which is required for .onion addresses
182 /// and to prevent DNS leaks. We don't allow `socks5://` to be used to preserve privacy.
183 ///
184 /// Example: `socks5h://127.0.0.1:9050` for a local Tor daemon.
185 #[cfg(feature = "socks5-proxy")]
186 pub socks5_proxy: Option<String>,
187
188 /// A fallback fee rate to use in sat/kWu when we fail to retrieve a fee rate from the
189 /// configured bitcoind/esplora connection.
190 ///
191 /// Example for 1 sat/vB: --fallback-fee-rate 250
192 pub fallback_fee_rate: Option<FeeRate>,
193
194 /// The number of confirmations required before considering a round tx
195 /// fully confirmed
196 ///
197 /// Default value: 6 for mainnet, 2 for testnets
198 pub round_tx_required_confirmations: BlockHeight,
199
200 /// The number of confirmations required before considering an offboard tx
201 /// confirmed. If set to 0, offboard movements are marked as successful
202 /// immediately without waiting for confirmation.
203 ///
204 /// Default value: 2 for mainnet
205 pub offboard_required_confirmations: BlockHeight,
206
207 /// How long, in seconds, a broadcast offboard tx may be missing from
208 /// both chain and mempool before the wallet reports the offboard as
209 /// lost. Within the grace period the wallet re-broadcasts the tx
210 /// instead: the chain backend might just be slow or out of sync.
211 ///
212 /// Default value: 3600 (one hour)
213 pub offboard_lost_tx_grace_period_secs: u64,
214
215 /// Daemon sync interval in seconds for periodic tasks (onchain, exits,
216 /// boards, offboards, maintenance, rounds, mailbox).
217 ///
218 /// Default value: 60
219 pub daemon_sync_interval_secs: u64,
220
221 /// The number of pieces to split arkoor and lightning-send change into,
222 /// between 1 (no splitting) and 3 (the server's default arkoor fanout
223 /// limit of 4, minus the payment output).
224 ///
225 /// Default value: 2
226 pub change_vtxo_split_factor: u8,
227
228 /// When set, the daemon skips all automatic wallet syncing — startup
229 /// sync, the fast/slow sync intervals, round event subscription, and
230 /// the mailbox subscription. Only the server connection heartbeat
231 /// keeps running. The operator is responsible for triggering syncs
232 /// via the REST API (e.g. `POST /sync`).
233 ///
234 /// Default value: false
235 pub daemon_manual_sync: bool,
236
237 /// How many consecutive unused seed-derived VTXO key indices a scan crosses
238 /// before concluding a VTXO isn't ours.
239 ///
240 /// Used by [Wallet::recover_vtxos](crate::Wallet::recover_vtxos) and
241 /// [Wallet::import_vtxos](crate::Wallet::import_vtxos). Every match extends
242 /// the window, so this bounds the run of unused indices, not the total keys
243 /// derived. Raise it for a wallet that handed out many addresses without
244 /// receiving into them. Capped at [MAX_VTXO_KEY_GAP_LIMIT].
245 ///
246 /// Default value: 250
247 pub vtxo_key_gap_limit: u32,
248}
249
250impl Config {
251 /// A network-dependent default config that sets some useful defaults
252 ///
253 /// The [Default::default] provides a sane default for mainnet
254 pub fn network_default(network: Network) -> Self {
255 #[allow(deprecated)]
256 let mut ret = Self {
257 server_address: "http://127.0.0.1:3535".to_owned(),
258 server_access_token: None,
259 user_agent: None,
260 esplora_address: None,
261 bitcoind_address: None,
262 bitcoind_cookiefile: None,
263 bitcoind_user: None,
264 bitcoind_pass: None,
265 bitcoind_zmq_address: None,
266 #[cfg(feature = "socks5-proxy")]
267 socks5_proxy: None,
268 vtxo_refresh_expiry_threshold: 144,
269 vtxo_exit_margin: 12,
270 htlc_recv_claim_delta: 18,
271 lightning_receive_claim_retries: 5,
272 fallback_fee_rate: Some(FeeRate::from_sat_per_vb_u32(2)),
273 round_tx_required_confirmations: 1,
274 offboard_required_confirmations: 2,
275 offboard_lost_tx_grace_period_secs: 3600,
276 daemon_sync_interval_secs: 60,
277 daemon_manual_sync: false,
278 change_vtxo_split_factor: 2,
279 vtxo_key_gap_limit: DEFAULT_VTXO_KEY_GAP_LIMIT,
280 };
281
282 if network != Network::Bitcoin {
283 ret.vtxo_refresh_expiry_threshold = 12;
284 ret.fallback_fee_rate = Some(FeeRate::from_sat_per_vb_u32(1));
285 ret.round_tx_required_confirmations = 1;
286 ret.offboard_required_confirmations = 0;
287 }
288
289 ret
290 }
291
292 /// Load config from the config file path, filling missing fields
293 /// from the network default.
294 ///
295 /// Config values are loaded in the following priority order (highest to lowest):
296 /// 1. Environment variables with `BARK_` prefix (e.g., `BARK_ESPLORA_ADDRESS`)
297 /// 2. Config file values
298 /// 3. Network defaults
299 pub fn load(network: Network, path: impl AsRef<Path>) -> anyhow::Result<Config> {
300 let default = config::Config::try_from(&Self::network_default(network))
301 .expect("default config failed to deconstruct");
302
303 let config = config::Config::builder()
304 .add_source(default)
305 .add_source(config::File::from(path.as_ref()).required(false))
306 .add_source(config::Environment::with_prefix("BARK"))
307 .build().context("error building config")?
308 .try_deserialize::<Config>().context("error parsing config")?;
309
310 // Caught here so an out-of-range limit is refused up front, rather than
311 // failing the first scan that reads it.
312 if config.vtxo_key_gap_limit > MAX_VTXO_KEY_GAP_LIMIT {
313 bail!("vtxo_key_gap_limit {} is above the maximum of {}",
314 config.vtxo_key_gap_limit, MAX_VTXO_KEY_GAP_LIMIT);
315 }
316
317 Ok(config)
318 }
319
320 /// Creates a [crate::chain::ChainSource] instance to communicate with a chain
321 /// backend from this [Config].
322 pub fn chain_source(&self) -> anyhow::Result<ChainSourceSpec> {
323 if let Some(ref url) = self.esplora_address {
324 Ok(ChainSourceSpec::Esplora {
325 url: url.clone(),
326 })
327 } else if let Some(ref url) = self.bitcoind_address {
328 let auth = if let Some(ref c) = self.bitcoind_cookiefile {
329 bitcoin_ext::rpc::Auth::CookieFile(c.clone())
330 } else {
331 bitcoin_ext::rpc::Auth::UserPass(
332 self.bitcoind_user.clone().context("need bitcoind auth config")?,
333 self.bitcoind_pass.as_ref().context("need bitcoind auth config")?
334 .leak_ref().clone(),
335 )
336 };
337 Ok(ChainSourceSpec::Bitcoind {
338 url: url.clone(),
339 auth,
340 zmq: self.bitcoind_zmq_address.clone(),
341 })
342 } else {
343 bail!("Need to either provide esplora or bitcoind info");
344 }
345 }
346}
347
348#[cfg(test)]
349mod test {
350 use super::*;
351
352 /// A wallet created before a field existed has a config file without it, so
353 /// loading must fall back to the network default instead of failing.
354 #[test]
355 fn config_file_without_gap_limit_gets_the_default() {
356 let dir = std::env::temp_dir().join(format!("bark-cfg-{}", std::process::id()));
357 std::fs::create_dir_all(&dir).unwrap();
358 let path = dir.join("config.toml");
359 std::fs::write(&path, "\
360 server_address = \"http://127.0.0.1:3535\"\n\
361 esplora_address = \"http://127.0.0.1:3002\"\n\
362 ").unwrap();
363
364 let config = Config::load(Network::Regtest, &path).expect("an old config should load");
365 assert_eq!(config.vtxo_key_gap_limit, DEFAULT_VTXO_KEY_GAP_LIMIT,
366 "a missing gap limit should fall back to the default");
367
368 std::fs::remove_dir_all(&dir).ok();
369 }
370
371 /// An out-of-range gap limit is refused when the config loads, rather than
372 /// surfacing later from whichever scan first reads it.
373 #[test]
374 fn config_file_with_an_out_of_range_gap_limit_is_refused() {
375 let dir = std::env::temp_dir().join(format!("bark-cfg-max-{}", std::process::id()));
376 std::fs::create_dir_all(&dir).unwrap();
377 let path = dir.join("config.toml");
378 let write = |limit: u32| std::fs::write(&path, format!("\
379 server_address = \"http://127.0.0.1:3535\"\n\
380 esplora_address = \"http://127.0.0.1:3002\"\n\
381 vtxo_key_gap_limit = {limit}\n\
382 ")).unwrap();
383
384 write(MAX_VTXO_KEY_GAP_LIMIT + 1);
385 let err = Config::load(Network::Regtest, &path)
386 .expect_err("a gap limit above the maximum should be refused");
387 assert!(err.to_string().contains("above the maximum"), "err: {err:#}");
388
389 // The maximum itself loads, so the guard is a ceiling.
390 write(MAX_VTXO_KEY_GAP_LIMIT);
391 let config = Config::load(Network::Regtest, &path).expect("the maximum should load");
392 assert_eq!(config.vtxo_key_gap_limit, MAX_VTXO_KEY_GAP_LIMIT);
393
394 std::fs::remove_dir_all(&dir).ok();
395 }
396}