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