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.
This commit is contained in:
parent
c3265e2514
commit
06a956ad20
@ -293,6 +293,8 @@ pub enum LoadError<L> {
|
|||||||
Descriptor(crate::descriptor::DescriptorError),
|
Descriptor(crate::descriptor::DescriptorError),
|
||||||
/// Loading data from the persistence backend failed.
|
/// Loading data from the persistence backend failed.
|
||||||
Load(L),
|
Load(L),
|
||||||
|
/// Wallet not initialized, persistence backend is empty.
|
||||||
|
NotInitialized,
|
||||||
/// Data loaded from persistence is missing network type.
|
/// Data loaded from persistence is missing network type.
|
||||||
MissingNetwork,
|
MissingNetwork,
|
||||||
/// Data loaded from persistence is missing genesis hash.
|
/// Data loaded from persistence is missing genesis hash.
|
||||||
@ -307,6 +309,9 @@ where
|
|||||||
match self {
|
match self {
|
||||||
LoadError::Descriptor(e) => e.fmt(f),
|
LoadError::Descriptor(e) => e.fmt(f),
|
||||||
LoadError::Load(e) => e.fmt(f),
|
LoadError::Load(e) => e.fmt(f),
|
||||||
|
LoadError::NotInitialized => {
|
||||||
|
write!(f, "wallet is not initialized, persistence backend is empty")
|
||||||
|
}
|
||||||
LoadError::MissingNetwork => write!(f, "loaded data is missing network type"),
|
LoadError::MissingNetwork => write!(f, "loaded data is missing network type"),
|
||||||
LoadError::MissingGenesis => write!(f, "loaded data is missing genesis hash"),
|
LoadError::MissingGenesis => write!(f, "loaded data is missing genesis hash"),
|
||||||
}
|
}
|
||||||
@ -330,6 +335,8 @@ pub enum NewOrLoadError<W, L> {
|
|||||||
Write(W),
|
Write(W),
|
||||||
/// Loading from the persistence backend failed.
|
/// Loading from the persistence backend failed.
|
||||||
Load(L),
|
Load(L),
|
||||||
|
/// Wallet is not initialized, persistence backend is empty.
|
||||||
|
NotInitialized,
|
||||||
/// The loaded genesis hash does not match what was provided.
|
/// The loaded genesis hash does not match what was provided.
|
||||||
LoadedGenesisDoesNotMatch {
|
LoadedGenesisDoesNotMatch {
|
||||||
/// The expected genesis block hash.
|
/// The expected genesis block hash.
|
||||||
@ -356,6 +363,9 @@ where
|
|||||||
NewOrLoadError::Descriptor(e) => e.fmt(f),
|
NewOrLoadError::Descriptor(e) => e.fmt(f),
|
||||||
NewOrLoadError::Write(e) => write!(f, "failed to write to persistence: {}", e),
|
NewOrLoadError::Write(e) => write!(f, "failed to write to persistence: {}", e),
|
||||||
NewOrLoadError::Load(e) => write!(f, "failed to load from persistence: {}", e),
|
NewOrLoadError::Load(e) => write!(f, "failed to load from persistence: {}", e),
|
||||||
|
NewOrLoadError::NotInitialized => {
|
||||||
|
write!(f, "wallet is not initialized, persistence backend is empty")
|
||||||
|
}
|
||||||
NewOrLoadError::LoadedGenesisDoesNotMatch { expected, got } => {
|
NewOrLoadError::LoadedGenesisDoesNotMatch { expected, got } => {
|
||||||
write!(f, "loaded genesis hash is not {}, got {:?}", expected, got)
|
write!(f, "loaded genesis hash is not {}, got {:?}", expected, got)
|
||||||
}
|
}
|
||||||
@ -451,11 +461,26 @@ impl<D> Wallet<D> {
|
|||||||
change_descriptor: Option<E>,
|
change_descriptor: Option<E>,
|
||||||
mut db: D,
|
mut db: D,
|
||||||
) -> Result<Self, LoadError<D::LoadError>>
|
) -> Result<Self, LoadError<D::LoadError>>
|
||||||
|
where
|
||||||
|
D: PersistBackend<ChangeSet>,
|
||||||
|
{
|
||||||
|
let changeset = db
|
||||||
|
.load_from_persistence()
|
||||||
|
.map_err(LoadError::Load)?
|
||||||
|
.ok_or(LoadError::NotInitialized)?;
|
||||||
|
Self::load_from_changeset(descriptor, change_descriptor, db, changeset)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_from_changeset<E: IntoWalletDescriptor>(
|
||||||
|
descriptor: E,
|
||||||
|
change_descriptor: Option<E>,
|
||||||
|
db: D,
|
||||||
|
changeset: ChangeSet,
|
||||||
|
) -> Result<Self, LoadError<D::LoadError>>
|
||||||
where
|
where
|
||||||
D: PersistBackend<ChangeSet>,
|
D: PersistBackend<ChangeSet>,
|
||||||
{
|
{
|
||||||
let secp = Secp256k1::new();
|
let secp = Secp256k1::new();
|
||||||
let changeset = db.load_from_persistence().map_err(LoadError::Load)?;
|
|
||||||
let network = changeset.network.ok_or(LoadError::MissingNetwork)?;
|
let network = changeset.network.ok_or(LoadError::MissingNetwork)?;
|
||||||
let chain =
|
let chain =
|
||||||
LocalChain::from_changeset(changeset.chain).map_err(|_| LoadError::MissingGenesis)?;
|
LocalChain::from_changeset(changeset.chain).map_err(|_| LoadError::MissingGenesis)?;
|
||||||
@ -517,8 +542,43 @@ impl<D> Wallet<D> {
|
|||||||
where
|
where
|
||||||
D: PersistBackend<ChangeSet>,
|
D: PersistBackend<ChangeSet>,
|
||||||
{
|
{
|
||||||
if db.is_empty().map_err(NewOrLoadError::Load)? {
|
let changeset = db.load_from_persistence().map_err(NewOrLoadError::Load)?;
|
||||||
return Self::new_with_genesis_hash(
|
match changeset {
|
||||||
|
Some(changeset) => {
|
||||||
|
let wallet =
|
||||||
|
Self::load_from_changeset(descriptor, change_descriptor, db, changeset)
|
||||||
|
.map_err(|e| match e {
|
||||||
|
LoadError::Descriptor(e) => NewOrLoadError::Descriptor(e),
|
||||||
|
LoadError::Load(e) => NewOrLoadError::Load(e),
|
||||||
|
LoadError::NotInitialized => NewOrLoadError::NotInitialized,
|
||||||
|
LoadError::MissingNetwork => {
|
||||||
|
NewOrLoadError::LoadedNetworkDoesNotMatch {
|
||||||
|
expected: network,
|
||||||
|
got: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LoadError::MissingGenesis => {
|
||||||
|
NewOrLoadError::LoadedGenesisDoesNotMatch {
|
||||||
|
expected: genesis_hash,
|
||||||
|
got: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
if wallet.network != network {
|
||||||
|
return Err(NewOrLoadError::LoadedNetworkDoesNotMatch {
|
||||||
|
expected: network,
|
||||||
|
got: Some(wallet.network),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if wallet.chain.genesis_hash() != genesis_hash {
|
||||||
|
return Err(NewOrLoadError::LoadedGenesisDoesNotMatch {
|
||||||
|
expected: genesis_hash,
|
||||||
|
got: Some(wallet.chain.genesis_hash()),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(wallet)
|
||||||
|
}
|
||||||
|
None => Self::new_with_genesis_hash(
|
||||||
descriptor,
|
descriptor,
|
||||||
change_descriptor,
|
change_descriptor,
|
||||||
db,
|
db,
|
||||||
@ -528,34 +588,8 @@ impl<D> Wallet<D> {
|
|||||||
.map_err(|e| match e {
|
.map_err(|e| match e {
|
||||||
NewError::Descriptor(e) => NewOrLoadError::Descriptor(e),
|
NewError::Descriptor(e) => NewOrLoadError::Descriptor(e),
|
||||||
NewError::Write(e) => NewOrLoadError::Write(e),
|
NewError::Write(e) => NewOrLoadError::Write(e),
|
||||||
});
|
}),
|
||||||
}
|
}
|
||||||
|
|
||||||
let wallet = Self::load(descriptor, change_descriptor, db).map_err(|e| match e {
|
|
||||||
LoadError::Descriptor(e) => NewOrLoadError::Descriptor(e),
|
|
||||||
LoadError::Load(e) => NewOrLoadError::Load(e),
|
|
||||||
LoadError::MissingNetwork => NewOrLoadError::LoadedNetworkDoesNotMatch {
|
|
||||||
expected: network,
|
|
||||||
got: None,
|
|
||||||
},
|
|
||||||
LoadError::MissingGenesis => NewOrLoadError::LoadedGenesisDoesNotMatch {
|
|
||||||
expected: genesis_hash,
|
|
||||||
got: None,
|
|
||||||
},
|
|
||||||
})?;
|
|
||||||
if wallet.network != network {
|
|
||||||
return Err(NewOrLoadError::LoadedNetworkDoesNotMatch {
|
|
||||||
expected: network,
|
|
||||||
got: Some(wallet.network),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if wallet.chain.genesis_hash() != genesis_hash {
|
|
||||||
return Err(NewOrLoadError::LoadedGenesisDoesNotMatch {
|
|
||||||
expected: genesis_hash,
|
|
||||||
got: Some(wallet.chain.genesis_hash()),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Ok(wallet)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the Bitcoin network the wallet is using.
|
/// Get the Bitcoin network the wallet is using.
|
||||||
|
@ -79,19 +79,10 @@ pub trait PersistBackend<C> {
|
|||||||
fn write_changes(&mut self, changeset: &C) -> Result<(), Self::WriteError>;
|
fn write_changes(&mut self, changeset: &C) -> Result<(), Self::WriteError>;
|
||||||
|
|
||||||
/// Return the aggregate changeset `C` from persistence.
|
/// Return the aggregate changeset `C` from persistence.
|
||||||
fn load_from_persistence(&mut self) -> Result<C, Self::LoadError>;
|
fn load_from_persistence(&mut self) -> Result<Option<C>, Self::LoadError>;
|
||||||
|
|
||||||
/// Returns whether the persistence backend contains no data.
|
|
||||||
fn is_empty(&mut self) -> Result<bool, Self::LoadError>
|
|
||||||
where
|
|
||||||
C: Append,
|
|
||||||
{
|
|
||||||
self.load_from_persistence()
|
|
||||||
.map(|changeset| changeset.is_empty())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<C: Default> PersistBackend<C> for () {
|
impl<C> PersistBackend<C> for () {
|
||||||
type WriteError = Infallible;
|
type WriteError = Infallible;
|
||||||
|
|
||||||
type LoadError = Infallible;
|
type LoadError = Infallible;
|
||||||
@ -100,11 +91,7 @@ impl<C: Default> PersistBackend<C> for () {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_from_persistence(&mut self) -> Result<C, Self::LoadError> {
|
fn load_from_persistence(&mut self) -> Result<Option<C>, Self::LoadError> {
|
||||||
Ok(C::default())
|
Ok(None)
|
||||||
}
|
|
||||||
|
|
||||||
fn is_empty(&mut self) -> Result<bool, Self::LoadError> {
|
|
||||||
Ok(true)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,7 +23,7 @@ pub struct Store<'a, C> {
|
|||||||
|
|
||||||
impl<'a, C> PersistBackend<C> for Store<'a, C>
|
impl<'a, C> PersistBackend<C> for Store<'a, C>
|
||||||
where
|
where
|
||||||
C: Default + Append + serde::Serialize + serde::de::DeserializeOwned,
|
C: Append + serde::Serialize + serde::de::DeserializeOwned,
|
||||||
{
|
{
|
||||||
type WriteError = std::io::Error;
|
type WriteError = std::io::Error;
|
||||||
|
|
||||||
@ -33,23 +33,14 @@ where
|
|||||||
self.append_changeset(changeset)
|
self.append_changeset(changeset)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_from_persistence(&mut self) -> Result<C, Self::LoadError> {
|
fn load_from_persistence(&mut self) -> Result<Option<C>, Self::LoadError> {
|
||||||
let (changeset, result) = self.aggregate_changesets();
|
self.aggregate_changesets().map_err(|e| e.iter_error)
|
||||||
result.map(|_| changeset)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_empty(&mut self) -> Result<bool, Self::LoadError> {
|
|
||||||
let init_pos = self.db_file.stream_position()?;
|
|
||||||
let stream_len = self.db_file.seek(io::SeekFrom::End(0))?;
|
|
||||||
let magic_len = self.magic.len() as u64;
|
|
||||||
self.db_file.seek(io::SeekFrom::Start(init_pos))?;
|
|
||||||
Ok(stream_len == magic_len)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, C> Store<'a, C>
|
impl<'a, C> Store<'a, C>
|
||||||
where
|
where
|
||||||
C: Default + Append + serde::Serialize + serde::de::DeserializeOwned,
|
C: Append + serde::Serialize + serde::de::DeserializeOwned,
|
||||||
{
|
{
|
||||||
/// Create a new [`Store`] file in write-only mode; error if the file exists.
|
/// Create a new [`Store`] file in write-only mode; error if the file exists.
|
||||||
///
|
///
|
||||||
@ -156,16 +147,24 @@ where
|
|||||||
///
|
///
|
||||||
/// **WARNING**: This method changes the write position of the underlying file. The next
|
/// **WARNING**: This method changes the write position of the underlying file. The next
|
||||||
/// changeset will be written over the erroring entry (or the end of the file if none existed).
|
/// changeset will be written over the erroring entry (or the end of the file if none existed).
|
||||||
pub fn aggregate_changesets(&mut self) -> (C, Result<(), IterError>) {
|
pub fn aggregate_changesets(&mut self) -> Result<Option<C>, AggregateChangesetsError<C>> {
|
||||||
let mut changeset = C::default();
|
let mut changeset = Option::<C>::None;
|
||||||
let result = (|| {
|
for next_changeset in self.iter_changesets() {
|
||||||
for next_changeset in self.iter_changesets() {
|
let next_changeset = match next_changeset {
|
||||||
changeset.append(next_changeset?);
|
Ok(next_changeset) => next_changeset,
|
||||||
|
Err(iter_error) => {
|
||||||
|
return Err(AggregateChangesetsError {
|
||||||
|
changeset,
|
||||||
|
iter_error,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
match &mut changeset {
|
||||||
|
Some(changeset) => changeset.append(next_changeset),
|
||||||
|
changeset => *changeset = Some(next_changeset),
|
||||||
}
|
}
|
||||||
Ok(())
|
}
|
||||||
})();
|
Ok(changeset)
|
||||||
|
|
||||||
(changeset, result)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Append a new changeset to the file and truncate the file to the end of the appended
|
/// Append a new changeset to the file and truncate the file to the end of the appended
|
||||||
@ -196,6 +195,24 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Error type for [`Store::aggregate_changesets`].
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct AggregateChangesetsError<C> {
|
||||||
|
/// The partially-aggregated changeset.
|
||||||
|
pub changeset: Option<C>,
|
||||||
|
|
||||||
|
/// The error returned by [`EntryIter`].
|
||||||
|
pub iter_error: IterError,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<C> std::fmt::Display for AggregateChangesetsError<C> {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
std::fmt::Display::fmt(&self.iter_error, f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<C: std::fmt::Debug> std::error::Error for AggregateChangesetsError<C> {}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
@ -248,25 +265,11 @@ mod test {
|
|||||||
{
|
{
|
||||||
let mut db = Store::<TestChangeSet>::open_or_create_new(&TEST_MAGIC_BYTES, &file_path)
|
let mut db = Store::<TestChangeSet>::open_or_create_new(&TEST_MAGIC_BYTES, &file_path)
|
||||||
.expect("must recover");
|
.expect("must recover");
|
||||||
let (recovered_changeset, r) = db.aggregate_changesets();
|
let recovered_changeset = db.aggregate_changesets().expect("must succeed");
|
||||||
r.expect("must succeed");
|
assert_eq!(recovered_changeset, Some(changeset));
|
||||||
assert_eq!(recovered_changeset, changeset);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn is_empty() {
|
|
||||||
let mut file = NamedTempFile::new().unwrap();
|
|
||||||
file.write_all(&TEST_MAGIC_BYTES).expect("should write");
|
|
||||||
|
|
||||||
let mut db =
|
|
||||||
Store::<TestChangeSet>::open(&TEST_MAGIC_BYTES, file.path()).expect("must open");
|
|
||||||
assert!(db.is_empty().expect("must read"));
|
|
||||||
db.write_changes(&vec!["hello".to_string(), "world".to_string()])
|
|
||||||
.expect("must write");
|
|
||||||
assert!(!db.is_empty().expect("must read"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn new_fails_if_file_is_too_short() {
|
fn new_fails_if_file_is_too_short() {
|
||||||
let mut file = NamedTempFile::new().unwrap();
|
let mut file = NamedTempFile::new().unwrap();
|
||||||
|
@ -687,7 +687,7 @@ where
|
|||||||
Err(err) => return Err(anyhow::anyhow!("failed to init db backend: {:?}", err)),
|
Err(err) => return Err(anyhow::anyhow!("failed to init db backend: {:?}", err)),
|
||||||
};
|
};
|
||||||
|
|
||||||
let init_changeset = db_backend.load_from_persistence()?;
|
let init_changeset = db_backend.load_from_persistence()?.unwrap_or_default();
|
||||||
|
|
||||||
Ok((
|
Ok((
|
||||||
args,
|
args,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user