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()?;
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 exit;
292pub mod movement;
293pub mod onchain;
294pub mod persist;
295pub mod round;
296pub mod vtxo_state;
297pub mod vtxo_selection;
298
299pub use self::config::Config;
300pub use self::persist::sqlite::SqliteClient;
301pub use self::vtxo_state::WalletVtxo;
302
303mod config;
304mod lnurl;
305mod psbtext;
306
307use std::collections::{HashMap, HashSet};
308
309use std::convert::TryFrom;
310use std::str::FromStr;
311use std::sync::Arc;
312
313use anyhow::{bail, Context};
314use bip39::Mnemonic;
315use bitcoin::{Amount, FeeRate, Network, OutPoint, ScriptBuf, Transaction};
316use bitcoin::bip32::{self, Fingerprint};
317use bitcoin::consensus::deserialize;
318use bitcoin::hashes::Hash;
319use bitcoin::hex::DisplayHex;
320use bitcoin::secp256k1::{self, Keypair, PublicKey};
321use lnurllib::lightning_address::LightningAddress;
322use lightning_invoice::Bolt11Invoice;
323use lightning::util::ser::Writeable;
324use log::{trace, debug, info, warn, error};
325use futures::StreamExt;
326
327use ark::{ArkInfo, OffboardRequest, ProtocolEncoding, Vtxo, VtxoId, VtxoPolicy, VtxoRequest};
328use ark::address::VtxoDelivery;
329use ark::arkoor::ArkoorPackageBuilder;
330use ark::board::{BoardBuilder, BOARD_FUNDING_TX_VTXO_VOUT};
331use ark::lightning::{Bolt12Invoice, Bolt12InvoiceExt, Invoice, Offer, Preimage, PaymentHash};
332use ark::musig;
333use ark::rounds::RoundId;
334use ark::tree::signed::{CachedSignedVtxoTree, SignedVtxoTreeSpec};
335use ark::vtxo::{VtxoRef, PubkeyVtxoPolicy, VtxoPolicyKind};
336use server_rpc::{self as rpc, protos, ServerConnection, TryFromBytes};
337use bitcoin_ext::{AmountExt, BlockDelta, BlockHeight, P2TR_DUST, TxStatus};
338
339use crate::exit::Exit;
340use crate::movement::{Movement, MovementArgs, MovementKind};
341use crate::onchain::{ChainSource, PreparePsbt, ExitUnilaterally, Utxo, GetWalletTx, SignPsbt};
342use crate::persist::BarkPersister;
343use crate::persist::models::{LightningReceive, PendingLightningSend, StoredVtxoRequest};
344use crate::round::{DesiredRoundParticipation, RoundParticipation, RoundResult};
345use crate::vtxo_selection::{FilterVtxos, VtxoFilter};
346use crate::vtxo_state::{VtxoState, VtxoStateKind, UNSPENT_STATES};
347use crate::vtxo_selection::RefreshStrategy;
348
349const ARK_PURPOSE_INDEX: u32 = 350;
350
351/// Leniency delta to allow claim when blocks were mined between htlc
352/// receive and claim preparation
353const LIGHTNING_PREPARE_CLAIM_DELTA: BlockDelta = 2;
354
355lazy_static::lazy_static! {
356	/// Global secp context.
357	static ref SECP: secp256k1::Secp256k1<secp256k1::All> = secp256k1::Secp256k1::new();
358}
359
360/// The detailled balance of a Lightning receive.
361#[derive(Debug, Clone)]
362pub struct LightningReceiveBalance {
363	/// Sum of all pending lightning invoices
364	pub total: Amount,
365	/// Sum of all invoices for which we received the HTLC VTXOs
366	pub claimable: Amount,
367}
368
369/// The different balances of a Bark wallet.
370#[derive(Debug, Clone)]
371pub struct Balance {
372	/// Coins that are spendable in the Ark, either in-round or out-of-round.
373	pub spendable: Amount,
374	/// Coins that are in the process of being sent over Lightning.
375	pub pending_lightning_send: Amount,
376	/// Coins that are in the process of being received over Lightning.
377	pub pending_lightning_receive: LightningReceiveBalance,
378	/// Coins locked in a round.
379	pub pending_in_round: Amount,
380	/// Coins that are in the process of unilaterally exiting the Ark.
381	/// None if exit subsystem was unavailable
382	pub pending_exit: Option<Amount>,
383	/// Coins that are pending sufficient confirmations from board transactions.
384	pub pending_board: Amount,
385}
386
387struct ArkoorCreateResult {
388	input: Vec<Vtxo>,
389	created: Vec<Vtxo>,
390	change: Option<Vtxo>,
391}
392
393pub struct UtxoInfo {
394	pub outpoint: OutPoint,
395	pub amount: Amount,
396	pub confirmation_height: Option<u32>,
397}
398
399impl From<Utxo> for UtxoInfo {
400	fn from(value: Utxo) -> Self {
401		match value {
402			Utxo::Local(o) => UtxoInfo {
403				outpoint: o.outpoint,
404				amount: o.amount,
405				confirmation_height: o.confirmation_height,
406			},
407			Utxo::Exit(e) => UtxoInfo {
408				outpoint: e.vtxo.point(),
409				amount: e.vtxo.amount(),
410				confirmation_height: Some(e.height),
411			},
412		}
413	}
414}
415
416/// Describes a completed transition of funds from onchain to offchain.
417#[derive(Debug, Clone, PartialEq, Eq, Hash)]
418pub struct Board {
419	/// The [bitcoin::Txid] of the funding-transaction.
420	/// This is the transaction that has to be confirmed
421	/// onchain for the board to succeed.
422	pub funding_txid: bitcoin::Txid,
423	/// The info for each [ark::Vtxo] that was created
424	/// in this board.
425	///
426	/// Currently, this is always a vector of length 1
427	pub vtxos: Vec<Vtxo>,
428}
429
430/// Describes a completed transition of funds from offchain to onchain collaboratively with the
431/// Ark server.
432#[derive(Debug, Clone, PartialEq, Eq, Hash)]
433pub struct Offboard {
434	/// The [RoundId] of the round in which the offboard occurred
435	pub round: RoundId,
436}
437
438/// Represents an offchain balance structure consisting of available funds, pending amounts in
439/// unconfirmed rounds, and pending exits.
440pub struct OffchainBalance {
441	/// Funds currently available for use. This reflects the spendable balance.
442	pub available: Amount,
443	/// Funds that are pending in unconfirmed operational rounds.
444	pub pending_in_round: Amount,
445	/// Funds being unilaterally exited. These may require more onchain confirmations to become
446	/// available onchain.
447	pub pending_exit: Amount,
448}
449
450/// Read-only properties of the Bark wallet.
451#[derive(Debug, Clone)]
452pub struct WalletProperties {
453	/// The Bitcoin network to run Bark on.
454	///
455	/// Default value: signet.
456	pub network: Network,
457
458	/// The wallet fingerpint
459	///
460	/// Used on wallet loading to check mnemonic correctness
461	pub fingerprint: Fingerprint,
462}
463
464/// Struct representing an extended private key derived from a
465/// wallet's seed, used to derive child VTXO keypairs
466///
467/// The VTXO seed is derived by applying a hardened derivation
468/// step at index 350 from the wallet's seed.
469pub struct VtxoSeed(bip32::Xpriv);
470
471impl VtxoSeed {
472	fn new(network: Network, seed: &[u8; 64]) -> Self {
473		let master = bip32::Xpriv::new_master(network, seed).unwrap();
474
475		Self(master.derive_priv(&SECP, &[ARK_PURPOSE_INDEX.into()]).unwrap())
476	}
477
478	fn fingerprint(&self) -> Fingerprint {
479		self.0.fingerprint(&SECP)
480	}
481
482	fn derive_keypair(&self, idx: u32) -> Keypair {
483		self.0.derive_priv(&SECP, &[idx.into()]).unwrap().to_keypair(&SECP)
484	}
485}
486
487/// The central entry point for using this library as an Ark wallet.
488///
489/// Overview
490/// - Wallet encapsulates the complete Ark client implementation:
491///   - address generation (Ark addresses/keys)
492///     - [Wallet::new_address],
493///     - [Wallet::new_address_with_index],
494///     - [Wallet::peak_address],
495///     - [Wallet::validate_arkoor_address]
496///   - boarding onchain funds into Ark from an onchain wallet (see [onchain::OnchainWallet])
497///     - [Wallet::board_amount],
498///     - [Wallet::board_all]
499///   - offboarding Ark funds to move them back onchain
500///     - [Wallet::offboard_vtxos],
501///     - [Wallet::offboard_all]
502///   - sending and receiving Ark payments (including to BOLT11/BOLT12 invoices)
503///     - [Wallet::send_arkoor_payment],
504///     - [Wallet::send_round_onchain_payment],
505///     - [Wallet::send_lightning_payment],
506///     - [Wallet::send_lnaddr],
507///     - [Wallet::pay_offer]
508///   - tracking, selecting, and refreshing VTXOs
509///     - [Wallet::vtxos],
510///     - [Wallet::vtxos_with],
511///     - [Wallet::refresh_vtxos]
512///   - syncing with the Ark server, unilateral exits and performing general maintenance
513///     - [Wallet::maintenance]: Syncs everything offchain-related and refreshes VTXOs where
514///       necessary,
515///     - [Wallet::maintenance_with_onchain]: The same as [Wallet::maintenance] but also syncs the
516///       onchain wallet and unilateral exits,
517///     - [Wallet::maintenance_refresh]: Refreshes VTXOs where necessary without syncing anything,
518///     - [Wallet::sync]: Syncs network fee-rates, ark rounds and arkoor payments,
519///     - [Wallet::sync_exits]: Updates the status of unilateral exits,
520///     - [Wallet::sync_pending_lightning_send_vtxos]: Updates the status of pending lightning payments,
521///     - [Wallet::check_and_claim_all_open_ln_receives]: Wait for payment receipt of all open invoices, then claim them,
522///     - [Wallet::sync_pending_boards]: Registers boards which are available for use
523///       in offchain payments
524///
525/// Key capabilities
526/// - Address management:
527///   - derive and peek deterministic Ark addresses and their indices
528/// - Funds lifecycle:
529///   - board funds from an external onchain wallet onto the Ark
530///   - send out-of-round Ark payments (arkoor)
531///   - offboard funds to onchain addresses
532///   - manage HTLCs and Lightning receives/sends
533/// - VTXO management:
534///   - query spendable and pending VTXOs
535///   - refresh expiring or risky VTXOs
536///   - compute balance broken down by spendable/pending states
537/// - Synchronization and maintenance:
538///   - sync against the Ark server and the onchain source
539///   - reconcile pending rounds, exits, and offchain state
540///   - periodic maintenance helpers (e.g., auto-register boards, refresh policies)
541///
542/// Construction and persistence
543/// - A [Wallet] is opened or created using a mnemonic and a backend implementing [BarkPersister].
544///   - [Wallet::create],
545///   - [Wallet::open]
546/// - Creation allows the use of an optional onchain wallet for boarding and [Exit] functionality.
547///   It also initializes any internal state and connects to the [onchain::ChainSource]. See
548///   [onchain::OnchainWallet] for an implementation of an onchain wallet using BDK.
549///   - [Wallet::create_with_onchain],
550///   - [Wallet::open_with_onchain]
551///
552/// Example
553/// ```
554/// # #[cfg(any(test, doc))]
555/// # async fn demo() -> anyhow::Result<()> {
556/// # use std::sync::Arc;
557/// # use bark::{Config, SqliteClient, Wallet};
558/// # use bark::onchain::OnchainWallet;
559/// # use bark::persist::BarkPersister;
560/// # use bark::persist::sqlite::helpers::in_memory_db;
561/// # use bip39::Mnemonic;
562/// # use bitcoin::Network;
563/// # let (db_path, _) = in_memory_db();
564/// let network = Network::Signet;
565/// let mnemonic = Mnemonic::generate(12)?;
566/// let cfg = Config {
567///   server_address: String::from("https://ark.signet.2nd.dev"),
568///   esplora_address: Some(String::from("https://esplora.signet.2nd.dev")),
569///   ..Default::default()
570/// };
571///
572/// // You can either use the included SQLite implementation or create your own.
573/// let persister = SqliteClient::open(db_path).await?;
574/// let db: Arc<dyn BarkPersister> = Arc::new(persister);
575///
576/// // Load or create an onchain wallet if needed
577/// let onchain_wallet = OnchainWallet::load_or_create(network, mnemonic.to_seed(""), db.clone())?;
578///
579/// // Create or open the Ark wallet
580/// let mut wallet = Wallet::create_with_onchain(
581/// 	&mnemonic,
582/// 	network,
583/// 	cfg.clone(),
584/// 	db,
585/// 	&onchain_wallet,
586/// 	false,
587/// ).await?;
588/// // let mut wallet = Wallet::create(&mnemonic, network, cfg.clone(), db.clone(), false).await?;
589/// // let mut wallet = Wallet::open(&mnemonic, db.clone(), cfg.clone()).await?;
590/// // let mut wallet = Wallet::open_with_onchain(
591/// //    &mnemonic, network, cfg.clone(), db.clone(), &onchain_wallet
592/// // ).await?;
593///
594/// // There are two main ways to update the wallet, the primary is to use one of the maintenance
595/// // commands which will sync everything, refresh VTXOs and reconcile pending lightning payments.
596/// wallet.maintenance().await?;
597/// wallet.maintenance_with_onchain(&mut onchain_wallet).await?;
598///
599/// // Alternatively, you can use the fine-grained sync commands to sync individual parts of the
600/// // wallet state and use `maintenance_refresh` where necessary to refresh VTXOs.
601/// wallet.sync().await?;
602/// wallet.sync_pending_lightning_send_vtxos().await?;
603/// wallet.register_all_confirmed_boards(&mut onchain_wallet).await?;
604/// wallet.sync_exits(&mut onchain_wallet).await?;
605/// wallet.maintenance_refresh().await?;
606///
607/// // Generate a new Ark address to receive funds via arkoor
608/// let addr = wallet.new_address()?;
609///
610/// // Query balance and VTXOs
611/// let balance = wallet.balance()?;
612/// let vtxos = wallet.vtxos()?;
613///
614/// // Progress any unilateral exits, make sure to sync first
615/// wallet.exit.progress_exit(&mut onchain_wallet, None).await?;
616///
617/// # Ok(())
618/// # }
619/// ```
620pub struct Wallet {
621	/// The chain source the wallet is connected to
622	pub chain: Arc<ChainSource>,
623
624	/// Exit subsystem handling unilateral exits and on-chain reconciliation outside Ark rounds.
625	pub exit: tokio::sync::RwLock<Exit>,
626
627	/// Active runtime configuration for networking, fees, policies and thresholds.
628	config: Config,
629
630	/// Persistence backend for wallet state (keys metadata, VTXOs, movements, round state, etc.).
631	db: Arc<dyn BarkPersister>,
632
633	/// Deterministic seed material used to derive VTXO ownership keypairs and addresses.
634	vtxo_seed: VtxoSeed,
635
636	/// Optional live connection to an Ark server for round participation and synchronization.
637	server: Option<ServerConnection>,
638
639}
640
641impl Wallet {
642	/// Creates a [onchain::ChainSource] instance to communicate with an onchain backend from the
643	/// given [Config].
644	pub fn chain_source<P: BarkPersister>(
645		config: &Config,
646	) -> anyhow::Result<onchain::ChainSourceSpec> {
647		if let Some(ref url) = config.esplora_address {
648			Ok(onchain::ChainSourceSpec::Esplora {
649				url: url.clone(),
650			})
651		} else if let Some(ref url) = config.bitcoind_address {
652			let auth = if let Some(ref c) = config.bitcoind_cookiefile {
653				bitcoin_ext::rpc::Auth::CookieFile(c.clone())
654			} else {
655				bitcoin_ext::rpc::Auth::UserPass(
656					config.bitcoind_user.clone().context("need bitcoind auth config")?,
657					config.bitcoind_pass.clone().context("need bitcoind auth config")?,
658				)
659			};
660			Ok(onchain::ChainSourceSpec::Bitcoind {
661				url: url.clone(),
662				auth,
663			})
664		} else {
665			bail!("Need to either provide esplora or bitcoind info");
666		}
667	}
668
669	/// Verifies that the bark [Wallet] can be used with the configured [onchain::ChainSource].
670	/// More specifically, if the [onchain::ChainSource] connects to Bitcoin Core it must be
671	/// a high enough version to support ephemeral anchors.
672	pub fn require_chainsource_version(&self) -> anyhow::Result<()> {
673		self.chain.require_version()
674	}
675
676	/// Derive and store the keypair directly after currently last revealed one,
677	/// together with its index.
678	pub fn derive_store_next_keypair(&self) -> anyhow::Result<(Keypair, u32)> {
679		let last_revealed = self.db.get_last_vtxo_key_index()?;
680
681		let index = last_revealed.map(|i| i + 1).unwrap_or(u32::MIN);
682		let keypair = self.vtxo_seed.derive_keypair(index);
683
684		self.db.store_vtxo_key(index, keypair.public_key())?;
685		Ok((keypair, index))
686	}
687
688	/// Retrieves a keypair based on the provided index and checks if the corresponding public key
689	/// exists in the [Vtxo] database.
690	///
691	/// # Arguments
692	///
693	/// * `index` - The index used to derive a keypair.
694	///
695	/// # Returns
696	///
697	/// * `Ok(Keypair)` - If the keypair is successfully derived and its public key exists in the
698	///   database.
699	/// * `Err(anyhow::Error)` - If the public key does not exist in the database or if an error
700	///   occurs during the database query.
701	pub fn peak_keypair(&self, index: u32) -> anyhow::Result<Keypair> {
702		let keypair = self.vtxo_seed.derive_keypair(index);
703		if self.db.get_public_key_idx(&keypair.public_key())?.is_some() {
704			Ok(keypair)
705		} else {
706			bail!("VTXO key {} does not exist, please derive it first", index)
707		}
708	}
709
710
711	/// Retrieves the [Keypair] for a provided [PublicKey]
712	///
713	/// # Arguments
714	///
715	/// * `public_key` - The public key for which the keypair must be found
716	///
717	/// # Returns
718	/// * `Ok(Some(u32, Keypair))` - If the pubkey is found, the derivation-index and keypair are
719	///                              returned
720	/// * `Ok(None)` - If the pubkey cannot be found in the database
721	/// * `Err(anyhow::Error)` - If an error occurred related to the database query
722	pub fn pubkey_keypair(&self, public_key: &PublicKey) -> anyhow::Result<Option<(u32, Keypair)>> {
723		if let Some(index) = self.db.get_public_key_idx(&public_key)? {
724			Ok(Some((index, self.vtxo_seed.derive_keypair(index))))
725		} else {
726			Ok(None)
727		}
728	}
729
730	/// Retrieves the [Keypair] for a provided [Vtxo]
731	///
732	/// # Arguments
733	///
734	/// * `vtxo` - The vtxo for which the key must be found
735	///
736	/// # Returns
737	/// * `Ok(Some(Keypair))` - If the pubkey is found, the keypair is returned
738	/// * `Err(anyhow::Error)` - If the corresponding public key doesn't exist
739	///   in the database or a database error occurred.
740	pub fn get_vtxo_key(&self, vtxo: &Vtxo) -> anyhow::Result<Keypair> {
741		let idx = self.db.get_public_key_idx(&vtxo.user_pubkey())?
742			.context("VTXO key not found")?;
743		Ok(self.vtxo_seed.derive_keypair(idx))
744	}
745
746	/// Generate a new [ark::Address].
747	pub fn new_address(&self) -> anyhow::Result<ark::Address> {
748		let ark = &self.require_server()?;
749		let network = self.properties()?.network;
750		let pubkey = self.derive_store_next_keypair()?.0.public_key();
751
752		Ok(ark::Address::builder()
753			.testnet(network != bitcoin::Network::Bitcoin)
754			.server_pubkey(ark.info.server_pubkey)
755			.pubkey_policy(pubkey)
756			.into_address().unwrap())
757	}
758
759	/// Peak for an [ark::Address] at the given key index.
760	///
761	/// May return an error if the address at the given index has not been derived yet.
762	pub fn peak_address(&self, index: u32) -> anyhow::Result<ark::Address> {
763		let ark = &self.require_server()?;
764		let network = self.properties()?.network;
765		let pubkey = self.peak_keypair(index)?.public_key();
766
767		Ok(ark::Address::builder()
768			.testnet(network != Network::Bitcoin)
769			.server_pubkey(ark.info.server_pubkey)
770			.pubkey_policy(pubkey)
771			.into_address().unwrap())
772	}
773
774	/// Generate a new [ark::Address] and returns the index of the key used to create it.
775	///
776	/// This derives and stores the keypair directly after currently last revealed one.
777	pub fn new_address_with_index(&self) -> anyhow::Result<(ark::Address, u32)> {
778		let ark = &self.require_server()?;
779		let network = self.properties()?.network;
780		let (keypair, index) = self.derive_store_next_keypair()?;
781		let pubkey = keypair.public_key();
782		let addr = ark::Address::builder()
783			.testnet(network != bitcoin::Network::Bitcoin)
784			.server_pubkey(ark.info.server_pubkey)
785			.pubkey_policy(pubkey)
786			.into_address()?;
787		Ok((addr, index))
788	}
789
790	/// Create a new wallet without an optional onchain backend. This will restrict features such as
791	/// boarding and unilateral exit.
792	///
793	/// The `force` flag will allow you to create the wallet even if a connection to the Ark server
794	/// cannot be established, it will not overwrite a wallet which has already been created.
795	pub async fn create<P: BarkPersister>(
796		mnemonic: &Mnemonic,
797		network: Network,
798		config: Config,
799		db: Arc<P>,
800		force: bool,
801	) -> anyhow::Result<Wallet> {
802		trace!("Config: {:?}", config);
803		if let Some(existing) = db.read_properties()? {
804			trace!("Existing config: {:?}", existing);
805			bail!("cannot overwrite already existing config")
806		}
807
808		if !force {
809			if let Err(_) = ServerConnection::connect(&config.server_address, network).await {
810				bail!("Not connected to a server. If you are sure use the --force flag.");
811			}
812		}
813
814		let wallet_fingerprint = VtxoSeed::new(network, &mnemonic.to_seed("")).fingerprint();
815		let properties = WalletProperties {
816			network: network,
817			fingerprint: wallet_fingerprint,
818		};
819
820		// write the config to db
821		db.init_wallet(&properties).context("cannot init wallet in the database")?;
822
823		// from then on we can open the wallet
824		let wallet = Wallet::open(&mnemonic, db, config).await.context("failed to open wallet")?;
825		wallet.require_chainsource_version()?;
826
827		Ok(wallet)
828	}
829
830	/// Create a new wallet with an onchain backend. This enables full Ark functionality. A default
831	/// implementation of an onchain wallet when the `onchain_bdk` feature is enabled. See
832	/// [onchain::OnchainWallet] for more details. Alternatively, implement [ExitUnilaterally] if
833	/// you have your own onchain wallet implementation.
834	///
835	/// The `force` flag will allow you to create the wallet even if a connection to the Ark server
836	/// cannot be established, it will not overwrite a wallet which has already been created.
837	pub async fn create_with_onchain<P: BarkPersister, W: ExitUnilaterally>(
838		mnemonic: &Mnemonic,
839		network: Network,
840		config: Config,
841		db: Arc<P>,
842		onchain: &W,
843		force: bool,
844	) -> anyhow::Result<Wallet> {
845		let mut wallet = Wallet::create(mnemonic, network, config, db, force).await?;
846		wallet.exit.get_mut().load(onchain).await?;
847		Ok(wallet)
848	}
849
850	/// Loads the bark wallet from the given database ensuring the fingerprint remains consistent.
851	pub async fn open<P: BarkPersister>(
852		mnemonic: &Mnemonic,
853		db: Arc<P>,
854		config: Config,
855	) -> anyhow::Result<Wallet> {
856		let properties = db.read_properties()?.context("Wallet is not initialised")?;
857
858		let seed = mnemonic.to_seed("");
859		let vtxo_seed = VtxoSeed::new(properties.network, &seed);
860
861		if properties.fingerprint != vtxo_seed.fingerprint() {
862			bail!("incorrect mnemonic")
863		}
864
865		let chain_source = if let Some(ref url) = config.esplora_address {
866			onchain::ChainSourceSpec::Esplora {
867				url: url.clone(),
868			}
869		} else if let Some(ref url) = config.bitcoind_address {
870			let auth = if let Some(ref c) = config.bitcoind_cookiefile {
871				bitcoin_ext::rpc::Auth::CookieFile(c.clone())
872			} else {
873				bitcoin_ext::rpc::Auth::UserPass(
874					config.bitcoind_user.clone().context("need bitcoind auth config")?,
875					config.bitcoind_pass.clone().context("need bitcoind auth config")?,
876				)
877			};
878			onchain::ChainSourceSpec::Bitcoind { url: url.clone(), auth }
879		} else {
880			bail!("Need to either provide esplora or bitcoind info");
881		};
882
883		let chain_source_client = ChainSource::new(
884			chain_source, properties.network, config.fallback_fee_rate,
885		).await?;
886		let chain = Arc::new(chain_source_client);
887
888		let server = match ServerConnection::connect(
889			&config.server_address, properties.network,
890		).await {
891			Ok(s) => Some(s),
892			Err(e) => {
893				warn!("Ark server handshake failed: {}", e);
894				None
895			}
896		};
897
898		let exit = tokio::sync::RwLock::new(Exit::new(db.clone(), chain.clone()).await?);
899
900		Ok(Wallet { config, db, vtxo_seed, exit, server, chain })
901	}
902
903	/// Similar to [Wallet::open] however this also unilateral exits using the provided onchain
904	/// wallet.
905	pub async fn open_with_onchain<P: BarkPersister, W: ExitUnilaterally>(
906		mnemonic: &Mnemonic,
907		db: Arc<P>,
908		onchain: &W,
909		cfg: Config,
910	) -> anyhow::Result<Wallet> {
911		let mut wallet = Wallet::open(mnemonic, db, cfg).await?;
912		wallet.exit.get_mut().load(onchain).await?;
913		Ok(wallet)
914	}
915
916	/// Returns the config used to create/load the bark [Wallet].
917	pub fn config(&self) -> &Config {
918		&self.config
919	}
920
921	/// Retrieves the [WalletProperties] of the current bark [Wallet].
922	pub fn properties(&self) -> anyhow::Result<WalletProperties> {
923		let properties = self.db.read_properties()?.context("Wallet is not initialised")?;
924		Ok(properties)
925	}
926
927	fn require_server(&self) -> anyhow::Result<ServerConnection> {
928		self.server.clone().context("You should be connected to Ark server to perform this action")
929	}
930
931	/// Return [ArkInfo] fetched on last handshake with the Ark server
932	pub fn ark_info(&self) -> Option<&ArkInfo> {
933		self.server.as_ref().map(|a| &a.info)
934	}
935
936	/// Return the [Balance] of the wallet.
937	///
938	/// Make sure you sync before calling this method.
939	pub fn balance(&self) -> anyhow::Result<Balance> {
940		let vtxos = self.vtxos()?;
941
942		let spendable = {
943			let mut v = vtxos.iter().collect();
944			VtxoStateKind::Spendable.filter_vtxos(&mut v)?;
945			v.into_iter().map(|v| v.amount()).sum::<Amount>()
946		};
947
948		let pending_lightning_send = self.pending_lightning_send_vtxos()?.iter().map(|v| v.amount())
949			.sum::<Amount>();
950
951		let pending_lightning_receive = self.pending_lightning_receive_balance()?;
952
953		let pending_board = self.pending_board_vtxos()?.iter().map(|v| v.amount()).sum::<Amount>();
954
955		let pending_in_round = self.db.get_in_round_vtxos()?.iter()
956			.map(|v| v.amount()).sum();
957
958		let pending_exit = self.exit.try_read().ok().map(|e| e.pending_total());
959
960		Ok(Balance {
961			spendable,
962			pending_in_round,
963			pending_lightning_send,
964			pending_lightning_receive,
965			pending_exit,
966			pending_board,
967		})
968	}
969
970	/// Retrieves the full state of a [Vtxo] for a given [VtxoId] if it exists in the database.
971	pub fn get_vtxo_by_id(&self, vtxo_id: VtxoId) -> anyhow::Result<WalletVtxo> {
972		let vtxo = self.db.get_wallet_vtxo(vtxo_id)
973			.with_context(|| format!("Error when querying vtxo {} in database", vtxo_id))?
974			.with_context(|| format!("The VTXO with id {} cannot be found", vtxo_id))?;
975		Ok(vtxo)
976	}
977
978	/// Fetches all wallet fund movements ordered from newest to oldest.
979	pub fn movements(&self) -> anyhow::Result<Vec<Movement>> {
980		Ok(self.db.get_movements()?)
981	}
982
983	/// Returns all VTXOs from the database.
984	pub fn all_vtxos(&self) -> anyhow::Result<Vec<WalletVtxo>> {
985		Ok(self.db.get_all_vtxos()?)
986	}
987
988	/// Returns all not spent vtxos
989	pub fn vtxos(&self) -> anyhow::Result<Vec<WalletVtxo>> {
990		Ok(self.db.get_vtxos_by_state(&UNSPENT_STATES)?)
991	}
992
993	/// Returns all vtxos matching the provided predicate
994	pub fn vtxos_with(&self, filter: &impl FilterVtxos) -> anyhow::Result<Vec<WalletVtxo>> {
995		let mut vtxos = self.vtxos()?;
996		filter.filter_vtxos(&mut vtxos).context("error filtering vtxos")?;
997		Ok(vtxos)
998	}
999
1000	/// Returns all spendable vtxos
1001	pub fn spendable_vtxos(&self) -> anyhow::Result<Vec<WalletVtxo>> {
1002		Ok(self.vtxos_with(&VtxoStateKind::Spendable)?)
1003	}
1004
1005	/// Returns all spendable vtxos matching the provided predicate
1006	pub fn spendable_vtxos_with(
1007		&self,
1008		filter: &impl FilterVtxos,
1009	) -> anyhow::Result<Vec<WalletVtxo>> {
1010		let mut vtxos = self.spendable_vtxos()?;
1011		filter.filter_vtxos(&mut vtxos).context("error filtering vtxos")?;
1012		Ok(vtxos)
1013	}
1014
1015	/// Returns all in-round VTXOs matching the provided predicate
1016	pub fn inround_vtxos_with(&self, filter: &impl FilterVtxos) -> anyhow::Result<Vec<WalletVtxo>> {
1017		let mut vtxos = self.db.get_in_round_vtxos()?;
1018		filter.filter_vtxos(&mut vtxos).context("error filtering vtxos")?;
1019		Ok(vtxos)
1020	}
1021
1022	/// Queries the database for any VTXO that is an unregistered board. There is a lag time between
1023	/// when a board is created and when it becomes spendable.
1024	///
1025	/// See [ArkInfo::required_board_confirmations] and [Wallet::sync_pending_boards].
1026	pub fn pending_board_vtxos(&self) -> anyhow::Result<Vec<WalletVtxo>> {
1027		let vtxos = self.db.get_all_pending_boards()?.iter()
1028			.map(|vtxo_id| self.get_vtxo_by_id(*vtxo_id))
1029			.collect::<anyhow::Result<Vec<_>>>()?;
1030
1031		debug_assert!(vtxos.iter().all(|v| matches!(v.state.kind(), VtxoStateKind::Locked)),
1032			"all pending board vtxos should be locked"
1033		);
1034
1035		Ok(vtxos)
1036	}
1037
1038	/// Queries the database for any VTXO that is an pending lightning send.
1039	pub fn pending_lightning_send_vtxos(&self) -> anyhow::Result<Vec<WalletVtxo>> {
1040		let vtxos = self.db.get_all_pending_lightning_send()?.into_iter()
1041			.flat_map(|pending_lightning_send| pending_lightning_send.htlc_vtxos)
1042			.collect::<Vec<_>>();
1043
1044		Ok(vtxos)
1045	}
1046
1047	/// Returns all vtxos that will expire within `threshold` blocks
1048	pub async fn get_expiring_vtxos(
1049		&self,
1050		threshold: BlockHeight,
1051	) -> anyhow::Result<Vec<WalletVtxo>> {
1052		let expiry = self.chain.tip().await? + threshold;
1053		let filter = VtxoFilter::new(&self).expires_before(expiry);
1054		Ok(self.spendable_vtxos_with(&filter)?)
1055	}
1056
1057	/// Attempts to register all pendings boards with the Ark server. A board transaction must have
1058	/// sufficient confirmations before it will be registered. For more details see
1059	/// [ArkInfo::required_board_confirmations].
1060	pub async fn sync_pending_boards(&self) -> anyhow::Result<()> {
1061		let ark_info = self.require_server()?.info;
1062		let current_height = self.chain.tip().await?;
1063		let unregistered_boards = self.pending_board_vtxos()?;
1064		let mut registered_boards = 0;
1065
1066		if unregistered_boards.is_empty() {
1067			return Ok(());
1068		}
1069
1070		trace!("Attempting registration of sufficiently confirmed boards");
1071
1072		for board in unregistered_boards {
1073			let anchor = board.vtxo.chain_anchor();
1074			let confs = match self.chain.tx_status(anchor.txid).await {
1075				Ok(TxStatus::Confirmed(block_ref)) => Some(current_height - (block_ref.height - 1)),
1076				Ok(TxStatus::Mempool) => Some(0),
1077				Ok(TxStatus::NotFound) => None,
1078				Err(_) => None,
1079			};
1080
1081			if let Some(confs) = confs {
1082				if confs >= ark_info.required_board_confirmations as BlockHeight {
1083					if let Err(e) = self.register_board(board.vtxo.id()).await {
1084						warn!("Failed to register board {}: {}", board.vtxo.id(), e);
1085					} else {
1086						info!("Registered board {}", board.vtxo.id());
1087						registered_boards += 1;
1088					}
1089				}
1090			}
1091		};
1092
1093		if registered_boards > 0 {
1094			info!("Registered {registered_boards} sufficiently confirmed boards");
1095		}
1096		Ok(())
1097	}
1098
1099	/// Performs maintenance tasks on the offchain wallet.
1100	///
1101	/// This can take a long period of time due to syncing rounds, arkoors, checking pending
1102	/// payments and refreshing VTXOs if necessary.
1103	pub async fn maintenance(&self) -> anyhow::Result<()> {
1104		info!("Starting wallet maintenance");
1105		self.sync().await;
1106		self.maintenance_refresh().await?;
1107		Ok(())
1108	}
1109
1110	/// Performs maintenance tasks on the onchain and offchain wallet.
1111	///
1112	/// This can take a long period of time due to syncing the onchain wallet, registering boards,
1113	/// syncing rounds, arkoors, and the exit system, checking pending lightning payments and
1114	/// refreshing VTXOs if necessary.
1115	pub async fn maintenance_with_onchain<W: PreparePsbt + SignPsbt + ExitUnilaterally>(
1116		&self,
1117		onchain: &mut W,
1118	) -> anyhow::Result<()> {
1119		info!("Starting wallet maintenance with onchain wallet");
1120		self.sync().await;
1121		self.maintenance_refresh().await?;
1122
1123		// NB: order matters here, after syncing lightning, we might have new exits to start
1124		self.sync_exits(onchain).await?;
1125
1126		Ok(())
1127	}
1128
1129	/// Performs a refresh of all VTXOs that are due to be refreshed, if any. This will include any
1130	/// VTXOs within the expiry threshold ([Config::vtxo_refresh_expiry_threshold]) or those which
1131	/// are uneconomical to exit due to onchain network conditions.
1132	///
1133	/// Returns a [RoundId] if a refresh occurs.
1134	pub async fn maintenance_refresh(&self) -> anyhow::Result<Option<RoundId>> {
1135		let vtxos = self.get_vtxos_to_refresh().await?.into_iter()
1136			.map(|v| v.id())
1137			.collect::<Vec<_>>();
1138		if vtxos.len() == 0 {
1139			return Ok(None);
1140		}
1141
1142		info!("Performing maintenance refresh");
1143		self.refresh_vtxos(vtxos).await
1144	}
1145
1146	/// Sync offchain wallet and update onchain fees. This is a much more lightweight alternative
1147	/// to [Wallet::maintenance] as it will not refresh VTXOs or sync the onchain wallet.
1148	///
1149	/// Notes:
1150	///   - even when onchain wallet is provided, the onchain wallet will not be sync, but
1151	///     - [Wallet::sync_pending_lightning_send_vtxos] will be called
1152	///   - [Wallet::sync_exits] will not be called
1153	pub async fn sync(&self) {
1154		tokio::join!(
1155			async {
1156				// NB: order matters here, if syncing call fails,
1157				// we still want to update the fee rates
1158				if let Err(e) = self.chain.update_fee_rates(self.config.fallback_fee_rate).await {
1159					warn!("Error updating fee rates: {:#}", e);
1160				}
1161			},
1162			async {
1163				if let Err(e) = self.sync_oors().await {
1164					warn!("Error in arkoor sync: {:#}", e);
1165				}
1166			},
1167			async {
1168				if let Err(e) = self.sync_pending_rounds().await {
1169					warn!("Error syncing pending rounds: {:#}", e);
1170				}
1171			},
1172			async {
1173				if let Err(e) = self.sync_pending_lightning_send_vtxos().await {
1174					warn!("Error syncing pending lightning payments: {:#}", e);
1175				}
1176			},
1177			async {
1178				if let Err(e) = self.claim_all_pending_htlc_recvs().await {
1179					warn!("Error claiming pending lightning receives: {:#}", e);
1180				}
1181			},
1182			async {
1183				if let Err(e) = self.sync_pending_boards().await {
1184					warn!("Error syncing pending boards: {:#}", e);
1185				}
1186			}
1187		);
1188	}
1189
1190	/// Sync the transaction status of unilateral exits
1191	///
1192	/// This will not progress the unilateral exits in any way, it will merely check the
1193	/// transaction status of each transaction as well as check whether any exits have become
1194	/// claimable or have been claimed.
1195	pub async fn sync_exits<W: ExitUnilaterally>(
1196		&self,
1197		onchain: &mut W,
1198	) -> anyhow::Result<()> {
1199		self.exit.write().await.sync_exit(onchain).await?;
1200		Ok(())
1201	}
1202
1203	/// Syncs pending lightning payments, verifying whether the payment status has changed and
1204	/// creating a revocation VTXO if necessary.
1205	pub async fn sync_pending_lightning_send_vtxos(&self) -> anyhow::Result<()> {
1206		let pending_payments = self.db.get_all_pending_lightning_send()?;
1207
1208		if pending_payments.is_empty() {
1209			return Ok(());
1210		}
1211
1212		info!("Syncing {} pending lightning sends", pending_payments.len());
1213
1214		for payment in pending_payments {
1215			self.check_lightning_payment(&payment).await?;
1216		}
1217
1218		Ok(())
1219	}
1220
1221	/// Drop a specific [Vtxo] from the database. This is destructive and will result in a loss of
1222	/// funds.
1223	pub async fn dangerous_drop_vtxo(&self, vtxo_id: VtxoId) -> anyhow::Result<()> {
1224		warn!("Drop vtxo {} from the database", vtxo_id);
1225		self.db.remove_vtxo(vtxo_id)?;
1226		Ok(())
1227	}
1228
1229	/// Drop all VTXOs from the database. This is destructive and will result in a loss of funds.
1230	//TODO(stevenroose) improve the way we expose dangerous methods
1231	pub async fn dangerous_drop_all_vtxos(&self) -> anyhow::Result<()> {
1232		warn!("Dropping all vtxos from the db...");
1233		for vtxo in self.vtxos()? {
1234			self.db.remove_vtxo(vtxo.id())?;
1235		}
1236
1237		self.exit.write().await.clear_exit()?;
1238		Ok(())
1239	}
1240
1241	/// Board a [Vtxo] with the given amount.
1242	///
1243	/// NB we will spend a little more onchain to cover fees.
1244	pub async fn board_amount<W: PreparePsbt + SignPsbt + GetWalletTx>(
1245		&self,
1246		onchain: &mut W,
1247		amount: Amount,
1248	) -> anyhow::Result<Board> {
1249		let (user_keypair, _) = self.derive_store_next_keypair()?;
1250		self.board(onchain, Some(amount), user_keypair).await
1251	}
1252
1253	/// Board a [Vtxo] with all the funds in your onchain wallet.
1254	pub async fn board_all<W: PreparePsbt + SignPsbt + GetWalletTx>(
1255		&self,
1256		onchain: &mut W,
1257	) -> anyhow::Result<Board> {
1258		let (user_keypair, _) = self.derive_store_next_keypair()?;
1259		self.board(onchain, None, user_keypair).await
1260	}
1261
1262	async fn board<W: PreparePsbt + SignPsbt + GetWalletTx>(
1263		&self,
1264		wallet: &mut W,
1265		amount: Option<Amount>,
1266		user_keypair: Keypair,
1267	) -> anyhow::Result<Board> {
1268		let mut srv = self.require_server()?;
1269		let properties = self.db.read_properties()?.context("Missing config")?;
1270		let current_height = self.chain.tip().await?;
1271
1272		let expiry_height = current_height + srv.info.vtxo_expiry_delta as BlockHeight;
1273		let builder = BoardBuilder::new(
1274			user_keypair.public_key(),
1275			expiry_height,
1276			srv.info.server_pubkey,
1277			srv.info.vtxo_exit_delta,
1278		);
1279
1280		let addr = bitcoin::Address::from_script(
1281			&builder.funding_script_pubkey(),
1282			properties.network,
1283		).unwrap();
1284
1285		// We create the board tx template, but don't sign it yet.
1286		let fee_rate = self.chain.fee_rates().await.regular;
1287		let (board_psbt, amount) = if let Some(amount) = amount {
1288			let psbt = wallet.prepare_tx([(addr, amount)], fee_rate)?;
1289			(psbt, amount)
1290		} else {
1291			let psbt = wallet.prepare_drain_tx(addr, fee_rate)?;
1292			assert_eq!(psbt.unsigned_tx.output.len(), 1);
1293			let amount = psbt.unsigned_tx.output[0].value;
1294			(psbt, amount)
1295		};
1296
1297		ensure!(amount >= srv.info.min_board_amount,
1298			"board amount of {amount} is less than minimum board amount required by server ({})",
1299			srv.info.min_board_amount,
1300		);
1301
1302		let utxo = OutPoint::new(board_psbt.unsigned_tx.compute_txid(), BOARD_FUNDING_TX_VTXO_VOUT);
1303		let builder = builder
1304			.set_funding_details(amount, utxo)
1305			.generate_user_nonces();
1306
1307		let cosign_resp = srv.client.request_board_cosign(protos::BoardCosignRequest {
1308			amount: amount.to_sat(),
1309			utxo: bitcoin::consensus::serialize(&utxo), //TODO(stevenroose) change to own
1310			expiry_height: expiry_height,
1311			user_pubkey: user_keypair.public_key().serialize().to_vec(),
1312			pub_nonce: builder.user_pub_nonce().serialize().to_vec(),
1313		}).await.context("error requesting board cosign")?
1314			.into_inner().try_into().context("invalid cosign response from server")?;
1315
1316		ensure!(builder.verify_cosign_response(&cosign_resp),
1317			"invalid board cosignature received from server",
1318		);
1319
1320		// Store vtxo first before we actually make the on-chain tx.
1321		let vtxo = builder.build_vtxo(&cosign_resp, &user_keypair)?;
1322
1323		self.db.register_movement(MovementArgs {
1324			kind: MovementKind::Board,
1325			spends: &[],
1326			receives: &[(&vtxo, VtxoState::Locked)],
1327			recipients: &[],
1328			fees: None,
1329		}).context("db error storing vtxo")?;
1330
1331		let tx = wallet.finish_tx(board_psbt)?;
1332
1333		self.db.store_pending_board(&vtxo, &tx)?;
1334
1335		trace!("Broadcasting board tx: {}", bitcoin::consensus::encode::serialize_hex(&tx));
1336		self.chain.broadcast_tx(&tx).await?;
1337
1338		info!("Board broadcasted");
1339		Ok(Board {
1340			funding_txid: tx.compute_txid(),
1341			vtxos: vec![vtxo.into()],
1342		})
1343	}
1344
1345	/// Registers a board to the Ark server
1346	async fn register_board(&self, vtxo: impl VtxoRef) -> anyhow::Result<Board> {
1347		trace!("Attempting to register board {} to server", vtxo.vtxo_id());
1348		let mut srv = self.require_server()?;
1349
1350		// Get the vtxo and funding transaction from the database
1351		let vtxo = match vtxo.vtxo() {
1352			Some(v) => v,
1353			None => {
1354				&self.db.get_wallet_vtxo(vtxo.vtxo_id())?
1355					.with_context(|| format!("VTXO doesn't exist: {}", vtxo.vtxo_id()))?
1356			},
1357		};
1358
1359		// Register the vtxo with the server
1360		srv.client.register_board_vtxo(protos::BoardVtxoRequest {
1361			board_vtxo: vtxo.serialize(),
1362		}).await.context("error registering board with the Ark server")?;
1363
1364		// Remember that we have stored the vtxo
1365		// No need to complain if the vtxo is already registered
1366		self.db.update_vtxo_state_checked(vtxo.vtxo_id(), VtxoState::Spendable, &UNSPENT_STATES)?;
1367
1368		self.db.remove_pending_board(&vtxo.vtxo_id())?;
1369
1370		let funding_txid = vtxo.chain_anchor().txid;
1371
1372		Ok(Board {
1373			funding_txid: funding_txid,
1374			vtxos: vec![vtxo.clone()],
1375		})
1376	}
1377
1378	fn build_vtxo(
1379		&self,
1380		vtxos: &CachedSignedVtxoTree,
1381		leaf_idx: usize,
1382	) -> anyhow::Result<Option<Vtxo>> {
1383		let vtxo = vtxos.build_vtxo(leaf_idx).context("invalid leaf idx..")?;
1384
1385		if self.db.get_wallet_vtxo(vtxo.id())?.is_some() {
1386			debug!("Not adding vtxo {} because it already exists", vtxo.id());
1387			return Ok(None)
1388		}
1389
1390		debug!("Built new vtxo {} with value {}", vtxo.id(), vtxo.amount());
1391		Ok(Some(vtxo))
1392	}
1393
1394	/// Checks if the provided VTXO has some counterparty risk in the current wallet
1395	///
1396	/// An arkoor vtxo is considered to have some counterparty risk
1397	/// if it is (directly or not) based on round VTXOs that aren't owned by the wallet
1398	fn has_counterparty_risk(&self, vtxo: &Vtxo) -> anyhow::Result<bool> {
1399		for past_pk in vtxo.past_arkoor_pubkeys() {
1400			if !self.db.get_public_key_idx(&past_pk)?.is_some() {
1401				return Ok(true);
1402			}
1403		}
1404		Ok(!self.db.get_public_key_idx(&vtxo.user_pubkey())?.is_some())
1405	}
1406
1407	/// Sync all past rounds
1408	///
1409	/// Intended for recovery after data loss.
1410	pub async fn sync_past_rounds(&self) -> anyhow::Result<()> {
1411		let mut srv = self.require_server()?;
1412
1413		let fresh_rounds = srv.client.get_fresh_rounds(protos::FreshRoundsRequest {
1414			last_round_txid: None,
1415		}).await?.into_inner().txids.into_iter()
1416			.map(|txid| RoundId::from_slice(&txid))
1417			.collect::<Result<Vec<_>, _>>()?;
1418
1419		if fresh_rounds.is_empty() {
1420			debug!("No new rounds to sync");
1421			return Ok(());
1422		}
1423
1424		debug!("Received {} new rounds from ark", fresh_rounds.len());
1425
1426		let last_pk_index = self.db.get_last_vtxo_key_index()?.unwrap_or_default();
1427		let pubkeys = (0..=last_pk_index).map(|idx| {
1428			self.vtxo_seed.derive_keypair(idx).public_key()
1429		}).collect::<HashSet<_>>();
1430
1431		let results = tokio_stream::iter(fresh_rounds).map(|round_id| {
1432			let pubkeys = pubkeys.clone();
1433			let mut srv = srv.clone();
1434
1435			async move {
1436				if self.db.get_round_attempt_by_round_txid(round_id)?.is_some() {
1437					debug!("Skipping round {} because it already exists", round_id);
1438					return Ok::<_, anyhow::Error>(());
1439				}
1440
1441				let req = protos::RoundId {
1442					txid: round_id.as_round_txid().to_byte_array().to_vec(),
1443				};
1444				let round = srv.client.get_round(req).await?.into_inner();
1445
1446				let tree = SignedVtxoTreeSpec::deserialize(&round.signed_vtxos)
1447					.context("invalid signed vtxo tree from srv")?
1448					.into_cached_tree();
1449
1450				let mut reqs = Vec::new();
1451				let mut vtxos = vec![];
1452				for (idx, dest) in tree.spec.spec.vtxos.iter().enumerate() {
1453					if pubkeys.contains(&dest.vtxo.policy.user_pubkey()) {
1454						if let Some(vtxo) = self.build_vtxo(&tree, idx)? {
1455							reqs.push(StoredVtxoRequest {
1456								request_policy: dest.vtxo.policy.clone(),
1457								amount: dest.vtxo.amount,
1458								state: VtxoState::Spendable,
1459							});
1460
1461							vtxos.push(vtxo);
1462						}
1463					}
1464				}
1465
1466				let round_tx = deserialize::<Transaction>(&round.funding_tx)?;
1467				self.db.store_pending_confirmation_round(round_id, round_tx, reqs, vtxos)?;
1468
1469				Ok(())
1470			}
1471		})
1472		.buffer_unordered(10)
1473		.collect::<Vec<_>>()
1474		.await;
1475
1476		for result in results {
1477			if let Err(e) = result {
1478				return Err(e).context("failed to sync round");
1479			}
1480		}
1481
1482		Ok(())
1483	}
1484
1485	async fn sync_oors(&self) -> anyhow::Result<()> {
1486		let last_pk_index = self.db.get_last_vtxo_key_index()?.unwrap_or_default();
1487		let pubkeys = (0..=last_pk_index).map(|idx| {
1488			self.vtxo_seed.derive_keypair(idx).public_key()
1489		}).collect::<Vec<_>>();
1490
1491		self.sync_arkoor_for_pubkeys(&pubkeys).await?;
1492
1493		Ok(())
1494	}
1495
1496	/// Sync with the Ark server and look for out-of-round received VTXOs by public key.
1497	async fn sync_arkoor_for_pubkeys(
1498		&self,
1499		public_keys: &[PublicKey],
1500	) -> anyhow::Result<()> {
1501		let mut srv = self.require_server()?;
1502
1503		for pubkeys in public_keys.chunks(rpc::MAX_NB_MAILBOX_PUBKEYS) {
1504			// Then sync OOR vtxos.
1505			debug!("Emptying OOR mailbox at Ark server...");
1506			let req = protos::ArkoorVtxosRequest {
1507				pubkeys: pubkeys.iter().map(|pk| pk.serialize().to_vec()).collect(),
1508			};
1509			let packages = srv.client.empty_arkoor_mailbox(req).await
1510				.context("error fetching oors")?.into_inner().packages;
1511			debug!("Ark server has {} arkoor packages for us", packages.len());
1512
1513			for package in packages {
1514				let mut vtxos = Vec::with_capacity(package.vtxos.len());
1515				for vtxo in package.vtxos {
1516					let vtxo = match Vtxo::deserialize(&vtxo) {
1517						Ok(vtxo) => vtxo,
1518						Err(e) => {
1519							warn!("Invalid vtxo from Ark server: {}", e);
1520							continue;
1521						}
1522					};
1523
1524
1525					let txid = vtxo.chain_anchor().txid;
1526					let chain_anchor = self.chain.get_tx(&txid).await?.with_context(|| {
1527						format!("received arkoor vtxo with unknown chain anchor: {}", txid)
1528					})?;
1529					if let Err(e) = vtxo.validate(&chain_anchor) {
1530						error!("Received invalid arkoor VTXO from server: {}", e);
1531						continue;
1532					}
1533
1534					match self.db.has_spent_vtxo(vtxo.id()) {
1535						Ok(spent) if spent => {
1536							debug!("Not adding OOR vtxo {} because it is considered spent", vtxo.id());
1537							continue;
1538						},
1539						_ => {}
1540					}
1541
1542					if let Ok(Some(_)) = self.db.get_wallet_vtxo(vtxo.id()) {
1543						debug!("Not adding OOR vtxo {} because it already exists", vtxo.id());
1544						continue;
1545					}
1546
1547					vtxos.push(vtxo);
1548				}
1549
1550				self.db.register_movement(MovementArgs {
1551					kind: MovementKind::ArkoorReceive,
1552					spends: &[],
1553					receives: &vtxos.iter().map(|v| (v, VtxoState::Spendable)).collect::<Vec<_>>(),
1554					recipients: &[],
1555					fees: None,
1556				}).context("failed to store OOR vtxo")?;
1557			}
1558		}
1559
1560		Ok(())
1561	}
1562
1563	async fn offboard<V: VtxoRef>(
1564		&mut self,
1565		vtxos: impl IntoIterator<Item = V>,
1566		destination: ScriptBuf,
1567	) -> anyhow::Result<Offboard> {
1568		let vtxos = {
1569			let vtxos = vtxos.into_iter();
1570			let mut ret = Vec::with_capacity(vtxos.size_hint().0);
1571			for v in vtxos {
1572				let vtxo = match v.vtxo() {
1573					Some(v) => v.clone(),
1574					None => self.get_vtxo_by_id(v.vtxo_id()).context("vtxo not found")?.vtxo,
1575				};
1576				ret.push(vtxo);
1577			}
1578			ret
1579		};
1580
1581		if vtxos.is_empty() {
1582			bail!("no VTXO to offboard");
1583		}
1584
1585		let participation = DesiredRoundParticipation::Offboard { vtxos, destination };
1586		let RoundResult { round_id, .. } = self.participate_round(participation).await
1587			.context("round failed")?;
1588
1589		Ok(Offboard { round: round_id })
1590	}
1591
1592	/// Offboard all VTXOs to a given [bitcoin::Address].
1593	pub async fn offboard_all(&mut self, address: bitcoin::Address) -> anyhow::Result<Offboard> {
1594		let input_vtxos = self.spendable_vtxos()?;
1595		Ok(self.offboard(input_vtxos, address.script_pubkey()).await?)
1596	}
1597
1598	/// Offboard the given VTXOs to a given [bitcoin::Address].
1599	pub async fn offboard_vtxos<V: VtxoRef>(
1600		&mut self,
1601		vtxos: impl IntoIterator<Item = V>,
1602		address: bitcoin::Address,
1603	) -> anyhow::Result<Offboard> {
1604		let input_vtxos =  vtxos
1605			.into_iter()
1606			.map(|v| {
1607				let id = v.vtxo_id();
1608				match self.db.get_wallet_vtxo(id)? {
1609					Some(vtxo) => Ok(vtxo.vtxo),
1610					_ => bail!("cannot find requested vtxo: {}", id),
1611				}
1612			})
1613			.collect::<anyhow::Result<Vec<_>>>()?;
1614
1615		Ok(self.offboard(input_vtxos, address.script_pubkey()).await?)
1616	}
1617
1618	/// This will refresh all provided VTXOs. Note that attempting to refresh a board VTXO which
1619	/// has not yet confirmed will result in an error.
1620	///
1621	/// Returns the [RoundId] of the round if a successful refresh occurred.
1622	/// It will return [None] if no [Vtxo] needed to be refreshed.
1623	pub async fn refresh_vtxos<V: VtxoRef>(
1624		&self,
1625		vtxos: impl IntoIterator<Item = V>,
1626	) -> anyhow::Result<Option<RoundId>> {
1627		let vtxos = {
1628			let mut ret = HashMap::new();
1629			for v in vtxos {
1630				let id = v.vtxo_id();
1631				let vtxo = self.get_vtxo_by_id(id)
1632					.with_context(|| format!("vtxo with id {} not found", id))?;
1633				if !ret.insert(id, vtxo).is_none() {
1634					bail!("duplicate VTXO id: {}", id);
1635				}
1636			}
1637			ret
1638		};
1639
1640		if vtxos.is_empty() {
1641			info!("Skipping refresh since no VTXOs are provided.");
1642			return Ok(None);
1643		}
1644
1645		let total_amount = vtxos.values().map(|v| v.vtxo.amount()).sum();
1646
1647		info!("Refreshing {} VTXOs (total amount = {}).", vtxos.len(), total_amount);
1648
1649		let (user_keypair, _) = self.derive_store_next_keypair()?;
1650		let req = VtxoRequest {
1651			policy: VtxoPolicy::Pubkey(PubkeyVtxoPolicy { user_pubkey: user_keypair.public_key() }),
1652			amount: total_amount,
1653		};
1654
1655		let participation = DesiredRoundParticipation::Funded(RoundParticipation {
1656			inputs: vtxos.into_values().map(|v| v.vtxo).collect(),
1657			outputs: vec![StoredVtxoRequest::from_parts(req.clone(), VtxoState::Spendable)],
1658			offboards: Vec::new(),
1659		});
1660		let RoundResult { round_id, .. } = self.participate_round(participation).await
1661			.context("round failed")?;
1662
1663		Ok(Some(round_id))
1664	}
1665
1666	/// This will find all VTXOs that meets must-refresh criteria.
1667	/// Then, if there are some VTXOs to refresh, it will
1668	/// also add those that meet should-refresh criteria.
1669	pub async fn get_vtxos_to_refresh(&self) -> anyhow::Result<Vec<WalletVtxo>> {
1670		let tip = self.chain.tip().await?;
1671		let fee_rate = self.chain.fee_rates().await.fast;
1672
1673		// Check if there is any VTXO that we must refresh
1674		let must_refresh_vtxos = self.spendable_vtxos_with(
1675			&RefreshStrategy::must_refresh(self, tip, fee_rate),
1676		)?;
1677		if must_refresh_vtxos.is_empty() {
1678			return Ok(vec![]);
1679		} else {
1680			// If we need to do a refresh, we take all the should_refresh vtxo's as well
1681			// This helps us to aggregate some VTXOs
1682			let should_refresh_vtxos = self.spendable_vtxos_with(
1683				&RefreshStrategy::should_refresh(self, tip, fee_rate),
1684			)?;
1685			Ok(should_refresh_vtxos)
1686		}
1687	}
1688
1689	/// Returns the block height at which the first VTXO will expire
1690	pub fn get_first_expiring_vtxo_blockheight(
1691		&self,
1692	) -> anyhow::Result<Option<BlockHeight>> {
1693		Ok(self.spendable_vtxos()?.iter().map(|v| v.expiry_height()).min())
1694	}
1695
1696	/// Returns the next block height at which we have a VTXO that we
1697	/// want to refresh
1698	pub fn get_next_required_refresh_blockheight(
1699		&self,
1700	) -> anyhow::Result<Option<BlockHeight>> {
1701		let first_expiry = self.get_first_expiring_vtxo_blockheight()?;
1702		Ok(first_expiry.map(|h| h.saturating_sub(self.config.vtxo_refresh_expiry_threshold)))
1703	}
1704
1705	/// Select several vtxos to cover the provided amount
1706	///
1707	/// Returns an error if amount cannot be reached
1708	///
1709	/// If `max_depth` is set, it will filter vtxos that have a depth greater than it.
1710	fn select_vtxos_to_cover(
1711		&self,
1712		amount: Amount,
1713		max_depth: Option<u16>,
1714		expiry_threshold: Option<BlockHeight>,
1715	) -> anyhow::Result<Vec<Vtxo>> {
1716		let inputs = self.spendable_vtxos()?;
1717
1718		// Iterate over all rows until the required amount is reached
1719		let mut result = Vec::new();
1720		let mut total_amount = bitcoin::Amount::ZERO;
1721		for input in inputs {
1722			if let Some(max_depth) = max_depth {
1723				if input.arkoor_depth() >= max_depth {
1724					warn!("VTXO {} reached max depth of {}, skipping it. \
1725						Please refresh your VTXO.", input.id(), max_depth,
1726					);
1727					continue;
1728				}
1729			}
1730
1731			// Check if vtxo is soon-to-expire for arkoor payments
1732			if let Some(threshold) = expiry_threshold {
1733				if input.expiry_height() < threshold {
1734					warn!("VTXO {} is expiring soon (expires at {}, threshold {}), \
1735						skipping for arkoor payment",
1736						input.id(), input.expiry_height(), threshold,
1737					);
1738					continue;
1739				}
1740			}
1741
1742			total_amount += input.amount();
1743			result.push(input.vtxo);
1744
1745			if total_amount >= amount {
1746				return Ok(result)
1747			}
1748		}
1749
1750		bail!("Insufficient money available. Needed {} but {} is available",
1751			amount, total_amount,
1752		);
1753	}
1754
1755	/// Create Arkoor VTXOs for a given destination and amount
1756	///
1757	/// Outputs cannot have more than one input, so we can create new
1758	/// arkoors for each input needed to match requested amount + one
1759	/// optional change output.
1760	async fn create_arkoor_vtxos(
1761		&self,
1762		destination_policy: VtxoPolicy,
1763		amount: Amount,
1764	) -> anyhow::Result<ArkoorCreateResult> {
1765		let mut srv = self.require_server()?;
1766		let change_pubkey = self.derive_store_next_keypair()?.0.public_key();
1767
1768		let req = VtxoRequest {
1769			amount: amount,
1770			policy: destination_policy,
1771		};
1772
1773		// Get current height for expiry checking
1774		let tip = self.chain.tip().await?;
1775		let inputs = self.select_vtxos_to_cover(
1776			req.amount,
1777			Some(srv.info.max_arkoor_depth),
1778			Some(tip + self.config.vtxo_refresh_expiry_threshold),
1779		)?;
1780
1781		let mut secs = Vec::with_capacity(inputs.len());
1782		let mut pubs = Vec::with_capacity(inputs.len());
1783		let mut keypairs = Vec::with_capacity(inputs.len());
1784		for input in inputs.iter() {
1785			let keypair = self.get_vtxo_key(&input)?;
1786			let (s, p) = musig::nonce_pair(&keypair);
1787			secs.push(s);
1788			pubs.push(p);
1789			keypairs.push(keypair);
1790		}
1791
1792		let builder = ArkoorPackageBuilder::new(&inputs, &pubs, req, Some(change_pubkey))?;
1793
1794		let req = protos::ArkoorPackageCosignRequest {
1795			arkoors: builder.arkoors.iter().map(|a| a.into()).collect(),
1796		};
1797		let cosign_resp: Vec<_> = srv.client.request_arkoor_package_cosign(req).await?
1798			.into_inner().try_into().context("invalid server cosign response")?;
1799		ensure!(builder.verify_cosign_response(&cosign_resp),
1800			"invalid arkoor cosignature received from server",
1801		);
1802
1803		let (sent, change) = builder.build_vtxos(&cosign_resp, &keypairs, secs)?;
1804
1805		if let Some(change) = change.as_ref() {
1806			info!("Added change VTXO of {}", change.amount());
1807		}
1808
1809		Ok(ArkoorCreateResult {
1810			input: inputs,
1811			created: sent,
1812			change: change,
1813		})
1814	}
1815
1816	/// Validate if we can send arkoor payments to the given [ark::Address], for example an error
1817	/// will be returned if the given [ark::Address] belongs to a different server (see
1818	/// [ark::address::ArkId]).
1819	pub fn validate_arkoor_address(&self, address: &ark::Address) -> anyhow::Result<()> {
1820		let asp = self.require_server()?;
1821
1822		if !address.ark_id().is_for_server(asp.info.server_pubkey) {
1823			bail!("Ark address is for different server");
1824		}
1825
1826		// Not all policies are supported for sending arkoor
1827		match address.policy().policy_type() {
1828			VtxoPolicyKind::Pubkey => {},
1829			VtxoPolicyKind::ServerHtlcRecv | VtxoPolicyKind::ServerHtlcSend => {
1830				bail!("VTXO policy in address cannot be used for arkoor payment: {}",
1831					address.policy().policy_type(),
1832				);
1833			}
1834		}
1835
1836		if address.delivery().is_empty() {
1837			bail!("No VTXO delivery mechanism provided in address");
1838		}
1839		// We first see if we know any of the deliveries, if not, we will log
1840		// the unknown onces.
1841		// We do this in two parts because we shouldn't log unknown ones if there is one known.
1842		if !address.delivery().iter().any(|d| !d.is_unknown()) {
1843			for d in address.delivery() {
1844				if let VtxoDelivery::Unknown { delivery_type, data } = d {
1845					info!("Unknown delivery in address: type={:#x}, data={}",
1846						delivery_type, data.as_hex(),
1847					);
1848				}
1849			}
1850		}
1851
1852		Ok(())
1853	}
1854
1855	/// Makes an out-of-round payment to the given [ark::Address]. This does not require waiting for
1856	/// a round, so it should be relatively instantaneous.
1857	///
1858	/// If the [Wallet] doesn't contain a VTXO larger than the given [Amount], multiple payments
1859	/// will be chained together, resulting in the recipient receiving multiple VTXOs.
1860	///
1861	/// Note that a change [Vtxo] may be created as a result of this call. With each payment these
1862	/// will become more uneconomical to unilaterally exit, so you should eventually refresh them
1863	/// with [Wallet::refresh_vtxos] or periodically call [Wallet::maintenance_refresh].
1864	pub async fn send_arkoor_payment(
1865		&self,
1866		destination: &ark::Address,
1867		amount: Amount,
1868	) -> anyhow::Result<Vec<Vtxo>> {
1869		let mut srv = self.require_server()?;
1870
1871		self.validate_arkoor_address(&destination).context("cannot send to address")?;
1872
1873		if amount < P2TR_DUST {
1874			bail!("Sent amount must be at least {}", P2TR_DUST);
1875		}
1876
1877		let arkoor = self.create_arkoor_vtxos(destination.policy().clone(), amount).await?;
1878
1879		let req = protos::ArkoorPackage {
1880			arkoors: arkoor.created.iter().map(|v| protos::ArkoorVtxo {
1881				pubkey: destination.policy().user_pubkey().serialize().to_vec(),
1882				vtxo: v.serialize().to_vec(),
1883			}).collect(),
1884		};
1885
1886		if let Err(e) = srv.client.post_arkoor_package_mailbox(req).await {
1887			error!("Failed to post the arkoor vtxo to the recipients mailbox: '{}'", e);
1888			//NB we will continue to at least not lose our own change
1889		}
1890
1891		self.db.register_movement(MovementArgs {
1892			kind: MovementKind::ArkoorSend,
1893			spends: &arkoor.input.iter().collect::<Vec<_>>(),
1894			receives: &arkoor.change.as_ref()
1895				.map(|v| vec![(v, VtxoState::Spendable)])
1896				.unwrap_or(vec![]),
1897			recipients: &[(&destination.to_string(), amount)],
1898			fees: None,
1899		}).context("failed to store arkoor vtxo")?;
1900
1901		Ok(arkoor.created)
1902	}
1903
1904	async fn process_lightning_revocation(&self, payment: &PendingLightningSend) -> anyhow::Result<()> {
1905		let mut srv = self.require_server()?;
1906		let htlc_vtxos = payment.htlc_vtxos.clone().into_iter()
1907			.map(|v: WalletVtxo| v.vtxo).collect::<Vec<_>>();
1908
1909		info!("Processing {} HTLC VTXOs for revocation", htlc_vtxos.len());
1910
1911		let mut secs = Vec::with_capacity(htlc_vtxos.len());
1912		let mut pubs = Vec::with_capacity(htlc_vtxos.len());
1913		let mut keypairs = Vec::with_capacity(htlc_vtxos.len());
1914		for input in htlc_vtxos.iter() {
1915			let keypair = self.get_vtxo_key(&input)?;
1916			let (s, p) = musig::nonce_pair(&keypair);
1917			secs.push(s);
1918			pubs.push(p);
1919			keypairs.push(keypair);
1920		}
1921
1922		let revocation = ArkoorPackageBuilder::new_htlc_revocation(&htlc_vtxos, &pubs)?;
1923
1924		let req = protos::RevokeLightningPaymentRequest {
1925			htlc_vtxo_ids: revocation.arkoors.iter()
1926				.map(|i| i.input.id().to_bytes().to_vec())
1927				.collect(),
1928			user_nonces: revocation.arkoors.iter()
1929				.map(|i| i.user_nonce.serialize().to_vec())
1930				.collect(),
1931		};
1932		let cosign_resp: Vec<_> = srv.client.revoke_lightning_payment(req).await?
1933			.into_inner().try_into().context("invalid server cosign response")?;
1934		ensure!(revocation.verify_cosign_response(&cosign_resp),
1935			"invalid arkoor cosignature received from server",
1936		);
1937
1938		let (vtxos, _) = revocation.build_vtxos(&cosign_resp, &keypairs, secs)?;
1939		for vtxo in &vtxos {
1940			info!("Got revocation VTXO: {}: {}", vtxo.id(), vtxo.amount());
1941		}
1942
1943		self.db.register_movement(MovementArgs {
1944			kind: MovementKind::LightningSendRevocation,
1945			spends: &htlc_vtxos.iter().collect::<Vec<_>>(),
1946			receives: &vtxos.iter().map(|v| (v, VtxoState::Spendable)).collect::<Vec<_>>(),
1947			recipients: &[],
1948			fees: None,
1949		})?;
1950
1951		self.db.remove_pending_lightning_send(payment.invoice.payment_hash())?;
1952
1953		info!("Revoked {} HTLC VTXOs", vtxos.len());
1954
1955		Ok(())
1956	}
1957
1958	/// Pays a Lightning [Invoice] using Ark VTXOs. This is also an out-of-round payment
1959	/// so the same [Wallet::send_arkoor_payment] rules apply.
1960	pub async fn send_lightning_payment(
1961		&self,
1962		invoice: Invoice,
1963		user_amount: Option<Amount>,
1964	) -> anyhow::Result<Preimage> {
1965		let mut srv = self.require_server()?;
1966		let properties = self.db.read_properties()?.context("Missing config")?;
1967
1968		if invoice.network() != properties.network {
1969			bail!("Invoice is for wrong network: {}", invoice.network());
1970		}
1971
1972		if self.db.check_recipient_exists(&invoice.to_string())? {
1973			bail!("Invoice has already been paid");
1974		}
1975
1976		invoice.check_signature()?;
1977
1978		let inv_amount = invoice.amount_msat().map(|v| Amount::from_msat_ceil(v));
1979		if let (Some(_), Some(inv)) = (user_amount, inv_amount) {
1980			bail!("Invoice has amount of {} encoded. Please omit user amount argument", inv);
1981		}
1982
1983		let amount = user_amount.or(inv_amount)
1984			.context("amount required on invoice without amount")?;
1985		if amount < P2TR_DUST {
1986			bail!("Sent amount must be at least {}", P2TR_DUST);
1987		}
1988
1989		let (change_keypair, _) = self.derive_store_next_keypair()?;
1990
1991		let inputs = self.select_vtxos_to_cover(amount, Some(srv.info.max_arkoor_depth), None)
1992			.context("Could not find enough suitable VTXOs to cover lightning payment")?;
1993
1994		let mut secs = Vec::with_capacity(inputs.len());
1995		let mut pubs = Vec::with_capacity(inputs.len());
1996		let mut keypairs = Vec::with_capacity(inputs.len());
1997		for input in inputs.iter() {
1998			let keypair = self.get_vtxo_key(&input)?;
1999			let (s, p) = musig::nonce_pair(&keypair);
2000			secs.push(s);
2001			pubs.push(p);
2002			keypairs.push(keypair);
2003		}
2004
2005		let req = protos::StartLightningPaymentRequest {
2006			invoice: invoice.to_string(),
2007			user_amount_sat: user_amount.map(|a| a.to_sat()),
2008			input_vtxo_ids: inputs.iter().map(|v| v.id().to_bytes().to_vec()).collect(),
2009			user_nonces: pubs.iter().map(|p| p.serialize().to_vec()).collect(),
2010			user_pubkey: change_keypair.public_key().serialize().to_vec(),
2011		};
2012
2013		let resp =  srv.client.start_lightning_payment(req).await
2014			.context("htlc request failed")?.into_inner();
2015
2016		let cosign_resp = resp.sigs.into_iter().map(|i| i.try_into())
2017			.collect::<Result<Vec<_>, _>>()?;
2018		let policy = VtxoPolicy::from_bytes(&resp.policy)?;
2019
2020		let pay_req = match policy {
2021			VtxoPolicy::ServerHtlcSend(policy) => {
2022				ensure!(policy.user_pubkey == change_keypair.public_key(), "user pubkey mismatch");
2023				ensure!(policy.payment_hash == invoice.payment_hash(), "payment hash mismatch");
2024				// TODO: ensure expiry is not too high? add new bark config to check against?
2025				VtxoRequest { amount: amount, policy: policy.into() }
2026			},
2027			_ => bail!("invalid policy returned from server"),
2028		};
2029
2030		let builder = ArkoorPackageBuilder::new(
2031			&inputs, &pubs, pay_req, Some(change_keypair.public_key()),
2032		)?;
2033
2034		ensure!(builder.verify_cosign_response(&cosign_resp),
2035			"invalid arkoor cosignature received from server",
2036		);
2037
2038		let (htlc_vtxos, change_vtxo) = builder.build_vtxos(&cosign_resp, &keypairs, secs)?;
2039
2040		// Validate the new vtxos. They have the same chain anchor.
2041		for (vtxo, input) in htlc_vtxos.iter().zip(inputs.iter()) {
2042			if let Ok(tx) = self.chain.get_tx(&input.chain_anchor().txid).await {
2043				let tx = tx.with_context(|| {
2044					format!("input vtxo chain anchor not found for lightning send htlc vtxo: {}", input.chain_anchor().txid)
2045				})?;
2046				vtxo.validate(&tx).context("invalid lightning htlc vtxo")?;
2047			} else {
2048				warn!("We couldn't validate the new VTXOs because of chain source error.");
2049			}
2050		}
2051
2052		// Validate the change vtxo. It has the same chain anchor as the last input.
2053		if let Some(ref change) = change_vtxo {
2054			let last_input = inputs.last().context("no inputs provided")?;
2055			let tx = self.chain.get_tx(&last_input.chain_anchor().txid).await?;
2056			let tx = tx.with_context(|| {
2057				format!("input vtxo chain anchor not found for lightning change vtxo: {}", last_input.chain_anchor().txid)
2058			})?;
2059			change.validate(&tx).context("invalid lightning change vtxo")?;
2060		}
2061
2062		self.db.register_movement(MovementArgs {
2063			kind: MovementKind::LightningSend,
2064			spends: &inputs.iter().collect::<Vec<_>>(),
2065			receives: &htlc_vtxos.iter()
2066				.map(|v| (v, VtxoState::Locked))
2067				.chain(change_vtxo.as_ref().map(|c| (c, VtxoState::Spendable)))
2068				.collect::<Vec<_>>(),
2069			recipients: &[],
2070			fees: None,
2071		}).context("failed to store OOR vtxo")?;
2072
2073		let payment = self.db.store_new_pending_lightning_send(
2074			&invoice, &amount, &htlc_vtxos.iter().map(|v| v.id()).collect::<Vec<_>>(),
2075		)?;
2076
2077		let req = protos::SignedLightningPaymentDetails {
2078			invoice: invoice.to_string(),
2079			htlc_vtxo_ids: htlc_vtxos.iter().map(|v| v.id().to_bytes().to_vec()).collect(),
2080			wait: true,
2081		};
2082
2083		let res = srv.client.finish_lightning_payment(req).await?.into_inner();
2084		debug!("Progress update: {}", res.progress_message);
2085		let payment_preimage = Preimage::try_from(res.payment_preimage()).ok();
2086
2087		if let Some(preimage) = payment_preimage {
2088			info!("Payment succeeded! Preimage: {}", preimage.as_hex());
2089			self.db.register_movement(MovementArgs {
2090				kind: MovementKind::LightningSend,
2091				spends: &htlc_vtxos.iter().collect::<Vec<_>>(),
2092				receives: &[],
2093				recipients: &[(&invoice.to_string(), amount)],
2094				fees: None,
2095			}).context("failed to store OOR vtxo")?;
2096
2097			self.db.remove_pending_lightning_send(payment.invoice.payment_hash())?;
2098			Ok(preimage)
2099		} else {
2100			self.process_lightning_revocation(&payment).await?;
2101			bail!("No preimage, payment failed: {}", res.progress_message);
2102		}
2103	}
2104
2105	/// Checks the status of a lightning payment associated with a set of VTXOs, processes the
2106	/// payment result and optionally takes appropriate actions based on the payment outcome.
2107	///
2108	/// # Arguments
2109	///
2110	/// * `htlc_vtxos` - Slice of [WalletVtxo] objects that represent HTLC outputs involved in the
2111	///                  payment.
2112	///
2113	/// # Returns
2114	///
2115	/// Returns `Ok(Some(Preimage))` if the payment is successfully completed and a preimage is
2116	/// received.
2117	/// Returns `Ok(None)` for payments still pending, failed payments or if necessary revocation
2118	/// or exit processing occurs.
2119	/// Returns an `Err` if an error occurs during the process.
2120	///
2121	/// # Behavior
2122	///
2123	/// - Validates that all HTLC VTXOs share the same invoice, amount and policy.
2124	/// - Sends a request to the lightning payment server to check the payment status.
2125	/// - Depending on the payment status:
2126	///   - **Failed**: Revokes the associated VTXOs.
2127	///   - **Pending**: Checks if the HTLC has expired based on the tip height. If expired,
2128	///     revokes the VTXOs.
2129	///   - **Complete**: Extracts the payment preimage, logs the payment, registers movement
2130	///     in the database and returns
2131	pub async fn check_lightning_payment(&self, payment: &PendingLightningSend)
2132		-> anyhow::Result<Option<Preimage>>
2133	{
2134		let mut srv = self.require_server()?;
2135		let tip = self.chain.tip().await?;
2136
2137		let payment_hash = payment.invoice.payment_hash();
2138
2139		let policy = payment.htlc_vtxos.first().context("no vtxo provided")?.vtxo.policy();
2140		debug_assert!(payment.htlc_vtxos.iter().all(|v| v.vtxo.policy() == policy),
2141			"All lightning htlc should have the same policy",
2142		);
2143		let policy = policy.as_server_htlc_send().context("VTXO is not an HTLC send")?;
2144		if policy.payment_hash != payment_hash {
2145			bail!("Payment hash mismatch");
2146		}
2147
2148		let req = protos::CheckLightningPaymentRequest {
2149			hash: policy.payment_hash.to_vec(),
2150			wait: false,
2151		};
2152		let res = srv.client.check_lightning_payment(req).await?.into_inner();
2153
2154		let payment_status = protos::PaymentStatus::try_from(res.status)?;
2155
2156		let should_revoke = match payment_status {
2157			protos::PaymentStatus::Failed => {
2158				info!("Payment failed ({}): revoking VTXO", res.progress_message);
2159				true
2160			},
2161			protos::PaymentStatus::Pending => {
2162				trace!("Payment is still pending, HTLC expiry: {}, tip: {}",
2163					policy.htlc_expiry, tip);
2164				if tip > policy.htlc_expiry {
2165					info!("Payment is still pending, but HTLC is expired: revoking VTXO");
2166					true
2167				} else {
2168					info!("Payment is still pending and HTLC is not expired ({}): \
2169						doing nothing for now", policy.htlc_expiry,
2170					);
2171					false
2172				}
2173			},
2174			protos::PaymentStatus::Complete => {
2175				let preimage: Preimage = res.payment_preimage
2176					.context("payment completed but no preimage")?
2177					.try_into().map_err(|_| anyhow!("preimage is not 32 bytes"))?;
2178				info!("Payment is complete, preimage, {}", preimage.as_hex());
2179
2180				self.db.register_movement(MovementArgs {
2181					kind: MovementKind::LightningSend,
2182					spends: &payment.htlc_vtxos.iter().map(|v| &v.vtxo).collect::<Vec<_>>(),
2183					receives: &[],
2184					recipients: &[(&payment.invoice.to_string(), payment.amount)],
2185					fees: None,
2186				}).context("failed to store OOR vtxo")?;
2187
2188				self.db.remove_pending_lightning_send(payment_hash)?;
2189
2190				return Ok(Some(preimage));
2191			},
2192		};
2193
2194		if should_revoke {
2195			if let Err(e) = self.process_lightning_revocation(payment).await {
2196				warn!("Failed to revoke VTXO: {}", e);
2197
2198				// if one of the htlc is about to expire, we exit all of them.
2199				// Maybe we want a different behavior here, but we have to decide whether
2200				// htlc vtxos revocation is a all or nothing process.
2201				let min_expiry = payment.htlc_vtxos.iter()
2202					.map(|v| v.vtxo.spec().expiry_height).min().unwrap();
2203
2204				if tip > min_expiry.saturating_sub(self.config().vtxo_refresh_expiry_threshold) {
2205					warn!("Some VTXO is about to expire soon, marking to exit");
2206					let vtxos = payment.htlc_vtxos
2207						.iter()
2208						.map(|v| v.vtxo.clone())
2209						.collect::<Vec<_>>();
2210					self.exit.write().await.mark_vtxos_for_exit(&vtxos);
2211
2212					self.db.remove_pending_lightning_send(payment_hash)?;
2213				}
2214			}
2215		}
2216
2217		Ok(None)
2218	}
2219
2220	/// Create, store and return a [Bolt11Invoice] for offchain boarding
2221	pub async fn bolt11_invoice(&self, amount: Amount) -> anyhow::Result<Bolt11Invoice> {
2222		let mut srv = self.require_server()?;
2223		let config = self.config();
2224
2225		// User needs to enfore the following delta:
2226		// - vtxo exit delta + htlc expiry delta (to give him time to exit the vtxo before htlc expires)
2227		// - vtxo exit margin (to give him time to exit the vtxo before htlc expires)
2228		// - htlc recv claim delta (to give him time to claim the htlc before it expires)
2229		let requested_min_cltv_delta = srv.info.vtxo_exit_delta +
2230			srv.info.htlc_expiry_delta +
2231			config.vtxo_exit_margin +
2232			config.htlc_recv_claim_delta +
2233			LIGHTNING_PREPARE_CLAIM_DELTA;
2234
2235		if requested_min_cltv_delta > srv.info.max_user_invoice_cltv_delta {
2236			bail!("HTLC CLTV delta ({}) is greater than Server's max HTLC recv CLTV delta: {}",
2237				requested_min_cltv_delta,
2238				srv.info.max_user_invoice_cltv_delta,
2239			);
2240		}
2241
2242		let preimage = Preimage::random();
2243		let payment_hash = preimage.compute_payment_hash();
2244		info!("Start bolt11 board with preimage / payment hash: {} / {}",
2245			preimage.as_hex(), payment_hash.as_hex());
2246
2247		let req = protos::StartLightningReceiveRequest {
2248			payment_hash: payment_hash.to_vec(),
2249			amount_sat: amount.to_sat(),
2250			min_cltv_delta: requested_min_cltv_delta as u32,
2251		};
2252
2253		let resp = srv.client.start_lightning_receive(req).await?.into_inner();
2254		info!("Ark Server is ready to receive LN payment to invoice: {}.", resp.bolt11);
2255
2256		let invoice = Bolt11Invoice::from_str(&resp.bolt11)
2257			.context("invalid bolt11 invoice returned by Ark server")?;
2258
2259		self.db.store_lightning_receive(
2260			payment_hash,
2261			preimage,
2262			&invoice,
2263			requested_min_cltv_delta,
2264		)?;
2265
2266		Ok(invoice)
2267	}
2268
2269	/// Fetches the status of a lightning receive for the given [PaymentHash].
2270	pub fn lightning_receive_status(
2271		&self,
2272		payment: impl Into<PaymentHash>,
2273	) -> anyhow::Result<Option<LightningReceive>> {
2274		Ok(self.db.fetch_lightning_receive_by_payment_hash(payment.into())?)
2275	}
2276
2277	/// Fetches all pending lightning receives ordered from newest to oldest.
2278	pub fn pending_lightning_receives(&self) -> anyhow::Result<Vec<LightningReceive>> {
2279		Ok(self.db.get_all_pending_lightning_receives()?)
2280	}
2281
2282	pub fn pending_lightning_receive_balance(&self) -> anyhow::Result<LightningReceiveBalance> {
2283		let pending_lightning_receives = self.pending_lightning_receives()?;
2284
2285		let mut total_pending_lightning_receive = Amount::ZERO;
2286		let mut claimable_pending_lightning_receive = Amount::ZERO;
2287		for receive in pending_lightning_receives {
2288			total_pending_lightning_receive += receive.invoice.amount_milli_satoshis()
2289				.map(|a| Amount::from_msat_floor(a))
2290				.expect("ln receive invoice should have amount");
2291			if let Some(htlc_vtxos) = receive.htlc_vtxos {
2292				claimable_pending_lightning_receive += htlc_vtxos.iter().map(|v| v.amount()).sum::<Amount>();
2293			}
2294		}
2295
2296		Ok(LightningReceiveBalance {
2297			total: total_pending_lightning_receive,
2298			claimable: claimable_pending_lightning_receive,
2299		})
2300	}
2301
2302	/// Claim incoming lightning payment with the given [PaymentHash].
2303	///
2304	/// This function reveals the preimage of the lightning payment in
2305	/// exchange of getting pubkey VTXOs from HTLC ones
2306	///
2307	/// # Arguments
2308	///
2309	/// * `payment_hash` - The [PaymentHash] of the lightning payment
2310	/// to wait for.
2311	/// * `vtxos` - The list of HTLC VTXOs that were previously granted
2312	/// by the Server, with the hash lock clause matching payment hash.
2313	///
2314	/// # Returns
2315	///
2316	/// Returns an `anyhow::Result<()>`, which is:
2317	/// * `Ok(())` if the process completes successfully.
2318	/// * `Err` if an error occurs at any stage of the operation.
2319	///
2320	/// # Remarks
2321	///
2322	/// * The list of HTLC VTXOs must have the hash lock clause matching the given
2323	///   [PaymentHash].
2324	async fn claim_ln_receive(
2325		&self,
2326		lightning_receive: &LightningReceive,
2327	) -> anyhow::Result<()> {
2328		let mut srv = self.require_server()?;
2329
2330		// order inputs by vtxoid before we generate nonces
2331		let inputs = {
2332			let htlc_vtxos = lightning_receive.htlc_vtxos.as_ref()
2333				.context("no HTLC VTXOs set on record yet")?;
2334			let mut ret = htlc_vtxos.iter().map(|v| &v.vtxo).collect::<Vec<_>>();
2335			ret.sort_by_key(|v| v.id());
2336			ret
2337		};
2338
2339		let (keypairs, sec_nonces, pub_nonces) = inputs.iter().map(|v| {
2340			let keypair = self.get_vtxo_key(v)?;
2341			let (sec_nonce, pub_nonce) = musig::nonce_pair(&keypair);
2342			Ok((keypair, sec_nonce, pub_nonce))
2343		}).collect::<anyhow::Result<(Vec<_>, Vec<_>, Vec<_>)>>()?;
2344
2345		// Claiming arkoor against preimage
2346		let (claim_keypair, _) = self.derive_store_next_keypair()?;
2347		let receive_policy = VtxoPolicy::new_pubkey(claim_keypair.public_key());
2348
2349		let pay_req = VtxoRequest {
2350			policy: receive_policy.clone(),
2351			amount: inputs.iter().map(|v| v.amount()).sum(),
2352		};
2353		trace!("ln arkoor builder params: inputs: {:?}; user_nonces: {:?}; req: {:?}",
2354			inputs.iter().map(|v| v.id()).collect::<Vec<_>>(), pub_nonces, pay_req,
2355		);
2356		let builder = ArkoorPackageBuilder::new(
2357			inputs.iter().copied(), &pub_nonces, pay_req, None,
2358		)?;
2359
2360		info!("Claiming arkoor against payment preimage");
2361		self.db.set_preimage_revealed(lightning_receive.payment_hash)?;
2362		let resp = srv.client.claim_lightning_receive(protos::ClaimLightningReceiveRequest {
2363			payment_hash: lightning_receive.payment_hash.to_byte_array().to_vec(),
2364			payment_preimage: lightning_receive.payment_preimage.to_vec(),
2365			vtxo_policy: receive_policy.serialize(),
2366			user_pub_nonces: pub_nonces.iter().map(|n| n.serialize().to_vec()).collect(),
2367		}).await?.into_inner();
2368		let cosign_resp: Vec<_> = resp.try_into().context("invalid cosign response")?;
2369
2370		ensure!(builder.verify_cosign_response(&cosign_resp),
2371			"invalid arkoor cosignature received from server",
2372		);
2373
2374		let (outputs, change) = builder.build_vtxos(&cosign_resp, &keypairs, sec_nonces)?;
2375		if change.is_some() {
2376			bail!("shouldn't have change VTXO, this is a bug");
2377		}
2378
2379		for (vtxo, input) in outputs.iter().zip(inputs.iter()) {
2380			if let Ok(tx) = self.chain.get_tx(&input.chain_anchor().txid).await {
2381				let tx = tx.with_context(|| {
2382					format!("input vtxo chain anchor not found for lightning receive vtxo: {}", input.chain_anchor().txid)
2383				})?;
2384				vtxo.validate(&tx).context("invalid lightning receive htlc vtxo")?;
2385			} else {
2386				warn!("We couldn't validate the new VTXOs because of chain source error.");
2387			}
2388		}
2389
2390		info!("Got arkoors from lightning: {}",
2391			outputs.iter().map(|v| v.id().to_string()).collect::<Vec<_>>().join(", "));
2392		self.db.register_movement(MovementArgs {
2393			kind: MovementKind::LightningReceive,
2394			spends: &inputs,
2395			receives: &outputs.iter().map(|v| (v, VtxoState::Spendable)).collect::<Vec<_>>(),
2396			recipients: &[],
2397			fees: None,
2398		})?;
2399
2400		self.db.remove_pending_lightning_receive(lightning_receive.payment_hash)?;
2401
2402		Ok(())
2403	}
2404
2405	/// Check for incoming lightning payment with the given [PaymentHash].
2406	///
2407	/// This function checks for an incoming lightning payment with the
2408	/// given [PaymentHash] and returns the HTLC VTXOs that are associated
2409	/// with it.
2410	///
2411	/// # Arguments
2412	///
2413	/// * `payment_hash` - The [PaymentHash] of the lightning payment
2414	/// to check for.
2415	/// * `wait` - Whether to wait for the payment to be received.
2416	///
2417	/// # Returns
2418	///
2419	/// Returns an `anyhow::Result<Vec<WalletVtxo>>`, which is:
2420	/// * `Ok(wallet_vtxos)` if the process completes successfully, where `wallet_vtxos` is
2421	///   the list of HTLC VTXOs that are associated with the payment.
2422	/// * `Err` if an error occurs at any stage of the operation.
2423	///
2424	/// # Remarks
2425	///
2426	/// * The invoice must contain an explicit amount specified in milli-satoshis.
2427	/// * The HTLC expiry height is calculated by adding the servers' HTLC expiry delta to the
2428	///   current chain tip.
2429	/// * The payment hash must be from an invoice previously generated using
2430	///   [Wallet::bolt11_invoice].
2431	pub async fn check_ln_receive(
2432		&self,
2433		payment_hash: PaymentHash,
2434		wait: bool,
2435	) -> anyhow::Result<LightningReceive> {
2436		let mut srv = self.require_server()?;
2437		let current_height = self.chain.tip().await?;
2438
2439		let mut lightning_receive = self.db.fetch_lightning_receive_by_payment_hash(payment_hash)?
2440			.context("no lightning receive found")?;
2441
2442		info!("Waiting for payment...");
2443		let sub = srv.client.check_lightning_receive(protos::CheckLightningReceiveRequest {
2444			hash: payment_hash.to_byte_array().to_vec(), wait,
2445		}).await?.into_inner();
2446
2447		let status = protos::LightningReceiveStatus::try_from(sub.status)
2448			.with_context(|| format!("unknown payment status: {}", sub.status))?;
2449		match status {
2450			// this is the good case
2451			protos::LightningReceiveStatus::Accepted
2452				| protos::LightningReceiveStatus::HtlcsReady => {},
2453			protos::LightningReceiveStatus::Created => bail!("sender didn't initiate payment yet"),
2454			protos::LightningReceiveStatus::Settled => bail!("payment already settled"),
2455			protos::LightningReceiveStatus::Cancelled => bail!("payment was canceled"),
2456		}
2457
2458		// If we have already HTLC VTXOs stored, let's only check if the server sent us the same ones
2459		if let Some(vtxos) = &lightning_receive.htlc_vtxos {
2460			debug_assert!({
2461				let vtxos_by_id = vtxos.iter().map(|v| (v.id(), v)).collect::<HashMap<_, _>>();
2462				sub.htlc_vtxos.iter().all(|v| {
2463					match VtxoId::from_slice(v) {
2464						Ok(id) => vtxos_by_id.contains_key(&id),
2465						Err(_) => false,
2466					}
2467				})
2468			}, "server sent HTLC VTXOs that we don't have");
2469
2470			return Ok(lightning_receive)
2471		}
2472
2473		let htlc_recv_expiry = current_height + lightning_receive.htlc_recv_cltv_delta as BlockHeight;
2474
2475		let (keypair, _) = self.derive_store_next_keypair()?;
2476		let req = protos::PrepareLightningReceiveClaimRequest {
2477			payment_hash: lightning_receive.payment_hash.to_vec(),
2478			user_pubkey: keypair.public_key().serialize().to_vec(),
2479			htlc_recv_expiry: htlc_recv_expiry,
2480		};
2481		let res = srv.client.prepare_lightning_receive_claim(req).await
2482			.context("error preparing lightning receive claim")?.into_inner();
2483		let vtxos = res.htlc_vtxos.into_iter()
2484			.map(|b| Vtxo::deserialize(&b))
2485			.collect::<Result<Vec<_>, _>>()
2486			.context("invalid htlc vtxos from server")?;
2487
2488		// sanity check the vtxos
2489		for vtxo in &vtxos {
2490			if let VtxoPolicy::ServerHtlcRecv(p) = vtxo.policy() {
2491				if p.payment_hash != lightning_receive.payment_hash {
2492					bail!("invalid payment hash on HTLC VTXOs received from server: {}",
2493						p.payment_hash,
2494					);
2495				}
2496				if p.user_pubkey != keypair.public_key() {
2497					bail!("invalid pubkey on HTLC VTXOs received from server: {}", p.user_pubkey);
2498				}
2499				if p.htlc_expiry < htlc_recv_expiry {
2500					bail!("HTLC VTXO expiry height is less than requested: Requested {}, received {}", htlc_recv_expiry, p.htlc_expiry);
2501				}
2502			} else {
2503				bail!("invalid HTLC VTXO policy: {:?}", vtxo.policy());
2504			}
2505		}
2506
2507		// check sum match invoice amount
2508		let invoice_amount = lightning_receive.invoice.amount_milli_satoshis().map(|a| Amount::from_msat_floor(a))
2509			.expect("ln receive invoice should have amount");
2510		ensure!(vtxos.iter().map(|v| v.amount()).sum::<Amount>() >= invoice_amount,
2511			"Server didn't return enough VTXOs to cover invoice amount"
2512		);
2513
2514		let vtxos = vtxos.into_iter().map(|v| WalletVtxo {
2515			vtxo: v.clone(),
2516			state: VtxoState::Locked,
2517		}).collect::<Vec<_>>();
2518
2519		self.db.register_movement(MovementArgs {
2520			kind: MovementKind::LightningReceive,
2521			spends: &[],
2522			receives: &vtxos.iter().map(|v| (&v.vtxo, v.state.clone())).collect::<Vec<_>>(),
2523			recipients: &[],
2524			fees: None,
2525		})?;
2526
2527		let vtxo_ids = vtxos.iter().map(|v| v.id()).collect::<Vec<_>>();
2528		self.db.set_lightning_receive_vtxos(payment_hash, &vtxo_ids)?;
2529
2530		lightning_receive.htlc_vtxos = Some(vtxos);
2531
2532		Ok(lightning_receive)
2533	}
2534
2535	/// Check and claim a Lightning receive
2536	///
2537	/// This function checks for an incoming lightning payment with the given [PaymentHash]
2538	/// and then claims the payment using returned HTLC VTXOs.
2539	///
2540	/// # Arguments
2541	///
2542	/// * `payment_hash` - The [PaymentHash] of the lightning payment
2543	/// to check for.
2544	/// * `wait` - Whether to wait for the payment to be received.
2545	///
2546	/// # Returns
2547	///
2548	/// Returns an `anyhow::Result<()>`, which is:
2549	/// * `Ok(())` if the process completes successfully.
2550	/// * `Err` if an error occurs at any stage of the operation.
2551	///
2552	/// # Remarks
2553	///
2554	/// * The payment hash must be from an invoice previously generated using
2555	///   [Wallet::bolt11_invoice].
2556	pub async fn check_and_claim_ln_receive(
2557		&self,
2558		payment_hash: PaymentHash,
2559		wait: bool,
2560	) -> anyhow::Result<()> {
2561		let receive = self.check_ln_receive(payment_hash, wait).await?;
2562		self.claim_ln_receive(&receive).await
2563	}
2564
2565	/// Check and claim all opened Lightning receive
2566	///
2567	/// This function fetches all opened lightning receives and then
2568	/// concurrently tries to check and claim them
2569	///
2570	/// # Arguments
2571	///
2572	/// * `wait` - Whether to wait for each payment to be received.
2573	///
2574	/// # Returns
2575	///
2576	/// Returns an `anyhow::Result<()>`, which is:
2577	/// * `Ok(())` if the process completes successfully.
2578	/// * `Err` if an error occurs at any stage of the operation.
2579	pub async fn check_and_claim_all_open_ln_receives(&self, wait: bool) -> anyhow::Result<()> {
2580		// Asynchronously attempts to claim all pending receive by converting the list into a stream
2581		tokio_stream::iter(self.pending_lightning_receives()?)
2582			.for_each_concurrent(3, |rcv| async move {
2583				if let Err(e) = self.check_and_claim_ln_receive(rcv.invoice.into(), wait).await {
2584					error!("Error claiming lightning receive: {}", e);
2585				}
2586			}).await;
2587
2588		Ok(())
2589	}
2590
2591	/// Claim all pending lightning receives
2592	///
2593	/// This is different from [Wallet::check_and_claim_all_open_ln_receives] in
2594	/// that it only affect pending lightning receives whose HTLC VTXOs
2595	/// have already been issued.
2596	///
2597	/// Note:
2598	///   - If an HTLC VTXO cannot be claimed and the htlc expiry is too close,
2599	///   it will either mark the htlc as cancelled (preimage not revealed) or
2600	///   mark the vtxo for exit (preimage revealed).
2601	async fn claim_all_pending_htlc_recvs(&self) -> anyhow::Result<()> {
2602		let srv = self.require_server()?;
2603		let tip = self.chain.tip().await?;
2604		let lightning_receives = self.db.get_all_pending_lightning_receives()?;
2605		info!("Syncing {} pending lightning receives", lightning_receives.len());
2606
2607		for lightning_receive in lightning_receives {
2608			let vtxos = match &lightning_receive.htlc_vtxos {
2609				Some(vtxos) => vtxos,
2610				None => continue,
2611			};
2612
2613			if let Err(e) = self.claim_ln_receive(&lightning_receive).await {
2614				error!("Failed to claim pubkey vtxo from htlc vtxo: {}", e);
2615
2616				let first_vtxo = &vtxos.first().unwrap().vtxo;
2617				debug_assert!(vtxos.iter().all(|v| {
2618					v.vtxo.policy() == first_vtxo.policy() && v.vtxo.exit_delta() == first_vtxo.exit_delta()
2619				}), "all htlc vtxos for the same payment hash should have the same policy and exit delta");
2620
2621				let vtxo_htlc_expiry = first_vtxo.policy().as_server_htlc_recv()
2622					.expect("only server htlc recv vtxos can be pending lightning recv").htlc_expiry;
2623
2624				let safe_exit_margin = first_vtxo.exit_delta() +
2625					srv.info.htlc_expiry_delta +
2626					self.config.vtxo_exit_margin;
2627
2628				if tip > vtxo_htlc_expiry.saturating_sub(safe_exit_margin as BlockHeight) {
2629					if lightning_receive.preimage_revealed_at.is_some() {
2630						warn!("HTLC-recv VTXOs are about to expire and preimage has been disclosed, must exit");
2631						self.exit.write().await.mark_vtxos_for_exit(&vtxos.iter().map(|v| v.vtxo.clone()).collect::<Vec<_>>());
2632					} else {
2633						warn!("HTLC-recv VTXOs are about to expire, but preimage has not been disclosed yet, mark htlc as cancelled");
2634						self.db.register_movement(MovementArgs {
2635							kind: MovementKind::LightningReceive,
2636							spends: &vtxos.iter().map(|v| &v.vtxo).collect::<Vec<_>>(),
2637							receives: &[],
2638							recipients: &[],
2639							fees: None,
2640						})?;
2641					}
2642				}
2643			}
2644		}
2645
2646		Ok(())
2647	}
2648
2649	/// Same as [Wallet::send_lightning_payment] but instead it pays a [LightningAddress].
2650	pub async fn send_lnaddr(
2651		&self,
2652		addr: &LightningAddress,
2653		amount: Amount,
2654		comment: Option<&str>,
2655	) -> anyhow::Result<(Bolt11Invoice, Preimage)> {
2656		let invoice = lnurl::lnaddr_invoice(addr, amount, comment).await
2657			.context("lightning address error")?;
2658		info!("Attempting to pay invoice {}", invoice);
2659		let preimage = self.send_lightning_payment(Invoice::Bolt11(invoice.clone()), None).await
2660			.context("bolt11 payment error")?;
2661		Ok((invoice, preimage))
2662	}
2663
2664	/// Attempts to pay the given BOLT12 [Offer] using offchain funds.
2665	pub async fn pay_offer(
2666		&self,
2667		offer: Offer,
2668		amount: Option<Amount>,
2669	) -> anyhow::Result<(Bolt12Invoice, Preimage)> {
2670		let mut srv = self.require_server()?;
2671
2672		let offer_bytes = {
2673			let mut bytes = Vec::new();
2674			offer.write(&mut bytes).unwrap();
2675			bytes
2676		};
2677
2678		let req = protos::FetchBolt12InvoiceRequest {
2679			offer: offer_bytes,
2680			amount_sat: amount.map(|a| a.to_sat()),
2681		};
2682
2683		let resp = srv.client.fetch_bolt12_invoice(req).await?.into_inner();
2684
2685		let invoice = Bolt12Invoice::try_from(resp.invoice)
2686			.map_err(|_| anyhow::anyhow!("invalid invoice"))?;
2687
2688		invoice.validate_issuance(offer)?;
2689
2690		let preimage = self.send_lightning_payment(Invoice::Bolt12(invoice.clone()), None).await
2691			.context("bolt11 payment error")?;
2692		Ok((invoice, preimage))
2693	}
2694
2695	/// Sends the given [Amount] to an onchain [bitcoin::Address]. This is an in-round operation
2696	/// which may take a long time to perform.
2697	pub async fn send_round_onchain_payment(
2698		&self,
2699		addr: bitcoin::Address,
2700		amount: Amount,
2701	) -> anyhow::Result<Offboard> {
2702		let balance = self.balance()?.spendable;
2703
2704		// do a quick check to fail early and not wait for round if we don't have enough money
2705		let early_fees = OffboardRequest::calculate_fee(
2706			&addr.script_pubkey(), FeeRate::BROADCAST_MIN,
2707		).expect("script from address");
2708
2709		if balance < amount + early_fees {
2710			bail!("Your balance is too low. Needed: {}, available: {}",
2711				amount + early_fees, balance,
2712			);
2713		}
2714
2715		let participation = DesiredRoundParticipation::OnchainPayment {
2716			destination: addr.script_pubkey(),
2717			amount,
2718		};
2719		let RoundResult { round_id, .. } = self.participate_round(participation).await
2720			.context("round failed")?;
2721
2722		Ok(Offboard { round: round_id })
2723	}
2724}