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::vtxo::WalletVtxo;
419
420use std::borrow::Cow;
421use std::collections::HashSet;
422use std::path::PathBuf;
423use std::sync::Arc;
424use std::time::Duration;
425
426use anyhow::{bail, Context};
427use bip39::Mnemonic;
428use bitcoin::{Amount, Network, OutPoint};
429use bitcoin::bip32::{self, ChildNumber, Fingerprint};
430use bitcoin::secp256k1::{self, Keypair, PublicKey};
431use futures::stream::FuturesUnordered;
432use log::{debug, error, info, trace, warn};
433use tokio_stream::StreamExt;
434
435use ark::{ArkInfo, ProtocolEncoding, Vtxo, VtxoId, VtxoPolicy, VtxoRequest};
436use ark::address::VtxoDelivery;
437use ark::fees::{validate_and_subtract_fee_min_dust, VtxoFeeInfo};
438use ark::rounds::{RoundAttempt, RoundEvent};
439use ark::vtxo::{Full, PubkeyVtxoPolicy, VtxoRef, VTXO_DUST};
440use ark::vtxo::policy::signing::VtxoSigner;
441use bitcoin_ext::{BlockHeight, TxStatus};
442use server_rpc::{protos, ServerConnection};
443use server_rpc::client::{ConnectError, CreateEndpointError};
444
445use crate::chain::{ChainSource, ChainSourceSpec};
446use crate::exit::Exit;
447use crate::lock_manager::LockManager;
448use crate::movement::{Movement, MovementId, PaymentMethod};
449use crate::movement::manager::MovementManager;
450use crate::notification::NotificationDispatch;
451use crate::onchain::{OnchainWalletTrait, Utxo};
452use crate::persist::BarkPersister;
453use crate::persist::models::{RoundStateId, StoredRoundState, Unlocked};
454#[cfg(feature = "socks5-proxy")]
455use crate::proxy::proxy_for_url;
456use crate::recovery::RecoveryReport;
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/// Log that the server public key has changed.
489///
490/// Recommends that the user perform an emergency exit to recover their
491/// funds on-chain, since a rotated server pubkey makes the original VTXO
492/// spend/exit conditions unreachable.
493fn log_server_pubkey_changed_error(expected: PublicKey, got: PublicKey) {
494	error!(
495	    "
496Server public key has changed!
497
498The Ark server's public key is different from the one stored when this
499wallet was created. This typically happens when:
500
501	- The server operator has rotated their keys
502	- You are connecting to a different server
503	- The server has been replaced
504
505For safety, this wallet will not connect to the server until you
506resolve this. You can recover your funds on-chain by doing an emergency exit.
507
508This will exit your VTXOs to on-chain Bitcoin without needing the server's cooperation.
509
510Expected: {expected}
511Got:      {got}")
512}
513
514/// Log that the server mailbox pubkey has changed.
515fn log_server_mailbox_pubkey_changed_error(expected: PublicKey, got: PublicKey) {
516	error!(
517	    "
518Server mailbox public key has changed!
519
520The Ark server's mailbox public key is different from the one stored when this
521wallet was created. This typically happens when:
522
523	- The server operator has rotated their keys
524	- You are connecting to a different server
525	- The server has been replaced
526
527For safety, this wallet will not connect to the server until you resolve this.
528
529Unlike a server pubkey change, your VTXOs are not at risk - the mailbox pubkey
530only affects address receive semantics. Any Ark addresses you previously
531shared will stop receiving new payments; you will need to share new addresses
532after reconnecting.
533
534Expected: {expected}
535Got:      {got}")
536}
537
538/// The detailled balance of a Lightning receive.
539#[derive(Debug, Clone)]
540pub struct LightningReceiveBalance {
541	/// Sum of all pending lightning invoices
542	pub total: Amount,
543	/// Sum of all invoices for which we received the HTLC VTXOs
544	pub claimable: Amount,
545}
546
547/// The different balances of a Bark wallet.
548#[derive(Debug, Clone)]
549pub struct Balance {
550	/// Coins that are spendable in the Ark, either in-round or out-of-round.
551	pub spendable: Amount,
552	/// Coins that are in the process of being sent over Lightning.
553	pub pending_lightning_send: Amount,
554	/// Coins that are in the process of being received over Lightning.
555	pub claimable_lightning_receive: Amount,
556	/// Coins locked in a round.
557	pub pending_in_round: Amount,
558	/// Coins held in VTXOs whose unilateral exit chain has confirmed onchain but which
559	/// haven't yet been drained back to the onchain wallet. While in this state the
560	/// VTXOs are [`vtxo::VtxoStateKind::Exited`] and unusable in the Ark protocol; the
561	/// drain transaction moves them to spendable onchain output.
562	/// None if exit subsystem was unavailable
563	pub pending_exit: Option<Amount>,
564	/// Coins that are pending sufficient confirmations from board transactions.
565	pub pending_board: Amount,
566}
567
568pub struct UtxoInfo {
569	pub outpoint: OutPoint,
570	pub amount: Amount,
571	pub confirmation_height: Option<u32>,
572}
573
574impl From<Utxo> for UtxoInfo {
575	fn from(value: Utxo) -> Self {
576		match value {
577			Utxo::Local(o) => UtxoInfo {
578				outpoint: o.outpoint,
579				amount: o.amount,
580				confirmation_height: o.confirmation_height,
581			},
582			Utxo::Exit(e) => UtxoInfo {
583				outpoint: e.vtxo.point(),
584				amount: e.vtxo.amount(),
585				confirmation_height: Some(e.height),
586			},
587		}
588	}
589}
590
591/// Represents an offchain balance structure consisting of available funds, pending amounts in
592/// unconfirmed rounds, and pending exits.
593pub struct OffchainBalance {
594	/// Funds currently available for use. This reflects the spendable balance.
595	pub available: Amount,
596	/// Funds that are pending in unconfirmed operational rounds.
597	pub pending_in_round: Amount,
598	/// Funds being unilaterally exited. These may require more onchain confirmations to become
599	/// available onchain.
600	pub pending_exit: Amount,
601}
602
603/// Read-only properties of the Bark wallet.
604#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
605pub struct WalletProperties {
606	/// The Bitcoin network to run Bark on.
607	///
608	/// Default value: signet.
609	pub network: Network,
610
611	/// The wallet fingerpint
612	///
613	/// Used on wallet loading to check mnemonic correctness
614	pub fingerprint: Fingerprint,
615
616	/// The server public key from the initial connection.
617	///
618	/// This is used to detect if the Ark server has been replaced,
619	/// which could indicate a malicious server. If the server pubkey
620	/// changes, the wallet will refuse to connect and warn the user
621	/// to perform an emergency exit.
622	pub server_pubkey: Option<PublicKey>,
623
624	/// The server's mailbox public key.
625	///
626	/// Stored so that Ark addresses can be generated without a live
627	/// connection to the Ark server. `None` indicates a wallet created
628	/// before this field was added; the value is populated on the first
629	/// successful handshake. If the key changes, the wallet refuses to
630	/// connect until the user resolves the rotation.
631	pub server_mailbox_pubkey: Option<PublicKey>,
632}
633
634/// Struct representing an extended private key derived from a
635/// wallet's seed, used to derive child VTXO keypairs
636///
637/// The VTXO seed is derived by applying a hardened derivation
638/// step at index 350 from the wallet's seed.
639pub struct WalletSeed {
640	master: bip32::Xpriv,
641	vtxo: bip32::Xpriv,
642}
643
644impl WalletSeed {
645	/// Create a new [WalletSeed] from a given BIP-32 master seed
646	pub fn new_from_seed(network: Network, seed: &[u8; 64]) -> Self {
647		let bark_path = [ChildNumber::from_hardened_idx(BARK_PURPOSE_INDEX).unwrap()];
648		let master = bip32::Xpriv::new_master(network, seed)
649			.expect("invalid seed")
650			.derive_priv(&SECP, &bark_path)
651			.expect("purpose is valid");
652
653		let vtxo_path = [ChildNumber::from_hardened_idx(VTXO_KEYS_INDEX).unwrap()];
654		let vtxo = master.derive_priv(&SECP, &vtxo_path)
655			.expect("vtxo path is valid");
656
657		Self { master, vtxo }
658	}
659
660	/// Create a new [WalletSeed] from a given BIP-39 [Mnemonic]
661	pub fn new_from_mnemonic(network: Network, mnemonic: &Mnemonic) -> Self {
662		Self::new_from_seed(network, &mnemonic.to_seed(""))
663	}
664
665	pub fn fingerprint(&self) -> Fingerprint {
666		self.master.fingerprint(&SECP)
667	}
668
669	fn derive_vtxo_keypair(&self, idx: u32) -> Keypair {
670		self.vtxo.derive_priv(&SECP, &[idx.into()]).unwrap().to_keypair(&SECP)
671	}
672
673	fn to_mailbox_keypair(&self) -> Keypair {
674		let mailbox_path = [ChildNumber::from_hardened_idx(MAILBOX_KEY_INDEX).unwrap()];
675		self.master.derive_priv(&SECP, &mailbox_path).unwrap().to_keypair(&SECP)
676	}
677
678	fn to_recovery_mailbox_keypair(&self) -> Keypair {
679		let mailbox_path = [ChildNumber::from_hardened_idx(RECOVERY_MAILBOX_KEY_INDEX).unwrap()];
680		self.master.derive_priv(&SECP, &mailbox_path).unwrap().to_keypair(&SECP)
681	}
682}
683
684/// Additional arguments for the [Wallet::open] function
685pub struct OpenWalletArgs {
686	/// Whether to run the background daemon
687	///
688	/// When disabled, you must manually call `Wallet::sync` to sync the wallet.
689	///
690	/// Default: true
691	pub run_daemon: bool,
692
693	/// The data directory to use for this wallet
694	///
695	/// This field can be used under most platforms as an alternative to
696	/// providing the `persister` and `lock_manager` fields.
697	///
698	/// This field is ignored if `persister` and `lock_manager` are provided
699	/// or for the wasm32 platform.
700	///
701	/// Default: none
702	pub datadir: Option<PathBuf>,
703
704	/// The persister to use for this wallet
705	///
706	/// Default: returned by [`crate::persist::platform_default`]
707	pub persister: Option<Arc<dyn BarkPersister>>,
708
709	/// The lock manager to use for this wallet
710	///
711	/// Default: returned by [`crate::lock_manager::platform_default`]
712	///
713	/// On some platforms (linux, macos, windows) the default lock manager
714	/// requires a datadir be provided.
715	pub lock_manager: Option<Box<dyn LockManager>>,
716
717	/// The onchain wallet to use, if any
718	///
719	/// Default: none
720	pub onchain: Option<Arc<tokio::sync::RwLock<dyn OnchainWalletTrait>>>,
721
722	/// Whether to create a new wallet if no wallet exists
723	///
724	///  Default: true
725	pub create_if_not_exists: bool,
726
727	/// Whether to create a new wallet even if the Ark server cannot be reached
728	///
729	/// Default: false
730	pub create_without_server: bool,
731
732	/// Whether to skip recovering VTXOs from the recovery mailbox.
733	///
734	/// When false (the default), recovery runs on wallet open.
735	///
736	/// Default: false
737	pub skip_recovery: bool,
738
739	/// A callback function to be called when the recovery is finished
740	///
741	/// Default: none
742	pub on_recovery_finished: Option<Box<dyn FnOnce(RecoveryReport) + Send + Sync>>,
743}
744
745impl Default for OpenWalletArgs {
746	fn default() -> Self {
747	    Self {
748			run_daemon: true,
749			onchain: None,
750			datadir: None,
751			persister: None,
752			lock_manager: None,
753			create_if_not_exists: true,
754			create_without_server: false,
755			skip_recovery: false,
756			on_recovery_finished: None,
757		}
758	}
759}
760
761struct WalletInner {
762	/// The chain source the wallet is connected to
763	chain: Arc<ChainSource>,
764
765	/// Exit subsystem handling unilateral exits and on-chain reconciliation outside Ark rounds.
766	exit: Exit,
767
768	/// Allows easy creation of and management of wallet fund movements.
769	movements: Arc<MovementManager>,
770
771	/// Dispatch for wallet notifications
772	notifications: NotificationDispatch,
773
774	/// Active runtime configuration for networking, fees, policies and thresholds.
775	config: Config,
776
777	/// Persistence backend for wallet state (keys metadata, VTXOs, movements, round state, etc.).
778	db: Arc<dyn BarkPersister>,
779
780	/// Coordinates access to the wallet's protected resources. The caller
781	/// picks a backend whose enforcement scope matches how the wallet is
782	/// deployed; see [`crate::lock_manager`].
783	lock_manager: Box<dyn LockManager>,
784
785	/// Deterministic seed material used to generate wallet keypairs.
786	seed: WalletSeed,
787
788	/// Live connection to an Ark server for round participation and synchronization.
789	///
790	/// Lazily initialised on first use via [`Wallet::require_server`]. A
791	/// [`OnceCell`] is the right primitive here: concurrent callers on a
792	/// cold cell all await the same in-flight `connect_to_server` future
793	/// instead of each opening a fresh gRPC channel.
794	server: tokio::sync::OnceCell<ServerConnection>,
795
796	/// Onchain wallet used for boarding, exit fee-bumping, and onchain syncing.
797	///
798	/// When present, the wallet can perform onchain operations without the
799	/// caller having to supply a wallet on every call.
800	onchain: Option<Arc<tokio::sync::RwLock<dyn OnchainWalletTrait>>>,
801
802	/// A handle to the currently running daemon, if any.
803	daemon: parking_lot::Mutex<Option<DaemonHandle>>,
804
805	/// The last chain tip at which we scanned spendable VTXOs for on-chain (force) exits.
806	/// The scan is skipped while the tip is unchanged, since a VTXO's on-chain status can
807	/// only change across blocks.
808	last_force_exit_scan_tip: tokio::sync::Mutex<Option<BlockHeight>>,
809
810	/// In-memory MuSig2 secret cosign nonces for in-flight round attempts.
811	/// See [`RoundSecretNonces`].
812	pub(crate) round_secret_nonces: RoundSecretNonces,
813}
814
815/// The central entry point for using this library as an Ark wallet.
816///
817/// Note that a [Wallet] instance can freely be [Clone]'ed to refer to the same
818/// wallet.
819///
820/// Overview
821/// - Wallet encapsulates the complete Ark client implementation:
822///   - address generation (Ark addresses/keys)
823///     - [Wallet::new_address],
824///     - [Wallet::new_address_with_index],
825///     - [Wallet::peek_address],
826///     - [Wallet::validate_arkoor_address]
827///   - boarding onchain funds into Ark from an onchain wallet (see [onchain::OnchainWallet])
828///     - [Wallet::board_amount],
829///     - [Wallet::board_all]
830///   - offboarding Ark funds to move them back onchain
831///     - [Wallet::offboard_vtxos],
832///     - [Wallet::offboard_all]
833///   - sending and receiving Ark payments (including to BOLT11/BOLT12 invoices)
834///     - [Wallet::send_arkoor_payment],
835///     - [Wallet::pay_lightning_invoice],
836///     - [Wallet::pay_lightning_address],
837///     - [Wallet::pay_lightning_offer]
838///   - tracking, selecting, and refreshing VTXOs
839///     - [Wallet::vtxos],
840///     - [Wallet::vtxos_with],
841///     - [Wallet::refresh_vtxos]
842///   - syncing with the Ark server, unilateral exits and performing general maintenance
843///     - [Wallet::maintenance]: Syncs everything offchain-related and refreshes VTXOs where
844///       necessary,
845///     - [Wallet::maintenance_with_onchain]: The same as [Wallet::maintenance] but also syncs the
846///       onchain wallet and unilateral exits,
847///     - [Wallet::maintenance_refresh]: Refreshes VTXOs where necessary without syncing anything,
848///     - [Wallet::sync]: Syncs network fee-rates, ark rounds and arkoor payments,
849///     - [Wallet::sync_exits]: Updates the status of unilateral exits,
850///     - [Wallet::sync_pending_lightning_send_vtxos]: Updates the status of pending lightning payments,
851///     - [Wallet::try_claim_all_lightning_receives]: Wait for payment receipt of all open invoices, then claim them,
852///     - [Wallet::sync_pending_boards]: Registers boards which are available for use
853///       in offchain payments
854///
855/// Key capabilities
856/// - Address management:
857///   - derive and peek deterministic Ark addresses and their indices
858/// - Funds lifecycle:
859///   - board funds from an external onchain wallet onto the Ark
860///   - send out-of-round Ark payments (arkoor)
861///   - offboard funds to onchain addresses
862///   - manage HTLCs and Lightning receives/sends
863/// - VTXO management:
864///   - query spendable and pending VTXOs
865///   - refresh expiring or risky VTXOs
866///   - compute balance broken down by spendable/pending states
867/// - Synchronization and maintenance:
868///   - sync against the Ark server and the onchain source
869///   - reconcile pending rounds, exits, and offchain state
870///   - periodic maintenance helpers (e.g., auto-register boards, refresh policies)
871///
872/// Construction and persistence
873///
874/// A [Wallet] is opened or created using a mnemonic and a backend implementing [BarkPersister].
875/// The [Wallet::open] function allows for opening and creating a wallet if it doesn't exist yet.
876/// Check out the documentation on [OpenWalletArgs] for all optional arguments.
877///
878/// Example
879/// ```no_run
880/// use std::path::PathBuf;
881/// use std::sync::Arc;
882/// use tokio::fs;
883/// use bark::{Config, onchain, Wallet, OpenWalletArgs, WalletSeed};
884/// use bark::lock_manager::memory::MemoryLockManager;
885/// use bark::persist::sqlite::SqliteClient;
886///
887/// const MNEMONIC_FILE : &str = "mnemonic";
888///
889/// #[tokio::main]
890/// async fn main() {
891/// 	// Pick the bitcoin network that will be used
892/// 	let network = bitcoin::Network::Signet;
893///
894/// 	// Configure the wallet
895/// 	let config = Config {
896/// 		server_address: String::from("https://ark.signet.2nd.dev"),
897/// 		esplora_address: Some(String::from("https://esplora.signet.2nd.dev")),
898/// 		..Config::network_default(network)
899/// 	};
900///
901/// 	// Create a sqlite database
902/// 	let datadir = PathBuf::from("./bark");
903///
904/// 	// Generate and seed and store it somewhere
905/// 	let mnemonic = bip39::Mnemonic::generate(12).expect("12 is valid");
906/// 	fs::write(datadir.join(MNEMONIC_FILE), mnemonic.to_string().as_bytes()).await.unwrap();
907/// 	let seed = WalletSeed::new_from_mnemonic(network, &mnemonic);
908///
909/// 	let wallet = Wallet::open(network, seed, config, OpenWalletArgs {
910/// 		datadir: Some(datadir),
911/// 		..Default::default()
912/// 	}).await.unwrap();
913/// }
914/// ```
915#[derive(Clone)]
916pub struct Wallet {
917	inner: Arc<WalletInner>,
918}
919
920impl Wallet {
921	pub async fn network(&self) -> anyhow::Result<Network> {
922		Ok(self.properties().await?.network)
923	}
924
925	/// Access the server's chain source
926	pub fn chain(&self) -> &Arc<ChainSource> {
927		&self.inner.chain
928	}
929
930	/// Access the exit manager
931	pub fn exit_mgr(&self) -> &Exit {
932		&self.inner.exit
933	}
934
935	/// Access the movements manager
936	pub fn movements_mgr(&self) -> &MovementManager {
937		&self.inner.movements
938	}
939
940	/// Peek at the keypair directly after currently last revealed one,
941	/// together with its index, without storing it.
942	pub async fn peek_next_keypair(&self) -> anyhow::Result<(Keypair, u32)> {
943		let last_revealed = self.inner.db.get_last_vtxo_key_index().await?;
944
945		let index = last_revealed.map(|i| i + 1).unwrap_or(u32::MIN);
946		let keypair = self.inner.seed.derive_vtxo_keypair(index);
947
948		Ok((keypair, index))
949	}
950
951	/// Derive and store the keypair directly after currently last revealed one,
952	/// together with its index.
953	pub async fn derive_store_next_keypair(&self) -> anyhow::Result<(Keypair, u32)> {
954		let (keypair, index) = self.peek_next_keypair().await?;
955		self.inner.db.store_vtxo_key(index, keypair.public_key()).await?;
956		Ok((keypair, index))
957	}
958
959	#[deprecated(note = "use peek_keypair instead")]
960	pub async fn peak_keypair(&self, index: u32) -> anyhow::Result<Keypair> {
961		self.peek_keypair(index).await
962	}
963
964	/// Retrieves a keypair based on the provided index and checks if the corresponding public key
965	/// exists in the [Vtxo] database.
966	///
967	/// # Arguments
968	///
969	/// * `index` - The index used to derive a keypair.
970	///
971	/// # Returns
972	///
973	/// * `Ok(Keypair)` - If the keypair is successfully derived and its public key exists in the
974	///   database.
975	/// * `Err(anyhow::Error)` - If the public key does not exist in the database or if an error
976	///   occurs during the database query.
977	pub async fn peek_keypair(&self, index: u32) -> anyhow::Result<Keypair> {
978		let keypair = self.inner.seed.derive_vtxo_keypair(index);
979		if self.inner.db.get_public_key_idx(&keypair.public_key()).await?.is_some() {
980			Ok(keypair)
981		} else {
982			bail!("VTXO key {} does not exist, please derive it first", index)
983		}
984	}
985
986
987	/// Retrieves the [Keypair] for a provided [PublicKey]
988	///
989	/// # Arguments
990	///
991	/// * `public_key` - The public key for which the keypair must be found
992	///
993	/// # Returns
994	/// * `Ok(Some(u32, Keypair))` - If the pubkey is found, the derivation-index and keypair are
995	///                              returned
996	/// * `Ok(None)` - If the pubkey cannot be found in the database
997	/// * `Err(anyhow::Error)` - If an error occurred related to the database query
998	pub async fn pubkey_keypair(&self, public_key: &PublicKey) -> anyhow::Result<Option<(u32, Keypair)>> {
999		if let Some(index) = self.inner.db.get_public_key_idx(&public_key).await? {
1000			Ok(Some((index, self.inner.seed.derive_vtxo_keypair(index))))
1001		} else {
1002			Ok(None)
1003		}
1004	}
1005
1006	/// Retrieves the [Keypair] for a provided [Vtxo]
1007	///
1008	/// # Arguments
1009	///
1010	/// * `vtxo` - The vtxo for which the key must be found
1011	///
1012	/// # Returns
1013	/// * `Ok(Some(Keypair))` - If the pubkey is found, the keypair is returned
1014	/// * `Err(anyhow::Error)` - If the corresponding public key doesn't exist
1015	///   in the database or a database error occurred.
1016	pub async fn get_vtxo_key(&self, vtxo: impl VtxoRef) -> anyhow::Result<Keypair> {
1017		let bare_vtxo = match vtxo.as_bare_vtxo() {
1018			Some(bare) => bare,
1019			None => Cow::Owned(self.get_vtxo_by_id(vtxo.vtxo_id()).await?.vtxo),
1020		};
1021		let pubkey = self.find_signable_clause(&bare_vtxo).await
1022			.context("VTXO is not signable by wallet")?
1023			.pubkey();
1024		let idx = self.inner.db.get_public_key_idx(&pubkey).await?
1025			.context("VTXO key not found")?;
1026		Ok(self.inner.seed.derive_vtxo_keypair(idx))
1027	}
1028
1029	#[deprecated(note = "use peek_address instead")]
1030	pub async fn peak_address(&self, index: u32) -> anyhow::Result<ark::Address> {
1031		self.peek_address(index).await
1032	}
1033
1034	/// Peek for an [ark::Address] at the given key index.
1035	///
1036	/// May return an error if the address at the given index has not been derived yet.
1037	pub async fn peek_address(&self, index: u32) -> anyhow::Result<ark::Address> {
1038		let properties = self.properties().await?;
1039		let network = properties.network;
1040		let keypair = self.peek_keypair(index).await?;
1041		let mailbox = self.mailbox_identifier();
1042
1043
1044		let (server_pubkey, mailbox_pubkey) =
1045			if let (Some(spk), Some(mpk)) = (properties.server_pubkey, properties.server_mailbox_pubkey) {
1046				(spk, mpk)
1047			} else {
1048				let (_, ark_info) = self.require_server().await?;
1049				(ark_info.server_pubkey, ark_info.mailbox_pubkey)
1050			};
1051
1052		Ok(ark::Address::builder()
1053			.testnet(network != bitcoin::Network::Bitcoin)
1054			.server_pubkey(server_pubkey)
1055			.pubkey_policy(keypair.public_key())
1056			.mailbox(mailbox_pubkey, mailbox, &keypair)
1057			.context("failed to assign mailbox")?
1058			.into_address()
1059			.context("failed to build address")?)
1060	}
1061
1062	/// Generate a new [ark::Address] and returns the index of the key used to create it.
1063	///
1064	/// This derives and stores the keypair directly after currently last revealed one.
1065	pub async fn new_address_with_index(&self) -> anyhow::Result<(ark::Address, u32)> {
1066		let (_, index) = self.derive_store_next_keypair().await?;
1067		let addr = self.peek_address(index).await?;
1068		Ok((addr, index))
1069	}
1070
1071	/// Generate a new mailbox [ark::Address].
1072	pub async fn new_address(&self) -> anyhow::Result<ark::Address> {
1073		let (addr, _) = self.new_address_with_index().await?;
1074		Ok(addr)
1075	}
1076
1077	/// Sign an arbitrary message with the key of the given [ark::Address].
1078	///
1079	/// Returns `Ok(None)` if the address is not one of this wallet's own
1080	/// addresses or its key has not been derived yet.
1081	///
1082	/// The resulting signature can be verified with
1083	/// [ark::message::verify_for_address] against the address.
1084	pub async fn sign_message(
1085		&self,
1086		message: &[u8],
1087		address: &ark::Address,
1088	) -> anyhow::Result<Option<secp256k1::schnorr::Signature>> {
1089		let pubkey = address.policy().user_pubkey();
1090		let Some((_, keypair)) = self.pubkey_keypair(&pubkey).await? else {
1091			return Ok(None);
1092		};
1093		Ok(Some(ark::message::sign(&keypair, message)))
1094	}
1095
1096	/// Create a new wallet
1097	///
1098	/// This function simply initiates a new wallet; use [Wallet::open] to open
1099	/// it afterwards. You can also call [Wallet::open] with `create_if_not_exists`
1100	/// set to true to avoid having to call this function.
1101	///
1102	/// `lock_manager` coordinates access to the wallet's protected resources. Pick a backend
1103	/// whose enforcement scope matches how the wallet is deployed — see [`crate::lock_manager`].
1104	pub async fn create(
1105		network: Network,
1106		seed: &WalletSeed,
1107		config: &Config,
1108		db: &dyn BarkPersister,
1109		lock_manager: &dyn LockManager,
1110		allow_unreachable_server: bool,
1111	) -> anyhow::Result<()> {
1112		trace!("Config: {:?}", config);
1113
1114		let wallet_fingerprint = seed.fingerprint();
1115
1116		// Block concurrent creators against the same locking universe. A
1117		// short timeout is fine: if a sibling process wins the race they
1118		// will have committed the wallet by the time we'd time out, and
1119		// the `read_properties` check below catches that case cleanly.
1120		let create_guard = lock_manager.lock(
1121			&format!("{}.create", wallet_fingerprint),
1122			Duration::from_secs(5),
1123		).await.context("wallet initialization already in progress")?;
1124
1125		if let Some(existing) = db.read_properties().await? {
1126			trace!("Existing config: {:?}", existing);
1127			bail!("cannot overwrite already existing config")
1128		}
1129
1130		// Try to connect to the server and get its pubkey
1131		let (server_pubkey, mailbox_pubkey) = match Self::connect_to_server(&config, network).await {
1132			Ok(conn) => {
1133				let ark_info = conn.ark_info().await;
1134				(Some(ark_info.server_pubkey), Some(ark_info.mailbox_pubkey))
1135			},
1136			Err(_) if allow_unreachable_server => (None, None),
1137			Err(err) => {
1138				bail!("Failed to connect to provided server: {:#}", err);
1139			},
1140		};
1141
1142		let properties = WalletProperties {
1143			network,
1144			fingerprint: wallet_fingerprint,
1145			server_pubkey,
1146			server_mailbox_pubkey: mailbox_pubkey,
1147		};
1148
1149		// write the config to db
1150		db.init_wallet(&properties).await.context("cannot init wallet in the database")?;
1151		info!("Created wallet with fingerprint: {}", wallet_fingerprint);
1152		if let Some(pk) = server_pubkey {
1153			info!("Stored server pubkey: {}", pk);
1154		}
1155
1156		// The wallet exists from this point on — drop the creation lock
1157		// so another process is free to open it.
1158		drop(create_guard);
1159
1160		Ok(())
1161	}
1162
1163	/// Open an existing wallet or create one if `options.create_if_not_exists` is true
1164	pub async fn open(
1165		network: Network,
1166		seed: WalletSeed,
1167		config: Config,
1168		args: OpenWalletArgs,
1169	) -> anyhow::Result<Wallet> {
1170		if !(1..=3).contains(&config.change_vtxo_split_factor) {
1171			bail!("change_vtxo_split_factor must be 1, 2 or 3, got {}",
1172				config.change_vtxo_split_factor,
1173			);
1174		}
1175
1176		let fingerprint = seed.fingerprint();
1177		let lock_manager = if let Some(lm) = args.lock_manager {
1178			lm
1179		} else {
1180			crate::lock_manager::platform_default(args.datadir.as_ref(), Some(fingerprint))
1181				.context("failed to instantiate platform default lock manager")?
1182		};
1183
1184		let db = if let Some(db) = args.persister {
1185			db
1186		} else {
1187			if let Some(ref datadir) = args.datadir {
1188				#[cfg(not(target_arch = "wasm32"))]
1189				if !datadir.exists() && args.create_if_not_exists {
1190					tokio::fs::create_dir_all(datadir).await.with_context(|| format!(
1191						"failed to create datadir at {}", datadir.display(),
1192					))?;
1193				}
1194			}
1195			crate::persist::platform_default(args.datadir.as_ref(), Some(fingerprint)).await
1196				.context("failed to instantiate platform default persister")?
1197		};
1198
1199		let mut created_now = false;
1200		let properties = if let Some(p) = db.read_properties().await? {
1201			p
1202		} else if args.create_if_not_exists {
1203			Self::create(
1204				network, &seed, &config, &*db, &*lock_manager, args.create_without_server,
1205			).await.context("error creating new wallet")?;
1206			created_now = true;
1207			db.read_properties().await?
1208				.context("create failed: no wallet properties after Wallet::create was called")?
1209		} else {
1210			bail!("wallet does not exist; use Wallet::create or \
1211				set options.create_if_not_exists to true");
1212		};
1213
1214		if properties.fingerprint != fingerprint {
1215			bail!("incorrect mnemonic")
1216		}
1217
1218		let chain_source = if let Some(ref url) = config.esplora_address {
1219			ChainSourceSpec::Esplora {
1220				url: url.clone(),
1221			}
1222		} else if let Some(ref url) = config.bitcoind_address {
1223			let auth = if let Some(ref c) = config.bitcoind_cookiefile {
1224				bitcoin_ext::rpc::Auth::CookieFile(c.clone())
1225			} else {
1226				bitcoin_ext::rpc::Auth::UserPass(
1227					config.bitcoind_user.clone().context("need bitcoind auth config")?,
1228					config.bitcoind_pass.as_ref().context("need bitcoind auth config")?
1229						.leak_ref().clone(),
1230				)
1231			};
1232			ChainSourceSpec::Bitcoind {
1233				url: url.clone(),
1234				auth,
1235				zmq: config.bitcoind_zmq_address.clone(),
1236			}
1237		} else {
1238			bail!("Need to either provide esplora or bitcoind info");
1239		};
1240
1241		#[cfg(feature = "socks5-proxy")]
1242		let chain_proxy = proxy_for_url(&config.socks5_proxy, chain_source.url())?;
1243		let chain_source_client = ChainSource::new(
1244			chain_source, properties.network, config.fallback_fee_rate,
1245			#[cfg(feature = "socks5-proxy")] chain_proxy.as_deref(),
1246		).await?;
1247		let chain = Arc::new(chain_source_client);
1248		chain.require_version().await
1249			.context("provided chain source doesn't meet version requirement")?;
1250
1251		let server = tokio::sync::OnceCell::new();
1252
1253		let notifications = NotificationDispatch::new();
1254		let movements = Arc::new(MovementManager::new(db.clone(), notifications.clone()));
1255		let exit = Exit::new(db.clone(), chain.clone(), movements.clone()).await?;
1256
1257		let onchain = args.onchain;
1258		let ret = Wallet { inner: Arc::new(WalletInner {
1259			config, db, lock_manager, seed, exit, movements, notifications, server, chain,
1260			onchain,
1261			daemon: parking_lot::Mutex::new(None),
1262			last_force_exit_scan_tip: tokio::sync::Mutex::new(None),
1263			round_secret_nonces: RoundSecretNonces::new(),
1264		})};
1265
1266		ret.inner.exit.load().await
1267			.context("error loading exit system after opening wallet")?;
1268
1269		if created_now {
1270			if !args.skip_recovery {
1271				// Recover any VTXOs backed up to the seed-derived recovery mailbox.
1272				// Best-effort so it can't abort wallet creation, but a failure means
1273				// funds may be missing — surface it loudly. Partial failures are
1274				// logged inside the recovery call.
1275				match ret.recover_from_mailbox().await {
1276					Ok(report) => {
1277						if let Some(callback) = args.on_recovery_finished {
1278							callback(report);
1279						}
1280					},
1281					Err(e) => {
1282						error!("VTXO recovery from the recovery mailbox failed; funds may be \
1283							missing from this wallet until recovery succeeds: {:#}", e);
1284					},
1285				}
1286			} else {
1287				info!("Seed-based wallet recovery explicitly skipped");
1288			}
1289		}
1290
1291		if args.run_daemon {
1292			ret.start_daemon()
1293				.context("failed to start daemon after opening wallet")?;
1294		}
1295
1296		Ok(ret)
1297	}
1298
1299	/// Returns the config used to create/load the bark [Wallet].
1300	pub fn config(&self) -> &Config {
1301		&self.inner.config
1302	}
1303
1304	/// Retrieves the [WalletProperties] of the current bark [Wallet].
1305	pub async fn properties(&self) -> anyhow::Result<WalletProperties> {
1306		let properties = self.inner.db.read_properties().await?.context("Wallet is not initialised")?;
1307		Ok(properties)
1308	}
1309
1310	/// Returns the fingerprint of the wallet.
1311	pub fn fingerprint(&self) -> Fingerprint {
1312		self.inner.seed.fingerprint()
1313	}
1314
1315	async fn connect_to_server(
1316		config: &Config,
1317		network: Network,
1318	) -> anyhow::Result<ServerConnection> {
1319		let server_address = crate::utils::url_with_default_https_scheme(&config.server_address);
1320		let mut builder = ServerConnection::builder()
1321			.address(&server_address)
1322			.network(network);
1323
1324		#[cfg(feature = "socks5-proxy")]
1325		if let Some(proxy) = proxy_for_url(&config.socks5_proxy, &server_address)? {
1326			builder = builder.proxy(&proxy)
1327		}
1328
1329		#[allow(deprecated)]
1330		{
1331			if let Some(ref token) = config.server_access_token {
1332				builder = builder.access_token(token);
1333			}
1334		}
1335
1336		if let Some(ref ua) = config.user_agent {
1337			builder = builder.user_agent(ua);
1338		}
1339
1340		builder.connect().await.map_err(wrap_server_connect_error)
1341			.context("Failed to connect to Ark server")
1342	}
1343
1344	async fn require_server(&self) -> anyhow::Result<(ServerConnection, ArkInfo)> {
1345		// Connect lazily if not yet connected. `get_or_try_init` ensures
1346		// concurrent callers on a cold cell all await the same in-flight
1347		// connect future instead of each opening a fresh gRPC channel.
1348		let conn = self.inner.server.get_or_try_init(|| async {
1349			let network = self.properties().await?.network;
1350			Self::connect_to_server(&self.inner.config, network).await
1351				.context("You should be connected to Ark server to perform this action")
1352		}).await?.clone();
1353
1354		let ark_info = conn.ark_info().await;
1355		self.check_and_store_server_keys(&ark_info).await?;
1356
1357		Ok((conn, ark_info))
1358	}
1359
1360	pub async fn refresh_server(&self) -> anyhow::Result<()> {
1361		// If the cell is still cold, initialise it with a fresh connection.
1362		// If it is already initialised, run a heartbeat against the existing
1363		// one — `OnceCell` does not support replacing a stored value, but
1364		// `ServerConnection` is built around a tonic `Channel` which
1365		// transparently reconnects, so we don't need to swap it.
1366		let srv = self.inner.server.get_or_try_init(|| async {
1367			let properties = self.properties().await?;
1368			Self::connect_to_server(&self.inner.config, properties.network).await
1369				.map_err(anyhow::Error::from)
1370		}).await?;
1371
1372		srv.check_connection().await?;
1373		let ark_info = srv.ark_info().await;
1374		ark_info.fees.validate().context("invalid fee schedule")?;
1375		self.check_and_store_server_keys(&ark_info).await?;
1376
1377		Ok(())
1378	}
1379
1380	/// Returns the configured onchain wallet, if any.
1381	pub fn onchain(&self) -> Option<Arc<tokio::sync::RwLock<dyn OnchainWalletTrait>>> {
1382		self.inner.onchain.clone()
1383	}
1384
1385	/// Sync the internal onchain wallet against the chain source, if one is configured.
1386	pub async fn sync_onchain(&self) -> anyhow::Result<()> {
1387		if let Some(onchain) = self.inner.onchain.as_ref() {
1388			onchain.write().await.sync(self.chain()).await?;
1389		}
1390		Ok(())
1391	}
1392
1393	/// Validate that the server's public keys match what we have stored,
1394	/// and persist them if this is the first time connecting after an upgrade.
1395	///
1396	/// Returns an error (via `bail!`) if either the server pubkey or mailbox
1397	/// pubkey differs from the stored value; callers must not proceed with
1398	/// server operations on error.
1399	async fn check_and_store_server_keys(&self, ark_info: &ArkInfo) -> anyhow::Result<()> {
1400		let properties = self.properties().await?;
1401
1402		if let Some(stored_pubkey) = properties.server_pubkey {
1403			if stored_pubkey != ark_info.server_pubkey {
1404				log_server_pubkey_changed_error(stored_pubkey, ark_info.server_pubkey);
1405				bail!("Server public key has changed. You should exit all your VTXOs!");
1406			}
1407		} else {
1408			self.inner.db.set_server_pubkey(ark_info.server_pubkey).await?;
1409			info!("Stored server pubkey for existing wallet: {}", ark_info.server_pubkey);
1410		}
1411
1412		if let Some(stored_mailbox_pubkey) = properties.server_mailbox_pubkey {
1413			if stored_mailbox_pubkey != ark_info.mailbox_pubkey {
1414				log_server_mailbox_pubkey_changed_error(stored_mailbox_pubkey, ark_info.mailbox_pubkey);
1415				bail!("Server mailbox public key has changed.");
1416			}
1417		} else {
1418			self.inner.db.set_server_mailbox_pubkey(ark_info.mailbox_pubkey).await?;
1419			info!("Stored server mailbox pubkey for existing wallet: {}", ark_info.mailbox_pubkey);
1420		}
1421
1422		Ok(())
1423	}
1424
1425	/// Return [ArkInfo] fetched on last handshake with the Ark server
1426	pub async fn ark_info(&self) -> anyhow::Result<Option<ArkInfo>> {
1427		match self.inner.server.get() {
1428			Some(srv) => Ok(Some(srv.ark_info().await)),
1429			None => Ok(None),
1430		}
1431	}
1432
1433	/// Return [ArkInfo], connecting lazily if not yet connected.
1434	///
1435	/// Errors if the server cannot be reached or if the server's pubkey
1436	/// or mailbox pubkey no longer matches what was stored at wallet
1437	/// creation.
1438	pub async fn require_ark_info(&self) -> anyhow::Result<ArkInfo> {
1439		let (_, ark_info) = self.require_server().await?;
1440		Ok(ark_info)
1441	}
1442
1443	/// Return the [Balance] of the wallet.
1444	///
1445	/// When not running the daemon, make sure you sync before calling this method.
1446	pub async fn balance(&self) -> anyhow::Result<Balance> {
1447		let vtxos = self.vtxos().await?;
1448
1449		let spendable = {
1450			let mut v = vtxos.iter().collect();
1451			VtxoStateKind::Spendable.filter_vtxos(&mut v).await?;
1452			v.into_iter().map(|v| v.amount()).sum::<Amount>()
1453		};
1454
1455		let pending_lightning_send = self.pending_lightning_send_vtxos().await?.iter()
1456			.map(|v| v.amount())
1457			.sum::<Amount>();
1458
1459		let claimable_lightning_receive = self.claimable_lightning_receive_balance().await?;
1460
1461		let pending_board = self.pending_board_vtxos().await?.iter()
1462			.map(|v| v.amount())
1463			.sum::<Amount>();
1464
1465		let pending_in_round = self.pending_round_balance().await?;
1466
1467		let pending_exit = self.exit_mgr().try_pending_total();
1468
1469		Ok(Balance {
1470			spendable,
1471			pending_in_round,
1472			pending_lightning_send,
1473			claimable_lightning_receive,
1474			pending_exit,
1475			pending_board,
1476		})
1477	}
1478
1479	/// Fetches [Vtxo]'s funding transaction and validates the VTXO against it.
1480	pub async fn validate_vtxo(&self, vtxo: &Vtxo<Full>) -> Result<(), VtxoValidationError> {
1481		let tx = self.inner.chain.get_tx(&vtxo.chain_anchor().txid).await
1482			.map_err(VtxoValidationError::Chain)?
1483			.ok_or(VtxoValidationError::AnchorNotFound)?;
1484
1485		vtxo.validate(&tx).map_err(VtxoValidationError::Invalid)
1486	}
1487
1488	/// Manually import a VTXO into the wallet.
1489	///
1490	/// # Arguments
1491	/// * `vtxo` - The VTXO to import
1492	///
1493	/// # Errors
1494	/// Returns an error if:
1495	/// - The VTXO's chain anchor is not found or invalid
1496	/// - The wallet doesn't own a signable clause for the VTXO
1497	pub async fn import_vtxo(&self, vtxo: &Vtxo<Full>) -> anyhow::Result<()> {
1498		if self.inner.db.get_wallet_vtxo(vtxo.id()).await?.is_some() {
1499			info!("VTXO {} already exists in wallet, skipping import", vtxo.id());
1500			return Ok(());
1501		}
1502
1503		self.validate_vtxo(vtxo).await.context("VTXO validation failed")?;
1504
1505		if self.find_signable_clause(vtxo).await.is_none() {
1506			bail!("VTXO {} is not owned by this wallet (no signable clause found)", vtxo.id());
1507		}
1508
1509		let current_height = self.inner.chain.tip().await?;
1510		if vtxo.expiry_height() <= current_height {
1511			bail!("Vtxo {} has expired", vtxo.id());
1512		}
1513
1514		self.store_spendable_vtxos([vtxo]).await.context("failed to store imported VTXO")?;
1515
1516		info!("Successfully imported VTXO {}", vtxo.id());
1517		Ok(())
1518	}
1519
1520	/// Retrieves the full state of a [Vtxo] for a given [VtxoId] if it exists in the database.
1521	pub async fn get_vtxo_by_id(&self, vtxo_id: VtxoId) -> anyhow::Result<WalletVtxo> {
1522		let vtxo = self.inner.db.get_wallet_vtxo(vtxo_id).await
1523			.with_context(|| format!("Error when querying vtxo {} in database", vtxo_id))?
1524			.with_context(|| format!("The VTXO with id {} cannot be found", vtxo_id))?;
1525		Ok(vtxo)
1526	}
1527
1528	/// Hydrate a VTXO into its full form, including the unilateral exit chain.
1529	///
1530	/// [Wallet::get_vtxo_by_id] returns the bare form ([WalletVtxo] holds
1531	/// [Vtxo<ark::vtxo::Bare>]). This method reads the genesis chain from the
1532	/// database and reassembles the full VTXO. Use it from external SDK
1533	/// callers that need the chain (e.g. to feed into [ArkoorPackageBuilder](ark::arkoor::ArkoorPackageBuilder)
1534	/// or [Wallet::register_vtxo_transactions_with_server]).
1535	pub async fn get_full_vtxo(&self, vtxo_id: VtxoId) -> anyhow::Result<Vtxo<Full>> {
1536		self.inner.db.get_full_vtxo(vtxo_id).await
1537			.with_context(|| format!("Error when querying full vtxo {} in database", vtxo_id))?
1538			.with_context(|| format!("The VTXO with id {} cannot be found", vtxo_id))
1539	}
1540
1541	/// Similar to [Wallet::get_full_vtxo] but it retrieves the full variant of each given VTXO.
1542	pub async fn get_full_vtxos<V: VtxoRef>(
1543		&self,
1544		vtxos: impl IntoIterator<Item = V>,
1545	) -> anyhow::Result<Vec<Vtxo<Full>>> {
1546		let ids = vtxos.into_iter().map(|v| v.vtxo_id()).collect::<Vec<_>>();
1547		self.inner.db.get_full_vtxos(&ids).await
1548			.with_context(||
1549				format!("Error when querying full vtxos in database with IDs: {:?}", ids)
1550			)
1551	}
1552
1553	/// Fetches all movements ordered from newest to oldest.
1554	#[deprecated(since="0.1.0-beta.5", note = "Use Wallet::history instead")]
1555	pub async fn movements(&self) -> anyhow::Result<Vec<Movement>> {
1556		self.history().await
1557	}
1558
1559	/// Fetches all wallet fund movements ordered from newest to oldest.
1560	pub async fn history(&self) -> anyhow::Result<Vec<Movement>> {
1561		Ok(self.inner.db.get_all_movements().await?)
1562	}
1563
1564	/// Applies an [RFC 7396](https://www.rfc-editor.org/rfc/rfc7396) JSON Merge Patch to the
1565	/// metadata of a movement.
1566	///
1567	/// ```no_run
1568	/// # use serde_json::json;
1569	/// # async fn example(
1570	/// #     wallet: &bark::Wallet,
1571	/// #     id: bark::movement::MovementId,
1572	/// # ) -> anyhow::Result<()> {
1573	/// // Add or overwrite a key.
1574	/// wallet.update_history_metadata(id, &json!({"note": "refund issued"})).await?;
1575	///
1576	/// // Delete a key (null means remove).
1577	/// wallet.update_history_metadata(id, &json!({"note": null})).await?;
1578	///
1579	/// // Nested merge.
1580	/// wallet.update_history_metadata(id, &json!({"counterparty": {"name": "Alice"}})).await?;
1581	/// # Ok(()) }
1582	/// ```
1583	pub async fn update_history_metadata(
1584		&self,
1585		movement_id: MovementId,
1586		patch: &serde_json::Value,
1587	) -> anyhow::Result<()> {
1588		self.inner.movements.patch_metadata(movement_id, patch).await?;
1589		Ok(())
1590	}
1591
1592	/// Query the wallet history by the given payment method
1593	pub async fn history_by_payment_method(
1594		&self,
1595		payment_method: &PaymentMethod,
1596	) -> anyhow::Result<Vec<Movement>> {
1597		let mut ret = self.inner.db.get_movements_by_payment_method(payment_method).await?;
1598		ret.sort_by_key(|m| m.id);
1599		Ok(ret)
1600	}
1601
1602	/// Returns all VTXOs from the database.
1603	pub async fn all_vtxos(&self) -> anyhow::Result<Vec<WalletVtxo>> {
1604		Ok(self.inner.db.get_all_vtxos().await?)
1605	}
1606
1607	/// Returns all not spent vtxos
1608	pub async fn vtxos(&self) -> anyhow::Result<Vec<WalletVtxo>> {
1609		Ok(self.inner.db.get_vtxos_by_state(&VtxoStateKind::UNSPENT_STATES).await?)
1610	}
1611
1612	/// Returns all vtxos matching the provided predicate
1613	pub async fn vtxos_with(&self, filter: &impl FilterVtxos) -> anyhow::Result<Vec<WalletVtxo>> {
1614		let mut vtxos = self.vtxos().await?;
1615		filter.filter_vtxos(&mut vtxos).await.context("error filtering vtxos")?;
1616		Ok(vtxos)
1617	}
1618
1619	/// Returns all spendable vtxos
1620	pub async fn spendable_vtxos(&self) -> anyhow::Result<Vec<WalletVtxo>> {
1621		Ok(self.vtxos_with(&VtxoStateKind::Spendable).await?)
1622	}
1623
1624	/// Returns all spendable vtxos matching the provided predicate
1625	pub async fn spendable_vtxos_with(
1626		&self,
1627		filter: &impl FilterVtxos,
1628	) -> anyhow::Result<Vec<WalletVtxo>> {
1629		let mut vtxos = self.spendable_vtxos().await?;
1630		filter.filter_vtxos(&mut vtxos).await.context("error filtering vtxos")?;
1631		Ok(vtxos)
1632	}
1633
1634	/// Returns all vtxos that will expire within `threshold` blocks
1635	pub async fn get_expiring_vtxos(
1636		&self,
1637		threshold: BlockHeight,
1638	) -> anyhow::Result<Vec<WalletVtxo>> {
1639		let expiry = self.inner.chain.tip().await? + threshold;
1640		let filter = VtxoFilter::new(&self).expires_before(expiry);
1641		Ok(self.spendable_vtxos_with(&filter).await?)
1642	}
1643
1644	/// Performs maintenance tasks and performs refresh interactively until finished when needed.
1645	///
1646	/// This can take a long period of time due to registering boards, syncing
1647	/// rounds, arkoors, checking pending lightning payments and refreshing VTXOs
1648	/// if necessary.
1649	pub async fn maintenance(&self) -> anyhow::Result<()> {
1650		info!("Starting wallet maintenance in interactive mode");
1651		self.sync().await;
1652
1653		// First try progress any rounds that exist, best effort.
1654		let rounds = self.progress_pending_rounds(None).await;
1655		if let Err(e) = rounds.as_ref() {
1656			warn!("Error progressing pending rounds: {:#}", e);
1657		}
1658
1659		// Then if there are still some participations open, try to cancel them.
1660		let states = self.inner.db.get_pending_round_state_ids().await?;
1661		for id in states {
1662			debug!("Cancelling pending round participation {}", id);
1663			let mut state = match self.lock_wait_round_state(id).await {
1664				Ok(Some(s)) => s,
1665				Ok(None) => continue, // round disappeared, not our problem
1666				Err(e) => {
1667					warn!("Failed to lock round state with id {}: {:#}", id, e);
1668					continue;
1669				}
1670			};
1671			if let Err(e) = state.state_mut().try_cancel(self).await {
1672				warn!("Error cancelling pending round: {:#}", e);
1673			}
1674		}
1675
1676		// And then call refresh so that we can start again.
1677		let refresh = self.maintenance_refresh().await;
1678		if let Err(e) = refresh.as_ref() {
1679			warn!("Error refreshing VTXOs: {:#}", e);
1680		}
1681
1682		if rounds.is_err() || refresh.is_err() {
1683			bail!("Maintenance encountered errors.\nprogress_rounds: {:#?}\nrefresh: {:#?}",
1684				rounds, refresh,
1685			);
1686		}
1687
1688		Ok(())
1689	}
1690
1691	/// Performs maintenance tasks and schedules delegated refresh when needed. This risks spending
1692	/// users' funds because refreshing may cost fees and any pending exits will be progressed.
1693	///
1694	/// This can take a long period of time due to syncing the onchain wallet, registering boards,
1695	/// syncing rounds, arkoors, and the exit system, checking pending lightning payments and
1696	/// refreshing VTXOs if necessary.
1697	pub async fn maintenance_delegated(&self) -> anyhow::Result<()> {
1698		info!("Starting wallet maintenance in delegated mode");
1699		self.sync().await;
1700		let rounds = self.progress_pending_rounds(None).await;
1701		if let Err(e) = rounds.as_ref() {
1702			warn!("Error progressing pending rounds: {:#}", e);
1703		}
1704		let refresh = self.maybe_schedule_maintenance_refresh_delegated().await;
1705		if let Err(e) = refresh.as_ref() {
1706			warn!("Error refreshing VTXOs: {:#}", e);
1707		}
1708
1709		if rounds.is_err() || refresh.is_err() {
1710			bail!("Delegated maintenance encountered errors.\n\
1711				progress_rounds: {:#?}\nrefresh: {:#?}",
1712				rounds, refresh,
1713			);
1714		}
1715
1716		Ok(())
1717	}
1718
1719	/// Actively join the given in-flight round `attempt` with all VTXOs due for
1720	/// maintenance refresh, dropping any input the server rejects as unusable and
1721	/// re-submitting the rest to the *same* attempt (the server keeps its submit
1722	/// window open after a rejection, so the corrected participation still lands
1723	/// in this round).
1724	///
1725	/// This is the shared core of interactive maintenance: the blocking
1726	/// [Wallet::maintenance_refresh] calls it and then drives the round to
1727	/// completion, while the daemon calls it on the round Attempt event and lets
1728	/// [Wallet::progress_pending_rounds] carry the round forward. Mirrors
1729	/// [Wallet::maybe_schedule_maintenance_refresh_delegated].
1730	///
1731	/// Returns the id of the round state we joined, or `None` if there was
1732	/// nothing economical to refresh.
1733	pub(crate) async fn join_round_for_maintenance_refresh(
1734		&self,
1735		attempt: &RoundAttempt,
1736	) -> anyhow::Result<Option<RoundStateId>> {
1737		self.maintenance_refresh_retry_loop(|part| async move {
1738			info!("Joining round {} for maintenance refresh ({} vtxos)",
1739				attempt.round_seq, part.inputs.len());
1740			Ok(Some(self.join_attempt_interactive(
1741				part, attempt, Some(RoundMovement::Refresh),
1742			).await?.id()))
1743		}).await.context("failed to join round for maintenance refresh")
1744	}
1745
1746	/// Checks VTXOs that are due to be refreshed, and schedules a delegated refresh if any
1747	///
1748	/// This will include any VTXOs within the expiry threshold
1749	/// ([Config::vtxo_refresh_expiry_threshold]) or those which
1750	/// are uneconomical to exit due to onchain network conditions.
1751	///
1752	/// Returns a [RoundStateId] if a refresh is scheduled.
1753	pub async fn maybe_schedule_maintenance_refresh_delegated(
1754		&self,
1755	) -> anyhow::Result<Option<RoundStateId>> {
1756		self.maintenance_refresh_retry_loop(|part| async move {
1757			info!("Scheduling delegated maintenance refresh ({} vtxos)", part.inputs.len());
1758			Ok(Some(self.join_next_round_delegated(part, Some(RoundMovement::Refresh)).await?.id()))
1759		}).await.context("failed to schedule delegated maintenance refresh")
1760	}
1761
1762	/// The retry loop shared by the interactive and delegated maintenance refreshes.
1763	///
1764	/// Selects the VTXOs due for refresh (minus any the server has already rejected
1765	/// as unusable), runs `attempt_refresh` for them, and if it fails naming unusable
1766	/// inputs, drops those and retries — up to 10 times. Both submission modes
1767	/// validate inputs synchronously, so a rejection surfaces here rather than
1768	/// poisoning the batch forever; `attempt_refresh` is the only part that differs.
1769	async fn maintenance_refresh_retry_loop<F, Fut>(
1770		&self,
1771		attempt_refresh: F,
1772	) -> anyhow::Result<Option<RoundStateId>>
1773	where
1774		F: Fn(RoundParticipation) -> Fut,
1775		Fut: Future<Output = anyhow::Result<Option<RoundStateId>>>,
1776	{
1777		let mut excluded = HashSet::new();
1778		for _ in 0..10 {
1779			let vtxos = self.get_vtxos_to_refresh_with_excluded(excluded.iter().copied()).await?;
1780			match (vtxos.is_empty(), excluded.is_empty()) {
1781				// Every VTXO due for refresh has been excluded as unusable by the server:
1782				// there is nothing left to submit, so surface an error rather than
1783				// silently reporting success.
1784				(true, false) => {
1785					warn!("no VTXOs to refresh after exclusions: {:?}", excluded);
1786					bail!("no VTXOs to refresh after excluding: {:?}", excluded);
1787				},
1788				// Nothing was due for refresh in the first place.
1789				(true, true) => return Ok(None),
1790				// Still have VTXOs to submit (possibly after dropping some exclusions).
1791				(false, _) => {},
1792			}
1793			let part = match self.build_refresh_participation(vtxos).await? {
1794				Some(participation) => participation,
1795				None => return Ok(None),
1796			};
1797
1798			match attempt_refresh(part).await {
1799				Ok(state_id) => return Ok(state_id),
1800				Err(e) => {
1801					let rejected = rejected_vtxos_from_error(&e).into_iter()
1802						.filter(|id| !excluded.contains(id))
1803						.collect::<Vec<_>>();
1804					if rejected.is_empty() {
1805						return Err(e);
1806					}
1807					warn!("Maintenance refresh rejected {} unusable input(s) ({:?}); \
1808						retrying without them", rejected.len(), rejected);
1809					excluded.extend(rejected);
1810				},
1811			}
1812		}
1813		bail!("Maintenance refresh failed after 10 retries");
1814	}
1815
1816	/// Performs an interactive refresh of all VTXOs that are due to be refreshed, if any
1817	///
1818	/// This will include any VTXOs within the expiry threshold
1819	/// ([Config::vtxo_refresh_expiry_threshold]) or those which
1820	/// are uneconomical to exit due to onchain network conditions.
1821	///
1822	/// Waits for a round to start, joins it, dropping any inputs the server rejects
1823	/// as unusable and retries within the same attempt, then drives that round to
1824	/// completion.
1825	///
1826	/// Returns a [RoundStatus] if a refresh occurs.
1827	pub async fn maintenance_refresh(&self) -> anyhow::Result<Option<RoundStatus>> {
1828		if self.get_vtxos_to_refresh().await?.is_empty() {
1829			return Ok(None);
1830		}
1831
1832		info!("Waiting for round to perform maintenance refresh...");
1833		let mut events = self.subscribe_round_events().await?;
1834		while let Some(event) = events.next().await {
1835			let event = event.context("error on round event stream")?;
1836			if let RoundEvent::Attempt(a) = event && a.attempt_seq == 0 {
1837				debug!("Round {} started, triggering maintenance refresh", a.round_seq);
1838				let state_id = match self.join_round_for_maintenance_refresh(&a).await? {
1839					Some(id) => id,
1840					None => return Ok(None),
1841				};
1842				// We submitted up-front, so drive the (now ongoing) round to completion
1843				// on this same event stream.
1844				let state = self.lock_wait_round_state(state_id).await?
1845					.context("maintenance refresh round state vanished after joining")?;
1846				return Ok(Some(self.drive_round_state(state, &mut events).await?));
1847			}
1848		}
1849		Ok(None)
1850	}
1851
1852	/// Sync offchain wallet and update onchain fees. This is a much more lightweight alternative
1853	/// to [Wallet::maintenance] as it will not refresh VTXOs or sync the onchain wallet.
1854	///
1855	/// Notes:
1856	/// - Exits are only synced if we detect onchain activity which has force-exited our VTXO.
1857	pub async fn sync(&self) {
1858		self.inner.chain.invalidate_caches().await;
1859
1860		futures::join!(
1861			async {
1862				// NB: order matters here, if syncing call fails,
1863				// we still want to update the fee rates
1864				if let Err(e) = self.inner.chain.update_fee_rates(self.inner.config.fallback_fee_rate).await {
1865					warn!("Error updating fee rates: {:#}", e);
1866				}
1867			},
1868			async {
1869				if let Err(e) = self.sync_mailbox().await {
1870					warn!("Error in mailbox sync: {:#}", e);
1871				}
1872			},
1873			async {
1874				if let Err(e) = self.sync_pending_rounds().await {
1875					warn!("Error while trying to progress rounds awaiting confirmations: {:#}", e);
1876				}
1877			},
1878			async {
1879				if let Err(e) = self.sync_pending_lightning_send_vtxos().await {
1880					warn!("Error syncing pending lightning payments: {:#}", e);
1881				}
1882			},
1883			async {
1884				if let Err(e) = self.sync_pending_arkoor_sends().await {
1885					warn!("Error syncing pending arkoor sends: {:#}", e);
1886				}
1887			},
1888			async {
1889				if let Err(e) = self.try_claim_all_lightning_receives(false).await {
1890					warn!("Error claiming pending lightning receives: {:#}", e);
1891				}
1892			},
1893			async {
1894				if let Err(e) = self.sync_pending_boards().await {
1895					warn!("Error syncing pending boards: {:#}", e);
1896				}
1897			},
1898			async {
1899				if let Err(e) = self.sync_pending_offboards().await {
1900					warn!("Error syncing pending offboards: {:#}", e);
1901				}
1902			},
1903			async {
1904				if let Err(e) = self.sync_force_exited_vtxos().await {
1905					warn!("Error scanning for on-chain-exited VTXOs: {:#}", e);
1906				}
1907			},
1908			async {
1909				// Re-assert recovery state so vtxos whose mailbox post or
1910				// registration failed at store time, or that predate the
1911				// recovery mechanism, get caught up (non-critical).
1912				if let Err(e) = self.catchup_recovery_vtxos().await {
1913					warn!("Failed to catch up recovery VTXOs with server: {:#}", e);
1914				}
1915			}
1916		);
1917	}
1918
1919	/// Sync the transaction status of unilateral exits
1920	///
1921	/// This will not progress the unilateral exits in any way, it will merely check the
1922	/// transaction status of each transaction as well as check whether any exits have become
1923	/// claimable or have been claimed.
1924	pub async fn sync_exits(&self) -> anyhow::Result<()> {
1925		self.exit_mgr().sync(&self).await?;
1926		Ok(())
1927	}
1928
1929	/// Progress unilateral exits
1930	///
1931	/// This risks spending users' funds because refreshing may cost fees and any
1932	/// pending exits will be progressed.
1933	pub async fn progress_exits(&self) -> anyhow::Result<()> {
1934		self.exit_mgr().progress_exits_with_cpfp(&self, None).await?;
1935		Ok(())
1936	}
1937
1938	/// Detect spendable VTXOs that were exited on-chain without the user asking for it — e.g.
1939	/// the server's watchman progressing a shared tree, or a third party's unilateral exit
1940	/// dragging a parent on-chain — and route them into the unilateral-exit flow so the funds
1941	/// can be claimed on-chain.
1942	///
1943	/// Such VTXOs are otherwise left `Spendable` by a normal sync even though the server now
1944	/// rejects spending them, leaving the user stuck. We detect them by checking, on each new
1945	/// chain tip, whether any spendable VTXO's own funding tx is already on-chain.
1946	///
1947	/// This is deliberately independent of the onchain wallet sync, since the onchain wallet
1948	/// may be disabled. The caller must also ensure that the wallet state is up to date before
1949	/// calling this.
1950	pub async fn sync_force_exited_vtxos(&self) -> anyhow::Result<()> {
1951		// A VTXO's on-chain status can only change across blocks, so only scan when the tip moves.
1952		let tip = self.inner.chain.tip().await?;
1953		let mut lock = self.inner.last_force_exit_scan_tip.lock().await;
1954		if *lock == Some(tip) {
1955			return Ok(());
1956		}
1957
1958		// Skip VTXOs already being exited.
1959		let exiting = self.exit_mgr().get_exit_vtxo_ids().await;
1960		let vtxos = self.inner.db.get_vtxos_by_state(&[VtxoStateKind::Spendable]).await?
1961			.into_iter()
1962			.filter(|v| !exiting.contains(&v.vtxo.id()));
1963
1964		// Check each candidate's funding tx in parallel.
1965		let mut checked = FuturesUnordered::new();
1966		for wv in vtxos {
1967			let chain = self.inner.chain.clone();
1968			checked.push(async move {
1969				let txid = wv.vtxo_id().to_point().txid;
1970				let status = chain.tx_status(txid).await;
1971				(wv, status)
1972			});
1973		}
1974
1975		let mut to_exit = Vec::new();
1976		while let Some((vtxo, status)) = futures::StreamExt::next(&mut checked).await {
1977			match status {
1978				Ok(TxStatus::NotFound) => {},
1979				Ok(_) => {
1980					info!("VTXO {} was exited on-chain without us; routing it to a claimable exit",
1981						vtxo.vtxo.id(),
1982					);
1983					to_exit.push(vtxo.vtxo);
1984				},
1985				Err(e) => warn!("Could not check on-chain status of VTXO {}: {:#}",
1986					vtxo.vtxo.id(), e,
1987				),
1988			}
1989		}
1990
1991		if !to_exit.is_empty() {
1992			self.exit_mgr().start_exit_for_vtxos(&to_exit).await
1993				.context("failed to start exit for on-chain-exited VTXOs")?;
1994
1995			*lock = Some(tip);
1996			self.sync_exits().await
1997				.context("failed to sync exits after starting new ones")?;
1998		} else {
1999			*lock = Some(tip);
2000		}
2001
2002		Ok(())
2003	}
2004
2005	/// Drop a specific [Vtxo] from the database. This is destructive and will result in a loss of
2006	/// funds.
2007	pub async fn dangerous_drop_vtxo(&self, vtxo_id: VtxoId) -> anyhow::Result<()> {
2008		warn!("Drop vtxo {} from the database", vtxo_id);
2009		self.inner.db.remove_vtxo(vtxo_id).await?;
2010		Ok(())
2011	}
2012
2013	/// Drop all VTXOs from the database. This is destructive and will result in a loss of funds.
2014	//TODO(stevenroose) improve the way we expose dangerous methods
2015	pub async fn dangerous_drop_all_vtxos(&self) -> anyhow::Result<()> {
2016		warn!("Dropping all vtxos from the db...");
2017		for vtxo in self.vtxos().await? {
2018			self.inner.db.remove_vtxo(vtxo.id()).await?;
2019		}
2020
2021		self.exit_mgr().dangerous_clear_exit().await?;
2022		Ok(())
2023	}
2024
2025	/// Checks if the provided VTXO has some counterparty risk in the current wallet.
2026	///
2027	/// An arkoor vtxo is considered to have some counterparty risk if it is
2028	/// (directly or not) based on round VTXOs that aren't owned by the
2029	/// wallet. The check inspects the genesis chain, so this takes a full
2030	/// VTXO; callers working from a bare listing should hydrate via
2031	/// [Wallet::get_full_vtxo] or [BarkPersister::get_full_vtxos] first.
2032	async fn has_counterparty_risk(&self, vtxo: &Vtxo<Full>) -> anyhow::Result<bool> {
2033		for past_pks in vtxo.past_arkoor_pubkeys() {
2034			let mut owns_any = false;
2035			for past_pk in past_pks {
2036				if self.inner.db.get_public_key_idx(&past_pk).await?.is_some() {
2037					owns_any = true;
2038					break;
2039				}
2040			}
2041			if !owns_any {
2042				return Ok(true);
2043			}
2044		}
2045
2046		let my_clause = self.find_signable_clause(vtxo).await;
2047		Ok(!my_clause.is_some())
2048	}
2049
2050	pub async fn build_refresh_participation<V: VtxoRef>(
2051		&self,
2052		vtxos: impl IntoIterator<Item = V>,
2053	) -> anyhow::Result<Option<RoundParticipation>> {
2054		self.inner_build_refresh_participation(vtxos, None).await
2055	}
2056
2057	pub async fn build_scheduled_refresh_participation<V: VtxoRef>(
2058		&self,
2059		vtxos: impl IntoIterator<Item = V>,
2060		height: BlockHeight,
2061	) -> anyhow::Result<Option<RoundParticipation>> {
2062		self.inner_build_refresh_participation(vtxos, Some(height)).await
2063	}
2064
2065	async fn inner_build_refresh_participation<V: VtxoRef>(
2066		&self,
2067		vtxos: impl IntoIterator<Item = V>,
2068		height: Option<BlockHeight>,
2069	) -> anyhow::Result<Option<RoundParticipation>> {
2070		let (vtxos, total_amount) = {
2071			let iter = vtxos.into_iter();
2072			let size_hint = iter.size_hint();
2073			let mut vtxos = Vec::<Vtxo<Full>>::with_capacity(size_hint.1.unwrap_or(size_hint.0));
2074			let mut amount = Amount::ZERO;
2075			for vref in iter {
2076				// We use a Vec here instead of a HashMap or a HashSet of IDs because for the kinds
2077				// of elements we expect to deal with, a Vec is likely to be quicker. The overhead
2078				// of hashing each ID and making additional allocations isn't likely to be worth it
2079				// for what is likely to be a handful of VTXOs or at most a couple of hundred.
2080				let id = vref.vtxo_id();
2081				if vtxos.iter().any(|v| v.id() == id) {
2082					bail!("duplicate VTXO id: {}", id);
2083				}
2084				let vtxo = if let Some(vtxo) = vref.into_full_vtxo() {
2085					vtxo
2086				} else {
2087					// Listings/selection return bare wallet vtxos; the round
2088					// flow needs the full chain to forfeit and register.
2089					self.inner.db.get_full_vtxo(id).await?
2090						.with_context(|| format!("vtxo with id {} not found", id))?
2091				};
2092				amount += vtxo.amount();
2093				vtxos.push(vtxo);
2094			}
2095			(vtxos, amount)
2096		};
2097
2098		if vtxos.is_empty() {
2099			info!("Skipping refresh since no VTXOs are provided.");
2100			return Ok(None);
2101		}
2102		ensure!(total_amount >= VTXO_DUST,
2103			"vtxo amount must be at least {} to participate in a round",
2104			VTXO_DUST,
2105		);
2106
2107		// Calculate refresh fees
2108		let (_, ark_info) = self.require_server().await?;
2109		let refresh_height = match height {
2110			Some(height) => height,
2111			None => self.inner.chain.tip().await?,
2112		};
2113
2114		let vtxo_fee_infos = vtxos.iter()
2115			.map(|v| VtxoFeeInfo::from_vtxo_and_tip(v, refresh_height));
2116		let fee = ark_info.fees.refresh.calculate(vtxo_fee_infos).context("fee overflowed")?;
2117		let output_amount = validate_and_subtract_fee_min_dust(total_amount, fee, VTXO_DUST)?;
2118
2119		info!("Refreshing {} VTXOs (total amount = {}, fee = {}, output = {}).",
2120			vtxos.len(), total_amount, fee, output_amount,
2121		);
2122		let (user_keypair, _) = self.derive_store_next_keypair().await?;
2123		let req = VtxoRequest {
2124			policy: VtxoPolicy::Pubkey(PubkeyVtxoPolicy { user_pubkey: user_keypair.public_key() }),
2125			amount: output_amount,
2126		};
2127
2128		Ok(Some(RoundParticipation {
2129			inputs: vtxos,
2130			outputs: vec![req],
2131			unblinded_mailbox_id: None,
2132		}))
2133	}
2134
2135	/// This will refresh all provided VTXOs in an interactive round and wait until end
2136	///
2137	/// Returns the [RoundStatus] of the round if a successful refresh occurred.
2138	/// It will return [None] if no [Vtxo] needed to be refreshed.
2139	pub async fn refresh_vtxos<V: VtxoRef>(
2140		&self,
2141		vtxos: impl IntoIterator<Item = V>,
2142	) -> anyhow::Result<Option<RoundStatus>> {
2143		let participation = match self.build_refresh_participation(vtxos).await? {
2144			Some(participation) => participation,
2145			None => return Ok(None),
2146		};
2147
2148		Ok(Some(self.participate_round(participation, Some(RoundMovement::Refresh)).await?))
2149	}
2150
2151	/// This will refresh all provided VTXOs in delegated (non-interactive) mode
2152	///
2153	/// Returns the [StoredRoundState] which can be used to track the round's
2154	/// progress later by calling sync. It will return [None] if no [Vtxo]
2155	/// needed to be refreshed.
2156	pub async fn refresh_vtxos_delegated<V: VtxoRef>(
2157		&self,
2158		vtxos: impl IntoIterator<Item = V>,
2159	) -> anyhow::Result<Option<StoredRoundState<Unlocked>>> {
2160		let part = match self.build_refresh_participation(vtxos).await? {
2161			Some(participation) => participation,
2162			None => return Ok(None),
2163		};
2164
2165		Ok(Some(self.join_delegated_round(
2166			part, Some(RoundMovement::Refresh), None,
2167		).await?))
2168	}
2169
2170	/// Same as [Wallet::refresh_vtxos_delegated] but it schedules the refresh for
2171	/// the given block height instead of the next round (see [Wallet::join_delegated_round]).
2172	pub async fn refresh_vtxos_scheduled<V: VtxoRef>(
2173		&self,
2174		vtxos: impl IntoIterator<Item = V>,
2175		scheduled_height: BlockHeight,
2176	) -> anyhow::Result<Option<StoredRoundState<Unlocked>>> {
2177		let part = match self
2178			.build_scheduled_refresh_participation(vtxos, scheduled_height).await?
2179		{
2180			Some(participation) => participation,
2181			None => return Ok(None),
2182		};
2183
2184		Ok(Some(self.join_delegated_round(
2185			part, Some(RoundMovement::Refresh), Some(scheduled_height),
2186		).await?))
2187	}
2188
2189	/// This will find all VTXOs that meets must-refresh criteria. Then, if there are some VTXOs to
2190	/// refresh, it will also add those that meet should-refresh criteria.
2191	pub async fn get_vtxos_to_refresh(&self) -> anyhow::Result<Vec<WalletVtxo>> {
2192		let vtxos = self.spendable_vtxos_with(&RefreshStrategy::should_refresh_if_must(
2193			self,
2194			self.inner.chain.tip().await?,
2195			self.inner.chain.fee_rates().await.fast,
2196		)).await?;
2197		Ok(vtxos)
2198	}
2199
2200	/// Similar to [Wallet::get_vtxos_to_refresh] but it allows VTXOs to be excluded from the
2201	/// result.
2202	pub async fn get_vtxos_to_refresh_with_excluded<V: VtxoRef>(
2203		&self,
2204		exclude: impl IntoIterator<Item = V>,
2205	) -> anyhow::Result<Vec<WalletVtxo>> {
2206		let mut vtxos = self.get_vtxos_to_refresh().await?;
2207		for v in exclude.into_iter() {
2208			if let Some(index) = vtxos.iter().position(|vtxo| vtxo.id() == v.vtxo_id()) {
2209				vtxos.swap_remove(index);
2210			}
2211		}
2212		Ok(vtxos)
2213	}
2214
2215	/// Returns the block height at which the first VTXO will expire
2216	pub async fn get_first_expiring_vtxo_blockheight(
2217		&self,
2218	) -> anyhow::Result<Option<BlockHeight>> {
2219		Ok(self.spendable_vtxos().await?.iter().map(|v| v.expiry_height()).min())
2220	}
2221
2222	/// Returns the next block height at which we have a VTXO that we
2223	/// want to refresh
2224	pub async fn get_next_required_refresh_blockheight(
2225		&self,
2226	) -> anyhow::Result<Option<BlockHeight>> {
2227		let first_expiry = self.get_first_expiring_vtxo_blockheight().await?;
2228		Ok(first_expiry.map(|h| h.saturating_sub(self.inner.config.vtxo_refresh_expiry_threshold)))
2229	}
2230
2231	/// Base [InputSelection] for spending VTXOs: skips VTXOs whose exit
2232	/// depth the server would reject as arkoor inputs. Those can only be
2233	/// spent again after a refresh.
2234	async fn spend_input_selection(&self) -> anyhow::Result<InputSelection> {
2235		let mut selection = InputSelection::new();
2236		if let Some(info) = self.ark_info().await? {
2237			selection = selection.max_exit_depth(info.max_vtxo_exit_depth);
2238		}
2239		Ok(selection)
2240	}
2241
2242	/// Select any spendable VTXOs to cover the provided amount.
2243	async fn select_any_vtxos_to_cover(
2244		&self,
2245		amount: Amount,
2246	) -> anyhow::Result<Vec<WalletVtxo>> {
2247		self.spend_input_selection().await?.select(self.spendable_vtxos().await?, amount)
2248	}
2249
2250	/// Determines which VTXOs to use for a fee-paying transaction where the fee is added on top of
2251	/// the desired amount. E.g., a lightning payment, a send-onchain payment.
2252	///
2253	/// See [InputSelection::fee_scheme].
2254	async fn select_any_vtxos_to_cover_with_fee<F>(
2255		&self,
2256		amount: Amount,
2257		calc_fee: F,
2258	) -> anyhow::Result<(Vec<WalletVtxo>, Amount)>
2259	where
2260		F: for<'a> Fn(Amount, SelectedFeeInfos<'a>) -> anyhow::Result<Amount>,
2261	{
2262		let tip = self.inner.chain.tip().await?;
2263		self.spend_input_selection().await?
2264			.fee_scheme(tip, calc_fee)
2265			.select(self.spendable_vtxos().await?, amount)
2266	}
2267
2268	/// Starts a daemon for the wallet.
2269	///
2270	/// The daemon uses the onchain wallet stored in [OpenWalletArgs::onchain] (if any)
2271	/// for background onchain syncing and exit fee-bumping.
2272	///
2273	/// The daemon holds only a weak reference to the wallet, so it doesn't
2274	/// keep the wallet alive: when the last [Wallet] clone is dropped, the
2275	/// daemon shuts itself down. Any single unit of daemon work in flight
2276	/// (a sync pass, a round participation) finishes gracefully first.
2277	pub fn start_daemon(&self) -> anyhow::Result<()> {
2278		let mut daemon = self.inner.daemon.lock();
2279		if daemon.is_some() {
2280			warn!("Called Wallet::start_daemon while daemon was already running.");
2281			return Ok(());
2282		}
2283
2284		let handle = crate::daemon::start_daemon(self);
2285		let _ = daemon.insert(handle);
2286
2287		Ok(())
2288	}
2289
2290	/// Stops the daemon for the wallet if it is running, otherwise does nothing.
2291	pub fn stop_daemon(&self) {
2292		let mut daemon = self.inner.daemon.lock();
2293		if let Some(handle) = daemon.take() {
2294			handle.stop();
2295		}
2296	}
2297
2298	/// Posts the IDs of all non-spent (spendable, locked and exited) VTXOs
2299	/// to the server's recovery mailbox and re-registers their fully-signed
2300	/// transaction chains, so a wallet recovering from seed can rebuild
2301	/// its state. Both server endpoints are idempotent.
2302	///
2303	/// Exited VTXOs are backed up too: their exit transactions being
2304	/// broadcast doesn't mean the on-chain outputs were claimed, and a
2305	/// wallet recovering from seed must learn about the exit to claim
2306	/// the funds.
2307	///
2308	/// VTXOs whose mailbox post and chain registration both succeeded once
2309	/// are marked [WalletVtxo::registered] and skipped from then on, so
2310	/// repeated syncs don't re-upload — or even re-read — the whole wallet.
2311	///
2312	/// VTXOs of boards that are still in progress are left out: the server
2313	/// only gets a vtxo row for those once the board registers, and that
2314	/// step posts them for recovery itself.
2315	async fn catchup_recovery_vtxos(&self) -> anyhow::Result<()> {
2316		let mut ids = self.inner.db.get_unregistered_vtxo_ids().await?;
2317		if ids.is_empty() {
2318			return Ok(());
2319		}
2320
2321		// TODO(pc): Investigate if we should first load `WalletVtxo` in case we have other reasons
2322		//  to exclude VTXOs other than pending boards. This works for now.
2323		let in_progress_boards = self.boards_in_progress().await?;
2324		ids.retain(|id| !in_progress_boards.iter().any(|b| b.vtxo_id == *id));
2325		if ids.is_empty() {
2326			return Ok(());
2327		}
2328
2329		// The mailbox post and the chain registration are independent and
2330		// both idempotent, so one failing must not stop the other. A vtxo
2331		// is only marked registered once both succeeded for it, so a failed
2332		// mailbox post keeps every vtxo eligible for the next catch-up.
2333		let posted = self.post_recovery_vtxo_ids(ids.iter().copied()).await
2334			.context("failed to post recovery vtxo IDs");
2335		let registered = self.register_recovery_vtxo_chains(&ids, posted.is_ok()).await;
2336
2337		match (posted, registered) {
2338			(Ok(()), registered) => registered,
2339			(posted, Ok(())) => posted,
2340			(Err(posted), Err(registered)) => {
2341				Err(registered.context(format!("mailbox post also failed: {:#}", posted)))
2342			},
2343		}
2344	}
2345
2346	/// Registers the fully-signed transaction chains of the given wallet
2347	/// VTXOs with the server, in chunks with a per-vtxo fallback. When
2348	/// `mark_registered` is set, every successfully registered vtxo is
2349	/// marked [WalletVtxo::registered]. Part of
2350	/// [`Self::catchup_recovery_vtxos`].
2351	async fn register_recovery_vtxo_chains(
2352		&self,
2353		ids: &[VtxoId],
2354		mark_registered: bool,
2355	) -> anyhow::Result<()> {
2356		const CHUNK_SIZE: usize = 20;
2357		let mut failed = 0;
2358		for chunk_ids in ids.chunks(CHUNK_SIZE) {
2359			// Load the full vtxos one chunk at a time: exit chains can be
2360			// tens of KB each, so don't hold every chain in memory at once.
2361			let chunk = self.inner.db.get_full_vtxos(chunk_ids).await
2362				.context("failed to load full vtxos for recovery registration")?;
2363			ensure!(chunk.len() == chunk_ids.len(),
2364				"loaded {} full vtxos for {} ids", chunk.len(), chunk_ids.len(),
2365			);
2366
2367			let mut succeeded = Vec::with_capacity(chunk.len());
2368			match self.register_vtxo_transactions_with_server(&chunk).await {
2369				Ok(()) => succeeded.extend(chunk.iter().map(|v| v.id())),
2370				Err(e) => {
2371					debug!("Failed to register chunk of {} vtxo transactions, \
2372						retrying one by one: {:#}", chunk.len(), e,
2373					);
2374					for vtxo in &chunk {
2375						match self.register_vtxo_transactions_with_server(
2376							std::slice::from_ref(vtxo),
2377						).await {
2378							Ok(()) => succeeded.push(vtxo.id()),
2379							Err(e) => {
2380								error!("Failed to register vtxo {} transactions with server; \
2381									recovery from seed may miss it until registration succeeds: {:#}",
2382									vtxo.id(), e,
2383								);
2384								failed += 1;
2385							},
2386						}
2387					}
2388				},
2389			}
2390			if mark_registered && !succeeded.is_empty() {
2391				self.inner.db.mark_vtxos_registered(&succeeded).await
2392					.context("failed to mark vtxos as registered for recovery")?;
2393			}
2394		}
2395		if failed > 0 {
2396			bail!("failed to register {} of {} vtxo transactions", failed, ids.len());
2397		}
2398		Ok(())
2399	}
2400
2401	/// Registers the signed transaction chains for the given VTXOs with the
2402	/// server. This must be called before spending VTXOs so the server can
2403	/// publish forfeits if needed.
2404	pub async fn register_vtxo_transactions_with_server(
2405		&self,
2406		vtxos: &[impl AsRef<Vtxo<Full>>],
2407	) -> anyhow::Result<()> {
2408		if vtxos.is_empty() {
2409			return Ok(());
2410		}
2411
2412		let (mut srv, _) = self.require_server().await?;
2413		srv.client.register_vtxo_transactions(protos::RegisterVtxoTransactionsRequest {
2414			vtxos: vtxos.iter().map(|v| v.as_ref().serialize()).collect(),
2415		}).await.context("failed to register vtxo transactions")?;
2416
2417		Ok(())
2418	}
2419}
2420
2421fn wrap_server_connect_error(err: ConnectError) -> anyhow::Error {
2422	match err {
2423		ConnectError::CreateEndpoint(CreateEndpointError::NoTransportBackend) => {
2424			anyhow!(MISSING_SERVER_TRANSPORT_HELP)
2425		},
2426		other => anyhow::Error::from(other),
2427	}
2428}
2429
2430impl std::ops::Drop for WalletInner {
2431	fn drop(&mut self) {
2432		// The daemon task holds only a Weak reference to this struct (see
2433		// [crate::daemon]), precisely so that it can't keep the wallet alive
2434		// after the last user-held [Wallet] clone is dropped. That also means
2435		// nobody is left to stop it at that point — do it here. Cancelling
2436		// the token wakes the daemon loops immediately; the task finishes
2437		// detached (Drop can't await it).
2438		if let Some(handle) = self.daemon.lock().take() {
2439			handle.stop();
2440		}
2441	}
2442}
2443
2444#[cfg(test)]
2445mod tests {
2446	use server_rpc::client::CreateEndpointError;
2447
2448	use super::{wrap_server_connect_error, MISSING_SERVER_TRANSPORT_HELP};
2449
2450	#[test]
2451	fn no_transport_connect_error_is_reworded_for_wallet_users() {
2452		let err = wrap_server_connect_error(CreateEndpointError::NoTransportBackend.into());
2453		assert!(err.to_string().contains(MISSING_SERVER_TRANSPORT_HELP));
2454		assert!(err.to_string().contains("feature `bark-wallet/native` or `bark-wallet/wasm-web`"));
2455	}
2456}