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