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-experimental-red.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 [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 tokio::fs;
66//! use bark::{Config, onchain, Wallet};
67//! use bark::persist::sqlite::SqliteClient;
68//!
69//! const MNEMONIC_FILE : &str = "mnemonic";
70//! const DB_FILE: &str = "db.sqlite";
71//!
72//! #[tokio::main]
73//! async fn main() {
74//! 	// Pick the bitcoin network that will be used
75//! 	let network = bitcoin::Network::Signet;
76//!
77//! 	// Configure the wallet
78//! 	let config = Config {
79//! 		server_address: String::from("https://ark.signet.2nd.dev"),
80//! 		esplora_address: Some(String::from("https://esplora.signet.2nd.dev")),
81//! 		..Config::network_default(network)
82//! 	};
83//!
84//!
85//! 	// Create a sqlite database
86//! 	let datadir = PathBuf::from("./bark");
87//! 	let db = Arc::new(SqliteClient::open(datadir.join(DB_FILE)).unwrap());
88//!
89//! 	// Generate and seed and store it somewhere
90//! 	let mnemonic = bip39::Mnemonic::generate(12).expect("12 is valid");
91//! 	fs::write(datadir.join(MNEMONIC_FILE), mnemonic.to_string().as_bytes()).await.unwrap();
92//!
93//! 	let wallet = Wallet::create(
94//! 		&mnemonic,
95//! 		network,
96//! 		config,
97//! 		db,
98//! 		false
99//! 	).await.unwrap();
100//! }
101//! ```
102//!
103//! ## Opening an existing Ark wallet
104//!
105//! The [Wallet] can be opened again by providing the [bip39::Mnemonic] and
106//! the [BarkPersister] again. Note, that [SqliteClient] implements the [BarkPersister]-trait.
107//!
108//! ```no_run
109//! # use std::sync::Arc;
110//! # use std::path::PathBuf;
111//! # use std::str::FromStr;
112//! #
113//! # use bip39;
114//! # use tokio::fs;
115//! #
116//! # use bark::{Config, Wallet};
117//! # use bark::persist::sqlite::SqliteClient;
118//! #
119//! const MNEMONIC_FILE : &str = "mnemonic";
120//! const DB_FILE: &str = "db.sqlite";
121//!
122//! #[tokio::main]
123//! async fn main() {
124//! 	let datadir = PathBuf::from("./bark");
125//! 	let config = Config {
126//! 		server_address: String::from("https://ark.signet.2nd.dev"),
127//! 		esplora_address: Some(String::from("https://esplora.signet.2nd.dev")),
128//! 		..Config::network_default(bitcoin::Network::Signet)
129//! 	};
130//!
131//! 	let db = Arc::new(SqliteClient::open(datadir.join(DB_FILE)).unwrap());
132//! 	let mnemonic_str = fs::read_to_string(datadir.join(DB_FILE)).await.unwrap();
133//! 	let mnemonic = bip39::Mnemonic::from_str(&mnemonic_str).unwrap();
134//! 	let wallet = Wallet::open(&mnemonic, db, config).await.unwrap();
135//! }
136//! ```
137//!
138//! ## Receiving coins
139//!
140//! For the time being we haven't implemented an Ark address type (yet). You
141//! can send funds directly to a public key.
142//!
143//! If you are on signet and your Ark server is [https://ark.signet.2nd.dev](https://ark.signet.2nd.dev),
144//! you can request some sats from our [faucet](https://signet.2nd.dev).
145//!
146//! ```no_run
147//! # use std::sync::Arc;
148//! # use std::str::FromStr;
149//! # use std::path::PathBuf;
150//! #
151//! # use tokio::fs;
152//! #
153//! # use bark::{Config, Wallet};
154//! # use bark::persist::sqlite::SqliteClient;
155//! #
156//! # const MNEMONIC_FILE : &str = "mnemonic";
157//! # const DB_FILE: &str = "db.sqlite";
158//! #
159//! # async fn get_wallet() -> Wallet {
160//! 	#   let datadir = PathBuf::from("./bark");
161//! 	#   let config = Config::network_default(bitcoin::Network::Signet);
162//! 	#
163//! 	#   let db = Arc::new(SqliteClient::open(datadir.join(DB_FILE)).unwrap());
164//! 	#   let mnemonic_str = fs::read_to_string(datadir.join(DB_FILE)).await.unwrap();
165//! 	#   let mnemonic = bip39::Mnemonic::from_str(&mnemonic_str).unwrap();
166//! 	#   Wallet::open(&mnemonic, db, config).await.unwrap()
167//! 	# }
168//! #
169//!
170//! #[tokio::main]
171//! async fn main() -> anyhow::Result<()> {
172//! 	let wallet = get_wallet().await;
173//! 	let address: ark::Address = wallet.new_address().await?;
174//! 	Ok(())
175//! }
176//! ```
177//!
178//! ## Inspecting the wallet
179//!
180//! An Ark wallet contains [ark::Vtxo]s. These are just like normal utxos
181//! in a bitcoin wallet. They just haven't been confirmed on chain (yet).
182//! However, the user remains in full control of the funds and can perform
183//! a unilateral exit at any time.
184//!
185//! The snippet below shows how you can inspect your [WalletVtxo]s.
186//!
187//! ```no_run
188//! # use std::sync::Arc;
189//! # use std::str::FromStr;
190//! # use std::path::PathBuf;
191//! #
192//! # use tokio::fs;
193//! #
194//! # use bark::{Config, Wallet};
195//! # use bark::persist::sqlite::SqliteClient;
196//! #
197//! # const MNEMONIC_FILE : &str = "mnemonic";
198//! # const DB_FILE: &str = "db.sqlite";
199//! #
200//! # async fn get_wallet() -> Wallet {
201//! 	#   let datadir = PathBuf::from("./bark");
202//! 	#
203//! 	#   let db = Arc::new(SqliteClient::open(datadir.join(DB_FILE)).unwrap());
204//! 	#   let mnemonic_str = fs::read_to_string(datadir.join(DB_FILE)).await.unwrap();
205//! 	#   let mnemonic = bip39::Mnemonic::from_str(&mnemonic_str).unwrap();
206//! 	#
207//! 	#   let config = Config::network_default(bitcoin::Network::Signet);
208//! 	#
209//! 	#   Wallet::open(&mnemonic, db, config).await.unwrap()
210//! 	# }
211//! #
212//!
213//! #[tokio::main]
214//! async fn main() -> anyhow::Result<()> {
215//! 	let mut wallet = get_wallet().await;
216//!
217//! 	// The vtxo's command doesn't sync your wallet
218//! 	// Make sure your app is synced before inspecting the wallet
219//! 	wallet.sync().await;
220//!
221//! 	let vtxos: Vec<bark::WalletVtxo> = wallet.vtxos().await.unwrap();
222//! 	Ok(())
223//! }
224//! ```
225//!
226//! Use [Wallet::balance] if you are only interested in the balance.
227//!
228//! ## Participating in a round
229//!
230//! You can participate in a round to refresh your coins. Typically,
231//! you want to refresh coins which are soon to expire or you might
232//! want to aggregate multiple small vtxos to keep the cost of exit
233//! under control.
234//!
235//! As a wallet developer you can implement your own refresh strategy.
236//! This gives you full control over which [ark::Vtxo]s are refreshed and
237//! which aren't.
238//!
239//! This example uses [RefreshStrategy::must_refresh] which is a sane
240//! default that selects all [ark::Vtxo]s that must be refreshed.
241//!
242//! ```no_run
243//! # use std::sync::Arc;
244//! # use std::str::FromStr;
245//! # use std::path::PathBuf;
246//! #
247//! # use tokio::fs;
248//! #
249//! # use bark::{Config, Wallet};
250//! # use bark::persist::sqlite::SqliteClient;
251//! #
252//! # const MNEMONIC_FILE : &str = "mnemonic";
253//! # const DB_FILE: &str = "db.sqlite";
254//! #
255//! # async fn get_wallet() -> Wallet {
256//! 	#   let datadir = PathBuf::from("./bark");
257//! 	#
258//! 	#   let db = Arc::new(SqliteClient::open(datadir.join(DB_FILE)).unwrap());
259//! 	#   let mnemonic_str = fs::read_to_string(datadir.join(DB_FILE)).await.unwrap();
260//! 	#   let mnemonic = bip39::Mnemonic::from_str(&mnemonic_str).unwrap();
261//! 	#
262//! 	#   let config = Config::network_default(bitcoin::Network::Signet);
263//! 	#
264//! 	#   Wallet::open(&mnemonic, db, config).await.unwrap()
265//! 	# }
266//! #
267//! use bark::vtxo::RefreshStrategy;
268//!
269//! #[tokio::main]
270//! async fn main() -> anyhow::Result<()> {
271//! 	let wallet = get_wallet().await;
272//!
273//! 	// Select all vtxos that refresh soon
274//! 	let tip = wallet.chain.tip().await?;
275//! 	let fee_rate = wallet.chain.fee_rates().await.fast;
276//! 	let strategy = RefreshStrategy::must_refresh(&wallet, tip, fee_rate);
277//!
278//! 	let vtxos = wallet.spendable_vtxos_with(&strategy).await?;
279//!		wallet.refresh_vtxos(vtxos).await?;
280//! 	Ok(())
281//! }
282//! ```
283
284#[cfg(all(any(target_os = "android", target_os = "ios"), feature = "tls-native-roots"))]
285compile_error!("feature `tls-native-roots` can't be used on Android or iOS, use `tls-webpki-roots` instead");
286
287pub extern crate ark;
288
289pub extern crate bip39;
290pub extern crate lightning_invoice;
291pub extern crate lnurl as lnurllib;
292
293#[macro_use] extern crate anyhow;
294#[macro_use] extern crate async_trait;
295#[macro_use] extern crate serde;
296
297pub mod chain;
298pub mod exit;
299pub mod movement;
300pub mod onchain;
301pub mod persist;
302pub mod round;
303pub mod subsystem;
304pub mod vtxo;
305
306#[cfg(feature = "pid-lock")]
307pub mod pid_lock;
308
309mod arkoor;
310mod board;
311mod config;
312mod daemon;
313mod fees;
314mod lightning;
315mod mailbox;
316mod notification;
317mod offboard;
318#[cfg(feature = "socks5-proxy")]
319mod proxy;
320mod psbtext;
321mod utils;
322
323pub use self::arkoor::ArkoorCreateResult;
324pub use self::config::{BarkNetwork, Config};
325pub use self::daemon::DaemonHandle;
326pub use self::fees::FeeEstimate;
327pub use self::notification::{WalletNotification, NotificationStream};
328pub use self::vtxo::WalletVtxo;
329
330use std::collections::HashSet;
331use std::sync::Arc;
332
333use anyhow::{bail, Context};
334use bip39::Mnemonic;
335use bitcoin::{Amount, Network, OutPoint};
336use bitcoin::bip32::{self, ChildNumber, Fingerprint};
337use bitcoin::secp256k1::{self, Keypair, PublicKey};
338use log::{trace, info, warn, error};
339use tokio::sync::{Mutex, RwLock};
340
341use ark::lightning::PaymentHash;
342
343use ark::{ArkInfo, ProtocolEncoding, Vtxo, VtxoId, VtxoPolicy, VtxoRequest};
344use ark::address::VtxoDelivery;
345use ark::fees::{validate_and_subtract_fee_min_dust, VtxoFeeInfo};
346use ark::vtxo::{Full, PubkeyVtxoPolicy, VtxoRef};
347use ark::vtxo::policy::signing::VtxoSigner;
348use bitcoin_ext::{BlockHeight, P2TR_DUST, TxStatus};
349use server_rpc::{protos, ServerConnection};
350use server_rpc::client::{ConnectError, CreateEndpointError};
351
352use crate::chain::{ChainSource, ChainSourceSpec};
353use crate::exit::Exit;
354use crate::movement::{Movement, MovementStatus, PaymentMethod};
355use crate::movement::manager::MovementManager;
356use crate::movement::update::MovementUpdate;
357use crate::notification::NotificationDispatch;
358use crate::onchain::{ExitUnilaterally, PreparePsbt, SignPsbt, Utxo};
359use crate::onchain::DaemonizableOnchainWallet;
360use crate::persist::BarkPersister;
361use crate::persist::models::{PendingOffboard, RoundStateId, StoredRoundState, Unlocked};
362#[cfg(feature = "socks5-proxy")]
363use crate::proxy::proxy_for_url;
364use crate::round::{RoundParticipation, RoundStateLockIndex, RoundStatus};
365use crate::subsystem::{ArkoorMovement, RoundMovement};
366use crate::vtxo::{FilterVtxos, RefreshStrategy, VtxoFilter, VtxoState, VtxoStateKind};
367
368#[cfg(all(feature = "wasm-web", feature = "socks5-proxy"))]
369compile_error!("features `wasm-web` does not support feature `socks5-proxy");
370
371/// Derivation index for Bark usage
372const BARK_PURPOSE_INDEX: u32 = 350;
373/// Derivation index used to generate keypairs to sign VTXOs
374const VTXO_KEYS_INDEX: u32 = 0;
375/// Derivation index used to generate keypair for the mailbox
376const MAILBOX_KEY_INDEX: u32 = 1;
377/// Derivation index used to generate keypair for the recovery mailbox
378const RECOVERY_MAILBOX_KEY_INDEX: u32 = 2;
379const MISSING_SERVER_TRANSPORT_HELP: &str =
380	"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.";
381
382lazy_static::lazy_static! {
383	/// Global secp context.
384	static ref SECP: secp256k1::Secp256k1<secp256k1::All> = secp256k1::Secp256k1::new();
385}
386
387/// Log that the server public key has changed.
388///
389/// Recommends that the user perform an emergency exit to recover their
390/// funds on-chain, since a rotated server pubkey makes the original VTXO
391/// spend/exit conditions unreachable.
392fn log_server_pubkey_changed_error(expected: PublicKey, got: PublicKey) {
393	error!(
394	    "
395Server public key has changed!
396
397The Ark server's public key is different from the one stored when this
398wallet was created. This typically happens when:
399
400	- The server operator has rotated their keys
401	- You are connecting to a different server
402	- The server has been replaced
403
404For safety, this wallet will not connect to the server until you
405resolve this. You can recover your funds on-chain by doing an emergency exit.
406
407This will exit your VTXOs to on-chain Bitcoin without needing the server's cooperation.
408
409Expected: {expected}
410Got:      {got}")
411}
412
413/// Log that the server mailbox pubkey has changed.
414fn log_server_mailbox_pubkey_changed_error(expected: PublicKey, got: PublicKey) {
415	error!(
416	    "
417Server mailbox public key has changed!
418
419The Ark server's mailbox public key is different from the one stored when this
420wallet was created. This typically happens when:
421
422	- The server operator has rotated their keys
423	- You are connecting to a different server
424	- The server has been replaced
425
426For safety, this wallet will not connect to the server until you resolve this.
427
428Unlike a server pubkey change, your VTXOs are not at risk - the mailbox pubkey
429only affects address receive semantics. Any Ark addresses you previously
430shared will stop receiving new payments; you will need to share new addresses
431after reconnecting.
432
433Expected: {expected}
434Got:      {got}")
435}
436
437/// The detailled balance of a Lightning receive.
438#[derive(Debug, Clone)]
439pub struct LightningReceiveBalance {
440	/// Sum of all pending lightning invoices
441	pub total: Amount,
442	/// Sum of all invoices for which we received the HTLC VTXOs
443	pub claimable: Amount,
444}
445
446/// The different balances of a Bark wallet.
447#[derive(Debug, Clone)]
448pub struct Balance {
449	/// Coins that are spendable in the Ark, either in-round or out-of-round.
450	pub spendable: Amount,
451	/// Coins that are in the process of being sent over Lightning.
452	pub pending_lightning_send: Amount,
453	/// Coins that are in the process of being received over Lightning.
454	pub claimable_lightning_receive: Amount,
455	/// Coins locked in a round.
456	pub pending_in_round: Amount,
457	/// Coins that are in the process of unilaterally exiting the Ark.
458	/// None if exit subsystem was unavailable
459	pub pending_exit: Option<Amount>,
460	/// Coins that are pending sufficient confirmations from board transactions.
461	pub pending_board: Amount,
462}
463
464pub struct UtxoInfo {
465	pub outpoint: OutPoint,
466	pub amount: Amount,
467	pub confirmation_height: Option<u32>,
468}
469
470impl From<Utxo> for UtxoInfo {
471	fn from(value: Utxo) -> Self {
472		match value {
473			Utxo::Local(o) => UtxoInfo {
474				outpoint: o.outpoint,
475				amount: o.amount,
476				confirmation_height: o.confirmation_height,
477			},
478			Utxo::Exit(e) => UtxoInfo {
479				outpoint: e.vtxo.point(),
480				amount: e.vtxo.amount(),
481				confirmation_height: Some(e.height),
482			},
483		}
484	}
485}
486
487/// Represents an offchain balance structure consisting of available funds, pending amounts in
488/// unconfirmed rounds, and pending exits.
489pub struct OffchainBalance {
490	/// Funds currently available for use. This reflects the spendable balance.
491	pub available: Amount,
492	/// Funds that are pending in unconfirmed operational rounds.
493	pub pending_in_round: Amount,
494	/// Funds being unilaterally exited. These may require more onchain confirmations to become
495	/// available onchain.
496	pub pending_exit: Amount,
497}
498
499/// Read-only properties of the Bark wallet.
500#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
501pub struct WalletProperties {
502	/// The Bitcoin network to run Bark on.
503	///
504	/// Default value: signet.
505	pub network: Network,
506
507	/// The wallet fingerpint
508	///
509	/// Used on wallet loading to check mnemonic correctness
510	pub fingerprint: Fingerprint,
511
512	/// The server public key from the initial connection.
513	///
514	/// This is used to detect if the Ark server has been replaced,
515	/// which could indicate a malicious server. If the server pubkey
516	/// changes, the wallet will refuse to connect and warn the user
517	/// to perform an emergency exit.
518	pub server_pubkey: Option<PublicKey>,
519
520	/// The server's mailbox public key.
521	///
522	/// Stored so that Ark addresses can be generated without a live
523	/// connection to the Ark server. `None` indicates a wallet created
524	/// before this field was added; the value is populated on the first
525	/// successful handshake. If the key changes, the wallet refuses to
526	/// connect until the user resolves the rotation.
527	pub server_mailbox_pubkey: Option<PublicKey>,
528}
529
530/// Struct representing an extended private key derived from a
531/// wallet's seed, used to derive child VTXO keypairs
532///
533/// The VTXO seed is derived by applying a hardened derivation
534/// step at index 350 from the wallet's seed.
535pub struct WalletSeed {
536	master: bip32::Xpriv,
537	vtxo: bip32::Xpriv,
538}
539
540impl WalletSeed {
541	fn new(network: Network, seed: &[u8; 64]) -> Self {
542		let bark_path = [ChildNumber::from_hardened_idx(BARK_PURPOSE_INDEX).unwrap()];
543		let master = bip32::Xpriv::new_master(network, seed)
544			.expect("invalid seed")
545			.derive_priv(&SECP, &bark_path)
546			.expect("purpose is valid");
547
548		let vtxo_path = [ChildNumber::from_hardened_idx(VTXO_KEYS_INDEX).unwrap()];
549		let vtxo = master.derive_priv(&SECP, &vtxo_path)
550			.expect("vtxo path is valid");
551
552		Self { master, vtxo }
553	}
554
555	fn fingerprint(&self) -> Fingerprint {
556		self.master.fingerprint(&SECP)
557	}
558
559	fn derive_vtxo_keypair(&self, idx: u32) -> Keypair {
560		self.vtxo.derive_priv(&SECP, &[idx.into()]).unwrap().to_keypair(&SECP)
561	}
562
563	fn to_mailbox_keypair(&self) -> Keypair {
564		let mailbox_path = [ChildNumber::from_hardened_idx(MAILBOX_KEY_INDEX).unwrap()];
565		self.master.derive_priv(&SECP, &mailbox_path).unwrap().to_keypair(&SECP)
566	}
567
568	fn to_recovery_mailbox_keypair(&self) -> Keypair {
569		let mailbox_path = [ChildNumber::from_hardened_idx(RECOVERY_MAILBOX_KEY_INDEX).unwrap()];
570		self.master.derive_priv(&SECP, &mailbox_path).unwrap().to_keypair(&SECP)
571	}
572}
573
574/// The central entry point for using this library as an Ark wallet.
575///
576/// Overview
577/// - Wallet encapsulates the complete Ark client implementation:
578///   - address generation (Ark addresses/keys)
579///     - [Wallet::new_address],
580///     - [Wallet::new_address_with_index],
581///     - [Wallet::peek_address],
582///     - [Wallet::validate_arkoor_address]
583///   - boarding onchain funds into Ark from an onchain wallet (see [onchain::OnchainWallet])
584///     - [Wallet::board_amount],
585///     - [Wallet::board_all]
586///   - offboarding Ark funds to move them back onchain
587///     - [Wallet::offboard_vtxos],
588///     - [Wallet::offboard_all]
589///   - sending and receiving Ark payments (including to BOLT11/BOLT12 invoices)
590///     - [Wallet::send_arkoor_payment],
591///     - [Wallet::pay_lightning_invoice],
592///     - [Wallet::pay_lightning_address],
593///     - [Wallet::pay_lightning_offer]
594///   - tracking, selecting, and refreshing VTXOs
595///     - [Wallet::vtxos],
596///     - [Wallet::vtxos_with],
597///     - [Wallet::refresh_vtxos]
598///   - syncing with the Ark server, unilateral exits and performing general maintenance
599///     - [Wallet::maintenance]: Syncs everything offchain-related and refreshes VTXOs where
600///       necessary,
601///     - [Wallet::maintenance_with_onchain]: The same as [Wallet::maintenance] but also syncs the
602///       onchain wallet and unilateral exits,
603///     - [Wallet::maintenance_refresh]: Refreshes VTXOs where necessary without syncing anything,
604///     - [Wallet::sync]: Syncs network fee-rates, ark rounds and arkoor payments,
605///     - [Wallet::sync_exits]: Updates the status of unilateral exits,
606///     - [Wallet::sync_pending_lightning_send_vtxos]: Updates the status of pending lightning payments,
607///     - [Wallet::try_claim_all_lightning_receives]: Wait for payment receipt of all open invoices, then claim them,
608///     - [Wallet::sync_pending_boards]: Registers boards which are available for use
609///       in offchain payments
610///
611/// Key capabilities
612/// - Address management:
613///   - derive and peek deterministic Ark addresses and their indices
614/// - Funds lifecycle:
615///   - board funds from an external onchain wallet onto the Ark
616///   - send out-of-round Ark payments (arkoor)
617///   - offboard funds to onchain addresses
618///   - manage HTLCs and Lightning receives/sends
619/// - VTXO management:
620///   - query spendable and pending VTXOs
621///   - refresh expiring or risky VTXOs
622///   - compute balance broken down by spendable/pending states
623/// - Synchronization and maintenance:
624///   - sync against the Ark server and the onchain source
625///   - reconcile pending rounds, exits, and offchain state
626///   - periodic maintenance helpers (e.g., auto-register boards, refresh policies)
627///
628/// Construction and persistence
629/// - A [Wallet] is opened or created using a mnemonic and a backend implementing [BarkPersister].
630///   - [Wallet::create],
631///   - [Wallet::open]
632/// - Creation allows the use of an optional onchain wallet for boarding and [Exit] functionality.
633///   It also initializes any internal state and connects to the [chain::ChainSource]. See
634///   [onchain::OnchainWallet] for an implementation of an onchain wallet using BDK.
635///   - [Wallet::create_with_onchain],
636///   - [Wallet::open_with_daemon]
637///
638/// Example
639/// ```
640/// # #[cfg(any(test, doc))]
641/// # async fn demo() -> anyhow::Result<()> {
642/// # use std::sync::Arc;
643/// # use bark::{Config, Wallet};
644/// # use bark::onchain::OnchainWallet;
645/// # use bark::persist::{BarkPersister, SqliteClient};
646/// # use bark::persist::sqlite::helpers::in_memory_db;
647/// # use bip39::Mnemonic;
648/// # use bitcoin::Network;
649/// # let (db_path, _) = in_memory_db();
650/// let network = Network::Signet;
651/// let mnemonic = Mnemonic::generate(12)?;
652/// let cfg = Config {
653///   server_address: String::from("https://ark.signet.2nd.dev"),
654///   esplora_address: Some(String::from("https://esplora.signet.2nd.dev")),
655///   ..Default::default()
656/// };
657///
658/// // You can either use the included SQLite implementation or create your own.
659/// let persister = SqliteClient::open(db_path).await?;
660/// let db: Arc<dyn BarkPersister> = Arc::new(persister);
661///
662/// // Load or create an onchain wallet if needed
663/// let onchain_wallet = OnchainWallet::load_or_create(network, mnemonic.to_seed(""), db.clone()).await?;
664///
665/// // Create or open the Ark wallet
666/// let mut wallet = Wallet::create_with_onchain(
667/// 	&mnemonic,
668/// 	network,
669/// 	cfg.clone(),
670/// 	db,
671/// 	&onchain_wallet,
672/// 	false,
673/// ).await?;
674/// // let mut wallet = Wallet::create(&mnemonic, network, cfg.clone(), db.clone(), false).await?;
675/// // let mut wallet = Wallet::open(&mnemonic, db.clone(), cfg.clone()).await?;
676/// // let mut wallet = Wallet::open_with_onchain(
677/// //    &mnemonic, network, cfg.clone(), db.clone(), &onchain_wallet
678/// // ).await?;
679///
680/// // There are two main ways to update the wallet, the primary is to use one of the maintenance
681/// // commands which will sync everything, refresh VTXOs and reconcile pending lightning payments.
682/// wallet.maintenance().await?;
683/// wallet.maintenance_with_onchain(&mut onchain_wallet).await?;
684///
685/// // Alternatively, you can use the fine-grained sync commands to sync individual parts of the
686/// // wallet state and use `maintenance_refresh` where necessary to refresh VTXOs.
687/// wallet.sync().await?;
688/// wallet.sync_pending_lightning_send_vtxos().await?;
689/// wallet.register_all_confirmed_boards(&mut onchain_wallet).await?;
690/// wallet.sync_exits(&mut onchain_wallet).await?;
691/// wallet.maintenance_refresh().await?;
692///
693/// // Generate a new Ark address to receive funds via arkoor
694/// let addr = wallet.new_address().await?;
695///
696/// // Query balance and VTXOs
697/// let balance = wallet.balance()?;
698/// let vtxos = wallet.vtxos()?;
699///
700/// // Progress any unilateral exits, make sure to sync first
701/// wallet.exit.progress_exit(&mut onchain_wallet, None).await?;
702///
703/// # Ok(())
704/// # }
705/// ```
706pub struct Wallet {
707	/// The chain source the wallet is connected to
708	pub chain: Arc<ChainSource>,
709
710	/// Exit subsystem handling unilateral exits and on-chain reconciliation outside Ark rounds.
711	pub exit: RwLock<Exit>,
712
713	/// Allows easy creation of and management of wallet fund movements.
714	pub movements: Arc<MovementManager>,
715
716	/// Dispatch for wallet notifications
717	notifications: NotificationDispatch,
718
719	/// Active runtime configuration for networking, fees, policies and thresholds.
720	config: Config,
721
722	/// Persistence backend for wallet state (keys metadata, VTXOs, movements, round state, etc.).
723	db: Arc<dyn BarkPersister>,
724
725	/// Deterministic seed material used to generate wallet keypairs.
726	seed: WalletSeed,
727
728	/// Optional live connection to an Ark server for round participation and synchronization.
729	server: parking_lot::RwLock<Option<ServerConnection>>,
730
731	/// Tracks payment hashes of lightning payments currently being processed.
732	/// Used to prevent concurrent payment attempts for the same invoice.
733	inflight_lightning_payments: Mutex<HashSet<PaymentHash>>,
734
735	/// Index of round states that are currently locked.
736	round_state_lock_index: RoundStateLockIndex,
737
738	/// A handle to the currently running daemon, if any.
739	daemon: parking_lot::Mutex<Option<DaemonHandle>>,
740}
741
742impl Wallet {
743	/// Creates a [chain::ChainSource] instance to communicate with an onchain backend from the
744	/// given [Config].
745	pub fn chain_source(
746		config: &Config,
747	) -> anyhow::Result<ChainSourceSpec> {
748		if let Some(ref url) = config.esplora_address {
749			Ok(ChainSourceSpec::Esplora {
750				url: url.clone(),
751			})
752		} else if let Some(ref url) = config.bitcoind_address {
753			let auth = if let Some(ref c) = config.bitcoind_cookiefile {
754				bitcoin_ext::rpc::Auth::CookieFile(c.clone())
755			} else {
756				bitcoin_ext::rpc::Auth::UserPass(
757					config.bitcoind_user.clone().context("need bitcoind auth config")?,
758					config.bitcoind_pass.clone().context("need bitcoind auth config")?,
759				)
760			};
761			Ok(ChainSourceSpec::Bitcoind {
762				url: url.clone(),
763				auth,
764			})
765		} else {
766			bail!("Need to either provide esplora or bitcoind info");
767		}
768	}
769
770	/// Verifies that the bark [Wallet] can be used with the configured [chain::ChainSource].
771	/// More specifically, if the [chain::ChainSource] connects to Bitcoin Core it must be
772	/// a high enough version to support ephemeral anchors.
773	pub fn require_chainsource_version(&self) -> anyhow::Result<()> {
774		self.chain.require_version()
775	}
776
777	pub async fn network(&self) -> anyhow::Result<Network> {
778		Ok(self.properties().await?.network)
779	}
780
781	/// Peek at the keypair directly after currently last revealed one,
782	/// together with its index, without storing it.
783	pub async fn peek_next_keypair(&self) -> anyhow::Result<(Keypair, u32)> {
784		let last_revealed = self.db.get_last_vtxo_key_index().await?;
785
786		let index = last_revealed.map(|i| i + 1).unwrap_or(u32::MIN);
787		let keypair = self.seed.derive_vtxo_keypair(index);
788
789		Ok((keypair, index))
790	}
791
792	/// Derive and store the keypair directly after currently last revealed one,
793	/// together with its index.
794	pub async fn derive_store_next_keypair(&self) -> anyhow::Result<(Keypair, u32)> {
795		let (keypair, index) = self.peek_next_keypair().await?;
796		self.db.store_vtxo_key(index, keypair.public_key()).await?;
797		Ok((keypair, index))
798	}
799
800	#[deprecated(note = "use peek_keypair instead")]
801	pub async fn peak_keypair(&self, index: u32) -> anyhow::Result<Keypair> {
802		self.peek_keypair(index).await
803	}
804
805	/// Retrieves a keypair based on the provided index and checks if the corresponding public key
806	/// exists in the [Vtxo] database.
807	///
808	/// # Arguments
809	///
810	/// * `index` - The index used to derive a keypair.
811	///
812	/// # Returns
813	///
814	/// * `Ok(Keypair)` - If the keypair is successfully derived and its public key exists in the
815	///   database.
816	/// * `Err(anyhow::Error)` - If the public key does not exist in the database or if an error
817	///   occurs during the database query.
818	pub async fn peek_keypair(&self, index: u32) -> anyhow::Result<Keypair> {
819		let keypair = self.seed.derive_vtxo_keypair(index);
820		if self.db.get_public_key_idx(&keypair.public_key()).await?.is_some() {
821			Ok(keypair)
822		} else {
823			bail!("VTXO key {} does not exist, please derive it first", index)
824		}
825	}
826
827
828	/// Retrieves the [Keypair] for a provided [PublicKey]
829	///
830	/// # Arguments
831	///
832	/// * `public_key` - The public key for which the keypair must be found
833	///
834	/// # Returns
835	/// * `Ok(Some(u32, Keypair))` - If the pubkey is found, the derivation-index and keypair are
836	///                              returned
837	/// * `Ok(None)` - If the pubkey cannot be found in the database
838	/// * `Err(anyhow::Error)` - If an error occurred related to the database query
839	pub async fn pubkey_keypair(&self, public_key: &PublicKey) -> anyhow::Result<Option<(u32, Keypair)>> {
840		if let Some(index) = self.db.get_public_key_idx(&public_key).await? {
841			Ok(Some((index, self.seed.derive_vtxo_keypair(index))))
842		} else {
843			Ok(None)
844		}
845	}
846
847	/// Retrieves the [Keypair] for a provided [Vtxo]
848	///
849	/// # Arguments
850	///
851	/// * `vtxo` - The vtxo for which the key must be found
852	///
853	/// # Returns
854	/// * `Ok(Some(Keypair))` - If the pubkey is found, the keypair is returned
855	/// * `Err(anyhow::Error)` - If the corresponding public key doesn't exist
856	///   in the database or a database error occurred.
857	pub async fn get_vtxo_key(&self, vtxo: impl VtxoRef) -> anyhow::Result<Keypair> {
858		let wallet_vtxo = self.get_vtxo_by_id(vtxo.vtxo_id()).await?;
859		let pubkey = self.find_signable_clause(&wallet_vtxo.vtxo).await
860			.context("VTXO is not signable by wallet")?
861			.pubkey();
862		let idx = self.db.get_public_key_idx(&pubkey).await?
863			.context("VTXO key not found")?;
864		Ok(self.seed.derive_vtxo_keypair(idx))
865	}
866
867	#[deprecated(note = "use peek_address instead")]
868	pub async fn peak_address(&self, index: u32) -> anyhow::Result<ark::Address> {
869		self.peek_address(index).await
870	}
871
872	/// Peek for an [ark::Address] at the given key index.
873	///
874	/// May return an error if the address at the given index has not been derived yet.
875	pub async fn peek_address(&self, index: u32) -> anyhow::Result<ark::Address> {
876		let properties = self.properties().await?;
877		let network = properties.network;
878		let keypair = self.peek_keypair(index).await?;
879		let mailbox = self.mailbox_identifier();
880
881		let (server_pubkey, mailbox_pubkey) =
882			if let (Some(spk), Some(mpk)) = (properties.server_pubkey, properties.server_mailbox_pubkey) {
883				(spk, mpk)
884			} else {
885				let (_, ark_info) = self.require_server().await?;
886				(ark_info.server_pubkey, ark_info.mailbox_pubkey)
887			};
888
889		Ok(ark::Address::builder()
890			.testnet(network != bitcoin::Network::Bitcoin)
891			.server_pubkey(server_pubkey)
892			.pubkey_policy(keypair.public_key())
893			.mailbox(mailbox_pubkey, mailbox, &keypair)
894			.expect("Failed to assign mailbox")
895			.into_address().unwrap())
896	}
897
898	/// Generate a new [ark::Address] and returns the index of the key used to create it.
899	///
900	/// This derives and stores the keypair directly after currently last revealed one.
901	pub async fn new_address_with_index(&self) -> anyhow::Result<(ark::Address, u32)> {
902		let (_, index) = self.derive_store_next_keypair().await?;
903		let addr = self.peek_address(index).await?;
904		Ok((addr, index))
905	}
906
907	/// Generate a new mailbox [ark::Address].
908	pub async fn new_address(&self) -> anyhow::Result<ark::Address> {
909		let (addr, _) = self.new_address_with_index().await?;
910		Ok(addr)
911	}
912
913	/// Create a new wallet without an optional onchain backend. This will restrict features such as
914	/// boarding and unilateral exit.
915	///
916	/// The `force` flag will allow you to create the wallet even if a connection to the Ark server
917	/// cannot be established, it will not overwrite a wallet which has already been created.
918	pub async fn create(
919		mnemonic: &Mnemonic,
920		network: Network,
921		config: Config,
922		db: Arc<dyn BarkPersister>,
923		force: bool,
924	) -> anyhow::Result<Wallet> {
925		trace!("Config: {:?}", config);
926		if let Some(existing) = db.read_properties().await? {
927			trace!("Existing config: {:?}", existing);
928			bail!("cannot overwrite already existing config")
929		}
930
931		// Try to connect to the server and get its pubkey
932		let (server_pubkey, mailbox_pubkey) = if !force {
933			match Self::connect_to_server(&config, network).await {
934				Ok(conn) => {
935					let ark_info = conn.ark_info().await?;
936					(Some(ark_info.server_pubkey), Some(ark_info.mailbox_pubkey))
937				}
938				Err(err) => {
939					bail!("Failed to connect to provided server (if you are sure use the --force flag): {:#}", err);
940				}
941			}
942		} else {
943			(None, None)
944		};
945
946		let wallet_fingerprint = WalletSeed::new(network, &mnemonic.to_seed("")).fingerprint();
947		let properties = WalletProperties {
948			network,
949			fingerprint: wallet_fingerprint,
950			server_pubkey,
951			server_mailbox_pubkey: mailbox_pubkey,
952		};
953
954		// write the config to db
955		db.init_wallet(&properties).await.context("cannot init wallet in the database")?;
956		info!("Created wallet with fingerprint: {}", wallet_fingerprint);
957		if let Some(pk) = server_pubkey {
958			info!("Stored server pubkey: {}", pk);
959		}
960
961		// from then on we can open the wallet
962		let wallet = Wallet::open(&mnemonic, db, config).await.context("failed to open wallet")?;
963		wallet.require_chainsource_version()?;
964
965		Ok(wallet)
966	}
967
968	/// Create a new wallet with an onchain backend. This enables full Ark functionality. A default
969	/// implementation of an onchain wallet when the `onchain-bdk` feature is enabled. See
970	/// [onchain::OnchainWallet] for more details. Alternatively, implement [ExitUnilaterally] if
971	/// you have your own onchain wallet implementation.
972	///
973	/// The `force` flag will allow you to create the wallet even if a connection to the Ark server
974	/// cannot be established, it will not overwrite a wallet which has already been created.
975	pub async fn create_with_onchain(
976		mnemonic: &Mnemonic,
977		network: Network,
978		config: Config,
979		db: Arc<dyn BarkPersister>,
980		onchain: &dyn ExitUnilaterally,
981		force: bool,
982	) -> anyhow::Result<Wallet> {
983		let mut wallet = Wallet::create(mnemonic, network, config, db, force).await?;
984		wallet.exit.get_mut().load(onchain).await?;
985		Ok(wallet)
986	}
987
988	/// Loads the bark wallet from the given database ensuring the fingerprint remains consistent.
989	pub async fn open(
990		mnemonic: &Mnemonic,
991		db: Arc<dyn BarkPersister>,
992		config: Config,
993	) -> anyhow::Result<Wallet> {
994		let properties = db.read_properties().await?.context("Wallet is not initialised")?;
995
996		let seed = {
997			let seed = mnemonic.to_seed("");
998			WalletSeed::new(properties.network, &seed)
999		};
1000
1001		if properties.fingerprint != seed.fingerprint() {
1002			bail!("incorrect mnemonic")
1003		}
1004
1005		let chain_source = if let Some(ref url) = config.esplora_address {
1006			ChainSourceSpec::Esplora {
1007				url: url.clone(),
1008			}
1009		} else if let Some(ref url) = config.bitcoind_address {
1010			let auth = if let Some(ref c) = config.bitcoind_cookiefile {
1011				bitcoin_ext::rpc::Auth::CookieFile(c.clone())
1012			} else {
1013				bitcoin_ext::rpc::Auth::UserPass(
1014					config.bitcoind_user.clone().context("need bitcoind auth config")?,
1015					config.bitcoind_pass.clone().context("need bitcoind auth config")?,
1016				)
1017			};
1018			ChainSourceSpec::Bitcoind { url: url.clone(), auth }
1019		} else {
1020			bail!("Need to either provide esplora or bitcoind info");
1021		};
1022
1023		#[cfg(feature = "socks5-proxy")]
1024		let chain_proxy = proxy_for_url(&config.socks5_proxy, chain_source.url())?;
1025		let chain_source_client = ChainSource::new(
1026			chain_source, properties.network, config.fallback_fee_rate,
1027			#[cfg(feature = "socks5-proxy")] chain_proxy.as_deref(),
1028		).await?;
1029		let chain = Arc::new(chain_source_client);
1030
1031		let server = parking_lot::RwLock::new(None);
1032
1033		let notifications = NotificationDispatch::new();
1034		let movements = Arc::new(MovementManager::new(db.clone(), notifications.clone()));
1035		let exit = RwLock::new(Exit::new(db.clone(), chain.clone(), movements.clone()).await?);
1036
1037		Ok(Wallet {
1038			config, db, seed, exit, movements, notifications, server, chain,
1039			inflight_lightning_payments: Mutex::new(HashSet::new()),
1040			round_state_lock_index: RoundStateLockIndex::new(),
1041			daemon: parking_lot::Mutex::new(None),
1042		})
1043	}
1044
1045	/// Similar to [Wallet::open] however this also unilateral exits using the provided onchain
1046	/// wallet.
1047	pub async fn open_with_onchain(
1048		mnemonic: &Mnemonic,
1049		db: Arc<dyn BarkPersister>,
1050		onchain: &dyn ExitUnilaterally,
1051		cfg: Config,
1052	) -> anyhow::Result<Wallet> {
1053		let mut wallet = Wallet::open(mnemonic, db, cfg).await?;
1054		wallet.exit.get_mut().load(onchain).await?;
1055		Ok(wallet)
1056	}
1057
1058	/// Similar to [Wallet::open] however this also starts the daemon, optionally with an onchain
1059	/// wallet, and returns a handle to the daemon.
1060	pub async fn open_with_daemon(
1061		mnemonic: &Mnemonic,
1062		db: Arc<dyn BarkPersister>,
1063		cfg: Config,
1064		onchain: Option<Arc<RwLock<dyn DaemonizableOnchainWallet>>>,
1065	) -> anyhow::Result<Arc<Wallet>> {
1066		let wallet = Arc::new(Wallet::open(mnemonic, db, cfg).await?);
1067		if let Some(onchain) = onchain.as_ref() {
1068			let mut onchain = onchain.write().await;
1069			wallet.exit.write().await.load(&mut *onchain).await?;
1070		}
1071
1072		wallet.clone().start_daemon(onchain)?;
1073
1074		Ok(wallet)
1075	}
1076
1077	/// Returns the config used to create/load the bark [Wallet].
1078	pub fn config(&self) -> &Config {
1079		&self.config
1080	}
1081
1082	/// Retrieves the [WalletProperties] of the current bark [Wallet].
1083	pub async fn properties(&self) -> anyhow::Result<WalletProperties> {
1084		let properties = self.db.read_properties().await?.context("Wallet is not initialised")?;
1085		Ok(properties)
1086	}
1087
1088	/// Returns the fingerprint of the wallet.
1089	pub fn fingerprint(&self) -> Fingerprint {
1090		self.seed.fingerprint()
1091	}
1092
1093	async fn connect_to_server(
1094		config: &Config,
1095		network: Network,
1096	) -> anyhow::Result<ServerConnection> {
1097		let mut builder = ServerConnection::builder()
1098			.address(&config.server_address)
1099			.network(network);
1100
1101		#[cfg(feature = "socks5-proxy")]
1102		if let Some(proxy) = proxy_for_url(&config.socks5_proxy, &config.server_address)? {
1103			builder = builder.proxy(&proxy)
1104		}
1105
1106		if let Some(ref token) = config.server_access_token {
1107			builder = builder.access_token(token);
1108		}
1109
1110		builder.connect().await.map_err(wrap_server_connect_error)
1111			.context("Failed to connect to Ark server")
1112	}
1113
1114	async fn require_server(&self) -> anyhow::Result<(ServerConnection, ArkInfo)> {
1115		// Connect lazily if not yet connected.
1116		if self.server.read().is_none() {
1117			let network = self.properties().await?.network;
1118			let conn = Self::connect_to_server(&self.config, network).await
1119				.context("You should be connected to Ark server to perform this action")?;
1120			let _ = self.server.write().insert(conn);
1121		}
1122
1123		let conn = self.server.read().clone()
1124			.context("You should be connected to Ark server to perform this action")?;
1125		let ark_info = conn.ark_info().await?;
1126
1127		self.check_and_store_server_keys(&ark_info).await?;
1128
1129		Ok((conn, ark_info))
1130	}
1131
1132	pub async fn refresh_server(&self) -> anyhow::Result<()> {
1133		let server = self.server.read().clone();
1134
1135		let srv = if let Some(srv) = server {
1136			srv.check_connection().await?;
1137			let ark_info = srv.ark_info().await?;
1138			ark_info.fees.validate().context("invalid fee schedule")?;
1139			self.check_and_store_server_keys(&ark_info).await?;
1140			srv
1141		} else {
1142			let properties = self.properties().await?;
1143			let conn = Self::connect_to_server(&self.config, properties.network).await?;
1144			let ark_info = conn.ark_info().await?;
1145			ark_info.fees.validate().context("invalid fee schedule")?;
1146			self.check_and_store_server_keys(&ark_info).await?;
1147			conn
1148		};
1149
1150		let _ = self.server.write().insert(srv);
1151
1152		Ok(())
1153	}
1154
1155	/// Validate that the server's public keys match what we have stored,
1156	/// and persist them if this is the first time connecting after an upgrade.
1157	///
1158	/// Returns an error (via `bail!`) if either the server pubkey or mailbox
1159	/// pubkey differs from the stored value; callers must not proceed with
1160	/// server operations on error.
1161	async fn check_and_store_server_keys(&self, ark_info: &ArkInfo) -> anyhow::Result<()> {
1162		let properties = self.properties().await?;
1163
1164		if let Some(stored_pubkey) = properties.server_pubkey {
1165			if stored_pubkey != ark_info.server_pubkey {
1166				log_server_pubkey_changed_error(stored_pubkey, ark_info.server_pubkey);
1167				bail!("Server public key has changed. You should exit all your VTXOs!");
1168			}
1169		} else {
1170			self.db.set_server_pubkey(ark_info.server_pubkey).await?;
1171			info!("Stored server pubkey for existing wallet: {}", ark_info.server_pubkey);
1172		}
1173
1174		if let Some(stored_mailbox_pubkey) = properties.server_mailbox_pubkey {
1175			if stored_mailbox_pubkey != ark_info.mailbox_pubkey {
1176				log_server_mailbox_pubkey_changed_error(stored_mailbox_pubkey, ark_info.mailbox_pubkey);
1177				bail!("Server mailbox public key has changed.");
1178			}
1179		} else {
1180			self.db.set_server_mailbox_pubkey(ark_info.mailbox_pubkey).await?;
1181			info!("Stored server mailbox pubkey for existing wallet: {}", ark_info.mailbox_pubkey);
1182		}
1183
1184		Ok(())
1185	}
1186
1187	/// Return [ArkInfo] fetched on last handshake with the Ark server
1188	pub async fn ark_info(&self) -> anyhow::Result<Option<ArkInfo>> {
1189		let server = self.server.read().clone();
1190		match server.as_ref() {
1191			Some(srv) => Ok(Some(srv.ark_info().await?)),
1192			_ => Ok(None),
1193		}
1194	}
1195
1196	/// Return [ArkInfo], connecting lazily if not yet connected.
1197	///
1198	/// Errors if the server cannot be reached or if the server's pubkey
1199	/// or mailbox pubkey no longer matches what was stored at wallet
1200	/// creation.
1201	pub async fn require_ark_info(&self) -> anyhow::Result<ArkInfo> {
1202		let (_, ark_info) = self.require_server().await?;
1203		Ok(ark_info)
1204	}
1205
1206	/// Return the [Balance] of the wallet.
1207	///
1208	/// Make sure you sync before calling this method.
1209	pub async fn balance(&self) -> anyhow::Result<Balance> {
1210		let vtxos = self.vtxos().await?;
1211
1212		let spendable = {
1213			let mut v = vtxos.iter().collect();
1214			VtxoStateKind::Spendable.filter_vtxos(&mut v).await?;
1215			v.into_iter().map(|v| v.amount()).sum::<Amount>()
1216		};
1217
1218		let pending_lightning_send = self.pending_lightning_send_vtxos().await?.iter()
1219			.map(|v| v.amount())
1220			.sum::<Amount>();
1221
1222		let claimable_lightning_receive = self.claimable_lightning_receive_balance().await?;
1223
1224		let pending_board = self.pending_board_vtxos().await?.iter()
1225			.map(|v| v.amount())
1226			.sum::<Amount>();
1227
1228		let pending_in_round = self.pending_round_balance().await?;
1229
1230		let pending_exit = self.exit.try_read().ok().map(|e| e.pending_total());
1231
1232		Ok(Balance {
1233			spendable,
1234			pending_in_round,
1235			pending_lightning_send,
1236			claimable_lightning_receive,
1237			pending_exit,
1238			pending_board,
1239		})
1240	}
1241
1242	/// Fetches [Vtxo]'s funding transaction and validates the VTXO against it.
1243	pub async fn validate_vtxo(&self, vtxo: &Vtxo<Full>) -> anyhow::Result<()> {
1244		let tx = self.chain.get_tx(&vtxo.chain_anchor().txid).await
1245			.context("could not fetch chain tx")?;
1246
1247		let tx = tx.with_context(|| {
1248			format!("vtxo chain anchor not found for vtxo: {}", vtxo.chain_anchor().txid)
1249		})?;
1250
1251		vtxo.validate(&tx)?;
1252
1253		Ok(())
1254	}
1255
1256	/// Manually import a VTXO into the wallet.
1257	///
1258	/// # Arguments
1259	/// * `vtxo` - The VTXO to import
1260	///
1261	/// # Errors
1262	/// Returns an error if:
1263	/// - The VTXO's chain anchor is not found or invalid
1264	/// - The wallet doesn't own a signable clause for the VTXO
1265	pub async fn import_vtxo(&self, vtxo: &Vtxo<Full>) -> anyhow::Result<()> {
1266		if self.db.get_wallet_vtxo(vtxo.id()).await?.is_some() {
1267			info!("VTXO {} already exists in wallet, skipping import", vtxo.id());
1268			return Ok(());
1269		}
1270
1271		self.validate_vtxo(vtxo).await.context("VTXO validation failed")?;
1272
1273		if self.find_signable_clause(vtxo).await.is_none() {
1274			bail!("VTXO {} is not owned by this wallet (no signable clause found)", vtxo.id());
1275		}
1276
1277		let current_height = self.chain.tip().await?;
1278		if vtxo.expiry_height() <= current_height {
1279			bail!("Vtxo {} has expired", vtxo.id());
1280		}
1281
1282		self.store_spendable_vtxos([vtxo]).await.context("failed to store imported VTXO")?;
1283
1284		info!("Successfully imported VTXO {}", vtxo.id());
1285		Ok(())
1286	}
1287
1288	/// Retrieves the full state of a [Vtxo] for a given [VtxoId] if it exists in the database.
1289	pub async fn get_vtxo_by_id(&self, vtxo_id: VtxoId) -> anyhow::Result<WalletVtxo> {
1290		let vtxo = self.db.get_wallet_vtxo(vtxo_id).await
1291			.with_context(|| format!("Error when querying vtxo {} in database", vtxo_id))?
1292			.with_context(|| format!("The VTXO with id {} cannot be found", vtxo_id))?;
1293		Ok(vtxo)
1294	}
1295
1296	/// Fetches all movements ordered from newest to oldest.
1297	#[deprecated(since="0.1.0-beta.5", note = "Use Wallet::history instead")]
1298	pub async fn movements(&self) -> anyhow::Result<Vec<Movement>> {
1299		self.history().await
1300	}
1301
1302	/// Fetches all wallet fund movements ordered from newest to oldest.
1303	pub async fn history(&self) -> anyhow::Result<Vec<Movement>> {
1304		Ok(self.db.get_all_movements().await?)
1305	}
1306
1307	/// Query the wallet history by the given payment method
1308	pub async fn history_by_payment_method(
1309		&self,
1310		payment_method: &PaymentMethod,
1311	) -> anyhow::Result<Vec<Movement>> {
1312		let mut ret = self.db.get_movements_by_payment_method(payment_method).await?;
1313		ret.sort_by_key(|m| m.id);
1314		Ok(ret)
1315	}
1316
1317	/// Returns all VTXOs from the database.
1318	pub async fn all_vtxos(&self) -> anyhow::Result<Vec<WalletVtxo>> {
1319		Ok(self.db.get_all_vtxos().await?)
1320	}
1321
1322	/// Returns all not spent vtxos
1323	pub async fn vtxos(&self) -> anyhow::Result<Vec<WalletVtxo>> {
1324		Ok(self.db.get_vtxos_by_state(&VtxoStateKind::UNSPENT_STATES).await?)
1325	}
1326
1327	/// Returns all vtxos matching the provided predicate
1328	pub async fn vtxos_with(&self, filter: &impl FilterVtxos) -> anyhow::Result<Vec<WalletVtxo>> {
1329		let mut vtxos = self.vtxos().await?;
1330		filter.filter_vtxos(&mut vtxos).await.context("error filtering vtxos")?;
1331		Ok(vtxos)
1332	}
1333
1334	/// Returns all spendable vtxos
1335	pub async fn spendable_vtxos(&self) -> anyhow::Result<Vec<WalletVtxo>> {
1336		Ok(self.vtxos_with(&VtxoStateKind::Spendable).await?)
1337	}
1338
1339	/// Returns all spendable vtxos matching the provided predicate
1340	pub async fn spendable_vtxos_with(
1341		&self,
1342		filter: &impl FilterVtxos,
1343	) -> anyhow::Result<Vec<WalletVtxo>> {
1344		let mut vtxos = self.spendable_vtxos().await?;
1345		filter.filter_vtxos(&mut vtxos).await.context("error filtering vtxos")?;
1346		Ok(vtxos)
1347	}
1348
1349	/// Returns all vtxos that will expire within `threshold` blocks
1350	pub async fn get_expiring_vtxos(
1351		&self,
1352		threshold: BlockHeight,
1353	) -> anyhow::Result<Vec<WalletVtxo>> {
1354		let expiry = self.chain.tip().await? + threshold;
1355		let filter = VtxoFilter::new(&self).expires_before(expiry);
1356		Ok(self.spendable_vtxos_with(&filter).await?)
1357	}
1358
1359	/// Checks pending offboard transactions for confirmation status.
1360	///
1361	/// - On confirmation with enough confs (or mempool with 0 required confs): finalize as successful.
1362	/// - On `NotFound`: wait at least 1 hour before canceling, in case the chain backend is slow.
1363	/// - On error (e.g. network drop): log and keep waiting — don't cancel due to transient failures.
1364	pub async fn sync_pending_offboards(&self) -> anyhow::Result<()> {
1365		let pending_offboards: Vec<PendingOffboard> = self.db.get_pending_offboards().await?;
1366
1367		if pending_offboards.is_empty() {
1368			return Ok(());
1369		}
1370
1371		let current_height = self.chain.tip().await?;
1372		let required_confs = self.config.offboard_required_confirmations;
1373
1374		trace!("Checking {} pending offboard transaction(s)", pending_offboards.len());
1375
1376		for pending in pending_offboards {
1377			let status = self.chain.tx_status(pending.offboard_txid).await;
1378
1379			match status {
1380				Ok(TxStatus::Confirmed(block_ref)) => {
1381					let confs = current_height - (block_ref.height - 1);
1382					if confs < required_confs as BlockHeight {
1383						trace!(
1384							"Offboard tx {} has {}/{} confirmations, waiting...",
1385							pending.offboard_txid, confs, required_confs,
1386						);
1387						continue;
1388					}
1389
1390					info!(
1391						"Offboard tx {} confirmed, finalizing movement {}",
1392						pending.offboard_txid, pending.movement_id,
1393					);
1394
1395					// Mark VTXOs as spent
1396					for vtxo_id in &pending.vtxo_ids {
1397						if let Err(e) = self.db.update_vtxo_state_checked(
1398							*vtxo_id,
1399							VtxoState::Spent,
1400							&[VtxoStateKind::Locked],
1401						).await {
1402							warn!("Failed to mark vtxo {} as spent: {:#}", vtxo_id, e);
1403						}
1404					}
1405
1406					// Finish the movement as successful
1407					if let Err(e) = self.movements.finish_movement(
1408						pending.movement_id,
1409						MovementStatus::Successful,
1410					).await {
1411						warn!("Failed to finish movement {}: {:#}", pending.movement_id, e);
1412					}
1413
1414					self.db.remove_pending_offboard(pending.movement_id).await?;
1415				}
1416				Ok(TxStatus::Mempool) => {
1417					if required_confs == 0 {
1418						info!(
1419							"Offboard tx {} in mempool with 0 required confirmations, \
1420							finalizing movement {}",
1421							pending.offboard_txid, pending.movement_id,
1422						);
1423
1424						// Mark VTXOs as spent
1425						for vtxo_id in &pending.vtxo_ids {
1426							if let Err(e) = self.db.update_vtxo_state_checked(
1427								*vtxo_id,
1428								VtxoState::Spent,
1429								&[VtxoStateKind::Locked],
1430							).await {
1431								warn!("Failed to mark vtxo {} as spent: {:#}", vtxo_id, e);
1432							}
1433						}
1434
1435						// Finish the movement as successful
1436						if let Err(e) = self.movements.finish_movement(
1437							pending.movement_id,
1438							MovementStatus::Successful,
1439						).await {
1440							warn!("Failed to finish movement {}: {:#}", pending.movement_id, e);
1441						}
1442
1443						self.db.remove_pending_offboard(pending.movement_id).await?;
1444					} else {
1445						trace!(
1446							"Offboard tx {} still in mempool, waiting...",
1447							pending.offboard_txid,
1448						);
1449					}
1450				}
1451				Ok(TxStatus::NotFound) => {
1452					// Don't cancel immediately — the chain backend might be slow
1453					// or temporarily out of sync. Wait at least 1 hour before
1454					// treating the tx as truly lost.
1455					let age = chrono::Local::now() - pending.created_at;
1456					if age < chrono::Duration::hours(1) {
1457						trace!(
1458							"Offboard tx {} not found, but only {} minutes old — waiting...",
1459							pending.offboard_txid, age.num_minutes(),
1460						);
1461						continue;
1462					}
1463
1464					warn!(
1465						"Offboard tx {} not found after {} minutes, canceling movement {}",
1466						pending.offboard_txid, age.num_minutes(), pending.movement_id,
1467					);
1468
1469					// Restore VTXOs to spendable
1470					for vtxo_id in &pending.vtxo_ids {
1471						if let Err(e) = self.db.update_vtxo_state_checked(
1472							*vtxo_id,
1473							VtxoState::Spendable,
1474							&[VtxoStateKind::Locked],
1475						).await {
1476							warn!("Failed to restore vtxo {} to spendable: {:#}", vtxo_id, e);
1477						}
1478					}
1479
1480					// Finish the movement as failed
1481					if let Err(e) = self.movements.finish_movement(
1482						pending.movement_id,
1483						MovementStatus::Failed,
1484					).await {
1485						warn!("Failed to fail movement {}: {:#}", pending.movement_id, e);
1486					}
1487
1488					self.db.remove_pending_offboard(pending.movement_id).await?;
1489				}
1490				Err(e) => {
1491					warn!(
1492						"Failed to check status of offboard tx {}: {:#}",
1493						pending.offboard_txid, e,
1494					);
1495				}
1496			}
1497		}
1498
1499		Ok(())
1500	}
1501
1502	/// Performs maintenance tasks and performs refresh interactively until finished when needed.
1503	/// This risks spending users' funds because refreshing may cost fees.
1504	///
1505	/// This can take a long period of time due to syncing rounds, arkoors, checking pending
1506	/// payments, progressing pending rounds, and refreshing VTXOs if necessary.
1507	pub async fn maintenance(&self) -> anyhow::Result<()> {
1508		info!("Starting wallet maintenance in interactive mode");
1509		self.sync().await;
1510
1511		let rounds = self.progress_pending_rounds(None).await;
1512		if let Err(e) = rounds.as_ref() {
1513			warn!("Error progressing pending rounds: {:#}", e);
1514		}
1515		let refresh = self.maintenance_refresh().await;
1516		if let Err(e) = refresh.as_ref() {
1517			warn!("Error refreshing VTXOs: {:#}", e);
1518		}
1519		if rounds.is_err() || refresh.is_err() {
1520			bail!("Maintenance encountered errors.\nprogress_rounds: {:#?}\nrefresh: {:#?}", rounds, refresh);
1521		}
1522		Ok(())
1523	}
1524
1525	/// Performs maintenance tasks and schedules delegated refresh when needed. This risks spending
1526	/// users' funds because refreshing may cost fees.
1527	///
1528	/// This can take a long period of time due to syncing rounds, arkoors, checking pending
1529	/// payments, progressing pending rounds, and refreshing VTXOs if necessary.
1530	pub async fn maintenance_delegated(&self) -> anyhow::Result<()> {
1531		info!("Starting wallet maintenance in delegated mode");
1532		self.sync().await;
1533		let rounds = self.progress_pending_rounds(None).await;
1534		if let Err(e) = rounds.as_ref() {
1535			warn!("Error progressing pending rounds: {:#}", e);
1536		}
1537		let refresh = self.maybe_schedule_maintenance_refresh_delegated().await;
1538		if let Err(e) = refresh.as_ref() {
1539			warn!("Error refreshing VTXOs: {:#}", e);
1540		}
1541		if rounds.is_err() || refresh.is_err() {
1542			bail!("Delegated maintenance encountered errors.\nprogress_rounds: {:#?}\nrefresh: {:#?}", rounds, refresh);
1543		}
1544		Ok(())
1545	}
1546
1547	/// Performs maintenance tasks and performs refresh interactively until finished when needed.
1548	/// This risks spending users' funds because refreshing may cost fees and any pending exits will
1549	/// be progressed.
1550	///
1551	/// This can take a long period of time due to syncing the onchain wallet, registering boards,
1552	/// syncing rounds, arkoors, and the exit system, checking pending lightning payments and
1553	/// refreshing VTXOs if necessary.
1554	pub async fn maintenance_with_onchain<W: PreparePsbt + SignPsbt + ExitUnilaterally>(
1555		&self,
1556		onchain: &mut W,
1557	) -> anyhow::Result<()> {
1558		info!("Starting wallet maintenance in interactive mode with onchain wallet");
1559
1560		// Maintenance will log so we don't need to.
1561		let maintenance = self.maintenance().await;
1562
1563		// NB: order matters here, after syncing lightning, we might have new exits to start
1564		let exit_sync = self.sync_exits(onchain).await;
1565		if let Err(e) = exit_sync.as_ref() {
1566			warn!("Error syncing exits: {:#}", e);
1567		}
1568		let exit_progress = self.exit.write().await.progress_exits(&self, onchain, None).await;
1569		if let Err(e) = exit_progress.as_ref() {
1570			warn!("Error progressing exits: {:#}", e);
1571		}
1572		if maintenance.is_err() || exit_sync.is_err() || exit_progress.is_err() {
1573			bail!("Maintenance encountered errors.\nmaintenance: {:#?}\nexit_sync: {:#?}\nexit_progress: {:#?}", maintenance, exit_sync, exit_progress);
1574		}
1575		Ok(())
1576	}
1577
1578	/// Performs maintenance tasks and schedules delegated refresh when needed. This risks spending
1579	/// users' funds because refreshing may cost fees and any pending exits will be progressed.
1580	///
1581	/// This can take a long period of time due to syncing the onchain wallet, registering boards,
1582	/// syncing rounds, arkoors, and the exit system, checking pending lightning payments and
1583	/// refreshing VTXOs if necessary.
1584	pub async fn maintenance_with_onchain_delegated<W: PreparePsbt + SignPsbt + ExitUnilaterally>(
1585		&self,
1586		onchain: &mut W,
1587	) -> anyhow::Result<()> {
1588		info!("Starting wallet maintenance in delegated mode with onchain wallet");
1589
1590		// Maintenance will log so we don't need to.
1591		let maintenance = self.maintenance_delegated().await;
1592
1593		// NB: order matters here, after syncing lightning, we might have new exits to start
1594		let exit_sync = self.sync_exits(onchain).await;
1595		if let Err(e) = exit_sync.as_ref() {
1596			warn!("Error syncing exits: {:#}", e);
1597		}
1598		let exit_progress = self.exit.write().await.progress_exits(&self, onchain, None).await;
1599		if let Err(e) = exit_progress.as_ref() {
1600			warn!("Error progressing exits: {:#}", e);
1601		}
1602		if maintenance.is_err() || exit_sync.is_err() || exit_progress.is_err() {
1603			bail!("Delegated maintenance encountered errors.\nmaintenance: {:#?}\nexit_sync: {:#?}\nexit_progress: {:#?}", maintenance, exit_sync, exit_progress);
1604		}
1605		Ok(())
1606	}
1607
1608	/// Checks VTXOs that are due to be refreshed, and schedules an interactive refresh if any
1609	///
1610	/// This will include any VTXOs within the expiry threshold
1611	/// ([Config::vtxo_refresh_expiry_threshold]) or those which
1612	/// are uneconomical to exit due to onchain network conditions.
1613	///
1614	/// Returns a [RoundStateId] if a refresh is scheduled.
1615	pub async fn maybe_schedule_maintenance_refresh(&self) -> anyhow::Result<Option<RoundStateId>> {
1616		let vtxos = self.get_vtxos_to_refresh().await?;
1617		if vtxos.len() == 0 {
1618			return Ok(None);
1619		}
1620
1621		let participation = match self.build_refresh_participation(vtxos).await? {
1622			Some(participation) => participation,
1623			None => return Ok(None),
1624		};
1625
1626		info!("Scheduling maintenance refresh ({} vtxos)", participation.inputs.len());
1627		let state = self.join_next_round(participation, Some(RoundMovement::Refresh)).await?;
1628		Ok(Some(state.id()))
1629	}
1630
1631	/// Checks VTXOs that are due to be refreshed, and schedules a delegated refresh if any
1632	///
1633	/// This will include any VTXOs within the expiry threshold
1634	/// ([Config::vtxo_refresh_expiry_threshold]) or those which
1635	/// are uneconomical to exit due to onchain network conditions.
1636	///
1637	/// Returns a [RoundStateId] if a refresh is scheduled.
1638	pub async fn maybe_schedule_maintenance_refresh_delegated(
1639		&self,
1640	) -> anyhow::Result<Option<RoundStateId>> {
1641		let vtxos = self.get_vtxos_to_refresh().await?;
1642		if vtxos.len() == 0 {
1643			return Ok(None);
1644		}
1645
1646		let participation = match self.build_refresh_participation(vtxos).await? {
1647			Some(participation) => participation,
1648			None => return Ok(None),
1649		};
1650
1651		info!("Scheduling delegated maintenance refresh ({} vtxos)", participation.inputs.len());
1652		let state = self.join_next_round_delegated(participation, Some(RoundMovement::Refresh)).await?;
1653		Ok(Some(state.id()))
1654	}
1655
1656	/// Performs an interactive refresh of all VTXOs that are due to be refreshed, if any
1657	///
1658	/// This will include any VTXOs within the expiry threshold
1659	/// ([Config::vtxo_refresh_expiry_threshold]) or those which
1660	/// are uneconomical to exit due to onchain network conditions.
1661	///
1662	/// Returns a [RoundStatus] if a refresh occurs.
1663	pub async fn maintenance_refresh(&self) -> anyhow::Result<Option<RoundStatus>> {
1664		let vtxos = self.get_vtxos_to_refresh().await?;
1665		if vtxos.len() == 0 {
1666			return Ok(None);
1667		}
1668
1669		info!("Performing maintenance refresh");
1670		self.refresh_vtxos(vtxos).await
1671	}
1672
1673	/// Sync offchain wallet and update onchain fees. This is a much more lightweight alternative
1674	/// to [Wallet::maintenance] as it will not refresh VTXOs or sync the onchain wallet.
1675	///
1676	/// Notes:
1677	/// - The exit system will not be synced as doing so requires the onchain wallet.
1678	pub async fn sync(&self) {
1679		futures::join!(
1680			async {
1681				// NB: order matters here, if syncing call fails,
1682				// we still want to update the fee rates
1683				if let Err(e) = self.chain.update_fee_rates(self.config.fallback_fee_rate).await {
1684					warn!("Error updating fee rates: {:#}", e);
1685				}
1686			},
1687			async {
1688				if let Err(e) = self.sync_mailbox().await {
1689					warn!("Error in mailbox sync: {:#}", e);
1690				}
1691			},
1692			async {
1693				if let Err(e) = self.sync_pending_rounds().await {
1694					warn!("Error while trying to progress rounds awaiting confirmations: {:#}", e);
1695				}
1696			},
1697			async {
1698				if let Err(e) = self.sync_pending_lightning_send_vtxos().await {
1699					warn!("Error syncing pending lightning payments: {:#}", e);
1700				}
1701			},
1702			async {
1703				if let Err(e) = self.try_claim_all_lightning_receives(false).await {
1704					warn!("Error claiming pending lightning receives: {:#}", e);
1705				}
1706			},
1707			async {
1708				if let Err(e) = self.sync_pending_boards().await {
1709					warn!("Error syncing pending boards: {:#}", e);
1710				}
1711			},
1712			async {
1713				if let Err(e) = self.sync_pending_offboards().await {
1714					warn!("Error syncing pending offboards: {:#}", e);
1715				}
1716			}
1717		);
1718	}
1719
1720	/// Sync the transaction status of unilateral exits
1721	///
1722	/// This will not progress the unilateral exits in any way, it will merely check the
1723	/// transaction status of each transaction as well as check whether any exits have become
1724	/// claimable or have been claimed.
1725	pub async fn sync_exits(
1726		&self,
1727		onchain: &mut dyn ExitUnilaterally,
1728	) -> anyhow::Result<()> {
1729		self.exit.write().await.sync(&self, onchain).await?;
1730		Ok(())
1731	}
1732
1733	/// Drop a specific [Vtxo] from the database. This is destructive and will result in a loss of
1734	/// funds.
1735	pub async fn dangerous_drop_vtxo(&self, vtxo_id: VtxoId) -> anyhow::Result<()> {
1736		warn!("Drop vtxo {} from the database", vtxo_id);
1737		self.db.remove_vtxo(vtxo_id).await?;
1738		Ok(())
1739	}
1740
1741	/// Drop all VTXOs from the database. This is destructive and will result in a loss of funds.
1742	//TODO(stevenroose) improve the way we expose dangerous methods
1743	pub async fn dangerous_drop_all_vtxos(&self) -> anyhow::Result<()> {
1744		warn!("Dropping all vtxos from the db...");
1745		for vtxo in self.vtxos().await? {
1746			self.db.remove_vtxo(vtxo.id()).await?;
1747		}
1748
1749		self.exit.write().await.dangerous_clear_exit().await?;
1750		Ok(())
1751	}
1752
1753	/// Checks if the provided VTXO has some counterparty risk in the current wallet
1754	///
1755	/// An arkoor vtxo is considered to have some counterparty risk
1756	/// if it is (directly or not) based on round VTXOs that aren't owned by the wallet
1757	async fn has_counterparty_risk(&self, vtxo: &Vtxo<Full>) -> anyhow::Result<bool> {
1758		for past_pks in vtxo.past_arkoor_pubkeys() {
1759			let mut owns_any = false;
1760			for past_pk in past_pks {
1761				if self.db.get_public_key_idx(&past_pk).await?.is_some() {
1762					owns_any = true;
1763					break;
1764				}
1765			}
1766			if !owns_any {
1767				return Ok(true);
1768			}
1769		}
1770
1771		let my_clause = self.find_signable_clause(vtxo).await;
1772		Ok(!my_clause.is_some())
1773	}
1774
1775	/// If there are any VTXOs that match the "must-refresh" and "should-refresh" criteria with a
1776	/// total value over the P2TR dust limit, they are added to the round participation and an
1777	/// additional output is also created.
1778	///
1779	/// Note: This assumes that the base refresh fee has already been paid.
1780	async fn add_should_refresh_vtxos(
1781		&self,
1782		participation: &mut RoundParticipation,
1783	) -> anyhow::Result<()> {
1784		// Get VTXOs that need and should be refreshed, then filter out any duplicates before
1785		// adjusting the round participation.
1786		let tip = self.chain.tip().await?;
1787		let mut vtxos_to_refresh = self.spendable_vtxos_with(
1788			&RefreshStrategy::should_refresh(self, tip, self.chain.fee_rates().await.fast),
1789		).await?;
1790		if vtxos_to_refresh.is_empty() {
1791			return Ok(());
1792		}
1793
1794		let excluded_ids = participation.inputs.iter().map(|v| v.vtxo_id())
1795			.collect::<HashSet<_>>();
1796		let mut total_amount = Amount::ZERO;
1797		for i in (0..vtxos_to_refresh.len()).rev() {
1798			let vtxo = &vtxos_to_refresh[i];
1799			if excluded_ids.contains(&vtxo.id()) {
1800				vtxos_to_refresh.swap_remove(i);
1801				continue;
1802			}
1803			total_amount += vtxo.amount();
1804		}
1805		if vtxos_to_refresh.is_empty() {
1806			// VTXOs are already included in the round participation.
1807			return Ok(());
1808		}
1809
1810		// We need to verify that the output we add won't end up below the dust limit when fees are
1811		// applied. We can assume the base fee has been paid by the current refresh participation.
1812		let (_, ark_info) = self.require_server().await?;
1813		let fee = ark_info.fees.refresh.calculate_no_base_fee(
1814			vtxos_to_refresh.iter().map(|wv| VtxoFeeInfo::from_vtxo_and_tip(&wv.vtxo, tip)),
1815		).context("fee overflowed")?;
1816
1817		// Only add these VTXOs if the output amount would be above dust after fees.
1818		let output_amount = match validate_and_subtract_fee_min_dust(total_amount, fee) {
1819			Ok(amount) => amount,
1820			Err(e) => {
1821				trace!("Cannot add should-refresh VTXOs: {}", e);
1822				return Ok(());
1823			},
1824		};
1825		info!(
1826			"Adding {} extra VTXOs to round participation total = {}, fee = {}, output = {}",
1827			vtxos_to_refresh.len(), total_amount, fee, output_amount,
1828		);
1829		let (user_keypair, _) = self.derive_store_next_keypair().await?;
1830		let req = VtxoRequest {
1831			policy: VtxoPolicy::new_pubkey(user_keypair.public_key()),
1832			amount: output_amount,
1833		};
1834		participation.inputs.reserve(vtxos_to_refresh.len());
1835		participation.inputs.extend(vtxos_to_refresh.into_iter().map(|wv| wv.vtxo));
1836		participation.outputs.push(req);
1837
1838		Ok(())
1839	}
1840
1841	pub async fn build_refresh_participation<V: VtxoRef>(
1842		&self,
1843		vtxos: impl IntoIterator<Item = V>,
1844	) -> anyhow::Result<Option<RoundParticipation>> {
1845		let (vtxos, total_amount) = {
1846			let iter = vtxos.into_iter();
1847			let size_hint = iter.size_hint();
1848			let mut vtxos = Vec::<Vtxo<Full>>::with_capacity(size_hint.1.unwrap_or(size_hint.0));
1849			let mut amount = Amount::ZERO;
1850			for vref in iter {
1851				// We use a Vec here instead of a HashMap or a HashSet of IDs because for the kinds
1852				// of elements we expect to deal with, a Vec is likely to be quicker. The overhead
1853				// of hashing each ID and making additional allocations isn't likely to be worth it
1854				// for what is likely to be a handful of VTXOs or at most a couple of hundred.
1855				let id = vref.vtxo_id();
1856				if vtxos.iter().any(|v| v.id() == id) {
1857					bail!("duplicate VTXO id: {}", id);
1858				}
1859				let vtxo = if let Some(vtxo) = vref.into_full_vtxo() {
1860					vtxo
1861				} else {
1862					self.get_vtxo_by_id(id).await
1863						.with_context(|| format!("vtxo with id {} not found", id))?.vtxo
1864				};
1865				amount += vtxo.amount();
1866				vtxos.push(vtxo);
1867			}
1868			(vtxos, amount)
1869		};
1870
1871		if vtxos.is_empty() {
1872			info!("Skipping refresh since no VTXOs are provided.");
1873			return Ok(None);
1874		}
1875		ensure!(total_amount >= P2TR_DUST,
1876			"vtxo amount must be at least {} to participate in a round",
1877			P2TR_DUST,
1878		);
1879
1880		// Calculate refresh fees
1881		let (_, ark_info) = self.require_server().await?;
1882		let current_height = self.chain.tip().await?;
1883		let vtxo_fee_infos = vtxos.iter()
1884			.map(|v| VtxoFeeInfo::from_vtxo_and_tip(v, current_height));
1885		let fee = ark_info.fees.refresh.calculate(vtxo_fee_infos).context("fee overflowed")?;
1886		let output_amount = validate_and_subtract_fee_min_dust(total_amount, fee)?;
1887
1888		info!("Refreshing {} VTXOs (total amount = {}, fee = {}, output = {}).",
1889			vtxos.len(), total_amount, fee, output_amount,
1890		);
1891		let (user_keypair, _) = self.derive_store_next_keypair().await?;
1892		let req = VtxoRequest {
1893			policy: VtxoPolicy::Pubkey(PubkeyVtxoPolicy { user_pubkey: user_keypair.public_key() }),
1894			amount: output_amount,
1895		};
1896
1897		Ok(Some(RoundParticipation {
1898			inputs: vtxos,
1899			outputs: vec![req],
1900			unblinded_mailbox_id: None,
1901		}))
1902	}
1903
1904	/// This will refresh all provided VTXOs in an interactive round and wait until end
1905	///
1906	/// Returns the [RoundStatus] of the round if a successful refresh occurred.
1907	/// It will return [None] if no [Vtxo] needed to be refreshed.
1908	pub async fn refresh_vtxos<V: VtxoRef>(
1909		&self,
1910		vtxos: impl IntoIterator<Item = V>,
1911	) -> anyhow::Result<Option<RoundStatus>> {
1912		let mut participation = match self.build_refresh_participation(vtxos).await? {
1913			Some(participation) => participation,
1914			None => return Ok(None),
1915		};
1916
1917		if let Err(e) = self.add_should_refresh_vtxos(&mut participation).await {
1918			warn!("Error trying to add additional VTXOs that should be refreshed: {:#}", e);
1919		}
1920
1921		Ok(Some(self.participate_round(participation, Some(RoundMovement::Refresh)).await?))
1922	}
1923
1924	/// This will refresh all provided VTXOs in delegated (non-interactive) mode
1925	///
1926	/// Returns the [StoredRoundState] which can be used to track the round's
1927	/// progress later by calling sync. It will return [None] if no [Vtxo]
1928	/// needed to be refreshed.
1929	pub async fn refresh_vtxos_delegated<V: VtxoRef>(
1930		&self,
1931		vtxos: impl IntoIterator<Item = V>,
1932	) -> anyhow::Result<Option<StoredRoundState<Unlocked>>> {
1933		let mut part = match self.build_refresh_participation(vtxos).await? {
1934			Some(participation) => participation,
1935			None => return Ok(None),
1936		};
1937
1938		if let Err(e) = self.add_should_refresh_vtxos(&mut part).await {
1939			warn!("Error trying to add additional VTXOs that should be refreshed: {:#}", e);
1940		}
1941
1942		Ok(Some(self.join_next_round_delegated(part, Some(RoundMovement::Refresh)).await?))
1943	}
1944
1945	/// This will find all VTXOs that meets must-refresh criteria. Then, if there are some VTXOs to
1946	/// refresh, it will also add those that meet should-refresh criteria.
1947	pub async fn get_vtxos_to_refresh(&self) -> anyhow::Result<Vec<WalletVtxo>> {
1948		let vtxos = self.spendable_vtxos_with(&RefreshStrategy::should_refresh_if_must(
1949			self,
1950			self.chain.tip().await?,
1951			self.chain.fee_rates().await.fast,
1952		)).await?;
1953		Ok(vtxos)
1954	}
1955
1956	/// Returns the block height at which the first VTXO will expire
1957	pub async fn get_first_expiring_vtxo_blockheight(
1958		&self,
1959	) -> anyhow::Result<Option<BlockHeight>> {
1960		Ok(self.spendable_vtxos().await?.iter().map(|v| v.expiry_height()).min())
1961	}
1962
1963	/// Returns the next block height at which we have a VTXO that we
1964	/// want to refresh
1965	pub async fn get_next_required_refresh_blockheight(
1966		&self,
1967	) -> anyhow::Result<Option<BlockHeight>> {
1968		let first_expiry = self.get_first_expiring_vtxo_blockheight().await?;
1969		Ok(first_expiry.map(|h| h.saturating_sub(self.config.vtxo_refresh_expiry_threshold)))
1970	}
1971
1972	/// Select several VTXOs to cover the provided amount
1973	///
1974	/// VTXOs are selected soonest-expiring-first.
1975	///
1976	/// Returns an error if amount cannot be reached.
1977	async fn select_vtxos_to_cover(
1978		&self,
1979		amount: Amount,
1980	) -> anyhow::Result<Vec<WalletVtxo>> {
1981		let mut vtxos = self.spendable_vtxos().await?;
1982		vtxos.sort_by_key(|v| v.expiry_height());
1983
1984		// Iterate over VTXOs until the required amount is reached
1985		let mut result = Vec::new();
1986		let mut total_amount = Amount::ZERO;
1987		for input in vtxos {
1988			total_amount += input.amount();
1989			result.push(input);
1990
1991			if total_amount >= amount {
1992				return Ok(result)
1993			}
1994		}
1995
1996		bail!("Insufficient money available. Needed {} but {} is available",
1997			amount, total_amount,
1998		);
1999	}
2000
2001	/// Determines which VTXOs to use for a fee-paying transaction where the fee is added on top of
2002	/// the desired amount. E.g., a lightning payment, a send-onchain payment.
2003	///
2004	/// Returns a collection of VTXOs capable of covering the desired amount as well as the
2005	/// calculated fee.
2006	async fn select_vtxos_to_cover_with_fee<F>(
2007		&self,
2008		amount: Amount,
2009		calc_fee: F,
2010	) -> anyhow::Result<(Vec<WalletVtxo>, Amount)>
2011	where
2012		F: for<'a> Fn(
2013			Amount, std::iter::Copied<std::slice::Iter<'a, VtxoFeeInfo>>,
2014		) -> anyhow::Result<Amount>,
2015	{
2016		let tip = self.chain.tip().await?;
2017
2018		// We need to loop to find suitable inputs due to the VTXOs having a direct impact on
2019		// how much we must pay in fees.
2020		const MAX_ITERATIONS: usize = 100;
2021		let mut fee = Amount::ZERO;
2022		let mut fee_info = Vec::new();
2023		for _ in 0..MAX_ITERATIONS {
2024			let required = amount.checked_add(fee)
2025				.context("Amount + fee overflow")?;
2026
2027			let vtxos = self.select_vtxos_to_cover(required).await
2028				.context("Could not find enough suitable VTXOs to cover payment + fees")?;
2029
2030			fee_info.reserve(vtxos.len());
2031			let mut vtxo_amount = Amount::ZERO;
2032			for vtxo in &vtxos {
2033				vtxo_amount += vtxo.amount();
2034				fee_info.push(VtxoFeeInfo::from_vtxo_and_tip(vtxo, tip));
2035			}
2036
2037			fee = calc_fee(amount, fee_info.iter().copied())?;
2038			if amount + fee <= vtxo_amount {
2039				trace!("Selected vtxos to cover amount + fee: amount = {}, fee = {}, total inputs = {}",
2040					amount, fee, vtxo_amount,
2041				);
2042				return Ok((vtxos, fee));
2043			}
2044			trace!("VTXO sum of {} did not exceed amount {} and fee {}, iterating again",
2045				vtxo_amount, amount, fee,
2046			);
2047			fee_info.clear();
2048		}
2049		bail!("Fee calculation did not converge after maximum iterations")
2050	}
2051
2052	/// Starts a daemon for the wallet.
2053	///
2054	/// Note:
2055	/// - This function doesn't check if a daemon is already running,
2056	/// so it's possible to start multiple daemons by mistake.
2057	pub fn start_daemon(
2058		self: &Arc<Self>,
2059		onchain: Option<Arc<RwLock<dyn DaemonizableOnchainWallet>>>,
2060	) -> anyhow::Result<()> {
2061		let mut daemon = self.daemon.lock();
2062		if daemon.is_some() {
2063			warn!("Called Wallet::start_daemon while daemon was already running.");
2064			return Ok(());
2065		}
2066
2067		// NB currently can't error but it's a pretty common method and quite likely that error
2068		// cases will be introduces later
2069		let handle = crate::daemon::start_daemon(self.clone(), onchain);
2070		let _ = daemon.insert(handle);
2071
2072		Ok(())
2073	}
2074
2075	/// Use [Wallet::start_daemon] instead.
2076	#[deprecated(since = "0.1.4", note = "use start_daemon instead")]
2077	pub fn run_daemon(
2078		self: &Arc<Self>,
2079		onchain: Option<Arc<RwLock<dyn DaemonizableOnchainWallet>>>,
2080	) -> anyhow::Result<()> {
2081		self.start_daemon(onchain)
2082	}
2083
2084	/// Stops the daemon for the wallet if it is running, otherwise does nothing.
2085	pub fn stop_daemon(&self) {
2086		let mut daemon = self.daemon.lock();
2087		if let Some(handle) = daemon.take() {
2088			handle.stop();
2089		}
2090	}
2091
2092	/// Registers the signed transaction chains for the given VTXOs with the
2093	/// server. This must be called before spending VTXOs so the server can
2094	/// publish forfeits if needed.
2095	pub async fn register_vtxo_transactions_with_server(
2096		&self,
2097		vtxos: &[impl AsRef<Vtxo<Full>>],
2098	) -> anyhow::Result<()> {
2099		if vtxos.is_empty() {
2100			return Ok(());
2101		}
2102
2103		let (mut srv, _) = self.require_server().await?;
2104		srv.client.register_vtxo_transactions(protos::RegisterVtxoTransactionsRequest {
2105			vtxos: vtxos.iter().map(|v| v.as_ref().serialize()).collect(),
2106		}).await.context("failed to register vtxo transactions")?;
2107
2108		Ok(())
2109	}
2110}
2111
2112fn wrap_server_connect_error(err: ConnectError) -> anyhow::Error {
2113	match err {
2114		ConnectError::CreateEndpoint(CreateEndpointError::NoTransportBackend) => {
2115			anyhow!(MISSING_SERVER_TRANSPORT_HELP)
2116		},
2117		other => anyhow::Error::from(other),
2118	}
2119}
2120
2121impl std::ops::Drop for Wallet {
2122	fn drop(&mut self) {
2123		self.stop_daemon();
2124	}
2125}
2126
2127#[cfg(test)]
2128mod tests {
2129	use server_rpc::client::CreateEndpointError;
2130
2131	use super::{wrap_server_connect_error, MISSING_SERVER_TRANSPORT_HELP};
2132
2133	#[test]
2134	fn no_transport_connect_error_is_reworded_for_wallet_users() {
2135		let err = wrap_server_connect_error(CreateEndpointError::NoTransportBackend.into());
2136		assert!(err.to_string().contains(MISSING_SERVER_TRANSPORT_HELP));
2137		assert!(err.to_string().contains("feature `bark-wallet/native` or `bark-wallet/wasm-web`"));
2138	}
2139}