Skip to content

Commit

Permalink
feat: add transaction fee & validation
Browse files Browse the repository at this point in the history
  • Loading branch information
slavik-pastushenko committed Nov 11, 2023
1 parent 66a67b5 commit 01eb80a
Show file tree
Hide file tree
Showing 3 changed files with 168 additions and 31 deletions.
22 changes: 16 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,11 @@ A Rust library provides a command-line interface (CLI) for interacting with bloc
- `get_transactions()`: Get a list of current transactions in the blockchain.
- `get_transaction(hash)`: Get a transaction by its hash.
- `add_transaction(from, to, amount)`: Add a new transaction to the blockchain.
- `validate_transaction(from, amount)`: Validate a new transaction to the blockchain.
- `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.
- `update_fee(fee)`: Update the transaction fee.
- `generate_new_block()`: Generate a new block and append it to the blockchain.
- `get_merkle(transactions)`: Calculate the Merkle root hash for a list of transactions.
- `proof_of_work(header)`: Perform the proof-of-work process to mine a block.
Expand All @@ -34,11 +36,18 @@ A Rust library provides a command-line interface (CLI) for interacting with bloc
| `address` | The address associated with the blockchain. |
| `difficulty` | The initial mining difficulty level of the network. |
| `reward` | The initial block reward for miners. |
| `fee` | The transaction fee. |

## Safety

This crate uses `#![forbid(unsafe_code)]` to ensure everything is implemented in 100% safe Rust.

## Documentation

