Repository: mintlayer/mintlayer-core
Pull request #713 (2023-02-17 00:14:17)

Added a WalletTx struct and a basic integration with the db store


Patch:
diff --git a/Cargo.lock b/Cargo.lock
index 2460d1832..0ebb48202 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -4166,8 +4166,15 @@ checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca"
 name = "wallet"
 version = "0.1.0"
 dependencies = [
+ "common",
  "logging",
+ "serialization",
+ "storage",
+ "tempfile",
+ "thiserror",
+ "utxo",
  "wallet-storage",
+ "wallet-types",
 ]
 
 [[package]]
@@ -4183,6 +4190,17 @@ dependencies = [
  "test-utils",
  "utils",
  "utxo",
+ "wallet-types",
+]
+
+[[package]]
+name = "wallet-types"
+version = "0.1.0"
+dependencies = [
+ "common",
+ "parity-scale-codec",
+ "serialization",
+ "thiserror",
 ]
 
 [[package]]
diff --git a/common/src/primitives/id/with_id.rs b/common/src/primitives/id/with_id.rs
index 40dbe0951..ef92d42dc 100644
--- a/common/src/primitives/id/with_id.rs
+++ b/common/src/primitives/id/with_id.rs
@@ -16,6 +16,7 @@
 //! ID caching mechanism
 
 use super::{Id, Idable};
+use serialization::{WrapperTypeDecode, WrapperTypeEncode};
 
 /// An object together with its pre-calculated ID.
 ///
@@ -78,12 +79,25 @@ impl<T: Idable> From<T> for WithId<T> {
     }
 }
 
