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