For more in-depth details, please refer to the full [documentation](https://docs.rs/blockchain-cli).

If you encounter any issues or have questions that are not addressed in the documentation, feel free to [submit an issue](https://github.com/slavik-pastushenko/blockchain-rust/issues).

## Usage

Run the following Cargo command in your project directory::
Expand All @@ -57,7 +66,8 @@ fn main() {
let mut chain = Chain::new(
String::from("bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh"),
2,
100.0
100.0,
0.01,
);

// Add a transaction
Expand Down Expand Up @@ -87,25 +97,25 @@ fn main() {

## Contributing

- Build an application:
Build an application:

```bash
cargo build
```

- Test an application:
Test an application:

```bash
cargo test
```

- Run an application:
Run an application:

```bash
cargo run
```

- Run [clippy](https://github.com/rust-lang/rust-clippy):
Run [clippy](https://github.com/rust-lang/rust-clippy):

```bash
cargo clippy --all-targets --all-features -- -D warnings
Expand All @@ -117,7 +127,7 @@ Run [lint](https://github.com/rust-lang/rustfmt):
cargo fmt
```

- Generate documentation in HTML format:
Generate documentation in HTML format:

```bash
cargo doc --open
Expand Down
36 changes: 35 additions & 1 deletion src/bin/blockchain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,21 @@ fn main() -> std::io::Result<()> {
})
.interact()?;

let fee: f32 = cliclack::input("Transaction fee")
.default_input("0.0")
.validate(|input: &String| {
if input.is_empty() {
Err("Please enter a transaction fee")
} else {
Ok(())
}
})
.interact()?;

let mut spinner = spinner();
spinner.start("Generating a genesis block...");

let mut chain = Chain::new(address.trim().to_string(), difficulty, reward);
let mut chain = Chain::new(address.trim().to_string(), difficulty, reward, fee);

spinner.stop("✅ Blockchain was created successfully");

Expand All @@ -53,6 +64,7 @@ fn main() -> std::io::Result<()> {
.item("generate_block", "Generate a new block", "")
.item("change_reward", "Change a reward", "")
.item("change_difficulty", "Change a difficulty", "")
.item("change_fee", "Change a transaction fee", "")
.item("exit", "Exit", "")
.interact()?;

Expand Down Expand Up @@ -175,6 +187,28 @@ fn main() -> std::io::Result<()> {
}
}
}
"change_fee" => {
let new_fee: String = cliclack::input("New transaction fee")
.validate(|input: &String| {
if input.is_empty() {
Err("Please enter a new transaction fee")
} else {
Ok(())
}
})
.interact()?;

let confirm = cliclack::confirm("Confirm changing a transaction fee").interact()?;

if confirm {
let res = chain.update_fee(new_fee.trim().parse().unwrap());

match res {
true => println!("✅ Transaction fee was changed successfully"),
false => println!("❌ Cannot change a transaction fee"),
}
}
}
"exit" => {
break;
}
Expand Down
141 changes: 117 additions & 24 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,74 +5,77 @@ use serde_derive::Serialize;
use sha2::{Digest, Sha256};
use std::fmt::Write;

/// An exchange of assets between two parties
/// Exchange of assets between two parties
#[derive(Debug, Clone, Serialize)]
pub struct Transaction {
/// A transaction hash
/// Transaction hash
pub hash: String,

/// A transaction sender address
/// Transaction sender address
pub from: String,

/// A transaction receiver address
/// Transaction receiver address
pub to: String,

/// A transaction amount
/// Transaction amount
pub amount: f32,

/// A transaction timestamp
/// Transaction timestamp
pub timestamp: i64,
}

/// A identifier of a particular block on an entire blockchain
/// Identifier of a particular block on an entire blockchain
#[derive(Debug, Serialize)]
pub struct BlockHeader {
/// A timestamp at which a block was mined
/// Timestamp at which a block was mined
pub timestamp: i64,

/// An integer to achieve the network's difficulty
/// Integer to achieve the network's difficulty
pub nonce: u32,

/// A hash of a previous block
/// Hash of a previous block
pub previous_hash: String,

/// A Merkel root hash
/// Merkel root hash
pub merkle: String,

/// A current difficulty level of the network
/// Current difficulty level of the network
pub difficulty: u32,
}

/// A data storage in a blockchain
/// Data storage in a blockchain
#[derive(Debug, Serialize)]
pub struct Block {
/// Information about the block and the miner
pub header: BlockHeader,

/// A total amount of transactions
/// Total amount of transactions
pub count: usize,

/// An amount of transactions
pub transactions: Vec<Transaction>,
}

/// A blockchain
/// Blockchain
#[derive(Debug, Serialize)]
pub struct Chain {
/// A chain of blocks
/// Chain of blocks
pub chain: Vec<Block>,

/// A list of transactions
/// List of transactions
pub current_transactions: Vec<Transaction>,

/// A current difficulty level of the network
/// Current difficulty level of the network
pub difficulty: u32,

/// A blockchain genesis address
/// Blockchain genesis address
pub address: String,

/// A block reward
/// Block reward
pub reward: f32,

/// Transaction fee
pub fee: f32,
}

impl Chain {
Expand All @@ -82,11 +85,13 @@ impl Chain {
/// - `address`: The address associated with the blockchain.
/// - `difficulty`: The initial mining difficulty level of the network.
/// - `reward`: The initial block reward for miners.
/// - `fee`: The transaction fee.
///
/// # Returns
/// A new `Chain` instance with the given parameters and a genesis block.
pub fn new(address: String, difficulty: u32, reward: f32) -> Self {
pub fn new(address: String, difficulty: u32, reward: f32, fee: f32) -> Self {
let mut chain = Chain {
fee,
reward,
address,
difficulty,
Expand Down Expand Up @@ -130,20 +135,48 @@ impl Chain {
/// # Returns
/// `true` if the transaction is successfully added to the current transactions.
pub fn add_transaction(&mut self, from: String, to: String, amount: f32) -> bool {
// Validate the transaction
if !self.validate_transaction(&from, amount) {
return false;
}

let timestamp = Utc::now().timestamp();
let hash = Chain::hash(&(&from, &to, amount, timestamp));
let total_amount = amount * self.fee;
let hash = Chain::hash(&(&from, &to, total_amount, timestamp));

self.current_transactions.push(Transaction {
to,
from,
hash,
amount,
amount: total_amount,
timestamp: Utc::now().timestamp(),
});

true
}

/// Validate a transaction.
///
/// # Arguments
/// - `from`: The sender's address.
/// - `amount`: The amount of the transaction.
///
/// # Returns
/// `true` if the transaction is valid, `false` otherwise.
pub fn validate_transaction(&self, from: &str, amount: f32) -> bool {
// Validate if the sender is not the root
if from == "Root" {
return false;
}

// Validate if the amount is non-negative
if amount <= 0.0 {
return false;
}

true
}

/// Get the hash of the last block in the blockchain.
///
/// # Returns
Expand Down Expand Up @@ -185,6 +218,19 @@ impl Chain {
true
}

/// Update the transaction fee.
///
/// # Arguments
/// - `fee`: The new transaction fee value.
///
/// # Returns
/// `true` if the transaction fee is successfully updated.
pub fn update_fee(&mut self, fee: f32) -> bool {
self.fee = fee;

true
}

/// Generate a new block and append it to the blockchain.
///
/// # Returns
Expand Down Expand Up @@ -318,7 +364,7 @@ mod tests {
use super::*;

fn setup() -> Chain {
Chain::new("Address".to_string(), 1, 100.0)
Chain::new("Address".to_string(), 1, 100.0, 0.0)
}

#[test]
Expand All @@ -331,6 +377,43 @@ mod tests {
assert_eq!(chain.current_transactions.len(), 1);
}

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

let result = chain.add_transaction("Sender".to_string(), "Receiver".to_string(), 0.0);

assert!(!result);
assert!(chain.current_transactions.is_empty());
}

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

let result = chain.validate_transaction("Sender", 10.0);

assert!(result);
}

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

let result = chain.validate_transaction("Sender", -1.0);

assert!(!result);
}

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

let result = chain.validate_transaction("Root", 0.01);

assert!(!result);
}

#[test]
fn test_get_transaction_found() {
let mut chain = setup();
Expand Down Expand Up @@ -403,6 +486,16 @@ mod tests {
assert_eq!(chain.reward, 50.0);
}

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

let result = chain.update_fee(0.02);

assert!(result);
assert_eq!(chain.fee, 0.02);
}

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

0 comments on commit 01eb80a

Please sign in to comment.