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