+/// Implement a marker trait for decoding WithId wrapper type.
+/// The decoding of such type is assumed to be the same as
+/// the wrapped type.
+impl<T: Idable> WrapperTypeDecode for WithId<T> {
+    type Wrapped = T;
+}
+
+/// Implement a marker trait that tells the encoder that
+/// WithId is a wrapper type and only the wrapped T
+/// would be encoded and not the id property.
+impl<T: Idable> WrapperTypeEncode for WithId<T> {}
+
 #[cfg(test)]
 mod test {
     use super::*;
+    use serialization::{Decode, DecodeAll, Encode};
     use typename::TypeName;
 
-    #[derive(Eq, PartialEq, Debug, Clone, serialization::Encode, TypeName)]
+    #[derive(Eq, PartialEq, Debug, Clone, Encode, Decode, TypeName)]
     struct TestStruct {
         num: u64,
         blurb: String,
@@ -96,6 +110,21 @@ mod test {
         }
     }
 
+    #[test]
+    fn serialization() {
+        let data = TestStruct {
+            num: 1337,
+            blurb: "Hello!".into(),
+        };
+
+        let data_clone = data.clone();
+        let encoded = WithId::new(data).encode();
+        let decoded = WithId::<TestStruct>::decode_all(&mut encoded.as_slice()).unwrap();
+
+        assert_eq!(data_clone.get_id(), decoded.get_id());
+        assert_eq!(data_clone, WithId::take(decoded));
+    }
+
     #[test]
     fn owned() {
         let data = TestStruct {
diff --git a/serialization/core/src/lib.rs b/serialization/core/src/lib.rs
index 8ce5244af..2207dd722 100644
--- a/serialization/core/src/lib.rs
+++ b/serialization/core/src/lib.rs
@@ -16,7 +16,10 @@
 //! Blockchain data encoding and decoding tools
 
 // Re-export SCALE traits
-pub use parity_scale_codec::{Codec, Decode, DecodeAll, Encode, EncodeLike, Input, Output};
+pub use parity_scale_codec::{
+    Codec, Decode, DecodeAll, Encode, EncodeLike, Input, Output, WrapperTypeDecode,
+    WrapperTypeEncode,
+};
 
 // Re-export SCALE types
 pub use parity_scale_codec::{Compact, Error};
diff --git a/storage/sqlite/src/lib.rs b/storage/sqlite/src/lib.rs
index 4d687b09e..1d3e7671a 100644
--- a/storage/sqlite/src/lib.rs
+++ b/storage/sqlite/src/lib.rs
@@ -21,7 +21,7 @@ mod queries;
 use rusqlite::{Connection, OpenFlags, OptionalExtension};
 use std::borrow::Cow;
 use std::cmp::max;
-use std::path::PathBuf;
+use std::path::{Path, PathBuf};
 use std::sync::{Mutex, MutexGuard};
 
 use crate::queries::SqliteQueries;
@@ -236,9 +236,9 @@ impl Sqlite {
     }
 
     /// New Sqlite database backend
-    pub fn new(path: PathBuf) -> Self {
+    pub fn new<P: AsRef<Path>>(path: P) -> Self {
         Self {
-            backend: SqliteStorageMode::File(path),
+            backend: SqliteStorageMode::File(path.as_ref().to_path_buf()),
         }
     }
 
diff --git a/wallet/Cargo.toml b/wallet/Cargo.toml
index 06582c32f..8a29923c0 100644
--- a/wallet/Cargo.toml
+++ b/wallet/Cargo.toml
@@ -8,5 +8,15 @@ rust-version.workspace = true
 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 
 [dependencies]
+common = {path = '../common'}
 logging = { path = "../logging" }
+serialization = { path = "../serialization" }
+storage = { path = '../storage', features = ['inmemory'] }
+utxo = { path = '../utxo' }
 wallet-storage = {path = './storage'}
+wallet-types = {path = './types'}
+
+thiserror.workspace = true
+
+[dev-dependencies]
+tempfile = "3.3"
\ No newline at end of file
diff --git a/wallet/src/lib.rs b/wallet/src/lib.rs
new file mode 100644
index 000000000..90eabfb3e
--- /dev/null
+++ b/wallet/src/lib.rs
@@ -0,0 +1,18 @@
+// Copyright (c) 2023 RBB S.r.l
+// opensource@mintlayer.org
+// SPDX-License-Identifier: MIT
+// Licensed under the MIT License;
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://github.com/mintlayer/mintlayer-core/blob/master/LICENSE
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+pub mod wallet;
+
+pub use crate::wallet::{Wallet, WalletError};
diff --git a/wallet/src/wallet.rs b/wallet/src/wallet.rs
new file mode 100644
index 000000000..fb7b0b354
--- /dev/null
+++ b/wallet/src/wallet.rs
@@ -0,0 +1,189 @@
+// Copyright (c) 2023 RBB S.r.l
+// opensource@mintlayer.org
+// SPDX-License-Identifier: MIT
+// Licensed under the MIT License;
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://github.com/mintlayer/mintlayer-core/blob/master/LICENSE
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+use std::collections::BTreeMap;
+use std::path::Path;
+
+use common::chain::{OutPoint, Transaction, TxOutput};
+use common::primitives::{Id, Idable};
+use utxo::Utxo;
+use wallet_storage::{
+    DefaultBackend, Store, StoreTxRw, TransactionRw, Transactional, WalletStorageWrite,
+};
+use wallet_types::{TxState, WalletTx};
+
+/// Wallet errors
+#[derive(thiserror::Error, Debug, Eq, PartialEq)]
+pub enum WalletError {
+    #[error("Wallet database error: {0}")]
+    DatabaseError(wallet_storage::Error),
+    #[error("Transaction already present: {0}")]
+    DuplicateTransaction(Id<Transaction>),
+    #[error("No transaction found: {0}")]
+    NoTransactionFound(Id<Transaction>),
+}
+#[allow(dead_code)] // TODO remove
+pub struct Wallet<B: storage::Backend> {
+    db: Store<B>,
+    txs: BTreeMap<Id<Transaction>, WalletTx>,
+    utxo: BTreeMap<OutPoint, Utxo>,
+}
+
+pub fn open_wallet_file<P: AsRef<Path>>(path: P) -> Result<Wallet<DefaultBackend>, WalletError> {
+    let db = Store::new(DefaultBackend::new(path)).map_err(WalletError::DatabaseError)?;
+
+    Wallet::load_wallet(db)
+}
+
+pub fn open_wallet_in_memory() -> Result<Wallet<DefaultBackend>, WalletError> {
+    let db = Store::new(DefaultBackend::new_in_memory()).map_err(WalletError::DatabaseError)?;
+
+    Wallet::load_wallet(db)
+}
+
+impl<B: storage::Backend> Wallet<B> {
+    fn load_wallet(db: Store<B>) -> Result<Self, WalletError> {
+        let txs = db.read_transactions().map_err(WalletError::DatabaseError)?;
+        let utxo = db.read_utxo_set().map_err(WalletError::DatabaseError)?;
+
+        Ok(Wallet { db, txs, utxo })
+    }
+
+    pub fn get_database(&self) -> &Store<B> {
+        &self.db
+    }
+
+    #[allow(dead_code)] // TODO remove
+    fn add_transaction(&mut self, tx: Transaction, state: TxState) -> Result<(), WalletError> {
+        let tx_id = tx.get_id();
+
+        if self.txs.contains_key(&tx_id) {
+            return Err(WalletError::DuplicateTransaction(tx_id));
+        }
+
+        let mut db_tx = self.db.transaction_rw(None).map_err(WalletError::DatabaseError)?;
+
+        let wallet_tx = WalletTx::new(tx, state);
+
+        db_tx.set_transaction(&tx_id, &wallet_tx).map_err(WalletError::DatabaseError)?;
+        db_tx.commit().map_err(WalletError::DatabaseError)?;
+
+        self.txs.insert(tx_id, wallet_tx);
+
+        // TODO add UTXO?
+
+        Ok(())
+    }
+
+    #[allow(dead_code)] // TODO remove
+    fn delete_transaction(&mut self, tx_id: Id<Transaction>) -> Result<(), WalletError> {
+        if !self.txs.contains_key(&tx_id) {
+            return Err(WalletError::NoTransactionFound(tx_id));
+        }
+
+        let mut db_tx = self.db.transaction_rw(None).map_err(WalletError::DatabaseError)?;
+        db_tx.del_transaction(&tx_id).map_err(WalletError::DatabaseError)?;
+        db_tx.commit().map_err(WalletError::DatabaseError)?;
+
+        self.txs.remove(&tx_id);
+
+        // TODO remove UTXO?
+
+        Ok(())
+    }
+
+    // TODO fix incompatibility between borrowing mut self and the database transaction
+    #[allow(dead_code)] // TODO remove
+    fn add_to_utxos(
+        &mut self,
+        tx: &Transaction,
+        db_tx: &mut StoreTxRw<B>,
+    ) -> Result<(), WalletError> {
+        for (i, output) in tx.outputs().iter().enumerate() {
+            // Check if this output belongs to this wallet or it is watched
+            if self.is_available_for_spending(output) && self.is_mine_or_watched(output) {
+                let outpoint = OutPoint::new(tx.get_id().into(), i as u32);
+                let utxo = Utxo::new(output.clone(), false, utxo::UtxoSource::Mempool);
+                self.utxo.insert(outpoint.clone(), utxo.clone());
+                db_tx.set_utxo(&outpoint, utxo).map_err(WalletError::DatabaseError)?;
+            }
+        }
+        Ok(())
+    }
+
+    #[allow(dead_code)] // TODO remove
+    fn is_available_for_spending(&self, _txo: &TxOutput) -> bool {
+        // TODO implement
+        true
+    }
+
+    #[allow(dead_code)] // TODO remove
+    fn is_mine_or_watched(&self, _txo: &TxOutput) -> bool {
+        // TODO implement
+        true
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use common::chain::{GenBlock, Transaction};
+    use common::primitives::H256;
+
+    #[test]
+    fn in_memory_wallet() {
+        let wallet = open_wallet_in_memory();
+        assert!(wallet.is_ok())
+    }
+
+    #[test]
+    fn wallet_transactions() {
+        let temp_dir_path = tempfile::TempDir::new().unwrap();
+        let wallet_path = temp_dir_path.path().join("test_wallet_transactions.sqlite");
+
+        let mut wallet = open_wallet_file(wallet_path.as_path()).expect("the wallet to load");
+
+        let tx1 = Transaction::new(1, vec![], vec![], 0).unwrap();
+        let tx2 = Transaction::new(2, vec![], vec![], 0).unwrap();
+        let tx3 = Transaction::new(3, vec![], vec![], 0).unwrap();
+        let tx4 = Transaction::new(4, vec![], vec![], 0).unwrap();
+
+        let block_id: Id<GenBlock> = H256::from_low_u64_le(123).into();
+
+        wallet.add_transaction(tx1.clone(), TxState::Confirmed(block_id)).unwrap();
+        wallet.add_transaction(tx2.clone(), TxState::Conflicted(block_id)).unwrap();
+        wallet.add_transaction(tx3.clone(), TxState::InMempool).unwrap();
+        wallet.add_transaction(tx4.clone(), TxState::Inactive).unwrap();
+        drop(wallet);
+
+        let mut wallet = open_wallet_file(wallet_path.as_path()).expect("the wallet to load");
+
+        assert_eq!(4, wallet.txs.len());
+        assert_eq!(&tx1, wallet.txs.get(&tx1.get_id()).unwrap().get_tx());
+        assert_eq!(&tx2, wallet.txs.get(&tx2.get_id()).unwrap().get_tx());
+        assert_eq!(&tx3, wallet.txs.get(&tx3.get_id()).unwrap().get_tx());
+        assert_eq!(&tx4, wallet.txs.get(&tx4.get_id()).unwrap().get_tx());
+
+        wallet.delete_transaction(tx1.get_id()).unwrap();
+        wallet.delete_transaction(tx3.get_id()).unwrap();
+        drop(wallet);
+
+        let wallet = open_wallet_file(wallet_path.as_path()).expect("the wallet to load");
+
+        assert_eq!(2, wallet.txs.len());
+        assert_eq!(&tx2, wallet.txs.get(&tx2.get_id()).unwrap().get_tx());
+        assert_eq!(&tx4, wallet.txs.get(&tx4.get_id()).unwrap().get_tx());
+    }
+}
diff --git a/wallet/storage/Cargo.toml b/wallet/storage/Cargo.toml
index 5661edea7..8e50f3661 100644
--- a/wallet/storage/Cargo.toml
+++ b/wallet/storage/Cargo.toml
@@ -13,6 +13,7 @@ serialization = { path = "../../serialization" }
 storage = { path = '../../storage', features = ['inmemory'] }
 storage-sqlite = { path = "../../storage/sqlite" }
 utxo = { path = '../../utxo' }
+wallet-types = { path = '../types' }
 
 [dev-dependencies]
 test-utils = {path = '../../test-utils'}
diff --git a/wallet/storage/src/internal/mod.rs b/wallet/storage/src/internal/mod.rs
index 0ece17c8d..4d5d6c508 100644
--- a/wallet/storage/src/internal/mod.rs
+++ b/wallet/storage/src/internal/mod.rs
@@ -15,7 +15,10 @@
 
 use std::collections::BTreeMap;
 
-use common::chain::OutPoint;
+use common::{
+    chain::{OutPoint, Transaction},
+    primitives::Id,
+};
 use utxo::Utxo;
 
 use crate::{
@@ -25,6 +28,7 @@ use crate::{
 
 mod store_tx;
 pub use store_tx::{StoreTxRo, StoreTxRw};
+use wallet_types::WalletTx;
 
 /// Store for wallet data, parametrized over the backend B
 pub struct Store<B: storage::Backend>(storage::Storage<B, Schema>);
@@ -47,6 +51,12 @@ impl<B: storage::Backend> Store<B> {
         let db_tx = self.transaction_ro()?;
         db_tx.0.get::<db::DBUtxo, _>().prefix_iter_decoded(&()).map(Iterator::collect)
     }
+
+    /// Collect and return all transactions from the storage
+    pub fn read_transactions(&self) -> crate::Result<BTreeMap<Id<Transaction>, WalletTx>> {
+        let db_tx = self.transaction_ro()?;
+        db_tx.0.get::<db::DBTxs, _>().prefix_iter_decoded(&()).map(Iterator::collect)
+    }
 }
 
 impl<B: storage::Backend> Clone for Store<B>
@@ -102,6 +112,7 @@ impl<B: storage::Backend> WalletStorageRead for Store<B> {
     delegate_to_transaction! {
         fn get_storage_version(&self) -> crate::Result<u32>;
         fn get_utxo(&self, outpoint: &OutPoint) -> crate::Result<Option<Utxo>>;
+        fn get_transaction(&self, id: &Id<Transaction>) -> crate::Result<Option<WalletTx>>;
     }
 }
 
@@ -110,6 +121,8 @@ impl<B: storage::Backend> WalletStorageWrite for Store<B> {
         fn set_storage_version(&mut self, version: u32) -> crate::Result<()>;
         fn set_utxo(&mut self, outpoint: &OutPoint, entry: Utxo) -> crate::Result<()>;
         fn del_utxo(&mut self, outpoint: &OutPoint) -> crate::Result<()>;
+        fn set_transaction(&mut self, id: &Id<Transaction>, tx: &WalletTx) -> crate::Result<()>;
+        fn del_transaction(&mut self, id: &Id<Transaction>) -> crate::Result<()>;
     }
 }
 
diff --git a/wallet/storage/src/internal/store_tx.rs b/wallet/storage/src/internal/store_tx.rs
index adac0e821..55650f110 100644
--- a/wallet/storage/src/internal/store_tx.rs
+++ b/wallet/storage/src/internal/store_tx.rs
@@ -13,10 +13,12 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-use common::chain::OutPoint;
+use common::chain::{OutPoint, Transaction};
+use common::primitives::Id;
 use serialization::{Codec, DecodeAll, Encode, EncodeLike};
 use storage::schema;
 use utxo::Utxo;
+use wallet_types::WalletTx;
 
 use crate::{
     schema::{self as db, Schema},
@@ -64,6 +66,10 @@ macro_rules! impl_read_ops {
             fn get_utxo(&self, outpoint: &OutPoint) -> crate::Result<Option<Utxo>> {
                 self.read::<db::DBUtxo, _, _>(outpoint)
             }
+
+            fn get_transaction(&self, id: &Id<Transaction>) -> crate::Result<Option<WalletTx>> {
+                self.read::<db::DBTxs, _, _>(id)
+            }
         }
 
         impl<'st, B: storage::Backend> $TxType<'st, B> {
@@ -106,6 +112,14 @@ impl<'st, B: storage::Backend> WalletStorageWrite for StoreTxRw<'st, B> {
     fn del_utxo(&mut self, outpoint: &OutPoint) -> crate::Result<()> {
         self.0.get_mut::<db::DBUtxo, _>().del(outpoint).map_err(Into::into)
     }
+
+    fn set_transaction(&mut self, id: &Id<Transaction>, tx: &WalletTx) -> crate::Result<()> {
+        self.write::<db::DBTxs, _, _, _>(id, tx)
+    }
+
+    fn del_transaction(&mut self, id: &Id<Transaction>) -> crate::Result<()> {
+        self.0.get_mut::<db::DBTxs, _>().del(id).map_err(Into::into)
+    }
 }
 
 impl<'st, B: storage::Backend> StoreTxRw<'st, B> {
diff --git a/wallet/storage/src/lib.rs b/wallet/storage/src/lib.rs
index 27b378b0f..40bfa66b5 100644
--- a/wallet/storage/src/lib.rs
+++ b/wallet/storage/src/lib.rs
@@ -19,10 +19,12 @@ mod internal;
 mod is_transaction_seal;
 pub mod schema;
 
-use common::chain::OutPoint;
-pub use internal::Store;
+pub use internal::{Store, StoreTxRo, StoreTxRw};
 
+use common::chain::{OutPoint, Transaction};
+use common::primitives::Id;
 use utxo::Utxo;
+use wallet_types::WalletTx;
 
 /// Possibly failing result of wallet storage query
 pub type Result<T> = storage::Result<T>;
@@ -31,16 +33,19 @@ pub type Error = storage::Error;
 /// Queries on persistent wallet data
 pub trait WalletStorageRead {
     /// Get storage version
-    fn get_storage_version(&self) -> crate::Result<u32>;
+    fn get_storage_version(&self) -> Result<u32>;
     fn get_utxo(&self, outpoint: &OutPoint) -> Result<Option<Utxo>>;
+    fn get_transaction(&self, id: &Id<Transaction>) -> Result<Option<WalletTx>>;
 }
 
 /// Modifying operations on persistent wallet data
 pub trait WalletStorageWrite: WalletStorageRead {
     /// Set storage version
-    fn set_storage_version(&mut self, version: u32) -> crate::Result<()>;
+    fn set_storage_version(&mut self, version: u32) -> Result<()>;
     fn set_utxo(&mut self, outpoint: &OutPoint, entry: Utxo) -> Result<()>;
     fn del_utxo(&mut self, outpoint: &OutPoint) -> Result<()>;
+    fn set_transaction(&mut self, id: &Id<Transaction>, tx: &WalletTx) -> Result<()>;
+    fn del_transaction(&mut self, id: &Id<Transaction>) -> Result<()>;
 }
 
 /// Marker trait for types where read/write operations are run in a transaction
@@ -77,3 +82,6 @@ pub trait Transactional<'t> {
 }
 
 pub trait WalletStorage: WalletStorageWrite + for<'tx> Transactional<'tx> + Send {}
+
+pub type DefaultBackend = storage_sqlite::Sqlite;
+pub type WalletStorageTxRwImpl<'st> = StoreTxRw<'st, storage_sqlite::Sqlite>;
diff --git a/wallet/storage/src/schema.rs b/wallet/storage/src/schema.rs
index a51d2fd1a..795c26fff 100644
--- a/wallet/storage/src/schema.rs
+++ b/wallet/storage/src/schema.rs
@@ -15,7 +15,12 @@
 
 //! Wallet database schema
 
-use common::chain::OutPoint;
+use wallet_types::WalletTx;
+
+use common::{
+    chain::{OutPoint, Transaction},
+    primitives::Id,
+};
 use utxo::Utxo;
 
 storage::decl_schema! {
@@ -25,5 +30,7 @@ storage::decl_schema! {
         pub DBValue: Map<Vec<u8>, Vec<u8>>,
         /// Store for Utxo Entries
         pub DBUtxo: Map<OutPoint, Utxo>,
+        /// Store for Transaction entries
+        pub DBTxs: Map<Id<Transaction>, WalletTx>,
     }
 }
diff --git a/wallet/types/Cargo.toml b/wallet/types/Cargo.toml
new file mode 100644
index 000000000..b47d581b4
--- /dev/null
+++ b/wallet/types/Cargo.toml
@@ -0,0 +1,15 @@
+[package]
+name = "wallet-types"
+license.workspace = true
+version.workspace = true
+edition.workspace = true
+rust-version.workspace = true
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+serialization = { path = "../../serialization" }
+common = { path = "../../common/" }
+
+thiserror.workspace = true
+parity-scale-codec.workspace = true
diff --git a/wallet/types/src/lib.rs b/wallet/types/src/lib.rs
new file mode 100644
index 000000000..4657be60e
--- /dev/null
+++ b/wallet/types/src/lib.rs
@@ -0,0 +1,18 @@
+// Copyright (c) 2023 RBB S.r.l
+// opensource@mintlayer.org
+// SPDX-License-Identifier: MIT
+// Licensed under the MIT License;
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://github.com/mintlayer/mintlayer-core/blob/master/LICENSE
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+pub mod wallet_tx;
+
+pub use wallet_tx::{TxState, WalletTx};
diff --git a/wallet/types/src/wallet_tx.rs b/wallet/types/src/wallet_tx.rs
new file mode 100644
index 000000000..a90e147b8
--- /dev/null
+++ b/wallet/types/src/wallet_tx.rs
@@ -0,0 +1,58 @@
+// Copyright (c) 2023 RBB S.r.l
+// opensource@mintlayer.org
+// SPDX-License-Identifier: MIT
+// Licensed under the MIT License;
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://github.com/mintlayer/mintlayer-core/blob/master/LICENSE
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+use serialization::{Decode, Encode};
+
+use common::chain::{GenBlock, Transaction};
+use common::primitives::id::WithId;
+use common::primitives::Id;
+
+#[derive(Debug, PartialEq, Eq, Clone, Decode, Encode)]
+pub enum TxState {
+    /// Confirmed transaction in a block
+    #[codec(index = 0)]
+    Confirmed(Id<GenBlock>),
+    /// Unconfirmed transaction in the mempool
+    #[codec(index = 1)]
+    InMempool,
+    /// Conflicted transaction with a confirmed block
+    #[codec(index = 2)]
+    Conflicted(Id<GenBlock>),
+    /// Transaction that is not confirmed or conflicted and is not in the mempool.
+    #[codec(index = 3)]
+    Inactive,
+}
+
+#[derive(Debug, PartialEq, Eq, Clone, Decode, Encode)]
+pub struct WalletTx {
+    /// The wrapped transaction with cached transaction id
+    tx: WithId<Transaction>,
+    /// The state of this transaction in relation to the inclusion or not on the blockchain. For
+    /// example it could be confirmed, pending in mempool, conflicted etc.
+    state: TxState,
+}
+
+impl WalletTx {
+    pub fn new(tx: Transaction, state: TxState) -> Self {
+        WalletTx {
+            tx: WithId::new(tx),
+            state,
+        }
+    }
+
+    pub fn get_tx(&self) -> &Transaction {
+        &self.tx
+    }
+}


Test patch:
diff --git a/wallet/storage/src/internal/test.rs b/wallet/storage/src/internal/test.rs
index f18e10059..770cf7903 100644
--- a/wallet/storage/src/internal/test.rs
+++ b/wallet/storage/src/internal/test.rs
@@ -14,6 +14,7 @@
 // limitations under the License.
 
 use super::*;
+use crate::DefaultBackend;
 use common::chain::tokens::OutputValue;
 use common::chain::{Destination, OutPointSourceId, OutputPurpose, TxOutput};
 use common::primitives::{Amount, Id, H256};
@@ -22,19 +23,10 @@ use crypto::random::Rng;
 use rstest::rstest;
 use test_utils::random::{make_seedable_rng, Seed};
 
-impl Store<storage_sqlite::Sqlite> {
-    /// Create a default storage (mostly for testing, may want to remove this later)
-    pub fn new_in_memory() -> crate::Result<Self> {
-        Self::new(storage_sqlite::Sqlite::new_in_memory())
-    }
-}
-
-type TestStore = Store<storage_sqlite::Sqlite>;
-
 #[test]
 fn storage_get_default_version_in_tx() {
     utils::concurrency::model(|| {
-        let store = TestStore::new_in_memory().unwrap();
+        let store = Store::new(DefaultBackend::new_in_memory()).unwrap();
         let vtx = store.transaction_ro().unwrap().get_storage_version().unwrap();
         let vst = store.get_storage_version().unwrap();
         assert_eq!(vtx, 1, "Default storage version wrong");
@@ -48,7 +40,7 @@ fn storage_get_default_version_in_tx() {
 #[case(Seed::from_entropy())]
 fn read_write_utxo_in_db_transaction(#[case] seed: Seed) {
     let mut rng = make_seedable_rng(seed);
-    let mut db_interface = TestStore::new_in_memory().unwrap();
+    let mut db_interface = Store::new(DefaultBackend::new_in_memory()).unwrap();
 
     // generate a utxo and outpoint
     let (_, pub_key) = PrivateKey::new_from_rng(&mut rng, KeyKind::Secp256k1Schnorr);