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