#[cfg(all(any(target_os = "android", target_os = "ios"), feature = "tls-native-roots"))]
compile_error!("feature `tls-native-roots` can't be used on Android or iOS, use `tls-webpki-roots` instead");
pub extern crate ark;
pub extern crate bip39;
pub extern crate lightning_invoice;
pub extern crate lnurl as lnurllib;
#[macro_use] extern crate anyhow;
#[macro_use] extern crate async_trait;
#[macro_use] extern crate serde;
pub mod actions;
pub mod chain;
pub mod exit;
pub mod movement;
pub mod onchain;
pub mod payment_request;
pub mod persist;
pub mod round;
pub mod subsystem;
pub mod vtxo;
pub mod lock_manager;
mod arkoor;
mod board;
mod config;
mod daemon;
mod fees;
mod lightning;
mod mailbox;
mod notification;
mod offboard;
#[cfg(feature = "socks5-proxy")]
mod proxy;
mod psbtext;
mod utils;
pub use self::arkoor::{ArkoorCreateResult, ArkoorAddressError};
pub use self::config::{BarkNetwork, Config};
pub use self::daemon::DaemonHandle;
pub use self::fees::FeeEstimate;
pub use self::notification::{WalletNotification, NotificationStream};
pub use self::vtxo::WalletVtxo;
pub use self::utils::time;
use std::borrow::Cow;
use std::collections::HashSet;
use std::iter;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
use anyhow::{bail, Context};
use bip39::Mnemonic;
use bitcoin::{Amount, Network, OutPoint};
use bitcoin::bip32::{self, ChildNumber, Fingerprint};
use bitcoin::secp256k1::{self, Keypair, PublicKey};
use futures::stream::FuturesUnordered;
use log::{debug, error, info, trace, warn};
use tokio_stream::StreamExt;
use ark::{ArkInfo, ProtocolEncoding, Vtxo, VtxoId, VtxoPolicy, VtxoRequest};
use ark::address::VtxoDelivery;
use ark::fees::{validate_and_subtract_fee_min_dust, VtxoFeeInfo};
use ark::rounds::{RoundAttempt, RoundEvent};
use ark::vtxo::{Full, PubkeyVtxoPolicy, VtxoRef};
use ark::vtxo::policy::signing::VtxoSigner;
use bitcoin_ext::{BlockHeight, P2TR_DUST, TxStatus};
use server_rpc::{protos, ServerConnection};
use server_rpc::client::{ConnectError, CreateEndpointError};
use crate::chain::{ChainSource, ChainSourceSpec};
use crate::exit::Exit;
use crate::lock_manager::LockManager;
use crate::movement::{Movement, MovementId, PaymentMethod};
use crate::movement::manager::MovementManager;
use crate::notification::NotificationDispatch;
use crate::onchain::{ExitUnilaterally, PreparePsbt, SignPsbt, Utxo};
use crate::onchain::DaemonizableOnchainWallet;
use crate::persist::BarkPersister;
use crate::persist::models::{RoundStateId, StoredRoundState, Unlocked};
#[cfg(feature = "socks5-proxy")]
use crate::proxy::proxy_for_url;
use crate::round::{RoundParticipation, RoundSecretNonces, RoundStatus};
use crate::subsystem::RoundMovement;
use crate::utils::rejected_vtxos_from_error;
use crate::vtxo::{FilterVtxos, RefreshStrategy, VtxoFilter, VtxoStateKind};
#[cfg(all(feature = "wasm-web", feature = "socks5-proxy"))]
compile_error!("features `wasm-web` does not support feature `socks5-proxy");
#[cfg(all(feature = "wasm-web", feature = "bitcoind-rpc"))]
compile_error!("`wasm-web` does not support the `bitcoind-rpc` feature");
const BARK_PURPOSE_INDEX: u32 = 350;
const VTXO_KEYS_INDEX: u32 = 0;
const MAILBOX_KEY_INDEX: u32 = 1;
const RECOVERY_MAILBOX_KEY_INDEX: u32 = 2;
const MISSING_SERVER_TRANSPORT_HELP: &str =
"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.";
const SUBSCRIBE_REQUEST_TIMEOUT: Duration = Duration::from_secs(60 * 60);
lazy_static::lazy_static! {
static ref SECP: secp256k1::Secp256k1<secp256k1::All> = secp256k1::Secp256k1::new();
}
fn log_server_pubkey_changed_error(expected: PublicKey, got: PublicKey) {
error!(
"
Server public key has changed!
The Ark server's public key is different from the one stored when this
wallet was created. This typically happens when:
- The server operator has rotated their keys
- You are connecting to a different server
- The server has been replaced
For safety, this wallet will not connect to the server until you
resolve this. You can recover your funds on-chain by doing an emergency exit.
This will exit your VTXOs to on-chain Bitcoin without needing the server's cooperation.
Expected: {expected}
Got: {got}")
}
fn log_server_mailbox_pubkey_changed_error(expected: PublicKey, got: PublicKey) {
error!(
"
Server mailbox public key has changed!
The Ark server's mailbox public key is different from the one stored when this
wallet was created. This typically happens when:
- The server operator has rotated their keys
- You are connecting to a different server
- The server has been replaced
For safety, this wallet will not connect to the server until you resolve this.
Unlike a server pubkey change, your VTXOs are not at risk - the mailbox pubkey
only affects address receive semantics. Any Ark addresses you previously
shared will stop receiving new payments; you will need to share new addresses
after reconnecting.
Expected: {expected}
Got: {got}")
}
#[derive(Debug, Clone)]
pub struct LightningReceiveBalance {
pub total: Amount,
pub claimable: Amount,
}
#[derive(Debug, Clone)]
pub struct Balance {
pub spendable: Amount,
pub pending_lightning_send: Amount,
pub claimable_lightning_receive: Amount,
pub pending_in_round: Amount,
pub pending_exit: Option<Amount>,
pub pending_board: Amount,
}
pub struct UtxoInfo {
pub outpoint: OutPoint,
pub amount: Amount,
pub confirmation_height: Option<u32>,
}
impl From<Utxo> for UtxoInfo {
fn from(value: Utxo) -> Self {
match value {
Utxo::Local(o) => UtxoInfo {
outpoint: o.outpoint,
amount: o.amount,
confirmation_height: o.confirmation_height,
},
Utxo::Exit(e) => UtxoInfo {
outpoint: e.vtxo.point(),
amount: e.vtxo.amount(),
confirmation_height: Some(e.height),
},
}
}
}
pub struct OffchainBalance {
pub available: Amount,
pub pending_in_round: Amount,
pub pending_exit: Amount,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct WalletProperties {
pub network: Network,
pub fingerprint: Fingerprint,
pub server_pubkey: Option<PublicKey>,
pub server_mailbox_pubkey: Option<PublicKey>,
}
pub struct WalletSeed {
master: bip32::Xpriv,
vtxo: bip32::Xpriv,
}
impl WalletSeed {
pub fn new_from_seed(network: Network, seed: &[u8; 64]) -> Self {
let bark_path = [ChildNumber::from_hardened_idx(BARK_PURPOSE_INDEX).unwrap()];
let master = bip32::Xpriv::new_master(network, seed)
.expect("invalid seed")
.derive_priv(&SECP, &bark_path)
.expect("purpose is valid");
let vtxo_path = [ChildNumber::from_hardened_idx(VTXO_KEYS_INDEX).unwrap()];
let vtxo = master.derive_priv(&SECP, &vtxo_path)
.expect("vtxo path is valid");
Self { master, vtxo }
}
pub fn new_from_mnemonic(network: Network, mnemonic: &Mnemonic) -> Self {
Self::new_from_seed(network, &mnemonic.to_seed(""))
}
pub fn fingerprint(&self) -> Fingerprint {
self.master.fingerprint(&SECP)
}
fn derive_vtxo_keypair(&self, idx: u32) -> Keypair {
self.vtxo.derive_priv(&SECP, &[idx.into()]).unwrap().to_keypair(&SECP)
}
fn to_mailbox_keypair(&self) -> Keypair {
let mailbox_path = [ChildNumber::from_hardened_idx(MAILBOX_KEY_INDEX).unwrap()];
self.master.derive_priv(&SECP, &mailbox_path).unwrap().to_keypair(&SECP)
}
fn to_recovery_mailbox_keypair(&self) -> Keypair {
let mailbox_path = [ChildNumber::from_hardened_idx(RECOVERY_MAILBOX_KEY_INDEX).unwrap()];
self.master.derive_priv(&SECP, &mailbox_path).unwrap().to_keypair(&SECP)
}
}
pub struct OpenWalletArgs {
pub run_daemon: bool,
pub datadir: Option<PathBuf>,
pub persister: Option<Arc<dyn BarkPersister>>,
pub lock_manager: Option<Box<dyn LockManager>>,
pub onchain: Option<Arc<tokio::sync::RwLock<dyn DaemonizableOnchainWallet>>>,
pub create_if_not_exists: bool,
pub create_without_server: bool,
}
impl Default for OpenWalletArgs {
fn default() -> Self {
Self {
run_daemon: true,
onchain: None,
datadir: None,
persister: None,
lock_manager: None,
create_if_not_exists: true,
create_without_server: false,
}
}
}
struct WalletInner {
chain: Arc<ChainSource>,
exit: Exit,
movements: Arc<MovementManager>,
notifications: NotificationDispatch,
config: Config,
db: Arc<dyn BarkPersister>,
lock_manager: Box<dyn LockManager>,
seed: WalletSeed,
server: tokio::sync::OnceCell<ServerConnection>,
daemon: parking_lot::Mutex<Option<DaemonHandle>>,
last_force_exit_scan_tip: tokio::sync::Mutex<Option<BlockHeight>>,
pub(crate) round_secret_nonces: RoundSecretNonces,
}
#[derive(Clone)]
pub struct Wallet {
inner: Arc<WalletInner>,
}
impl Wallet {
pub async fn network(&self) -> anyhow::Result<Network> {
Ok(self.properties().await?.network)
}
pub fn chain(&self) -> &Arc<ChainSource> {
&self.inner.chain
}
pub fn exit_mgr(&self) -> &Exit {
&self.inner.exit
}
pub fn movements_mgr(&self) -> &MovementManager {
&self.inner.movements
}
pub async fn peek_next_keypair(&self) -> anyhow::Result<(Keypair, u32)> {
let last_revealed = self.inner.db.get_last_vtxo_key_index().await?;
let index = last_revealed.map(|i| i + 1).unwrap_or(u32::MIN);
let keypair = self.inner.seed.derive_vtxo_keypair(index);
Ok((keypair, index))
}
pub async fn derive_store_next_keypair(&self) -> anyhow::Result<(Keypair, u32)> {
let (keypair, index) = self.peek_next_keypair().await?;
self.inner.db.store_vtxo_key(index, keypair.public_key()).await?;
Ok((keypair, index))
}
#[deprecated(note = "use peek_keypair instead")]
pub async fn peak_keypair(&self, index: u32) -> anyhow::Result<Keypair> {
self.peek_keypair(index).await
}
pub async fn peek_keypair(&self, index: u32) -> anyhow::Result<Keypair> {
let keypair = self.inner.seed.derive_vtxo_keypair(index);
if self.inner.db.get_public_key_idx(&keypair.public_key()).await?.is_some() {
Ok(keypair)
} else {
bail!("VTXO key {} does not exist, please derive it first", index)
}
}
pub async fn pubkey_keypair(&self, public_key: &PublicKey) -> anyhow::Result<Option<(u32, Keypair)>> {
if let Some(index) = self.inner.db.get_public_key_idx(&public_key).await? {
Ok(Some((index, self.inner.seed.derive_vtxo_keypair(index))))
} else {
Ok(None)
}
}
pub async fn get_vtxo_key(&self, vtxo: impl VtxoRef) -> anyhow::Result<Keypair> {
let bare_vtxo = match vtxo.as_bare_vtxo() {
Some(bare) => bare,
None => Cow::Owned(self.get_vtxo_by_id(vtxo.vtxo_id()).await?.vtxo),
};
let pubkey = self.find_signable_clause(&bare_vtxo).await
.context("VTXO is not signable by wallet")?
.pubkey();
let idx = self.inner.db.get_public_key_idx(&pubkey).await?
.context("VTXO key not found")?;
Ok(self.inner.seed.derive_vtxo_keypair(idx))
}
#[deprecated(note = "use peek_address instead")]
pub async fn peak_address(&self, index: u32) -> anyhow::Result<ark::Address> {
self.peek_address(index).await
}
pub async fn peek_address(&self, index: u32) -> anyhow::Result<ark::Address> {
let properties = self.properties().await?;
let network = properties.network;
let keypair = self.peek_keypair(index).await?;
let mailbox = self.mailbox_identifier();
let (server_pubkey, mailbox_pubkey) =
if let (Some(spk), Some(mpk)) = (properties.server_pubkey, properties.server_mailbox_pubkey) {
(spk, mpk)
} else {
let (_, ark_info) = self.require_server().await?;
(ark_info.server_pubkey, ark_info.mailbox_pubkey)
};
Ok(ark::Address::builder()
.testnet(network != bitcoin::Network::Bitcoin)
.server_pubkey(server_pubkey)
.pubkey_policy(keypair.public_key())
.mailbox(mailbox_pubkey, mailbox, &keypair)
.expect("Failed to assign mailbox")
.into_address().unwrap())
}
pub async fn new_address_with_index(&self) -> anyhow::Result<(ark::Address, u32)> {
let (_, index) = self.derive_store_next_keypair().await?;
let addr = self.peek_address(index).await?;
Ok((addr, index))
}
pub async fn new_address(&self) -> anyhow::Result<ark::Address> {
let (addr, _) = self.new_address_with_index().await?;
Ok(addr)
}
pub async fn create(
network: Network,
seed: &WalletSeed,
config: &Config,
db: &dyn BarkPersister,
lock_manager: &dyn LockManager,
allow_unreachable_server: bool,
) -> anyhow::Result<()> {
trace!("Config: {:?}", config);
let wallet_fingerprint = seed.fingerprint();
let create_guard = lock_manager.lock(
&format!("{}.create", wallet_fingerprint),
Duration::from_secs(5),
).await.context("wallet initialization already in progress")?;
if let Some(existing) = db.read_properties().await? {
trace!("Existing config: {:?}", existing);
bail!("cannot overwrite already existing config")
}
let (server_pubkey, mailbox_pubkey) = match Self::connect_to_server(&config, network).await {
Ok(conn) => {
let ark_info = conn.ark_info().await;
(Some(ark_info.server_pubkey), Some(ark_info.mailbox_pubkey))
},
Err(_) if allow_unreachable_server => (None, None),
Err(err) => {
bail!("Failed to connect to provided server: {:#}", err);
},
};
let properties = WalletProperties {
network,
fingerprint: wallet_fingerprint,
server_pubkey,
server_mailbox_pubkey: mailbox_pubkey,
};
db.init_wallet(&properties).await.context("cannot init wallet in the database")?;
info!("Created wallet with fingerprint: {}", wallet_fingerprint);
if let Some(pk) = server_pubkey {
info!("Stored server pubkey: {}", pk);
}
drop(create_guard);
Ok(())
}
pub async fn open(
network: Network,
seed: WalletSeed,
config: Config,
args: OpenWalletArgs,
) -> anyhow::Result<Wallet> {
let fingerprint = seed.fingerprint();
let lock_manager = if let Some(lm) = args.lock_manager {
lm
} else {
crate::lock_manager::platform_default(args.datadir.as_ref(), Some(fingerprint))
.context("failed to instantiate platform default lock manager")?
};
let db = if let Some(db) = args.persister {
db
} else {
if let Some(ref datadir) = args.datadir {
#[cfg(not(target_arch = "wasm32"))]
if !datadir.exists() && args.create_if_not_exists {
tokio::fs::create_dir_all(datadir).await.with_context(|| format!(
"failed to create datadir at {}", datadir.display(),
))?;
}
}
crate::persist::platform_default(args.datadir.as_ref(), Some(fingerprint)).await
.context("failed to instantiate platform default persister")?
};
let properties = if let Some(p) = db.read_properties().await? {
p
} else if args.create_if_not_exists {
Self::create(
network, &seed, &config, &*db, &*lock_manager, args.create_without_server,
).await.context("error creating new wallet")?;
db.read_properties().await?
.context("create failed: no wallet properties after Wallet::create was called")?
} else {
bail!("wallet does not exist; use Wallet::create or \
set options.create_if_not_exists to true");
};
if properties.fingerprint != fingerprint {
bail!("incorrect mnemonic")
}
let chain_source = if let Some(ref url) = config.esplora_address {
ChainSourceSpec::Esplora {
url: url.clone(),
}
} else if let Some(ref url) = config.bitcoind_address {
let auth = if let Some(ref c) = config.bitcoind_cookiefile {
bitcoin_ext::rpc::Auth::CookieFile(c.clone())
} else {
bitcoin_ext::rpc::Auth::UserPass(
config.bitcoind_user.clone().context("need bitcoind auth config")?,
config.bitcoind_pass.clone().context("need bitcoind auth config")?,
)
};
ChainSourceSpec::Bitcoind { url: url.clone(), auth }
} else {
bail!("Need to either provide esplora or bitcoind info");
};
#[cfg(feature = "socks5-proxy")]
let chain_proxy = proxy_for_url(&config.socks5_proxy, chain_source.url())?;
let chain_source_client = ChainSource::new(
chain_source, properties.network, config.fallback_fee_rate,
#[cfg(feature = "socks5-proxy")] chain_proxy.as_deref(),
).await?;
let chain = Arc::new(chain_source_client);
chain.require_version().await
.context("provided chain source doesn't meet version requirement")?;
let server = tokio::sync::OnceCell::new();
let notifications = NotificationDispatch::new();
let movements = Arc::new(MovementManager::new(db.clone(), notifications.clone()));
let exit = Exit::new(db.clone(), chain.clone(), movements.clone()).await?;
let ret = Wallet { inner: Arc::new(WalletInner {
config, db, lock_manager, seed, exit, movements, notifications, server, chain,
daemon: parking_lot::Mutex::new(None),
last_force_exit_scan_tip: tokio::sync::Mutex::new(None),
round_secret_nonces: RoundSecretNonces::new(),
})};
ret.inner.exit.load().await
.context("error loading exit system after opening wallet")?;
if args.run_daemon {
ret.start_daemon(args.onchain)
.context("failed to start daemon after opening wallet")?;
}
Ok(ret)
}
pub fn config(&self) -> &Config {
&self.inner.config
}
pub async fn properties(&self) -> anyhow::Result<WalletProperties> {
let properties = self.inner.db.read_properties().await?.context("Wallet is not initialised")?;
Ok(properties)
}
pub fn fingerprint(&self) -> Fingerprint {
self.inner.seed.fingerprint()
}
async fn connect_to_server(
config: &Config,
network: Network,
) -> anyhow::Result<ServerConnection> {
let server_address = crate::utils::url_with_default_https_scheme(&config.server_address);
let mut builder = ServerConnection::builder()
.address(&server_address)
.network(network);
#[cfg(feature = "socks5-proxy")]
if let Some(proxy) = proxy_for_url(&config.socks5_proxy, &server_address)? {
builder = builder.proxy(&proxy)
}
if let Some(ref token) = config.server_access_token {
builder = builder.access_token(token);
}
if let Some(ref ua) = config.user_agent {
builder = builder.user_agent(ua);
}
builder.connect().await.map_err(wrap_server_connect_error)
.context("Failed to connect to Ark server")
}
async fn require_server(&self) -> anyhow::Result<(ServerConnection, ArkInfo)> {
let conn = self.inner.server.get_or_try_init(|| async {
let network = self.properties().await?.network;
Self::connect_to_server(&self.inner.config, network).await
.context("You should be connected to Ark server to perform this action")
}).await?.clone();
let ark_info = conn.ark_info().await;
self.check_and_store_server_keys(&ark_info).await?;
Ok((conn, ark_info))
}
pub async fn refresh_server(&self) -> anyhow::Result<()> {
let srv = self.inner.server.get_or_try_init(|| async {
let properties = self.properties().await?;
Self::connect_to_server(&self.inner.config, properties.network).await
.map_err(anyhow::Error::from)
}).await?;
srv.check_connection().await?;
let ark_info = srv.ark_info().await;
ark_info.fees.validate().context("invalid fee schedule")?;
self.check_and_store_server_keys(&ark_info).await?;
Ok(())
}
async fn check_and_store_server_keys(&self, ark_info: &ArkInfo) -> anyhow::Result<()> {
let properties = self.properties().await?;
if let Some(stored_pubkey) = properties.server_pubkey {
if stored_pubkey != ark_info.server_pubkey {
log_server_pubkey_changed_error(stored_pubkey, ark_info.server_pubkey);
bail!("Server public key has changed. You should exit all your VTXOs!");
}
} else {
self.inner.db.set_server_pubkey(ark_info.server_pubkey).await?;
info!("Stored server pubkey for existing wallet: {}", ark_info.server_pubkey);
}
if let Some(stored_mailbox_pubkey) = properties.server_mailbox_pubkey {
if stored_mailbox_pubkey != ark_info.mailbox_pubkey {
log_server_mailbox_pubkey_changed_error(stored_mailbox_pubkey, ark_info.mailbox_pubkey);
bail!("Server mailbox public key has changed.");
}
} else {
self.inner.db.set_server_mailbox_pubkey(ark_info.mailbox_pubkey).await?;
info!("Stored server mailbox pubkey for existing wallet: {}", ark_info.mailbox_pubkey);
}
Ok(())
}
pub async fn ark_info(&self) -> anyhow::Result<Option<ArkInfo>> {
match self.inner.server.get() {
Some(srv) => Ok(Some(srv.ark_info().await)),
None => Ok(None),
}
}
pub async fn require_ark_info(&self) -> anyhow::Result<ArkInfo> {
let (_, ark_info) = self.require_server().await?;
Ok(ark_info)
}
pub async fn balance(&self) -> anyhow::Result<Balance> {
let vtxos = self.vtxos().await?;
let spendable = {
let mut v = vtxos.iter().collect();
VtxoStateKind::Spendable.filter_vtxos(&mut v).await?;
v.into_iter().map(|v| v.amount()).sum::<Amount>()
};
let pending_lightning_send = self.pending_lightning_send_vtxos().await?.iter()
.map(|v| v.amount())
.sum::<Amount>();
let claimable_lightning_receive = self.claimable_lightning_receive_balance().await?;
let pending_board = self.pending_board_vtxos().await?.iter()
.map(|v| v.amount())
.sum::<Amount>();
let pending_in_round = self.pending_round_balance().await?;
let pending_exit = self.exit_mgr().try_pending_total();
Ok(Balance {
spendable,
pending_in_round,
pending_lightning_send,
claimable_lightning_receive,
pending_exit,
pending_board,
})
}
pub async fn validate_vtxo(&self, vtxo: &Vtxo<Full>) -> anyhow::Result<()> {
let tx = self.inner.chain.get_tx(&vtxo.chain_anchor().txid).await
.context("could not fetch chain tx")?;
let tx = tx.with_context(|| {
format!("vtxo chain anchor not found for vtxo: {}", vtxo.chain_anchor().txid)
})?;
vtxo.validate(&tx)?;
Ok(())
}
pub async fn import_vtxo(&self, vtxo: &Vtxo<Full>) -> anyhow::Result<()> {
if self.inner.db.get_wallet_vtxo(vtxo.id()).await?.is_some() {
info!("VTXO {} already exists in wallet, skipping import", vtxo.id());
return Ok(());
}
self.validate_vtxo(vtxo).await.context("VTXO validation failed")?;
if self.find_signable_clause(vtxo).await.is_none() {
bail!("VTXO {} is not owned by this wallet (no signable clause found)", vtxo.id());
}
let current_height = self.inner.chain.tip().await?;
if vtxo.expiry_height() <= current_height {
bail!("Vtxo {} has expired", vtxo.id());
}
self.store_spendable_vtxos([vtxo]).await.context("failed to store imported VTXO")?;
info!("Successfully imported VTXO {}", vtxo.id());
Ok(())
}
pub async fn get_vtxo_by_id(&self, vtxo_id: VtxoId) -> anyhow::Result<WalletVtxo> {
let vtxo = self.inner.db.get_wallet_vtxo(vtxo_id).await
.with_context(|| format!("Error when querying vtxo {} in database", vtxo_id))?
.with_context(|| format!("The VTXO with id {} cannot be found", vtxo_id))?;
Ok(vtxo)
}
pub async fn get_full_vtxo(&self, vtxo_id: VtxoId) -> anyhow::Result<Vtxo<Full>> {
self.inner.db.get_full_vtxo(vtxo_id).await
.with_context(|| format!("Error when querying full vtxo {} in database", vtxo_id))?
.with_context(|| format!("The VTXO with id {} cannot be found", vtxo_id))
}
pub async fn get_full_vtxos<V: VtxoRef>(
&self,
vtxos: impl IntoIterator<Item = V>,
) -> anyhow::Result<Vec<Vtxo<Full>>> {
let ids = vtxos.into_iter().map(|v| v.vtxo_id()).collect::<Vec<_>>();
self.inner.db.get_full_vtxos(&ids).await
.with_context(||
format!("Error when querying full vtxos in database with IDs: {:?}", ids)
)
}
#[deprecated(since="0.1.0-beta.5", note = "Use Wallet::history instead")]
pub async fn movements(&self) -> anyhow::Result<Vec<Movement>> {
self.history().await
}
pub async fn history(&self) -> anyhow::Result<Vec<Movement>> {
Ok(self.inner.db.get_all_movements().await?)
}
pub async fn update_history_metadata(
&self,
movement_id: MovementId,
patch: &serde_json::Value,
) -> anyhow::Result<()> {
self.inner.movements.patch_metadata(movement_id, patch).await?;
Ok(())
}
pub async fn history_by_payment_method(
&self,
payment_method: &PaymentMethod,
) -> anyhow::Result<Vec<Movement>> {
let mut ret = self.inner.db.get_movements_by_payment_method(payment_method).await?;
ret.sort_by_key(|m| m.id);
Ok(ret)
}
pub async fn all_vtxos(&self) -> anyhow::Result<Vec<WalletVtxo>> {
Ok(self.inner.db.get_all_vtxos().await?)
}
pub async fn vtxos(&self) -> anyhow::Result<Vec<WalletVtxo>> {
Ok(self.inner.db.get_vtxos_by_state(&VtxoStateKind::UNSPENT_STATES).await?)
}
pub async fn vtxos_with(&self, filter: &impl FilterVtxos) -> anyhow::Result<Vec<WalletVtxo>> {
let mut vtxos = self.vtxos().await?;
filter.filter_vtxos(&mut vtxos).await.context("error filtering vtxos")?;
Ok(vtxos)
}
pub async fn spendable_vtxos(&self) -> anyhow::Result<Vec<WalletVtxo>> {
Ok(self.vtxos_with(&VtxoStateKind::Spendable).await?)
}
pub async fn spendable_vtxos_with(
&self,
filter: &impl FilterVtxos,
) -> anyhow::Result<Vec<WalletVtxo>> {
let mut vtxos = self.spendable_vtxos().await?;
filter.filter_vtxos(&mut vtxos).await.context("error filtering vtxos")?;
Ok(vtxos)
}
pub async fn get_expiring_vtxos(
&self,
threshold: BlockHeight,
) -> anyhow::Result<Vec<WalletVtxo>> {
let expiry = self.inner.chain.tip().await? + threshold;
let filter = VtxoFilter::new(&self).expires_before(expiry);
Ok(self.spendable_vtxos_with(&filter).await?)
}
pub async fn maintenance(&self) -> anyhow::Result<()> {
info!("Starting wallet maintenance in interactive mode");
self.sync().await;
let rounds = self.progress_pending_rounds(None).await;
if let Err(e) = rounds.as_ref() {
warn!("Error progressing pending rounds: {:#}", e);
}
let states = self.inner.db.get_pending_round_state_ids().await?;
for id in states {
debug!("Cancelling pending round participation {}", id);
let mut state = match self.lock_wait_round_state(id).await {
Ok(Some(s)) => s,
Ok(None) => continue, Err(e) => {
warn!("Failed to lock round state with id {}: {:#}", id, e);
continue;
}
};
if let Err(e) = state.state_mut().try_cancel(self).await {
warn!("Error cancelling pending round: {:#}", e);
}
}
let refresh = self.maintenance_refresh().await;
if let Err(e) = refresh.as_ref() {
warn!("Error refreshing VTXOs: {:#}", e);
}
if rounds.is_err() || refresh.is_err() {
bail!("Maintenance encountered errors.\nprogress_rounds: {:#?}\nrefresh: {:#?}", rounds, refresh);
}
Ok(())
}
pub async fn maintenance_delegated(&self) -> anyhow::Result<()> {
info!("Starting wallet maintenance in delegated mode");
self.sync().await;
let rounds = self.progress_pending_rounds(None).await;
if let Err(e) = rounds.as_ref() {
warn!("Error progressing pending rounds: {:#}", e);
}
let refresh = self.maybe_schedule_maintenance_refresh_delegated().await;
if let Err(e) = refresh.as_ref() {
warn!("Error refreshing VTXOs: {:#}", e);
}
if rounds.is_err() || refresh.is_err() {
bail!("Delegated maintenance encountered errors.\nprogress_rounds: {:#?}\nrefresh: {:#?}", rounds, refresh);
}
Ok(())
}
pub async fn maintenance_with_onchain<W: PreparePsbt + SignPsbt + ExitUnilaterally>(
&self,
onchain: &mut W,
) -> anyhow::Result<()> {
info!("Starting wallet maintenance in interactive mode with onchain wallet");
let maintenance = self.maintenance().await;
let exit_sync = self.sync_exits().await;
if let Err(e) = exit_sync.as_ref() {
warn!("Error syncing exits: {:#}", e);
}
let exit_progress = self.exit_mgr().progress_exits_with_bdk(self, onchain, None).await;
if let Err(e) = exit_progress.as_ref() {
warn!("Error progressing exits: {:#}", e);
}
if maintenance.is_err() || exit_sync.is_err() || exit_progress.is_err() {
bail!("Maintenance encountered errors.\nmaintenance: {:#?}\nexit_sync: {:#?}\nexit_progress: {:#?}", maintenance, exit_sync, exit_progress);
}
Ok(())
}
pub async fn maintenance_with_onchain_delegated<W: PreparePsbt + SignPsbt + ExitUnilaterally>(
&self,
onchain: &mut W,
) -> anyhow::Result<()> {
info!("Starting wallet maintenance in delegated mode with onchain wallet");
let maintenance = self.maintenance_delegated().await;
let exit_sync = self.sync_exits().await;
if let Err(e) = exit_sync.as_ref() {
warn!("Error syncing exits: {:#}", e);
}
let exit_progress = self.exit_mgr().progress_exits_with_bdk(self, onchain, None).await;
if let Err(e) = exit_progress.as_ref() {
warn!("Error progressing exits: {:#}", e);
}
if maintenance.is_err() || exit_sync.is_err() || exit_progress.is_err() {
bail!("Delegated maintenance encountered errors.\nmaintenance: {:#?}\nexit_sync: {:#?}\nexit_progress: {:#?}", maintenance, exit_sync, exit_progress);
}
Ok(())
}
pub(crate) async fn join_round_for_maintenance_refresh(
&self,
attempt: &RoundAttempt,
) -> anyhow::Result<Option<RoundStateId>> {
self.maintenance_refresh_retry_loop(|part| async move {
info!("Joining round {} for maintenance refresh ({} vtxos)",
attempt.round_seq, part.inputs.len());
Ok(Some(self.join_attempt_interactive(
part, attempt, Some(RoundMovement::Refresh),
).await?.id()))
}).await
}
pub async fn maybe_schedule_maintenance_refresh_delegated(
&self,
) -> anyhow::Result<Option<RoundStateId>> {
self.maintenance_refresh_retry_loop(|part| async move {
info!("Scheduling delegated maintenance refresh ({} vtxos)", part.inputs.len());
Ok(Some(self.join_next_round_delegated(part, Some(RoundMovement::Refresh)).await?.id()))
}).await
}
async fn maintenance_refresh_retry_loop<F, Fut>(
&self,
attempt_refresh: F,
) -> anyhow::Result<Option<RoundStateId>>
where
F: Fn(RoundParticipation) -> Fut,
Fut: Future<Output = anyhow::Result<Option<RoundStateId>>>,
{
let mut excluded = HashSet::new();
for _ in 0..10 {
let vtxos = self.get_vtxos_to_refresh_with_excluded(excluded.iter().copied()).await?;
if vtxos.is_empty() {
return Ok(None);
}
let part = match self.build_refresh_participation(vtxos).await? {
Some(participation) => participation,
None => return Ok(None),
};
match attempt_refresh(part).await {
Ok(state_id) => return Ok(state_id),
Err(e) => {
let rejected = rejected_vtxos_from_error(&e).into_iter()
.filter(|id| !excluded.contains(id))
.collect::<Vec<_>>();
if rejected.is_empty() {
return Err(e);
}
warn!("Maintenance refresh rejected {} unusable input(s) ({:?}); \
retrying without them", rejected.len(), rejected);
excluded.extend(rejected);
},
}
}
bail!("Maintenance refresh failed after 10 retries");
}
pub async fn maintenance_refresh(&self) -> anyhow::Result<Option<RoundStatus>> {
if self.get_vtxos_to_refresh().await?.is_empty() {
return Ok(None);
}
info!("Waiting for round to perform maintenance refresh...");
let mut events = self.subscribe_round_events().await?;
while let Some(event) = events.next().await {
let event = event.context("error on round event stream")?;
if let RoundEvent::Attempt(a) = event && a.attempt_seq == 0 {
debug!("Round {} started, triggering maintenance refresh", a.round_seq);
let state_id = match self.join_round_for_maintenance_refresh(&a).await? {
Some(id) => id,
None => return Ok(None),
};
let state = self.lock_wait_round_state(state_id).await?
.context("maintenance refresh round state vanished after joining")?;
return Ok(Some(self.drive_round_state(state, &mut events).await?));
}
}
Ok(None)
}
pub async fn sync(&self) {
futures::join!(
async {
if let Err(e) = self.inner.chain.update_fee_rates(self.inner.config.fallback_fee_rate).await {
warn!("Error updating fee rates: {:#}", e);
}
},
async {
if let Err(e) = self.sync_mailbox().await {
warn!("Error in mailbox sync: {:#}", e);
}
},
async {
if let Err(e) = self.sync_pending_rounds().await {
warn!("Error while trying to progress rounds awaiting confirmations: {:#}", e);
}
},
async {
if let Err(e) = self.sync_pending_lightning_send_vtxos().await {
warn!("Error syncing pending lightning payments: {:#}", e);
}
},
async {
if let Err(e) = self.sync_pending_arkoor_sends().await {
warn!("Error syncing pending arkoor sends: {:#}", e);
}
},
async {
if let Err(e) = self.try_claim_all_lightning_receives(false).await {
warn!("Error claiming pending lightning receives: {:#}", e);
}
},
async {
if let Err(e) = self.sync_pending_boards().await {
warn!("Error syncing pending boards: {:#}", e);
}
},
async {
if let Err(e) = self.sync_pending_offboards().await {
warn!("Error syncing pending offboards: {:#}", e);
}
},
async {
if let Err(e) = self.sync_force_exited_vtxos().await {
warn!("Error scanning for on-chain-exited VTXOs: {:#}", e);
}
}
);
}
pub async fn sync_exits(&self) -> anyhow::Result<()> {
self.exit_mgr().sync(&self).await?;
Ok(())
}
pub async fn sync_force_exited_vtxos(&self) -> anyhow::Result<()> {
let tip = self.inner.chain.tip().await?;
let mut lock = self.inner.last_force_exit_scan_tip.lock().await;
if *lock == Some(tip) {
return Ok(());
}
let exiting = self.exit_mgr().get_exit_vtxo_ids().await;
let vtxos = self.inner.db.get_vtxos_by_state(&[VtxoStateKind::Spendable]).await?
.into_iter()
.filter(|v| !exiting.contains(&v.vtxo.id()));
let mut checked = FuturesUnordered::new();
for wv in vtxos {
let chain = self.inner.chain.clone();
checked.push(async move {
let txid = wv.vtxo_id().to_point().txid;
let status = chain.tx_status(txid).await;
(wv, status)
});
}
let mut to_exit = Vec::new();
while let Some((vtxo, status)) = futures::StreamExt::next(&mut checked).await {
match status {
Ok(TxStatus::NotFound) => {},
Ok(_) => {
info!("VTXO {} was exited on-chain without us; routing it to a claimable exit",
vtxo.vtxo.id(),
);
to_exit.push(vtxo.vtxo);
},
Err(e) => warn!("Could not check on-chain status of VTXO {}: {:#}",
vtxo.vtxo.id(), e,
),
}
}
if !to_exit.is_empty() {
self.exit_mgr().start_exit_for_vtxos(&to_exit).await
.context("failed to start exit for on-chain-exited VTXOs")?;
*lock = Some(tip);
self.sync_exits().await
.context("failed to sync exits after starting new ones")?;
} else {
*lock = Some(tip);
}
Ok(())
}
pub async fn dangerous_drop_vtxo(&self, vtxo_id: VtxoId) -> anyhow::Result<()> {
warn!("Drop vtxo {} from the database", vtxo_id);
self.inner.db.remove_vtxo(vtxo_id).await?;
Ok(())
}
pub async fn dangerous_drop_all_vtxos(&self) -> anyhow::Result<()> {
warn!("Dropping all vtxos from the db...");
for vtxo in self.vtxos().await? {
self.inner.db.remove_vtxo(vtxo.id()).await?;
}
self.exit_mgr().dangerous_clear_exit().await?;
Ok(())
}
async fn has_counterparty_risk(&self, vtxo: &Vtxo<Full>) -> anyhow::Result<bool> {
for past_pks in vtxo.past_arkoor_pubkeys() {
let mut owns_any = false;
for past_pk in past_pks {
if self.inner.db.get_public_key_idx(&past_pk).await?.is_some() {
owns_any = true;
break;
}
}
if !owns_any {
return Ok(true);
}
}
let my_clause = self.find_signable_clause(vtxo).await;
Ok(!my_clause.is_some())
}
async fn add_should_refresh_vtxos<V: VtxoRef>(
&self,
participation: &mut RoundParticipation,
exclude: impl IntoIterator<Item = V>,
) -> anyhow::Result<()> {
let tip = self.inner.chain.tip().await?;
let mut vtxos_to_refresh = self.spendable_vtxos_with(
&RefreshStrategy::should_refresh(self, tip, self.inner.chain.fee_rates().await.fast),
).await?;
if vtxos_to_refresh.is_empty() {
return Ok(());
}
let excluded_ids = participation.inputs.iter()
.map(|v| v.vtxo_id())
.chain(exclude.into_iter().map(|v| v.vtxo_id()))
.collect::<HashSet<_>>();
let mut total_amount = Amount::ZERO;
for i in (0..vtxos_to_refresh.len()).rev() {
let vtxo = &vtxos_to_refresh[i];
if excluded_ids.contains(&vtxo.id()) {
vtxos_to_refresh.swap_remove(i);
continue;
}
total_amount += vtxo.amount();
}
if vtxos_to_refresh.is_empty() {
return Ok(());
}
let (_, ark_info) = self.require_server().await?;
let fee = ark_info.fees.refresh.calculate_no_base_fee(
vtxos_to_refresh.iter().map(|wv| VtxoFeeInfo::from_vtxo_and_tip(&wv.vtxo, tip)),
).context("fee overflowed")?;
let output_amount = match validate_and_subtract_fee_min_dust(total_amount, fee) {
Ok(amount) => amount,
Err(e) => {
trace!("Cannot add should-refresh VTXOs: {}", e);
return Ok(());
},
};
info!(
"Adding {} extra VTXOs to round participation total = {}, fee = {}, output = {}",
vtxos_to_refresh.len(), total_amount, fee, output_amount,
);
let (user_keypair, _) = self.derive_store_next_keypair().await?;
let req = VtxoRequest {
policy: VtxoPolicy::new_pubkey(user_keypair.public_key()),
amount: output_amount,
};
let extra_ids = vtxos_to_refresh.into_iter().map(|wv| wv.id()).collect::<Vec<_>>();
let extra_full = self.inner.db.get_full_vtxos(&extra_ids).await
.context("failed to hydrate refresh candidates")?;
participation.inputs.reserve(extra_full.len());
participation.inputs.extend(extra_full);
participation.outputs.push(req);
Ok(())
}
pub async fn build_refresh_participation<V: VtxoRef>(
&self,
vtxos: impl IntoIterator<Item = V>,
) -> anyhow::Result<Option<RoundParticipation>> {
let (vtxos, total_amount) = {
let iter = vtxos.into_iter();
let size_hint = iter.size_hint();
let mut vtxos = Vec::<Vtxo<Full>>::with_capacity(size_hint.1.unwrap_or(size_hint.0));
let mut amount = Amount::ZERO;
for vref in iter {
let id = vref.vtxo_id();
if vtxos.iter().any(|v| v.id() == id) {
bail!("duplicate VTXO id: {}", id);
}
let vtxo = if let Some(vtxo) = vref.into_full_vtxo() {
vtxo
} else {
self.inner.db.get_full_vtxo(id).await?
.with_context(|| format!("vtxo with id {} not found", id))?
};
amount += vtxo.amount();
vtxos.push(vtxo);
}
(vtxos, amount)
};
if vtxos.is_empty() {
info!("Skipping refresh since no VTXOs are provided.");
return Ok(None);
}
ensure!(total_amount >= P2TR_DUST,
"vtxo amount must be at least {} to participate in a round",
P2TR_DUST,
);
let (_, ark_info) = self.require_server().await?;
let current_height = self.inner.chain.tip().await?;
let vtxo_fee_infos = vtxos.iter()
.map(|v| VtxoFeeInfo::from_vtxo_and_tip(v, current_height));
let fee = ark_info.fees.refresh.calculate(vtxo_fee_infos).context("fee overflowed")?;
let output_amount = validate_and_subtract_fee_min_dust(total_amount, fee)?;
info!("Refreshing {} VTXOs (total amount = {}, fee = {}, output = {}).",
vtxos.len(), total_amount, fee, output_amount,
);
let (user_keypair, _) = self.derive_store_next_keypair().await?;
let req = VtxoRequest {
policy: VtxoPolicy::Pubkey(PubkeyVtxoPolicy { user_pubkey: user_keypair.public_key() }),
amount: output_amount,
};
Ok(Some(RoundParticipation {
inputs: vtxos,
outputs: vec![req],
unblinded_mailbox_id: None,
}))
}
pub async fn refresh_vtxos<V: VtxoRef>(
&self,
vtxos: impl IntoIterator<Item = V>,
) -> anyhow::Result<Option<RoundStatus>> {
let mut participation = match self.build_refresh_participation(vtxos).await? {
Some(participation) => participation,
None => return Ok(None),
};
if let Err(e) = self.add_should_refresh_vtxos(
&mut participation, iter::empty::<VtxoId>(),
).await {
warn!("Error trying to add additional VTXOs that should be refreshed: {:#}", e);
}
Ok(Some(self.participate_round(participation, Some(RoundMovement::Refresh)).await?))
}
pub async fn refresh_vtxos_delegated<V: VtxoRef>(
&self,
vtxos: impl IntoIterator<Item = V>,
) -> anyhow::Result<Option<StoredRoundState<Unlocked>>> {
let mut part = match self.build_refresh_participation(vtxos).await? {
Some(participation) => participation,
None => return Ok(None),
};
if let Err(e) = self.add_should_refresh_vtxos(&mut part, iter::empty::<VtxoId>()).await {
warn!("Error trying to add additional VTXOs that should be refreshed: {:#}", e);
}
Ok(Some(self.join_next_round_delegated(part, Some(RoundMovement::Refresh)).await?))
}
pub async fn get_vtxos_to_refresh(&self) -> anyhow::Result<Vec<WalletVtxo>> {
let vtxos = self.spendable_vtxos_with(&RefreshStrategy::should_refresh_if_must(
self,
self.inner.chain.tip().await?,
self.inner.chain.fee_rates().await.fast,
)).await?;
Ok(vtxos)
}
pub async fn get_vtxos_to_refresh_with_excluded<V: VtxoRef>(
&self,
exclude: impl IntoIterator<Item = V>,
) -> anyhow::Result<Vec<WalletVtxo>> {
let mut vtxos = self.get_vtxos_to_refresh().await?;
for v in exclude.into_iter() {
if let Some(index) = vtxos.iter().position(|vtxo| vtxo.id() == v.vtxo_id()) {
vtxos.swap_remove(index);
}
}
Ok(vtxos)
}
pub async fn get_first_expiring_vtxo_blockheight(
&self,
) -> anyhow::Result<Option<BlockHeight>> {
Ok(self.spendable_vtxos().await?.iter().map(|v| v.expiry_height()).min())
}
pub async fn get_next_required_refresh_blockheight(
&self,
) -> anyhow::Result<Option<BlockHeight>> {
let first_expiry = self.get_first_expiring_vtxo_blockheight().await?;
Ok(first_expiry.map(|h| h.saturating_sub(self.inner.config.vtxo_refresh_expiry_threshold)))
}
async fn select_vtxos_to_cover(
&self,
amount: Amount,
) -> anyhow::Result<Vec<WalletVtxo>> {
let mut vtxos = self.spendable_vtxos().await?;
self.sort_vtxos_for_selection(&mut vtxos);
let (last, _total_amount) = self.select_vtxos_inner(amount, &vtxos)?;
vtxos.truncate(last+1);
Ok(vtxos)
}
async fn select_vtxos_to_cover_with_fee<F>(
&self,
amount: Amount,
calc_fee: F,
) -> anyhow::Result<(Vec<WalletVtxo>, Amount)>
where
F: for<'a> Fn(
Amount, std::iter::Copied<std::slice::Iter<'a, VtxoFeeInfo>>,
) -> anyhow::Result<Amount>,
{
let tip = self.inner.chain.tip().await?;
let mut vtxos = self.spendable_vtxos().await?;
self.sort_vtxos_for_selection(&mut vtxos);
let fee_info = vtxos.iter()
.map(|v| VtxoFeeInfo::from_vtxo_and_tip(v, tip))
.collect::<Vec<_>>();
const MAX_ITERATIONS: usize = 100;
let mut fee = Amount::ZERO;
for _ in 0..MAX_ITERATIONS {
let required = amount.checked_add(fee)
.context("Amount + fee overflow")?;
let (last, vtxo_amount) = self.select_vtxos_inner(required, &vtxos)
.context("Could not find enough suitable VTXOs to cover payment + fees")?;
fee = calc_fee(amount, fee_info[..=last].iter().copied())?;
if amount + fee <= vtxo_amount {
trace!("Selected vtxos to cover amount + fee: amount = {}, fee = {}, total inputs = {}",
amount, fee, vtxo_amount,
);
vtxos.truncate(last+1);
return Ok((vtxos, fee));
}
trace!("VTXO sum of {} did not exceed amount {} and fee {}, iterating again",
vtxo_amount, amount, fee,
);
}
bail!("Fee calculation did not converge after maximum iterations")
}
fn sort_vtxos_for_selection(&self, vtxos: &mut Vec<WalletVtxo>) {
vtxos.sort_by_key(|v| v.expiry_height());
}
fn select_vtxos_inner(
&self,
amount: Amount,
vtxos: &Vec<WalletVtxo>,
) -> anyhow::Result<(usize, Amount)> {
let mut total_amount = Amount::ZERO;
for (i, vtxo) in vtxos.iter().enumerate() {
total_amount += vtxo.amount();
if total_amount >= amount {
return Ok((i, total_amount))
}
}
bail!("Insufficient money available. Needed {} but {} is available",
amount, total_amount,
);
}
pub fn start_daemon(
&self,
onchain: Option<Arc<tokio::sync::RwLock<dyn DaemonizableOnchainWallet>>>,
) -> anyhow::Result<()> {
let mut daemon = self.inner.daemon.lock();
if daemon.is_some() {
warn!("Called Wallet::start_daemon while daemon was already running.");
return Ok(());
}
let handle = crate::daemon::start_daemon(self.clone(), onchain);
let _ = daemon.insert(handle);
Ok(())
}
#[deprecated(since = "0.1.4", note = "use start_daemon instead")]
pub fn run_daemon(
&self,
onchain: Option<Arc<tokio::sync::RwLock<dyn DaemonizableOnchainWallet>>>,
) -> anyhow::Result<()> {
self.start_daemon(onchain)
}
pub fn stop_daemon(&self) {
let mut daemon = self.inner.daemon.lock();
if let Some(handle) = daemon.take() {
handle.stop();
}
}
pub async fn register_vtxo_transactions_with_server(
&self,
vtxos: &[impl AsRef<Vtxo<Full>>],
) -> anyhow::Result<()> {
if vtxos.is_empty() {
return Ok(());
}
let (mut srv, _) = self.require_server().await?;
srv.client.register_vtxo_transactions(protos::RegisterVtxoTransactionsRequest {
vtxos: vtxos.iter().map(|v| v.as_ref().serialize()).collect(),
}).await.context("failed to register vtxo transactions")?;
Ok(())
}
}
fn wrap_server_connect_error(err: ConnectError) -> anyhow::Error {
match err {
ConnectError::CreateEndpoint(CreateEndpointError::NoTransportBackend) => {
anyhow!(MISSING_SERVER_TRANSPORT_HELP)
},
other => anyhow::Error::from(other),
}
}
impl std::ops::Drop for WalletInner {
fn drop(&mut self) {
if let Some(handle) = self.daemon.lock().take() {
handle.stop();
}
}
}
#[cfg(test)]
mod tests {
use server_rpc::client::CreateEndpointError;
use super::{wrap_server_connect_error, MISSING_SERVER_TRANSPORT_HELP};
#[test]
fn no_transport_connect_error_is_reworded_for_wallet_users() {
let err = wrap_server_connect_error(CreateEndpointError::NoTransportBackend.into());
assert!(err.to_string().contains(MISSING_SERVER_TRANSPORT_HELP));
assert!(err.to_string().contains("feature `bark-wallet/native` or `bark-wallet/wasm-web`"));
}
}