Skip to main content

bark/
lib.rs

1//! ![bark: Ark on bitcoin](https://gitlab.com/ark-bitcoin/bark/-/raw/master/assets/bark-header-white.jpg)
2//!
3//! <div align="center">
4//! <h1>Bark: Ark on bitcoin</h1>
5//! <p>Fast, low-cost, self-custodial payments on bitcoin.</p>
6//! </div>
7//!
8//! <p align="center">
9//! <br />
10//! <a href="https://docs.second.tech">Docs</a> ·
11//! <a href="https://gitlab.com/ark-bitcoin/bark/-/issues">Issues</a> ·
12//! <a href="https://second.tech">Website</a> ·
13//! <a href="https://blog.second.tech">Blog</a> ·
14//! <a href="https://www.youtube.com/@2ndbtc">YouTube</a>
15//! </p>
16//!
17//! <div align="center">
18//!
19//! [![Release](https://img.shields.io/gitlab/v/release/ark-bitcoin/bark?gitlab_url=https://gitlab.com&sort=semver&label=release)
20//! [![Project Status](https://img.shields.io/badge/status-experimental-red.svg)](https://gitlab.com/ark-bitcoin/bark)
21//! [![License](https://img.shields.io/badge/license-CC0--1.0-blue.svg)](https://gitlab.com/ark-bitcoin/bark/-/blob/master/LICENSE)
22//! [![PRs welcome](https://img.shields.io/badge/PRs-welcome-brightgreen?logo=git)](https://gitlab.com/ark-bitcoin/bark/-/blob/master/CONTRIBUTING.md)
23//! [![Community](https://img.shields.io/badge/community-forum-blue?logo=discourse)](https://community.second.tech)
24//!
25//! </div>
26//! <br />
27//!
28//! Bark is an implementation of the Ark protocol on bitcoin, led by [Second](https://second.tech).
29//!
30//! # A tour of Bark
31//!
32//! Integrating the Ark-protocol offers
33//!
34//! - 🏃‍♂️ **Smooth boarding**: No channels to open, no on-chain setup required—create a wallet and start transacting
35//! - 🤌 **Simplified UX**: Send and receive without managing channels, liquidity, or routing
36//! - 🌐 **Universal payments**: Send Ark, Lightning, and on-chain payments from a single off-chain balance
37//! - 🔌 **Easier integration**: Client-server architecture reduces complexity compared to P2P protocols
38//! - 💸 **Lower costs**: Instant payments at a fraction of on-chain fees
39//! - 🔒 **Self-custodial**: Users maintain full control of their funds at all times
40//!
41//! This guide puts focus on how to use the Rust-API and assumes
42//! some basic familiarity with the Ark protocol. We refer to the
43//! [protocol docs](http://docs.second.tech/ark-protocol) for an introduction.
44//!
45//! ## Creating an Ark wallet
46//!
47//! The user experience of setting up an Ark wallet is pretty similar
48//! to setting up an onchain wallet. You need to provide a [bip39::Mnemonic] which
49//! can be used to recover funds. Typically, most apps request the user
50//! to write down the mnemonic or ensure they use another method for a secure back-up.
51//!
52//! The user can select an Ark server and a [chain::ChainSource] as part of
53//! the configuration. The example below configures
54//!
55//! You will also need a place to store all [ark::Vtxo]s on the users device.
56//! We have implemented [`persist::sqlite::SqliteClient`] which is a sane default on most devices
57//! (requires the `sqlite` feature). However, it is possible to implement a
58//! [BarkPersister] if you have other requirements.
59//!
60//! The code-snippet below shows how you can create a [Wallet].
61//!
62//! ```no_run
63//! use std::path::PathBuf;
64//! use std::sync::Arc;
65//! use tokio::fs;
66//! use bark::{Config, onchain, Wallet};
67//! use bark::lock_manager::memory::MemoryLockManager;
68//! use bark::persist::sqlite::SqliteClient;
69//!
70//! const MNEMONIC_FILE : &str = "mnemonic";
71//! const DB_FILE: &str = "db.sqlite";
72//!
73//! #[tokio::main]
74//! async fn main() {
75//! 	// Pick the bitcoin network that will be used
76//! 	let network = bitcoin::Network::Signet;
77//!
78//! 	// Configure the wallet
79//! 	let config = Config {
80//! 		server_address: String::from("https://ark.signet.2nd.dev"),
81//! 		esplora_address: Some(String::from("https://esplora.signet.2nd.dev")),
82//! 		..Config::network_default(network)
83//! 	};
84//!
85//!
86//! 	// Create a sqlite database
87//! 	let datadir = PathBuf::from("./bark");
88//! 	let db = Arc::new(SqliteClient::open(datadir.join(DB_FILE)).unwrap());
89//!
90//! 	// Generate and seed and store it somewhere
91//! 	let mnemonic = bip39::Mnemonic::generate(12).expect("12 is valid");
92//! 	fs::write(datadir.join(MNEMONIC_FILE), mnemonic.to_string().as_bytes()).await.unwrap();
93//!
94//! 	let lock_manager = Box::new(MemoryLockManager::new());
95//! 	let wallet = Wallet::create(
96//! 		&mnemonic,
97//! 		network,
98//! 		config,
99//! 		db,
100//! 		lock_manager,
101//! 		false
102//! 	).await.unwrap();
103//! }
104//! ```
105//!
106//! ## Opening an existing Ark wallet
107//!
108//! The [Wallet] can be opened again by providing the [bip39::Mnemonic] and
109//! the [BarkPersister] again. Note, that [`persist::sqlite::SqliteClient`] implements the [BarkPersister]-trait.
110//!
111//! ```no_run
112//! # use std::sync::Arc;
113//! # use std::path::PathBuf;
114//! # use std::str::FromStr;
115//! #
116//! # use bip39;
117//! # use tokio::fs;
118//! #
119//! # use bark::{Config, Wallet};
120//! # use bark::lock_manager::memory::MemoryLockManager;
121//! # use bark::persist::sqlite::SqliteClient;
122//! #
123//! const MNEMONIC_FILE : &str = "mnemonic";
124//! const DB_FILE: &str = "db.sqlite";
125//!
126//! #[tokio::main]
127//! async fn main() {
128//! 	let datadir = PathBuf::from("./bark");
129//! 	let config = Config {
130//! 		server_address: String::from("https://ark.signet.2nd.dev"),
131//! 		esplora_address: Some(String::from("https://esplora.signet.2nd.dev")),
132//! 		..Config::network_default(bitcoin::Network::Signet)
133//! 	};
134//!
135//! 	let db = Arc::new(SqliteClient::open(datadir.join(DB_FILE)).unwrap());
136//! 	let mnemonic_str = fs::read_to_string(datadir.join(DB_FILE)).await.unwrap();
137//! 	let mnemonic = bip39::Mnemonic::from_str(&mnemonic_str).unwrap();
138//! 	let lock_manager = Box::new(MemoryLockManager::new());
139//! 	let wallet = Wallet::open(&mnemonic, db, config, lock_manager).await.unwrap();
140//! }
141//! ```
142//!
143//! ## Receiving coins
144//!
145//! For the time being we haven't implemented an Ark address type (yet). You
146//! can send funds directly to a public key.
147//!
148//! If you are on signet and your Ark server is [https://ark.signet.2nd.dev](https://ark.signet.2nd.dev),
149//! you can request some sats from our [faucet](https://signet.2nd.dev).
150//!
151//! ```no_run
152//! # use std::sync::Arc;
153//! # use std::str::FromStr;
154//! # use std::path::PathBuf;
155//! #
156//! # use tokio::fs;
157//! #
158//! # use bark::{Config, Wallet};
159//! # use bark::lock_manager::memory::MemoryLockManager;
160//! # use bark::persist::sqlite::SqliteClient;
161//! #
162//! # const MNEMONIC_FILE : &str = "mnemonic";
163//! # const DB_FILE: &str = "db.sqlite";
164//! #
165//! # async fn get_wallet() -> Wallet {
166//! 	#   let datadir = PathBuf::from("./bark");
167//! 	#   let config = Config::network_default(bitcoin::Network::Signet);
168//! 	#
169//! 	#   let db = Arc::new(SqliteClient::open(datadir.join(DB_FILE)).unwrap());
170//! 	#   let mnemonic_str = fs::read_to_string(datadir.join(DB_FILE)).await.unwrap();
171//! 	#   let mnemonic = bip39::Mnemonic::from_str(&mnemonic_str).unwrap();
172//! 	#   let lock_manager = Box::new(MemoryLockManager::new());
173//! 	#   Wallet::open(&mnemonic, db, config, lock_manager).await.unwrap()
174//! 	# }
175//! #
176//!
177//! #[tokio::main]
178//! async fn main() -> anyhow::Result<()> {
179//! 	let wallet = get_wallet().await;
180//! 	let address: ark::Address = wallet.new_address().await?;
181//! 	Ok(())
182//! }
183//! ```
184//!
185//! ## Inspecting the wallet
186//!
187//! An Ark wallet contains [ark::Vtxo]s. These are just like normal utxos
188//! in a bitcoin wallet. They just haven't been confirmed on chain (yet).
189//! However, the user remains in full control of the funds and can perform
190//! a unilateral exit at any time.
191//!
192//! The snippet below shows how you can inspect your [WalletVtxo]s.
193//!
194//! ```no_run
195//! # use std::sync::Arc;
196//! # use std::str::FromStr;
197//! # use std::path::PathBuf;
198//! #
199//! # use tokio::fs;
200//! #
201//! # use bark::{Config, Wallet};
202//! # use bark::lock_manager::memory::MemoryLockManager;
203//! # use bark::persist::sqlite::SqliteClient;
204//! #
205//! # const MNEMONIC_FILE : &str = "mnemonic";
206//! # const DB_FILE: &str = "db.sqlite";
207//! #
208//! # async fn get_wallet() -> Wallet {
209//! 	#   let datadir = PathBuf::from("./bark");
210//! 	#
211//! 	#   let db = Arc::new(SqliteClient::open(datadir.join(DB_FILE)).unwrap());
212//! 	#   let mnemonic_str = fs::read_to_string(datadir.join(DB_FILE)).await.unwrap();
213//! 	#   let mnemonic = bip39::Mnemonic::from_str(&mnemonic_str).unwrap();
214//! 	#
215//! 	#   let config = Config::network_default(bitcoin::Network::Signet);
216//! 	#
217//! 	#   let lock_manager = Box::new(MemoryLockManager::new());
218//! 	#   Wallet::open(&mnemonic, db, config, lock_manager).await.unwrap()
219//! 	# }
220//! #
221//!
222//! #[tokio::main]
223//! async fn main() -> anyhow::Result<()> {
224//! 	let mut wallet = get_wallet().await;
225//!
226//! 	// The vtxo's command doesn't sync your wallet
227//! 	// When you're not running the daemon, make sure your app is synced
228//! 	// before inspecting the wallet
229//! 	wallet.sync().await;
230//!
231//! 	let vtxos: Vec<bark::WalletVtxo> = wallet.vtxos().await.unwrap();
232//! 	Ok(())
233//! }
234//! ```
235//!
236//! Use [Wallet::balance] if you are only interested in the balance.
237//!
238//! ## Participating in a round
239//!
240//! You can participate in a round to refresh your coins. Typically,
241//! you want to refresh coins which are soon to expire or you might
242//! want to aggregate multiple small vtxos to keep the cost of exit
243//! under control.
244//!
245//! As a wallet developer you can implement your own refresh strategy.
246//! This gives you full control over which [ark::Vtxo]s are refreshed and
247//! which aren't.
248//!
249//! This example uses [RefreshStrategy::must_refresh] which is a sane
250//! default that selects all [ark::Vtxo]s that must be refreshed.
251//!
252//! ```no_run
253//! # use std::sync::Arc;
254//! # use std::str::FromStr;
255//! # use std::path::PathBuf;
256//! #
257//! # use tokio::fs;
258//! #
259//! # use bark::{Config, Wallet};
260//! # use bark::lock_manager::memory::MemoryLockManager;
261//! # use bark::persist::sqlite::SqliteClient;
262//! #
263//! # const MNEMONIC_FILE : &str = "mnemonic";
264//! # const DB_FILE: &str = "db.sqlite";
265//! #
266//! # async fn get_wallet() -> Wallet {
267//! 	#   let datadir = PathBuf::from("./bark");
268//! 	#
269//! 	#   let db = Arc::new(SqliteClient::open(datadir.join(DB_FILE)).unwrap());
270//! 	#   let mnemonic_str = fs::read_to_string(datadir.join(DB_FILE)).await.unwrap();
271//! 	#   let mnemonic = bip39::Mnemonic::from_str(&mnemonic_str).unwrap();
272//! 	#
273//! 	#   let config = Config::network_default(bitcoin::Network::Signet);
274//! 	#
275//! 	#   let lock_manager = Box::new(MemoryLockManager::new());
276//! 	#   Wallet::open(&mnemonic, db, config, lock_manager).await.unwrap()
277//! 	# }
278//! #
279//! use bark::vtxo::RefreshStrategy;
280//!
281//! #[tokio::main]
282//! async fn main() -> anyhow::Result<()> {
283//! 	let wallet = get_wallet().await;
284//!
285//! 	// Select all vtxos that refresh soon
286//! 	let tip = wallet.chain().tip().await?;
287//! 	let fee_rate = wallet.chain().fee_rates().await.fast;
288//! 	let strategy = RefreshStrategy::must_refresh(&wallet, tip, fee_rate);
289//!
290//! 	let vtxos = wallet.spendable_vtxos_with(&strategy).await?;
291//!		wallet.refresh_vtxos(vtxos).await?;
292//! 	Ok(())
293//! }
294//! ```
295
296#[cfg(all(any(target_os = "android", target_os = "ios"), feature = "tls-native-roots"))]
297compile_error!("feature `tls-native-roots` can't be used on Android or iOS, use `tls-webpki-roots` instead");
298
299pub extern crate ark;
300
301pub extern crate bip39;
302pub extern crate lightning_invoice;
303pub extern crate lnurl as lnurllib;
304
305#[macro_use] extern crate anyhow;
306#[macro_use] extern crate async_trait;
307#[macro_use] extern crate serde;
308
309pub mod actions;
310pub mod chain;
311pub mod exit;
312pub mod movement;
313pub mod onchain;
314pub mod payment_request;
315pub mod persist;
316pub mod round;
317pub mod subsystem;
318pub mod vtxo;
319
320pub mod lock_manager;
321
322mod arkoor;
323mod board;
324mod config;
325mod daemon;
326mod fees;
327mod lightning;
328mod mailbox;
329mod notification;
330mod offboard;
331#[cfg(feature = "socks5-proxy")]
332mod proxy;
333mod psbtext;
334mod utils;
335
336pub use self::arkoor::{ArkoorCreateResult, ArkoorAddressError};
337pub use self::config::{BarkNetwork, Config};
338pub use self::daemon::DaemonHandle;
339pub use self::fees::FeeEstimate;
340pub use self::notification::{WalletNotification, NotificationStream};
341pub use self::vtxo::WalletVtxo;
342pub use self::utils::time;
343
344use std::borrow::Cow;
345use std::collections::HashSet;
346use std::sync::Arc;
347use std::time::Duration;
348
349use anyhow::{bail, Context};
350use ark::rounds::RoundEvent;
351use bip39::Mnemonic;
352use bitcoin::{Amount, Network, OutPoint};
353use bitcoin::bip32::{self, ChildNumber, Fingerprint};
354use bitcoin::secp256k1::{self, Keypair, PublicKey};
355use log::{debug, error, info, trace, warn};
356use tokio_stream::StreamExt;
357
358use ark::{ArkInfo, ProtocolEncoding, Vtxo, VtxoId, VtxoPolicy, VtxoRequest};
359use ark::address::VtxoDelivery;
360use ark::fees::{validate_and_subtract_fee_min_dust, VtxoFeeInfo};
361use ark::vtxo::{Full, PubkeyVtxoPolicy, VtxoRef};
362use ark::vtxo::policy::signing::VtxoSigner;
363use bitcoin_ext::{BlockHeight, P2TR_DUST};
364use server_rpc::{protos, ServerConnection};
365use server_rpc::client::{ConnectError, CreateEndpointError};
366
367use crate::chain::{ChainSource, ChainSourceSpec};
368use crate::exit::Exit;
369use crate::lock_manager::LockManager;
370use crate::movement::{Movement, MovementId, PaymentMethod};
371use crate::movement::manager::MovementManager;
372use crate::movement::update::MovementUpdate;
373use crate::notification::NotificationDispatch;
374use crate::onchain::{ExitUnilaterally, PreparePsbt, SignPsbt, Utxo};
375use crate::onchain::DaemonizableOnchainWallet;
376use crate::persist::BarkPersister;
377use crate::persist::models::{RoundStateId, StoredRoundState, Unlocked};
378#[cfg(feature = "socks5-proxy")]
379use crate::proxy::proxy_for_url;
380use crate::round::{RoundParticipation, RoundSecretNonces, RoundStatus};
381use crate::subsystem::{ArkoorMovement, RoundMovement};
382use crate::vtxo::{FilterVtxos, RefreshStrategy, VtxoFilter, VtxoStateKind};
383
384#[cfg(all(feature = "wasm-web", feature = "socks5-proxy"))]
385compile_error!("features `wasm-web` does not support feature `socks5-proxy");
386
387#[cfg(all(feature = "wasm-web", feature = "bitcoind-rpc"))]
388compile_error!("`wasm-web` does not support the `bitcoind-rpc` feature");
389
390/// Derivation index for Bark usage
391const BARK_PURPOSE_INDEX: u32 = 350;
392/// Derivation index used to generate keypairs to sign VTXOs
393const VTXO_KEYS_INDEX: u32 = 0;
394/// Derivation index used to generate keypair for the mailbox
395const MAILBOX_KEY_INDEX: u32 = 1;
396/// Derivation index used to generate keypair for the recovery mailbox
397const RECOVERY_MAILBOX_KEY_INDEX: u32 = 2;
398const MISSING_SERVER_TRANSPORT_HELP: &str =
399	"This build of bark-wallet does not include an Ark server transport backend. Enable feature `bark-wallet/native` or `bark-wallet/wasm-web` to use server-backed wallet functionality.";
400
401lazy_static::lazy_static! {
402	/// Global secp context.
403	static ref SECP: secp256k1::Secp256k1<secp256k1::All> = secp256k1::Secp256k1::new();
404}
405
406/// Log that the server public key has changed.
407///
408/// Recommends that the user perform an emergency exit to recover their
409/// funds on-chain, since a rotated server pubkey makes the original VTXO
410/// spend/exit conditions unreachable.
411fn log_server_pubkey_changed_error(expected: PublicKey, got: PublicKey) {
412	error!(
413	    "
414Server public key has changed!
415
416The Ark server's public key is different from the one stored when this
417wallet was created. This typically happens when:
418
419	- The server operator has rotated their keys
420	- You are connecting to a different server
421	- The server has been replaced
422
423For safety, this wallet will not connect to the server until you
424resolve this. You can recover your funds on-chain by doing an emergency exit.
425
426This will exit your VTXOs to on-chain Bitcoin without needing the server's cooperation.
427
428Expected: {expected}
429Got:      {got}")
430}
431
432/// Log that the server mailbox pubkey has changed.
433fn log_server_mailbox_pubkey_changed_error(expected: PublicKey, got: PublicKey) {
434	error!(
435	    "
436Server mailbox public key has changed!
437
438The Ark server's mailbox public key is different from the one stored when this
439wallet was created. This typically happens when:
440
441	- The server operator has rotated their keys
442	- You are connecting to a different server
443	- The server has been replaced
444
445For safety, this wallet will not connect to the server until you resolve this.
446
447Unlike a server pubkey change, your VTXOs are not at risk - the mailbox pubkey
448only affects address receive semantics. Any Ark addresses you previously
449shared will stop receiving new payments; you will need to share new addresses
450after reconnecting.
451
452Expected: {expected}
453Got:      {got}")
454}
455
456/// The detailled balance of a Lightning receive.
457#[derive(Debug, Clone)]
458pub struct LightningReceiveBalance {
459	/// Sum of all pending lightning invoices
460	pub total: Amount,
461	/// Sum of all invoices for which we received the HTLC VTXOs
462	pub claimable: Amount,
463}
464
465/// The different balances of a Bark wallet.
466#[derive(Debug, Clone)]
467pub struct Balance {
468	/// Coins that are spendable in the Ark, either in-round or out-of-round.
469	pub spendable: Amount,
470	/// Coins that are in the process of being sent over Lightning.
471	pub pending_lightning_send: Amount,
472	/// Coins that are in the process of being received over Lightning.
473	pub claimable_lightning_receive: Amount,
474	/// Coins locked in a round.
475	pub pending_in_round: Amount,
476	/// Coins that are in the process of unilaterally exiting the Ark.
477	/// None if exit subsystem was unavailable
478	pub pending_exit: Option<Amount>,
479	/// Coins that are pending sufficient confirmations from board transactions.
480	pub pending_board: Amount,
481}
482
483pub struct UtxoInfo {
484	pub outpoint: OutPoint,
485	pub amount: Amount,
486	pub confirmation_height: Option<u32>,
487}
488
489impl From<Utxo> for UtxoInfo {
490	fn from(value: Utxo) -> Self {
491		match value {
492			Utxo::Local(o) => UtxoInfo {
493				outpoint: o.outpoint,
494				amount: o.amount,
495				confirmation_height: o.confirmation_height,
496			},
497			Utxo::Exit(e) => UtxoInfo {
498				outpoint: e.vtxo.point(),
499				amount: e.vtxo.amount(),
500				confirmation_height: Some(e.height),
501			},
502		}
503	}
504}
505
506/// Represents an offchain balance structure consisting of available funds, pending amounts in
507/// unconfirmed rounds, and pending exits.
508pub struct OffchainBalance {
509	/// Funds currently available for use. This reflects the spendable balance.
510	pub available: Amount,
511	/// Funds that are pending in unconfirmed operational rounds.
512	pub pending_in_round: Amount,
513	/// Funds being unilaterally exited. These may require more onchain confirmations to become
514	/// available onchain.
515	pub pending_exit: Amount,
516}
517
518/// Read-only properties of the Bark wallet.
519#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
520pub struct WalletProperties {
521	/// The Bitcoin network to run Bark on.
522	///
523	/// Default value: signet.
524	pub network: Network,
525
526	/// The wallet fingerpint
527	///
528	/// Used on wallet loading to check mnemonic correctness
529	pub fingerprint: Fingerprint,
530
531	/// The server public key from the initial connection.
532	///
533	/// This is used to detect if the Ark server has been replaced,
534	/// which could indicate a malicious server. If the server pubkey
535	/// changes, the wallet will refuse to connect and warn the user
536	/// to perform an emergency exit.
537	pub server_pubkey: Option<PublicKey>,
538
539	/// The server's mailbox public key.
540	///
541	/// Stored so that Ark addresses can be generated without a live
542	/// connection to the Ark server. `None` indicates a wallet created
543	/// before this field was added; the value is populated on the first
544	/// successful handshake. If the key changes, the wallet refuses to
545	/// connect until the user resolves the rotation.
546	pub server_mailbox_pubkey: Option<PublicKey>,
547}
548
549/// Struct representing an extended private key derived from a
550/// wallet's seed, used to derive child VTXO keypairs
551///
552/// The VTXO seed is derived by applying a hardened derivation
553/// step at index 350 from the wallet's seed.
554pub struct WalletSeed {
555	master: bip32::Xpriv,
556	vtxo: bip32::Xpriv,
557}
558
559impl WalletSeed {
560	fn new(network: Network, seed: &[u8; 64]) -> Self {
561		let bark_path = [ChildNumber::from_hardened_idx(BARK_PURPOSE_INDEX).unwrap()];
562		let master = bip32::Xpriv::new_master(network, seed)
563			.expect("invalid seed")
564			.derive_priv(&SECP, &bark_path)
565			.expect("purpose is valid");
566
567		let vtxo_path = [ChildNumber::from_hardened_idx(VTXO_KEYS_INDEX).unwrap()];
568		let vtxo = master.derive_priv(&SECP, &vtxo_path)
569			.expect("vtxo path is valid");
570
571		Self { master, vtxo }
572	}
573
574	fn fingerprint(&self) -> Fingerprint {
575		self.master.fingerprint(&SECP)
576	}
577
578	fn derive_vtxo_keypair(&self, idx: u32) -> Keypair {
579		self.vtxo.derive_priv(&SECP, &[idx.into()]).unwrap().to_keypair(&SECP)
580	}
581
582	fn to_mailbox_keypair(&self) -> Keypair {
583		let mailbox_path = [ChildNumber::from_hardened_idx(MAILBOX_KEY_INDEX).unwrap()];
584		self.master.derive_priv(&SECP, &mailbox_path).unwrap().to_keypair(&SECP)
585	}
586
587	fn to_recovery_mailbox_keypair(&self) -> Keypair {
588		let mailbox_path = [ChildNumber::from_hardened_idx(RECOVERY_MAILBOX_KEY_INDEX).unwrap()];
589		self.master.derive_priv(&SECP, &mailbox_path).unwrap().to_keypair(&SECP)
590	}
591}
592
593struct WalletInner {
594	/// The chain source the wallet is connected to
595	chain: Arc<ChainSource>,
596
597	/// Exit subsystem handling unilateral exits and on-chain reconciliation outside Ark rounds.
598	exit: Exit,
599
600	/// Allows easy creation of and management of wallet fund movements.
601	movements: Arc<MovementManager>,
602
603	/// Dispatch for wallet notifications
604	notifications: NotificationDispatch,
605
606	/// Active runtime configuration for networking, fees, policies and thresholds.
607	config: Config,
608
609	/// Persistence backend for wallet state (keys metadata, VTXOs, movements, round state, etc.).
610	db: Arc<dyn BarkPersister>,
611
612	/// Coordinates access to the wallet's protected resources. The caller
613	/// picks a backend whose enforcement scope matches how the wallet is
614	/// deployed; see [`crate::lock_manager`].
615	lock_manager: Box<dyn LockManager>,
616
617	/// Deterministic seed material used to generate wallet keypairs.
618	seed: WalletSeed,
619
620	/// Live connection to an Ark server for round participation and synchronization.
621	///
622	/// Lazily initialised on first use via [`Wallet::require_server`]. A
623	/// [`OnceCell`] is the right primitive here: concurrent callers on a
624	/// cold cell all await the same in-flight `connect_to_server` future
625	/// instead of each opening a fresh gRPC channel.
626	server: tokio::sync::OnceCell<ServerConnection>,
627
628	/// A handle to the currently running daemon, if any.
629	daemon: parking_lot::Mutex<Option<DaemonHandle>>,
630
631	/// In-memory MuSig2 secret cosign nonces for in-flight round attempts.
632	/// See [`RoundSecretNonces`].
633	pub(crate) round_secret_nonces: RoundSecretNonces,
634}
635
636/// The central entry point for using this library as an Ark wallet.
637///
638/// Not that a [Wallet] instance can freely be [Clone]'ed to refer to the same
639/// wallet.
640///
641/// Overview
642/// - Wallet encapsulates the complete Ark client implementation:
643///   - address generation (Ark addresses/keys)
644///     - [Wallet::new_address],
645///     - [Wallet::new_address_with_index],
646///     - [Wallet::peek_address],
647///     - [Wallet::validate_arkoor_address]
648///   - boarding onchain funds into Ark from an onchain wallet (see [onchain::OnchainWallet])
649///     - [Wallet::board_amount],
650///     - [Wallet::board_all]
651///   - offboarding Ark funds to move them back onchain
652///     - [Wallet::offboard_vtxos],
653///     - [Wallet::offboard_all]
654///   - sending and receiving Ark payments (including to BOLT11/BOLT12 invoices)
655///     - [Wallet::send_arkoor_payment],
656///     - [Wallet::pay_lightning_invoice],
657///     - [Wallet::pay_lightning_address],
658///     - [Wallet::pay_lightning_offer]
659///   - tracking, selecting, and refreshing VTXOs
660///     - [Wallet::vtxos],
661///     - [Wallet::vtxos_with],
662///     - [Wallet::refresh_vtxos]
663///   - syncing with the Ark server, unilateral exits and performing general maintenance
664///     - [Wallet::maintenance]: Syncs everything offchain-related and refreshes VTXOs where
665///       necessary,
666///     - [Wallet::maintenance_with_onchain]: The same as [Wallet::maintenance] but also syncs the
667///       onchain wallet and unilateral exits,
668///     - [Wallet::maintenance_refresh]: Refreshes VTXOs where necessary without syncing anything,
669///     - [Wallet::sync]: Syncs network fee-rates, ark rounds and arkoor payments,
670///     - [Wallet::sync_exits]: Updates the status of unilateral exits,
671///     - [Wallet::sync_pending_lightning_send_vtxos]: Updates the status of pending lightning payments,
672///     - [Wallet::try_claim_all_lightning_receives]: Wait for payment receipt of all open invoices, then claim them,
673///     - [Wallet::sync_pending_boards]: Registers boards which are available for use
674///       in offchain payments
675///
676/// Key capabilities
677/// - Address management:
678///   - derive and peek deterministic Ark addresses and their indices
679/// - Funds lifecycle:
680///   - board funds from an external onchain wallet onto the Ark
681///   - send out-of-round Ark payments (arkoor)
682///   - offboard funds to onchain addresses
683///   - manage HTLCs and Lightning receives/sends
684/// - VTXO management:
685///   - query spendable and pending VTXOs
686///   - refresh expiring or risky VTXOs
687///   - compute balance broken down by spendable/pending states
688/// - Synchronization and maintenance:
689///   - sync against the Ark server and the onchain source
690///   - reconcile pending rounds, exits, and offchain state
691///   - periodic maintenance helpers (e.g., auto-register boards, refresh policies)
692///
693/// Construction and persistence
694/// - A [Wallet] is opened or created using a mnemonic and a backend implementing [BarkPersister].
695///   - [Wallet::create],
696///   - [Wallet::open]
697/// - Creation allows the use of an optional onchain wallet for boarding and [Exit] functionality.
698///   It also initializes any internal state and connects to the [chain::ChainSource]. See
699///   [onchain::OnchainWallet] for an implementation of an onchain wallet using BDK.
700///   - [Wallet::create_with_exits],
701///   - [Wallet::open_with_daemon]
702///
703/// Example
704/// ```
705/// # #[cfg(any(test, doc))]
706/// # async fn demo() -> anyhow::Result<()> {
707/// # use std::sync::Arc;
708/// # use bark::{Config, Wallet};
709/// # use bark::lock_manager::memory::MemoryLockManager;
710/// # use bark::onchain::OnchainWallet;
711/// # use bark::persist::{BarkPersister, SqliteClient};
712/// # use bark::persist::sqlite::helpers::in_memory_db;
713/// # use bip39::Mnemonic;
714/// # use bitcoin::Network;
715/// # let (db_path, _) = in_memory_db();
716/// let network = Network::Signet;
717/// let mnemonic = Mnemonic::generate(12)?;
718/// let cfg = Config {
719///   server_address: String::from("https://ark.signet.2nd.dev"),
720///   esplora_address: Some(String::from("https://esplora.signet.2nd.dev")),
721///   ..Default::default()
722/// };
723///
724/// // You can either use the included SQLite implementation or create your own.
725/// let persister = SqliteClient::open(db_path).await?;
726/// let db: Arc<dyn BarkPersister> = Arc::new(persister);
727///
728/// // Load or create an onchain wallet if needed
729/// let onchain_wallet = OnchainWallet::load_or_create(network, mnemonic.to_seed(""), db.clone()).await?;
730///
731/// // Create or open the Ark wallet
732/// let lock_manager = Box::new(MemoryLockManager::new());
733/// let mut wallet = Wallet::create_with_exits(
734/// 	&mnemonic,
735/// 	network,
736/// 	cfg.clone(),
737/// 	db,
738/// 	lock_manager,
739/// 	false,
740/// ).await?;
741/// // let mut wallet = Wallet::create(&mnemonic, network, cfg.clone(), db.clone(), Box::new(MemoryLockManager::new()), false).await?;
742/// // let mut wallet = Wallet::open(&mnemonic, db.clone(), cfg.clone(), Box::new(MemoryLockManager::new())).await?;
743/// // let mut wallet = Wallet::open_with_exits(
744/// //    &mnemonic, db.clone(), cfg.clone(), Box::new(MemoryLockManager::new())
745/// // ).await?;
746
747
748///
749/// // There are two main ways to update the wallet, the primary is to use one of the maintenance
750/// // commands which will sync everything, refresh VTXOs and reconcile pending lightning payments.
751/// wallet.maintenance().await?;
752/// wallet.maintenance_with_onchain(&mut onchain_wallet).await?;
753///
754/// // Alternatively, you can use the fine-grained sync commands to sync individual parts of the
755/// // wallet state and use `maintenance_refresh` where necessary to refresh VTXOs.
756/// wallet.sync().await?;
757/// wallet.sync_pending_lightning_send_vtxos().await?;
758/// wallet.register_all_confirmed_boards(&mut onchain_wallet).await?;
759/// wallet.sync_exits().await?;
760/// wallet.maintenance_refresh().await?;
761///
762/// // Generate a new Ark address to receive funds via arkoor
763/// let addr = wallet.new_address().await?;
764///
765/// // Query balance and VTXOs
766/// let balance = wallet.balance()?;
767/// let vtxos = wallet.vtxos()?;
768///
769/// // Progress any unilateral exits, make sure to sync first
770/// wallet.exit_mgr().sync_no_progress().await?;
771/// wallet.exit_mgr().progress_exits_with_bdk(&wallet, &mut onchain_wallet, None).await?;
772///
773/// # Ok(())
774/// # }
775/// ```
776#[derive(Clone)]
777pub struct Wallet {
778	inner: Arc<WalletInner>,
779}
780
781impl Wallet {
782	/// Verifies that the bark [Wallet] can be used with the configured [chain::ChainSource].
783	/// More specifically, if the [chain::ChainSource] connects to Bitcoin Core it must be
784	/// a high enough version to support ephemeral anchors.
785	pub async fn require_chainsource_version(&self) -> anyhow::Result<()> {
786		self.inner.chain.require_version().await
787	}
788
789	pub async fn network(&self) -> anyhow::Result<Network> {
790		Ok(self.properties().await?.network)
791	}
792
793	/// Access the server's chain source
794	pub fn chain(&self) -> &Arc<ChainSource> {
795		&self.inner.chain
796	}
797
798	/// Access the exit manager
799	pub fn exit_mgr(&self) -> &Exit {
800		&self.inner.exit
801	}
802
803	/// Access the movements manager
804	pub fn movements_mgr(&self) -> &MovementManager {
805		&self.inner.movements
806	}
807
808	/// Peek at the keypair directly after currently last revealed one,
809	/// together with its index, without storing it.
810	pub async fn peek_next_keypair(&self) -> anyhow::Result<(Keypair, u32)> {
811		let last_revealed = self.inner.db.get_last_vtxo_key_index().await?;
812
813		let index = last_revealed.map(|i| i + 1).unwrap_or(u32::MIN);
814		let keypair = self.inner.seed.derive_vtxo_keypair(index);
815
816		Ok((keypair, index))
817	}
818
819	/// Derive and store the keypair directly after currently last revealed one,
820	/// together with its index.
821	pub async fn derive_store_next_keypair(&self) -> anyhow::Result<(Keypair, u32)> {
822		let (keypair, index) = self.peek_next_keypair().await?;
823		self.inner.db.store_vtxo_key(index, keypair.public_key()).await?;
824		Ok((keypair, index))
825	}
826
827	#[deprecated(note = "use peek_keypair instead")]
828	pub async fn peak_keypair(&self, index: u32) -> anyhow::Result<Keypair> {
829		self.peek_keypair(index).await
830	}
831
832	/// Retrieves a keypair based on the provided index and checks if the corresponding public key
833	/// exists in the [Vtxo] database.
834	///
835	/// # Arguments
836	///
837	/// * `index` - The index used to derive a keypair.
838	///
839	/// # Returns
840	///
841	/// * `Ok(Keypair)` - If the keypair is successfully derived and its public key exists in the
842	///   database.
843	/// * `Err(anyhow::Error)` - If the public key does not exist in the database or if an error
844	///   occurs during the database query.
845	pub async fn peek_keypair(&self, index: u32) -> anyhow::Result<Keypair> {
846		let keypair = self.inner.seed.derive_vtxo_keypair(index);
847		if self.inner.db.get_public_key_idx(&keypair.public_key()).await?.is_some() {
848			Ok(keypair)
849		} else {
850			bail!("VTXO key {} does not exist, please derive it first", index)
851		}
852	}
853
854
855	/// Retrieves the [Keypair] for a provided [PublicKey]
856	///
857	/// # Arguments
858	///
859	/// * `public_key` - The public key for which the keypair must be found
860	///
861	/// # Returns
862	/// * `Ok(Some(u32, Keypair))` - If the pubkey is found, the derivation-index and keypair are
863	///                              returned
864	/// * `Ok(None)` - If the pubkey cannot be found in the database
865	/// * `Err(anyhow::Error)` - If an error occurred related to the database query
866	pub async fn pubkey_keypair(&self, public_key: &PublicKey) -> anyhow::Result<Option<(u32, Keypair)>> {
867		if let Some(index) = self.inner.db.get_public_key_idx(&public_key).await? {
868			Ok(Some((index, self.inner.seed.derive_vtxo_keypair(index))))
869		} else {
870			Ok(None)
871		}
872	}
873
874	/// Retrieves the [Keypair] for a provided [Vtxo]
875	///
876	/// # Arguments
877	///
878	/// * `vtxo` - The vtxo for which the key must be found
879	///
880	/// # Returns
881	/// * `Ok(Some(Keypair))` - If the pubkey is found, the keypair is returned
882	/// * `Err(anyhow::Error)` - If the corresponding public key doesn't exist
883	///   in the database or a database error occurred.
884	pub async fn get_vtxo_key(&self, vtxo: impl VtxoRef) -> anyhow::Result<Keypair> {
885		let bare_vtxo = match vtxo.as_bare_vtxo() {
886			Some(bare) => bare,
887			None => Cow::Owned(self.get_vtxo_by_id(vtxo.vtxo_id()).await?.vtxo),
888		};
889		let pubkey = self.find_signable_clause(&bare_vtxo).await
890			.context("VTXO is not signable by wallet")?
891			.pubkey();
892		let idx = self.inner.db.get_public_key_idx(&pubkey).await?
893			.context("VTXO key not found")?;
894		Ok(self.inner.seed.derive_vtxo_keypair(idx))
895	}
896
897	#[deprecated(note = "use peek_address instead")]
898	pub async fn peak_address(&self, index: u32) -> anyhow::Result<ark::Address> {
899		self.peek_address(index).await
900	}
901
902	/// Peek for an [ark::Address] at the given key index.
903	///
904	/// May return an error if the address at the given index has not been derived yet.
905	pub async fn peek_address(&self, index: u32) -> anyhow::Result<ark::Address> {
906		let properties = self.properties().await?;
907		let network = properties.network;
908		let keypair = self.peek_keypair(index).await?;
909		let mailbox = self.mailbox_identifier();
910
911
912		let (server_pubkey, mailbox_pubkey) =
913			if let (Some(spk), Some(mpk)) = (properties.server_pubkey, properties.server_mailbox_pubkey) {
914				(spk, mpk)
915			} else {
916				let (_, ark_info) = self.require_server().await?;
917				(ark_info.server_pubkey, ark_info.mailbox_pubkey)
918			};
919
920		Ok(ark::Address::builder()
921			.testnet(network != bitcoin::Network::Bitcoin)
922			.server_pubkey(server_pubkey)
923			.pubkey_policy(keypair.public_key())
924			.mailbox(mailbox_pubkey, mailbox, &keypair)
925			.expect("Failed to assign mailbox")
926			.into_address().unwrap())
927	}
928
929	/// Generate a new [ark::Address] and returns the index of the key used to create it.
930	///
931	/// This derives and stores the keypair directly after currently last revealed one.
932	pub async fn new_address_with_index(&self) -> anyhow::Result<(ark::Address, u32)> {
933		let (_, index) = self.derive_store_next_keypair().await?;
934		let addr = self.peek_address(index).await?;
935		Ok((addr, index))
936	}
937
938	/// Generate a new mailbox [ark::Address].
939	pub async fn new_address(&self) -> anyhow::Result<ark::Address> {
940		let (addr, _) = self.new_address_with_index().await?;
941		Ok(addr)
942	}
943
944	/// Create a new wallet without an optional onchain backend. This will restrict features such as
945	/// boarding and unilateral exit.
946	///
947	/// `lock_manager` coordinates access to the wallet's protected resources. Pick a backend
948	/// whose enforcement scope matches how the wallet is deployed — see [`crate::lock_manager`].
949	///
950	/// The `force` flag will allow you to create the wallet even if a connection to the Ark server
951	/// cannot be established, it will not overwrite a wallet which has already been created.
952	pub async fn create(
953		mnemonic: &Mnemonic,
954		network: Network,
955		config: Config,
956		db: Arc<dyn BarkPersister>,
957		lock_manager: Box<dyn LockManager>,
958		force: bool,
959	) -> anyhow::Result<Wallet> {
960		trace!("Config: {:?}", config);
961
962		let wallet_fingerprint = WalletSeed::new(network, &mnemonic.to_seed("")).fingerprint();
963
964		// Block concurrent creators against the same locking universe. A
965		// short timeout is fine: if a sibling process wins the race they
966		// will have committed the wallet by the time we'd time out, and
967		// the `read_properties` check below catches that case cleanly.
968		let create_guard = lock_manager.lock(
969			&format!("{}.create", wallet_fingerprint),
970			Duration::from_secs(5),
971		).await.context("wallet initialization already in progress")?;
972
973		if let Some(existing) = db.read_properties().await? {
974			trace!("Existing config: {:?}", existing);
975			bail!("cannot overwrite already existing config")
976		}
977
978		// Try to connect to the server and get its pubkey
979		let (server_pubkey, mailbox_pubkey) = if !force {
980			match Self::connect_to_server(&config, network).await {
981				Ok(conn) => {
982					let ark_info = conn.ark_info().await;
983					(Some(ark_info.server_pubkey), Some(ark_info.mailbox_pubkey))
984				}
985				Err(err) => {
986					bail!("Failed to connect to provided server (if you are sure use the --force flag): {:#}", err);
987				}
988			}
989		} else {
990			(None, None)
991		};
992
993		let properties = WalletProperties {
994			network,
995			fingerprint: wallet_fingerprint,
996			server_pubkey,
997			server_mailbox_pubkey: mailbox_pubkey,
998		};
999
1000		// write the config to db
1001		db.init_wallet(&properties).await.context("cannot init wallet in the database")?;
1002		info!("Created wallet with fingerprint: {}", wallet_fingerprint);
1003		if let Some(pk) = server_pubkey {
1004			info!("Stored server pubkey: {}", pk);
1005		}
1006
1007		// The wallet exists from this point on — drop the creation lock
1008		// so another process is free to open it.
1009		drop(create_guard);
1010
1011		// from then on we can open the wallet
1012		let wallet = Wallet::open(&mnemonic, db, config, lock_manager).await.context("failed to open wallet")?;
1013		wallet.require_chainsource_version().await?;
1014
1015		Ok(wallet)
1016	}
1017
1018	/// Create a new wallet and eagerly load any persisted exit state from the database.
1019	///
1020	/// The `force` flag will allow you to create the wallet even if a connection to the Ark server
1021	/// cannot be established, it will not overwrite a wallet which has already been created.
1022	///
1023	/// TODO: consider making [Wallet::create] always load exits so this wrapper is unnecessary.
1024	pub async fn create_with_exits(
1025		mnemonic: &Mnemonic,
1026		network: Network,
1027		config: Config,
1028		db: Arc<dyn BarkPersister>,
1029		lock_manager: Box<dyn LockManager>,
1030		force: bool,
1031	) -> anyhow::Result<Wallet> {
1032		let wallet = Wallet::create(mnemonic, network, config, db, lock_manager, force).await?;
1033		wallet.inner.exit.load().await?;
1034		Ok(wallet)
1035	}
1036
1037	/// Loads the bark wallet from the given database ensuring the fingerprint remains consistent.
1038	///
1039	/// `lock_manager` coordinates access to the wallet's protected resources. Pick a backend
1040	/// whose enforcement scope matches how the wallet is deployed — see [`crate::lock_manager`].
1041	pub async fn open(
1042		mnemonic: &Mnemonic,
1043		db: Arc<dyn BarkPersister>,
1044		config: Config,
1045		lock_manager: Box<dyn LockManager>,
1046	) -> anyhow::Result<Wallet> {
1047		let properties = db.read_properties().await?.context("Wallet is not initialised")?;
1048
1049		let seed = {
1050			let seed = mnemonic.to_seed("");
1051			WalletSeed::new(properties.network, &seed)
1052		};
1053
1054		if properties.fingerprint != seed.fingerprint() {
1055			bail!("incorrect mnemonic")
1056		}
1057
1058		let chain_source = if let Some(ref url) = config.esplora_address {
1059			ChainSourceSpec::Esplora {
1060				url: url.clone(),
1061			}
1062		} else if let Some(ref url) = config.bitcoind_address {
1063			let auth = if let Some(ref c) = config.bitcoind_cookiefile {
1064				bitcoin_ext::rpc::Auth::CookieFile(c.clone())
1065			} else {
1066				bitcoin_ext::rpc::Auth::UserPass(
1067					config.bitcoind_user.clone().context("need bitcoind auth config")?,
1068					config.bitcoind_pass.clone().context("need bitcoind auth config")?,
1069				)
1070			};
1071			ChainSourceSpec::Bitcoind { url: url.clone(), auth }
1072		} else {
1073			bail!("Need to either provide esplora or bitcoind info");
1074		};
1075
1076		#[cfg(feature = "socks5-proxy")]
1077		let chain_proxy = proxy_for_url(&config.socks5_proxy, chain_source.url())?;
1078		let chain_source_client = ChainSource::new(
1079			chain_source, properties.network, config.fallback_fee_rate,
1080			#[cfg(feature = "socks5-proxy")] chain_proxy.as_deref(),
1081		).await?;
1082		let chain = Arc::new(chain_source_client);
1083
1084		let server = tokio::sync::OnceCell::new();
1085
1086		let notifications = NotificationDispatch::new();
1087		let movements = Arc::new(MovementManager::new(db.clone(), notifications.clone()));
1088		let exit = Exit::new(db.clone(), chain.clone(), movements.clone()).await?;
1089
1090		Ok(Wallet { inner: Arc::new(WalletInner {
1091			config, db, lock_manager, seed, exit, movements, notifications, server, chain,
1092			daemon: parking_lot::Mutex::new(None),
1093			round_secret_nonces: RoundSecretNonces::new(),
1094		})})
1095	}
1096
1097	/// Open a wallet and eagerly load any persisted exit state from the database.
1098	///
1099	/// TODO: consider making [Wallet::open] always load exits so this wrapper is unnecessary.
1100	pub async fn open_with_exits(
1101		mnemonic: &Mnemonic,
1102		db: Arc<dyn BarkPersister>,
1103		cfg: Config,
1104		lock_manager: Box<dyn LockManager>,
1105	) -> anyhow::Result<Wallet> {
1106		let wallet = Wallet::open(mnemonic, db, cfg, lock_manager).await?;
1107		wallet.inner.exit.load().await?;
1108		Ok(wallet)
1109	}
1110
1111	/// Similar to [Wallet::open] however this also starts the daemon, optionally with an onchain
1112	/// wallet, and returns a handle to the daemon.
1113	pub async fn open_with_daemon(
1114		mnemonic: &Mnemonic,
1115		db: Arc<dyn BarkPersister>,
1116		cfg: Config,
1117		onchain: Option<Arc<tokio::sync::RwLock<dyn DaemonizableOnchainWallet>>>,
1118		lock_manager: Box<dyn LockManager>,
1119	) -> anyhow::Result<Wallet> {
1120		let wallet = Wallet::open(mnemonic, db, cfg, lock_manager).await?;
1121		if onchain.is_some() {
1122			wallet.inner.exit.load().await?;
1123		}
1124
1125		wallet.start_daemon(onchain)?;
1126
1127		Ok(wallet)
1128	}
1129
1130	/// Returns the config used to create/load the bark [Wallet].
1131	pub fn config(&self) -> &Config {
1132		&self.inner.config
1133	}
1134
1135	/// Retrieves the [WalletProperties] of the current bark [Wallet].
1136	pub async fn properties(&self) -> anyhow::Result<WalletProperties> {
1137		let properties = self.inner.db.read_properties().await?.context("Wallet is not initialised")?;
1138		Ok(properties)
1139	}
1140
1141	/// Returns the fingerprint of the wallet.
1142	pub fn fingerprint(&self) -> Fingerprint {
1143		self.inner.seed.fingerprint()
1144	}
1145
1146	async fn connect_to_server(
1147		config: &Config,
1148		network: Network,
1149	) -> anyhow::Result<ServerConnection> {
1150		let server_address = crate::utils::url_with_default_https_scheme(&config.server_address);
1151		let mut builder = ServerConnection::builder()
1152			.address(&server_address)
1153			.network(network);
1154
1155		#[cfg(feature = "socks5-proxy")]
1156		if let Some(proxy) = proxy_for_url(&config.socks5_proxy, &server_address)? {
1157			builder = builder.proxy(&proxy)
1158		}
1159
1160		if let Some(ref token) = config.server_access_token {
1161			builder = builder.access_token(token);
1162		}
1163
1164		builder.connect().await.map_err(wrap_server_connect_error)
1165			.context("Failed to connect to Ark server")
1166	}
1167
1168	async fn require_server(&self) -> anyhow::Result<(ServerConnection, ArkInfo)> {
1169		// Connect lazily if not yet connected. `get_or_try_init` ensures
1170		// concurrent callers on a cold cell all await the same in-flight
1171		// connect future instead of each opening a fresh gRPC channel.
1172		let conn = self.inner.server.get_or_try_init(|| async {
1173			let network = self.properties().await?.network;
1174			Self::connect_to_server(&self.inner.config, network).await
1175				.context("You should be connected to Ark server to perform this action")
1176		}).await?.clone();
1177
1178		let ark_info = conn.ark_info().await;
1179		self.check_and_store_server_keys(&ark_info).await?;
1180
1181		Ok((conn, ark_info))
1182	}
1183
1184	pub async fn refresh_server(&self) -> anyhow::Result<()> {
1185		// If the cell is still cold, initialise it with a fresh connection.
1186		// If it is already initialised, run a heartbeat against the existing
1187		// one — `OnceCell` does not support replacing a stored value, but
1188		// `ServerConnection` is built around a tonic `Channel` which
1189		// transparently reconnects, so we don't need to swap it.
1190		let srv = self.inner.server.get_or_try_init(|| async {
1191			let properties = self.properties().await?;
1192			Self::connect_to_server(&self.inner.config, properties.network).await
1193				.map_err(anyhow::Error::from)
1194		}).await?;
1195
1196		srv.check_connection().await?;
1197		let ark_info = srv.ark_info().await;
1198		ark_info.fees.validate().context("invalid fee schedule")?;
1199		self.check_and_store_server_keys(&ark_info).await?;
1200
1201		Ok(())
1202	}
1203
1204	/// Validate that the server's public keys match what we have stored,
1205	/// and persist them if this is the first time connecting after an upgrade.
1206	///
1207	/// Returns an error (via `bail!`) if either the server pubkey or mailbox
1208	/// pubkey differs from the stored value; callers must not proceed with
1209	/// server operations on error.
1210	async fn check_and_store_server_keys(&self, ark_info: &ArkInfo) -> anyhow::Result<()> {
1211		let properties = self.properties().await?;
1212
1213		if let Some(stored_pubkey) = properties.server_pubkey {
1214			if stored_pubkey != ark_info.server_pubkey {
1215				log_server_pubkey_changed_error(stored_pubkey, ark_info.server_pubkey);
1216				bail!("Server public key has changed. You should exit all your VTXOs!");
1217			}
1218		} else {
1219			self.inner.db.set_server_pubkey(ark_info.server_pubkey).await?;
1220			info!("Stored server pubkey for existing wallet: {}", ark_info.server_pubkey);
1221		}
1222
1223		if let Some(stored_mailbox_pubkey) = properties.server_mailbox_pubkey {
1224			if stored_mailbox_pubkey != ark_info.mailbox_pubkey {
1225				log_server_mailbox_pubkey_changed_error(stored_mailbox_pubkey, ark_info.mailbox_pubkey);
1226				bail!("Server mailbox public key has changed.");
1227			}
1228		} else {
1229			self.inner.db.set_server_mailbox_pubkey(ark_info.mailbox_pubkey).await?;
1230			info!("Stored server mailbox pubkey for existing wallet: {}", ark_info.mailbox_pubkey);
1231		}
1232
1233		Ok(())
1234	}
1235
1236	/// Return [ArkInfo] fetched on last handshake with the Ark server
1237	pub async fn ark_info(&self) -> anyhow::Result<Option<ArkInfo>> {
1238		match self.inner.server.get() {
1239			Some(srv) => Ok(Some(srv.ark_info().await)),
1240			None => Ok(None),
1241		}
1242	}
1243
1244	/// Return [ArkInfo], connecting lazily if not yet connected.
1245	///
1246	/// Errors if the server cannot be reached or if the server's pubkey
1247	/// or mailbox pubkey no longer matches what was stored at wallet
1248	/// creation.
1249	pub async fn require_ark_info(&self) -> anyhow::Result<ArkInfo> {
1250		let (_, ark_info) = self.require_server().await?;
1251		Ok(ark_info)
1252	}
1253
1254	/// Return the [Balance] of the wallet.
1255	///
1256	/// When not running the daemon, make sure you sync before calling this method.
1257	pub async fn balance(&self) -> anyhow::Result<Balance> {
1258		let vtxos = self.vtxos().await?;
1259
1260		let spendable = {
1261			let mut v = vtxos.iter().collect();
1262			VtxoStateKind::Spendable.filter_vtxos(&mut v).await?;
1263			v.into_iter().map(|v| v.amount()).sum::<Amount>()
1264		};
1265
1266		let pending_lightning_send = self.pending_lightning_send_vtxos().await?.iter()
1267			.map(|v| v.amount())
1268			.sum::<Amount>();
1269
1270		let claimable_lightning_receive = self.claimable_lightning_receive_balance().await?;
1271
1272		let pending_board = self.pending_board_vtxos().await?.iter()
1273			.map(|v| v.amount())
1274			.sum::<Amount>();
1275
1276		let pending_in_round = self.pending_round_balance().await?;
1277
1278		let pending_exit = self.exit_mgr().try_pending_total();
1279
1280		Ok(Balance {
1281			spendable,
1282			pending_in_round,
1283			pending_lightning_send,
1284			claimable_lightning_receive,
1285			pending_exit,
1286			pending_board,
1287		})
1288	}
1289
1290	/// Fetches [Vtxo]'s funding transaction and validates the VTXO against it.
1291	pub async fn validate_vtxo(&self, vtxo: &Vtxo<Full>) -> anyhow::Result<()> {
1292		let tx = self.inner.chain.get_tx(&vtxo.chain_anchor().txid).await
1293			.context("could not fetch chain tx")?;
1294
1295		let tx = tx.with_context(|| {
1296			format!("vtxo chain anchor not found for vtxo: {}", vtxo.chain_anchor().txid)
1297		})?;
1298
1299		vtxo.validate(&tx)?;
1300
1301		Ok(())
1302	}
1303
1304	/// Manually import a VTXO into the wallet.
1305	///
1306	/// # Arguments
1307	/// * `vtxo` - The VTXO to import
1308	///
1309	/// # Errors
1310	/// Returns an error if:
1311	/// - The VTXO's chain anchor is not found or invalid
1312	/// - The wallet doesn't own a signable clause for the VTXO
1313	pub async fn import_vtxo(&self, vtxo: &Vtxo<Full>) -> anyhow::Result<()> {
1314		if self.inner.db.get_wallet_vtxo(vtxo.id()).await?.is_some() {
1315			info!("VTXO {} already exists in wallet, skipping import", vtxo.id());
1316			return Ok(());
1317		}
1318
1319		self.validate_vtxo(vtxo).await.context("VTXO validation failed")?;
1320
1321		if self.find_signable_clause(vtxo).await.is_none() {
1322			bail!("VTXO {} is not owned by this wallet (no signable clause found)", vtxo.id());
1323		}
1324
1325		let current_height = self.inner.chain.tip().await?;
1326		if vtxo.expiry_height() <= current_height {
1327			bail!("Vtxo {} has expired", vtxo.id());
1328		}
1329
1330		self.store_spendable_vtxos([vtxo]).await.context("failed to store imported VTXO")?;
1331
1332		info!("Successfully imported VTXO {}", vtxo.id());
1333		Ok(())
1334	}
1335
1336	/// Retrieves the full state of a [Vtxo] for a given [VtxoId] if it exists in the database.
1337	pub async fn get_vtxo_by_id(&self, vtxo_id: VtxoId) -> anyhow::Result<WalletVtxo> {
1338		let vtxo = self.inner.db.get_wallet_vtxo(vtxo_id).await
1339			.with_context(|| format!("Error when querying vtxo {} in database", vtxo_id))?
1340			.with_context(|| format!("The VTXO with id {} cannot be found", vtxo_id))?;
1341		Ok(vtxo)
1342	}
1343
1344	/// Hydrate a VTXO into its full form, including the unilateral exit chain.
1345	///
1346	/// [Wallet::get_vtxo_by_id] returns the bare form ([WalletVtxo] holds
1347	/// [Vtxo<ark::vtxo::Bare>]). This method reads the genesis chain from the
1348	/// database and reassembles the full VTXO. Use it from external SDK
1349	/// callers that need the chain (e.g. to feed into [ArkoorPackageBuilder]
1350	/// or [Wallet::register_vtxo_transactions_with_server]).
1351	pub async fn get_full_vtxo(&self, vtxo_id: VtxoId) -> anyhow::Result<Vtxo<Full>> {
1352		self.inner.db.get_full_vtxo(vtxo_id).await
1353			.with_context(|| format!("Error when querying full vtxo {} in database", vtxo_id))?
1354			.with_context(|| format!("The VTXO with id {} cannot be found", vtxo_id))
1355	}
1356
1357	/// Similar to [Wallet::get_full_vtxo] but it retrieves the full variant of each given VTXO.
1358	pub async fn get_full_vtxos<V: VtxoRef>(
1359		&self,
1360		vtxos: impl IntoIterator<Item = V>,
1361	) -> anyhow::Result<Vec<Vtxo<Full>>> {
1362		let ids = vtxos.into_iter().map(|v| v.vtxo_id()).collect::<Vec<_>>();
1363		self.inner.db.get_full_vtxos(&ids).await
1364			.with_context(||
1365				format!("Error when querying full vtxos in database with IDs: {:?}", ids)
1366			)
1367	}
1368
1369	/// Fetches all movements ordered from newest to oldest.
1370	#[deprecated(since="0.1.0-beta.5", note = "Use Wallet::history instead")]
1371	pub async fn movements(&self) -> anyhow::Result<Vec<Movement>> {
1372		self.history().await
1373	}
1374
1375	/// Fetches all wallet fund movements ordered from newest to oldest.
1376	pub async fn history(&self) -> anyhow::Result<Vec<Movement>> {
1377		Ok(self.inner.db.get_all_movements().await?)
1378	}
1379
1380	/// Applies an [RFC 7396](https://www.rfc-editor.org/rfc/rfc7396) JSON Merge Patch to the
1381	/// metadata of a movement.
1382	///
1383	/// ```no_run
1384	/// # use serde_json::json;
1385	/// # async fn example(
1386	/// #     wallet: &bark::Wallet,
1387	/// #     id: bark::movement::MovementId,
1388	/// # ) -> anyhow::Result<()> {
1389	/// // Add or overwrite a key.
1390	/// wallet.update_history_metadata(id, &json!({"note": "refund issued"})).await?;
1391	///
1392	/// // Delete a key (null means remove).
1393	/// wallet.update_history_metadata(id, &json!({"note": null})).await?;
1394	///
1395	/// // Nested merge.
1396	/// wallet.update_history_metadata(id, &json!({"counterparty": {"name": "Alice"}})).await?;
1397	/// # Ok(()) }
1398	/// ```
1399	pub async fn update_history_metadata(
1400		&self,
1401		movement_id: MovementId,
1402		patch: &serde_json::Value,
1403	) -> anyhow::Result<()> {
1404		self.inner.movements.patch_metadata(movement_id, patch).await?;
1405		Ok(())
1406	}
1407
1408	/// Query the wallet history by the given payment method
1409	pub async fn history_by_payment_method(
1410		&self,
1411		payment_method: &PaymentMethod,
1412	) -> anyhow::Result<Vec<Movement>> {
1413		let mut ret = self.inner.db.get_movements_by_payment_method(payment_method).await?;
1414		ret.sort_by_key(|m| m.id);
1415		Ok(ret)
1416	}
1417
1418	/// Returns all VTXOs from the database.
1419	pub async fn all_vtxos(&self) -> anyhow::Result<Vec<WalletVtxo>> {
1420		Ok(self.inner.db.get_all_vtxos().await?)
1421	}
1422
1423	/// Returns all not spent vtxos
1424	pub async fn vtxos(&self) -> anyhow::Result<Vec<WalletVtxo>> {
1425		Ok(self.inner.db.get_vtxos_by_state(&VtxoStateKind::UNSPENT_STATES).await?)
1426	}
1427
1428	/// Returns all vtxos matching the provided predicate
1429	pub async fn vtxos_with(&self, filter: &impl FilterVtxos) -> anyhow::Result<Vec<WalletVtxo>> {
1430		let mut vtxos = self.vtxos().await?;
1431		filter.filter_vtxos(&mut vtxos).await.context("error filtering vtxos")?;
1432		Ok(vtxos)
1433	}
1434
1435	/// Returns all spendable vtxos
1436	pub async fn spendable_vtxos(&self) -> anyhow::Result<Vec<WalletVtxo>> {
1437		Ok(self.vtxos_with(&VtxoStateKind::Spendable).await?)
1438	}
1439
1440	/// Returns all spendable vtxos matching the provided predicate
1441	pub async fn spendable_vtxos_with(
1442		&self,
1443		filter: &impl FilterVtxos,
1444	) -> anyhow::Result<Vec<WalletVtxo>> {
1445		let mut vtxos = self.spendable_vtxos().await?;
1446		filter.filter_vtxos(&mut vtxos).await.context("error filtering vtxos")?;
1447		Ok(vtxos)
1448	}
1449
1450	/// Returns all vtxos that will expire within `threshold` blocks
1451	pub async fn get_expiring_vtxos(
1452		&self,
1453		threshold: BlockHeight,
1454	) -> anyhow::Result<Vec<WalletVtxo>> {
1455		let expiry = self.inner.chain.tip().await? + threshold;
1456		let filter = VtxoFilter::new(&self).expires_before(expiry);
1457		Ok(self.spendable_vtxos_with(&filter).await?)
1458	}
1459
1460	/// Performs maintenance tasks and performs refresh interactively until finished when needed.
1461	/// This risks spending users' funds because refreshing may cost fees.
1462	///
1463	/// This can take a long period of time due to syncing rounds, arkoors, checking pending
1464	/// payments, progressing pending rounds, and refreshing VTXOs if necessary.
1465	pub async fn maintenance(&self) -> anyhow::Result<()> {
1466		info!("Starting wallet maintenance in interactive mode");
1467		self.sync().await;
1468
1469		// First try progress any rounds that exist, best effort.
1470		let rounds = self.progress_pending_rounds(None).await;
1471		if let Err(e) = rounds.as_ref() {
1472			warn!("Error progressing pending rounds: {:#}", e);
1473		}
1474
1475		// Then if there are still some participations open, try to cancel them.
1476		let states = self.inner.db.get_pending_round_state_ids().await?;
1477		for id in states {
1478			debug!("Cancelling pending round participation {}", id);
1479			let mut state = match self.lock_wait_round_state(id).await {
1480				Ok(Some(s)) => s,
1481				Ok(None) => continue, // round disappeared, not our problem
1482				Err(e) => {
1483					warn!("Failed to lock round state with id {}: {:#}", id, e);
1484					continue;
1485				}
1486			};
1487			if let Err(e) = state.state_mut().try_cancel(self).await {
1488				warn!("Error cancelling pending round: {:#}", e);
1489			}
1490		}
1491
1492		// And then call refresh so that we can start again.
1493		let refresh = self.maintenance_refresh().await;
1494		if let Err(e) = refresh.as_ref() {
1495			warn!("Error refreshing VTXOs: {:#}", e);
1496		}
1497		if rounds.is_err() || refresh.is_err() {
1498			bail!("Maintenance encountered errors.\nprogress_rounds: {:#?}\nrefresh: {:#?}", rounds, refresh);
1499		}
1500		Ok(())
1501	}
1502
1503	/// Performs maintenance tasks and schedules delegated refresh when needed. This risks spending
1504	/// users' funds because refreshing may cost fees.
1505	///
1506	/// This can take a long period of time due to syncing rounds, arkoors, checking pending
1507	/// payments, progressing pending rounds, and refreshing VTXOs if necessary.
1508	pub async fn maintenance_delegated(&self) -> anyhow::Result<()> {
1509		info!("Starting wallet maintenance in delegated mode");
1510		self.sync().await;
1511		let rounds = self.progress_pending_rounds(None).await;
1512		if let Err(e) = rounds.as_ref() {
1513			warn!("Error progressing pending rounds: {:#}", e);
1514		}
1515		let refresh = self.maybe_schedule_maintenance_refresh_delegated().await;
1516		if let Err(e) = refresh.as_ref() {
1517			warn!("Error refreshing VTXOs: {:#}", e);
1518		}
1519		if rounds.is_err() || refresh.is_err() {
1520			bail!("Delegated maintenance encountered errors.\nprogress_rounds: {:#?}\nrefresh: {:#?}", rounds, refresh);
1521		}
1522		Ok(())
1523	}
1524
1525	/// Performs maintenance tasks and performs refresh interactively until finished when needed.
1526	/// This risks spending users' funds because refreshing may cost fees and any pending exits will
1527	/// be progressed.
1528	///
1529	/// This can take a long period of time due to syncing the onchain wallet, registering boards,
1530	/// syncing rounds, arkoors, and the exit system, checking pending lightning payments and
1531	/// refreshing VTXOs if necessary.
1532	pub async fn maintenance_with_onchain<W: PreparePsbt + SignPsbt + ExitUnilaterally>(
1533		&self,
1534		onchain: &mut W,
1535	) -> anyhow::Result<()> {
1536		info!("Starting wallet maintenance in interactive mode with onchain wallet");
1537
1538		// Maintenance will log so we don't need to.
1539		let maintenance = self.maintenance().await;
1540
1541		// NB: order matters here, after syncing lightning, we might have new exits to start
1542		let exit_sync = self.sync_exits().await;
1543		if let Err(e) = exit_sync.as_ref() {
1544			warn!("Error syncing exits: {:#}", e);
1545		}
1546		let exit_progress = self.exit_mgr().progress_exits_with_bdk(self, onchain, None).await;
1547		if let Err(e) = exit_progress.as_ref() {
1548			warn!("Error progressing exits: {:#}", e);
1549		}
1550		if maintenance.is_err() || exit_sync.is_err() || exit_progress.is_err() {
1551			bail!("Maintenance encountered errors.\nmaintenance: {:#?}\nexit_sync: {:#?}\nexit_progress: {:#?}", maintenance, exit_sync, exit_progress);
1552		}
1553		Ok(())
1554	}
1555
1556	/// Performs maintenance tasks and schedules delegated refresh when needed. This risks spending
1557	/// users' funds because refreshing may cost fees and any pending exits will be progressed.
1558	///
1559	/// This can take a long period of time due to syncing the onchain wallet, registering boards,
1560	/// syncing rounds, arkoors, and the exit system, checking pending lightning payments and
1561	/// refreshing VTXOs if necessary.
1562	pub async fn maintenance_with_onchain_delegated<W: PreparePsbt + SignPsbt + ExitUnilaterally>(
1563		&self,
1564		onchain: &mut W,
1565	) -> anyhow::Result<()> {
1566		info!("Starting wallet maintenance in delegated mode with onchain wallet");
1567
1568		// Maintenance will log so we don't need to.
1569		let maintenance = self.maintenance_delegated().await;
1570
1571		// NB: order matters here, after syncing lightning, we might have new exits to start
1572		let exit_sync = self.sync_exits().await;
1573		if let Err(e) = exit_sync.as_ref() {
1574			warn!("Error syncing exits: {:#}", e);
1575		}
1576		let exit_progress = self.exit_mgr().progress_exits_with_bdk(self, onchain, None).await;
1577		if let Err(e) = exit_progress.as_ref() {
1578			warn!("Error progressing exits: {:#}", e);
1579		}
1580		if maintenance.is_err() || exit_sync.is_err() || exit_progress.is_err() {
1581			bail!("Delegated maintenance encountered errors.\nmaintenance: {:#?}\nexit_sync: {:#?}\nexit_progress: {:#?}", maintenance, exit_sync, exit_progress);
1582		}
1583		Ok(())
1584	}
1585
1586	/// Checks VTXOs that are due to be refreshed, and schedules an interactive refresh if any
1587	///
1588	/// This will include any VTXOs within the expiry threshold
1589	/// ([Config::vtxo_refresh_expiry_threshold]) or those which
1590	/// are uneconomical to exit due to onchain network conditions.
1591	///
1592	/// Returns a [RoundStateId] if a refresh is scheduled.
1593	pub async fn maybe_schedule_maintenance_refresh(&self) -> anyhow::Result<Option<RoundStateId>> {
1594		let vtxos = self.get_vtxos_to_refresh().await?;
1595		if vtxos.len() == 0 {
1596			return Ok(None);
1597		}
1598
1599		let participation = match self.build_refresh_participation(vtxos).await? {
1600			Some(participation) => participation,
1601			None => return Ok(None),
1602		};
1603
1604		info!("Scheduling maintenance refresh ({} vtxos)", participation.inputs.len());
1605		let state = self.join_next_round(participation, Some(RoundMovement::Refresh)).await?;
1606		Ok(Some(state.id()))
1607	}
1608
1609	/// Checks VTXOs that are due to be refreshed, and schedules a delegated refresh if any
1610	///
1611	/// This will include any VTXOs within the expiry threshold
1612	/// ([Config::vtxo_refresh_expiry_threshold]) or those which
1613	/// are uneconomical to exit due to onchain network conditions.
1614	///
1615	/// Returns a [RoundStateId] if a refresh is scheduled.
1616	pub async fn maybe_schedule_maintenance_refresh_delegated(
1617		&self,
1618	) -> anyhow::Result<Option<RoundStateId>> {
1619		let vtxos = self.get_vtxos_to_refresh().await?;
1620		if vtxos.len() == 0 {
1621			return Ok(None);
1622		}
1623
1624		let participation = match self.build_refresh_participation(vtxos).await? {
1625			Some(participation) => participation,
1626			None => return Ok(None),
1627		};
1628
1629		info!("Scheduling delegated maintenance refresh ({} vtxos)", participation.inputs.len());
1630		let state = self.join_next_round_delegated(participation, Some(RoundMovement::Refresh)).await?;
1631		Ok(Some(state.id()))
1632	}
1633
1634	/// Performs an interactive refresh of all VTXOs that are due to be refreshed, if any
1635	///
1636	/// This will include any VTXOs within the expiry threshold
1637	/// ([Config::vtxo_refresh_expiry_threshold]) or those which
1638	/// are uneconomical to exit due to onchain network conditions.
1639	///
1640	/// Returns a [RoundStatus] if a refresh occurs.
1641	pub async fn maintenance_refresh(&self) -> anyhow::Result<Option<RoundStatus>> {
1642		let vtxos = self.get_vtxos_to_refresh().await?;
1643		if vtxos.len() == 0 {
1644			return Ok(None);
1645		}
1646
1647		info!("Waiting for round to perform maintenance refresh...");
1648		let mut events = self.subscribe_round_events().await?;
1649		while let Some(event) = events.next().await {
1650			match event {
1651				Ok(RoundEvent::Attempt(a)) if a.attempt_seq == 0 => {
1652					let vtxos = self.get_vtxos_to_refresh().await?;
1653					if vtxos.len() == 0 {
1654						return Ok(None);
1655					}
1656					debug!("Round {} started, triggering refresh", a.round_seq);
1657					return self.refresh_vtxos(vtxos).await;
1658				},
1659				_ => {},
1660			}
1661		}
1662		Ok(None)
1663	}
1664
1665	/// Sync offchain wallet and update onchain fees. This is a much more lightweight alternative
1666	/// to [Wallet::maintenance] as it will not refresh VTXOs or sync the onchain wallet.
1667	///
1668	/// Notes:
1669	/// - The exit system is not synced here. Call [Wallet::sync_exits] explicitly, or use
1670	///   [Wallet::maintenance_with_onchain] for a full sync including onchain fee bumping.
1671	pub async fn sync(&self) {
1672		futures::join!(
1673			async {
1674				// NB: order matters here, if syncing call fails,
1675				// we still want to update the fee rates
1676				if let Err(e) = self.inner.chain.update_fee_rates(self.inner.config.fallback_fee_rate).await {
1677					warn!("Error updating fee rates: {:#}", e);
1678				}
1679			},
1680			async {
1681				if let Err(e) = self.sync_mailbox().await {
1682					warn!("Error in mailbox sync: {:#}", e);
1683				}
1684			},
1685			async {
1686				if let Err(e) = self.sync_pending_rounds().await {
1687					warn!("Error while trying to progress rounds awaiting confirmations: {:#}", e);
1688				}
1689			},
1690			async {
1691				if let Err(e) = self.sync_pending_lightning_send_vtxos().await {
1692					warn!("Error syncing pending lightning payments: {:#}", e);
1693				}
1694			},
1695			async {
1696				if let Err(e) = self.try_claim_all_lightning_receives(false).await {
1697					warn!("Error claiming pending lightning receives: {:#}", e);
1698				}
1699			},
1700			async {
1701				if let Err(e) = self.sync_pending_boards().await {
1702					warn!("Error syncing pending boards: {:#}", e);
1703				}
1704			},
1705			async {
1706				if let Err(e) = self.sync_pending_offboards().await {
1707					warn!("Error syncing pending offboards: {:#}", e);
1708				}
1709			}
1710		);
1711	}
1712
1713	/// Sync the transaction status of unilateral exits
1714	///
1715	/// This will not progress the unilateral exits in any way, it will merely check the
1716	/// transaction status of each transaction as well as check whether any exits have become
1717	/// claimable or have been claimed.
1718	pub async fn sync_exits(&self) -> anyhow::Result<()> {
1719		self.exit_mgr().sync(&self).await?;
1720		Ok(())
1721	}
1722
1723	/// Drop a specific [Vtxo] from the database. This is destructive and will result in a loss of
1724	/// funds.
1725	pub async fn dangerous_drop_vtxo(&self, vtxo_id: VtxoId) -> anyhow::Result<()> {
1726		warn!("Drop vtxo {} from the database", vtxo_id);
1727		self.inner.db.remove_vtxo(vtxo_id).await?;
1728		Ok(())
1729	}
1730
1731	/// Drop all VTXOs from the database. This is destructive and will result in a loss of funds.
1732	//TODO(stevenroose) improve the way we expose dangerous methods
1733	pub async fn dangerous_drop_all_vtxos(&self) -> anyhow::Result<()> {
1734		warn!("Dropping all vtxos from the db...");
1735		for vtxo in self.vtxos().await? {
1736			self.inner.db.remove_vtxo(vtxo.id()).await?;
1737		}
1738
1739		self.exit_mgr().dangerous_clear_exit().await?;
1740		Ok(())
1741	}
1742
1743	/// Checks if the provided VTXO has some counterparty risk in the current wallet.
1744	///
1745	/// An arkoor vtxo is considered to have some counterparty risk if it is
1746	/// (directly or not) based on round VTXOs that aren't owned by the
1747	/// wallet. The check inspects the genesis chain, so this takes a full
1748	/// VTXO; callers working from a bare listing should hydrate via
1749	/// [Wallet::get_full_vtxo] or [BarkPersister::get_full_vtxos] first.
1750	async fn has_counterparty_risk(&self, vtxo: &Vtxo<Full>) -> anyhow::Result<bool> {
1751		for past_pks in vtxo.past_arkoor_pubkeys() {
1752			let mut owns_any = false;
1753			for past_pk in past_pks {
1754				if self.inner.db.get_public_key_idx(&past_pk).await?.is_some() {
1755					owns_any = true;
1756					break;
1757				}
1758			}
1759			if !owns_any {
1760				return Ok(true);
1761			}
1762		}
1763
1764		let my_clause = self.find_signable_clause(vtxo).await;
1765		Ok(!my_clause.is_some())
1766	}
1767
1768	/// If there are any VTXOs that match the "must-refresh" and "should-refresh" criteria with a
1769	/// total value over the P2TR dust limit, they are added to the round participation and an
1770	/// additional output is also created.
1771	///
1772	/// Note: This assumes that the base refresh fee has already been paid.
1773	async fn add_should_refresh_vtxos(
1774		&self,
1775		participation: &mut RoundParticipation,
1776	) -> anyhow::Result<()> {
1777		// Get VTXOs that need and should be refreshed, then filter out any duplicates before
1778		// adjusting the round participation.
1779		let tip = self.inner.chain.tip().await?;
1780		let mut vtxos_to_refresh = self.spendable_vtxos_with(
1781			&RefreshStrategy::should_refresh(self, tip, self.inner.chain.fee_rates().await.fast),
1782		).await?;
1783		if vtxos_to_refresh.is_empty() {
1784			return Ok(());
1785		}
1786
1787		let excluded_ids = participation.inputs.iter().map(|v| v.vtxo_id())
1788			.collect::<HashSet<_>>();
1789		let mut total_amount = Amount::ZERO;
1790		for i in (0..vtxos_to_refresh.len()).rev() {
1791			let vtxo = &vtxos_to_refresh[i];
1792			if excluded_ids.contains(&vtxo.id()) {
1793				vtxos_to_refresh.swap_remove(i);
1794				continue;
1795			}
1796			total_amount += vtxo.amount();
1797		}
1798		if vtxos_to_refresh.is_empty() {
1799			// VTXOs are already included in the round participation.
1800			return Ok(());
1801		}
1802
1803		// We need to verify that the output we add won't end up below the dust limit when fees are
1804		// applied. We can assume the base fee has been paid by the current refresh participation.
1805		let (_, ark_info) = self.require_server().await?;
1806		let fee = ark_info.fees.refresh.calculate_no_base_fee(
1807			vtxos_to_refresh.iter().map(|wv| VtxoFeeInfo::from_vtxo_and_tip(&wv.vtxo, tip)),
1808		).context("fee overflowed")?;
1809
1810		// Only add these VTXOs if the output amount would be above dust after fees.
1811		let output_amount = match validate_and_subtract_fee_min_dust(total_amount, fee) {
1812			Ok(amount) => amount,
1813			Err(e) => {
1814				trace!("Cannot add should-refresh VTXOs: {}", e);
1815				return Ok(());
1816			},
1817		};
1818		info!(
1819			"Adding {} extra VTXOs to round participation total = {}, fee = {}, output = {}",
1820			vtxos_to_refresh.len(), total_amount, fee, output_amount,
1821		);
1822		let (user_keypair, _) = self.derive_store_next_keypair().await?;
1823		let req = VtxoRequest {
1824			policy: VtxoPolicy::new_pubkey(user_keypair.public_key()),
1825			amount: output_amount,
1826		};
1827		let extra_ids = vtxos_to_refresh.into_iter().map(|wv| wv.id()).collect::<Vec<_>>();
1828		let extra_full = self.inner.db.get_full_vtxos(&extra_ids).await
1829			.context("failed to hydrate refresh candidates")?;
1830		participation.inputs.reserve(extra_full.len());
1831		participation.inputs.extend(extra_full);
1832		participation.outputs.push(req);
1833
1834		Ok(())
1835	}
1836
1837	pub async fn build_refresh_participation<V: VtxoRef>(
1838		&self,
1839		vtxos: impl IntoIterator<Item = V>,
1840	) -> anyhow::Result<Option<RoundParticipation>> {
1841		let (vtxos, total_amount) = {
1842			let iter = vtxos.into_iter();
1843			let size_hint = iter.size_hint();
1844			let mut vtxos = Vec::<Vtxo<Full>>::with_capacity(size_hint.1.unwrap_or(size_hint.0));
1845			let mut amount = Amount::ZERO;
1846			for vref in iter {
1847				// We use a Vec here instead of a HashMap or a HashSet of IDs because for the kinds
1848				// of elements we expect to deal with, a Vec is likely to be quicker. The overhead
1849				// of hashing each ID and making additional allocations isn't likely to be worth it
1850				// for what is likely to be a handful of VTXOs or at most a couple of hundred.
1851				let id = vref.vtxo_id();
1852				if vtxos.iter().any(|v| v.id() == id) {
1853					bail!("duplicate VTXO id: {}", id);
1854				}
1855				let vtxo = if let Some(vtxo) = vref.into_full_vtxo() {
1856					vtxo
1857				} else {
1858					// Listings/selection return bare wallet vtxos; the round
1859					// flow needs the full chain to forfeit and register.
1860					self.inner.db.get_full_vtxo(id).await?
1861						.with_context(|| format!("vtxo with id {} not found", id))?
1862				};
1863				amount += vtxo.amount();
1864				vtxos.push(vtxo);
1865			}
1866			(vtxos, amount)
1867		};
1868
1869		if vtxos.is_empty() {
1870			info!("Skipping refresh since no VTXOs are provided.");
1871			return Ok(None);
1872		}
1873		ensure!(total_amount >= P2TR_DUST,
1874			"vtxo amount must be at least {} to participate in a round",
1875			P2TR_DUST,
1876		);
1877
1878		// Calculate refresh fees
1879		let (_, ark_info) = self.require_server().await?;
1880		let current_height = self.inner.chain.tip().await?;
1881		let vtxo_fee_infos = vtxos.iter()
1882			.map(|v| VtxoFeeInfo::from_vtxo_and_tip(v, current_height));
1883		let fee = ark_info.fees.refresh.calculate(vtxo_fee_infos).context("fee overflowed")?;
1884		let output_amount = validate_and_subtract_fee_min_dust(total_amount, fee)?;
1885
1886		info!("Refreshing {} VTXOs (total amount = {}, fee = {}, output = {}).",
1887			vtxos.len(), total_amount, fee, output_amount,
1888		);
1889		let (user_keypair, _) = self.derive_store_next_keypair().await?;
1890		let req = VtxoRequest {
1891			policy: VtxoPolicy::Pubkey(PubkeyVtxoPolicy { user_pubkey: user_keypair.public_key() }),
1892			amount: output_amount,
1893		};
1894
1895		Ok(Some(RoundParticipation {
1896			inputs: vtxos,
1897			outputs: vec![req],
1898			unblinded_mailbox_id: None,
1899		}))
1900	}
1901
1902	/// This will refresh all provided VTXOs in an interactive round and wait until end
1903	///
1904	/// Returns the [RoundStatus] of the round if a successful refresh occurred.
1905	/// It will return [None] if no [Vtxo] needed to be refreshed.
1906	pub async fn refresh_vtxos<V: VtxoRef>(
1907		&self,
1908		vtxos: impl IntoIterator<Item = V>,
1909	) -> anyhow::Result<Option<RoundStatus>> {
1910		let mut participation = match self.build_refresh_participation(vtxos).await? {
1911			Some(participation) => participation,
1912			None => return Ok(None),
1913		};
1914
1915		if let Err(e) = self.add_should_refresh_vtxos(&mut participation).await {
1916			warn!("Error trying to add additional VTXOs that should be refreshed: {:#}", e);
1917		}
1918
1919		Ok(Some(self.participate_round(participation, Some(RoundMovement::Refresh)).await?))
1920	}
1921
1922	/// This will refresh all provided VTXOs in delegated (non-interactive) mode
1923	///
1924	/// Returns the [StoredRoundState] which can be used to track the round's
1925	/// progress later by calling sync. It will return [None] if no [Vtxo]
1926	/// needed to be refreshed.
1927	pub async fn refresh_vtxos_delegated<V: VtxoRef>(
1928		&self,
1929		vtxos: impl IntoIterator<Item = V>,
1930	) -> anyhow::Result<Option<StoredRoundState<Unlocked>>> {
1931		let mut part = match self.build_refresh_participation(vtxos).await? {
1932			Some(participation) => participation,
1933			None => return Ok(None),
1934		};
1935
1936		if let Err(e) = self.add_should_refresh_vtxos(&mut part).await {
1937			warn!("Error trying to add additional VTXOs that should be refreshed: {:#}", e);
1938		}
1939
1940		Ok(Some(self.join_next_round_delegated(part, Some(RoundMovement::Refresh)).await?))
1941	}
1942
1943	/// This will find all VTXOs that meets must-refresh criteria. Then, if there are some VTXOs to
1944	/// refresh, it will also add those that meet should-refresh criteria.
1945	pub async fn get_vtxos_to_refresh(&self) -> anyhow::Result<Vec<WalletVtxo>> {
1946		let vtxos = self.spendable_vtxos_with(&RefreshStrategy::should_refresh_if_must(
1947			self,
1948			self.inner.chain.tip().await?,
1949			self.inner.chain.fee_rates().await.fast,
1950		)).await?;
1951		Ok(vtxos)
1952	}
1953
1954	/// Returns the block height at which the first VTXO will expire
1955	pub async fn get_first_expiring_vtxo_blockheight(
1956		&self,
1957	) -> anyhow::Result<Option<BlockHeight>> {
1958		Ok(self.spendable_vtxos().await?.iter().map(|v| v.expiry_height()).min())
1959	}
1960
1961	/// Returns the next block height at which we have a VTXO that we
1962	/// want to refresh
1963	pub async fn get_next_required_refresh_blockheight(
1964		&self,
1965	) -> anyhow::Result<Option<BlockHeight>> {
1966		let first_expiry = self.get_first_expiring_vtxo_blockheight().await?;
1967		Ok(first_expiry.map(|h| h.saturating_sub(self.inner.config.vtxo_refresh_expiry_threshold)))
1968	}
1969
1970	/// Select several VTXOs to cover the provided amount
1971	///
1972	/// VTXOs are selected soonest-expiring-first.
1973	///
1974	/// Returns an error if amount cannot be reached.
1975	async fn select_vtxos_to_cover(
1976		&self,
1977		amount: Amount,
1978	) -> anyhow::Result<Vec<WalletVtxo>> {
1979		let mut vtxos = self.spendable_vtxos().await?;
1980		self.sort_vtxos_for_selection(&mut vtxos);
1981
1982		let (last, _total_amount) = self.select_vtxos_inner(amount, &vtxos)?;
1983		vtxos.truncate(last+1);
1984		Ok(vtxos)
1985	}
1986
1987	/// Determines which VTXOs to use for a fee-paying transaction where the fee is added on top of
1988	/// the desired amount. E.g., a lightning payment, a send-onchain payment.
1989	///
1990	/// Returns a collection of VTXOs capable of covering the desired amount as well as the
1991	/// calculated fee.
1992	async fn select_vtxos_to_cover_with_fee<F>(
1993		&self,
1994		amount: Amount,
1995		calc_fee: F,
1996	) -> anyhow::Result<(Vec<WalletVtxo>, Amount)>
1997	where
1998		F: for<'a> Fn(
1999			Amount, std::iter::Copied<std::slice::Iter<'a, VtxoFeeInfo>>,
2000		) -> anyhow::Result<Amount>,
2001	{
2002		let tip = self.inner.chain.tip().await?;
2003		let mut vtxos = self.spendable_vtxos().await?;
2004		self.sort_vtxos_for_selection(&mut vtxos);
2005
2006		let fee_info = vtxos.iter()
2007			.map(|v| VtxoFeeInfo::from_vtxo_and_tip(v, tip))
2008			.collect::<Vec<_>>();
2009
2010		// We need to loop to find suitable inputs due to the VTXOs having a direct impact on
2011		// how much we must pay in fees.
2012		const MAX_ITERATIONS: usize = 100;
2013		let mut fee = Amount::ZERO;
2014		for _ in 0..MAX_ITERATIONS {
2015			let required = amount.checked_add(fee)
2016				.context("Amount + fee overflow")?;
2017
2018			let (last, vtxo_amount) = self.select_vtxos_inner(required, &vtxos)
2019				.context("Could not find enough suitable VTXOs to cover payment + fees")?;
2020			fee = calc_fee(amount, fee_info[..=last].iter().copied())?;
2021
2022			if amount + fee <= vtxo_amount {
2023				trace!("Selected vtxos to cover amount + fee: amount = {}, fee = {}, total inputs = {}",
2024					amount, fee, vtxo_amount,
2025				);
2026				vtxos.truncate(last+1);
2027				return Ok((vtxos, fee));
2028			}
2029			trace!("VTXO sum of {} did not exceed amount {} and fee {}, iterating again",
2030				vtxo_amount, amount, fee,
2031			);
2032		}
2033		bail!("Fee calculation did not converge after maximum iterations")
2034	}
2035
2036	/// Sorts the given `vtxos` in place ready for selection to cover funds.
2037	fn sort_vtxos_for_selection(&self, vtxos: &mut Vec<WalletVtxo>) {
2038		vtxos.sort_by_key(|v| v.expiry_height());
2039	}
2040
2041	/// Iterates through the given `Vec` until either the given `amount` can be covered for a
2042	/// payment or until the `Vec` is exhausted, at which point an error will be returned.
2043	///
2044	/// Returns the index of the last VTXO included in the selection, as well as the total amount of
2045	/// the selected VTXOs.
2046	fn select_vtxos_inner(
2047		&self,
2048		amount: Amount,
2049		vtxos: &Vec<WalletVtxo>,
2050	) -> anyhow::Result<(usize, Amount)> {
2051		// Iterate over VTXOs until the required amount is reached
2052		let mut total_amount = Amount::ZERO;
2053		for (i, vtxo) in vtxos.iter().enumerate() {
2054			total_amount += vtxo.amount();
2055
2056			if total_amount >= amount {
2057				return Ok((i, total_amount))
2058			}
2059		}
2060
2061		bail!("Insufficient money available. Needed {} but {} is available",
2062			amount, total_amount,
2063		);
2064	}
2065
2066	/// Starts a daemon for the wallet.
2067	///
2068	/// Note:
2069	/// - This function doesn't check if a daemon is already running,
2070	/// so it's possible to start multiple daemons by mistake.
2071	pub fn start_daemon(
2072		&self,
2073		onchain: Option<Arc<tokio::sync::RwLock<dyn DaemonizableOnchainWallet>>>,
2074	) -> anyhow::Result<()> {
2075		let mut daemon = self.inner.daemon.lock();
2076		if daemon.is_some() {
2077			warn!("Called Wallet::start_daemon while daemon was already running.");
2078			return Ok(());
2079		}
2080
2081		// NB currently can't error but it's a pretty common method and quite likely that error
2082		// cases will be introduces later
2083		let handle = crate::daemon::start_daemon(self.clone(), onchain);
2084		let _ = daemon.insert(handle);
2085
2086		Ok(())
2087	}
2088
2089	/// Use [Wallet::start_daemon] instead.
2090	#[deprecated(since = "0.1.4", note = "use start_daemon instead")]
2091	pub fn run_daemon(
2092		&self,
2093		onchain: Option<Arc<tokio::sync::RwLock<dyn DaemonizableOnchainWallet>>>,
2094	) -> anyhow::Result<()> {
2095		self.start_daemon(onchain)
2096	}
2097
2098	/// Stops the daemon for the wallet if it is running, otherwise does nothing.
2099	pub fn stop_daemon(&self) {
2100		let mut daemon = self.inner.daemon.lock();
2101		if let Some(handle) = daemon.take() {
2102			handle.stop();
2103		}
2104	}
2105
2106	/// Registers the signed transaction chains for the given VTXOs with the
2107	/// server. This must be called before spending VTXOs so the server can
2108	/// publish forfeits if needed.
2109	pub async fn register_vtxo_transactions_with_server(
2110		&self,
2111		vtxos: &[impl AsRef<Vtxo<Full>>],
2112	) -> anyhow::Result<()> {
2113		if vtxos.is_empty() {
2114			return Ok(());
2115		}
2116
2117		let (mut srv, _) = self.require_server().await?;
2118		srv.client.register_vtxo_transactions(protos::RegisterVtxoTransactionsRequest {
2119			vtxos: vtxos.iter().map(|v| v.as_ref().serialize()).collect(),
2120		}).await.context("failed to register vtxo transactions")?;
2121
2122		Ok(())
2123	}
2124}
2125
2126fn wrap_server_connect_error(err: ConnectError) -> anyhow::Error {
2127	match err {
2128		ConnectError::CreateEndpoint(CreateEndpointError::NoTransportBackend) => {
2129			anyhow!(MISSING_SERVER_TRANSPORT_HELP)
2130		},
2131		other => anyhow::Error::from(other),
2132	}
2133}
2134
2135impl std::ops::Drop for WalletInner {
2136	fn drop(&mut self) {
2137		if let Some(handle) = self.daemon.lock().take() {
2138			handle.stop();
2139		}
2140	}
2141}
2142
2143#[cfg(test)]
2144mod tests {
2145	use server_rpc::client::CreateEndpointError;
2146
2147	use super::{wrap_server_connect_error, MISSING_SERVER_TRANSPORT_HELP};
2148
2149	#[test]
2150	fn no_transport_connect_error_is_reworded_for_wallet_users() {
2151		let err = wrap_server_connect_error(CreateEndpointError::NoTransportBackend.into());
2152		assert!(err.to_string().contains(MISSING_SERVER_TRANSPORT_HELP));
2153		assert!(err.to_string().contains("feature `bark-wallet/native` or `bark-wallet/wasm-web`"));
2154	}
2155}