diff --git a/Cargo.lock b/Cargo.lock index 6463b52eb..c57ddba3e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -241,6 +241,7 @@ version = "0.1.0" dependencies = [ "bincode 1.1.2 (registry+https://github.com/rust-lang/crates.io-index)", "blake2 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", + "byteorder 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)", "chain-core 0.1.0", "failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", "hex 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/client-core/Cargo.toml b/client-core/Cargo.toml index cbe48bb25..3ded7eecf 100644 --- a/client-core/Cargo.toml +++ b/client-core/Cargo.toml @@ -16,8 +16,8 @@ blake2 = "0.8" hex = "0.3" zeroize = "0.6" zeroize_derive = "0.1" +byteorder = "1.3" sled = { version = "0.19", optional = true } [features] default = ["sled"] -hash-map = [] diff --git a/client-core/README.md b/client-core/README.md new file mode 100644 index 000000000..ef9923663 --- /dev/null +++ b/client-core/README.md @@ -0,0 +1,96 @@ +# Crypto.com Chain Client + +This crate exposes following functionalities for interacting with Crypto.com Chain: +- Wallet creation +- Address generation +- Transaction syncing and storage +- Balance tracking +- Transaction creation and signing + +## Design + +Below is a high level design diagram of this crate: + +
+ Client Design +
+ +### `Storage` trait + +This trait declares APIs for different database operations such as `clear`, `get`, `set`, `contains_key`, etc. This +crate provides a default implementation (`SledStorage`) of this trait using `Sled` embedded database. + +### `SecureStorage` trait + +This trait exposes APIs for securely getting and setting values in `Storage`. This crates is automatically implemented +for all the types implementing `Storage` trait. + +### `Chain` trait + +This trait exposes APIs for communicating with Crypto.com Chain via ABCI. Currently, this crate exposes following APIs: +- `query_transaction_changes`: Queries Crypto.com chain for balance changes for different `addresses` from + `last_block_height`. + +### Services + +`Storage` implementation provide generic APIs over any storage backend. In order to provide intended public interface +(`Wallet`) through this crate, we need specific services for handling storage of different entities, like, keys, +wallets, balance, and transactions. + +#### `KeyService` + +`KeyService` exposes key related operations (`generate` and `get_keys`) on top of any `Storage` implementation. +- `generate`: Generates a new private key for given `wallet_id` and encrypts it with given `passphrase` before storing. +- `get_keys`: Returns all the keys stored for given `wallet_id` and decrypts them with given `passphrase`. + +#### `WalletService` + +`WalletService` exposes wallet related operations (`create` and `get`) on top of any `Storage` implementation. +- `create`: Creates a new wallet and returns `wallet_id`. This function also encrypts all the data using `passphrase` + before storing it in `Storage`. +- `get`: Retrieves a `wallet_id` from `Storage` and decrypts it with given `passphrase`. + +#### `BalanceService` + +`BalanceService` exposes balance related operations (`sync`, `sync_all` and `get_balance`) on top of any `Storage` and +`Chain` implementation. +- `sync`: Updates balance for given `wallet_id` and `addresses` after querying new transactions from Crypto.com Chain. + This function first retrieves current `balance` and `last_block_height` from `Storage` and then queries `Chain` for + any updates since `last_block_height`. After successful query, it updates the data in `Storage`. +- `sync_all`: This works in similar way as `sync` except it sets `last_block_height = 0` and queries for all the + transactions since genesis block. +- `get_balance`: Returns balance for a given `wallet_id` from `Storage`. + +### `Wallet` trait + +Crypto.com exposes public interface through `Wallet` trait which contains following functions with default +implementations: + +- `new_wallet`: Creates a new wallet with given `name` and encrypts it with given `passphrase`. This function internally + calls `crate` function of `WalletService`. +- `get_public_keys`: Retrieves all public keys corresponding to given wallet `name` and `passphrase`. This function + internally uses `KeyService` for get this information. +- `get_addresses`: Retrieves all addresses corresponding to given wallet `name` and `passphrase`. This function + internally uses `KeyService` for get this information. +- `generate_public_key`: Generates a new public key for given wallet `name` and `passphrase`. This function internally + uses `KeyService`. +- `generate_address`: Generates a new address (redeem) for given wallet `name` and `passphrase`. This function + internally uses `KeyService`. +- `get_balance`: Retrieves current balance for given wallet `name` and `passphrase`. This function internally uses + `BalanceService` to get the balance. +- `sync_balance`: Synchronizes and returns current balance for given wallet `name` and `passphrase`. This function + internally uses `BalanceService::sync` to synchronize balance. +- `recalculate_balance`: Recalculate current balance for given wallet `name` and `passphrase` from genesis. This + function internally uses `BalanceService::sync_all` to synchronize balance. + +## API Documentation + +To see this crate's API docs. Run following command from `chain` directory. +``` +cargo doc --package client-core --no-deps --open +``` + +### Warning + +This is a work-in-progress crate and is unusable in its current state. These is no implementation for Chain ABCI client +(`Chain` trait) as of now. diff --git a/client-core/client_design.png b/client-core/client_design.png new file mode 100644 index 000000000..9a1e6ff3e Binary files /dev/null and b/client-core/client_design.png differ diff --git a/client-core/src/balance.rs b/client-core/src/balance.rs new file mode 100644 index 000000000..1fa375223 --- /dev/null +++ b/client-core/src/balance.rs @@ -0,0 +1,6 @@ +//! Types for tracking balance changes +mod balance_change; +mod transaction_change; + +pub use self::balance_change::BalanceChange; +pub use self::transaction_change::TransactionChange; diff --git a/client-core/src/balance/balance_change.rs b/client-core/src/balance/balance_change.rs new file mode 100644 index 000000000..174242ccd --- /dev/null +++ b/client-core/src/balance/balance_change.rs @@ -0,0 +1,85 @@ +use std::ops::Add; + +use failure::ResultExt; + +use crate::{ErrorKind, Result}; + +use chain_core::init::coin::Coin; + +/// Incoming or Outgoing balance change +#[derive(Debug)] +pub enum BalanceChange { + /// Represents balance addition + Incoming(Coin), + /// Represents balance reduction + Outgoing(Coin), +} + +#[allow(clippy::suspicious_arithmetic_impl)] +impl Add<&BalanceChange> for Coin { + type Output = Result; + + fn add(self, other: &BalanceChange) -> Self::Output { + match other { + BalanceChange::Incoming(change) => { + Ok((self + change).context(ErrorKind::BalanceAdditionError)?) + } + BalanceChange::Outgoing(change) => { + Ok((self - change).context(ErrorKind::BalanceAdditionError)?) + } + } + } +} + +impl Add for Coin { + type Output = Result; + + fn add(self, other: BalanceChange) -> Self::Output { + self + &other + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn add_incoming() { + let coin = Coin::zero() + + BalanceChange::Incoming(Coin::new(30).expect("Unable to create new coin")); + + assert_eq!( + Coin::new(30).expect("Unable to create new coin"), + coin.expect("Unable to add coins"), + "Coins does not match" + ); + } + + #[test] + fn add_incoming_fail() { + let coin = Coin::max() + + BalanceChange::Incoming(Coin::new(30).expect("Unable to create new coin")); + + assert!(coin.is_err(), "Created coin greater than max value") + } + + #[test] + fn add_outgoing() { + let coin = Coin::new(40).expect("Unable to create new coin") + + BalanceChange::Outgoing(Coin::new(30).expect("Unable to create new coin")); + + assert_eq!( + Coin::new(10).expect("Unable to create new coin"), + coin.expect("Unable to add coins"), + "Coins does not match" + ); + } + + #[test] + fn add_outgoing_fail() { + let coin = Coin::zero() + + BalanceChange::Outgoing(Coin::new(30).expect("Unable to create new coin")); + + assert!(coin.is_err(), "Created negative coin") + } +} diff --git a/client-core/src/balance/transaction_change.rs b/client-core/src/balance/transaction_change.rs new file mode 100644 index 000000000..60a5aef26 --- /dev/null +++ b/client-core/src/balance/transaction_change.rs @@ -0,0 +1,97 @@ +use std::ops::Add; + +use crate::balance::BalanceChange; +use crate::Result; + +use chain_core::init::coin::Coin; +use chain_core::tx::data::address::ExtendedAddr; +use chain_core::tx::data::TxId; + +/// Represents balance change in a transaction +#[derive(Debug)] +pub struct TransactionChange { + /// ID of transaction which caused this change + pub transaction_id: TxId, + /// Address which is affected by this change + pub address: ExtendedAddr, + /// Change in balance + pub balance_change: BalanceChange, +} + +impl Add<&TransactionChange> for Coin { + type Output = Result; + + fn add(self, other: &TransactionChange) -> Self::Output { + self + &other.balance_change + } +} + +impl Add for Coin { + type Output = Result; + + fn add(self, other: TransactionChange) -> Self::Output { + self + &other + } +} + +#[cfg(test)] +mod tests { + use super::*; + use chain_core::tx::data::txid_hash; + + fn get_transaction_change(balance_change: BalanceChange) -> TransactionChange { + TransactionChange { + transaction_id: txid_hash(&[0, 1, 2]), + address: ExtendedAddr::BasicRedeem(Default::default()), + balance_change, + } + } + + #[test] + fn add_incoming() { + let coin = Coin::zero() + + get_transaction_change(BalanceChange::Incoming( + Coin::new(30).expect("Unable to create new coin"), + )); + + assert_eq!( + Coin::new(30).expect("Unable to create new coin"), + coin.expect("Unable to add coins"), + "Coins does not match" + ); + } + + #[test] + fn add_incoming_fail() { + let coin = Coin::max() + + get_transaction_change(BalanceChange::Incoming( + Coin::new(30).expect("Unable to create new coin"), + )); + + assert!(coin.is_err(), "Created coin greater than max value") + } + + #[test] + fn add_outgoing() { + let coin = Coin::new(40).expect("Unable to create new coin") + + get_transaction_change(BalanceChange::Outgoing( + Coin::new(30).expect("Unable to create new coin"), + )); + + assert_eq!( + Coin::new(10).expect("Unable to create new coin"), + coin.expect("Unable to add coins"), + "Coins does not match" + ); + } + + #[test] + fn add_outgoing_fail() { + let coin = Coin::zero() + + get_transaction_change(BalanceChange::Outgoing( + Coin::new(30).expect("Unable to create new coin"), + )); + + assert!(coin.is_err(), "Created negative coin") + } +} diff --git a/client-core/src/chain.rs b/client-core/src/chain.rs new file mode 100644 index 000000000..5827fffa0 --- /dev/null +++ b/client-core/src/chain.rs @@ -0,0 +1,23 @@ +//! Communication between client and chain + +#[cfg(test)] +mod mock_chain; + +#[cfg(test)] +pub use mock_chain::MockChain; + +use crate::balance::TransactionChange; +use crate::Result; + +/// Interface for a backend agnostic communication between client and chain +/// +/// ### Warning +/// This is a WIP trait and will change in future based on requirements. +pub trait Chain { + /// Queries Crypto.com chain for changes for different `addresses` from `last_block_height` + fn query_transaction_changes( + &self, + addresses: Vec, + last_block_height: u64, + ) -> Result<(Vec, u64)>; +} diff --git a/client-core/src/chain/mock_chain.rs b/client-core/src/chain/mock_chain.rs new file mode 100644 index 000000000..3e52e872f --- /dev/null +++ b/client-core/src/chain/mock_chain.rs @@ -0,0 +1,18 @@ +#![cfg(test)] + +use crate::balance::TransactionChange; +use crate::{Chain, Result}; + +/// A mock chain client +#[derive(Clone, Default)] +pub struct MockChain; + +impl Chain for MockChain { + fn query_transaction_changes( + &self, + _addresses: Vec, + _last_block_height: u64, + ) -> Result<(Vec, u64)> { + Ok((Default::default(), Default::default())) + } +} diff --git a/client-core/src/error.rs b/client-core/src/error.rs index 72251f71a..9033ac7be 100644 --- a/client-core/src/error.rs +++ b/client-core/src/error.rs @@ -47,6 +47,12 @@ pub enum ErrorKind { /// Error while locking a shared resource #[fail(display = "Error while locking a shared resource")] LockError, + /// Error while adding two balances + #[fail(display = "Error while adding two balances")] + BalanceAdditionError, + /// Balance not found + #[fail(display = "Balance not found")] + BalanceNotFound, } impl Fail for Error { diff --git a/client-core/src/lib.rs b/client-core/src/lib.rs index e4f5da1a8..e418e6cef 100644 --- a/client-core/src/lib.rs +++ b/client-core/src/lib.rs @@ -4,9 +4,9 @@ //! This crate exposes following functionalities for interacting with Crypto.com Chain: //! - Wallet creation //! - Address generation -//! - Transaction creation and signing //! - Transaction syncing and storage //! - Balance tracking +//! - Transaction creation and signing //! //! ## Features //! @@ -16,12 +16,20 @@ //! - Implementation of [`Wallet`](crate::Wallet) trait using [`SledStorage`](crate::storage::SledStorage) //! - Enable with **`"sled"`** feature flag. //! - This feature is enabled by **default**. +//! +//! ### Warning +//! +//! This is a work-in-progress crate and is unusable in its current state. +pub mod balance; +pub mod chain; pub mod error; pub mod key; pub mod service; pub mod storage; pub mod wallet; +#[doc(inline)] +pub use chain::Chain; #[doc(inline)] pub use error::{Error, ErrorKind, Result}; #[doc(inline)] diff --git a/client-core/src/service.rs b/client-core/src/service.rs index dedeabe44..4dd85ca9e 100644 --- a/client-core/src/service.rs +++ b/client-core/src/service.rs @@ -1,6 +1,8 @@ //! Management services +mod balance_service; mod key_service; mod wallet_service; +pub use self::balance_service::BalanceService; pub use self::key_service::KeyService; pub use self::wallet_service::WalletService; diff --git a/client-core/src/service/balance_service.rs b/client-core/src/service/balance_service.rs new file mode 100644 index 000000000..d0e5ed8c1 --- /dev/null +++ b/client-core/src/service/balance_service.rs @@ -0,0 +1,178 @@ +use byteorder::{ByteOrder, LittleEndian}; +use failure::ResultExt; + +use chain_core::init::coin::Coin; + +use crate::{Chain, Error, ErrorKind, Result, SecureStorage, Storage}; + +/// Exposes functionalities for transaction storage and syncing +#[derive(Default)] +pub struct BalanceService { + chain: C, + storage: T, +} + +impl BalanceService +where + C: Chain, + T: Storage, +{ + /// Creates a new instance of transaction service. + pub fn new(chain: C, storage: T) -> Self { + Self { chain, storage } + } + + /// Updates balance after querying new transactions from Crypto.com Chain. + pub fn sync(&self, wallet_id: &str, passphrase: &str, addresses: Vec) -> Result { + let bytes = self + .storage + .get_secure(wallet_id.as_bytes(), passphrase.as_bytes())?; + + let mut storage_unit = match bytes { + None => Default::default(), + Some(bytes) => StorageUnit::deserialize_from(&bytes)?, + }; + + let (transaction_changes, block_height) = self + .chain + .query_transaction_changes(addresses, storage_unit.last_block_height)?; + + storage_unit.last_block_height = block_height; + + for change in transaction_changes { + storage_unit.balance = (storage_unit.balance + change)?; + } + + self.storage.set_secure( + wallet_id.as_bytes(), + storage_unit.serialize(), + passphrase.as_bytes(), + )?; + + Ok(storage_unit.balance) + } + + /// Updates balance after querying all transactions from Crypto.com Chain. + /// + /// # Warning + /// This should only be used when you need to recalculate balance from whole history of blockchain. + pub fn sync_all( + &self, + wallet_id: &str, + passphrase: &str, + addresses: Vec, + ) -> Result { + let bytes = self + .storage + .get_secure(wallet_id.as_bytes(), passphrase.as_bytes())?; + + let mut storage_unit = match bytes { + None => Default::default(), + Some(bytes) => StorageUnit::deserialize_from(&bytes)?, + }; + + let (transaction_changes, block_height) = + self.chain.query_transaction_changes(addresses, 0)?; + + storage_unit.last_block_height = block_height; + + for change in transaction_changes { + storage_unit.balance = (storage_unit.balance + change)?; + } + + self.storage.set_secure( + wallet_id.as_bytes(), + storage_unit.serialize(), + passphrase.as_bytes(), + )?; + + Ok(storage_unit.balance) + } + + /// Returns balance for a given wallet ID. + pub fn get_balance(&self, wallet_id: &str, passphrase: &str) -> Result> { + let bytes = self + .storage + .get_secure(wallet_id.as_bytes(), passphrase.as_bytes())?; + + match bytes { + None => Ok(None), + Some(bytes) => { + let storage_unit = StorageUnit::deserialize_from(&bytes)?; + Ok(Some(storage_unit.balance)) + } + } + } +} + +#[derive(Debug, PartialEq)] +pub(self) struct StorageUnit { + pub(self) balance: Coin, + pub(self) last_block_height: u64, +} + +impl StorageUnit { + pub fn serialize(&self) -> Vec { + let mut bytes: [u8; 16] = [0; 16]; + + LittleEndian::write_u64(&mut bytes[0..8], *self.balance); + LittleEndian::write_u64(&mut bytes[8..16], self.last_block_height); + + bytes.to_vec() + } + + pub fn deserialize_from(bytes: &[u8]) -> Result { + if 16 != bytes.len() { + Err(Error::from(ErrorKind::DeserializationError)) + } else { + Ok(StorageUnit { + balance: Coin::new(LittleEndian::read_u64(&bytes[0..8])) + .context(ErrorKind::DeserializationError)?, + last_block_height: LittleEndian::read_u64(&bytes[8..16]), + }) + } + } +} + +impl Default for StorageUnit { + fn default() -> Self { + StorageUnit { + balance: Coin::zero(), + last_block_height: Default::default(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn storage_unit_serialization() { + let storage_unit = StorageUnit::default(); + + let bytes = storage_unit.serialize(); + let new_storage_unit = + StorageUnit::deserialize_from(&bytes).expect("Unable to deserialize"); + + assert_eq!( + storage_unit, new_storage_unit, + "Serialization / deserialization implemented incorrectly" + ); + } + + #[test] + fn storage_unit_serialization_failure() { + let storage_unit = StorageUnit::default(); + + let bytes = storage_unit.serialize(); + let error = + StorageUnit::deserialize_from(&bytes[0..15]).expect_err("Deserialized incorrect value"); + + assert_eq!( + error.kind(), + ErrorKind::DeserializationError, + "Invalid error type" + ); + } +} diff --git a/client-core/src/service/key_service.rs b/client-core/src/service/key_service.rs index ba817d737..1cc5be292 100644 --- a/client-core/src/service/key_service.rs +++ b/client-core/src/service/key_service.rs @@ -1,7 +1,7 @@ use bincode::{deserialize, serialize}; use failure::ResultExt; -use crate::{ErrorKind, PrivateKey, Result, SecureStorage}; +use crate::{ErrorKind, PrivateKey, Result, SecureStorage, Storage}; /// Exposes functionality for managing public and private keys #[derive(Default)] @@ -11,7 +11,7 @@ pub struct KeyService { impl KeyService where - T: SecureStorage, + T: Storage, { /// Creates a new instance of key service pub fn new(storage: T) -> Self { @@ -83,13 +83,18 @@ mod tests { .generate("wallet_id", "passphrase") .expect("Unable to generate private key"); + let new_private_key = key_service + .generate("wallet_id", "passphrase") + .expect("Unable to generate private key"); + let keys = key_service .get_keys("wallet_id", "passphrase") .expect("Unable to get keys from storage") .expect("No keys found"); - assert_eq!(1, keys.len(), "Unexpected key length"); + assert_eq!(2, keys.len(), "Unexpected key length"); assert_eq!(private_key, keys[0], "Invalid private key found"); + assert_eq!(new_private_key, keys[1], "Invalid private key found"); let error = key_service .get_keys("wallet_id", "incorrect_passphrase") diff --git a/client-core/src/storage/sled_storage.rs b/client-core/src/storage/sled_storage.rs index 63dcd564f..2ae5f2a71 100644 --- a/client-core/src/storage/sled_storage.rs +++ b/client-core/src/storage/sled_storage.rs @@ -108,5 +108,13 @@ mod tests { let value = std::str::from_utf8(&value).expect("Unable to deserialize bytes"); assert_eq!("value", value, "Incorrect value found"); + + storage.clear().expect("Unable to clean database"); + + assert_eq!( + 0, + storage.keys().expect("Unable to get keys").len(), + "Keys present even after clearing" + ); } } diff --git a/client-core/src/wallet.rs b/client-core/src/wallet.rs index 7cad66692..5ca3d18d4 100644 --- a/client-core/src/wallet.rs +++ b/client-core/src/wallet.rs @@ -10,21 +10,29 @@ use zeroize::Zeroize; use chain_core::init::address::RedeemAddress; -use crate::service::{KeyService, WalletService}; -use crate::{ErrorKind, PublicKey, Result, Storage}; +use crate::service::{BalanceService, KeyService, WalletService}; +use crate::{Chain, ErrorKind, PublicKey, Result, Storage}; /// Interface for a generic wallet -pub trait Wallet +pub trait Wallet where + C: Chain, K: Storage, W: Storage, + B: Storage, { + /// Returns associated Crypto.com Chain client + fn chain(&self) -> &C; + /// Returns associated key service fn key_service(&self) -> &KeyService; /// Returns associated wallet service fn wallet_service(&self) -> &WalletService; + /// Returns associated balance service + fn balance_service(&self) -> &BalanceService; + /// Creates a new wallet with given name fn new_wallet(&self, name: &str, passphrase: &str) -> Result { self.wallet_service().create(name, passphrase) @@ -35,7 +43,7 @@ where let wallet_id = self.wallet_service().get(name, passphrase)?; match wallet_id { - None => Ok(None), + None => Err(ErrorKind::WalletNotFound.into()), Some(wallet_id) => { let keys = self.key_service().get_keys(&wallet_id, passphrase)?; @@ -92,4 +100,60 @@ where let address = RedeemAddress::from(&public_key); Ok(encode(address.0)) } + + /// Retrieves current balance of wallet + fn get_balance(&self, name: &str, passphrase: &str) -> Result> { + let wallet_id = self.wallet_service().get(name, passphrase)?; + + match wallet_id { + None => Err(ErrorKind::WalletNotFound.into()), + Some(wallet_id) => Ok(self + .balance_service() + .get_balance(&wallet_id, passphrase)? + .map(Into::into)), + } + } + + /// Synchronizes and returns current balance of wallet + fn sync_balance(&self, name: &str, passphrase: &str) -> Result { + let addresses = self.get_addresses(name, passphrase)?; + + match addresses { + None => Ok(0), + Some(addresses) => { + let wallet_id = self.wallet_service().get(name, passphrase)?; + + match wallet_id { + None => Err(ErrorKind::WalletNotFound.into()), + Some(wallet_id) => self + .balance_service() + .sync(&wallet_id, passphrase, addresses) + .map(Into::into), + } + } + } + } + + /// Recalculate current balance of wallet + /// + /// # Warning + /// This should only be used when you need to recalculate balance from whole history of blockchain. + fn recalculate_balance(&self, name: &str, passphrase: &str) -> Result { + let addresses = self.get_addresses(name, passphrase)?; + + match addresses { + None => Ok(0), + Some(addresses) => { + let wallet_id = self.wallet_service().get(name, passphrase)?; + + match wallet_id { + None => Err(ErrorKind::WalletNotFound.into()), + Some(wallet_id) => self + .balance_service() + .sync_all(&wallet_id, passphrase, addresses) + .map(Into::into), + } + } + } + } } diff --git a/client-core/src/wallet/sled_wallet.rs b/client-core/src/wallet/sled_wallet.rs index e4d316749..c03175b45 100644 --- a/client-core/src/wallet/sled_wallet.rs +++ b/client-core/src/wallet/sled_wallet.rs @@ -1,23 +1,31 @@ #![cfg(feature = "sled")] +#[cfg(not(test))] use std::path::Path; -use crate::service::{KeyService, WalletService}; +use crate::service::{BalanceService, KeyService, WalletService}; use crate::storage::SledStorage; -use crate::{Result, Wallet}; +use crate::{Chain, Result, Wallet}; const KEY_PATH: &str = "key"; const WALLET_PATH: &str = "wallet"; +const BALANCE_PATH: &str = "balance"; /// Wallet backed by [`SledStorage`](crate::storage::SledStorage) -pub struct SledWallet { +pub struct SledWallet { + chain: C, key_service: KeyService, wallet_service: WalletService, + balance_service: BalanceService, } -impl SledWallet { - /// Creates a new instance with specified path for data storage - pub fn new>(path: P) -> Result { +impl SledWallet +where + C: Chain + Clone, +{ + /// Creates a new instance with specified path for data storage' + #[cfg(not(test))] + pub fn new>(path: P, chain: C) -> Result { let mut path_buf = path.as_ref().to_path_buf(); path_buf.push(KEY_PATH); @@ -30,14 +38,49 @@ impl SledWallet { let wallet_storage = SledStorage::new(path_buf.as_path())?; let wallet_service = WalletService::new(wallet_storage); + path_buf.pop(); + path_buf.push(BALANCE_PATH); + + let balance_storage = SledStorage::new(path_buf.as_path())?; + let balance_service = BalanceService::new(chain.clone(), balance_storage); + + Ok(SledWallet { + chain, + key_service, + wallet_service, + balance_service, + }) + } + + /// Creates a new instance with specified path for data storage' + #[cfg(test)] + pub fn new(path: String, chain: C) -> Result { + let key_storage = SledStorage::new(path.clone() + KEY_PATH)?; + let key_service = KeyService::new(key_storage); + + let wallet_storage = SledStorage::new(path.clone() + WALLET_PATH)?; + let wallet_service = WalletService::new(wallet_storage); + + let balance_storage = SledStorage::new(path + BALANCE_PATH)?; + let balance_service = BalanceService::new(chain.clone(), balance_storage); + Ok(SledWallet { + chain, key_service, wallet_service, + balance_service, }) } } -impl Wallet for SledWallet { +impl Wallet for SledWallet +where + C: Chain, +{ + fn chain(&self) -> &C { + &self.chain + } + fn key_service(&self) -> &KeyService { &self.key_service } @@ -45,16 +88,22 @@ impl Wallet for SledWallet { fn wallet_service(&self) -> &WalletService { &self.wallet_service } + + fn balance_service(&self) -> &BalanceService { + &self.balance_service + } } #[cfg(test)] mod tests { use super::SledWallet; - use crate::Wallet; + use crate::chain::MockChain; + use crate::{ErrorKind, Wallet}; #[test] fn check_happy_flow() { - let wallet = SledWallet::new("./wallet-test").expect("Unable to create sled wallet"); + let wallet = SledWallet::new("./wallet-test".to_string(), MockChain::default()) + .expect("Unable to create sled wallet"); wallet .new_wallet("name", "passphrase") @@ -64,7 +113,7 @@ mod tests { None, wallet .get_addresses("name", "passphrase") - .expect("Unable to retrieve addresses"), + .expect("Unable to get addresses for wallet"), "Wallet already has keys" ); @@ -79,5 +128,23 @@ mod tests { assert_eq!(1, addresses.len(), "Invalid addresses length"); assert_eq!(address, addresses[0], "Addresses don't match"); + + assert_eq!( + ErrorKind::WalletNotFound, + wallet + .get_public_keys("name_new", "passphrase") + .expect_err("Found public keys for non existent wallet") + .kind(), + "Invalid public key present in database" + ); + + assert_eq!( + ErrorKind::WalletNotFound, + wallet + .generate_public_key("name_new", "passphrase") + .expect_err("Generated public key for non existent wallet") + .kind(), + "Error of invalid kind received" + ); } }