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