Skip to content

Commit 06a956a

Browse files
committed
feat!: change load_from_persistence to return an option
`PersistBackend::is_empty` is removed. Instead, `load_from_persistence` returns an option of the changeset. `None` means persistence is empty. This is a better API than a separate method. We can now differentiate between a persisted single changeset and nothing persisted. `Store::aggregate_changeset` is refactored to return a `Result` instead of a tuple. A new error type (`AggregateChangesetsError`) is introduced to include the partially-aggregated changeset in the error. This is a more idiomatic API.
1 parent c3265e2 commit 06a956a

File tree

4 files changed

+110
-86
lines changed

4 files changed

+110
-86
lines changed

crates/bdk/src/wallet/mod.rs

Lines changed: 64 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,8 @@ pub enum LoadError<L> {
293293
Descriptor(crate::descriptor::DescriptorError),
294294
/// Loading data from the persistence backend failed.
295295
Load(L),
296+
/// Wallet not initialized, persistence backend is empty.
297+
NotInitialized,
296298
/// Data loaded from persistence is missing network type.
297299
MissingNetwork,
298300
/// Data loaded from persistence is missing genesis hash.
@@ -307,6 +309,9 @@ where
307309
match self {
308310
LoadError::Descriptor(e) => e.fmt(f),
309311
LoadError::Load(e) => e.fmt(f),
312+
LoadError::NotInitialized => {
313+
write!(f, "wallet is not initialized, persistence backend is empty")
314+
}
310315
LoadError::MissingNetwork => write!(f, "loaded data is missing network type"),
311316
LoadError::MissingGenesis => write!(f, "loaded data is missing genesis hash"),
312317
}
@@ -330,6 +335,8 @@ pub enum NewOrLoadError<W, L> {
330335
Write(W),
331336
/// Loading from the persistence backend failed.
332337
Load(L),
338+
/// Wallet is not initialized, persistence backend is empty.
339+
NotInitialized,
333340
/// The loaded genesis hash does not match what was provided.
334341
LoadedGenesisDoesNotMatch {
335342
/// The expected genesis block hash.
@@ -356,6 +363,9 @@ where
356363
NewOrLoadError::Descriptor(e) => e.fmt(f),
357364
NewOrLoadError::Write(e) => write!(f, "failed to write to persistence: {}", e),
358365
NewOrLoadError::Load(e) => write!(f, "failed to load from persistence: {}", e),
366+
NewOrLoadError::NotInitialized => {
367+
write!(f, "wallet is not initialized, persistence backend is empty")
368+
}
359369
NewOrLoadError::LoadedGenesisDoesNotMatch { expected, got } => {
360370
write!(f, "loaded genesis hash is not {}, got {:?}", expected, got)
361371
}
@@ -451,11 +461,26 @@ impl<D> Wallet<D> {
451461
change_descriptor: Option<E>,
452462
mut db: D,
453463
) -> Result<Self, LoadError<D::LoadError>>
464+
where
465+
D: PersistBackend<ChangeSet>,
466+
{
467+
let changeset = db
468+
.load_from_persistence()
469+
.map_err(LoadError::Load)?
470+
.ok_or(LoadError::NotInitialized)?;
471+
Self::load_from_changeset(descriptor, change_descriptor, db, changeset)
472+
}
473+
474+
fn load_from_changeset<E: IntoWalletDescriptor>(
475+
descriptor: E,
476+
change_descriptor: Option<E>,
477+
db: D,
478+
changeset: ChangeSet,
479+
) -> Result<Self, LoadError<D::LoadError>>
454480
where
455481
D: PersistBackend<ChangeSet>,
456482
{
457483
let secp = Secp256k1::new();
458-
let changeset = db.load_from_persistence().map_err(LoadError::Load)?;
459484
let network = changeset.network.ok_or(LoadError::MissingNetwork)?;
460485
let chain =
461486
LocalChain::from_changeset(changeset.chain).map_err(|_| LoadError::MissingGenesis)?;
@@ -517,8 +542,43 @@ impl<D> Wallet<D> {
517542
where
518543
D: PersistBackend<ChangeSet>,
519544
{
520-
if db.is_empty().map_err(NewOrLoadError::Load)? {
521-
return Self::new_with_genesis_hash(
545+
let changeset = db.load_from_persistence().map_err(NewOrLoadError::Load)?;
546+
match changeset {
547+
Some(changeset) => {
548+
let wallet =
549+
Self::load_from_changeset(descriptor, change_descriptor, db, changeset)
550+
.map_err(|e| match e {
551+
LoadError::Descriptor(e) => NewOrLoadError::Descriptor(e),
552+
LoadError::Load(e) => NewOrLoadError::Load(e),
553+
LoadError::NotInitialized => NewOrLoadError::NotInitialized,
554+
LoadError::MissingNetwork => {
555+
NewOrLoadError::LoadedNetworkDoesNotMatch {
556+
expected: network,
557+
got: None,
558+
}
559+
}
560+
LoadError::MissingGenesis => {
561+
NewOrLoadError::LoadedGenesisDoesNotMatch {
562+
expected: genesis_hash,
563+
got: None,
564+
}
565+
}
566+
})?;
567+
if wallet.network != network {
568+
return Err(NewOrLoadError::LoadedNetworkDoesNotMatch {
569+
expected: network,
570+
got: Some(wallet.network),
571+
});
572+
}
573+
if wallet.chain.genesis_hash() != genesis_hash {
574+
return Err(NewOrLoadError::LoadedGenesisDoesNotMatch {
575+
expected: genesis_hash,
576+
got: Some(wallet.chain.genesis_hash()),
577+
});
578+
}
579+
Ok(wallet)
580+
}
581+
None => Self::new_with_genesis_hash(
522582
descriptor,
523583
change_descriptor,
524584
db,
@@ -528,34 +588,8 @@ impl<D> Wallet<D> {
528588
.map_err(|e| match e {
529589
NewError::Descriptor(e) => NewOrLoadError::Descriptor(e),
530590
NewError::Write(e) => NewOrLoadError::Write(e),
531-
});
532-
}
533-
534-
let wallet = Self::load(descriptor, change_descriptor, db).map_err(|e| match e {
535-
LoadError::Descriptor(e) => NewOrLoadError::Descriptor(e),
536-
LoadError::Load(e) => NewOrLoadError::Load(e),
537-
LoadError::MissingNetwork => NewOrLoadError::LoadedNetworkDoesNotMatch {
538-
expected: network,
539-
got: None,
540-
},
541-
LoadError::MissingGenesis => NewOrLoadError::LoadedGenesisDoesNotMatch {
542-
expected: genesis_hash,
543-
got: None,
544-
},
545-
})?;
546-
if wallet.network != network {
547-
return Err(NewOrLoadError::LoadedNetworkDoesNotMatch {
548-
expected: network,
549-
got: Some(wallet.network),
550-
});
551-
}
552-
if wallet.chain.genesis_hash() != genesis_hash {
553-
return Err(NewOrLoadError::LoadedGenesisDoesNotMatch {
554-
expected: genesis_hash,
555-
got: Some(wallet.chain.genesis_hash()),
556-
});
591+
}),
557592
}
558-
Ok(wallet)
559593
}
560594

561595
/// Get the Bitcoin network the wallet is using.

crates/chain/src/persist.rs

Lines changed: 4 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -79,19 +79,10 @@ pub trait PersistBackend<C> {
7979
fn write_changes(&mut self, changeset: &C) -> Result<(), Self::WriteError>;
8080

8181
/// Return the aggregate changeset `C` from persistence.
82-
fn load_from_persistence(&mut self) -> Result<C, Self::LoadError>;
83-
84-
/// Returns whether the persistence backend contains no data.
85-
fn is_empty(&mut self) -> Result<bool, Self::LoadError>
86-
where
87-
C: Append,
88-
{
89-
self.load_from_persistence()
90-
.map(|changeset| changeset.is_empty())
91-
}
82+
fn load_from_persistence(&mut self) -> Result<Option<C>, Self::LoadError>;
9283
}
9384

94-
impl<C: Default> PersistBackend<C> for () {
85+
impl<C> PersistBackend<C> for () {
9586
type WriteError = Infallible;
9687

9788
type LoadError = Infallible;
@@ -100,11 +91,7 @@ impl<C: Default> PersistBackend<C> for () {
10091
Ok(())
10192
}
10293

103-
fn load_from_persistence(&mut self) -> Result<C, Self::LoadError> {
104-
Ok(C::default())
105-
}
106-
107-
fn is_empty(&mut self) -> Result<bool, Self::LoadError> {
108-
Ok(true)
94+
fn load_from_persistence(&mut self) -> Result<Option<C>, Self::LoadError> {
95+
Ok(None)
10996
}
11097
}

crates/file_store/src/store.rs

Lines changed: 41 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ pub struct Store<'a, C> {
2323

2424
impl<'a, C> PersistBackend<C> for Store<'a, C>
2525
where
26-
C: Default + Append + serde::Serialize + serde::de::DeserializeOwned,
26+
C: Append + serde::Serialize + serde::de::DeserializeOwned,
2727
{
2828
type WriteError = std::io::Error;
2929

@@ -33,23 +33,14 @@ where
3333
self.append_changeset(changeset)
3434
}
3535

36-
fn load_from_persistence(&mut self) -> Result<C, Self::LoadError> {
37-
let (changeset, result) = self.aggregate_changesets();
38-
result.map(|_| changeset)
39-
}
40-
41-
fn is_empty(&mut self) -> Result<bool, Self::LoadError> {
42-
let init_pos = self.db_file.stream_position()?;
43-
let stream_len = self.db_file.seek(io::SeekFrom::End(0))?;
44-
let magic_len = self.magic.len() as u64;
45-
self.db_file.seek(io::SeekFrom::Start(init_pos))?;
46-
Ok(stream_len == magic_len)
36+
fn load_from_persistence(&mut self) -> Result<Option<C>, Self::LoadError> {
37+
self.aggregate_changesets().map_err(|e| e.iter_error)
4738
}
4839
}
4940

5041
impl<'a, C> Store<'a, C>
5142
where
52-
C: Default + Append + serde::Serialize + serde::de::DeserializeOwned,
43+
C: Append + serde::Serialize + serde::de::DeserializeOwned,
5344
{
5445
/// Create a new [`Store`] file in write-only mode; error if the file exists.
5546
///
@@ -156,16 +147,24 @@ where
156147
///
157148
/// **WARNING**: This method changes the write position of the underlying file. The next
158149
/// changeset will be written over the erroring entry (or the end of the file if none existed).
159-
pub fn aggregate_changesets(&mut self) -> (C, Result<(), IterError>) {
160-
let mut changeset = C::default();
161-
let result = (|| {
162-
for next_changeset in self.iter_changesets() {
163-
changeset.append(next_changeset?);
150+
pub fn aggregate_changesets(&mut self) -> Result<Option<C>, AggregateChangesetsError<C>> {
151+
let mut changeset = Option::<C>::None;
152+
for next_changeset in self.iter_changesets() {
153+
let next_changeset = match next_changeset {
154+
Ok(next_changeset) => next_changeset,
155+
Err(iter_error) => {
156+
return Err(AggregateChangesetsError {
157+
changeset,
158+
iter_error,
159+
})
160+
}
161+
};
162+
match &mut changeset {
163+
Some(changeset) => changeset.append(next_changeset),
164+
changeset => *changeset = Some(next_changeset),
164165
}
165-
Ok(())
166-
})();
167-
168-
(changeset, result)
166+
}
167+
Ok(changeset)
169168
}
170169

171170
/// Append a new changeset to the file and truncate the file to the end of the appended
@@ -196,6 +195,24 @@ where
196195
}
197196
}
198197

198+
/// Error type for [`Store::aggregate_changesets`].
199+
#[derive(Debug)]
200+
pub struct AggregateChangesetsError<C> {
201+
/// The partially-aggregated changeset.
202+
pub changeset: Option<C>,
203+
204+
/// The error returned by [`EntryIter`].
205+
pub iter_error: IterError,
206+
}
207+
208+
impl<C> std::fmt::Display for AggregateChangesetsError<C> {
209+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
210+
std::fmt::Display::fmt(&self.iter_error, f)
211+
}
212+
}
213+
214+
impl<C: std::fmt::Debug> std::error::Error for AggregateChangesetsError<C> {}
215+
199216
#[cfg(test)]
200217
mod test {
201218
use super::*;
@@ -248,25 +265,11 @@ mod test {
248265
{
249266
let mut db = Store::<TestChangeSet>::open_or_create_new(&TEST_MAGIC_BYTES, &file_path)
250267
.expect("must recover");
251-
let (recovered_changeset, r) = db.aggregate_changesets();
252-
r.expect("must succeed");
253-
assert_eq!(recovered_changeset, changeset);
268+
let recovered_changeset = db.aggregate_changesets().expect("must succeed");
269+
assert_eq!(recovered_changeset, Some(changeset));
254270
}
255271
}
256272

257-
#[test]
258-
fn is_empty() {
259-
let mut file = NamedTempFile::new().unwrap();
260-
file.write_all(&TEST_MAGIC_BYTES).expect("should write");
261-
262-
let mut db =
263-
Store::<TestChangeSet>::open(&TEST_MAGIC_BYTES, file.path()).expect("must open");
264-
assert!(db.is_empty().expect("must read"));
265-
db.write_changes(&vec!["hello".to_string(), "world".to_string()])
266-
.expect("must write");
267-
assert!(!db.is_empty().expect("must read"));
268-
}
269-
270273
#[test]
271274
fn new_fails_if_file_is_too_short() {
272275
let mut file = NamedTempFile::new().unwrap();

example-crates/example_cli/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -687,7 +687,7 @@ where
687687
Err(err) => return Err(anyhow::anyhow!("failed to init db backend: {:?}", err)),
688688
};
689689

690-
let init_changeset = db_backend.load_from_persistence()?;
690+
let init_changeset = db_backend.load_from_persistence()?.unwrap_or_default();
691691

692692
Ok((
693693
args,

0 commit comments

Comments
 (0)