Skip to main content

bark/
import.rs

1//! Manual VTXO import.
2//!
3//! An import re-adds a VTXO the wallet has no record of but can prove it owns,
4//! after a lost database or a restore from an older backup.
5//!
6//! The wallet owns a VTXO when it can derive the VTXO's user pubkey from its
7//! seed. That keypair also authorizes the query that asks the server whether
8//! the VTXO is spent. A spent VTXO is stored as spent rather than refused,
9//! since it is part of the wallet's history.
10
11use anyhow::{anyhow, Context};
12use bitcoin::secp256k1::Keypair;
13use log::{info, warn};
14
15use ark::{Vtxo, VtxoId};
16use ark::vtxo::Full;
17use server_rpc::protos::VtxoSpendState;
18
19use crate::Wallet;
20use crate::vtxo::{VtxoState, VtxoValidationError};
21
22/// Why a VTXO could not be imported.
23#[derive(Debug, thiserror::Error)]
24pub enum ImportVtxoError {
25	#[error("vtxo {id} failed to validate: {source}")]
26	Invalid {
27		id: VtxoId,
28		source: VtxoValidationError,
29	},
30
31	#[error("unable to derive the key for vtxo {id} from this seed with a {gap_limit} gap limit")]
32	KeyNotFound {
33		id: VtxoId,
34		gap_limit: u32,
35	},
36
37	#[error("vtxo {id} is neither spendable nor spent, so it cannot be imported: the server \
38		reports it as {state:?}")]
39	InFlight {
40		id: VtxoId,
41		state: VtxoSpendState,
42	},
43
44	#[error("unexpected error: {0:#}")]
45	Transient(#[from] anyhow::Error),
46}
47
48/// Arguments for [`Wallet::import_vtxos`].
49#[derive(Debug, Clone, Default)]
50pub struct ImportVtxoArgs {
51	/// Gap limit for the key scan that decides whether we own the VTXOs,
52	/// overriding [`crate::Config::vtxo_key_gap_limit`].
53	///
54	/// Default: none
55	pub gap_limit: Option<u32>,
56
57	/// Import as spendable without asking the server for each VTXO's state. Skipping it can leave
58	/// the wallet with spent VTXOs incorrectly marked as spendable.
59	///
60	/// Default: false
61	pub skip_status_check: bool,
62
63	/// Keep the VTXOs that import successfully even when another one in the
64	/// batch fails. The returned ids are the ones that were kept.
65	///
66	/// Default: false, so a single failure discards the whole batch.
67	pub allow_partial: bool,
68}
69
70impl Wallet {
71	/// Manually import VTXOs into the wallet.
72	///
73	/// Returns the ids now held, whether this call stored them or found them
74	/// already there, so a failed batch can be retried. Each VTXO is stored in the
75	/// state the server reports. One VTXO that cannot be imported discards the
76	/// whole batch, unless [`ImportVtxoArgs::allow_partial`] is set: the VTXOs
77	/// that did import are then kept, and the failure is logged.
78	pub async fn import_vtxos(
79		&self,
80		vtxos: &[Vtxo<Full>],
81		args: ImportVtxoArgs,
82	) -> Result<Vec<VtxoId>, ImportVtxoError> {
83		let mut ids = Vec::<VtxoId>::with_capacity(vtxos.len());
84		let mut pending = Vec::<&Vtxo<Full>>::with_capacity(vtxos.len());
85		for vtxo in vtxos {
86			let vtxo_id = vtxo.id();
87			if self.inner.db.get_wallet_vtxo(vtxo_id).await?.is_some() {
88				info!("VTXO {} already exists in wallet, skipping import", vtxo_id);
89				ids.push(vtxo_id);
90			} else if !pending.iter().any(|v| v.id() == vtxo_id) {
91				if let Err(e) = self.validate_vtxo(vtxo).await {
92					let e = ImportVtxoError::Invalid { id: vtxo_id, source: e };
93					if !args.allow_partial {
94						return Err(e);
95					}
96					warn!("Not importing vtxo {vtxo_id}: {e}");
97				} else {
98					pending.push(vtxo);
99				}
100			}
101		}
102		if pending.is_empty() {
103			return Ok(ids);
104		}
105
106		let gap_limit = args.gap_limit.unwrap_or(self.inner.config.vtxo_key_gap_limit);
107		// Collected before the await: a closure over `&&Vtxo` held across it is not
108		// general enough over lifetimes for the axum handler's Send bound.
109		let user_pubkeys = pending.iter().map(|v| v.user_pubkey()).collect::<Vec<_>>();
110		let keypairs = self.find_vtxo_keypairs(user_pubkeys, gap_limit).await
111			.context("error scanning the vtxo key space")?;
112
113		let mut to_store = Vec::with_capacity(pending.len());
114		for vtxo in &pending {
115			let id = vtxo.id();
116			let state = match keypairs.get(&vtxo.user_pubkey()) {
117				None => Err(ImportVtxoError::KeyNotFound { id, gap_limit }),
118				Some(keypair) => self.import_vtxo_state(vtxo, keypair, &args).await,
119			};
120			match state {
121				Ok(state) => to_store.push((*vtxo, state)),
122				Err(e) => {
123					if !args.allow_partial {
124						return Err(e);
125					}
126					warn!("Not importing vtxo {id}: {e}");
127				},
128			}
129		}
130		if to_store.is_empty() {
131			return Ok(ids);
132		}
133
134		let rows = to_store.iter().map(|(v, s)| (*v, s)).collect::<Vec<_>>();
135		self.inner.db.store_vtxos(&rows).await
136			.context("failed to store imported VTXOs")?;
137
138		for (vtxo, state) in &to_store {
139			info!("Successfully imported VTXO {} as {}", vtxo.id(), state.kind());
140			ids.push(vtxo.id());
141		}
142
143		Ok(ids)
144	}
145
146	/// The state to store `vtxo` in, which the server decides unless the caller
147	/// opted out of the query.
148	///
149	/// `keypair` must be `vtxo`'s user keypair: it signs the attestation the
150	/// query needs.
151	async fn import_vtxo_state(
152		&self,
153		vtxo: &Vtxo<Full>,
154		keypair: &Keypair,
155		args: &ImportVtxoArgs,
156	) -> Result<VtxoState, ImportVtxoError> {
157		if args.skip_status_check {
158			return Ok(VtxoState::Spendable);
159		}
160
161		let id = vtxo.id();
162		Ok(match self.fetch_vtxo_spend_state(id, keypair).await? {
163			VtxoSpendState::Spendable => VtxoState::Spendable,
164			VtxoSpendState::Spent => VtxoState::Spent,
165			state @ (
166				VtxoSpendState::Unclaimed
167				| VtxoSpendState::Unregistered
168				| VtxoSpendState::HtlcRecvUnclaimed
169			) => return Err(ImportVtxoError::InFlight { id, state }),
170			VtxoSpendState::Unspecified => return Err(ImportVtxoError::Transient(
171				anyhow!("server returned an unspecified spend state for vtxo {id}"),
172			)),
173		})
174	}
175
176	/// Manually import a single VTXO into the wallet.
177	///
178	/// See [`Wallet::import_vtxos`], which this defers to.
179	pub async fn import_vtxo(
180		&self,
181		vtxo: &Vtxo<Full>,
182		args: ImportVtxoArgs,
183	) -> Result<(), ImportVtxoError> {
184		self.import_vtxos(std::slice::from_ref(vtxo), args).await?;
185		Ok(())
186	}
187}