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//! 	// Make sure your app is synced before inspecting the wallet
228//! 	wallet.sync().await;
229//!
230//! 	let vtxos: Vec<bark::WalletVtxo> = wallet.vtxos().await.unwrap();
231//! 	Ok(())
232//! }
233//! ```
234//!
235//! Use [Wallet::balance] if you are only interested in the balance.
236//!
237//! ## Participating in a round
238//!
239//! You can participate in a round to refresh your coins. Typically,
240//! you want to refresh coins which are soon to expire or you might
241//! want to aggregate multiple small vtxos to keep the cost of exit
242//! under control.
243//!
244//! As a wallet developer you can implement your own refresh strategy.
245//! This gives you full control over which [ark::Vtxo]s are refreshed and
246//! which aren't.
247//!
248//! This example uses [RefreshStrategy::must_refresh] which is a sane
249//! default that selects all [ark::Vtxo]s that must be refreshed.
250//!
251//! ```no_run
252//! # use std::sync::Arc;
253//! # use std::str::FromStr;
254//! # use std::path::PathBuf;
255//! #
256//! # use tokio::fs;
257//! #
258//! # use bark::{Config, Wallet};
259//! # use bark::lock_manager::memory::MemoryLockManager;
260//! # use bark::persist::sqlite::SqliteClient;
261//! #
262//! # const MNEMONIC_FILE : &str = "mnemonic";
263//! # const DB_FILE: &str = "db.sqlite";
264//! #
265//! # async fn get_wallet() -> Wallet {
266//! 	#   let datadir = PathBuf::from("./bark");
267//! 	#
268//! 	#   let db = Arc::new(SqliteClient::open(datadir.join(DB_FILE)).unwrap());
269//! 	#   let mnemonic_str = fs::read_to_string(datadir.join(DB_FILE)).await.unwrap();
270//! 	#   let mnemonic = bip39::Mnemonic::from_str(&mnemonic_str).unwrap();
271//! 	#
272//! 	#   let config = Config::network_default(bitcoin::Network::Signet);
273//! 	#
274//! 	#   let lock_manager = Box::new(MemoryLockManager::new());
275//! 	#   Wallet::open(&mnemonic, db, config, lock_manager).await.unwrap()
276//! 	# }
277//! #
278//! use bark::vtxo::RefreshStrategy;
279//!
280//! #[tokio::main]
281//! async fn main() -> anyhow::Result<()> {
282//! 	let wallet = get_wallet().await;
283//!
284//! 	// Select all vtxos that refresh soon
285//! 	let tip = wallet.chain().tip().await?;
286//! 	let fee_rate = wallet.chain().fee_rates().await.fast;
287//! 	let strategy = RefreshStrategy::must_refresh(&wallet, tip, fee_rate);
288//!
289//! 	let vtxos = wallet.spendable_vtxos_with(&strategy).await?;
290//!		wallet.refresh_vtxos(vtxos).await?;
291//! 	Ok(())
292//! }
293//! ```
294
295#[cfg(all(any(target_os = "android", target_os = "ios"), feature = "tls-native-roots"))]
296compile_error!("feature `tls-native-roots` can't be used on Android or iOS, use `tls-webpki-roots` instead");
297
298pub extern crate ark;
299
300pub extern crate bip39;
301pub extern crate lightning_invoice;
302pub extern crate lnurl as lnurllib;
303
304#[macro_use] extern crate anyhow;
305#[macro_use] extern crate async_trait;
306#[macro_use] extern crate serde;
307
308pub mod actions;
309pub mod chain;
310pub mod exit;
311pub mod movement;
312pub mod onchain;
313pub mod payment_request;
314pub mod persist;
315pub mod round;
316pub mod subsystem;
317pub mod vtxo;
318
319pub mod lock_manager;
320
321mod arkoor;
322mod board;
323mod config;
324mod daemon;
325mod fees;
326mod lightning;
327mod mailbox;
328mod notification;
329mod offboard;
330#[cfg(feature = "socks5-proxy")]
331mod proxy;
332mod psbtext;
333mod utils;
334
335pub use self::arkoor::{ArkoorCreateResult, ArkoorAddressError};
336pub use self::config::{BarkNetwork, Config};
337pub use self::daemon::DaemonHandle;
338pub use self::fees::FeeEstimate;
339pub use self::notification::{WalletNotification, NotificationStream};
340pub use self::vtxo::WalletVtxo;
341
342use std::borrow::Cow;
343use std::collections::HashSet;
344use std::sync::Arc;
345use std::time::Duration;
346
347use anyhow::{bail, Context};
348use bip39::Mnemonic;
349use bitcoin::{Amount, Network, OutPoint};
350use bitcoin::bip32::{self, ChildNumber, Fingerprint};
351use bitcoin::secp256k1::{self, Keypair, PublicKey};
352use log::{trace, info, warn, error};
353
354use ark::{ArkInfo, ProtocolEncoding, Vtxo, VtxoId, VtxoPolicy, VtxoRequest};
355use ark::address::VtxoDelivery;
356use ark::fees::{validate_and_subtract_fee_min_dust, VtxoFeeInfo};
357use ark::vtxo::{Full, PubkeyVtxoPolicy, VtxoRef};
358use ark::vtxo::policy::signing::VtxoSigner;
359use bitcoin_ext::{BlockHeight, P2TR_DUST};
360use server_rpc::{protos, ServerConnection};
361use server_rpc::client::{ConnectError, CreateEndpointError};
362use crate::chain::{ChainSource, ChainSourceSpec};
363use crate::exit::Exit;
364use crate::lock_manager::LockManager;
365use crate::movement::{Movement, MovementId, PaymentMethod};
366use crate::movement::manager::MovementManager;
367use crate::movement::update::MovementUpdate;
368use crate::notification::NotificationDispatch;
369use crate::onchain::{ExitUnilaterally, PreparePsbt, SignPsbt, Utxo};
370use crate::onchain::DaemonizableOnchainWallet;
371use crate::persist::BarkPersister;
372use crate::persist::models::{RoundStateId, StoredRoundState, Unlocked};
373#[cfg(feature = "socks5-proxy")]
374use crate::proxy::proxy_for_url;
375use crate::round::{RoundParticipation, RoundStatus};
376use crate::subsystem::{ArkoorMovement, RoundMovement};
377use crate::vtxo::{FilterVtxos, RefreshStrategy, VtxoFilter, VtxoStateKind};
378
379#[cfg(all(feature = "wasm-web", feature = "socks5-proxy"))]
380compile_error!("features `wasm-web` does not support feature `socks5-proxy");
381
382#[cfg(all(feature = "wasm-web", feature = "bitcoind-rpc"))]
383compile_error!("`wasm-web` does not support the `bitcoind-rpc` feature");
384
385/// If a streaming connection was alive for at least this long before
386/// dropping, treat it as a normal idle timeout (e.g. proxy-side) rather
387/// than a server failure.
388const HEALTHY_STREAM_DURATION: Duration = Duration::from_secs(59);
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_onchain],
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_onchain(
730/// 	&mnemonic,
731/// 	network,
732/// 	cfg.clone(),
733/// 	db,
734/// 	lock_manager,
735/// 	&onchain_wallet,
736/// 	false,
737/// ).await?;
738/// // let mut wallet = Wallet::create(&mnemonic, network, cfg.clone(), db.clone(), Box::new(MemoryLockManager::new()), false).await?;
739/// // let mut wallet = Wallet::open(&mnemonic, db.clone(), cfg.clone(), Box::new(MemoryLockManager::new())).await?;
740/// // let mut wallet = Wallet::open_with_onchain(
741/// //    &mnemonic, network, cfg.clone(), db.clone(), &onchain_wallet
742/// // ).await?;
743///
744/// // There are two main ways to update the wallet, the primary is to use one of the maintenance
745/// // commands which will sync everything, refresh VTXOs and reconcile pending lightning payments.
746/// wallet.maintenance().await?;
747/// wallet.maintenance_with_onchain(&mut onchain_wallet).await?;
748///
749/// // Alternatively, you can use the fine-grained sync commands to sync individual parts of the
750/// // wallet state and use `maintenance_refresh` where necessary to refresh VTXOs.
751/// wallet.sync().await?;
752/// wallet.sync_pending_lightning_send_vtxos().await?;
753/// wallet.register_all_confirmed_boards(&mut onchain_wallet).await?;
754/// wallet.sync_exits(&mut onchain_wallet).await?;
755/// wallet.maintenance_refresh().await?;
756///
757/// // Generate a new Ark address to receive funds via arkoor
758/// let addr = wallet.new_address().await?;
759///
760/// // Query balance and VTXOs
761/// let balance = wallet.balance()?;
762/// let vtxos = wallet.vtxos()?;
763///
764/// // Progress any unilateral exits, make sure to sync first
765/// wallet.exit_mgr().progress_exits(&wallet, &mut onchain_wallet, None).await?;
766///
767/// # Ok(())
768/// # }
769/// ```
770#[derive(Clone)]
771pub struct Wallet {
772	inner: Arc<WalletInner>,
773}
774
775impl Wallet {
776	/// Verifies that the bark [Wallet] can be used with the configured [chain::ChainSource].
777	/// More specifically, if the [chain::ChainSource] connects to Bitcoin Core it must be
778	/// a high enough version to support ephemeral anchors.
779	pub async fn require_chainsource_version(&self) -> anyhow::Result<()> {
780		self.inner.chain.require_version().await
781	}
782
783	pub async fn network(&self) -> anyhow::Result<Network> {
784		Ok(self.properties().await?.network)
785	}
786
787	/// Access the server's chain source
788	pub fn chain(&self) -> &Arc<ChainSource> {
789		&self.inner.chain
790	}
791
792	/// Access the exit manager
793	pub fn exit_mgr(&self) -> &Exit {
794		&self.inner.exit
795	}
796
797	/// Access the movements manager
798	pub fn movements_mgr(&self) -> &MovementManager {
799		&self.inner.movements
800	}
801
802	/// Peek at the keypair directly after currently last revealed one,
803	/// together with its index, without storing it.
804	pub async fn peek_next_keypair(&self) -> anyhow::Result<(Keypair, u32)> {
805		let last_revealed = self.inner.db.get_last_vtxo_key_index().await?;
806
807		let index = last_revealed.map(|i| i + 1).unwrap_or(u32::MIN);
808		let keypair = self.inner.seed.derive_vtxo_keypair(index);
809
810		Ok((keypair, index))
811	}
812
813	/// Derive and store the keypair directly after currently last revealed one,
814	/// together with its index.
815	pub async fn derive_store_next_keypair(&self) -> anyhow::Result<(Keypair, u32)> {
816		let (keypair, index) = self.peek_next_keypair().await?;
817		self.inner.db.store_vtxo_key(index, keypair.public_key()).await?;
818		Ok((keypair, index))
819	}
820
821	#[deprecated(note = "use peek_keypair instead")]
822	pub async fn peak_keypair(&self, index: u32) -> anyhow::Result<Keypair> {
823		self.peek_keypair(index).await
824	}
825
826	/// Retrieves a keypair based on the provided index and checks if the corresponding public key
827	/// exists in the [Vtxo] database.
828	///
829	/// # Arguments
830	///
831	/// * `index` - The index used to derive a keypair.
832	///
833	/// # Returns
834	///
835	/// * `Ok(Keypair)` - If the keypair is successfully derived and its public key exists in the
836	///   database.
837	/// * `Err(anyhow::Error)` - If the public key does not exist in the database or if an error
838	///   occurs during the database query.
839	pub async fn peek_keypair(&self, index: u32) -> anyhow::Result<Keypair> {
840		let keypair = self.inner.seed.derive_vtxo_keypair(index);
841		if self.inner.db.get_public_key_idx(&keypair.public_key()).await?.is_some() {
842			Ok(keypair)
843		} else {
844			bail!("VTXO key {} does not exist, please derive it first", index)
845		}
846	}
847
848
849	/// Retrieves the [Keypair] for a provided [PublicKey]
850	///
851	/// # Arguments
852	///
853	/// * `public_key` - The public key for which the keypair must be found
854	///
855	/// # Returns
856	/// * `Ok(Some(u32, Keypair))` - If the pubkey is found, the derivation-index and keypair are
857	///                              returned
858	/// * `Ok(None)` - If the pubkey cannot be found in the database
859	/// * `Err(anyhow::Error)` - If an error occurred related to the database query
860	pub async fn pubkey_keypair(&self, public_key: &PublicKey) -> anyhow::Result<Option<(u32, Keypair)>> {
861		if let Some(index) = self.inner.db.get_public_key_idx(&public_key).await? {
862			Ok(Some((index, self.inner.seed.derive_vtxo_keypair(index))))
863		} else {
864			Ok(None)
865		}
866	}
867
868	/// Retrieves the [Keypair] for a provided [Vtxo]
869	///
870	/// # Arguments
871	///
872	/// * `vtxo` - The vtxo for which the key must be found
873	///
874	/// # Returns
875	/// * `Ok(Some(Keypair))` - If the pubkey is found, the keypair is returned
876	/// * `Err(anyhow::Error)` - If the corresponding public key doesn't exist
877	///   in the database or a database error occurred.
878	pub async fn get_vtxo_key(&self, vtxo: impl VtxoRef) -> anyhow::Result<Keypair> {
879		let bare_vtxo = match vtxo.as_bare_vtxo() {
880			Some(bare) => bare,
881			None => Cow::Owned(self.get_vtxo_by_id(vtxo.vtxo_id()).await?.vtxo),
882		};
883		let pubkey = self.find_signable_clause(&bare_vtxo).await
884			.context("VTXO is not signable by wallet")?
885			.pubkey();
886		let idx = self.inner.db.get_public_key_idx(&pubkey).await?
887			.context("VTXO key not found")?;
888		Ok(self.inner.seed.derive_vtxo_keypair(idx))
889	}
890
891	#[deprecated(note = "use peek_address instead")]
892	pub async fn peak_address(&self, index: u32) -> anyhow::Result<ark::Address> {
893		self.peek_address(index).await
894	}
895
896	/// Peek for an [ark::Address] at the given key index.
897	///
898	/// May return an error if the address at the given index has not been derived yet.
899	pub async fn peek_address(&self, index: u32) -> anyhow::Result<ark::Address> {
900		let properties = self.properties().await?;
901		let network = properties.network;
902		let keypair = self.peek_keypair(index).await?;
903		let mailbox = self.mailbox_identifier();
904
905
906		let (server_pubkey, mailbox_pubkey) =
907			if let (Some(spk), Some(mpk)) = (properties.server_pubkey, properties.server_mailbox_pubkey) {
908				(spk, mpk)
909			} else {
910				let (_, ark_info) = self.require_server().await?;
911				(ark_info.server_pubkey, ark_info.mailbox_pubkey)
912			};
913
914		Ok(ark::Address::builder()
915			.testnet(network != bitcoin::Network::Bitcoin)
916			.server_pubkey(server_pubkey)
917			.pubkey_policy(keypair.public_key())
918			.mailbox(mailbox_pubkey, mailbox, &keypair)
919			.expect("Failed to assign mailbox")
920			.into_address().unwrap())
921	}
922
923	/// Generate a new [ark::Address] and returns the index of the key used to create it.
924	///
925	/// This derives and stores the keypair directly after currently last revealed one.
926	pub async fn new_address_with_index(&self) -> anyhow::Result<(ark::Address, u32)> {
927		let (_, index) = self.derive_store_next_keypair().await?;
928		let addr = self.peek_address(index).await?;
929		Ok((addr, index))
930	}
931
932	/// Generate a new mailbox [ark::Address].
933	pub async fn new_address(&self) -> anyhow::Result<ark::Address> {
934		let (addr, _) = self.new_address_with_index().await?;
935		Ok(addr)
936	}
937
938	/// Create a new wallet without an optional onchain backend. This will restrict features such as
939	/// boarding and unilateral exit.
940	///
941	/// `lock_manager` coordinates access to the wallet's protected resources. Pick a backend
942	/// whose enforcement scope matches how the wallet is deployed — see [`crate::lock_manager`].
943	///
944	/// The `force` flag will allow you to create the wallet even if a connection to the Ark server
945	/// cannot be established, it will not overwrite a wallet which has already been created.
946	pub async fn create(
947		mnemonic: &Mnemonic,
948		network: Network,
949		config: Config,
950		db: Arc<dyn BarkPersister>,
951		lock_manager: Box<dyn LockManager>,
952		force: bool,
953	) -> anyhow::Result<Wallet> {
954		trace!("Config: {:?}", config);
955
956		let wallet_fingerprint = WalletSeed::new(network, &mnemonic.to_seed("")).fingerprint();
957
958		// Block concurrent creators against the same locking universe. A
959		// short timeout is fine: if a sibling process wins the race they
960		// will have committed the wallet by the time we'd time out, and
961		// the `read_properties` check below catches that case cleanly.
962		let create_guard = lock_manager.lock(
963			&format!("{}.create", wallet_fingerprint),
964			Duration::from_secs(5),
965		).await.context("wallet initialization already in progress")?;
966
967		if let Some(existing) = db.read_properties().await? {
968			trace!("Existing config: {:?}", existing);
969			bail!("cannot overwrite already existing config")
970		}
971
972		// Try to connect to the server and get its pubkey
973		let (server_pubkey, mailbox_pubkey) = if !force {
974			match Self::connect_to_server(&config, network).await {
975				Ok(conn) => {
976					let ark_info = conn.ark_info().await;
977					(Some(ark_info.server_pubkey), Some(ark_info.mailbox_pubkey))
978				}
979				Err(err) => {
980					bail!("Failed to connect to provided server (if you are sure use the --force flag): {:#}", err);
981				}
982			}
983		} else {
984			(None, None)
985		};
986
987		let properties = WalletProperties {
988			network,
989			fingerprint: wallet_fingerprint,
990			server_pubkey,
991			server_mailbox_pubkey: mailbox_pubkey,
992		};
993
994		// write the config to db
995		db.init_wallet(&properties).await.context("cannot init wallet in the database")?;
996		info!("Created wallet with fingerprint: {}", wallet_fingerprint);
997		if let Some(pk) = server_pubkey {
998			info!("Stored server pubkey: {}", pk);
999		}
1000
1001		// The wallet exists from this point on — drop the creation lock
1002		// so another process is free to open it.
1003		drop(create_guard);
1004
1005		// from then on we can open the wallet
1006		let wallet = Wallet::open(&mnemonic, db, config, lock_manager).await.context("failed to open wallet")?;
1007		wallet.require_chainsource_version().await?;
1008
1009		Ok(wallet)
1010	}
1011
1012	/// Create a new wallet with an onchain backend. This enables full Ark functionality. A default
1013	/// implementation of an onchain wallet when the `onchain-bdk` feature is enabled. See
1014	/// [onchain::OnchainWallet] for more details. Alternatively, implement [ExitUnilaterally] if
1015	/// you have your own onchain wallet implementation.
1016	///
1017	/// The `force` flag will allow you to create the wallet even if a connection to the Ark server
1018	/// cannot be established, it will not overwrite a wallet which has already been created.
1019	pub async fn create_with_onchain(
1020		mnemonic: &Mnemonic,
1021		network: Network,
1022		config: Config,
1023		db: Arc<dyn BarkPersister>,
1024		lock_manager: Box<dyn LockManager>,
1025		onchain: &dyn ExitUnilaterally,
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(onchain).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	/// Similar to [Wallet::open] however this also unilateral exits using the provided onchain
1093	/// wallet.
1094	pub async fn open_with_onchain(
1095		mnemonic: &Mnemonic,
1096		db: Arc<dyn BarkPersister>,
1097		onchain: &dyn ExitUnilaterally,
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(onchain).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 let Some(onchain) = onchain.as_ref() {
1117			let mut onchain = onchain.write().await;
1118			wallet.inner.exit.load(&mut *onchain).await?;
1119		}
1120
1121		wallet.start_daemon(onchain)?;
1122
1123		Ok(wallet)
1124	}
1125
1126	/// Returns the config used to create/load the bark [Wallet].
1127	pub fn config(&self) -> &Config {
1128		&self.inner.config
1129	}
1130
1131	/// Retrieves the [WalletProperties] of the current bark [Wallet].
1132	pub async fn properties(&self) -> anyhow::Result<WalletProperties> {
1133		let properties = self.inner.db.read_properties().await?.context("Wallet is not initialised")?;
1134		Ok(properties)
1135	}
1136
1137	/// Returns the fingerprint of the wallet.
1138	pub fn fingerprint(&self) -> Fingerprint {
1139		self.inner.seed.fingerprint()
1140	}
1141
1142	async fn connect_to_server(
1143		config: &Config,
1144		network: Network,
1145	) -> anyhow::Result<ServerConnection> {
1146		let mut builder = ServerConnection::builder()
1147			.address(&config.server_address)
1148			.network(network);
1149
1150		#[cfg(feature = "socks5-proxy")]
1151		if let Some(proxy) = proxy_for_url(&config.socks5_proxy, &config.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	/// 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		let rounds = self.progress_pending_rounds(None).await;
1465		if let Err(e) = rounds.as_ref() {
1466			warn!("Error progressing pending rounds: {:#}", e);
1467		}
1468		let refresh = self.maintenance_refresh().await;
1469		if let Err(e) = refresh.as_ref() {
1470			warn!("Error refreshing VTXOs: {:#}", e);
1471		}
1472		if rounds.is_err() || refresh.is_err() {
1473			bail!("Maintenance encountered errors.\nprogress_rounds: {:#?}\nrefresh: {:#?}", rounds, refresh);
1474		}
1475		Ok(())
1476	}
1477
1478	/// Performs maintenance tasks and schedules delegated refresh when needed. This risks spending
1479	/// users' funds because refreshing may cost fees.
1480	///
1481	/// This can take a long period of time due to syncing rounds, arkoors, checking pending
1482	/// payments, progressing pending rounds, and refreshing VTXOs if necessary.
1483	pub async fn maintenance_delegated(&self) -> anyhow::Result<()> {
1484		info!("Starting wallet maintenance in delegated mode");
1485		self.sync().await;
1486		let rounds = self.progress_pending_rounds(None).await;
1487		if let Err(e) = rounds.as_ref() {
1488			warn!("Error progressing pending rounds: {:#}", e);
1489		}
1490		let refresh = self.maybe_schedule_maintenance_refresh_delegated().await;
1491		if let Err(e) = refresh.as_ref() {
1492			warn!("Error refreshing VTXOs: {:#}", e);
1493		}
1494		if rounds.is_err() || refresh.is_err() {
1495			bail!("Delegated maintenance encountered errors.\nprogress_rounds: {:#?}\nrefresh: {:#?}", rounds, refresh);
1496		}
1497		Ok(())
1498	}
1499
1500	/// Performs maintenance tasks and performs refresh interactively until finished when needed.
1501	/// This risks spending users' funds because refreshing may cost fees and any pending exits will
1502	/// be progressed.
1503	///
1504	/// This can take a long period of time due to syncing the onchain wallet, registering boards,
1505	/// syncing rounds, arkoors, and the exit system, checking pending lightning payments and
1506	/// refreshing VTXOs if necessary.
1507	pub async fn maintenance_with_onchain<W: PreparePsbt + SignPsbt + ExitUnilaterally>(
1508		&self,
1509		onchain: &mut W,
1510	) -> anyhow::Result<()> {
1511		info!("Starting wallet maintenance in interactive mode with onchain wallet");
1512
1513		// Maintenance will log so we don't need to.
1514		let maintenance = self.maintenance().await;
1515
1516		// NB: order matters here, after syncing lightning, we might have new exits to start
1517		let exit_sync = self.sync_exits(onchain).await;
1518		if let Err(e) = exit_sync.as_ref() {
1519			warn!("Error syncing exits: {:#}", e);
1520		}
1521		let exit_progress = self.exit_mgr().progress_exits(&self, onchain, None).await;
1522		if let Err(e) = exit_progress.as_ref() {
1523			warn!("Error progressing exits: {:#}", e);
1524		}
1525		if maintenance.is_err() || exit_sync.is_err() || exit_progress.is_err() {
1526			bail!("Maintenance encountered errors.\nmaintenance: {:#?}\nexit_sync: {:#?}\nexit_progress: {:#?}", maintenance, exit_sync, exit_progress);
1527		}
1528		Ok(())
1529	}
1530
1531	/// Performs maintenance tasks and schedules delegated refresh when needed. This risks spending
1532	/// users' funds because refreshing may cost fees and any pending exits will be progressed.
1533	///
1534	/// This can take a long period of time due to syncing the onchain wallet, registering boards,
1535	/// syncing rounds, arkoors, and the exit system, checking pending lightning payments and
1536	/// refreshing VTXOs if necessary.
1537	pub async fn maintenance_with_onchain_delegated<W: PreparePsbt + SignPsbt + ExitUnilaterally>(
1538		&self,
1539		onchain: &mut W,
1540	) -> anyhow::Result<()> {
1541		info!("Starting wallet maintenance in delegated mode with onchain wallet");
1542
1543		// Maintenance will log so we don't need to.
1544		let maintenance = self.maintenance_delegated().await;
1545
1546		// NB: order matters here, after syncing lightning, we might have new exits to start
1547		let exit_sync = self.sync_exits(onchain).await;
1548		if let Err(e) = exit_sync.as_ref() {
1549			warn!("Error syncing exits: {:#}", e);
1550		}
1551		let exit_progress = self.exit_mgr().progress_exits(&self, onchain, None).await;
1552		if let Err(e) = exit_progress.as_ref() {
1553			warn!("Error progressing exits: {:#}", e);
1554		}
1555		if maintenance.is_err() || exit_sync.is_err() || exit_progress.is_err() {
1556			bail!("Delegated maintenance encountered errors.\nmaintenance: {:#?}\nexit_sync: {:#?}\nexit_progress: {:#?}", maintenance, exit_sync, exit_progress);
1557		}
1558		Ok(())
1559	}
1560
1561	/// Checks VTXOs that are due to be refreshed, and schedules an interactive refresh if any
1562	///
1563	/// This will include any VTXOs within the expiry threshold
1564	/// ([Config::vtxo_refresh_expiry_threshold]) or those which
1565	/// are uneconomical to exit due to onchain network conditions.
1566	///
1567	/// Returns a [RoundStateId] if a refresh is scheduled.
1568	pub async fn maybe_schedule_maintenance_refresh(&self) -> anyhow::Result<Option<RoundStateId>> {
1569		let vtxos = self.get_vtxos_to_refresh().await?;
1570		if vtxos.len() == 0 {
1571			return Ok(None);
1572		}
1573
1574		let participation = match self.build_refresh_participation(vtxos).await? {
1575			Some(participation) => participation,
1576			None => return Ok(None),
1577		};
1578
1579		info!("Scheduling maintenance refresh ({} vtxos)", participation.inputs.len());
1580		let state = self.join_next_round(participation, Some(RoundMovement::Refresh)).await?;
1581		Ok(Some(state.id()))
1582	}
1583
1584	/// Checks VTXOs that are due to be refreshed, and schedules a delegated refresh if any
1585	///
1586	/// This will include any VTXOs within the expiry threshold
1587	/// ([Config::vtxo_refresh_expiry_threshold]) or those which
1588	/// are uneconomical to exit due to onchain network conditions.
1589	///
1590	/// Returns a [RoundStateId] if a refresh is scheduled.
1591	pub async fn maybe_schedule_maintenance_refresh_delegated(
1592		&self,
1593	) -> anyhow::Result<Option<RoundStateId>> {
1594		let vtxos = self.get_vtxos_to_refresh().await?;
1595		if vtxos.len() == 0 {
1596			return Ok(None);
1597		}
1598
1599		let participation = match self.build_refresh_participation(vtxos).await? {
1600			Some(participation) => participation,
1601			None => return Ok(None),
1602		};
1603
1604		info!("Scheduling delegated maintenance refresh ({} vtxos)", participation.inputs.len());
1605		let state = self.join_next_round_delegated(participation, Some(RoundMovement::Refresh)).await?;
1606		Ok(Some(state.id()))
1607	}
1608
1609	/// Performs an interactive refresh of all VTXOs that are due to be refreshed, if any
1610	///
1611	/// This will include any VTXOs within the expiry threshold
1612	/// ([Config::vtxo_refresh_expiry_threshold]) or those which
1613	/// are uneconomical to exit due to onchain network conditions.
1614	///
1615	/// Returns a [RoundStatus] if a refresh occurs.
1616	pub async fn maintenance_refresh(&self) -> anyhow::Result<Option<RoundStatus>> {
1617		let vtxos = self.get_vtxos_to_refresh().await?;
1618		if vtxos.len() == 0 {
1619			return Ok(None);
1620		}
1621
1622		info!("Performing maintenance refresh");
1623		self.refresh_vtxos(vtxos).await
1624	}
1625
1626	/// Sync offchain wallet and update onchain fees. This is a much more lightweight alternative
1627	/// to [Wallet::maintenance] as it will not refresh VTXOs or sync the onchain wallet.
1628	///
1629	/// Notes:
1630	/// - The exit system will not be synced as doing so requires the onchain wallet.
1631	pub async fn sync(&self) {
1632		futures::join!(
1633			async {
1634				// NB: order matters here, if syncing call fails,
1635				// we still want to update the fee rates
1636				if let Err(e) = self.inner.chain.update_fee_rates(self.inner.config.fallback_fee_rate).await {
1637					warn!("Error updating fee rates: {:#}", e);
1638				}
1639			},
1640			async {
1641				if let Err(e) = self.sync_mailbox().await {
1642					warn!("Error in mailbox sync: {:#}", e);
1643				}
1644			},
1645			async {
1646				if let Err(e) = self.sync_pending_rounds().await {
1647					warn!("Error while trying to progress rounds awaiting confirmations: {:#}", e);
1648				}
1649			},
1650			async {
1651				if let Err(e) = self.sync_pending_lightning_send_vtxos().await {
1652					warn!("Error syncing pending lightning payments: {:#}", e);
1653				}
1654			},
1655			async {
1656				if let Err(e) = self.try_claim_all_lightning_receives(false).await {
1657					warn!("Error claiming pending lightning receives: {:#}", e);
1658				}
1659			},
1660			async {
1661				if let Err(e) = self.sync_pending_boards().await {
1662					warn!("Error syncing pending boards: {:#}", e);
1663				}
1664			},
1665			async {
1666				if let Err(e) = self.sync_pending_offboards().await {
1667					warn!("Error syncing pending offboards: {:#}", e);
1668				}
1669			}
1670		);
1671	}
1672
1673	/// Sync the transaction status of unilateral exits
1674	///
1675	/// This will not progress the unilateral exits in any way, it will merely check the
1676	/// transaction status of each transaction as well as check whether any exits have become
1677	/// claimable or have been claimed.
1678	pub async fn sync_exits(
1679		&self,
1680		onchain: &mut dyn ExitUnilaterally,
1681	) -> anyhow::Result<()> {
1682		self.exit_mgr().sync(&self, onchain).await?;
1683		Ok(())
1684	}
1685
1686	/// Drop a specific [Vtxo] from the database. This is destructive and will result in a loss of
1687	/// funds.
1688	pub async fn dangerous_drop_vtxo(&self, vtxo_id: VtxoId) -> anyhow::Result<()> {
1689		warn!("Drop vtxo {} from the database", vtxo_id);
1690		self.inner.db.remove_vtxo(vtxo_id).await?;
1691		Ok(())
1692	}
1693
1694	/// Drop all VTXOs from the database. This is destructive and will result in a loss of funds.
1695	//TODO(stevenroose) improve the way we expose dangerous methods
1696	pub async fn dangerous_drop_all_vtxos(&self) -> anyhow::Result<()> {
1697		warn!("Dropping all vtxos from the db...");
1698		for vtxo in self.vtxos().await? {
1699			self.inner.db.remove_vtxo(vtxo.id()).await?;
1700		}
1701
1702		self.exit_mgr().dangerous_clear_exit().await?;
1703		Ok(())
1704	}
1705
1706	/// Checks if the provided VTXO has some counterparty risk in the current wallet.
1707	///
1708	/// An arkoor vtxo is considered to have some counterparty risk if it is
1709	/// (directly or not) based on round VTXOs that aren't owned by the
1710	/// wallet. The check inspects the genesis chain, so this takes a full
1711	/// VTXO; callers working from a bare listing should hydrate via
1712	/// [Wallet::get_full_vtxo] or [BarkPersister::get_full_vtxos] first.
1713	async fn has_counterparty_risk(&self, vtxo: &Vtxo<Full>) -> anyhow::Result<bool> {
1714		for past_pks in vtxo.past_arkoor_pubkeys() {
1715			let mut owns_any = false;
1716			for past_pk in past_pks {
1717				if self.inner.db.get_public_key_idx(&past_pk).await?.is_some() {
1718					owns_any = true;
1719					break;
1720				}
1721			}
1722			if !owns_any {
1723				return Ok(true);
1724			}
1725		}
1726
1727		let my_clause = self.find_signable_clause(vtxo).await;
1728		Ok(!my_clause.is_some())
1729	}
1730
1731	/// If there are any VTXOs that match the "must-refresh" and "should-refresh" criteria with a
1732	/// total value over the P2TR dust limit, they are added to the round participation and an
1733	/// additional output is also created.
1734	///
1735	/// Note: This assumes that the base refresh fee has already been paid.
1736	async fn add_should_refresh_vtxos(
1737		&self,
1738		participation: &mut RoundParticipation,
1739	) -> anyhow::Result<()> {
1740		// Get VTXOs that need and should be refreshed, then filter out any duplicates before
1741		// adjusting the round participation.
1742		let tip = self.inner.chain.tip().await?;
1743		let mut vtxos_to_refresh = self.spendable_vtxos_with(
1744			&RefreshStrategy::should_refresh(self, tip, self.inner.chain.fee_rates().await.fast),
1745		).await?;
1746		if vtxos_to_refresh.is_empty() {
1747			return Ok(());
1748		}
1749
1750		let excluded_ids = participation.inputs.iter().map(|v| v.vtxo_id())
1751			.collect::<HashSet<_>>();
1752		let mut total_amount = Amount::ZERO;
1753		for i in (0..vtxos_to_refresh.len()).rev() {
1754			let vtxo = &vtxos_to_refresh[i];
1755			if excluded_ids.contains(&vtxo.id()) {
1756				vtxos_to_refresh.swap_remove(i);
1757				continue;
1758			}
1759			total_amount += vtxo.amount();
1760		}
1761		if vtxos_to_refresh.is_empty() {
1762			// VTXOs are already included in the round participation.
1763			return Ok(());
1764		}
1765
1766		// We need to verify that the output we add won't end up below the dust limit when fees are
1767		// applied. We can assume the base fee has been paid by the current refresh participation.
1768		let (_, ark_info) = self.require_server().await?;
1769		let fee = ark_info.fees.refresh.calculate_no_base_fee(
1770			vtxos_to_refresh.iter().map(|wv| VtxoFeeInfo::from_vtxo_and_tip(&wv.vtxo, tip)),
1771		).context("fee overflowed")?;
1772
1773		// Only add these VTXOs if the output amount would be above dust after fees.
1774		let output_amount = match validate_and_subtract_fee_min_dust(total_amount, fee) {
1775			Ok(amount) => amount,
1776			Err(e) => {
1777				trace!("Cannot add should-refresh VTXOs: {}", e);
1778				return Ok(());
1779			},
1780		};
1781		info!(
1782			"Adding {} extra VTXOs to round participation total = {}, fee = {}, output = {}",
1783			vtxos_to_refresh.len(), total_amount, fee, output_amount,
1784		);
1785		let (user_keypair, _) = self.derive_store_next_keypair().await?;
1786		let req = VtxoRequest {
1787			policy: VtxoPolicy::new_pubkey(user_keypair.public_key()),
1788			amount: output_amount,
1789		};
1790		let extra_ids = vtxos_to_refresh.into_iter().map(|wv| wv.id()).collect::<Vec<_>>();
1791		let extra_full = self.inner.db.get_full_vtxos(&extra_ids).await
1792			.context("failed to hydrate refresh candidates")?;
1793		participation.inputs.reserve(extra_full.len());
1794		participation.inputs.extend(extra_full);
1795		participation.outputs.push(req);
1796
1797		Ok(())
1798	}
1799
1800	pub async fn build_refresh_participation<V: VtxoRef>(
1801		&self,
1802		vtxos: impl IntoIterator<Item = V>,
1803	) -> anyhow::Result<Option<RoundParticipation>> {
1804		let (vtxos, total_amount) = {
1805			let iter = vtxos.into_iter();
1806			let size_hint = iter.size_hint();
1807			let mut vtxos = Vec::<Vtxo<Full>>::with_capacity(size_hint.1.unwrap_or(size_hint.0));
1808			let mut amount = Amount::ZERO;
1809			for vref in iter {
1810				// We use a Vec here instead of a HashMap or a HashSet of IDs because for the kinds
1811				// of elements we expect to deal with, a Vec is likely to be quicker. The overhead
1812				// of hashing each ID and making additional allocations isn't likely to be worth it
1813				// for what is likely to be a handful of VTXOs or at most a couple of hundred.
1814				let id = vref.vtxo_id();
1815				if vtxos.iter().any(|v| v.id() == id) {
1816					bail!("duplicate VTXO id: {}", id);
1817				}
1818				let vtxo = if let Some(vtxo) = vref.into_full_vtxo() {
1819					vtxo
1820				} else {
1821					// Listings/selection return bare wallet vtxos; the round
1822					// flow needs the full chain to forfeit and register.
1823					self.inner.db.get_full_vtxo(id).await?
1824						.with_context(|| format!("vtxo with id {} not found", id))?
1825				};
1826				amount += vtxo.amount();
1827				vtxos.push(vtxo);
1828			}
1829			(vtxos, amount)
1830		};
1831
1832		if vtxos.is_empty() {
1833			info!("Skipping refresh since no VTXOs are provided.");
1834			return Ok(None);
1835		}
1836		ensure!(total_amount >= P2TR_DUST,
1837			"vtxo amount must be at least {} to participate in a round",
1838			P2TR_DUST,
1839		);
1840
1841		// Calculate refresh fees
1842		let (_, ark_info) = self.require_server().await?;
1843		let current_height = self.inner.chain.tip().await?;
1844		let vtxo_fee_infos = vtxos.iter()
1845			.map(|v| VtxoFeeInfo::from_vtxo_and_tip(v, current_height));
1846		let fee = ark_info.fees.refresh.calculate(vtxo_fee_infos).context("fee overflowed")?;
1847		let output_amount = validate_and_subtract_fee_min_dust(total_amount, fee)?;
1848
1849		info!("Refreshing {} VTXOs (total amount = {}, fee = {}, output = {}).",
1850			vtxos.len(), total_amount, fee, output_amount,
1851		);
1852		let (user_keypair, _) = self.derive_store_next_keypair().await?;
1853		let req = VtxoRequest {
1854			policy: VtxoPolicy::Pubkey(PubkeyVtxoPolicy { user_pubkey: user_keypair.public_key() }),
1855			amount: output_amount,
1856		};
1857
1858		Ok(Some(RoundParticipation {
1859			inputs: vtxos,
1860			outputs: vec![req],
1861			unblinded_mailbox_id: None,
1862		}))
1863	}
1864
1865	/// This will refresh all provided VTXOs in an interactive round and wait until end
1866	///
1867	/// Returns the [RoundStatus] of the round if a successful refresh occurred.
1868	/// It will return [None] if no [Vtxo] needed to be refreshed.
1869	pub async fn refresh_vtxos<V: VtxoRef>(
1870		&self,
1871		vtxos: impl IntoIterator<Item = V>,
1872	) -> anyhow::Result<Option<RoundStatus>> {
1873		let mut participation = match self.build_refresh_participation(vtxos).await? {
1874			Some(participation) => participation,
1875			None => return Ok(None),
1876		};
1877
1878		if let Err(e) = self.add_should_refresh_vtxos(&mut participation).await {
1879			warn!("Error trying to add additional VTXOs that should be refreshed: {:#}", e);
1880		}
1881
1882		Ok(Some(self.participate_round(participation, Some(RoundMovement::Refresh)).await?))
1883	}
1884
1885	/// This will refresh all provided VTXOs in delegated (non-interactive) mode
1886	///
1887	/// Returns the [StoredRoundState] which can be used to track the round's
1888	/// progress later by calling sync. It will return [None] if no [Vtxo]
1889	/// needed to be refreshed.
1890	pub async fn refresh_vtxos_delegated<V: VtxoRef>(
1891		&self,
1892		vtxos: impl IntoIterator<Item = V>,
1893	) -> anyhow::Result<Option<StoredRoundState<Unlocked>>> {
1894		let mut part = match self.build_refresh_participation(vtxos).await? {
1895			Some(participation) => participation,
1896			None => return Ok(None),
1897		};
1898
1899		if let Err(e) = self.add_should_refresh_vtxos(&mut part).await {
1900			warn!("Error trying to add additional VTXOs that should be refreshed: {:#}", e);
1901		}
1902
1903		Ok(Some(self.join_next_round_delegated(part, Some(RoundMovement::Refresh)).await?))
1904	}
1905
1906	/// This will find all VTXOs that meets must-refresh criteria. Then, if there are some VTXOs to
1907	/// refresh, it will also add those that meet should-refresh criteria.
1908	pub async fn get_vtxos_to_refresh(&self) -> anyhow::Result<Vec<WalletVtxo>> {
1909		let vtxos = self.spendable_vtxos_with(&RefreshStrategy::should_refresh_if_must(
1910			self,
1911			self.inner.chain.tip().await?,
1912			self.inner.chain.fee_rates().await.fast,
1913		)).await?;
1914		Ok(vtxos)
1915	}
1916
1917	/// Returns the block height at which the first VTXO will expire
1918	pub async fn get_first_expiring_vtxo_blockheight(
1919		&self,
1920	) -> anyhow::Result<Option<BlockHeight>> {
1921		Ok(self.spendable_vtxos().await?.iter().map(|v| v.expiry_height()).min())
1922	}
1923
1924	/// Returns the next block height at which we have a VTXO that we
1925	/// want to refresh
1926	pub async fn get_next_required_refresh_blockheight(
1927		&self,
1928	) -> anyhow::Result<Option<BlockHeight>> {
1929		let first_expiry = self.get_first_expiring_vtxo_blockheight().await?;
1930		Ok(first_expiry.map(|h| h.saturating_sub(self.inner.config.vtxo_refresh_expiry_threshold)))
1931	}
1932
1933	/// Select several VTXOs to cover the provided amount
1934	///
1935	/// VTXOs are selected soonest-expiring-first.
1936	///
1937	/// Returns an error if amount cannot be reached.
1938	async fn select_vtxos_to_cover(
1939		&self,
1940		amount: Amount,
1941	) -> anyhow::Result<Vec<WalletVtxo>> {
1942		let mut vtxos = self.spendable_vtxos().await?;
1943		self.sort_vtxos_for_selection(&mut vtxos);
1944
1945		let (last, _total_amount) = self.select_vtxos_inner(amount, &vtxos)?;
1946		vtxos.truncate(last+1);
1947		Ok(vtxos)
1948	}
1949
1950	/// Determines which VTXOs to use for a fee-paying transaction where the fee is added on top of
1951	/// the desired amount. E.g., a lightning payment, a send-onchain payment.
1952	///
1953	/// Returns a collection of VTXOs capable of covering the desired amount as well as the
1954	/// calculated fee.
1955	async fn select_vtxos_to_cover_with_fee<F>(
1956		&self,
1957		amount: Amount,
1958		calc_fee: F,
1959	) -> anyhow::Result<(Vec<WalletVtxo>, Amount)>
1960	where
1961		F: for<'a> Fn(
1962			Amount, std::iter::Copied<std::slice::Iter<'a, VtxoFeeInfo>>,
1963		) -> anyhow::Result<Amount>,
1964	{
1965		let tip = self.inner.chain.tip().await?;
1966		let mut vtxos = self.spendable_vtxos().await?;
1967		self.sort_vtxos_for_selection(&mut vtxos);
1968
1969		let fee_info = vtxos.iter()
1970			.map(|v| VtxoFeeInfo::from_vtxo_and_tip(v, tip))
1971			.collect::<Vec<_>>();
1972
1973		// We need to loop to find suitable inputs due to the VTXOs having a direct impact on
1974		// how much we must pay in fees.
1975		const MAX_ITERATIONS: usize = 100;
1976		let mut fee = Amount::ZERO;
1977		for _ in 0..MAX_ITERATIONS {
1978			let required = amount.checked_add(fee)
1979				.context("Amount + fee overflow")?;
1980
1981			let (last, vtxo_amount) = self.select_vtxos_inner(required, &vtxos)
1982				.context("Could not find enough suitable VTXOs to cover payment + fees")?;
1983			fee = calc_fee(amount, fee_info[..=last].iter().copied())?;
1984
1985			if amount + fee <= vtxo_amount {
1986				trace!("Selected vtxos to cover amount + fee: amount = {}, fee = {}, total inputs = {}",
1987					amount, fee, vtxo_amount,
1988				);
1989				vtxos.truncate(last+1);
1990				return Ok((vtxos, fee));
1991			}
1992			trace!("VTXO sum of {} did not exceed amount {} and fee {}, iterating again",
1993				vtxo_amount, amount, fee,
1994			);
1995		}
1996		bail!("Fee calculation did not converge after maximum iterations")
1997	}
1998
1999	/// Sorts the given `vtxos` in place ready for selection to cover funds.
2000	fn sort_vtxos_for_selection(&self, vtxos: &mut Vec<WalletVtxo>) {
2001		vtxos.sort_by_key(|v| v.expiry_height());
2002	}
2003
2004	/// Iterates through the given `Vec` until either the given `amount` can be covered for a
2005	/// payment or until the `Vec` is exhausted, at which point an error will be returned.
2006	///
2007	/// Returns the index of the last VTXO included in the selection, as well as the total amount of
2008	/// the selected VTXOs.
2009	fn select_vtxos_inner(
2010		&self,
2011		amount: Amount,
2012		vtxos: &Vec<WalletVtxo>,
2013	) -> anyhow::Result<(usize, Amount)> {
2014		// Iterate over VTXOs until the required amount is reached
2015		let mut total_amount = Amount::ZERO;
2016		for (i, vtxo) in vtxos.iter().enumerate() {
2017			total_amount += vtxo.amount();
2018
2019			if total_amount >= amount {
2020				return Ok((i, total_amount))
2021			}
2022		}
2023
2024		bail!("Insufficient money available. Needed {} but {} is available",
2025			amount, total_amount,
2026		);
2027	}
2028
2029	/// Starts a daemon for the wallet.
2030	///
2031	/// Note:
2032	/// - This function doesn't check if a daemon is already running,
2033	/// so it's possible to start multiple daemons by mistake.
2034	pub fn start_daemon(
2035		&self,
2036		onchain: Option<Arc<tokio::sync::RwLock<dyn DaemonizableOnchainWallet>>>,
2037	) -> anyhow::Result<()> {
2038		let mut daemon = self.inner.daemon.lock();
2039		if daemon.is_some() {
2040			warn!("Called Wallet::start_daemon while daemon was already running.");
2041			return Ok(());
2042		}
2043
2044		// NB currently can't error but it's a pretty common method and quite likely that error
2045		// cases will be introduces later
2046		let handle = crate::daemon::start_daemon(self.clone(), onchain);
2047		let _ = daemon.insert(handle);
2048
2049		Ok(())
2050	}
2051
2052	/// Use [Wallet::start_daemon] instead.
2053	#[deprecated(since = "0.1.4", note = "use start_daemon instead")]
2054	pub fn run_daemon(
2055		&self,
2056		onchain: Option<Arc<tokio::sync::RwLock<dyn DaemonizableOnchainWallet>>>,
2057	) -> anyhow::Result<()> {
2058		self.start_daemon(onchain)
2059	}
2060
2061	/// Stops the daemon for the wallet if it is running, otherwise does nothing.
2062	pub fn stop_daemon(&self) {
2063		let mut daemon = self.inner.daemon.lock();
2064		if let Some(handle) = daemon.take() {
2065			handle.stop();
2066		}
2067	}
2068
2069	/// Registers the signed transaction chains for the given VTXOs with the
2070	/// server. This must be called before spending VTXOs so the server can
2071	/// publish forfeits if needed.
2072	pub async fn register_vtxo_transactions_with_server(
2073		&self,
2074		vtxos: &[impl AsRef<Vtxo<Full>>],
2075	) -> anyhow::Result<()> {
2076		if vtxos.is_empty() {
2077			return Ok(());
2078		}
2079
2080		let (mut srv, _) = self.require_server().await?;
2081		srv.client.register_vtxo_transactions(protos::RegisterVtxoTransactionsRequest {
2082			vtxos: vtxos.iter().map(|v| v.as_ref().serialize()).collect(),
2083		}).await.context("failed to register vtxo transactions")?;
2084
2085		Ok(())
2086	}
2087}
2088
2089fn wrap_server_connect_error(err: ConnectError) -> anyhow::Error {
2090	match err {
2091		ConnectError::CreateEndpoint(CreateEndpointError::NoTransportBackend) => {
2092			anyhow!(MISSING_SERVER_TRANSPORT_HELP)
2093		},
2094		other => anyhow::Error::from(other),
2095	}
2096}
2097
2098impl std::ops::Drop for WalletInner {
2099	fn drop(&mut self) {
2100		if let Some(handle) = self.daemon.lock().take() {
2101			handle.stop();
2102		}
2103	}
2104}
2105
2106#[cfg(test)]
2107mod tests {
2108	use server_rpc::client::CreateEndpointError;
2109
2110	use super::{wrap_server_connect_error, MISSING_SERVER_TRANSPORT_HELP};
2111
2112	#[test]
2113	fn no_transport_connect_error_is_reworded_for_wallet_users() {
2114		let err = wrap_server_connect_error(CreateEndpointError::NoTransportBackend.into());
2115		assert!(err.to_string().contains(MISSING_SERVER_TRANSPORT_HELP));
2116		assert!(err.to_string().contains("feature `bark-wallet/native` or `bark-wallet/wasm-web`"));
2117	}
2118}