1use std::{
18 borrow::Cow,
19 fmt::{self, Display},
20 io::Cursor,
21};
22
23use anyhow::{Context, anyhow};
24use async_trait::async_trait;
25use lexe_serde::hexstr_or_bytes;
26use lightning::util::ser::{MaybeReadable, ReadableArgs, Writeable};
27use serde::{Deserialize, Serialize, de::DeserializeOwned};
28use tracing::{debug, warn};
29
30use crate::{error::BackendApiError, types::Empty};
31
32pub const SINGLETON_DIRECTORY: &str = ".";
36
37pub const BROADCASTED_TXS_DIR: &str = "broadcasted_txs";
38pub const CHANNEL_MONITORS_ARCHIVE_DIR: &str = "channel_monitors_archive";
39pub const CHANNEL_MONITORS_DIR: &str = "channel_monitors";
40pub const EVENTS_DIR: &str = "events";
41pub const MIGRATIONS_DIR: &str = "migrations";
42pub const UNSWEPT_OUTPUTS_EVENTS: &str = "unswept_outputs-events";
43pub const UNSWEPT_OUTPUTS_TXS: &str = "unswept_outputs-txs";
44
45pub const CHANNEL_MANAGER_FILENAME: &str = "channel_manager";
46pub const PW_ENC_ROOT_SEED_FILENAME: &str = "password_encrypted_root_seed";
47pub const WALLET_CHANGESET_LEGACY_FILENAME: &str = "bdk_wallet_changeset";
53pub const WALLET_CHANGESET_V2_FILENAME: &str = "bdk_wallet_changeset_v2";
54
55pub static REVOCABLE_CLIENTS_FILE_ID: VfsFileId =
56 VfsFileId::new_const(SINGLETON_DIRECTORY, "revocable_clients");
57
58#[async_trait]
63pub trait Vfs {
64 async fn get_file(
70 &self,
71 file_id: &VfsFileId,
72 ) -> Result<Option<VfsFile>, BackendApiError>;
73
74 async fn upsert_file(
78 &self,
79 file_id: &VfsFileId,
80 data: bytes::Bytes,
81 retries: usize,
82 ) -> Result<Empty, BackendApiError>;
83
84 async fn delete_file(
88 &self,
89 file_id: &VfsFileId,
90 ) -> Result<Empty, BackendApiError>;
91
92 async fn list_directory(
97 &self,
98 dir: &VfsDirectory,
99 ) -> Result<VfsDirectoryList, BackendApiError>;
100
101 async fn get_directory(
105 &self,
106 dir: &VfsDirectory,
107 ) -> Result<Vec<VfsFile>, BackendApiError> {
108 let directory_list = self.list_directory(dir).await?;
110
111 let fetch_futs = directory_list.filenames.into_iter().map(|filename| {
113 let file_id =
114 VfsFileId::new(directory_list.dirname.clone(), filename);
115 async move { self.get_file(&file_id).await }
116 });
117
118 let maybe_files = futures::future::try_join_all(fetch_futs).await?;
122 let files = maybe_files.into_iter().flatten().collect();
123
124 Ok(files)
125 }
126
127 fn encrypt_json<T: Serialize>(
129 &self,
130 file_id: VfsFileId,
131 value: &T,
132 ) -> VfsFile;
133
134 fn encrypt_ldk_writeable<W: Writeable>(
136 &self,
137 file_id: VfsFileId,
138 writeable: &W,
139 ) -> VfsFile;
140
141 fn encrypt_bytes(
146 &self,
147 file_id: VfsFileId,
148 plaintext_bytes: &[u8],
149 ) -> VfsFile;
150
151 fn decrypt_file(
153 &self,
154 expected_file_id: &VfsFileId,
155 file: VfsFile,
156 ) -> anyhow::Result<Vec<u8>>;
157
158 async fn read_json<T: DeserializeOwned>(
162 &self,
163 file_id: &VfsFileId,
164 ) -> anyhow::Result<Option<T>> {
165 let json_bytes = match self.read_bytes(file_id).await? {
166 Some(bytes) => bytes,
167 None => return Ok(None),
168 };
169 let value = serde_json::from_slice(json_bytes.as_slice())
170 .with_context(|| format!("{file_id}"))
171 .context("JSON deserialization failed")?;
172 Ok(Some(value))
173 }
174
175 async fn read_dir_json<T: DeserializeOwned>(
177 &self,
178 dir: &VfsDirectory,
179 ) -> anyhow::Result<Vec<(VfsFileId, T)>> {
180 let ids_and_bytes = self.read_dir_bytes(dir).await?;
181 let mut ids_and_values = Vec::with_capacity(ids_and_bytes.len());
182 for (file_id, bytes) in ids_and_bytes {
183 let value = serde_json::from_slice(bytes.as_slice())
184 .with_context(|| format!("{file_id}"))
185 .context("JSON deserialization failed (in dir)")?;
186 ids_and_values.push((file_id, value));
187 }
188 Ok(ids_and_values)
189 }
190
191 async fn read_readableargs<T, A>(
194 &self,
195 file_id: &VfsFileId,
196 read_args: A,
197 ) -> anyhow::Result<Option<T>>
198 where
199 T: ReadableArgs<A>,
200 A: Send,
201 {
202 let bytes = match self.read_bytes(file_id).await? {
203 Some(b) => b,
204 None => return Ok(None),
205 };
206
207 let value = Self::deser_readableargs(file_id, &bytes, read_args)?;
208
209 Ok(Some(value))
210 }
211
212 async fn read_dir_maybereadable<T: MaybeReadable>(
216 &self,
217 dir: &VfsDirectory,
218 ) -> anyhow::Result<Vec<(VfsFileId, T)>> {
219 let ids_and_bytes = self.read_dir_bytes(dir).await?;
220 let mut ids_and_values = Vec::with_capacity(ids_and_bytes.len());
221 for (file_id, bytes) in ids_and_bytes {
222 let mut reader = Cursor::new(&bytes);
223 let maybe_value = T::read(&mut reader)
224 .map_err(|e| anyhow!("{e:?}"))
225 .with_context(|| format!("{file_id}"))
226 .context("LDK MaybeReadable deserialization failed (in dir)")?;
227 if let Some(event) = maybe_value {
228 ids_and_values.push((file_id, event));
229 }
230 }
231 Ok(ids_and_values)
232 }
233
234 async fn read_bytes(
236 &self,
237 file_id: &VfsFileId,
238 ) -> anyhow::Result<Option<Vec<u8>>> {
239 match self.read_file(file_id).await? {
240 Some(file) => {
241 let data = self.decrypt_file(file_id, file)?;
242 Ok(Some(data))
243 }
244 None => Ok(None),
245 }
246 }
247
248 async fn read_dir_bytes(
251 &self,
252 dir: &VfsDirectory,
253 ) -> anyhow::Result<Vec<(VfsFileId, Vec<u8>)>> {
254 let files = self.read_dir_files(dir).await?;
255 let file_ids_and_bytes = files
256 .into_iter()
257 .map(|file| {
258 let expected_file_id = VfsFileId::new(
260 dir.dirname.clone(),
261 file.id.filename.clone(),
262 );
263 let bytes = self.decrypt_file(&expected_file_id, file)?;
264 Ok((expected_file_id, bytes))
265 })
266 .collect::<anyhow::Result<Vec<(VfsFileId, Vec<u8>)>>>()?;
267 Ok(file_ids_and_bytes)
268 }
269
270 async fn read_file(
272 &self,
273 file_id: &VfsFileId,
274 ) -> anyhow::Result<Option<VfsFile>> {
275 debug!("Reading file {file_id}");
276 let result = self
277 .get_file(file_id)
278 .await
279 .with_context(|| format!("Couldn't fetch file from DB: {file_id}"));
280
281 if result.is_ok() {
282 debug!("Done: Read {file_id}");
283 } else {
284 warn!("Error: Failed to read {file_id}");
285 }
286 result
287 }
288
289 async fn read_dir_files(
291 &self,
292 dir: &VfsDirectory,
293 ) -> anyhow::Result<Vec<VfsFile>> {
294 debug!("Reading directory {dir}");
295 let result = self
296 .get_directory(dir)
297 .await
298 .with_context(|| format!("Couldn't fetch VFS dir from DB: {dir}"));
299
300 if result.is_ok() {
301 debug!("Done: Read directory {dir}");
302 } else {
303 warn!("Error: Failed to read directory {dir}");
304 }
305 result
306 }
307
308 fn deser_readableargs<T, A>(
310 file_id: &VfsFileId,
311 bytes: &[u8],
312 read_args: A,
313 ) -> anyhow::Result<T>
314 where
315 T: ReadableArgs<A>,
316 A: Send,
317 {
318 let mut reader = Cursor::new(bytes);
319 let value = T::read(&mut reader, read_args)
320 .map_err(|e| anyhow!("{e:?}"))
321 .with_context(|| format!("{file_id}"))
322 .context("LDK ReadableArgs deserialization failed")?;
323 Ok(value)
324 }
325
326 async fn persist_json<T: Serialize + Send + Sync>(
328 &self,
329 file_id: VfsFileId,
330 value: &T,
331 retries: usize,
332 ) -> anyhow::Result<()> {
333 let file = self.encrypt_json::<T>(file_id, value);
334 self.persist_file(file, retries).await
335 }
336
337 async fn persist_ldk_writeable<W: Writeable + Send + Sync>(
339 &self,
340 file_id: VfsFileId,
341 writeable: &W,
342 retries: usize,
343 ) -> anyhow::Result<()> {
344 let file = self.encrypt_ldk_writeable(file_id, writeable);
345 self.persist_file(file, retries).await
346 }
347
348 async fn persist_bytes(
353 &self,
354 file_id: VfsFileId,
355 plaintext_bytes: &[u8],
356 retries: usize,
357 ) -> anyhow::Result<()> {
358 let file = self.encrypt_bytes(file_id, plaintext_bytes);
359 self.persist_file(file, retries).await
360 }
361
362 async fn persist_file(
364 &self,
365 file: VfsFile,
366 retries: usize,
367 ) -> anyhow::Result<()> {
368 let file_id = &file.id;
369 let bytes = file.data.len();
370 debug!("Persisting file {file_id} <{bytes} bytes>");
371
372 let result = self
373 .upsert_file(&file.id, file.data.into(), retries)
374 .await
375 .map(|_| ())
376 .with_context(|| format!("Couldn't persist file to DB: {file_id}"));
377
378 if result.is_ok() {
379 debug!("Done: Persisted {file_id} <{bytes} bytes>");
380 } else {
381 warn!("Error: Failed to persist {file_id} <{bytes} bytes>");
382 }
383 result
384 }
385
386 async fn remove_file(&self, file_id: &VfsFileId) -> anyhow::Result<()> {
388 debug!("Deleting file {file_id}");
389 let result = self
390 .delete_file(file_id)
391 .await
392 .map(|_| ())
393 .with_context(|| format!("{file_id}"))
394 .context("Couldn't delete file from DB");
395
396 if result.is_ok() {
397 debug!("Done: Deleted {file_id}");
398 } else {
399 warn!("Error: Failed to delete {file_id}");
400 }
401 result
402 }
403}
404
405#[derive(Clone, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)]
412#[derive(Serialize, Deserialize)]
413pub struct VfsDirectory {
414 pub dirname: Cow<'static, str>,
415}
416
417#[derive(Clone, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)]
422#[derive(Serialize, Deserialize)]
423pub struct VfsFileId {
424 #[serde(flatten)]
426 pub dir: VfsDirectory,
427 pub filename: Cow<'static, str>,
428}
429
430#[derive(Clone, Debug, Eq, PartialEq)]
433#[derive(Serialize, Deserialize)]
434pub struct VfsFile {
435 #[serde(flatten)]
436 pub id: VfsFileId,
437 #[serde(with = "hexstr_or_bytes")]
438 pub data: Vec<u8>,
439}
440
441#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
443pub struct MaybeVfsFile {
444 pub maybe_file: Option<VfsFile>,
445}
446
447#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
449pub struct VecVfsFile {
450 pub files: Vec<VfsFile>,
451}
452
453#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
455pub struct VfsDirectoryList {
456 pub dirname: Cow<'static, str>,
457 pub filenames: Vec<String>,
458}
459
460#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
463pub struct VecVfsFileId {
464 pub file_ids: Vec<VfsFileId>,
465}
466
467impl VfsDirectory {
468 pub fn new(dirname: impl Into<Cow<'static, str>>) -> Self {
469 Self {
470 dirname: dirname.into(),
471 }
472 }
473
474 pub const fn new_const(dirname: &'static str) -> Self {
475 Self {
476 dirname: Cow::Borrowed(dirname),
477 }
478 }
479}
480
481impl VfsFileId {
482 pub fn new(
483 dirname: impl Into<Cow<'static, str>>,
484 filename: impl Into<Cow<'static, str>>,
485 ) -> Self {
486 Self {
487 dir: VfsDirectory {
488 dirname: dirname.into(),
489 },
490 filename: filename.into(),
491 }
492 }
493
494 pub const fn new_const(
495 dirname: &'static str,
496 filename: &'static str,
497 ) -> Self {
498 Self {
499 dir: VfsDirectory {
500 dirname: Cow::Borrowed(dirname),
501 },
502 filename: Cow::Borrowed(filename),
503 }
504 }
505}
506
507impl VfsFile {
508 pub fn new(
509 dirname: impl Into<Cow<'static, str>>,
510 filename: impl Into<Cow<'static, str>>,
511 data: Vec<u8>,
512 ) -> Self {
513 Self {
514 id: VfsFileId {
515 dir: VfsDirectory {
516 dirname: dirname.into(),
517 },
518 filename: filename.into(),
519 },
520 data,
521 }
522 }
523
524 pub fn from_parts(id: VfsFileId, data: impl Into<Vec<u8>>) -> Self {
528 Self {
529 id,
530 data: data.into(),
531 }
532 }
533}
534
535impl Display for VfsDirectory {
536 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
537 write!(f, "{dirname}", dirname = self.dirname)
538 }
539}
540
541impl Display for VfsFileId {
542 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
543 let dirname = &self.dir.dirname;
544 let filename = &self.filename;
545 write!(f, "{dirname}/{filename}")
546 }
547}
548
549#[cfg(any(test, feature = "test-utils"))]
552mod prop {
553 use lexe_common::test_utils::arbitrary;
554 use proptest::{
555 arbitrary::{Arbitrary, any},
556 strategy::{BoxedStrategy, Strategy},
557 };
558
559 use super::*;
560
561 impl Arbitrary for VfsDirectory {
562 type Strategy = BoxedStrategy<Self>;
563 type Parameters = ();
564
565 fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
566 arbitrary::any_string().prop_map(VfsDirectory::new).boxed()
567 }
568 }
569
570 impl Arbitrary for VfsFileId {
571 type Strategy = BoxedStrategy<Self>;
572 type Parameters = ();
573
574 fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
575 (any::<VfsDirectory>(), arbitrary::any_string())
576 .prop_map(|(dir, filename)| VfsFileId {
577 dir,
578 filename: filename.into(),
579 })
580 .boxed()
581 }
582 }
583}
584
585#[cfg(test)]
586mod test {
587 use lexe_common::test_utils::roundtrip;
588
589 use super::*;
590
591 #[test]
592 fn vfs_directory_roundtrip() {
593 roundtrip::query_string_roundtrip_proptest::<VfsDirectory>();
594 }
595
596 #[test]
597 fn vfs_file_id_roundtrip() {
598 roundtrip::query_string_roundtrip_proptest::<VfsFileId>();
599 }
600}