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