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 address = &config.server_address;
1054		#[cfg(feature = "socks5-proxy")]
1055		if let Some(proxy) = proxy_for_url(&config.socks5_proxy, address)? {
1056			return ServerConnection::connect_via_proxy(address, network, &proxy).await
1057				.context("Failed to connect Ark server via proxy");
1058		}
1059		ServerConnection::connect(address, network).await
1060			.context("Failed to connect to Ark server")
1061	}
1062
1063	async fn require_server(&self) -> anyhow::Result<(ServerConnection, ArkInfo)> {
1064		let conn = self.server.read().clone()
1065			.context("You should be connected to Ark server to perform this action")?;
1066		let ark_info = conn.ark_info().await?;
1067
1068		// Check if server pubkey has changed
1069		if let Some(stored_pubkey) = self.properties().await?.server_pubkey {
1070			if stored_pubkey != ark_info.server_pubkey {
1071				log_server_pubkey_changed_error(stored_pubkey, ark_info.server_pubkey);
1072				bail!("Server public key has changed. You should exit all your VTXOs!");
1073			}
1074		} else {
1075			// First time connecting after upgrade - store the server pubkey
1076			self.db.set_server_pubkey(ark_info.server_pubkey).await?;
1077			info!("Stored server pubkey for existing wallet: {}", ark_info.server_pubkey);
1078		}
1079
1080		Ok((conn, ark_info))
1081	}
1082
1083	pub async fn refresh_server(&self) -> anyhow::Result<()> {
1084		let server = self.server.read().clone();
1085		let properties = self.properties().await?;
1086
1087		let srv = if let Some(srv) = server {
1088			srv.check_connection().await?;
1089			let ark_info = srv.ark_info().await?;
1090			ark_info.fees.validate().context("invalid fee schedule")?;
1091
1092			// Check if server pubkey has changed
1093			if let Some(stored_pubkey) = properties.server_pubkey {
1094				if stored_pubkey != ark_info.server_pubkey {
1095					log_server_pubkey_changed_error(stored_pubkey, ark_info.server_pubkey);
1096					bail!("Server public key has changed. You should exit all your VTXOs!");
1097				}
1098			} else {
1099				// First time connecting after upgrade - store the server pubkey
1100				self.db.set_server_pubkey(ark_info.server_pubkey).await?;
1101				info!("Stored server pubkey for existing wallet: {}", ark_info.server_pubkey);
1102			}
1103
1104			srv
1105		} else {
1106			let conn = Self::connect_to_server(&self.config, properties.network).await?;
1107			let ark_info = conn.ark_info().await?;
1108			ark_info.fees.validate().context("invalid fee schedule")?;
1109
1110			// Check if server pubkey has changed
1111			if let Some(stored_pubkey) = properties.server_pubkey {
1112				if stored_pubkey != ark_info.server_pubkey {
1113					log_server_pubkey_changed_error(stored_pubkey, ark_info.server_pubkey);
1114					bail!("Server public key has changed. You should exit all your VTXOs!");
1115				}
1116			} else {
1117				// First time connecting after upgrade - store the server pubkey
1118				self.db.set_server_pubkey(ark_info.server_pubkey).await?;
1119				info!("Stored server pubkey for existing wallet: {}", ark_info.server_pubkey);
1120			}
1121
1122			conn
1123		};
1124
1125		let _ = self.server.write().insert(srv);
1126
1127		Ok(())
1128	}
1129
1130	/// Return [ArkInfo] fetched on last handshake with the Ark server
1131	pub async fn ark_info(&self) -> anyhow::Result<Option<ArkInfo>> {
1132		let server = self.server.read().clone();
1133		match server.as_ref() {
1134			Some(srv) => Ok(Some(srv.ark_info().await?)),
1135			_ => Ok(None),
1136		}
1137	}
1138
1139	/// Return the [Balance] of the wallet.
1140	///
1141	/// Make sure you sync before calling this method.
1142	pub async fn balance(&self) -> anyhow::Result<Balance> {
1143		let vtxos = self.vtxos().await?;
1144
1145		let spendable = {
1146			let mut v = vtxos.iter().collect();
1147			VtxoStateKind::Spendable.filter_vtxos(&mut v).await?;
1148			v.into_iter().map(|v| v.amount()).sum::<Amount>()
1149		};
1150
1151		let pending_lightning_send = self.pending_lightning_send_vtxos().await?.iter()
1152			.map(|v| v.amount())
1153			.sum::<Amount>();
1154
1155		let claimable_lightning_receive = self.claimable_lightning_receive_balance().await?;
1156
1157		let pending_board = self.pending_board_vtxos().await?.iter()
1158			.map(|v| v.amount())
1159			.sum::<Amount>();
1160
1161		let pending_in_round = self.pending_round_balance().await?;
1162
1163		let pending_exit = self.exit.try_read().ok().map(|e| e.pending_total());
1164
1165		Ok(Balance {
1166			spendable,
1167			pending_in_round,
1168			pending_lightning_send,
1169			claimable_lightning_receive,
1170			pending_exit,
1171			pending_board,
1172		})
1173	}
1174
1175	/// Fetches [Vtxo]'s funding transaction and validates the VTXO against it.
1176	pub async fn validate_vtxo(&self, vtxo: &Vtxo<Full>) -> anyhow::Result<()> {
1177		let tx = self.chain.get_tx(&vtxo.chain_anchor().txid).await
1178			.context("could not fetch chain tx")?;
1179
1180		let tx = tx.with_context(|| {
1181			format!("vtxo chain anchor not found for vtxo: {}", vtxo.chain_anchor().txid)
1182		})?;
1183
1184		vtxo.validate(&tx)?;
1185
1186		Ok(())
1187	}
1188
1189	/// Manually import a VTXO into the wallet.
1190	///
1191	/// # Arguments
1192	/// * `vtxo` - The VTXO to import
1193	///
1194	/// # Errors
1195	/// Returns an error if:
1196	/// - The VTXO's chain anchor is not found or invalid
1197	/// - The wallet doesn't own a signable clause for the VTXO
1198	pub async fn import_vtxo(&self, vtxo: &Vtxo<Full>) -> anyhow::Result<()> {
1199		if self.db.get_wallet_vtxo(vtxo.id()).await?.is_some() {
1200			info!("VTXO {} already exists in wallet, skipping import", vtxo.id());
1201			return Ok(());
1202		}
1203
1204		self.validate_vtxo(vtxo).await.context("VTXO validation failed")?;
1205
1206		if self.find_signable_clause(vtxo).await.is_none() {
1207			bail!("VTXO {} is not owned by this wallet (no signable clause found)", vtxo.id());
1208		}
1209
1210		let current_height = self.chain.tip().await?;
1211		if vtxo.expiry_height() <= current_height {
1212			bail!("Vtxo {} has expired", vtxo.id());
1213		}
1214
1215		self.store_spendable_vtxos([vtxo]).await.context("failed to store imported VTXO")?;
1216
1217		info!("Successfully imported VTXO {}", vtxo.id());
1218		Ok(())
1219	}
1220
1221	/// Retrieves the full state of a [Vtxo] for a given [VtxoId] if it exists in the database.
1222	pub async fn get_vtxo_by_id(&self, vtxo_id: VtxoId) -> anyhow::Result<WalletVtxo> {
1223		let vtxo = self.db.get_wallet_vtxo(vtxo_id).await
1224			.with_context(|| format!("Error when querying vtxo {} in database", vtxo_id))?
1225			.with_context(|| format!("The VTXO with id {} cannot be found", vtxo_id))?;
1226		Ok(vtxo)
1227	}
1228
1229	/// Fetches all movements ordered from newest to oldest.
1230	#[deprecated(since="0.1.0-beta.5", note = "Use Wallet::history instead")]
1231	pub async fn movements(&self) -> anyhow::Result<Vec<Movement>> {
1232		self.history().await
1233	}
1234
1235	/// Fetches all wallet fund movements ordered from newest to oldest.
1236	pub async fn history(&self) -> anyhow::Result<Vec<Movement>> {
1237		Ok(self.db.get_all_movements().await?)
1238	}
1239
1240	/// Query the wallet history by the given payment method
1241	pub async fn history_by_payment_method(
1242		&self,
1243		payment_method: &PaymentMethod,
1244	) -> anyhow::Result<Vec<Movement>> {
1245		let mut ret = self.db.get_movements_by_payment_method(payment_method).await?;
1246		ret.sort_by_key(|m| m.id);
1247		Ok(ret)
1248	}
1249
1250	/// Returns all VTXOs from the database.
1251	pub async fn all_vtxos(&self) -> anyhow::Result<Vec<WalletVtxo>> {
1252		Ok(self.db.get_all_vtxos().await?)
1253	}
1254
1255	/// Returns all not spent vtxos
1256	pub async fn vtxos(&self) -> anyhow::Result<Vec<WalletVtxo>> {
1257		Ok(self.db.get_vtxos_by_state(&VtxoStateKind::UNSPENT_STATES).await?)
1258	}
1259
1260	/// Returns all vtxos matching the provided predicate
1261	pub async fn vtxos_with(&self, filter: &impl FilterVtxos) -> anyhow::Result<Vec<WalletVtxo>> {
1262		let mut vtxos = self.vtxos().await?;
1263		filter.filter_vtxos(&mut vtxos).await.context("error filtering vtxos")?;
1264		Ok(vtxos)
1265	}
1266
1267	/// Returns all spendable vtxos
1268	pub async fn spendable_vtxos(&self) -> anyhow::Result<Vec<WalletVtxo>> {
1269		Ok(self.vtxos_with(&VtxoStateKind::Spendable).await?)
1270	}
1271
1272	/// Returns all spendable vtxos matching the provided predicate
1273	pub async fn spendable_vtxos_with(
1274		&self,
1275		filter: &impl FilterVtxos,
1276	) -> anyhow::Result<Vec<WalletVtxo>> {
1277		let mut vtxos = self.spendable_vtxos().await?;
1278		filter.filter_vtxos(&mut vtxos).await.context("error filtering vtxos")?;
1279		Ok(vtxos)
1280	}
1281
1282	/// Returns all vtxos that will expire within `threshold` blocks
1283	pub async fn get_expiring_vtxos(
1284		&self,
1285		threshold: BlockHeight,
1286	) -> anyhow::Result<Vec<WalletVtxo>> {
1287		let expiry = self.chain.tip().await? + threshold;
1288		let filter = VtxoFilter::new(&self).expires_before(expiry);
1289		Ok(self.spendable_vtxos_with(&filter).await?)
1290	}
1291
1292	/// Checks pending offboard transactions for confirmation status.
1293	///
1294	/// - On confirmation with enough confs (or mempool with 0 required confs): finalize as successful.
1295	/// - On `NotFound`: wait at least 1 hour before canceling, in case the chain backend is slow.
1296	/// - On error (e.g. network drop): log and keep waiting — don't cancel due to transient failures.
1297	pub async fn sync_pending_offboards(&self) -> anyhow::Result<()> {
1298		let pending_offboards: Vec<PendingOffboard> = self.db.get_pending_offboards().await?;
1299
1300		if pending_offboards.is_empty() {
1301			return Ok(());
1302		}
1303
1304		let current_height = self.chain.tip().await?;
1305		let required_confs = self.config.offboard_required_confirmations;
1306
1307		trace!("Checking {} pending offboard transaction(s)", pending_offboards.len());
1308
1309		for pending in pending_offboards {
1310			let status = self.chain.tx_status(pending.offboard_txid).await;
1311
1312			match status {
1313				Ok(TxStatus::Confirmed(block_ref)) => {
1314					let confs = current_height - (block_ref.height - 1);
1315					if confs < required_confs as BlockHeight {
1316						trace!(
1317							"Offboard tx {} has {}/{} confirmations, waiting...",
1318							pending.offboard_txid, confs, required_confs,
1319						);
1320						continue;
1321					}
1322
1323					info!(
1324						"Offboard tx {} confirmed, finalizing movement {}",
1325						pending.offboard_txid, pending.movement_id,
1326					);
1327
1328					// Mark VTXOs as spent
1329					for vtxo_id in &pending.vtxo_ids {
1330						if let Err(e) = self.db.update_vtxo_state_checked(
1331							*vtxo_id,
1332							VtxoState::Spent,
1333							&[VtxoStateKind::Locked],
1334						).await {
1335							warn!("Failed to mark vtxo {} as spent: {:#}", vtxo_id, e);
1336						}
1337					}
1338
1339					// Finish the movement as successful
1340					if let Err(e) = self.movements.finish_movement(
1341						pending.movement_id,
1342						MovementStatus::Successful,
1343					).await {
1344						warn!("Failed to finish movement {}: {:#}", pending.movement_id, e);
1345					}
1346
1347					self.db.remove_pending_offboard(pending.movement_id).await?;
1348				}
1349				Ok(TxStatus::Mempool) => {
1350					if required_confs == 0 {
1351						info!(
1352							"Offboard tx {} in mempool with 0 required confirmations, \
1353							finalizing movement {}",
1354							pending.offboard_txid, pending.movement_id,
1355						);
1356
1357						// Mark VTXOs as spent
1358						for vtxo_id in &pending.vtxo_ids {
1359							if let Err(e) = self.db.update_vtxo_state_checked(
1360								*vtxo_id,
1361								VtxoState::Spent,
1362								&[VtxoStateKind::Locked],
1363							).await {
1364								warn!("Failed to mark vtxo {} as spent: {:#}", vtxo_id, e);
1365							}
1366						}
1367
1368						// Finish the movement as successful
1369						if let Err(e) = self.movements.finish_movement(
1370							pending.movement_id,
1371							MovementStatus::Successful,
1372						).await {
1373							warn!("Failed to finish movement {}: {:#}", pending.movement_id, e);
1374						}
1375
1376						self.db.remove_pending_offboard(pending.movement_id).await?;
1377					} else {
1378						trace!(
1379							"Offboard tx {} still in mempool, waiting...",
1380							pending.offboard_txid,
1381						);
1382					}
1383				}
1384				Ok(TxStatus::NotFound) => {
1385					// Don't cancel immediately — the chain backend might be slow
1386					// or temporarily out of sync. Wait at least 1 hour before
1387					// treating the tx as truly lost.
1388					let age = chrono::Local::now() - pending.created_at;
1389					if age < chrono::Duration::hours(1) {
1390						trace!(
1391							"Offboard tx {} not found, but only {} minutes old — waiting...",
1392							pending.offboard_txid, age.num_minutes(),
1393						);
1394						continue;
1395					}
1396
1397					warn!(
1398						"Offboard tx {} not found after {} minutes, canceling movement {}",
1399						pending.offboard_txid, age.num_minutes(), pending.movement_id,
1400					);
1401
1402					// Restore VTXOs to spendable
1403					for vtxo_id in &pending.vtxo_ids {
1404						if let Err(e) = self.db.update_vtxo_state_checked(
1405							*vtxo_id,
1406							VtxoState::Spendable,
1407							&[VtxoStateKind::Locked],
1408						).await {
1409							warn!("Failed to restore vtxo {} to spendable: {:#}", vtxo_id, e);
1410						}
1411					}
1412
1413					// Finish the movement as failed
1414					if let Err(e) = self.movements.finish_movement(
1415						pending.movement_id,
1416						MovementStatus::Failed,
1417					).await {
1418						warn!("Failed to fail movement {}: {:#}", pending.movement_id, e);
1419					}
1420
1421					self.db.remove_pending_offboard(pending.movement_id).await?;
1422				}
1423				Err(e) => {
1424					warn!(
1425						"Failed to check status of offboard tx {}: {:#}",
1426						pending.offboard_txid, e,
1427					);
1428				}
1429			}
1430		}
1431
1432		Ok(())
1433	}
1434
1435	/// Performs maintenance tasks and performs refresh interactively until finished when needed.
1436	/// This risks spending users' funds because refreshing may cost fees.
1437	///
1438	/// This can take a long period of time due to syncing rounds, arkoors, checking pending
1439	/// payments, progressing pending rounds, and refreshing VTXOs if necessary.
1440	pub async fn maintenance(&self) -> anyhow::Result<()> {
1441		info!("Starting wallet maintenance in interactive mode");
1442		self.sync().await;
1443
1444		let rounds = self.progress_pending_rounds(None).await;
1445		if let Err(e) = rounds.as_ref() {
1446			warn!("Error progressing pending rounds: {:#}", e);
1447		}
1448		let refresh = self.maintenance_refresh().await;
1449		if let Err(e) = refresh.as_ref() {
1450			warn!("Error refreshing VTXOs: {:#}", e);
1451		}
1452		if rounds.is_err() || refresh.is_err() {
1453			bail!("Maintenance encountered errors.\nprogress_rounds: {:#?}\nrefresh: {:#?}", rounds, refresh);
1454		}
1455		Ok(())
1456	}
1457
1458	/// Performs maintenance tasks and schedules delegated refresh when needed. This risks spending
1459	/// users' funds because refreshing may cost fees.
1460	///
1461	/// This can take a long period of time due to syncing rounds, arkoors, checking pending
1462	/// payments, progressing pending rounds, and refreshing VTXOs if necessary.
1463	pub async fn maintenance_delegated(&self) -> anyhow::Result<()> {
1464		info!("Starting wallet maintenance in delegated mode");
1465		self.sync().await;
1466		let rounds = self.progress_pending_rounds(None).await;
1467		if let Err(e) = rounds.as_ref() {
1468			warn!("Error progressing pending rounds: {:#}", e);
1469		}
1470		let refresh = self.maybe_schedule_maintenance_refresh_delegated().await;
1471		if let Err(e) = refresh.as_ref() {
1472			warn!("Error refreshing VTXOs: {:#}", e);
1473		}
1474		if rounds.is_err() || refresh.is_err() {
1475			bail!("Delegated maintenance encountered errors.\nprogress_rounds: {:#?}\nrefresh: {:#?}", rounds, refresh);
1476		}
1477		Ok(())
1478	}
1479
1480	/// Performs maintenance tasks and performs refresh interactively until finished when needed.
1481	/// This risks spending users' funds because refreshing may cost fees and any pending exits will
1482	/// be progressed.
1483	///
1484	/// This can take a long period of time due to syncing the onchain wallet, registering boards,
1485	/// syncing rounds, arkoors, and the exit system, checking pending lightning payments and
1486	/// refreshing VTXOs if necessary.
1487	pub async fn maintenance_with_onchain<W: PreparePsbt + SignPsbt + ExitUnilaterally>(
1488		&self,
1489		onchain: &mut W,
1490	) -> anyhow::Result<()> {
1491		info!("Starting wallet maintenance in interactive mode with onchain wallet");
1492
1493		// Maintenance will log so we don't need to.
1494		let maintenance = self.maintenance().await;
1495
1496		// NB: order matters here, after syncing lightning, we might have new exits to start
1497		let exit_sync = self.sync_exits(onchain).await;
1498		if let Err(e) = exit_sync.as_ref() {
1499			warn!("Error syncing exits: {:#}", e);
1500		}
1501		let exit_progress = self.exit.write().await.progress_exits(&self, onchain, None).await;
1502		if let Err(e) = exit_progress.as_ref() {
1503			warn!("Error progressing exits: {:#}", e);
1504		}
1505		if maintenance.is_err() || exit_sync.is_err() || exit_progress.is_err() {
1506			bail!("Maintenance encountered errors.\nmaintenance: {:#?}\nexit_sync: {:#?}\nexit_progress: {:#?}", maintenance, exit_sync, exit_progress);
1507		}
1508		Ok(())
1509	}
1510
1511	/// Performs maintenance tasks and schedules delegated refresh when needed. This risks spending
1512	/// users' funds because refreshing may cost fees and any pending exits will be progressed.
1513	///
1514	/// This can take a long period of time due to syncing the onchain wallet, registering boards,
1515	/// syncing rounds, arkoors, and the exit system, checking pending lightning payments and
1516	/// refreshing VTXOs if necessary.
1517	pub async fn maintenance_with_onchain_delegated<W: PreparePsbt + SignPsbt + ExitUnilaterally>(
1518		&self,
1519		onchain: &mut W,
1520	) -> anyhow::Result<()> {
1521		info!("Starting wallet maintenance in delegated mode with onchain wallet");
1522
1523		// Maintenance will log so we don't need to.
1524		let maintenance = self.maintenance_delegated().await;
1525
1526		// NB: order matters here, after syncing lightning, we might have new exits to start
1527		let exit_sync = self.sync_exits(onchain).await;
1528		if let Err(e) = exit_sync.as_ref() {
1529			warn!("Error syncing exits: {:#}", e);
1530		}
1531		let exit_progress = self.exit.write().await.progress_exits(&self, onchain, None).await;
1532		if let Err(e) = exit_progress.as_ref() {
1533			warn!("Error progressing exits: {:#}", e);
1534		}
1535		if maintenance.is_err() || exit_sync.is_err() || exit_progress.is_err() {
1536			bail!("Delegated maintenance encountered errors.\nmaintenance: {:#?}\nexit_sync: {:#?}\nexit_progress: {:#?}", maintenance, exit_sync, exit_progress);
1537		}
1538		Ok(())
1539	}
1540
1541	/// Checks VTXOs that are due to be refreshed, and schedules an interactive refresh if any
1542	///
1543	/// This will include any VTXOs within the expiry threshold
1544	/// ([Config::vtxo_refresh_expiry_threshold]) or those which
1545	/// are uneconomical to exit due to onchain network conditions.
1546	///
1547	/// Returns a [RoundStateId] if a refresh is scheduled.
1548	pub async fn maybe_schedule_maintenance_refresh(&self) -> anyhow::Result<Option<RoundStateId>> {
1549		let vtxos = self.get_vtxos_to_refresh().await?;
1550		if vtxos.len() == 0 {
1551			return Ok(None);
1552		}
1553
1554		let participation = match self.build_refresh_participation(vtxos).await? {
1555			Some(participation) => participation,
1556			None => return Ok(None),
1557		};
1558
1559		info!("Scheduling maintenance refresh ({} vtxos)", participation.inputs.len());
1560		let state = self.join_next_round(participation, Some(RoundMovement::Refresh)).await?;
1561		Ok(Some(state.id()))
1562	}
1563
1564	/// Checks VTXOs that are due to be refreshed, and schedules a delegated refresh if any
1565	///
1566	/// This will include any VTXOs within the expiry threshold
1567	/// ([Config::vtxo_refresh_expiry_threshold]) or those which
1568	/// are uneconomical to exit due to onchain network conditions.
1569	///
1570	/// Returns a [RoundStateId] if a refresh is scheduled.
1571	pub async fn maybe_schedule_maintenance_refresh_delegated(
1572		&self,
1573	) -> anyhow::Result<Option<RoundStateId>> {
1574		let vtxos = self.get_vtxos_to_refresh().await?;
1575		if vtxos.len() == 0 {
1576			return Ok(None);
1577		}
1578
1579		let participation = match self.build_refresh_participation(vtxos).await? {
1580			Some(participation) => participation,
1581			None => return Ok(None),
1582		};
1583
1584		info!("Scheduling delegated maintenance refresh ({} vtxos)", participation.inputs.len());
1585		let state = self.join_next_round_delegated(participation, Some(RoundMovement::Refresh)).await?;
1586		Ok(Some(state.id()))
1587	}
1588
1589	/// Performs an interactive refresh of all VTXOs that are due to be refreshed, if any
1590	///
1591	/// This will include any VTXOs within the expiry threshold
1592	/// ([Config::vtxo_refresh_expiry_threshold]) or those which
1593	/// are uneconomical to exit due to onchain network conditions.
1594	///
1595	/// Returns a [RoundStatus] if a refresh occurs.
1596	pub async fn maintenance_refresh(&self) -> anyhow::Result<Option<RoundStatus>> {
1597		let vtxos = self.get_vtxos_to_refresh().await?;
1598		if vtxos.len() == 0 {
1599			return Ok(None);
1600		}
1601
1602		info!("Performing maintenance refresh");
1603		self.refresh_vtxos(vtxos).await
1604	}
1605
1606	/// Sync offchain wallet and update onchain fees. This is a much more lightweight alternative
1607	/// to [Wallet::maintenance] as it will not refresh VTXOs or sync the onchain wallet.
1608	///
1609	/// Notes:
1610	/// - The exit system will not be synced as doing so requires the onchain wallet.
1611	pub async fn sync(&self) {
1612		futures::join!(
1613			async {
1614				// NB: order matters here, if syncing call fails,
1615				// we still want to update the fee rates
1616				if let Err(e) = self.chain.update_fee_rates(self.config.fallback_fee_rate).await {
1617					warn!("Error updating fee rates: {:#}", e);
1618				}
1619			},
1620			async {
1621				if let Err(e) = self.sync_mailbox().await {
1622					warn!("Error in mailbox sync: {:#}", e);
1623				}
1624			},
1625			async {
1626				if let Err(e) = self.sync_pending_rounds().await {
1627					warn!("Error while trying to progress rounds awaiting confirmations: {:#}", e);
1628				}
1629			},
1630			async {
1631				if let Err(e) = self.sync_pending_lightning_send_vtxos().await {
1632					warn!("Error syncing pending lightning payments: {:#}", e);
1633				}
1634			},
1635			async {
1636				if let Err(e) = self.try_claim_all_lightning_receives(false).await {
1637					warn!("Error claiming pending lightning receives: {:#}", e);
1638				}
1639			},
1640			async {
1641				if let Err(e) = self.sync_pending_boards().await {
1642					warn!("Error syncing pending boards: {:#}", e);
1643				}
1644			},
1645			async {
1646				if let Err(e) = self.sync_pending_offboards().await {
1647					warn!("Error syncing pending offboards: {:#}", e);
1648				}
1649			}
1650		);
1651	}
1652
1653	/// Sync the transaction status of unilateral exits
1654	///
1655	/// This will not progress the unilateral exits in any way, it will merely check the
1656	/// transaction status of each transaction as well as check whether any exits have become
1657	/// claimable or have been claimed.
1658	pub async fn sync_exits(
1659		&self,
1660		onchain: &mut dyn ExitUnilaterally,
1661	) -> anyhow::Result<()> {
1662		self.exit.write().await.sync(&self, onchain).await?;
1663		Ok(())
1664	}
1665
1666	/// Drop a specific [Vtxo] from the database. This is destructive and will result in a loss of
1667	/// funds.
1668	pub async fn dangerous_drop_vtxo(&self, vtxo_id: VtxoId) -> anyhow::Result<()> {
1669		warn!("Drop vtxo {} from the database", vtxo_id);
1670		self.db.remove_vtxo(vtxo_id).await?;
1671		Ok(())
1672	}
1673
1674	/// Drop all VTXOs from the database. This is destructive and will result in a loss of funds.
1675	//TODO(stevenroose) improve the way we expose dangerous methods
1676	pub async fn dangerous_drop_all_vtxos(&self) -> anyhow::Result<()> {
1677		warn!("Dropping all vtxos from the db...");
1678		for vtxo in self.vtxos().await? {
1679			self.db.remove_vtxo(vtxo.id()).await?;
1680		}
1681
1682		self.exit.write().await.dangerous_clear_exit().await?;
1683		Ok(())
1684	}
1685
1686	/// Checks if the provided VTXO has some counterparty risk in the current wallet
1687	///
1688	/// An arkoor vtxo is considered to have some counterparty risk
1689	/// if it is (directly or not) based on round VTXOs that aren't owned by the wallet
1690	async fn has_counterparty_risk(&self, vtxo: &Vtxo<Full>) -> anyhow::Result<bool> {
1691		for past_pks in vtxo.past_arkoor_pubkeys() {
1692			let mut owns_any = false;
1693			for past_pk in past_pks {
1694				if self.db.get_public_key_idx(&past_pk).await?.is_some() {
1695					owns_any = true;
1696					break;
1697				}
1698			}
1699			if !owns_any {
1700				return Ok(true);
1701			}
1702		}
1703
1704		let my_clause = self.find_signable_clause(vtxo).await;
1705		Ok(!my_clause.is_some())
1706	}
1707
1708	/// If there are any VTXOs that match the "must-refresh" and "should-refresh" criteria with a
1709	/// total value over the P2TR dust limit, they are added to the round participation and an
1710	/// additional output is also created.
1711	///
1712	/// Note: This assumes that the base refresh fee has already been paid.
1713	async fn add_should_refresh_vtxos(
1714		&self,
1715		participation: &mut RoundParticipation,
1716	) -> anyhow::Result<()> {
1717		// Get VTXOs that need and should be refreshed, then filter out any duplicates before
1718		// adjusting the round participation.
1719		let tip = self.chain.tip().await?;
1720		let mut vtxos_to_refresh = self.spendable_vtxos_with(
1721			&RefreshStrategy::should_refresh(self, tip, self.chain.fee_rates().await.fast),
1722		).await?;
1723		if vtxos_to_refresh.is_empty() {
1724			return Ok(());
1725		}
1726
1727		let excluded_ids = participation.inputs.iter().map(|v| v.vtxo_id())
1728			.collect::<HashSet<_>>();
1729		let mut total_amount = Amount::ZERO;
1730		for i in (0..vtxos_to_refresh.len()).rev() {
1731			let vtxo = &vtxos_to_refresh[i];
1732			if excluded_ids.contains(&vtxo.id()) {
1733				vtxos_to_refresh.swap_remove(i);
1734				continue;
1735			}
1736			total_amount += vtxo.amount();
1737		}
1738		if vtxos_to_refresh.is_empty() {
1739			// VTXOs are already included in the round participation.
1740			return Ok(());
1741		}
1742
1743		// We need to verify that the output we add won't end up below the dust limit when fees are
1744		// applied. We can assume the base fee has been paid by the current refresh participation.
1745		let (_, ark_info) = self.require_server().await?;
1746		let fee = ark_info.fees.refresh.calculate_no_base_fee(
1747			vtxos_to_refresh.iter().map(|wv| VtxoFeeInfo::from_vtxo_and_tip(&wv.vtxo, tip)),
1748		).context("fee overflowed")?;
1749
1750		// Only add these VTXOs if the output amount would be above dust after fees.
1751		let output_amount = match validate_and_subtract_fee_min_dust(total_amount, fee) {
1752			Ok(amount) => amount,
1753			Err(e) => {
1754				trace!("Cannot add should-refresh VTXOs: {}", e);
1755				return Ok(());
1756			},
1757		};
1758		info!(
1759			"Adding {} extra VTXOs to round participation total = {}, fee = {}, output = {}",
1760			vtxos_to_refresh.len(), total_amount, fee, output_amount,
1761		);
1762		let (user_keypair, _) = self.derive_store_next_keypair().await?;
1763		let req = VtxoRequest {
1764			policy: VtxoPolicy::new_pubkey(user_keypair.public_key()),
1765			amount: output_amount,
1766		};
1767		participation.inputs.reserve(vtxos_to_refresh.len());
1768		participation.inputs.extend(vtxos_to_refresh.into_iter().map(|wv| wv.vtxo));
1769		participation.outputs.push(req);
1770
1771		Ok(())
1772	}
1773
1774	pub async fn build_refresh_participation<V: VtxoRef>(
1775		&self,
1776		vtxos: impl IntoIterator<Item = V>,
1777	) -> anyhow::Result<Option<RoundParticipation>> {
1778		let (vtxos, total_amount) = {
1779			let iter = vtxos.into_iter();
1780			let size_hint = iter.size_hint();
1781			let mut vtxos = Vec::<Vtxo<Full>>::with_capacity(size_hint.1.unwrap_or(size_hint.0));
1782			let mut amount = Amount::ZERO;
1783			for vref in iter {
1784				// We use a Vec here instead of a HashMap or a HashSet of IDs because for the kinds
1785				// of elements we expect to deal with, a Vec is likely to be quicker. The overhead
1786				// of hashing each ID and making additional allocations isn't likely to be worth it
1787				// for what is likely to be a handful of VTXOs or at most a couple of hundred.
1788				let id = vref.vtxo_id();
1789				if vtxos.iter().any(|v| v.id() == id) {
1790					bail!("duplicate VTXO id: {}", id);
1791				}
1792				let vtxo = if let Some(vtxo) = vref.into_full_vtxo() {
1793					vtxo
1794				} else {
1795					self.get_vtxo_by_id(id).await
1796						.with_context(|| format!("vtxo with id {} not found", id))?.vtxo
1797				};
1798				amount += vtxo.amount();
1799				vtxos.push(vtxo);
1800			}
1801			(vtxos, amount)
1802		};
1803
1804		if vtxos.is_empty() {
1805			info!("Skipping refresh since no VTXOs are provided.");
1806			return Ok(None);
1807		}
1808		ensure!(total_amount >= P2TR_DUST,
1809			"vtxo amount must be at least {} to participate in a round",
1810			P2TR_DUST,
1811		);
1812
1813		// Calculate refresh fees
1814		let (_, ark_info) = self.require_server().await?;
1815		let current_height = self.chain.tip().await?;
1816		let vtxo_fee_infos = vtxos.iter()
1817			.map(|v| VtxoFeeInfo::from_vtxo_and_tip(v, current_height));
1818		let fee = ark_info.fees.refresh.calculate(vtxo_fee_infos).context("fee overflowed")?;
1819		let output_amount = validate_and_subtract_fee_min_dust(total_amount, fee)?;
1820
1821		info!("Refreshing {} VTXOs (total amount = {}, fee = {}, output = {}).",
1822			vtxos.len(), total_amount, fee, output_amount,
1823		);
1824		let (user_keypair, _) = self.derive_store_next_keypair().await?;
1825		let req = VtxoRequest {
1826			policy: VtxoPolicy::Pubkey(PubkeyVtxoPolicy { user_pubkey: user_keypair.public_key() }),
1827			amount: output_amount,
1828		};
1829
1830		Ok(Some(RoundParticipation {
1831			inputs: vtxos,
1832			outputs: vec![req],
1833			unblinded_mailbox_id: None,
1834		}))
1835	}
1836
1837	/// This will refresh all provided VTXOs in an interactive round and wait until end
1838	///
1839	/// Returns the [RoundStatus] of the round if a successful refresh occurred.
1840	/// It will return [None] if no [Vtxo] needed to be refreshed.
1841	pub async fn refresh_vtxos<V: VtxoRef>(
1842		&self,
1843		vtxos: impl IntoIterator<Item = V>,
1844	) -> anyhow::Result<Option<RoundStatus>> {
1845		let mut participation = match self.build_refresh_participation(vtxos).await? {
1846			Some(participation) => participation,
1847			None => return Ok(None),
1848		};
1849
1850		if let Err(e) = self.add_should_refresh_vtxos(&mut participation).await {
1851			warn!("Error trying to add additional VTXOs that should be refreshed: {:#}", e);
1852		}
1853
1854		Ok(Some(self.participate_round(participation, Some(RoundMovement::Refresh)).await?))
1855	}
1856
1857	/// This will refresh all provided VTXOs in delegated (non-interactive) mode
1858	///
1859	/// Returns the [StoredRoundState] which can be used to track the round's
1860	/// progress later by calling sync. It will return [None] if no [Vtxo]
1861	/// needed to be refreshed.
1862	pub async fn refresh_vtxos_delegated<V: VtxoRef>(
1863		&self,
1864		vtxos: impl IntoIterator<Item = V>,
1865	) -> anyhow::Result<Option<StoredRoundState<Unlocked>>> {
1866		let mut part = match self.build_refresh_participation(vtxos).await? {
1867			Some(participation) => participation,
1868			None => return Ok(None),
1869		};
1870
1871		if let Err(e) = self.add_should_refresh_vtxos(&mut part).await {
1872			warn!("Error trying to add additional VTXOs that should be refreshed: {:#}", e);
1873		}
1874
1875		Ok(Some(self.join_next_round_delegated(part, Some(RoundMovement::Refresh)).await?))
1876	}
1877
1878	/// This will find all VTXOs that meets must-refresh criteria. Then, if there are some VTXOs to
1879	/// refresh, it will also add those that meet should-refresh criteria.
1880	pub async fn get_vtxos_to_refresh(&self) -> anyhow::Result<Vec<WalletVtxo>> {
1881		let vtxos = self.spendable_vtxos_with(&RefreshStrategy::should_refresh_if_must(
1882			self,
1883			self.chain.tip().await?,
1884			self.chain.fee_rates().await.fast,
1885		)).await?;
1886		Ok(vtxos)
1887	}
1888
1889	/// Returns the block height at which the first VTXO will expire
1890	pub async fn get_first_expiring_vtxo_blockheight(
1891		&self,
1892	) -> anyhow::Result<Option<BlockHeight>> {
1893		Ok(self.spendable_vtxos().await?.iter().map(|v| v.expiry_height()).min())
1894	}
1895
1896	/// Returns the next block height at which we have a VTXO that we
1897	/// want to refresh
1898	pub async fn get_next_required_refresh_blockheight(
1899		&self,
1900	) -> anyhow::Result<Option<BlockHeight>> {
1901		let first_expiry = self.get_first_expiring_vtxo_blockheight().await?;
1902		Ok(first_expiry.map(|h| h.saturating_sub(self.config.vtxo_refresh_expiry_threshold)))
1903	}
1904
1905	/// Select several VTXOs to cover the provided amount
1906	///
1907	/// VTXOs are selected soonest-expiring-first.
1908	///
1909	/// Returns an error if amount cannot be reached.
1910	async fn select_vtxos_to_cover(
1911		&self,
1912		amount: Amount,
1913	) -> anyhow::Result<Vec<WalletVtxo>> {
1914		let mut vtxos = self.spendable_vtxos().await?;
1915		vtxos.sort_by_key(|v| v.expiry_height());
1916
1917		// Iterate over VTXOs until the required amount is reached
1918		let mut result = Vec::new();
1919		let mut total_amount = Amount::ZERO;
1920		for input in vtxos {
1921			total_amount += input.amount();
1922			result.push(input);
1923
1924			if total_amount >= amount {
1925				return Ok(result)
1926			}
1927		}
1928
1929		bail!("Insufficient money available. Needed {} but {} is available",
1930			amount, total_amount,
1931		);
1932	}
1933
1934	/// Determines which VTXOs to use for a fee-paying transaction where the fee is added on top of
1935	/// the desired amount. E.g., a lightning payment, a send-onchain payment.
1936	///
1937	/// Returns a collection of VTXOs capable of covering the desired amount as well as the
1938	/// calculated fee.
1939	async fn select_vtxos_to_cover_with_fee<F>(
1940		&self,
1941		amount: Amount,
1942		calc_fee: F,
1943	) -> anyhow::Result<(Vec<WalletVtxo>, Amount)>
1944	where
1945		F: for<'a> Fn(
1946			Amount, std::iter::Copied<std::slice::Iter<'a, VtxoFeeInfo>>,
1947		) -> anyhow::Result<Amount>,
1948	{
1949		let tip = self.chain.tip().await?;
1950
1951		// We need to loop to find suitable inputs due to the VTXOs having a direct impact on
1952		// how much we must pay in fees.
1953		const MAX_ITERATIONS: usize = 100;
1954		let mut fee = Amount::ZERO;
1955		let mut fee_info = Vec::new();
1956		for _ in 0..MAX_ITERATIONS {
1957			let required = amount.checked_add(fee)
1958				.context("Amount + fee overflow")?;
1959
1960			let vtxos = self.select_vtxos_to_cover(required).await
1961				.context("Could not find enough suitable VTXOs to cover payment + fees")?;
1962
1963			fee_info.reserve(vtxos.len());
1964			let mut vtxo_amount = Amount::ZERO;
1965			for vtxo in &vtxos {
1966				vtxo_amount += vtxo.amount();
1967				fee_info.push(VtxoFeeInfo::from_vtxo_and_tip(vtxo, tip));
1968			}
1969
1970			fee = calc_fee(amount, fee_info.iter().copied())?;
1971			if amount + fee <= vtxo_amount {
1972				trace!("Selected vtxos to cover amount + fee: amount = {}, fee = {}, total inputs = {}",
1973					amount, fee, vtxo_amount,
1974				);
1975				return Ok((vtxos, fee));
1976			}
1977			trace!("VTXO sum of {} did not exceed amount {} and fee {}, iterating again",
1978				vtxo_amount, amount, fee,
1979			);
1980			fee_info.clear();
1981		}
1982		bail!("Fee calculation did not converge after maximum iterations")
1983	}
1984
1985	/// Starts a daemon for the wallet.
1986	///
1987	/// Note:
1988	/// - This function doesn't check if a daemon is already running,
1989	/// so it's possible to start multiple daemons by mistake.
1990	pub fn run_daemon(
1991		self: &Arc<Self>,
1992		onchain: Option<Arc<RwLock<dyn DaemonizableOnchainWallet>>>,
1993	) -> anyhow::Result<DaemonHandle> {
1994		// NB currently can't error but it's a pretty common method and quite likely that error
1995		// cases will be introduces later
1996		Ok(crate::daemon::start_daemon(self.clone(), onchain))
1997	}
1998
1999	/// Registers VTXOs with the server by sending their signed transaction chains.
2000	/// This should be called before spending VTXOs to ensure the server can
2001	/// publish forfeits if needed.
2002	pub async fn register_vtxos_with_server(
2003		&self,
2004		vtxos: &[impl AsRef<Vtxo<Full>>],
2005	) -> anyhow::Result<()> {
2006		if vtxos.is_empty() {
2007			return Ok(());
2008		}
2009
2010		let (mut srv, _) = self.require_server().await?;
2011		srv.client.register_vtxos(protos::RegisterVtxosRequest {
2012			vtxos: vtxos.iter().map(|v| v.as_ref().serialize()).collect(),
2013		}).await.context("failed to register vtxos")?;
2014
2015		Ok(())
2016	}
2017}