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:
+
+
+
+
+
+### `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