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