Skip to main content

bark/
lib.rs

1//! ![bark: Ark on bitcoin](https://gitlab.com/ark-bitcoin/bark/-/raw/master/assets/bark-header-white.jpg)
2//!
3//! <div align="center">
4//! <h1>Bark: Ark on bitcoin</h1>
5//! <p>Fast, low-cost, self-custodial payments on bitcoin.</p>
6//! </div>
7//!
8//! <p align="center">
9//! <br />
10//! <a href="https://docs.second.tech">Docs</a> ·
11//! <a href="https://gitlab.com/ark-bitcoin/bark/-/issues">Issues</a> ·
12//! <a href="https://second.tech">Website</a> ·
13//! <a href="https://blog.second.tech">Blog</a> ·
14//! <a href="https://www.youtube.com/@2ndbtc">YouTube</a>
15//! </p>
16//!
17//! <div align="center">
18//!
19//! [![Release](https://img.shields.io/gitlab/v/release/ark-bitcoin/bark?gitlab_url=https://gitlab.com&sort=semver&label=release)
20//! [![Project Status](https://img.shields.io/badge/status-active-brightgreen.svg)](https://gitlab.com/ark-bitcoin/bark)
21//! [![License](https://img.shields.io/badge/license-CC0--1.0-blue.svg)](https://gitlab.com/ark-bitcoin/bark/-/blob/master/LICENSE)
22//! [![PRs welcome](https://img.shields.io/badge/PRs-welcome-brightgreen?logo=git)](https://gitlab.com/ark-bitcoin/bark/-/blob/master/CONTRIBUTING.md)
23//! [![Community](https://img.shields.io/badge/community-forum-blue?logo=discourse)](https://community.second.tech)
24//!
25//! </div>
26//! <br />
27//!
28//! Bark is an implementation of the Ark protocol on bitcoin, led by [Second](https://second.tech).
29//!
30//! # A tour of Bark
31//!
32//! Integrating the Ark-protocol offers
33//!
34//! - 🏃‍♂️ **Smooth boarding**: No channels to open, no on-chain setup required—create a wallet and start transacting
35//! - 🤌 **Simplified UX**: Send and receive without managing channels, liquidity, or routing
36//! - 🌐 **Universal payments**: Send Ark, Lightning, and on-chain payments from a single off-chain balance
37//! - 🔌 **Easier integration**: Client-server architecture reduces complexity compared to P2P protocols
38//! - 💸 **Lower costs**: Instant payments at a fraction of on-chain fees
39//! - 🔒 **Self-custodial**: Users maintain full control of their funds at all times
40//!
41//! This guide puts focus on how to use the Rust-API and assumes
42//! some basic familiarity with the Ark protocol. We refer to the
43//! [protocol docs](http://docs.second.tech/ark-protocol) for an introduction.
44//!
45//! ## Creating an Ark wallet
46//!
47//! The user experience of setting up an Ark wallet is pretty similar
48//! to setting up an onchain wallet. You need to provide a [bip39::Mnemonic] which
49//! can be used to recover funds. Typically, most apps request the user
50//! to write down the mnemonic or ensure they use another method for a secure back-up.
51//!
52//! The user can select an Ark server and a [chain::ChainSource] as part of
53//! the configuration. The example below configures
54//!
55//! You will also need a place to store all [ark::Vtxo]s on the users device.
56//! We have implemented [`persist::sqlite::SqliteClient`] which is a sane default on most devices
57//! (requires the `sqlite` feature). However, it is possible to implement a
58//! [BarkPersister] if you have other requirements.
59//!
60//! The code-snippet below shows how you can create a [Wallet].
61//!
62//! ```no_run
63//! use std::path::PathBuf;
64//! use std::sync::Arc;
65//! use bark::{Config, onchain, Wallet, OpenWalletArgs, WalletSeed};
66//! use bark::lock_manager::memory::MemoryLockManager;
67//! use bark::persist::sqlite::SqliteClient;
68//!
69//! const MNEMONIC_FILE : &str = "mnemonic";
70//!
71//! #[tokio::main]
72//! async fn main() {
73//! 	// Pick the bitcoin network that will be used
74//! 	let network = bitcoin::Network::Signet;
75//!
76//! 	// Configure the wallet
77//! 	let config = Config {
78//! 		server_address: String::from("https://ark.signet.2nd.dev"),
79//! 		esplora_address: Some(String::from("https://esplora.signet.2nd.dev")),
80//! 		..Config::network_default(network)
81//! 	};
82//!
83//! 	// Create a sqlite database
84//! 	let datadir = PathBuf::from("./bark");
85//!
86//! 	// Generate and seed and store it somewhere
87//! 	let mnemonic = bip39::Mnemonic::generate(12).expect("12 is valid");
88//! 	tokio::fs::write(datadir.join(MNEMONIC_FILE), mnemonic.to_string().as_bytes()).await.unwrap();
89//! 	let seed = WalletSeed::new_from_mnemonic(network, &mnemonic);
90//!
91//! 	let wallet = Wallet::open(network, seed, config, OpenWalletArgs {
92//! 		datadir: Some(datadir),
93//! 		..Default::default()
94//! 	}).await.unwrap();
95//! }
96//! ```
97//!
98//! ## Opening an existing Ark wallet
99//!
100//! The [Wallet] can be opened again by providing the [bip39::Mnemonic] and
101//! the [BarkPersister] again. Note, that [`persist::sqlite::SqliteClient`] implements the [BarkPersister]-trait.
102//!
103//! ```no_run
104//! # use std::sync::Arc;
105//! # use std::path::PathBuf;
106//! # use std::str::FromStr;
107//! #
108//! # use bip39;
109//! # use bitcoin::Network;
110//! # use tokio::fs;
111//! #
112//! # use bark::{Config, Wallet, WalletSeed, OpenWalletArgs};
113//! # use bark::lock_manager::memory::MemoryLockManager;
114//! # use bark::persist::sqlite::SqliteClient;
115//! #
116//! const MNEMONIC_FILE : &str = "mnemonic";
117//!
118//! #[tokio::main]
119//! async fn main() {
120//! 	let datadir = PathBuf::from("./bark");
121//! 	let config = Config {
122//! 		server_address: String::from("https://ark.signet.2nd.dev"),
123//! 		esplora_address: Some(String::from("https://esplora.signet.2nd.dev")),
124//! 		..Config::network_default(Network::Signet)
125//! 	};
126//!
127//! 	let mnemonic_str = fs::read_to_string(datadir.join(MNEMONIC_FILE)).await.unwrap();
128//! 	let mnemonic = bip39::Mnemonic::from_str(&mnemonic_str).unwrap();
129//! 	let seed = WalletSeed::new_from_mnemonic(Network::Signet, &mnemonic);
130//! 	let wallet = Wallet::open(Network::Signet, seed, config, OpenWalletArgs {
131//! 		datadir: Some(datadir),
132//! 		..Default::default()
133//! 	}).await.unwrap();
134//! }
135//! ```
136//!
137//! ## Receiving coins
138//!
139//! For the time being we haven't implemented an Ark address type (yet). You
140//! can send funds directly to a public key.
141//!
142//! If you are on signet and your Ark server is [https://ark.signet.2nd.dev](https://ark.signet.2nd.dev),
143//! you can request some sats from our [faucet](https://signet.2nd.dev).
144//!
145//! ```no_run
146//! # use std::sync::Arc;
147//! # use std::str::FromStr;
148//! # use std::path::PathBuf;
149//! #
150//! # use bitcoin::Network;
151//! # use tokio::fs;
152//! #
153//! # use bark::{Config, Wallet, OpenWalletArgs, WalletSeed};
154//! # use bark::lock_manager::memory::MemoryLockManager;
155//! # use bark::persist::sqlite::SqliteClient;
156//! #
157//! # const MNEMONIC_FILE : &str = "mnemonic";
158//! #
159//! # async fn get_wallet() -> Wallet {
160//! #   let datadir = PathBuf::from("./bark");
161//! #
162//! #   let mnemonic_str = fs::read_to_string(datadir.join(MNEMONIC_FILE)).await.unwrap();
163//! #   let mnemonic = bip39::Mnemonic::from_str(&mnemonic_str).unwrap();
164//! #   let seed = WalletSeed::new_from_mnemonic(Network::Signet, &mnemonic);
165//! #
166//! #   let config = Config::network_default(bitcoin::Network::Signet);
167//! #   Wallet::open(Network::Signet, seed, config, OpenWalletArgs {
168//! #   	datadir: Some(datadir),
169//! #   	..Default::default()
170//! #   }).await.unwrap()
171//! # }
172//!
173//! #[tokio::main]
174//! async fn main() -> anyhow::Result<()> {
175//! 	let wallet = get_wallet().await;
176//! 	let address: ark::Address = wallet.new_address().await?;
177//! 	Ok(())
178//! }
179//! ```
180//!
181//! ## Sending coins
182//!
183//! Bark can pay to an [ark::Address], a BOLT11 invoice, a BOLT12 offer,
184//! a lightning address, an LNURL-pay link, an onchain [bitcoin::Address]
185//! or a BIP 321 payment URI, all from the same off-chain balance.
186//!
187//! Use [Wallet::parse_payment_request] to parse whatever payment string
188//! your user pasted. The resulting [PaymentRequest] lists each way the
189//! destination can be paid as an [AvailablePaymentMethod].
190//!
191//! Some payment methods need an amount from your user, e.g. a bare address
192//! or an amountless invoice. Use [movement::PaymentMethod::requires_amount]
193//! to decide whether to prompt for one. You can also show the cost of a
194//! payment upfront with [Wallet::estimate_payment_fee].
195//!
196//! ```no_run
197//! # use std::sync::Arc;
198//! # use std::str::FromStr;
199//! # use std::path::PathBuf;
200//! #
201//! # use bitcoin::Network;
202//! # use tokio::fs;
203//! #
204//! # use bark::{Config, Wallet, OpenWalletArgs, WalletSeed};
205//! # use bark::lock_manager::memory::MemoryLockManager;
206//! # use bark::persist::sqlite::SqliteClient;
207//! #
208//! # const MNEMONIC_FILE : &str = "mnemonic";
209//! #
210//! # async fn get_wallet() -> Wallet {
211//! #   let datadir = PathBuf::from("./bark");
212//! #
213//! #   let mnemonic_str = fs::read_to_string(datadir.join(MNEMONIC_FILE)).await.unwrap();
214//! #   let mnemonic = bip39::Mnemonic::from_str(&mnemonic_str).unwrap();
215//! #   let seed = WalletSeed::new_from_mnemonic(Network::Signet, &mnemonic);
216//! #
217//! #   let config = Config::network_default(bitcoin::Network::Signet);
218//! #   Wallet::open(Network::Signet, seed, config, OpenWalletArgs {
219//! #   	datadir: Some(datadir),
220//! #   	..Default::default()
221//! #   }).await.unwrap()
222//! # }
223//! #
224//! #[tokio::main]
225//! async fn main() -> anyhow::Result<()> {
226//! 	let mut wallet = get_wallet().await;
227//!
228//! 	// Sending spends your vtxos, so make sure the wallet is synced.
229//! 	wallet.sync().await;
230//!
231//! 	// Anything your user can paste: an Ark address, a BOLT11 invoice,
232//! 	// a BOLT12 offer, a lightning address, a bitcoin address or a
233//! 	// BIP 321 payment URI.
234//! 	let destination = "tark1pwh9vsmezqqpharv69q4z8m6x364d5m5prnmcalcalq9pdmzw0y7mpveck4pcfhezqypczkrrj3lkx5ue4qrf4jc7ztpt9htdttmh2judhqnu7aue8p0y9mq47jn9z";
235//!
236//! 	// Parse the destination into its available payment options.
237//! 	let request = wallet.parse_payment_request(destination).await?;
238//!
239//! 	// Or estimate fees and pick an option in your interface.
240//! 	let option = request.default_option().expect("no usable payment method");
241//!
242//! 	// Use the request amount, or ask your user for one.
243//! 	let amount = request.amount.or(Some(bitcoin::Amount::from_sat(10_000)));
244//!
245//! 	// Use the pay output in your app.
246//! 	let pay_output = wallet.send_payment(&option.method, amount, None::<&str>, true).await?;
247//!
248//! 	Ok(())
249//! }
250//! ```
251//!
252//! ## Inspecting the wallet
253//!
254//! An Ark wallet contains [ark::Vtxo]s. These are just like normal utxos
255//! in a bitcoin wallet. They just haven't been confirmed on chain (yet).
256//! However, the user remains in full control of the funds and can perform
257//! a unilateral exit at any time.
258//!
259//! The snippet below shows how you can inspect your [WalletVtxo]s.
260//!
261//! ```no_run
262//! # use std::sync::Arc;
263//! # use std::str::FromStr;
264//! # use std::path::PathBuf;
265//! #
266//! # use bitcoin::Network;
267//! # use tokio::fs;
268//! #
269//! # use bark::{Config, Wallet, OpenWalletArgs, WalletSeed};
270//! # use bark::lock_manager::memory::MemoryLockManager;
271//! # use bark::persist::sqlite::SqliteClient;
272//! #
273//! # const MNEMONIC_FILE : &str = "mnemonic";
274//! #
275//! # async fn get_wallet() -> Wallet {
276//! #   let datadir = PathBuf::from("./bark");
277//! #
278//! #   let mnemonic_str = fs::read_to_string(datadir.join(MNEMONIC_FILE)).await.unwrap();
279//! #   let mnemonic = bip39::Mnemonic::from_str(&mnemonic_str).unwrap();
280//! #   let seed = WalletSeed::new_from_mnemonic(Network::Signet, &mnemonic);
281//! #
282//! #   let config = Config::network_default(bitcoin::Network::Signet);
283//! #   Wallet::open(Network::Signet, seed, config, OpenWalletArgs {
284//! #   	datadir: Some(datadir),
285//! #   	..Default::default()
286//! #   }).await.unwrap()
287//! # }
288//! #
289//!
290//! #[tokio::main]
291//! async fn main() -> anyhow::Result<()> {
292//! 	let mut wallet = get_wallet().await;
293//!
294//! 	// The vtxo's command doesn't sync your wallet
295//! 	// When you're not running the daemon, make sure your app is synced
296//! 	// before inspecting the wallet
297//! 	wallet.sync().await;
298//!
299//! 	let vtxos: Vec<bark::WalletVtxo> = wallet.vtxos().await.unwrap();
300//! 	Ok(())
301//! }
302//! ```
303//!
304//! Use [Wallet::balance] if you are only interested in the balance.
305//!
306//! ## Participating in a round
307//!
308//! You can participate in a round to refresh your coins. Typically,
309//! you want to refresh coins which are soon to expire or you might
310//! want to aggregate multiple small vtxos to keep the cost of exit
311//! under control.
312//!
313//! As a wallet developer you can implement your own refresh strategy.
314//! This gives you full control over which [ark::Vtxo]s are refreshed and
315//! which aren't.
316//!
317//! This example uses [RefreshStrategy::must_refresh] which is a sane
318//! default that selects all [ark::Vtxo]s that must be refreshed.
319//!
320//! ```no_run
321//! # use std::sync::Arc;
322//! # use std::str::FromStr;
323//! # use std::path::PathBuf;
324//! #
325//! # use bitcoin::Network;
326//! # use tokio::fs;
327//! #
328//! # use bark::{Config, Wallet, OpenWalletArgs, WalletSeed};
329//! # use bark::lock_manager::memory::MemoryLockManager;
330//! # use bark::persist::sqlite::SqliteClient;
331//! #
332//! # const MNEMONIC_FILE : &str = "mnemonic";
333//! #
334//! # async fn get_wallet() -> Wallet {
335//! #   let datadir = PathBuf::from("./bark");
336//! #
337//! #   let mnemonic_str = fs::read_to_string(datadir.join(MNEMONIC_FILE)).await.unwrap();
338//! #   let mnemonic = bip39::Mnemonic::from_str(&mnemonic_str).unwrap();
339//! #   let seed = WalletSeed::new_from_mnemonic(Network::Signet, &mnemonic);
340//! #
341//! #   let config = Config::network_default(bitcoin::Network::Signet);
342//! #   Wallet::open(Network::Signet, seed, config, OpenWalletArgs {
343//! #   	datadir: Some(datadir),
344//! #   	..Default::default()
345//! #   }).await.unwrap()
346//! # }
347//! #
348//! use bark::vtxo::RefreshStrategy;
349//!
350//! #[tokio::main]
351//! async fn main() -> anyhow::Result<()> {
352//! 	let wallet = get_wallet().await;
353//!
354//! 	// Select all vtxos that refresh soon
355//! 	let tip = wallet.chain().tip().await?;
356//! 	let fee_rate = wallet.chain().fee_rates().await.fast;
357//! 	let strategy = RefreshStrategy::must_refresh(&wallet, tip, fee_rate);
358//!
359//! 	let vtxos = wallet.spendable_vtxos_with(&strategy).await?;
360//!		wallet.refresh_vtxos(vtxos).await?;
361//! 	Ok(())
362//! }
363//! ```
364//!
365//! Terms of service may apply, check your server's `ArkInfo.tos_link`.
366
367#[cfg(all(any(target_os = "android", target_os = "ios"), feature = "tls-native-roots"))]
368compile_error!("feature `tls-native-roots` can't be used on Android or iOS, use `tls-webpki-roots` instead");
369
370pub extern crate ark;
371
372pub extern crate bip39;
373pub extern crate lightning_invoice;
374pub extern crate lnurl as lnurllib;
375
376#[macro_use] extern crate anyhow;
377#[macro_use] extern crate async_trait;
378#[macro_use] extern crate serde;
379
380pub mod actions;
381pub mod chain;
382pub mod exit;
383pub use bark_common::fs_perms;
384pub use bark_common::secret;
385pub mod movement;
386pub mod onchain;
387pub mod payment_request;
388pub mod persist;
389pub mod round;
390pub mod subsystem;
391pub mod vtxo;
392
393pub mod lock_manager;
394
395mod arkoor;
396mod board;
397mod config;
398mod daemon;
399mod fees;
400mod lightning;
401mod mailbox;
402mod notification;
403mod offboard;
404#[cfg(feature = "socks5-proxy")]
405mod proxy;
406mod recovery;
407mod psbtext;
408mod utils;
409
410pub use self::arkoor::{ArkoorCreateResult, ArkoorAddressError};
411pub use self::payment_request::{
412	AvailablePaymentMethod, PaymentInitOutput, PaymentMethodParsingError, PaymentRequest,
413};
414pub use self::config::{BarkNetwork, Config};
415pub use self::daemon::{tip_watcher, DaemonHandle};
416pub use self::fees::FeeEstimate;
417pub use self::notification::{WalletNotification, NotificationStream};
418pub use self::recovery::{RecoveryReport, RecoveryReportEntry, RecoveryStatus};
419pub use self::vtxo::WalletVtxo;
420
421use std::borrow::Cow;
422use std::collections::HashSet;
423use std::path::PathBuf;
424use std::sync::Arc;
425use std::time::Duration;
426
427use anyhow::{bail, Context};
428use bip39::Mnemonic;
429use bitcoin::{Amount, Network, OutPoint};
430use bitcoin::bip32::{self, ChildNumber, Fingerprint};
431use bitcoin::secp256k1::{self, Keypair, PublicKey};
432use futures::stream::FuturesUnordered;
433use log::{debug, error, info, trace, warn};
434use tokio_stream::StreamExt;
435
436use ark::{ArkInfo, ProtocolEncoding, Vtxo, VtxoId, VtxoPolicy, VtxoRequest};
437use ark::address::VtxoDelivery;
438use ark::fees::{validate_and_subtract_fee_min_dust, VtxoFeeInfo};
439use ark::rounds::{RoundAttempt, RoundEvent};
440use ark::vtxo::{Full, PubkeyVtxoPolicy, VtxoRef, VTXO_DUST};
441use ark::vtxo::policy::signing::VtxoSigner;
442use bitcoin_ext::{BlockDelta, BlockHeight, TxStatus};
443use server_rpc::{protos, ServerConnection};
444use server_rpc::client::{ConnectError, CreateEndpointError};
445
446use crate::chain::{ChainSource, ChainSourceSpec};
447use crate::exit::Exit;
448use crate::lock_manager::LockManager;
449use crate::movement::{Movement, MovementId, PaymentMethod};
450use crate::movement::manager::MovementManager;
451use crate::notification::NotificationDispatch;
452use crate::onchain::{OnchainWalletTrait, Utxo};
453use crate::persist::BarkPersister;
454use crate::persist::models::{RoundStateId, StoredRoundState, Unlocked};
455#[cfg(feature = "socks5-proxy")]
456use crate::proxy::proxy_for_url;
457use crate::round::{RoundParticipation, RoundSecretNonces, RoundStatus};
458use crate::subsystem::RoundMovement;
459use crate::utils::rejected_vtxos_from_error;
460use crate::vtxo::{FilterVtxos, RefreshStrategy, VtxoFilter, VtxoStateKind, VtxoValidationError};
461use crate::vtxo::selection::{InputSelection, SelectedFeeInfos};
462
463#[cfg(all(feature = "wasm-web", feature = "socks5-proxy"))]
464compile_error!("features `wasm-web` does not support feature `socks5-proxy");
465
466#[cfg(all(feature = "wasm-web", feature = "bitcoind-rpc"))]
467compile_error!("`wasm-web` does not support the `bitcoind-rpc` feature");
468
469/// Derivation index for Bark usage
470const BARK_PURPOSE_INDEX: u32 = 350;
471/// Derivation index used to generate keypairs to sign VTXOs
472const VTXO_KEYS_INDEX: u32 = 0;
473/// Derivation index used to generate keypair for the mailbox
474const MAILBOX_KEY_INDEX: u32 = 1;
475/// Derivation index used to generate keypair for the recovery mailbox
476const RECOVERY_MAILBOX_KEY_INDEX: u32 = 2;
477const MISSING_SERVER_TRANSPORT_HELP: &str =
478	"This build of bark-wallet does not include an Ark server transport backend. Enable feature `bark-wallet/native` or `bark-wallet/wasm-web` to use server-backed wallet functionality.";
479
480/// The timeout value to use for streaming subscribe requests to the Ark server
481const SUBSCRIBE_REQUEST_TIMEOUT: Duration = Duration::from_secs(60 * 60);
482
483lazy_static::lazy_static! {
484	/// Global secp context.
485	static ref SECP: secp256k1::Secp256k1<secp256k1::All> = secp256k1::Secp256k1::new();
486}
487
488/// Cap on server-advertised round nonces; each requested output allocates
489/// this many musig nonce pairs, so a huge value would DoS the client.
490const MAX_NB_ROUND_NONCES: usize = 16;
491
492/// Minimum `vtxo_exit_delta` accepted on mainnet: roughly eight hours of
493/// blocks to notice the server-only timeout leaf and broadcast a unilateral
494/// exit. Non-production networks (regtest, signet, testnet) only require it
495/// to be non-zero so tests and integrator sessions can use short deltas.
496const MIN_MAINNET_VTXO_EXIT_DELTA: BlockDelta = 96;
497
498/// Refuse an [ArkInfo] whose parameters would make participation unsafe:
499/// `vtxo_lifetime` must leave room to board and broadcast a unilateral exit
500/// before the server-only timeout leaf activates; `vtxo_exit_delta` must keep
501/// its CSV window; `nb_round_nonces` must be non-zero and bounded.
502fn check_ark_info_safe(ark_info: &ArkInfo, vtxo_exit_margin: BlockDelta) -> anyhow::Result<()> {
503	let required = ark_info.required_board_confirmations;
504	let margin = vtxo_exit_margin as usize;
505	let min_safe = required.saturating_add(margin);
506	ensure!(ark_info.vtxo_exit_delta > 0,
507		"server-advertised vtxo_exit_delta is 0; refusing to connect",
508	);
509	ensure!(ark_info.nb_round_nonces > 0,
510		"server-advertised nb_round_nonces is 0; refusing to connect",
511	);
512	if ark_info.network == Network::Bitcoin {
513		// No upper bound: a long lifetime is safe because unilateral exit is
514		// always available before the server-only timeout leaf activates.
515		ensure!((ark_info.vtxo_lifetime as usize) > min_safe,
516			"server-advertised vtxo_lifetime {} is unsafe (minimum > {} = \
517			required_board_confirmations {} + vtxo_exit_margin {}); refusing to connect",
518			ark_info.vtxo_lifetime, min_safe, required, margin,
519		);
520		ensure!(ark_info.vtxo_exit_delta >= MIN_MAINNET_VTXO_EXIT_DELTA,
521			"server-advertised vtxo_exit_delta {} is below the mainnet minimum of {} \
522			 blocks; refusing to connect",
523			ark_info.vtxo_exit_delta, MIN_MAINNET_VTXO_EXIT_DELTA,
524		);
525		ensure!(ark_info.nb_round_nonces <= MAX_NB_ROUND_NONCES,
526			"server-advertised nb_round_nonces {} exceeds cap {}; refusing to connect",
527			ark_info.nb_round_nonces, MAX_NB_ROUND_NONCES,
528		);
529	}
530	Ok(())
531}
532
533/// Log that the server public key has changed.
534///
535/// Recommends that the user perform an emergency exit to recover their
536/// funds on-chain, since a rotated server pubkey makes the original VTXO
537/// spend/exit conditions unreachable.
538fn log_server_pubkey_changed_error(expected: PublicKey, got: PublicKey) {
539	error!(
540	    "
541Server public key has changed!
542
543The Ark server's public key is different from the one stored when this
544wallet was created. This typically happens when:
545
546	- The server operator has rotated their keys
547	- You are connecting to a different server
548	- The server has been replaced
549
550For safety, this wallet will not connect to the server until you
551resolve this. You can recover your funds on-chain by doing an emergency exit.
552
553This will exit your VTXOs to on-chain Bitcoin without needing the server's cooperation.
554
555Expected: {expected}
556Got:      {got}")
557}
558
559/// Log that the server mailbox pubkey has changed.
560fn log_server_mailbox_pubkey_changed_error(expected: PublicKey, got: PublicKey) {
561	error!(
562	    "
563Server mailbox public key has changed!
564
565The Ark server's mailbox public key is different from the one stored when this
566wallet was created. This typically happens when:
567
568	- The server operator has rotated their keys
569	- You are connecting to a different server
570	- The server has been replaced
571
572For safety, this wallet will not connect to the server until you resolve this.
573
574Unlike a server pubkey change, your VTXOs are not at risk - the mailbox pubkey
575only affects address receive semantics. Any Ark addresses you previously
576shared will stop receiving new payments; you will need to share new addresses
577after reconnecting.
578
579Expected: {expected}
580Got:      {got}")
581}
582
583/// The detailled balance of a Lightning receive.
584#[derive(Debug, Clone)]
585pub struct LightningReceiveBalance {
586	/// Sum of all pending lightning invoices
587	pub total: Amount,
588	/// Sum of all invoices for which we received the HTLC VTXOs
589	pub claimable: Amount,
590}
591
592/// The different balances of a Bark wallet.
593#[derive(Debug, Clone)]
594pub struct Balance {
595	/// Coins that are spendable in the Ark, either in-round or out-of-round.
596	pub spendable: Amount,
597	/// Coins that are in the process of being sent over Lightning.
598	pub pending_lightning_send: Amount,
599	/// Coins that are in the process of being received over Lightning.
600	pub claimable_lightning_receive: Amount,
601	/// Coins locked in a round.
602	pub pending_in_round: Amount,
603	/// Coins held in VTXOs whose unilateral exit chain has confirmed onchain but which
604	/// haven't yet been drained back to the onchain wallet. While in this state the
605	/// VTXOs are [`vtxo::VtxoStateKind::Exited`] and unusable in the Ark protocol; the
606	/// drain transaction moves them to spendable onchain output.
607	/// None if exit subsystem was unavailable
608	pub pending_exit: Option<Amount>,
609	/// Coins that are pending sufficient confirmations from board transactions.
610	pub pending_board: Amount,
611}
612
613pub struct UtxoInfo {
614	pub outpoint: OutPoint,
615	pub amount: Amount,
616	pub confirmation_height: Option<u32>,
617}
618
619impl From<Utxo> for UtxoInfo {
620	fn from(value: Utxo) -> Self {
621		match value {
622			Utxo::Local(o) => UtxoInfo {
623				outpoint: o.outpoint,
624				amount: o.amount,
625				confirmation_height: o.confirmation_height,
626			},
627			Utxo::Exit(e) => UtxoInfo {
628				outpoint: e.vtxo.point(),
629				amount: e.vtxo.amount(),
630				confirmation_height: Some(e.height),
631			},
632		}
633	}
634}
635
636/// Represents an offchain balance structure consisting of available funds, pending amounts in
637/// unconfirmed rounds, and pending exits.
638pub struct OffchainBalance {
639	/// Funds currently available for use. This reflects the spendable balance.
640	pub available: Amount,
641	/// Funds that are pending in unconfirmed operational rounds.
642	pub pending_in_round: Amount,
643	/// Funds being unilaterally exited. These may require more onchain confirmations to become
644	/// available onchain.
645	pub pending_exit: Amount,
646}
647
648/// Read-only properties of the Bark wallet.
649#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
650pub struct WalletProperties {
651	/// The Bitcoin network to run Bark on.
652	///
653	/// Default value: signet.
654	pub network: Network,
655
656	/// The wallet fingerpint
657	///
658	/// Used on wallet loading to check mnemonic correctness
659	pub fingerprint: Fingerprint,
660
661	/// The server public key from the initial connection.
662	///
663	/// This is used to detect if the Ark server has been replaced,
664	/// which could indicate a malicious server. If the server pubkey
665	/// changes, the wallet will refuse to connect and warn the user
666	/// to perform an emergency exit.
667	pub server_pubkey: Option<PublicKey>,
668
669	/// The server's mailbox public key.
670	///
671	/// Stored so that Ark addresses can be generated without a live
672	/// connection to the Ark server. `None` indicates a wallet created
673	/// before this field was added; the value is populated on the first
674	/// successful handshake. If the key changes, the wallet refuses to
675	/// connect until the user resolves the rotation.
676	pub server_mailbox_pubkey: Option<PublicKey>,
677}
678
679/// Struct representing an extended private key derived from a
680/// wallet's seed, used to derive child VTXO keypairs
681///
682/// The VTXO seed is derived by applying a hardened derivation
683/// step at index 350 from the wallet's seed.
684pub struct WalletSeed {
685	master: bip32::Xpriv,
686	vtxo: bip32::Xpriv,
687}
688
689impl WalletSeed {
690	/// Create a new [WalletSeed] from a given BIP-32 master seed
691	pub fn new_from_seed(network: Network, seed: &[u8; 64]) -> Self {
692		let bark_path = [ChildNumber::from_hardened_idx(BARK_PURPOSE_INDEX).unwrap()];
693		let master = bip32::Xpriv::new_master(network, seed)
694			.expect("invalid seed")
695			.derive_priv(&SECP, &bark_path)
696			.expect("purpose is valid");
697
698		let vtxo_path = [ChildNumber::from_hardened_idx(VTXO_KEYS_INDEX).unwrap()];
699		let vtxo = master.derive_priv(&SECP, &vtxo_path)
700			.expect("vtxo path is valid");
701
702		Self { master, vtxo }
703	}
704
705	/// Create a new [WalletSeed] from a given BIP-39 [Mnemonic]
706	pub fn new_from_mnemonic(network: Network, mnemonic: &Mnemonic) -> Self {
707		Self::new_from_seed(network, &mnemonic.to_seed(""))
708	}
709
710	pub fn fingerprint(&self) -> Fingerprint {
711		self.master.fingerprint(&SECP)
712	}
713
714	fn derive_vtxo_keypair(&self, idx: u32) -> Keypair {
715		self.vtxo.derive_priv(&SECP, &[idx.into()]).unwrap().to_keypair(&SECP)
716	}
717
718	fn to_mailbox_keypair(&self) -> Keypair {
719		let mailbox_path = [ChildNumber::from_hardened_idx(MAILBOX_KEY_INDEX).unwrap()];
720		self.master.derive_priv(&SECP, &mailbox_path).unwrap().to_keypair(&SECP)
721	}
722
723	fn to_recovery_mailbox_keypair(&self) -> Keypair {
724		let mailbox_path = [ChildNumber::from_hardened_idx(RECOVERY_MAILBOX_KEY_INDEX).unwrap()];
725		self.master.derive_priv(&SECP, &mailbox_path).unwrap().to_keypair(&SECP)
726	}
727}
728
729/// Additional arguments for the [Wallet::open] function
730pub struct OpenWalletArgs {
731	/// Whether to run the background daemon
732	///
733	/// When disabled, you must manually call `Wallet::sync` to sync the wallet.
734	///
735	/// Default: true
736	pub run_daemon: bool,
737
738	/// The data directory to use for this wallet
739	///
740	/// This field can be used under most platforms as an alternative to
741	/// providing the `persister` and `lock_manager` fields.
742	///
743	/// This field is ignored if `persister` and `lock_manager` are provided
744	/// or for the wasm32 platform.
745	///
746	/// Default: none
747	pub datadir: Option<PathBuf>,
748
749	/// The persister to use for this wallet
750	///
751	/// Default: returned by [`crate::persist::platform_default`]
752	pub persister: Option<Arc<dyn BarkPersister>>,
753
754	/// The lock manager to use for this wallet
755	///
756	/// Default: returned by [`crate::lock_manager::platform_default`]
757	///
758	/// On some platforms (linux, macos, windows) the default lock manager
759	/// requires a datadir be provided.
760	pub lock_manager: Option<Box<dyn LockManager>>,
761
762	/// The onchain wallet to use, if any
763	///
764	/// Default: none
765	pub onchain: Option<Arc<tokio::sync::RwLock<dyn OnchainWalletTrait>>>,
766
767	/// Whether to create a new wallet if no wallet exists
768	///
769	///  Default: true
770	pub create_if_not_exists: bool,
771
772	/// Whether to create a new wallet even if the Ark server cannot be reached
773	///
774	/// Default: false
775	pub create_without_server: bool,
776
777	/// Whether to skip recovering VTXOs from the recovery mailbox.
778	///
779	/// When false (the default), recovery runs on wallet open.
780	///
781	/// Default: false
782	pub skip_recovery: bool,
783
784	/// A callback function to be called with the outcome of the recovery scan.
785	///
786	/// Called exactly once per successful open, including when no scan ran, so
787	/// the absence of a call never has to be interpreted.
788	///
789	/// Default: none
790	pub on_recovery_finished: Option<Box<dyn FnOnce(RecoveryStatus) + Send + Sync>>,
791}
792
793impl Default for OpenWalletArgs {
794	fn default() -> Self {
795	    Self {
796			run_daemon: true,
797			onchain: None,
798			datadir: None,
799			persister: None,
800			lock_manager: None,
801			create_if_not_exists: true,
802			create_without_server: false,
803			skip_recovery: false,
804			on_recovery_finished: None,
805		}
806	}
807}
808
809struct WalletInner {
810	/// The chain source the wallet is connected to
811	chain: Arc<ChainSource>,
812
813	/// Exit subsystem handling unilateral exits and on-chain reconciliation outside Ark rounds.
814	exit: Exit,
815
816	/// Allows easy creation of and management of wallet fund movements.
817	movements: Arc<MovementManager>,
818
819	/// Dispatch for wallet notifications
820	notifications: NotificationDispatch,
821
822	/// Active runtime configuration for networking, fees, policies and thresholds.
823	config: Config,
824
825	/// Persistence backend for wallet state (keys metadata, VTXOs, movements, round state, etc.).
826	db: Arc<dyn BarkPersister>,
827
828	/// Coordinates access to the wallet's protected resources. The caller
829	/// picks a backend whose enforcement scope matches how the wallet is
830	/// deployed; see [`crate::lock_manager`].
831	lock_manager: Box<dyn LockManager>,
832
833	/// Deterministic seed material used to generate wallet keypairs.
834	seed: WalletSeed,
835
836	/// Live connection to an Ark server for round participation and synchronization.
837	///
838	/// Lazily initialised on first use via [`Wallet::require_server`]. A
839	/// [`OnceCell`] is the right primitive here: concurrent callers on a
840	/// cold cell all await the same in-flight `connect_to_server` future
841	/// instead of each opening a fresh gRPC channel.
842	server: tokio::sync::OnceCell<ServerConnection>,
843
844	/// Onchain wallet used for boarding, exit fee-bumping, and onchain syncing.
845	///
846	/// When present, the wallet can perform onchain operations without the
847	/// caller having to supply a wallet on every call.
848	onchain: Option<Arc<tokio::sync::RwLock<dyn OnchainWalletTrait>>>,
849
850	/// A handle to the currently running daemon, if any.
851	daemon: parking_lot::Mutex<Option<DaemonHandle>>,
852
853	/// The last chain tip at which we scanned spendable VTXOs for on-chain (force) exits.
854	/// The scan is skipped while the tip is unchanged, since a VTXO's on-chain status can
855	/// only change across blocks.
856	last_force_exit_scan_tip: tokio::sync::Mutex<Option<BlockHeight>>,
857
858	/// In-memory MuSig2 secret cosign nonces for in-flight round attempts.
859	/// See [`RoundSecretNonces`].
860	pub(crate) round_secret_nonces: RoundSecretNonces,
861}
862
863/// The central entry point for using this library as an Ark wallet.
864///
865/// Note that a [Wallet] instance can freely be [Clone]'ed to refer to the same
866/// wallet.
867///
868/// Overview
869/// - Wallet encapsulates the complete Ark client implementation:
870///   - address generation (Ark addresses/keys)
871///     - [Wallet::new_address],
872///     - [Wallet::new_address_with_index],
873///     - [Wallet::peek_address],
874///     - [Wallet::validate_arkoor_address]
875///   - boarding onchain funds into Ark from an onchain wallet (see [onchain::OnchainWallet])
876///     - [Wallet::board_amount],
877///     - [Wallet::board_all]
878///   - offboarding Ark funds to move them back onchain
879///     - [Wallet::offboard_vtxos],
880///     - [Wallet::offboard_all]
881///   - sending and receiving Ark payments (including to BOLT11/BOLT12 invoices)
882///     - [Wallet::send_arkoor_payment],
883///     - [Wallet::pay_lightning_invoice],
884///     - [Wallet::pay_lightning_address],
885///     - [Wallet::pay_lightning_offer]
886///   - tracking, selecting, and refreshing VTXOs
887///     - [Wallet::vtxos],
888///     - [Wallet::vtxos_with],
889///     - [Wallet::refresh_vtxos]
890///   - syncing with the Ark server, unilateral exits and performing general maintenance
891///     - [Wallet::maintenance]: Syncs everything offchain-related and refreshes VTXOs where
892///       necessary,
893///     - [Wallet::maintenance_with_onchain]: The same as [Wallet::maintenance] but also syncs the
894///       onchain wallet and unilateral exits,
895///     - [Wallet::maintenance_refresh]: Refreshes VTXOs where necessary without syncing anything,
896///     - [Wallet::sync]: Syncs network fee-rates, ark rounds and arkoor payments,
897///     - [Wallet::sync_exits]: Updates the status of unilateral exits,
898///     - [Wallet::sync_pending_lightning_send_vtxos]: Updates the status of pending lightning payments,
899///     - [Wallet::try_claim_all_lightning_receives]: Wait for payment receipt of all open invoices, then claim them,
900///     - [Wallet::sync_pending_boards]: Registers boards which are available for use
901///       in offchain payments
902///
903/// Key capabilities
904/// - Address management:
905///   - derive and peek deterministic Ark addresses and their indices
906/// - Funds lifecycle:
907///   - board funds from an external onchain wallet onto the Ark
908///   - send out-of-round Ark payments (arkoor)
909///   - offboard funds to onchain addresses
910///   - manage HTLCs and Lightning receives/sends
911/// - VTXO management:
912///   - query spendable and pending VTXOs
913///   - refresh expiring or risky VTXOs
914///   - compute balance broken down by spendable/pending states
915/// - Synchronization and maintenance:
916///   - sync against the Ark server and the onchain source
917///   - reconcile pending rounds, exits, and offchain state
918///   - periodic maintenance helpers (e.g., auto-register boards, refresh policies)
919///
920/// Construction and persistence
921///
922/// A [Wallet] is opened or created using a mnemonic and a backend implementing [BarkPersister].
923/// The [Wallet::open] function allows for opening and creating a wallet if it doesn't exist yet.
924/// Check out the documentation on [OpenWalletArgs] for all optional arguments.
925///
926/// Example
927/// ```no_run
928/// use std::path::PathBuf;
929/// use std::sync::Arc;
930/// use tokio::fs;
931/// use bark::{Config, onchain, Wallet, OpenWalletArgs, WalletSeed};
932/// use bark::lock_manager::memory::MemoryLockManager;
933/// use bark::persist::sqlite::SqliteClient;
934///
935/// const MNEMONIC_FILE : &str = "mnemonic";
936///
937/// #[tokio::main]
938/// async fn main() {
939/// 	// Pick the bitcoin network that will be used
940/// 	let network = bitcoin::Network::Signet;
941///
942/// 	// Configure the wallet
943/// 	let config = Config {
944/// 		server_address: String::from("https://ark.signet.2nd.dev"),
945/// 		esplora_address: Some(String::from("https://esplora.signet.2nd.dev")),
946/// 		..Config::network_default(network)
947/// 	};
948///
949/// 	// Create a sqlite database
950/// 	let datadir = PathBuf::from("./bark");
951///
952/// 	// Generate and seed and store it somewhere
953/// 	let mnemonic = bip39::Mnemonic::generate(12).expect("12 is valid");
954/// 	fs::write(datadir.join(MNEMONIC_FILE), mnemonic.to_string().as_bytes()).await.unwrap();
955/// 	let seed = WalletSeed::new_from_mnemonic(network, &mnemonic);
956///
957/// 	let wallet = Wallet::open(network, seed, config, OpenWalletArgs {
958/// 		datadir: Some(datadir),
959/// 		..Default::default()
960/// 	}).await.unwrap();
961/// }
962/// ```
963#[derive(Clone)]
964pub struct Wallet {
965	inner: Arc<WalletInner>,
966}
967
968impl Wallet {
969	pub async fn network(&self) -> anyhow::Result<Network> {
970		Ok(self.properties().await?.network)
971	}
972
973	/// Access the server's chain source
974	pub fn chain(&self) -> &Arc<ChainSource> {
975		&self.inner.chain
976	}
977
978	/// Access the exit manager
979	pub fn exit_mgr(&self) -> &Exit {
980		&self.inner.exit
981	}
982
983	/// Access the movements manager
984	pub fn movements_mgr(&self) -> &MovementManager {
985		&self.inner.movements
986	}
987
988	/// Peek at the keypair directly after currently last revealed one,
989	/// together with its index, without storing it.
990	pub async fn peek_next_keypair(&self) -> anyhow::Result<(Keypair, u32)> {
991		let last_revealed = self.inner.db.get_last_vtxo_key_index().await?;
992
993		let index = last_revealed.map(|i| i + 1).unwrap_or(u32::MIN);
994		let keypair = self.inner.seed.derive_vtxo_keypair(index);
995
996		Ok((keypair, index))
997	}
998
999	/// Derive and store the keypair directly after currently last revealed one,
1000	/// together with its index.
1001	pub async fn derive_store_next_keypair(&self) -> anyhow::Result<(Keypair, u32)> {
1002		let (keypair, index) = self.peek_next_keypair().await?;
1003		self.inner.db.store_vtxo_key(index, keypair.public_key()).await?;
1004		Ok((keypair, index))
1005	}
1006
1007	#[deprecated(note = "use peek_keypair instead")]
1008	pub async fn peak_keypair(&self, index: u32) -> anyhow::Result<Keypair> {
1009		self.peek_keypair(index).await
1010	}
1011
1012	/// Retrieves a keypair based on the provided index and checks if the corresponding public key
1013	/// exists in the [Vtxo] database.
1014	///
1015	/// # Arguments
1016	///
1017	/// * `index` - The index used to derive a keypair.
1018	///
1019	/// # Returns
1020	///
1021	/// * `Ok(Keypair)` - If the keypair is successfully derived and its public key exists in the
1022	///   database.
1023	/// * `Err(anyhow::Error)` - If the public key does not exist in the database or if an error
1024	///   occurs during the database query.
1025	pub async fn peek_keypair(&self, index: u32) -> anyhow::Result<Keypair> {
1026		let keypair = self.inner.seed.derive_vtxo_keypair(index);
1027		if self.inner.db.get_public_key_idx(&keypair.public_key()).await?.is_some() {
1028			Ok(keypair)
1029		} else {
1030			bail!("VTXO key {} does not exist, please derive it first", index)
1031		}
1032	}
1033
1034
1035	/// Retrieves the [Keypair] for a provided [PublicKey]
1036	///
1037	/// # Arguments
1038	///
1039	/// * `public_key` - The public key for which the keypair must be found
1040	///
1041	/// # Returns
1042	/// * `Ok(Some(u32, Keypair))` - If the pubkey is found, the derivation-index and keypair are
1043	///                              returned
1044	/// * `Ok(None)` - If the pubkey cannot be found in the database
1045	/// * `Err(anyhow::Error)` - If an error occurred related to the database query
1046	pub async fn pubkey_keypair(&self, public_key: &PublicKey) -> anyhow::Result<Option<(u32, Keypair)>> {
1047		if let Some(index) = self.inner.db.get_public_key_idx(&public_key).await? {
1048			Ok(Some((index, self.inner.seed.derive_vtxo_keypair(index))))
1049		} else {
1050			Ok(None)
1051		}
1052	}
1053
1054	/// Retrieves the [Keypair] for a provided [Vtxo]
1055	///
1056	/// # Arguments
1057	///
1058	/// * `vtxo` - The vtxo for which the key must be found
1059	///
1060	/// # Returns
1061	/// * `Ok(Some(Keypair))` - If the pubkey is found, the keypair is returned
1062	/// * `Err(anyhow::Error)` - If the corresponding public key doesn't exist
1063	///   in the database or a database error occurred.
1064	pub async fn get_vtxo_key(&self, vtxo: impl VtxoRef) -> anyhow::Result<Keypair> {
1065		let bare_vtxo = match vtxo.as_bare_vtxo() {
1066			Some(bare) => bare,
1067			None => Cow::Owned(self.get_vtxo_by_id(vtxo.vtxo_id()).await?.vtxo),
1068		};
1069		let pubkey = self.find_signable_clause(&bare_vtxo).await
1070			.context("VTXO is not signable by wallet")?
1071			.pubkey();
1072		let idx = self.inner.db.get_public_key_idx(&pubkey).await?
1073			.context("VTXO key not found")?;
1074		Ok(self.inner.seed.derive_vtxo_keypair(idx))
1075	}
1076
1077	#[deprecated(note = "use peek_address instead")]
1078	pub async fn peak_address(&self, index: u32) -> anyhow::Result<ark::Address> {
1079		self.peek_address(index).await
1080	}
1081
1082	/// Peek for an [ark::Address] at the given key index.
1083	///
1084	/// May return an error if the address at the given index has not been derived yet.
1085	pub async fn peek_address(&self, index: u32) -> anyhow::Result<ark::Address> {
1086		let properties = self.properties().await?;
1087		let network = properties.network;
1088		let keypair = self.peek_keypair(index).await?;
1089		let mailbox = self.mailbox_identifier();
1090
1091
1092		let (server_pubkey, mailbox_pubkey) =
1093			if let (Some(spk), Some(mpk)) = (properties.server_pubkey, properties.server_mailbox_pubkey) {
1094				(spk, mpk)
1095			} else {
1096				let (_, ark_info) = self.require_server().await?;
1097				(ark_info.server_pubkey, ark_info.mailbox_pubkey)
1098			};
1099
1100		Ok(ark::Address::builder()
1101			.testnet(network != bitcoin::Network::Bitcoin)
1102			.server_pubkey(server_pubkey)
1103			.pubkey_policy(keypair.public_key())
1104			.mailbox(mailbox_pubkey, mailbox, &keypair)
1105			.context("failed to assign mailbox")?
1106			.into_address()
1107			.context("failed to build address")?)
1108	}
1109
1110	/// Generate a new [ark::Address] and returns the index of the key used to create it.
1111	///
1112	/// This derives and stores the keypair directly after currently last revealed one.
1113	pub async fn new_address_with_index(&self) -> anyhow::Result<(ark::Address, u32)> {
1114		let (_, index) = self.derive_store_next_keypair().await?;
1115		let addr = self.peek_address(index).await?;
1116		Ok((addr, index))
1117	}
1118
1119	/// Generate a new mailbox [ark::Address].
1120	pub async fn new_address(&self) -> anyhow::Result<ark::Address> {
1121		let (addr, _) = self.new_address_with_index().await?;
1122		Ok(addr)
1123	}
1124
1125	/// Sign an arbitrary message with the key of the given [ark::Address].
1126	///
1127	/// Returns `Ok(None)` if the address is not one of this wallet's own
1128	/// addresses or its key has not been derived yet.
1129	///
1130	/// The resulting signature can be verified with
1131	/// [ark::message::verify_for_address] against the address.
1132	pub async fn sign_message(
1133		&self,
1134		message: &[u8],
1135		address: &ark::Address,
1136	) -> anyhow::Result<Option<secp256k1::schnorr::Signature>> {
1137		let pubkey = address.policy().user_pubkey();
1138		let Some((_, keypair)) = self.pubkey_keypair(&pubkey).await? else {
1139			return Ok(None);
1140		};
1141		Ok(Some(ark::message::sign(&keypair, message)))
1142	}
1143
1144	/// Create a new wallet
1145	///
1146	/// This function simply initiates a new wallet; use [Wallet::open] to open
1147	/// it afterwards. You can also call [Wallet::open] with `create_if_not_exists`
1148	/// set to true to avoid having to call this function.
1149	///
1150	/// `lock_manager` coordinates access to the wallet's protected resources. Pick a backend
1151	/// whose enforcement scope matches how the wallet is deployed — see [`crate::lock_manager`].
1152	pub async fn create(
1153		network: Network,
1154		seed: &WalletSeed,
1155		config: &Config,
1156		db: &dyn BarkPersister,
1157		lock_manager: &dyn LockManager,
1158		allow_unreachable_server: bool,
1159	) -> anyhow::Result<()> {
1160		trace!("Config: {:?}", config);
1161
1162		let wallet_fingerprint = seed.fingerprint();
1163
1164		// Block concurrent creators against the same locking universe. A
1165		// short timeout is fine: if a sibling process wins the race they
1166		// will have committed the wallet by the time we'd time out, and
1167		// the `read_properties` check below catches that case cleanly.
1168		let create_guard = lock_manager.lock(
1169			&format!("{}.create", wallet_fingerprint),
1170			Duration::from_secs(5),
1171		).await.context("wallet initialization already in progress")?;
1172
1173		if let Some(existing) = db.read_properties().await? {
1174			trace!("Existing config: {:?}", existing);
1175			bail!("cannot overwrite already existing config")
1176		}
1177
1178		// Try to connect to the server and get its pubkey. An unsafe ArkInfo
1179		// is treated the same as an unreachable server: when
1180		// `allow_unreachable_server` is set, both drop to `(None, None)` so
1181		// we can still produce an offline wallet and revisit the server on
1182		// the next open.
1183		let (server_pubkey, mailbox_pubkey) = match Self::connect_to_server(&config, network).await {
1184			Ok(conn) => {
1185				let ark_info = conn.ark_info().await;
1186				match check_ark_info_safe(&ark_info, config.vtxo_exit_margin) {
1187					Ok(()) => (Some(ark_info.server_pubkey), Some(ark_info.mailbox_pubkey)),
1188					Err(err) if allow_unreachable_server => {
1189						warn!("server-advertised ArkInfo is unsafe, \
1190							treating as unavailable: {:#}", err);
1191						(None, None)
1192					},
1193					Err(err) => return Err(err),
1194				}
1195			},
1196			Err(_) if allow_unreachable_server => (None, None),
1197			Err(err) => {
1198				bail!("Failed to connect to provided server: {:#}", err);
1199			},
1200		};
1201
1202		let properties = WalletProperties {
1203			network,
1204			fingerprint: wallet_fingerprint,
1205			server_pubkey,
1206			server_mailbox_pubkey: mailbox_pubkey,
1207		};
1208
1209		// write the config to db
1210		db.init_wallet(&properties).await.context("cannot init wallet in the database")?;
1211		info!("Created wallet with fingerprint: {}", wallet_fingerprint);
1212		if let Some(pk) = server_pubkey {
1213			info!("Stored server pubkey: {}", pk);
1214		}
1215
1216		// The wallet exists from this point on — drop the creation lock
1217		// so another process is free to open it.
1218		drop(create_guard);
1219
1220		Ok(())
1221	}
1222
1223	/// Open an existing wallet or create one if `options.create_if_not_exists` is true
1224	pub async fn open(
1225		network: Network,
1226		seed: WalletSeed,
1227		config: Config,
1228		args: OpenWalletArgs,
1229	) -> anyhow::Result<Wallet> {
1230		if !(1..=3).contains(&config.change_vtxo_split_factor) {
1231			bail!("change_vtxo_split_factor must be 1, 2 or 3, got {}",
1232				config.change_vtxo_split_factor,
1233			);
1234		}
1235
1236		let fingerprint = seed.fingerprint();
1237		let lock_manager = if let Some(lm) = args.lock_manager {
1238			lm
1239		} else {
1240			crate::lock_manager::platform_default(args.datadir.as_ref(), Some(fingerprint))
1241				.context("failed to instantiate platform default lock manager")?
1242		};
1243
1244		let db = if let Some(db) = args.persister {
1245			db
1246		} else {
1247			if let Some(ref datadir) = args.datadir {
1248				#[cfg(not(target_arch = "wasm32"))]
1249				if !datadir.exists() && args.create_if_not_exists {
1250					tokio::fs::create_dir_all(datadir).await.with_context(|| format!(
1251						"failed to create datadir at {}", datadir.display(),
1252					))?;
1253				}
1254			}
1255			crate::persist::platform_default(args.datadir.as_ref(), Some(fingerprint)).await
1256				.context("failed to instantiate platform default persister")?
1257		};
1258
1259		let mut created_now = false;
1260		let properties = if let Some(p) = db.read_properties().await? {
1261			p
1262		} else if args.create_if_not_exists {
1263			Self::create(
1264				network, &seed, &config, &*db, &*lock_manager, args.create_without_server,
1265			).await.context("error creating new wallet")?;
1266			created_now = true;
1267			db.read_properties().await?
1268				.context("create failed: no wallet properties after Wallet::create was called")?
1269		} else {
1270			bail!("wallet does not exist; use Wallet::create or \
1271				set options.create_if_not_exists to true");
1272		};
1273
1274		if properties.fingerprint != fingerprint {
1275			bail!("incorrect mnemonic")
1276		}
1277
1278		let chain_source = if let Some(ref url) = config.esplora_address {
1279			ChainSourceSpec::Esplora {
1280				url: url.clone(),
1281			}
1282		} else if let Some(ref url) = config.bitcoind_address {
1283			let auth = if let Some(ref c) = config.bitcoind_cookiefile {
1284				bitcoin_ext::rpc::Auth::CookieFile(c.clone())
1285			} else {
1286				bitcoin_ext::rpc::Auth::UserPass(
1287					config.bitcoind_user.clone().context("need bitcoind auth config")?,
1288					config.bitcoind_pass.as_ref().context("need bitcoind auth config")?
1289						.leak_ref().clone(),
1290				)
1291			};
1292			ChainSourceSpec::Bitcoind {
1293				url: url.clone(),
1294				auth,
1295				zmq: config.bitcoind_zmq_address.clone(),
1296			}
1297		} else {
1298			bail!("Need to either provide esplora or bitcoind info");
1299		};
1300
1301		#[cfg(feature = "socks5-proxy")]
1302		let chain_proxy = proxy_for_url(&config.socks5_proxy, chain_source.url())?;
1303		let chain_source_client = ChainSource::new(
1304			chain_source, properties.network, config.fallback_fee_rate,
1305			#[cfg(feature = "socks5-proxy")] chain_proxy.as_deref(),
1306		).await?;
1307		let chain = Arc::new(chain_source_client);
1308		chain.require_version().await
1309			.context("provided chain source doesn't meet version requirement")?;
1310
1311		let server = tokio::sync::OnceCell::new();
1312
1313		let notifications = NotificationDispatch::new();
1314		let movements = Arc::new(MovementManager::new(db.clone(), notifications.clone()));
1315		let exit = Exit::new(db.clone(), chain.clone(), movements.clone()).await?;
1316
1317		let onchain = args.onchain;
1318		let ret = Wallet { inner: Arc::new(WalletInner {
1319			config, db, lock_manager, seed, exit, movements, notifications, server, chain,
1320			onchain,
1321			daemon: parking_lot::Mutex::new(None),
1322			last_force_exit_scan_tip: tokio::sync::Mutex::new(None),
1323			round_secret_nonces: RoundSecretNonces::new(),
1324		})};
1325
1326		ret.inner.exit.load().await
1327			.context("error loading exit system after opening wallet")?;
1328
1329		let recovery = if !created_now {
1330			RecoveryStatus::NotRun
1331		} else if args.skip_recovery {
1332			info!("Seed-based wallet recovery explicitly skipped");
1333			RecoveryStatus::NotRun
1334		} else {
1335			// Recover any VTXOs backed up to the seed-derived recovery mailbox.
1336			// Best-effort so it can't abort wallet creation, but a failure means
1337			// funds may be missing — surface it loudly. Partial failures are
1338			// logged inside the recovery call.
1339			match ret.recover_from_mailbox().await {
1340				Ok(report) => RecoveryStatus::Completed(report),
1341				Err(e) => {
1342					error!("VTXO recovery from the recovery mailbox failed; funds may be \
1343						missing from this wallet until recovery succeeds: {:#}", e);
1344					RecoveryStatus::Failed(e)
1345				},
1346			}
1347		};
1348
1349		if args.run_daemon {
1350			ret.start_daemon()
1351				.context("failed to start daemon after opening wallet")?;
1352		}
1353
1354		// Last, so the callback only ever fires for an open that returns a wallet.
1355		if let Some(callback) = args.on_recovery_finished {
1356			callback(recovery);
1357		}
1358
1359		Ok(ret)
1360	}
1361
1362	/// Returns the config used to create/load the bark [Wallet].
1363	pub fn config(&self) -> &Config {
1364		&self.inner.config
1365	}
1366
1367	/// Retrieves the [WalletProperties] of the current bark [Wallet].
1368	pub async fn properties(&self) -> anyhow::Result<WalletProperties> {
1369		let properties = self.inner.db.read_properties().await?.context("Wallet is not initialised")?;
1370		Ok(properties)
1371	}
1372
1373	/// Returns the fingerprint of the wallet.
1374	pub fn fingerprint(&self) -> Fingerprint {
1375		self.inner.seed.fingerprint()
1376	}
1377
1378	async fn connect_to_server(
1379		config: &Config,
1380		network: Network,
1381	) -> anyhow::Result<ServerConnection> {
1382		let server_address = crate::utils::url_with_default_https_scheme(&config.server_address);
1383		let mut builder = ServerConnection::builder()
1384			.address(&server_address)
1385			.network(network);
1386
1387		#[cfg(feature = "socks5-proxy")]
1388		if let Some(proxy) = proxy_for_url(&config.socks5_proxy, &server_address)? {
1389			builder = builder.proxy(&proxy)
1390		}
1391
1392		#[allow(deprecated)]
1393		{
1394			if let Some(ref token) = config.server_access_token {
1395				builder = builder.access_token(token);
1396			}
1397		}
1398
1399		if let Some(ref ua) = config.user_agent {
1400			builder = builder.user_agent(ua);
1401		}
1402
1403		builder.connect().await.map_err(wrap_server_connect_error)
1404			.context("Failed to connect to Ark server")
1405	}
1406
1407	async fn require_server(&self) -> anyhow::Result<(ServerConnection, ArkInfo)> {
1408		// Connect lazily if not yet connected. `get_or_try_init` ensures
1409		// concurrent callers on a cold cell all await the same in-flight
1410		// connect future instead of each opening a fresh gRPC channel.
1411		let conn = self.inner.server.get_or_try_init(|| async {
1412			let network = self.properties().await?.network;
1413			Self::connect_to_server(&self.inner.config, network).await
1414				.context("You should be connected to Ark server to perform this action")
1415		}).await?.clone();
1416
1417		let ark_info = conn.ark_info().await;
1418		check_ark_info_safe(&ark_info, self.inner.config.vtxo_exit_margin)?;
1419		self.check_and_store_server_keys(&ark_info).await?;
1420
1421		Ok((conn, ark_info))
1422	}
1423
1424	pub async fn refresh_server(&self) -> anyhow::Result<()> {
1425		// If the cell is still cold, initialise it with a fresh connection.
1426		// If it is already initialised, run a heartbeat against the existing
1427		// one — `OnceCell` does not support replacing a stored value, but
1428		// `ServerConnection` is built around a tonic `Channel` which
1429		// transparently reconnects, so we don't need to swap it.
1430		let srv = self.inner.server.get_or_try_init(|| async {
1431			let properties = self.properties().await?;
1432			Self::connect_to_server(&self.inner.config, properties.network).await
1433				.map_err(anyhow::Error::from)
1434		}).await?;
1435
1436		srv.check_connection().await?;
1437		let ark_info = srv.ark_info().await;
1438		ark_info.fees.validate().context("invalid fee schedule")?;
1439		check_ark_info_safe(&ark_info, self.inner.config.vtxo_exit_margin)?;
1440		self.check_and_store_server_keys(&ark_info).await?;
1441
1442		Ok(())
1443	}
1444
1445	/// Returns the configured onchain wallet, if any.
1446	pub fn onchain(&self) -> Option<Arc<tokio::sync::RwLock<dyn OnchainWalletTrait>>> {
1447		self.inner.onchain.clone()
1448	}
1449
1450	/// Sync the internal onchain wallet against the chain source, if one is configured.
1451	pub async fn sync_onchain(&self) -> anyhow::Result<()> {
1452		if let Some(onchain) = self.inner.onchain.as_ref() {
1453			onchain.write().await.sync(self.chain()).await?;
1454		}
1455		Ok(())
1456	}
1457
1458	/// Validate that the server's public keys match what we have stored,
1459	/// and persist them if this is the first time connecting after an upgrade.
1460	///
1461	/// Returns an error (via `bail!`) if either the server pubkey or mailbox
1462	/// pubkey differs from the stored value; callers must not proceed with
1463	/// server operations on error.
1464	async fn check_and_store_server_keys(&self, ark_info: &ArkInfo) -> anyhow::Result<()> {
1465		let properties = self.properties().await?;
1466
1467		if let Some(stored_pubkey) = properties.server_pubkey {
1468			if stored_pubkey != ark_info.server_pubkey {
1469				log_server_pubkey_changed_error(stored_pubkey, ark_info.server_pubkey);
1470				bail!("Server public key has changed. You should exit all your VTXOs!");
1471			}
1472		} else {
1473			self.inner.db.set_server_pubkey(ark_info.server_pubkey).await?;
1474			info!("Stored server pubkey for existing wallet: {}", ark_info.server_pubkey);
1475		}
1476
1477		if let Some(stored_mailbox_pubkey) = properties.server_mailbox_pubkey {
1478			if stored_mailbox_pubkey != ark_info.mailbox_pubkey {
1479				log_server_mailbox_pubkey_changed_error(stored_mailbox_pubkey, ark_info.mailbox_pubkey);
1480				bail!("Server mailbox public key has changed.");
1481			}
1482		} else {
1483			self.inner.db.set_server_mailbox_pubkey(ark_info.mailbox_pubkey).await?;
1484			info!("Stored server mailbox pubkey for existing wallet: {}", ark_info.mailbox_pubkey);
1485		}
1486
1487		Ok(())
1488	}
1489
1490	/// Return [ArkInfo] fetched on last handshake with the Ark server
1491	pub async fn ark_info(&self) -> anyhow::Result<Option<ArkInfo>> {
1492		match self.inner.server.get() {
1493			Some(srv) => Ok(Some(srv.ark_info().await)),
1494			None => Ok(None),
1495		}
1496	}
1497
1498	/// Return [ArkInfo], connecting lazily if not yet connected.
1499	///
1500	/// Errors if the server cannot be reached or if the server's pubkey
1501	/// or mailbox pubkey no longer matches what was stored at wallet
1502	/// creation.
1503	pub async fn require_ark_info(&self) -> anyhow::Result<ArkInfo> {
1504		let (_, ark_info) = self.require_server().await?;
1505		Ok(ark_info)
1506	}
1507
1508	/// Return the [Balance] of the wallet.
1509	///
1510	/// When not running the daemon, make sure you sync before calling this method.
1511	pub async fn balance(&self) -> anyhow::Result<Balance> {
1512		let vtxos = self.vtxos().await?;
1513
1514		let spendable = {
1515			let mut v = vtxos.iter().collect();
1516			VtxoStateKind::Spendable.filter_vtxos(&mut v).await?;
1517			v.into_iter().map(|v| v.amount()).sum::<Amount>()
1518		};
1519
1520		let pending_lightning_send = self.pending_lightning_send_vtxos().await?.iter()
1521			.map(|v| v.amount())
1522			.sum::<Amount>();
1523
1524		let claimable_lightning_receive = self.claimable_lightning_receive_balance().await?;
1525
1526		let pending_board = self.pending_board_vtxos().await?.iter()
1527			.map(|v| v.amount())
1528			.sum::<Amount>();
1529
1530		let pending_in_round = self.pending_round_balance().await?;
1531
1532		let pending_exit = self.exit_mgr().try_pending_total();
1533
1534		Ok(Balance {
1535			spendable,
1536			pending_in_round,
1537			pending_lightning_send,
1538			claimable_lightning_receive,
1539			pending_exit,
1540			pending_board,
1541		})
1542	}
1543
1544	/// Fetches [Vtxo]'s funding transaction and validates the VTXO against it.
1545	pub async fn validate_vtxo(&self, vtxo: &Vtxo<Full>) -> Result<(), VtxoValidationError> {
1546		let tx = self.inner.chain.get_tx(&vtxo.chain_anchor().txid).await
1547			.map_err(VtxoValidationError::Chain)?
1548			.ok_or(VtxoValidationError::AnchorNotFound)?;
1549
1550		vtxo.validate(&tx).map_err(VtxoValidationError::Invalid)
1551	}
1552
1553	/// Manually import a VTXO into the wallet.
1554	///
1555	/// # Arguments
1556	/// * `vtxo` - The VTXO to import
1557	///
1558	/// # Errors
1559	/// Returns an error if:
1560	/// - The VTXO's chain anchor is not found or invalid
1561	/// - The wallet doesn't own a signable clause for the VTXO
1562	pub async fn import_vtxo(&self, vtxo: &Vtxo<Full>) -> anyhow::Result<()> {
1563		if self.inner.db.get_wallet_vtxo(vtxo.id()).await?.is_some() {
1564			info!("VTXO {} already exists in wallet, skipping import", vtxo.id());
1565			return Ok(());
1566		}
1567
1568		self.validate_vtxo(vtxo).await.context("VTXO validation failed")?;
1569
1570		if self.find_signable_clause(vtxo).await.is_none() {
1571			bail!("VTXO {} is not owned by this wallet (no signable clause found)", vtxo.id());
1572		}
1573
1574		let current_height = self.inner.chain.tip().await?;
1575		if vtxo.expiry_height() <= current_height {
1576			bail!("Vtxo {} has expired", vtxo.id());
1577		}
1578
1579		self.store_spendable_vtxos([vtxo]).await.context("failed to store imported VTXO")?;
1580
1581		info!("Successfully imported VTXO {}", vtxo.id());
1582		Ok(())
1583	}
1584
1585	/// Retrieves the full state of a [Vtxo] for a given [VtxoId] if it exists in the database.
1586	pub async fn get_vtxo_by_id(&self, vtxo_id: VtxoId) -> anyhow::Result<WalletVtxo> {
1587		let vtxo = self.inner.db.get_wallet_vtxo(vtxo_id).await
1588			.with_context(|| format!("Error when querying vtxo {} in database", vtxo_id))?
1589			.with_context(|| format!("The VTXO with id {} cannot be found", vtxo_id))?;
1590		Ok(vtxo)
1591	}
1592
1593	/// Hydrate a VTXO into its full form, including the unilateral exit chain.
1594	///
1595	/// [Wallet::get_vtxo_by_id] returns the bare form ([WalletVtxo] holds
1596	/// [Vtxo<ark::vtxo::Bare>]). This method reads the genesis chain from the
1597	/// database and reassembles the full VTXO. Use it from external SDK
1598	/// callers that need the chain (e.g. to feed into [ArkoorPackageBuilder](ark::arkoor::ArkoorPackageBuilder)
1599	/// or [Wallet::register_vtxo_transactions_with_server]).
1600	pub async fn get_full_vtxo(&self, vtxo_id: VtxoId) -> anyhow::Result<Vtxo<Full>> {
1601		self.inner.db.get_full_vtxo(vtxo_id).await
1602			.with_context(|| format!("Error when querying full vtxo {} in database", vtxo_id))?
1603			.with_context(|| format!("The VTXO with id {} cannot be found", vtxo_id))
1604	}
1605
1606	/// Similar to [Wallet::get_full_vtxo] but it retrieves the full variant of each given VTXO.
1607	pub async fn get_full_vtxos<V: VtxoRef>(
1608		&self,
1609		vtxos: impl IntoIterator<Item = V>,
1610	) -> anyhow::Result<Vec<Vtxo<Full>>> {
1611		let ids = vtxos.into_iter().map(|v| v.vtxo_id()).collect::<Vec<_>>();
1612		self.inner.db.get_full_vtxos(&ids).await
1613			.with_context(||
1614				format!("Error when querying full vtxos in database with IDs: {:?}", ids)
1615			)
1616	}
1617
1618	/// Fetches all movements ordered from newest to oldest.
1619	#[deprecated(since="0.1.0-beta.5", note = "Use Wallet::history instead")]
1620	pub async fn movements(&self) -> anyhow::Result<Vec<Movement>> {
1621		self.history().await
1622	}
1623
1624	/// Fetches all wallet fund movements ordered from newest to oldest.
1625	pub async fn history(&self) -> anyhow::Result<Vec<Movement>> {
1626		Ok(self.inner.db.get_all_movements().await?)
1627	}
1628
1629	/// Applies an [RFC 7396](https://www.rfc-editor.org/rfc/rfc7396) JSON Merge Patch to the
1630	/// metadata of a movement.
1631	///
1632	/// ```no_run
1633	/// # use serde_json::json;
1634	/// # async fn example(
1635	/// #     wallet: &bark::Wallet,
1636	/// #     id: bark::movement::MovementId,
1637	/// # ) -> anyhow::Result<()> {
1638	/// // Add or overwrite a key.
1639	/// wallet.update_history_metadata(id, &json!({"note": "refund issued"})).await?;
1640	///
1641	/// // Delete a key (null means remove).
1642	/// wallet.update_history_metadata(id, &json!({"note": null})).await?;
1643	///
1644	/// // Nested merge.
1645	/// wallet.update_history_metadata(id, &json!({"counterparty": {"name": "Alice"}})).await?;
1646	/// # Ok(()) }
1647	/// ```
1648	pub async fn update_history_metadata(
1649		&self,
1650		movement_id: MovementId,
1651		patch: &serde_json::Value,
1652	) -> anyhow::Result<()> {
1653		self.inner.movements.patch_metadata(movement_id, patch).await?;
1654		Ok(())
1655	}
1656
1657	/// Query the wallet history by the given payment method
1658	pub async fn history_by_payment_method(
1659		&self,
1660		payment_method: &PaymentMethod,
1661	) -> anyhow::Result<Vec<Movement>> {
1662		let mut ret = self.inner.db.get_movements_by_payment_method(payment_method).await?;
1663		ret.sort_by_key(|m| m.id);
1664		Ok(ret)
1665	}
1666
1667	/// Returns all VTXOs from the database.
1668	pub async fn all_vtxos(&self) -> anyhow::Result<Vec<WalletVtxo>> {
1669		Ok(self.inner.db.get_all_vtxos().await?)
1670	}
1671
1672	/// Returns all not spent vtxos
1673	pub async fn vtxos(&self) -> anyhow::Result<Vec<WalletVtxo>> {
1674		Ok(self.inner.db.get_vtxos_by_state(&VtxoStateKind::UNSPENT_STATES).await?)
1675	}
1676
1677	/// Returns all vtxos matching the provided predicate
1678	pub async fn vtxos_with(&self, filter: &impl FilterVtxos) -> anyhow::Result<Vec<WalletVtxo>> {
1679		let mut vtxos = self.vtxos().await?;
1680		filter.filter_vtxos(&mut vtxos).await.context("error filtering vtxos")?;
1681		Ok(vtxos)
1682	}
1683
1684	/// Returns all spendable vtxos
1685	pub async fn spendable_vtxos(&self) -> anyhow::Result<Vec<WalletVtxo>> {
1686		Ok(self.vtxos_with(&VtxoStateKind::Spendable).await?)
1687	}
1688
1689	/// Returns all spendable vtxos matching the provided predicate
1690	pub async fn spendable_vtxos_with(
1691		&self,
1692		filter: &impl FilterVtxos,
1693	) -> anyhow::Result<Vec<WalletVtxo>> {
1694		let mut vtxos = self.spendable_vtxos().await?;
1695		filter.filter_vtxos(&mut vtxos).await.context("error filtering vtxos")?;
1696		Ok(vtxos)
1697	}
1698
1699	/// Returns all vtxos that will expire within `threshold` blocks
1700	pub async fn get_expiring_vtxos(
1701		&self,
1702		threshold: BlockHeight,
1703	) -> anyhow::Result<Vec<WalletVtxo>> {
1704		let expiry = self.inner.chain.tip().await? + threshold;
1705		let filter = VtxoFilter::new(&self).expires_before(expiry);
1706		Ok(self.spendable_vtxos_with(&filter).await?)
1707	}
1708
1709	/// Performs maintenance tasks and performs refresh interactively until finished when needed.
1710	///
1711	/// This can take a long period of time due to registering boards, syncing
1712	/// rounds, arkoors, checking pending lightning payments and refreshing VTXOs
1713	/// if necessary.
1714	pub async fn maintenance(&self) -> anyhow::Result<()> {
1715		info!("Starting wallet maintenance in interactive mode");
1716		self.sync().await;
1717
1718		// First try progress any rounds that exist, best effort.
1719		let rounds = self.progress_pending_rounds(None).await;
1720		if let Err(e) = rounds.as_ref() {
1721			warn!("Error progressing pending rounds: {:#}", e);
1722		}
1723
1724		// Then if there are still some participations open, try to cancel them.
1725		let states = self.inner.db.get_pending_round_state_ids().await?;
1726		for id in states {
1727			debug!("Cancelling pending round participation {}", id);
1728			let mut state = match self.lock_wait_round_state(id).await {
1729				Ok(Some(s)) => s,
1730				Ok(None) => continue, // round disappeared, not our problem
1731				Err(e) => {
1732					warn!("Failed to lock round state with id {}: {:#}", id, e);
1733					continue;
1734				}
1735			};
1736			if let Err(e) = state.state_mut().try_cancel(self).await {
1737				warn!("Error cancelling pending round: {:#}", e);
1738			}
1739		}
1740
1741		// And then call refresh so that we can start again.
1742		let refresh = self.maintenance_refresh().await;
1743		if let Err(e) = refresh.as_ref() {
1744			warn!("Error refreshing VTXOs: {:#}", e);
1745		}
1746
1747		if rounds.is_err() || refresh.is_err() {
1748			bail!("Maintenance encountered errors.\nprogress_rounds: {:#?}\nrefresh: {:#?}",
1749				rounds, refresh,
1750			);
1751		}
1752
1753		Ok(())
1754	}
1755
1756	/// Performs maintenance tasks and schedules delegated refresh when needed. This risks spending
1757	/// users' funds because refreshing may cost fees and any pending exits will be progressed.
1758	///
1759	/// This can take a long period of time due to syncing the onchain wallet, registering boards,
1760	/// syncing rounds, arkoors, and the exit system, checking pending lightning payments and
1761	/// refreshing VTXOs if necessary.
1762	pub async fn maintenance_delegated(&self) -> anyhow::Result<()> {
1763		info!("Starting wallet maintenance in delegated mode");
1764		self.sync().await;
1765		let rounds = self.progress_pending_rounds(None).await;
1766		if let Err(e) = rounds.as_ref() {
1767			warn!("Error progressing pending rounds: {:#}", e);
1768		}
1769		let refresh = self.maybe_schedule_maintenance_refresh_delegated().await;
1770		if let Err(e) = refresh.as_ref() {
1771			warn!("Error refreshing VTXOs: {:#}", e);
1772		}
1773
1774		if rounds.is_err() || refresh.is_err() {
1775			bail!("Delegated maintenance encountered errors.\n\
1776				progress_rounds: {:#?}\nrefresh: {:#?}",
1777				rounds, refresh,
1778			);
1779		}
1780
1781		Ok(())
1782	}
1783
1784	/// Actively join the given in-flight round `attempt` with all VTXOs due for
1785	/// maintenance refresh, dropping any input the server rejects as unusable and
1786	/// re-submitting the rest to the *same* attempt (the server keeps its submit
1787	/// window open after a rejection, so the corrected participation still lands
1788	/// in this round).
1789	///
1790	/// This is the shared core of interactive maintenance: the blocking
1791	/// [Wallet::maintenance_refresh] calls it and then drives the round to
1792	/// completion, while the daemon calls it on the round Attempt event and lets
1793	/// [Wallet::progress_pending_rounds] carry the round forward. Mirrors
1794	/// [Wallet::maybe_schedule_maintenance_refresh_delegated].
1795	///
1796	/// Returns the id of the round state we joined, or `None` if there was
1797	/// nothing economical to refresh.
1798	pub(crate) async fn join_round_for_maintenance_refresh(
1799		&self,
1800		attempt: &RoundAttempt,
1801	) -> anyhow::Result<Option<RoundStateId>> {
1802		self.maintenance_refresh_retry_loop(|part| async move {
1803			info!("Joining round {} for maintenance refresh ({} vtxos)",
1804				attempt.round_seq, part.inputs.len());
1805			Ok(Some(self.join_attempt_interactive(
1806				part, attempt, Some(RoundMovement::Refresh),
1807			).await?.id()))
1808		}).await.context("failed to join round for maintenance refresh")
1809	}
1810
1811	/// Checks VTXOs that are due to be refreshed, and schedules a delegated refresh if any
1812	///
1813	/// This will include any VTXOs within the expiry threshold
1814	/// ([Config::vtxo_refresh_expiry_threshold]) or those which
1815	/// are uneconomical to exit due to onchain network conditions.
1816	///
1817	/// Returns a [RoundStateId] if a refresh is scheduled.
1818	pub async fn maybe_schedule_maintenance_refresh_delegated(
1819		&self,
1820	) -> anyhow::Result<Option<RoundStateId>> {
1821		self.maintenance_refresh_retry_loop(|part| async move {
1822			info!("Scheduling delegated maintenance refresh ({} vtxos)", part.inputs.len());
1823			Ok(Some(self.join_next_round_delegated(part, Some(RoundMovement::Refresh)).await?.id()))
1824		}).await.context("failed to schedule delegated maintenance refresh")
1825	}
1826
1827	/// The retry loop shared by the interactive and delegated maintenance refreshes.
1828	///
1829	/// Selects the VTXOs due for refresh (minus any the server has already rejected
1830	/// as unusable), runs `attempt_refresh` for them, and if it fails naming unusable
1831	/// inputs, drops those and retries — up to 10 times. Both submission modes
1832	/// validate inputs synchronously, so a rejection surfaces here rather than
1833	/// poisoning the batch forever; `attempt_refresh` is the only part that differs.
1834	async fn maintenance_refresh_retry_loop<F, Fut>(
1835		&self,
1836		attempt_refresh: F,
1837	) -> anyhow::Result<Option<RoundStateId>>
1838	where
1839		F: Fn(RoundParticipation) -> Fut,
1840		Fut: Future<Output = anyhow::Result<Option<RoundStateId>>>,
1841	{
1842		let mut excluded = HashSet::new();
1843		for _ in 0..10 {
1844			let vtxos = self.get_vtxos_to_refresh_with_excluded(excluded.iter().copied()).await?;
1845			match (vtxos.is_empty(), excluded.is_empty()) {
1846				// Every VTXO due for refresh has been excluded as unusable by the server:
1847				// there is nothing left to submit, so surface an error rather than
1848				// silently reporting success.
1849				(true, false) => {
1850					warn!("no VTXOs to refresh after exclusions: {:?}", excluded);
1851					bail!("no VTXOs to refresh after excluding: {:?}", excluded);
1852				},
1853				// Nothing was due for refresh in the first place.
1854				(true, true) => return Ok(None),
1855				// Still have VTXOs to submit (possibly after dropping some exclusions).
1856				(false, _) => {},
1857			}
1858			let part = match self.build_refresh_participation(vtxos).await? {
1859				Some(participation) => participation,
1860				None => return Ok(None),
1861			};
1862
1863			match attempt_refresh(part).await {
1864				Ok(state_id) => return Ok(state_id),
1865				Err(e) => {
1866					let rejected = rejected_vtxos_from_error(&e).into_iter()
1867						.filter(|id| !excluded.contains(id))
1868						.collect::<Vec<_>>();
1869					if rejected.is_empty() {
1870						return Err(e);
1871					}
1872					warn!("Maintenance refresh rejected {} unusable input(s) ({:?}); \
1873						retrying without them", rejected.len(), rejected);
1874					excluded.extend(rejected);
1875				},
1876			}
1877		}
1878		bail!("Maintenance refresh failed after 10 retries");
1879	}
1880
1881	/// Performs an interactive refresh of all VTXOs that are due to be refreshed, if any
1882	///
1883	/// This will include any VTXOs within the expiry threshold
1884	/// ([Config::vtxo_refresh_expiry_threshold]) or those which
1885	/// are uneconomical to exit due to onchain network conditions.
1886	///
1887	/// Waits for a round to start, joins it, dropping any inputs the server rejects
1888	/// as unusable and retries within the same attempt, then drives that round to
1889	/// completion.
1890	///
1891	/// Returns a [RoundStatus] if a refresh occurs.
1892	pub async fn maintenance_refresh(&self) -> anyhow::Result<Option<RoundStatus>> {
1893		if self.get_vtxos_to_refresh().await?.is_empty() {
1894			return Ok(None);
1895		}
1896
1897		info!("Waiting for round to perform maintenance refresh...");
1898		let mut events = self.subscribe_round_events().await?;
1899		while let Some(event) = events.next().await {
1900			let event = event.context("error on round event stream")?;
1901			if let RoundEvent::Attempt(a) = event && a.attempt_seq == 0 {
1902				debug!("Round {} started, triggering maintenance refresh", a.round_seq);
1903				let state_id = match self.join_round_for_maintenance_refresh(&a).await? {
1904					Some(id) => id,
1905					None => return Ok(None),
1906				};
1907				// We submitted up-front, so drive the (now ongoing) round to completion
1908				// on this same event stream.
1909				let state = self.lock_wait_round_state(state_id).await?
1910					.context("maintenance refresh round state vanished after joining")?;
1911				return Ok(Some(self.drive_round_state(state, &mut events).await?));
1912			}
1913		}
1914		Ok(None)
1915	}
1916
1917	/// Sync offchain wallet and update onchain fees. This is a much more lightweight alternative
1918	/// to [Wallet::maintenance] as it will not refresh VTXOs or sync the onchain wallet.
1919	///
1920	/// Notes:
1921	/// - Exits are only synced if we detect onchain activity which has force-exited our VTXO.
1922	pub async fn sync(&self) {
1923		self.inner.chain.invalidate_caches().await;
1924
1925		futures::join!(
1926			async {
1927				// NB: order matters here, if syncing call fails,
1928				// we still want to update the fee rates
1929				if let Err(e) = self.inner.chain.update_fee_rates(self.inner.config.fallback_fee_rate).await {
1930					warn!("Error updating fee rates: {:#}", e);
1931				}
1932			},
1933			async {
1934				if let Err(e) = self.sync_mailbox().await {
1935					warn!("Error in mailbox sync: {:#}", e);
1936				}
1937			},
1938			async {
1939				if let Err(e) = self.sync_pending_rounds().await {
1940					warn!("Error while trying to progress rounds awaiting confirmations: {:#}", e);
1941				}
1942			},
1943			async {
1944				if let Err(e) = self.sync_pending_lightning_send_vtxos().await {
1945					warn!("Error syncing pending lightning payments: {:#}", e);
1946				}
1947			},
1948			async {
1949				if let Err(e) = self.sync_pending_arkoor_sends().await {
1950					warn!("Error syncing pending arkoor sends: {:#}", e);
1951				}
1952			},
1953			async {
1954				if let Err(e) = self.try_claim_all_lightning_receives(false).await {
1955					warn!("Error claiming pending lightning receives: {:#}", e);
1956				}
1957			},
1958			async {
1959				if let Err(e) = self.sync_pending_boards().await {
1960					warn!("Error syncing pending boards: {:#}", e);
1961				}
1962			},
1963			async {
1964				if let Err(e) = self.sync_pending_offboards().await {
1965					warn!("Error syncing pending offboards: {:#}", e);
1966				}
1967			},
1968			async {
1969				if let Err(e) = self.sync_force_exited_vtxos().await {
1970					warn!("Error scanning for on-chain-exited VTXOs: {:#}", e);
1971				}
1972			},
1973			async {
1974				// Re-assert recovery state so vtxos whose mailbox post or
1975				// registration failed at store time, or that predate the
1976				// recovery mechanism, get caught up (non-critical).
1977				if let Err(e) = self.catchup_recovery_vtxos().await {
1978					warn!("Failed to catch up recovery VTXOs with server: {:#}", e);
1979				}
1980			}
1981		);
1982	}
1983
1984	/// Sync the transaction status of unilateral exits
1985	///
1986	/// This will not progress the unilateral exits in any way, it will merely check the
1987	/// transaction status of each transaction as well as check whether any exits have become
1988	/// claimable or have been claimed.
1989	pub async fn sync_exits(&self) -> anyhow::Result<()> {
1990		self.exit_mgr().sync(&self).await?;
1991		Ok(())
1992	}
1993
1994	/// Progress unilateral exits
1995	///
1996	/// This risks spending users' funds because refreshing may cost fees and any
1997	/// pending exits will be progressed.
1998	pub async fn progress_exits(&self) -> anyhow::Result<()> {
1999		self.exit_mgr().progress_exits_with_cpfp(&self, None).await?;
2000		Ok(())
2001	}
2002
2003	/// Detect spendable VTXOs that were exited on-chain without the user asking for it — e.g.
2004	/// the server's watchman progressing a shared tree, or a third party's unilateral exit
2005	/// dragging a parent on-chain — and route them into the unilateral-exit flow so the funds
2006	/// can be claimed on-chain.
2007	///
2008	/// Such VTXOs are otherwise left `Spendable` by a normal sync even though the server now
2009	/// rejects spending them, leaving the user stuck. We detect them by checking, on each new
2010	/// chain tip, whether any spendable VTXO's own funding tx is already on-chain.
2011	///
2012	/// This is deliberately independent of the onchain wallet sync, since the onchain wallet
2013	/// may be disabled. The caller must also ensure that the wallet state is up to date before
2014	/// calling this.
2015	pub async fn sync_force_exited_vtxos(&self) -> anyhow::Result<()> {
2016		// A VTXO's on-chain status can only change across blocks, so only scan when the tip moves.
2017		let tip = self.inner.chain.tip().await?;
2018		let mut lock = self.inner.last_force_exit_scan_tip.lock().await;
2019		if *lock == Some(tip) {
2020			return Ok(());
2021		}
2022
2023		// Skip VTXOs already being exited.
2024		let exiting = self.exit_mgr().get_exit_vtxo_ids().await;
2025		let vtxos = self.inner.db.get_vtxos_by_state(&[VtxoStateKind::Spendable]).await?
2026			.into_iter()
2027			.filter(|v| !exiting.contains(&v.vtxo.id()));
2028
2029		// Check each candidate's funding tx in parallel.
2030		let mut checked = FuturesUnordered::new();
2031		for wv in vtxos {
2032			let chain = self.inner.chain.clone();
2033			checked.push(async move {
2034				let txid = wv.vtxo_id().to_point().txid;
2035				let status = chain.tx_status(txid).await;
2036				(wv, status)
2037			});
2038		}
2039
2040		let mut to_exit = Vec::new();
2041		while let Some((vtxo, status)) = futures::StreamExt::next(&mut checked).await {
2042			match status {
2043				Ok(TxStatus::NotFound) => {},
2044				Ok(_) => {
2045					info!("VTXO {} was exited on-chain without us; routing it to a claimable exit",
2046						vtxo.vtxo.id(),
2047					);
2048					to_exit.push(vtxo.vtxo);
2049				},
2050				Err(e) => warn!("Could not check on-chain status of VTXO {}: {:#}",
2051					vtxo.vtxo.id(), e,
2052				),
2053			}
2054		}
2055
2056		if !to_exit.is_empty() {
2057			self.exit_mgr().start_exit_for_vtxos(&to_exit).await
2058				.context("failed to start exit for on-chain-exited VTXOs")?;
2059
2060			*lock = Some(tip);
2061			self.sync_exits().await
2062				.context("failed to sync exits after starting new ones")?;
2063		} else {
2064			*lock = Some(tip);
2065		}
2066
2067		Ok(())
2068	}
2069
2070	/// Drop a specific [Vtxo] from the database. This is destructive and will result in a loss of
2071	/// funds.
2072	pub async fn dangerous_drop_vtxo(&self, vtxo_id: VtxoId) -> anyhow::Result<()> {
2073		warn!("Drop vtxo {} from the database", vtxo_id);
2074		self.inner.db.remove_vtxo(vtxo_id).await?;
2075		Ok(())
2076	}
2077
2078	/// Drop all VTXOs from the database. This is destructive and will result in a loss of funds.
2079	//TODO(stevenroose) improve the way we expose dangerous methods
2080	pub async fn dangerous_drop_all_vtxos(&self) -> anyhow::Result<()> {
2081		warn!("Dropping all vtxos from the db...");
2082		for vtxo in self.vtxos().await? {
2083			self.inner.db.remove_vtxo(vtxo.id()).await?;
2084		}
2085
2086		self.exit_mgr().dangerous_clear_exit().await?;
2087		Ok(())
2088	}
2089
2090	/// Checks if the provided VTXO has some counterparty risk in the current wallet.
2091	///
2092	/// An arkoor vtxo is considered to have some counterparty risk if it is
2093	/// (directly or not) based on round VTXOs that aren't owned by the
2094	/// wallet. The check inspects the genesis chain, so this takes a full
2095	/// VTXO; callers working from a bare listing should hydrate via
2096	/// [Wallet::get_full_vtxo] or [BarkPersister::get_full_vtxos] first.
2097	async fn has_counterparty_risk(&self, vtxo: &Vtxo<Full>) -> anyhow::Result<bool> {
2098		for past_pks in vtxo.past_arkoor_pubkeys() {
2099			let mut owns_any = false;
2100			for past_pk in past_pks {
2101				if self.inner.db.get_public_key_idx(&past_pk).await?.is_some() {
2102					owns_any = true;
2103					break;
2104				}
2105			}
2106			if !owns_any {
2107				return Ok(true);
2108			}
2109		}
2110
2111		let my_clause = self.find_signable_clause(vtxo).await;
2112		Ok(!my_clause.is_some())
2113	}
2114
2115	pub async fn build_refresh_participation<V: VtxoRef>(
2116		&self,
2117		vtxos: impl IntoIterator<Item = V>,
2118	) -> anyhow::Result<Option<RoundParticipation>> {
2119		self.inner_build_refresh_participation(vtxos, None).await
2120	}
2121
2122	pub async fn build_scheduled_refresh_participation<V: VtxoRef>(
2123		&self,
2124		vtxos: impl IntoIterator<Item = V>,
2125		height: BlockHeight,
2126	) -> anyhow::Result<Option<RoundParticipation>> {
2127		self.inner_build_refresh_participation(vtxos, Some(height)).await
2128	}
2129
2130	async fn inner_build_refresh_participation<V: VtxoRef>(
2131		&self,
2132		vtxos: impl IntoIterator<Item = V>,
2133		height: Option<BlockHeight>,
2134	) -> anyhow::Result<Option<RoundParticipation>> {
2135		let (vtxos, total_amount) = {
2136			let iter = vtxos.into_iter();
2137			let size_hint = iter.size_hint();
2138			let mut vtxos = Vec::<Vtxo<Full>>::with_capacity(size_hint.1.unwrap_or(size_hint.0));
2139			let mut amount = Amount::ZERO;
2140			for vref in iter {
2141				// We use a Vec here instead of a HashMap or a HashSet of IDs because for the kinds
2142				// of elements we expect to deal with, a Vec is likely to be quicker. The overhead
2143				// of hashing each ID and making additional allocations isn't likely to be worth it
2144				// for what is likely to be a handful of VTXOs or at most a couple of hundred.
2145				let id = vref.vtxo_id();
2146				if vtxos.iter().any(|v| v.id() == id) {
2147					bail!("duplicate VTXO id: {}", id);
2148				}
2149				let vtxo = if let Some(vtxo) = vref.into_full_vtxo() {
2150					vtxo
2151				} else {
2152					// Listings/selection return bare wallet vtxos; the round
2153					// flow needs the full chain to forfeit and register.
2154					self.inner.db.get_full_vtxo(id).await?
2155						.with_context(|| format!("vtxo with id {} not found", id))?
2156				};
2157				amount += vtxo.amount();
2158				vtxos.push(vtxo);
2159			}
2160			(vtxos, amount)
2161		};
2162
2163		if vtxos.is_empty() {
2164			info!("Skipping refresh since no VTXOs are provided.");
2165			return Ok(None);
2166		}
2167		ensure!(total_amount >= VTXO_DUST,
2168			"vtxo amount must be at least {} to participate in a round",
2169			VTXO_DUST,
2170		);
2171
2172		// Calculate refresh fees
2173		let (_, ark_info) = self.require_server().await?;
2174		let refresh_height = match height {
2175			Some(height) => height,
2176			None => self.inner.chain.tip().await?,
2177		};
2178
2179		let vtxo_fee_infos = vtxos.iter()
2180			.map(|v| VtxoFeeInfo::from_vtxo_and_tip(v, refresh_height));
2181		let fee = ark_info.fees.refresh.calculate(vtxo_fee_infos).context("fee overflowed")?;
2182		let output_amount = validate_and_subtract_fee_min_dust(total_amount, fee, VTXO_DUST)?;
2183
2184		info!("Refreshing {} VTXOs (total amount = {}, fee = {}, output = {}).",
2185			vtxos.len(), total_amount, fee, output_amount,
2186		);
2187		let (user_keypair, _) = self.derive_store_next_keypair().await?;
2188		let req = VtxoRequest {
2189			policy: VtxoPolicy::Pubkey(PubkeyVtxoPolicy { user_pubkey: user_keypair.public_key() }),
2190			amount: output_amount,
2191		};
2192
2193		Ok(Some(RoundParticipation {
2194			inputs: vtxos,
2195			outputs: vec![req],
2196			unblinded_mailbox_id: None,
2197		}))
2198	}
2199
2200	/// This will refresh all provided VTXOs in an interactive round and wait until end
2201	///
2202	/// Returns the [RoundStatus] of the round if a successful refresh occurred.
2203	/// It will return [None] if no [Vtxo] needed to be refreshed.
2204	pub async fn refresh_vtxos<V: VtxoRef>(
2205		&self,
2206		vtxos: impl IntoIterator<Item = V>,
2207	) -> anyhow::Result<Option<RoundStatus>> {
2208		let participation = match self.build_refresh_participation(vtxos).await? {
2209			Some(participation) => participation,
2210			None => return Ok(None),
2211		};
2212
2213		Ok(Some(self.participate_round(participation, Some(RoundMovement::Refresh)).await?))
2214	}
2215
2216	/// This will refresh all provided VTXOs in delegated (non-interactive) mode
2217	///
2218	/// Returns the [StoredRoundState] which can be used to track the round's
2219	/// progress later by calling sync. It will return [None] if no [Vtxo]
2220	/// needed to be refreshed.
2221	pub async fn refresh_vtxos_delegated<V: VtxoRef>(
2222		&self,
2223		vtxos: impl IntoIterator<Item = V>,
2224	) -> anyhow::Result<Option<StoredRoundState<Unlocked>>> {
2225		let part = match self.build_refresh_participation(vtxos).await? {
2226			Some(participation) => participation,
2227			None => return Ok(None),
2228		};
2229
2230		Ok(Some(self.join_delegated_round(
2231			part, Some(RoundMovement::Refresh), None,
2232		).await?))
2233	}
2234
2235	/// Same as [Wallet::refresh_vtxos_delegated] but it schedules the refresh for
2236	/// the given block height instead of the next round (see [Wallet::join_delegated_round]).
2237	pub async fn refresh_vtxos_scheduled<V: VtxoRef>(
2238		&self,
2239		vtxos: impl IntoIterator<Item = V>,
2240		scheduled_height: BlockHeight,
2241	) -> anyhow::Result<Option<StoredRoundState<Unlocked>>> {
2242		let part = match self
2243			.build_scheduled_refresh_participation(vtxos, scheduled_height).await?
2244		{
2245			Some(participation) => participation,
2246			None => return Ok(None),
2247		};
2248
2249		Ok(Some(self.join_delegated_round(
2250			part, Some(RoundMovement::Refresh), Some(scheduled_height),
2251		).await?))
2252	}
2253
2254	/// This will find all VTXOs that meets must-refresh criteria. Then, if there are some VTXOs to
2255	/// refresh, it will also add those that meet should-refresh criteria.
2256	pub async fn get_vtxos_to_refresh(&self) -> anyhow::Result<Vec<WalletVtxo>> {
2257		let vtxos = self.spendable_vtxos_with(&RefreshStrategy::should_refresh_if_must(
2258			self,
2259			self.inner.chain.tip().await?,
2260			self.inner.chain.fee_rates().await.fast,
2261		)).await?;
2262		Ok(vtxos)
2263	}
2264
2265	/// Similar to [Wallet::get_vtxos_to_refresh] but it allows VTXOs to be excluded from the
2266	/// result.
2267	pub async fn get_vtxos_to_refresh_with_excluded<V: VtxoRef>(
2268		&self,
2269		exclude: impl IntoIterator<Item = V>,
2270	) -> anyhow::Result<Vec<WalletVtxo>> {
2271		let mut vtxos = self.get_vtxos_to_refresh().await?;
2272		for v in exclude.into_iter() {
2273			if let Some(index) = vtxos.iter().position(|vtxo| vtxo.id() == v.vtxo_id()) {
2274				vtxos.swap_remove(index);
2275			}
2276		}
2277		Ok(vtxos)
2278	}
2279
2280	/// Returns the block height at which the first VTXO will expire
2281	pub async fn get_first_expiring_vtxo_blockheight(
2282		&self,
2283	) -> anyhow::Result<Option<BlockHeight>> {
2284		Ok(self.spendable_vtxos().await?.iter().map(|v| v.expiry_height()).min())
2285	}
2286
2287	/// Returns the next block height at which we have a VTXO that we
2288	/// want to refresh
2289	pub async fn get_next_required_refresh_blockheight(
2290		&self,
2291	) -> anyhow::Result<Option<BlockHeight>> {
2292		let first_expiry = self.get_first_expiring_vtxo_blockheight().await?;
2293		Ok(first_expiry.map(|h| {
2294			h.saturating_sub(self.inner.config.vtxo_refresh_expiry_threshold as BlockHeight)
2295		}))
2296	}
2297
2298	/// Base [InputSelection] for spending VTXOs: skips VTXOs whose exit
2299	/// depth the server would reject as arkoor inputs. Those can only be
2300	/// spent again after a refresh.
2301	async fn spend_input_selection(&self) -> anyhow::Result<InputSelection> {
2302		let mut selection = InputSelection::new();
2303		if let Some(info) = self.ark_info().await? {
2304			selection = selection.max_exit_depth(info.max_vtxo_exit_depth);
2305		}
2306		Ok(selection)
2307	}
2308
2309	/// Select any spendable VTXOs to cover the provided amount.
2310	async fn select_any_vtxos_to_cover(
2311		&self,
2312		amount: Amount,
2313	) -> anyhow::Result<Vec<WalletVtxo>> {
2314		self.spend_input_selection().await?.select(self.spendable_vtxos().await?, amount)
2315	}
2316
2317	/// Determines which VTXOs to use for a fee-paying transaction where the fee is added on top of
2318	/// the desired amount. E.g., a lightning payment, a send-onchain payment.
2319	///
2320	/// See [InputSelection::fee_scheme].
2321	async fn select_any_vtxos_to_cover_with_fee<F>(
2322		&self,
2323		amount: Amount,
2324		calc_fee: F,
2325	) -> anyhow::Result<(Vec<WalletVtxo>, Amount)>
2326	where
2327		F: for<'a> Fn(Amount, SelectedFeeInfos<'a>) -> anyhow::Result<Amount>,
2328	{
2329		let tip = self.inner.chain.tip().await?;
2330		self.spend_input_selection().await?
2331			.fee_scheme(tip, calc_fee)
2332			.select(self.spendable_vtxos().await?, amount)
2333	}
2334
2335	/// Starts a daemon for the wallet.
2336	///
2337	/// The daemon uses the onchain wallet stored in [OpenWalletArgs::onchain] (if any)
2338	/// for background onchain syncing and exit fee-bumping.
2339	///
2340	/// The daemon holds only a weak reference to the wallet, so it doesn't
2341	/// keep the wallet alive: when the last [Wallet] clone is dropped, the
2342	/// daemon shuts itself down. Any single unit of daemon work in flight
2343	/// (a sync pass, a round participation) finishes gracefully first.
2344	pub fn start_daemon(&self) -> anyhow::Result<()> {
2345		let mut daemon = self.inner.daemon.lock();
2346		if daemon.is_some() {
2347			warn!("Called Wallet::start_daemon while daemon was already running.");
2348			return Ok(());
2349		}
2350
2351		let handle = crate::daemon::start_daemon(self);
2352		let _ = daemon.insert(handle);
2353
2354		Ok(())
2355	}
2356
2357	/// Stops the daemon for the wallet if it is running, otherwise does nothing.
2358	pub fn stop_daemon(&self) {
2359		let mut daemon = self.inner.daemon.lock();
2360		if let Some(handle) = daemon.take() {
2361			handle.stop();
2362		}
2363	}
2364
2365	/// Stops the daemon for the wallet if it is running and waits until its
2366	/// tasks have finished.
2367	pub async fn stop_daemon_wait(&self) -> anyhow::Result<()> {
2368		let handle = self.inner.daemon.lock().take();
2369		if let Some(handle) = handle {
2370			handle.stop_wait().await?;
2371		}
2372		Ok(())
2373	}
2374
2375	/// Posts the IDs of all non-spent (spendable, locked and exited) VTXOs
2376	/// to the server's recovery mailbox and re-registers their fully-signed
2377	/// transaction chains, so a wallet recovering from seed can rebuild
2378	/// its state. Both server endpoints are idempotent.
2379	///
2380	/// Exited VTXOs are backed up too: their exit transactions being
2381	/// broadcast doesn't mean the on-chain outputs were claimed, and a
2382	/// wallet recovering from seed must learn about the exit to claim
2383	/// the funds.
2384	///
2385	/// VTXOs whose mailbox post and chain registration both succeeded once
2386	/// are marked [WalletVtxo::registered] and skipped from then on, so
2387	/// repeated syncs don't re-upload — or even re-read — the whole wallet.
2388	///
2389	/// VTXOs of boards that are still in progress are left out: the server
2390	/// only gets a vtxo row for those once the board registers, and that
2391	/// step posts them for recovery itself.
2392	async fn catchup_recovery_vtxos(&self) -> anyhow::Result<()> {
2393		let mut ids = self.inner.db.get_unregistered_vtxo_ids().await?;
2394		if ids.is_empty() {
2395			return Ok(());
2396		}
2397
2398		// TODO(pc): Investigate if we should first load `WalletVtxo` in case we have other reasons
2399		//  to exclude VTXOs other than pending boards. This works for now.
2400		let in_progress_boards = self.boards_in_progress().await?;
2401		ids.retain(|id| !in_progress_boards.iter().any(|b| b.vtxo_id == *id));
2402		if ids.is_empty() {
2403			return Ok(());
2404		}
2405
2406		// The mailbox post and the chain registration are independent and
2407		// both idempotent, so one failing must not stop the other. A vtxo
2408		// is only marked registered once both succeeded for it, so a failed
2409		// mailbox post keeps every vtxo eligible for the next catch-up.
2410		let posted = self.post_recovery_vtxo_ids(ids.iter().copied()).await
2411			.context("failed to post recovery vtxo IDs");
2412		let registered = self.register_recovery_vtxo_chains(&ids, posted.is_ok()).await;
2413
2414		match (posted, registered) {
2415			(Ok(()), registered) => registered,
2416			(posted, Ok(())) => posted,
2417			(Err(posted), Err(registered)) => {
2418				Err(registered.context(format!("mailbox post also failed: {:#}", posted)))
2419			},
2420		}
2421	}
2422
2423	/// Registers the fully-signed transaction chains of the given wallet
2424	/// VTXOs with the server, in chunks with a per-vtxo fallback. When
2425	/// `mark_registered` is set, every successfully registered vtxo is
2426	/// marked [WalletVtxo::registered]. Part of
2427	/// [`Self::catchup_recovery_vtxos`].
2428	async fn register_recovery_vtxo_chains(
2429		&self,
2430		ids: &[VtxoId],
2431		mark_registered: bool,
2432	) -> anyhow::Result<()> {
2433		const CHUNK_SIZE: usize = 20;
2434		let mut failed = 0;
2435		for chunk_ids in ids.chunks(CHUNK_SIZE) {
2436			// Load the full vtxos one chunk at a time: exit chains can be
2437			// tens of KB each, so don't hold every chain in memory at once.
2438			let chunk = self.inner.db.get_full_vtxos(chunk_ids).await
2439				.context("failed to load full vtxos for recovery registration")?;
2440			ensure!(chunk.len() == chunk_ids.len(),
2441				"loaded {} full vtxos for {} ids", chunk.len(), chunk_ids.len(),
2442			);
2443
2444			let mut succeeded = Vec::with_capacity(chunk.len());
2445			match self.register_vtxo_transactions_with_server(&chunk).await {
2446				Ok(()) => succeeded.extend(chunk.iter().map(|v| v.id())),
2447				Err(e) => {
2448					debug!("Failed to register chunk of {} vtxo transactions, \
2449						retrying one by one: {:#}", chunk.len(), e,
2450					);
2451					for vtxo in &chunk {
2452						match self.register_vtxo_transactions_with_server(
2453							std::slice::from_ref(vtxo),
2454						).await {
2455							Ok(()) => succeeded.push(vtxo.id()),
2456							Err(e) => {
2457								error!("Failed to register vtxo {} transactions with server; \
2458									recovery from seed may miss it until registration succeeds: {:#}",
2459									vtxo.id(), e,
2460								);
2461								failed += 1;
2462							},
2463						}
2464					}
2465				},
2466			}
2467			if mark_registered && !succeeded.is_empty() {
2468				self.inner.db.mark_vtxos_registered(&succeeded).await
2469					.context("failed to mark vtxos as registered for recovery")?;
2470			}
2471		}
2472		if failed > 0 {
2473			bail!("failed to register {} of {} vtxo transactions", failed, ids.len());
2474		}
2475		Ok(())
2476	}
2477
2478	/// Registers the signed transaction chains for the given VTXOs with the
2479	/// server. This must be called before spending VTXOs so the server can
2480	/// publish forfeits if needed.
2481	pub async fn register_vtxo_transactions_with_server(
2482		&self,
2483		vtxos: &[impl AsRef<Vtxo<Full>>],
2484	) -> anyhow::Result<()> {
2485		if vtxos.is_empty() {
2486			return Ok(());
2487		}
2488
2489		let (mut srv, _) = self.require_server().await?;
2490		srv.client.register_vtxo_transactions(protos::RegisterVtxoTransactionsRequest {
2491			vtxos: vtxos.iter().map(|v| v.as_ref().serialize()).collect(),
2492		}).await.context("failed to register vtxo transactions")?;
2493
2494		Ok(())
2495	}
2496}
2497
2498fn wrap_server_connect_error(err: ConnectError) -> anyhow::Error {
2499	match err {
2500		ConnectError::CreateEndpoint(CreateEndpointError::NoTransportBackend) => {
2501			anyhow!(MISSING_SERVER_TRANSPORT_HELP)
2502		},
2503		other => anyhow::Error::from(other),
2504	}
2505}
2506
2507impl std::ops::Drop for WalletInner {
2508	fn drop(&mut self) {
2509		// The daemon task holds only a Weak reference to this struct (see
2510		// [crate::daemon]), precisely so that it can't keep the wallet alive
2511		// after the last user-held [Wallet] clone is dropped. That also means
2512		// nobody is left to stop it at that point — do it here. Cancelling
2513		// the token wakes the daemon loops immediately; the task finishes
2514		// detached (Drop can't await it).
2515		if let Some(handle) = self.daemon.lock().take() {
2516			handle.stop();
2517		}
2518	}
2519}
2520
2521#[cfg(test)]
2522mod tests {
2523	use bitcoin::{Amount, FeeRate, Network};
2524	use bitcoin::secp256k1::PublicKey;
2525
2526	use ark::ArkInfo;
2527	use server_rpc::client::CreateEndpointError;
2528
2529	use super::{
2530		check_ark_info_safe, wrap_server_connect_error,
2531		MAX_NB_ROUND_NONCES, MIN_MAINNET_VTXO_EXIT_DELTA, MISSING_SERVER_TRANSPORT_HELP,
2532	};
2533
2534	#[test]
2535	fn no_transport_connect_error_is_reworded_for_wallet_users() {
2536		let err = wrap_server_connect_error(CreateEndpointError::NoTransportBackend.into());
2537		assert!(err.to_string().contains(MISSING_SERVER_TRANSPORT_HELP));
2538		assert!(err.to_string().contains("feature `bark-wallet/native` or `bark-wallet/wasm-web`"));
2539	}
2540
2541	fn ark_info_with_lifetime(vtxo_lifetime: u16, required_board_confirmations: usize) -> ArkInfo {
2542		use std::str::FromStr as _;
2543		let pk = PublicKey::from_str(
2544			"0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"
2545		).unwrap();
2546		#[allow(deprecated)]
2547		ArkInfo {
2548			network: Network::Regtest,
2549			server_pubkey: pk,
2550			mailbox_pubkey: pk,
2551			round_interval: std::time::Duration::from_secs(60),
2552			nb_round_nonces: 8,
2553			vtxo_exit_delta: 48,
2554			vtxo_lifetime,
2555			htlc_send_expiry_delta: 100,
2556			htlc_expiry_delta: 100,
2557			max_vtxo_amount: None,
2558			required_board_confirmations,
2559			max_user_invoice_cltv_delta: 100,
2560			min_board_amount: Amount::from_sat(1000),
2561			vtxo_expiry_delta: vtxo_lifetime,
2562			offboard_feerate: FeeRate::ZERO,
2563			max_offboard_inputs: 1,
2564			ln_receive_anti_dos_required: false,
2565			fees: Default::default(),
2566			max_vtxo_exit_depth: 10,
2567			tos_link: None,
2568		}
2569	}
2570
2571	#[test]
2572	fn ark_info_with_zero_lifetime_is_rejected() {
2573		let mut ai = ark_info_with_lifetime(0, 6);
2574		ai.network = Network::Bitcoin;
2575		ai.vtxo_exit_delta = MIN_MAINNET_VTXO_EXIT_DELTA;
2576		let err = check_ark_info_safe(&ai, 12).unwrap_err().to_string();
2577		assert!(err.contains("unsafe"), "unexpected error: {err}");
2578	}
2579
2580	#[test]
2581	fn ark_info_at_the_boundary_is_rejected() {
2582		// vtxo_lifetime == required + margin: still not strictly greater than
2583		// the minimum, so the strict check refuses it.
2584		let mut ai = ark_info_with_lifetime(6 + 12, 6);
2585		ai.network = Network::Bitcoin;
2586		ai.vtxo_exit_delta = MIN_MAINNET_VTXO_EXIT_DELTA;
2587		assert!(check_ark_info_safe(&ai, 12).is_err());
2588	}
2589
2590	#[test]
2591	fn ark_info_one_block_over_the_boundary_is_accepted() {
2592		let ai = ark_info_with_lifetime(6 + 12 + 1, 6);
2593		check_ark_info_safe(&ai, 12).unwrap();
2594	}
2595
2596	#[test]
2597	fn ark_info_generous_lifetime_is_accepted() {
2598		let ai = ark_info_with_lifetime(4032, 6);
2599		check_ark_info_safe(&ai, 12).unwrap();
2600	}
2601
2602	#[test]
2603	fn ark_info_with_zero_vtxo_exit_delta_is_rejected() {
2604		let mut ai = ark_info_with_lifetime(4032, 6);
2605		ai.vtxo_exit_delta = 0;
2606		let err = check_ark_info_safe(&ai, 12).unwrap_err().to_string();
2607		assert!(err.contains("vtxo_exit_delta"), "unexpected error: {err}");
2608	}
2609
2610	#[test]
2611	fn ark_info_below_mainnet_vtxo_exit_delta_is_rejected() {
2612		let mut ai = ark_info_with_lifetime(4032, 6);
2613		ai.network = Network::Bitcoin;
2614		ai.vtxo_exit_delta = MIN_MAINNET_VTXO_EXIT_DELTA - 1;
2615		let err = check_ark_info_safe(&ai, 12).unwrap_err().to_string();
2616		assert!(err.contains("mainnet minimum"), "unexpected error: {err}");
2617	}
2618
2619	#[test]
2620	fn ark_info_at_mainnet_vtxo_exit_delta_is_accepted() {
2621		let mut ai = ark_info_with_lifetime(4032, 6);
2622		ai.network = Network::Bitcoin;
2623		ai.vtxo_exit_delta = MIN_MAINNET_VTXO_EXIT_DELTA;
2624		check_ark_info_safe(&ai, 12).unwrap();
2625	}
2626
2627	#[test]
2628	fn ark_info_with_zero_nb_round_nonces_is_rejected() {
2629		let mut ai = ark_info_with_lifetime(4032, 6);
2630		ai.nb_round_nonces = 0;
2631		let err = check_ark_info_safe(&ai, 12).unwrap_err().to_string();
2632		assert!(err.contains("nb_round_nonces"), "unexpected error: {err}");
2633	}
2634
2635	#[test]
2636	fn ark_info_at_nb_round_nonces_cap_is_accepted() {
2637		let mut ai = ark_info_with_lifetime(4032, 6);
2638		ai.nb_round_nonces = MAX_NB_ROUND_NONCES;
2639		check_ark_info_safe(&ai, 12).unwrap();
2640	}
2641
2642	#[test]
2643	fn ark_info_over_nb_round_nonces_cap_is_rejected() {
2644		let mut ai = ark_info_with_lifetime(4032, 6);
2645		ai.network = Network::Bitcoin;
2646		ai.vtxo_exit_delta = MIN_MAINNET_VTXO_EXIT_DELTA;
2647		ai.nb_round_nonces = MAX_NB_ROUND_NONCES + 1;
2648		let err = check_ark_info_safe(&ai, 12).unwrap_err().to_string();
2649		assert!(err.contains("nb_round_nonces"), "unexpected error: {err}");
2650	}
2651
2652}