From 59261a72555719b3af1e2da9d5bbabc929a256ea Mon Sep 17 00:00:00 2001 From: Devashish Dixit Date: Tue, 26 Mar 2019 11:06:29 +0800 Subject: [PATCH 1/9] Added more tests --- client-core/src/service/key_service.rs | 7 ++++++- client-core/src/storage/sled_storage.rs | 8 ++++++++ client-core/src/wallet/sled_wallet.rs | 19 ++++++++++++++++++- 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/client-core/src/service/key_service.rs b/client-core/src/service/key_service.rs index ba817d737..1ce19aa9d 100644 --- a/client-core/src/service/key_service.rs +++ b/client-core/src/service/key_service.rs @@ -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/sled_wallet.rs b/client-core/src/wallet/sled_wallet.rs index e4d316749..794a280d2 100644 --- a/client-core/src/wallet/sled_wallet.rs +++ b/client-core/src/wallet/sled_wallet.rs @@ -50,7 +50,7 @@ impl Wallet for SledWallet { #[cfg(test)] mod tests { use super::SledWallet; - use crate::Wallet; + use crate::{ErrorKind, Wallet}; #[test] fn check_happy_flow() { @@ -79,5 +79,22 @@ mod tests { assert_eq!(1, addresses.len(), "Invalid addresses length"); assert_eq!(address, addresses[0], "Addresses don't match"); + + assert_eq!( + None, + wallet + .get_public_keys("name_new", "passphrase") + .expect("Unable to get public keys"), + "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" + ); } } From 65ac57f41c0d717680b9b1177b094eadc51531e5 Mon Sep 17 00:00:00 2001 From: Devashish Dixit Date: Tue, 26 Mar 2019 17:24:39 +0800 Subject: [PATCH 2/9] Initial version of transaction storage and syncing --- Cargo.lock | 1 + client-core/Cargo.toml | 1 + client-core/src/balance.rs | 6 + client-core/src/balance/balance_change.rs | 85 ++++++++++ client-core/src/balance/transaction_change.rs | 94 +++++++++++ client-core/src/chain.rs | 16 ++ client-core/src/error.rs | 6 + client-core/src/lib.rs | 10 +- client-core/src/service.rs | 2 + client-core/src/service/key_service.rs | 4 +- .../src/service/transaction_service.rs | 148 ++++++++++++++++++ client-core/src/wallet/sled_wallet.rs | 22 ++- 12 files changed, 390 insertions(+), 5 deletions(-) create mode 100644 client-core/src/balance.rs create mode 100644 client-core/src/balance/balance_change.rs create mode 100644 client-core/src/balance/transaction_change.rs create mode 100644 client-core/src/chain.rs create mode 100644 client-core/src/service/transaction_service.rs diff --git a/Cargo.lock b/Cargo.lock index 3f90bfbbe..39e83c422 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -240,6 +240,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..0028b552e 100644 --- a/client-core/Cargo.toml +++ b/client-core/Cargo.toml @@ -16,6 +16,7 @@ blake2 = "0.8" hex = "0.3" zeroize = "0.6" zeroize_derive = "0.1" +byteorder = "1.3" sled = { version = "0.19", optional = true } [features] 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..a710ee6df --- /dev/null +++ b/client-core/src/balance/transaction_change.rs @@ -0,0 +1,94 @@ +use std::ops::Add; + +use crate::balance::BalanceChange; +use crate::Result; + +use chain_core::init::coin::Coin; + +/// Represents balance change in a transaction +#[derive(Debug)] +pub struct TransactionChange { + /// ID of transaction which caused this change + pub transaction_id: String, + /// Address which is affected by this change + pub address: String, + /// 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::*; + + fn get_transaction_change(balance_change: BalanceChange) -> TransactionChange { + TransactionChange { + transaction_id: Default::default(), + address: 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..e44685924 --- /dev/null +++ b/client-core/src/chain.rs @@ -0,0 +1,16 @@ +//! Communication between client and chain +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/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..196bff7b7 100644 --- a/client-core/src/service.rs +++ b/client-core/src/service.rs @@ -1,6 +1,8 @@ //! Management services mod key_service; +mod transaction_service; mod wallet_service; pub use self::key_service::KeyService; +pub use self::transaction_service::TransactionService; pub use self::wallet_service::WalletService; diff --git a/client-core/src/service/key_service.rs b/client-core/src/service/key_service.rs index 1ce19aa9d..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 { diff --git a/client-core/src/service/transaction_service.rs b/client-core/src/service/transaction_service.rs new file mode 100644 index 000000000..ad6f5b6a7 --- /dev/null +++ b/client-core/src/service/transaction_service.rs @@ -0,0 +1,148 @@ +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 TransactionService { + chain: C, + storage: T, +} + +impl TransactionService +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) -> Result { + unimplemented!() + } + + /// 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())?; + + let storage_unit = match bytes { + None => Err(ErrorKind::BalanceNotFound.into()), + Some(bytes) => StorageUnit::deserialize_from(&bytes), + }?; + + Ok(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/wallet/sled_wallet.rs b/client-core/src/wallet/sled_wallet.rs index 794a280d2..baee090e7 100644 --- a/client-core/src/wallet/sled_wallet.rs +++ b/client-core/src/wallet/sled_wallet.rs @@ -1,5 +1,6 @@ #![cfg(feature = "sled")] +#[cfg(not(test))] use std::path::Path; use crate::service::{KeyService, WalletService}; @@ -16,7 +17,8 @@ pub struct SledWallet { } impl SledWallet { - /// Creates a new instance with specified path for data storage + /// Creates a new instance with specified path for data storage' + #[cfg(not(test))] pub fn new>(path: P) -> Result { let mut path_buf = path.as_ref().to_path_buf(); path_buf.push(KEY_PATH); @@ -35,6 +37,21 @@ impl SledWallet { wallet_service, }) } + + /// Creates a new instance with specified path for data storage' + #[cfg(test)] + pub fn new(path: String) -> Result { + let key_storage = SledStorage::new(path.clone() + KEY_PATH)?; + let key_service = KeyService::new(key_storage); + + let wallet_storage = SledStorage::new(path + WALLET_PATH)?; + let wallet_service = WalletService::new(wallet_storage); + + Ok(SledWallet { + key_service, + wallet_service, + }) + } } impl Wallet for SledWallet { @@ -54,7 +71,8 @@ mod tests { #[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()).expect("Unable to create sled wallet"); wallet .new_wallet("name", "passphrase") From fa8176f9d6e1d1bbc23c0544a5073b1fcc45a67b Mon Sep 17 00:00:00 2001 From: Devashish Dixit Date: Wed, 27 Mar 2019 13:40:06 +0800 Subject: [PATCH 3/9] Use types from chain_core for transaction_id and address --- client-core/src/balance/transaction_change.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/client-core/src/balance/transaction_change.rs b/client-core/src/balance/transaction_change.rs index a710ee6df..4911ea7f0 100644 --- a/client-core/src/balance/transaction_change.rs +++ b/client-core/src/balance/transaction_change.rs @@ -4,14 +4,16 @@ 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: String, + pub transaction_id: TxId, /// Address which is affected by this change - pub address: String, + pub address: ExtendedAddr, /// Change in balance pub balance_change: BalanceChange, } @@ -39,7 +41,7 @@ mod tests { fn get_transaction_change(balance_change: BalanceChange) -> TransactionChange { TransactionChange { transaction_id: Default::default(), - address: Default::default(), + address: ExtendedAddr::BasicRedeem(Default::default()), balance_change, } } From 46f2f7566f52089c1707856bfc8942da74f3fb5f Mon Sep 17 00:00:00 2001 From: Devashish Dixit Date: Mon, 1 Apr 2019 11:15:39 +0800 Subject: [PATCH 4/9] Integrated balance service with wallet trait --- client-core/Cargo.toml | 1 - client-core/src/chain.rs | 7 +++ client-core/src/chain/mock_chain.rs | 18 +++++++ client-core/src/service.rs | 4 +- ...nsaction_service.rs => balance_service.rs} | 37 ++++++++++++-- client-core/src/wallet.rs | 14 +++-- client-core/src/wallet/sled_wallet.rs | 51 +++++++++++++++---- 7 files changed, 112 insertions(+), 20 deletions(-) create mode 100644 client-core/src/chain/mock_chain.rs rename client-core/src/service/{transaction_service.rs => balance_service.rs} (80%) diff --git a/client-core/Cargo.toml b/client-core/Cargo.toml index 0028b552e..3ded7eecf 100644 --- a/client-core/Cargo.toml +++ b/client-core/Cargo.toml @@ -21,4 +21,3 @@ sled = { version = "0.19", optional = true } [features] default = ["sled"] -hash-map = [] diff --git a/client-core/src/chain.rs b/client-core/src/chain.rs index e44685924..5827fffa0 100644 --- a/client-core/src/chain.rs +++ b/client-core/src/chain.rs @@ -1,4 +1,11 @@ //! Communication between client and chain + +#[cfg(test)] +mod mock_chain; + +#[cfg(test)] +pub use mock_chain::MockChain; + use crate::balance::TransactionChange; use crate::Result; diff --git a/client-core/src/chain/mock_chain.rs b/client-core/src/chain/mock_chain.rs new file mode 100644 index 000000000..f743cda51 --- /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)> { + unimplemented!() + } +} diff --git a/client-core/src/service.rs b/client-core/src/service.rs index 196bff7b7..4dd85ca9e 100644 --- a/client-core/src/service.rs +++ b/client-core/src/service.rs @@ -1,8 +1,8 @@ //! Management services +mod balance_service; mod key_service; -mod transaction_service; mod wallet_service; +pub use self::balance_service::BalanceService; pub use self::key_service::KeyService; -pub use self::transaction_service::TransactionService; pub use self::wallet_service::WalletService; diff --git a/client-core/src/service/transaction_service.rs b/client-core/src/service/balance_service.rs similarity index 80% rename from client-core/src/service/transaction_service.rs rename to client-core/src/service/balance_service.rs index ad6f5b6a7..462c1423e 100644 --- a/client-core/src/service/transaction_service.rs +++ b/client-core/src/service/balance_service.rs @@ -7,12 +7,12 @@ use crate::{Chain, Error, ErrorKind, Result, SecureStorage, Storage}; /// Exposes functionalities for transaction storage and syncing #[derive(Default)] -pub struct TransactionService { +pub struct BalanceService { chain: C, storage: T, } -impl TransactionService +impl BalanceService where C: Chain, T: Storage, @@ -56,8 +56,37 @@ where /// /// # 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) -> Result { - unimplemented!() + 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. diff --git a/client-core/src/wallet.rs b/client-core/src/wallet.rs index 7cad66692..0769278a1 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 assiciated 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) diff --git a/client-core/src/wallet/sled_wallet.rs b/client-core/src/wallet/sled_wallet.rs index baee090e7..c11efb197 100644 --- a/client-core/src/wallet/sled_wallet.rs +++ b/client-core/src/wallet/sled_wallet.rs @@ -3,23 +3,29 @@ #[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 { +impl SledWallet +where + C: Chain + Clone, +{ /// Creates a new instance with specified path for data storage' #[cfg(not(test))] - pub fn new>(path: P) -> Result { + pub fn new>(path: P, chain: C) -> Result { let mut path_buf = path.as_ref().to_path_buf(); path_buf.push(KEY_PATH); @@ -32,29 +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) -> Result { + 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 + WALLET_PATH)?; + 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 } @@ -62,17 +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::chain::MockChain; use crate::{ErrorKind, Wallet}; #[test] fn check_happy_flow() { - let wallet = - SledWallet::new("./wallet-test".to_string()).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") From 0654acdf41139f0dd07658b2c5f7a6713ce02ee0 Mon Sep 17 00:00:00 2001 From: Devashish Dixit Date: Mon, 1 Apr 2019 11:59:52 +0800 Subject: [PATCH 5/9] Added balance functions in wallet --- client-core/src/chain/mock_chain.rs | 2 +- client-core/src/service/balance_service.rs | 15 +++--- client-core/src/wallet.rs | 60 +++++++++++++++++++++- client-core/src/wallet/sled_wallet.rs | 7 +-- 4 files changed, 71 insertions(+), 13 deletions(-) diff --git a/client-core/src/chain/mock_chain.rs b/client-core/src/chain/mock_chain.rs index f743cda51..3e52e872f 100644 --- a/client-core/src/chain/mock_chain.rs +++ b/client-core/src/chain/mock_chain.rs @@ -13,6 +13,6 @@ impl Chain for MockChain { _addresses: Vec, _last_block_height: u64, ) -> Result<(Vec, u64)> { - unimplemented!() + Ok((Default::default(), Default::default())) } } diff --git a/client-core/src/service/balance_service.rs b/client-core/src/service/balance_service.rs index 462c1423e..d0e5ed8c1 100644 --- a/client-core/src/service/balance_service.rs +++ b/client-core/src/service/balance_service.rs @@ -90,17 +90,18 @@ where } /// Returns balance for a given wallet ID. - pub fn get_balance(&self, wallet_id: &str, passphrase: &str) -> Result { + pub fn get_balance(&self, wallet_id: &str, passphrase: &str) -> Result> { let bytes = self .storage .get_secure(wallet_id.as_bytes(), passphrase.as_bytes())?; - let storage_unit = match bytes { - None => Err(ErrorKind::BalanceNotFound.into()), - Some(bytes) => StorageUnit::deserialize_from(&bytes), - }?; - - Ok(storage_unit.balance) + match bytes { + None => Ok(None), + Some(bytes) => { + let storage_unit = StorageUnit::deserialize_from(&bytes)?; + Ok(Some(storage_unit.balance)) + } + } } } diff --git a/client-core/src/wallet.rs b/client-core/src/wallet.rs index 0769278a1..5ca3d18d4 100644 --- a/client-core/src/wallet.rs +++ b/client-core/src/wallet.rs @@ -21,7 +21,7 @@ where W: Storage, B: Storage, { - /// Returns assiciated Crypto.com Chain client + /// Returns associated Crypto.com Chain client fn chain(&self) -> &C; /// Returns associated key service @@ -43,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)?; @@ -100,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 c11efb197..c03175b45 100644 --- a/client-core/src/wallet/sled_wallet.rs +++ b/client-core/src/wallet/sled_wallet.rs @@ -113,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" ); @@ -130,10 +130,11 @@ mod tests { assert_eq!(address, addresses[0], "Addresses don't match"); assert_eq!( - None, + ErrorKind::WalletNotFound, wallet .get_public_keys("name_new", "passphrase") - .expect("Unable to get public keys"), + .expect_err("Found public keys for non existent wallet") + .kind(), "Invalid public key present in database" ); From d1f76091610f0c12c6062c699dab5bd4efa8463f Mon Sep 17 00:00:00 2001 From: Devashish Dixit Date: Mon, 1 Apr 2019 12:11:22 +0800 Subject: [PATCH 6/9] Merged upstream master --- client-core/src/balance/transaction_change.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client-core/src/balance/transaction_change.rs b/client-core/src/balance/transaction_change.rs index 4911ea7f0..60a5aef26 100644 --- a/client-core/src/balance/transaction_change.rs +++ b/client-core/src/balance/transaction_change.rs @@ -37,10 +37,11 @@ impl Add for Coin { #[cfg(test)] mod tests { use super::*; + use chain_core::tx::data::txid_hash; fn get_transaction_change(balance_change: BalanceChange) -> TransactionChange { TransactionChange { - transaction_id: Default::default(), + transaction_id: txid_hash(&[0, 1, 2]), address: ExtendedAddr::BasicRedeem(Default::default()), balance_change, } From 4ebb702817e4df27b958c4f83d534c23f67fa5d6 Mon Sep 17 00:00:00 2001 From: Devashish Dixit Date: Mon, 1 Apr 2019 13:45:59 +0800 Subject: [PATCH 7/9] Added README for client-core --- client-core/README.md | 102 ++++++++++++++++++++++++++++++++++ client-core/client_design.png | Bin 0 -> 27604 bytes 2 files changed, 102 insertions(+) create mode 100644 client-core/README.md create mode 100644 client-core/client_design.png diff --git a/client-core/README.md b/client-core/README.md new file mode 100644 index 000000000..2efd691b9 --- /dev/null +++ b/client-core/README.md @@ -0,0 +1,102 @@ +# 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. + +### 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 0000000000000000000000000000000000000000..9a1e6ff3e6e5149abc1c0447c74919258e17f3bb GIT binary patch literal 27604 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 literal 0 HcmV?d00001 From cdcd98110c1ae38ada51ce7cbc7903f0607d7f9c Mon Sep 17 00:00:00 2001 From: Devashish Dixit Date: Mon, 1 Apr 2019 13:47:34 +0800 Subject: [PATCH 8/9] Image style in client-core README.md --- client-core/README.md | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/client-core/README.md b/client-core/README.md index 2efd691b9..e035c7ee3 100644 --- a/client-core/README.md +++ b/client-core/README.md @@ -11,19 +11,6 @@ This crate exposes following functionalities for interacting with Crypto.com Cha Below is a high level design diagram of this crate: -
Client Design
From e2434617f735842bacec68621db19a9841ae475d Mon Sep 17 00:00:00 2001 From: Devashish Dixit Date: Mon, 1 Apr 2019 13:54:26 +0800 Subject: [PATCH 9/9] Added instructions to generate API documentation --- client-core/README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/client-core/README.md b/client-core/README.md index e035c7ee3..ef9923663 100644 --- a/client-core/README.md +++ b/client-core/README.md @@ -83,6 +83,13 @@ implementations: - `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