Skip to content

Commit

Permalink
feat(wallet): add transaction history
Browse files Browse the repository at this point in the history
  • Loading branch information
slavik-pastushenko committed Feb 18, 2024
1 parent 4268adf commit 2513294
Show file tree
Hide file tree
Showing 9 changed files with 147 additions and 34 deletions.
12 changes: 0 additions & 12 deletions .github/dependabot.yml

This file was deleted.

2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ jobs:
- name: Run lint
run: cargo fmt -- --check
- name: Run tests
run: cargo tarpaulin --lib --out xml
run: cargo tarpaulin --out xml
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v3
env:
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ A Rust crate provides an interface for interacting with a blockchain.
- `validate_transaction(from, amount)`: Validate a new transaction to the blockchain.
- `create_wallet(email)`: Create a new wallet with a unique email and an initial balance.
- `get_wallet_balance(address)`: Get a wallet's balance based on its address.
- `get_wallet_transactions(address)`: Get a wallet's transaction history based on its address.
- `get_last_hash()`: Get the hash of the last block in the blockchain.
- `update_difficulty(difficulty)`: Update the mining difficulty of the blockchain.
- `update_reward(reward)`: Update the block reward.
Expand Down
42 changes: 33 additions & 9 deletions examples/api-axum/src/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,14 +81,39 @@ pub async fn get_wallet_balance(
let chain = state.chain.lock().unwrap();
let balance = chain.get_wallet_balance(params.address);

if balance.is_none() {
return (
match balance {
Some(balance) => (StatusCode::OK, Json(json!({ "data": balance }))),
None => (
StatusCode::NOT_FOUND,
Json(json!({ "message": "Wallet is not found" })),
);
),
}
}

/// Get a list of transactions of a wallet.
///
/// # Arguments
///
/// * `state` - The application state.
/// * `params` - The request query parameters.
///
/// # Returns
///
/// The list of transactions of the wallet.
pub async fn get_wallet_transactions(
State(state): State<AppState>,
Query(params): Query<GetWalletBalance>,
) -> impl IntoResponse {
let chain = state.chain.lock().unwrap();
let transaction = chain.get_wallet_transactions(params.address);

(StatusCode::OK, Json(json!({ "data": balance })))
match transaction {
Some(transaction) => (StatusCode::OK, Json(json!({ "data": transaction }))),
None => (
StatusCode::NOT_FOUND,
Json(json!({ "message": "Wallet is not found" })),
),
}
}

/// Get all transactions.
Expand Down Expand Up @@ -124,14 +149,13 @@ pub async fn get_transaction(
let chain = state.chain.lock().unwrap();
let transaction = chain.get_transaction(hash);

if transaction.is_none() {
return (
match transaction {
Some(transaction) => (StatusCode::OK, Json(json!({ "data": transaction }))),
None => (
StatusCode::NOT_FOUND,
Json(json!({ "message": "Transaction is not found" })),
);
),
}

(StatusCode::OK, Json(json!({ "data": transaction })))
}

/// Add a new transaction.
Expand Down
4 changes: 4 additions & 0 deletions examples/api-axum/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ async fn main() {
.route("/transactions", get(handlers::get_transactions))
.route("/transactions", post(handlers::add_transaction))
.route("/wallet/balance", get(handlers::get_wallet_balance))
.route(
"/wallet/transactions",
get(handlers::get_wallet_transactions),
)
.route("/wallet/create", post(handlers::create_wallet))
.with_state(state);

Expand Down
29 changes: 26 additions & 3 deletions examples/cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@ fn main() -> std::io::Result<()> {
.initial_value("add_transaction")
.item("create_wallet", "Create a new wallet", "")
.item("get_wallet_balance", "Get a wallet balance", "")
.item(
"get_wallet_transactions",
"Get a wallet transaction history",
"",
)
.item("add_transaction", "Add a new transaction", "")
.item("get_transaction", "Get a transaction", "")
.item("get_transactions", "Get all transactions", "")
Expand Down Expand Up @@ -96,9 +101,27 @@ fn main() -> std::io::Result<()> {

let balance = chain.get_wallet_balance(address);

match balance.is_some() {
true => println!("✅ Wallet balance: {}", balance.unwrap()),
false => println!("❌ Cannot find a wallet"),
match balance {
Some(balance) => println!("✅ Wallet balance: {}", balance),
None => println!("❌ Cannot find a wallet"),
}
}
"get_wallet_transactions" => {
let address: String = cliclack::input("Address")
.validate(|input: &String| {
if input.is_empty() {
Err("Please enter an address")
} else {
Ok(())
}
})
.interact()?;

let transactions = chain.get_wallet_transactions(address);

match transactions {
Some(transactions) => println!("✅ Wallet transactions: {:?}", transactions),
None => println!("❌ Cannot find a wallet"),
}
}
"add_transaction" => {
Expand Down
49 changes: 40 additions & 9 deletions src/chain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,17 +90,19 @@ impl Chain {
pub fn add_transaction(&mut self, from: String, to: String, amount: f64) -> bool {
let total = amount * self.fee;

// Validate the transaction
if !self.validate_transaction(&from, &to, total) {
return false;
}
// Validate the transaction and create a new transaction if it is valid
let transaction = match self.validate_transaction(&from, &to, total) {
true => Transaction::new(from.to_owned(), to.to_owned(), self.fee, total),
false => return false,
};

// Update sender's balance
match self.wallets.get_mut(&from) {
Some(wallet) => {
wallet.balance -= total;

wallet
// Add the transaction to the sender's transaction history
wallet.transactions.push(transaction.hash.to_owned());
}
None => return false,
};
Expand All @@ -110,14 +112,12 @@ impl Chain {
Some(wallet) => {
wallet.balance += amount;

wallet
// Add the transaction to the receiver's transaction history
wallet.transactions.push(transaction.hash.to_owned());
}
None => return false,
};

// Create a transaction
let transaction = Transaction::new(from, to, self.fee, total);

// Add the transaction to the current transactions
self.current_transactions.push(transaction);

Expand Down Expand Up @@ -196,6 +196,37 @@ impl Chain {
self.wallets.get(&address).map(|wallet| wallet.balance)
}

/// Get a wallet's transaction history based on its address.
///
/// # Arguments
/// - `address`: The unique wallet address.
///
/// # Returns
/// The wallet transaction history.
pub fn get_wallet_transactions(&self, address: String) -> Option<Vec<Transaction>> {
match self
.wallets
.get(&address)
.map(|wallet| wallet.transactions.to_owned())
{
// Get the transaction history of the wallet
Some(txs) => {
let mut result = Vec::new();

for tx in txs {
match self.get_transaction(tx) {
Some(transaction) => result.push(transaction.to_owned()),
None => continue,
}
}

Some(result)
}
// Return None if the wallet is not found
None => None,
}
}

/// Get the hash of the last block in the blockchain.
///
/// # Returns
Expand Down
5 changes: 5 additions & 0 deletions src/wallet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ pub struct Wallet {

/// The current balance of the wallet.
pub balance: f64,

/// A history of transactions associated with the wallet.
pub transactions: Vec<String>,
}

impl Wallet {
Expand All @@ -30,6 +33,7 @@ impl Wallet {
email,
address,
balance,
transactions: vec![],
}
}
}
Expand All @@ -48,5 +52,6 @@ mod tests {
assert_eq!(wallet.email, email);
assert_eq!(wallet.address, address);
assert_eq!(wallet.balance, balance);
assert!(wallet.transactions.is_empty());
}
}
37 changes: 37 additions & 0 deletions tests/chain_integration_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,43 @@ fn test_get_wallet_balance_not_found() {
assert!(result.is_none());
}

#[test]
fn test_get_wallet_transactions() {
let mut chain = setup();

let from = chain.create_wallet("[email protected]".to_string());
let to = chain.create_wallet("[email protected]".to_string());

let sender = chain.wallets.get_mut(&from).unwrap();
sender.balance += 20.0;

chain.add_transaction(from.clone(), to.clone(), 10.0);

let transactions = chain.get_wallet_transactions(from).unwrap();

assert!(!transactions.is_empty());
}

#[test]
fn test_get_new_wallet_transactions() {
let mut chain = setup();

let from = chain.create_wallet("[email protected]".to_string());

let transactions = chain.get_wallet_transactions(from).unwrap();

assert!(transactions.is_empty());
}

#[test]
fn test_get_wallet_transactions_not_found() {
let chain = setup();

let transactions = chain.get_wallet_transactions("address".to_string());

assert!(transactions.is_none());
}

#[test]
fn test_get_last_hash() {
let chain = setup();
Expand Down

0 comments on commit 2513294

Please sign in to comment.