Skip to content
This repository was archived by the owner on Apr 3, 2026. It is now read-only.

Commit 46d251c

Browse files
authored
feat: Exercise #7. Object Oriented Programming
BREAKING CHANGE: Exercise added
2 parents 6d70d98 + d56e04f commit 46d251c

11 files changed

Lines changed: 569 additions & 1 deletion

File tree

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
from time import time
2+
# CLASS IMPORTS
3+
# Have in mind that in order to correctly import a class located in a folder
4+
# You have to call first the folder (or folders in order) and lastly, the file
5+
# In this case, I make the import of the Class at the end to avoid verbosity
6+
from classes.printable import Printable
7+
8+
# Inject Printable class to include a repetable function to reuse that logic across other classes
9+
class Block(Printable):
10+
def __init__(self, index, previous_hash, transactions, proof, timestamp=None):
11+
self.index = index
12+
self.previous_hash = previous_hash
13+
self.transactions = transactions
14+
self.proof = proof
15+
self.timestamp = time() if timestamp is None else timestamp
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
import functools
2+
import json
3+
# CLASS IMPORTS
4+
# Have in mind that in order to correctly import a class located in a folder
5+
# You have to call first the folder (or folders in order) and lastly, the file
6+
# In this case, I make the import of the Class at the end to avoid verbosity
7+
from classes.block import Block
8+
from classes.transaction import Transaction
9+
from classes.verification import Verification
10+
# OTHER IMPORTS
11+
from utils import hash_block, add__line
12+
13+
MINING_REWARD = 10
14+
15+
class Blockchain:
16+
def __init__(self, node_hosting_id):
17+
genesis_block = Block(0, '', [], 100)
18+
self.hosting_id = node_hosting_id
19+
# Getters and setters are the ways to get and set class attributes without accesing them directly (check encapsulation concept)
20+
# To implement these methods, we can use one of two ways
21+
# The first one is to create private attributes (starting with double underscore)
22+
# These two attributes below will be set as private to be accesible only inside the class
23+
# self.__chain = [genesis_block]
24+
# self.__open_transactions = []
25+
# The second one is creating the attributes and add specific methods for its handling
26+
self.chain = [genesis_block]
27+
self.open_transactions = []
28+
# When you create an instance of this class, it will start the load data process
29+
self.load_data()
30+
31+
# This is a get method using the first way of encapsulation
32+
# def get_chain(self):
33+
# return self.__chain[:]
34+
35+
# This is a get method using the first way of encapsulation
36+
# def get_open_transactions(self):
37+
# return self.__open_transactions[:]
38+
39+
# This is a get method using the second way of encapsulation
40+
# The first difference is adding a decorator to indicate is a property accessor method
41+
# The second one is its method name, is the same as the property
42+
# And at last, it returns the private property (with double underscore) even if it has been asigned as a public property
43+
@property
44+
def chain(self):
45+
return self.__chain[:]
46+
47+
@property
48+
def open_transactions(self):
49+
return self.__open_transactions[:]
50+
51+
# And this is the setter method, it also starts with a decorator, but related to the property
52+
# The method includes the self reference (to the class) and a value argument that will after be used to set the private attribute value
53+
@chain.setter
54+
def chain(self, val):
55+
self.__chain = val
56+
57+
@open_transactions.setter
58+
def open_transactions(self, val):
59+
self.__open_transactions = val
60+
61+
62+
def load_data(self):
63+
try:
64+
with open('blockchain.txt', mode='r') as f:
65+
file_content = f.readlines()
66+
67+
blockchain = json.loads(file_content[0])
68+
updated_blockchain = []
69+
updated_transactions = []
70+
71+
for current_block in blockchain:
72+
block_transactions = [Transaction(tx['sender'], tx['recipient'], tx['amount']) for tx in current_block.transactions]
73+
updated_block = Block(
74+
current_block.proof,
75+
current_block.previous_hash,
76+
block_transactions,
77+
current_block.proof)
78+
updated_blockchain.append(updated_block)
79+
80+
for tx in current_block.transactions:
81+
updated_transaction = Transaction(tx['sender'], tx['recipient'], tx['amount'])
82+
updated_transactions.append(updated_transaction)
83+
84+
# Before learning about properties, the variable was been updated like this:
85+
# self.__chain = updated_blockchain
86+
# Now, updating class attribute in this way (instead the way before), you
87+
# are accessing the property instead the variable
88+
self.chain = updated_blockchain
89+
# This is the recommended alternative due is using specific features for class encapsulation
90+
self.open_transactions = updated_transactions
91+
except (IOError, IndexError):
92+
# Exception logic has been handled in class initializer
93+
pass
94+
finally:
95+
print('Cleanup')
96+
97+
def save_data(self):
98+
# To create the chain in a json-accepted format, we have to parse the list as a dictionary list
99+
savable_chain = [block.__dict__ for block in [Block(block_el.index, block_el.previous_hash, [tx.__dict__ for tx in block_el.transactions], block_el.proof, block_el.timestamp) for block_el in self.__chain]]
100+
savable_transactions = [tx.__dict__ for tx in self.__open_transactions]
101+
102+
with open('blockchain.txt', mode='w') as f:
103+
f.write(json.dumps(savable_chain))
104+
f.write('\n')
105+
f.write(json.dumps(savable_transactions))
106+
107+
def get_balance(self, participant):
108+
# There are several cases on this exercise where the variable access through private
109+
# calling istead the property is recommended because of this example
110+
# self.chain # you get access to a copy of the chain, only recommended for instances
111+
# self.__chain # you get access to the chain created, recommended for internal class usage in other methods
112+
# The reason of this differnce is to avoid using copies of the data inside the class
113+
# that could not be on sync with the latest data
114+
# But once you are accessing through an instance, is required to access though the property (getter) and not the private attribute
115+
sent_transactions = [[tx.amount for tx in block.transactions if tx.sender == participant] for block in self.__chain]
116+
recieved_transactions = [[tx.amount for tx in block.transactions if tx.recipient == participant] for block in self.__chain]
117+
open_sent_transactions = [tx.amount for tx in self.__open_transactions if tx.sender == participant]
118+
119+
sent_transactions.append(open_sent_transactions)
120+
sent_amounts = functools.reduce(lambda tx_sum, tx_amt: tx_sum + sum(tx_amt), sent_transactions, 0)
121+
recieved_amounts = functools.reduce(lambda tx_sum, tx_amt: tx_sum + sum(tx_amt), recieved_transactions, 0)
122+
123+
return recieved_amounts - sent_amounts
124+
125+
def take_last_blockchain_value(self):
126+
if len(self.__chain) < 1:
127+
return None
128+
129+
return self.__chain[-1]
130+
131+
def add_transaction(self, sender, recipient, amount=1):
132+
"""
133+
Add a new transaction to the list of open transactions (which will be added to the next mined block)
134+
135+
Arguments:
136+
:sender: The sender of the coins.
137+
:recipient: The recipient of the coins.
138+
:amount: The amount of the transaction.
139+
"""
140+
new_transaction = Transaction(sender, recipient, amount)
141+
142+
# Using class and static methods, those can be called without need to instanciate it
143+
if Verification.verify_transaction(new_transaction, self.get_balance):
144+
self.__open_transactions.append(new_transaction)
145+
self.save_data()
146+
else:
147+
print('Transaction failed! Not enough balance!')
148+
add__line()
149+
150+
def mine_block(self):
151+
last_block = self.__chain[-1]
152+
hashed_block = hash_block(last_block)
153+
proof_of_work_value = self.proof_of_work()
154+
155+
reward_transaction = Transaction('MINING', self.hosting_id, MINING_REWARD)
156+
157+
transactions = self.__open_transactions.copy()
158+
transactions.append(reward_transaction)
159+
new_block = Block(
160+
len(self.__chain),
161+
hashed_block,
162+
transactions,
163+
proof_of_work_value
164+
)
165+
self.__chain.append(new_block)
166+
self.__open_transactions = []
167+
self.save_data()
168+
print('Block added!')
169+
add__line()
170+
return True
171+
172+
def proof_of_work(self):
173+
last_block = self.__chain[-1]
174+
hash_last_block = hash_block(last_block)
175+
proof = 0
176+
# Using class and static methods, those can be called without need to instanciate it
177+
while not Verification.valid_proof(self.__open_transactions, hash_last_block, proof):
178+
proof += 1
179+
return proof
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
class Printable:
2+
def __repr__(self):
3+
return str(self.__dict__)
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
from collections import OrderedDict
2+
# CLASS IMPORTS
3+
# Have in mind that in order to correctly import a class located in a folder
4+
# You have to call first the folder (or folders in order) and lastly, the file
5+
# In this case, I make the import of the Class at the end to avoid verbosity
6+
from classes.printable import Printable
7+
8+
# Inject Printable class to include a repetable function to reuse that logic across other classes
9+
class Transaction(Printable):
10+
def __init__(self, sender, recipient, amount):
11+
self.sender = sender
12+
self.recipient = recipient
13+
self.amount = amount
14+
15+
def to_ordered_dict(self):
16+
return OrderedDict([
17+
('sender'), self.sender,
18+
('recipient'), self.recipient,
19+
('amount'), self.amount])
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# OTHER IMPORTS
2+
from utils import hash_block, hash_string_256
3+
4+
# On this particular class, we will implement a different type of class itself
5+
# This type of class will use static and class methods
6+
# We will do this change to avoid instance creations on the program
7+
# It only need to class methods and never access its attributes
8+
# None of this cases need to access to class attributes
9+
# therefore there is no [__init__] method nor self reference (because does not access the class attributes, again)
10+
# The way to implement this type of methods in the class is through a decorator (starts with an @) that indicates the type of method
11+
class Verification:
12+
# classmethod can access inside class references (such as other methods)
13+
# It has a reference to class context (called [cls] as convention, but you can rename it)
14+
@classmethod
15+
def verify_chain(cls, blockchain):
16+
""" The function helps to verify the integrity of the blockchain by checking if each block's previous hash matches the hash of the previous block. """
17+
for (index, block) in enumerate(blockchain):
18+
if index == 0:
19+
continue
20+
if block.previous_hash != hash_block(blockchain[index - 1]):
21+
return False
22+
if not cls.valid_proof(block.transactions[:-1], block.previous_hash, block.proof):
23+
print('Proof of work is invalid')
24+
return False
25+
return True
26+
27+
# staticmethod is an isolated method that only works with the provided data
28+
# It has NO reference to the class, so its arguments are referencing what the calling will require
29+
@staticmethod
30+
def verify_transaction(transaction, get_balance):
31+
sender_balance = get_balance(transaction.sender)
32+
33+
if sender_balance >= transaction.amount:
34+
return True
35+
return False
36+
37+
@classmethod
38+
def verify_transactions(self, open_transactions):
39+
""" The function verifies all open transactions to ensure they are valid. """
40+
return all([tx for tx in open_transactions if not self.verify_transaction(tx)])
41+
42+
@staticmethod
43+
def valid_proof(transactions, last_hash, proof):
44+
# A good way to avoid to include that much code as the OrderedDict creation is to assign that logic inside the component
45+
# Doing it so will make callable for each object in a one-lined for floop
46+
guess = (str([tx.to_ordered_dict() for tx in transactions]) + str(last_hash) + str(proof)).encode()
47+
guess_hash = hash_string_256(guess)
48+
49+
return guess_hash[0:2] == '00'
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
from utils import add__line
2+
from uuid import uuid4
3+
# CLASS IMPORTS
4+
# Have in mind that in order to correctly import a class located in a folder
5+
# You have to call first the folder (or folders in order) and lastly, the file
6+
# In this case, I make the import of the Class at the end to avoid verbosity
7+
from classes.blockchain import Blockchain
8+
from classes.verification import Verification
9+
10+
class Node:
11+
def __init__(self):
12+
# ANY UUID is not JSON parseable, so we have to stringify it
13+
self.id = str(uuid4())
14+
self.blockchain = Blockchain(self.id)
15+
print(self.blockchain.chain)
16+
17+
def generate_options_menu(self):
18+
add__line()
19+
print('Please choose an option:')
20+
print('1: Add a new transaction')
21+
print('2: Mine a new block')
22+
print('3: Output all blockchain blocks')
23+
print('h: Manipulate blockchain')
24+
print('q: Quit')
25+
add__line()
26+
27+
def get_user_input(self):
28+
user_input = input('Please enter your selection: ')
29+
add__line()
30+
return user_input
31+
32+
def get_transaction_value(self):
33+
""" Returns the input of the user (a new transaction amount and its recipient) as a tuple """
34+
tx_recipient_input = input('Please enter the recipient of the transaction: ')
35+
tx_amount_input = float(input('Please enter your transaction input: '))
36+
add__line()
37+
38+
return tx_recipient_input, tx_amount_input
39+
40+
def return_all_blocks(self):
41+
print('---Outputting all blocks---')
42+
43+
for block in self.blockchain.chain:
44+
print(f'Outputting block: {block}')
45+
add__line()
46+
47+
def listen_for_input(self):
48+
waiting_for_input = True
49+
50+
while waiting_for_input:
51+
self.generate_options_menu()
52+
53+
user_choice = self.get_user_input()
54+
55+
if user_choice == '1':
56+
tx_input_data = self.get_transaction_value()
57+
recipient, amount = tx_input_data
58+
self.blockchain.add_transaction(self.id, recipient, amount)
59+
elif user_choice == '2':
60+
self.blockchain.mine_block()
61+
elif user_choice == '3':
62+
self.return_all_blocks()
63+
elif user_choice == '4':
64+
# Using class and static methods, those can be called without need to instanciate it
65+
if Verification.verify_transactions(self.blockchain.get_open_transactions()):
66+
print('Trasnsaction VALID')
67+
else:
68+
print('Trasnsaction INVALID')
69+
elif user_choice == 'q':
70+
waiting_for_input = False
71+
else:
72+
print('Invalid input, please choose a valid option')
73+
# Using class and static methods, those can be called without need to instanciate it
74+
if not Verification.verify_chain(self.blockchain.chain):
75+
print('Invalid blockchain!')
76+
waiting_for_input = False
77+
print(f"Balance of {self.id}: {self.blockchain.get_balance(self.id)}")
78+
else:
79+
print('User left!')
80+
81+
add__line()
82+
print('Done!')
83+
84+
node = Node()
85+
node.listen_for_input()
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import hashlib
2+
import json
3+
4+
def hash_string_256(string):
5+
""" Hash a string using sha256 """
6+
return hashlib.sha256(string).hexdigest()
7+
8+
def hash_block(block):
9+
""" Hash a block using its strucutre as base """
10+
# To resolve json parsing (does not accept every type of data), the better solution is to create a dictonary copy of the block
11+
hashable_block = block.__dict__.copy()
12+
# Have in mind that the __dict__ convertion will convert ONLY the data in the Block object, but not the transactions inside the block
13+
hashable_block['transactions'] = [tx.to_ordered_dict() for tx in hashable_block['transactions']]
14+
encoded_block = json.dumps(hashable_block, sort_keys=True).encode()
15+
16+
return hash_string_256(encoded_block)
17+
18+
def add__line():
19+
print('------------------------------')

0 commit comments

Comments
 (0)