The Blockchain Class

The idea

Here is the complete implementation of the Blockchain class. You can copy and paste the code into your main.ts file.

We can start by creating the class and the constructor. Here we have the chain, the pending transactions and the wallet.

If the chain is empty, we create the genesis block; otherwise, we use the last block of the chain as the previous hash of the new block. We check if the chain is valid and if the wallet has enough balance to send the transactions.

📄main.ts
1import { Wallet } from './wallet';
2
3const user1 = {
4	address: '0x1234567890abcdef',
5	balance: 100
6}
7
8const user2 = {
9	address: '0xabcdef1234567890',
10	balance: 200
11}
12
13export class Blockchain{
14  // the Blockchain have the following properties:
15  // chain -> array of Blocks
16  // pendingTransactions -> array of Transactions to be added to the next block
17  // wallet -> Wallet instance (with balances & keys of the users)
18  chain: Block[]
19  pendingTransactions: Transaction[] = [];
20  wallet: Wallet;
21
22  constructor(existingChain?: Block[], existingWallet?: Wallet) {
23    // if the chain is already existing, use it
24    if (existingChain) {
25      this.chain = existingChain;
26    } else {
27      this.chain = [this.createGenesis()];
28    }
29    // initialize the pending transactions
30    this.pendingTransactions = [];
31    // if the wallet is not existing, create a new one
32    if (existingWallet) {
33      this.wallet = existingWallet;
34    } else {
35      // create a new wallet with the initial balances
36      this.wallet = new Wallet([
37        { address: user1.address, balance: user1.balance },
38        { address: user2.address, balance: user2.balance }
39      ]);
40    }
41  }
42}

The latestBlock() & addBlock() methods

The latestBlock() method is used to get the latest block of the chain. And the addBlock() method is used to add a new block to the chain.

📄main.ts
1// get the latest block of the chain
2latestBlock() {
3	return this.chain[this.chain.length - 1]
4}
5
6// add a new block to the chain
7addBlock(newBlock: Block) {
8	newBlock.index = this.latestBlock().index + 1; // set the index
9	newBlock.previousHash = this.latestBlock().hash; // set the previousHash
10	
11	// mine the block with difficulty 4
12	newBlock.mineBlock(4);
13	
14	this.chain.push(newBlock); // add the block to the chain
15}

The addTransaction() method

The addTransaction() method is used to add a new transaction to the pending transactions. First we check if the sender has enough balance to send the transaction. Then we check if the transaction is valid and if it is not a double spending. Finally, we process the transaction and add it to the pending transactions. If the pending transactions are 10, we create a new block and add it to the chain.

📄main.ts
1// add a transaction to the pending transactions
2addTransaction(transaction: Transaction) {
3	// check if the sender has enough balance
4	if (!this.wallet.hasEnoughBalance(transaction.senderAddress, transaction.amount + transaction.fee)) {
5		throw new Error("Insufficient balance");
6	}
7
8	// check for double spending in pending transactions
9	if (this.wallet.checkDoubleSpending([...this.pendingTransactions, transaction])) {
10		throw new Error("Double spending detected");
11	}
12
13	// process the transaction (update balances)
14	if (!this.wallet.processTransaction(transaction)) {
15		throw new Error("Transaction processing failed");
16	}
17
18	// add the transaction to the pending transactions
19	this.pendingTransactions.push(transaction);
20
21	if (this.pendingTransactions.length === 10) {
22		const newBlock = new Block(new Date().toISOString(), [...this.pendingTransactions]);
23		this.addBlock(newBlock);
24		this.pendingTransactions = []; // empty the pending transactions
25	}
26}

The minePendingTransactions() & getPendingTransactions() methods

The minePendingTransactions() method is used to mine the pending transactions. If there are pending transactions, we create a new block and add it to the chain. And the getPendingTransactions() method is used to get the pending transactions.

📄main.ts
1// manually mine pending transactions
2minePendingTransactions() {
3	if (this.pendingTransactions.length > 0) {
4		const newBlock = new Block(new Date().toISOString(), [...this.pendingTransactions]);
5		this.addBlock(newBlock);
6		// don't reset the wallet, only the pending transactions
7		this.pendingTransactions = [];
8		return true;
9	}
10	return false;
11}
12
13// get pending transactions
14getPendingTransactions(): Transaction[] {
15	// return the pending transactions (in format of the Transaction type)
16	return [...this.pendingTransactions];
17}

The checkValid() method

And the checkValid() method is used to check if the chain is valid. We check if the hash is valid, if the previous hash is valid, if the number of transactions is valid, if the block has no transactions and if the addresses are valid. Finally, we return true if the chain is valid.

📄main.ts
1// check if the chain is valid
2checkValid() {
3	for (let i = 1; i < this.chain.length; i++) {
4		const currentBlock = this.chain[i];
5		const previousBlock = this.chain[i - 1];
6
7		// check if the hash is valid
8		if (currentBlock.hash !== currentBlock.calculateHash()) {
9			console.error(`❌ Invalid hash at block ${currentBlock.index}`);
10			return false;
11		}
12
13		// check if the previous hash is valid
14		if (currentBlock.previousHash !== previousBlock.hash) {
15			console.error(`❌ Invalid previous hash at block ${currentBlock.index}`);
16			return false;
17		}
18
19		// check if the number of transactions is valid
20		if (currentBlock.transactions.length > 10) {
21			console.error(`❌ Block ${currentBlock.index} has more than 10 transactions`);
22			return false;
23		}
24
25		// check if the block has no transactions
26		if (currentBlock.transactions.length === 0) {
27			console.error(`❌ Block ${currentBlock.index} has no transactions`);
28			return false;
29		}
30
31		// check if the addresses are valid
32		for (const tx of currentBlock.transactions) {
33			if (!tx.senderAddress || !tx.receiverAddress) {
34				console.error(`❌ Invalid addresses in a transaction at block ${currentBlock.index}`);
35			}
36		}
37	}
38
39	return true;
40}