La tua prima Blockchain in TypeScript

14 apr 2025Tobia Bartolomei
Ciao dev! 👋
In questo articolo ti porto passo dopo passo nella creazione di una blockchain funzionante in TypeScript.
Vedremo come costruire blocchi, effettuare transazioni, simulare un wallet e validare l’intera chain. È un esercizio fantastico per capire come funziona davvero una blockchain.
Requisiti di base
Per iniziare, assicurati di avere:
- •Node.js
- •TypeScript
- •Un minimo di conoscenza su classi, array e oggetti in TypeScript
Struttura del progetto
Creiamo una struttura semplice:
1/blockchain
2 ├── lib/
3 │ ├── blockchain/
4 │ │ └── main.ts
5 │ ├── wallet.ts
6 │ ├── data.ts
7 └── types.ts
Nel file types.ts definiamo la struttura di una transazione:
1export interface Transaction {
2 senderAddress: string;
3 receiverAddress: string;
4 amount: number;
5 note?: string;
6 timestamp: string;
7 fee: number;
8}
Nel file data.ts definiamo i nostri utenti fittizi:
1export const user1 = {
2 address: "user1-address",
3 balance: 100
4};
5
6export const user2 = {
7 address: "user2-address",
8 balance: 100
9};
La classe Wallet
Nel file wallet.ts andiamo a creare una classe per i portafogli dei nostri utenti, in modo da gestire bilanci e transazioni più comodamente:
1import { Transaction } from './types';
2
3interface Balance {
4 address: string;
5 balance: number;
6}
7
8export class Wallet {
9 balances: Balance[];
10
11 constructor(initialBalances: Balance[]) {
12 this.balances = initialBalances;
13 }
14
15 getBalance(address: string): number {
16 const user = this.balances.find(b => b.address === address);
17 return user ? user.balance : 0;
18 }
19
20 hasEnoughBalance(address: string, amount: number): boolean {
21 return this.getBalance(address) >= amount;
22 }
23
24 processTransaction(tx: Transaction): boolean {
25 const sender = this.balances.find(b => b.address === tx.senderAddress);
26 const receiver = this.balances.find(b => b.address === tx.receiverAddress);
27
28 if (!sender || sender.balance < tx.amount + tx.fee) return false;
29
30 sender.balance -= tx.amount + tx.fee;
31
32 if (receiver) {
33 receiver.balance += tx.amount;
34 } else {
35 this.balances.push({ address: tx.receiverAddress, balance: tx.amount });
36 }
37
38 return true;
39 }
40
41 checkDoubleSpending(pending: Transaction[]): boolean {
42 const tempBalances = [...this.balances.map(b => ({ ...b }))];
43 for (const tx of pending) {
44 const sender = tempBalances.find(b => b.address === tx.senderAddress);
45 if (!sender || sender.balance < tx.amount + tx.fee) return true;
46 sender.balance -= tx.amount + tx.fee;
47 }
48 return false;
49 }
50}
main.ts: Blockchain e blocchi
Ora la parte centrale: creiamo blocchi e la blockchain. Qui mettiamo tutto insieme: transazioni, mining e validazione.
Ogni blocco rappresenta un gruppo di transazioni, e contiene:
- •Timestamp:
- •Lista di transazioni
- •Hash del blocco precedente
- •Hash (calcolato con SHA256)
- •Nonce (per il mining)
- •Fee totale delle transazioni
1import { SHA256 } from 'crypto-js';
2import { Transaction } from '../types';
3import { Wallet } from '../wallet';
4import { user1, user2 } from '../data';
5
6export class Block {
7 index: number;
8 timestamp: string;
9 transactions: Transaction[];
10 previousHash: string;
11 hash: string;
12 nonce: number;
13 fee: number;
14
15 constructor(timestamp: string, transactions: Transaction[]) {
16 if (transactions.length > 10) throw new Error("Max 10 transazioni per blocco.");
17 this.index = 0;
18 this.timestamp = timestamp;
19 this.transactions = transactions;
20 this.previousHash = "0";
21 this.hash = this.calculateHash();
22 this.nonce = 0;
23 this.fee = 0;
24 }
25
26 calculateHash() {
27 return SHA256(
28 this.index +
29 this.previousHash +
30 this.timestamp +
31 JSON.stringify(this.transactions) +
32 this.nonce
33 ).toString();
34 }
35
36 mineBlock(difficulty: number) {
37 const target = Array(difficulty + 1).join("0");
38 while (this.hash.substring(0, difficulty) !== target) {
39 this.nonce++;
40 this.hash = this.calculateHash();
41 }
42 }
43}
La classe Blockchain
Questa classe tiene insieme tutta la chain, le transazioni in sospeso e un portafoglio fittizio:
1export class Blockchain {
2 chain: Block[];
3 pendingTransactions: Transaction[] = [];
4 wallet: Wallet;
5
6 constructor(existingChain?: Block[], existingWallet?: Wallet) {
7 this.chain = existingChain || [this.createGenesis()];
8 this.wallet = existingWallet || new Wallet([
9 { address: user1.address, balance: user1.balance },
10 { address: user2.address, balance: user2.balance }
11 ]);
12 }
13
14 createGenesis() {
15 return new Block(new Date('2025-01-01').toISOString(), [{
16 senderAddress: 'chain',
17 receiverAddress: 'chain',
18 amount: 0,
19 note: 'Genesis Block',
20 timestamp: new Date('2025-01-01').toISOString(),
21 fee: 0
22 }]);
23 }
24
25 latestBlock() {
26 return this.chain[this.chain.length - 1];
27 }
28
29 addBlock(newBlock: Block) {
30 newBlock.index = this.latestBlock().index + 1;
31 newBlock.previousHash = this.latestBlock().hash;
32 newBlock.mineBlock(4);
33 this.chain.push(newBlock);
34 }
35
36 addTransaction(tx: Transaction) {
37 if (!this.wallet.hasEnoughBalance(tx.senderAddress, tx.amount + tx.fee)) {
38 throw new Error("Saldo insufficiente.");
39 }
40
41 if (this.wallet.checkDoubleSpending([...this.pendingTransactions, tx])) {
42 throw new Error("Double spending rilevato.");
43 }
44
45 if (!this.wallet.processTransaction(tx)) {
46 throw new Error("Errore durante il processamento.");
47 }
48
49 this.pendingTransactions.push(tx);
50
51 if (this.pendingTransactions.length === 10) {
52 const newBlock = new Block(new Date().toISOString(), [...this.pendingTransactions]);
53 this.addBlock(newBlock);
54 this.pendingTransactions = [];
55 }
56 }
57
58 minePendingTransactions() {
59 if (this.pendingTransactions.length > 0) {
60 const newBlock = new Block(new Date().toISOString(), [...this.pendingTransactions]);
61 this.addBlock(newBlock);
62 this.pendingTransactions = [];
63 return true;
64 }
65 return false;
66 }
67
68 getPendingTransactions(): Transaction[] {
69 return [...this.pendingTransactions];
70 }
71
72 checkValid(): boolean {
73 for (let i = 1; i < this.chain.length; i++) {
74 const current = this.chain[i];
75 const previous = this.chain[i - 1];
76
77 if (current.hash !== current.calculateHash()) return false;
78 if (current.previousHash !== previous.hash) return false;
79 if (current.transactions.length > 10 || current.transactions.length === 0) return false;
80 for (const tx of current.transactions) {
81 if (!tx.senderAddress || !tx.receiverAddress) return false;
82 }
83 }
84 return true;
85 }
86
87 calculateFee(amount: number): number {
88 const baseFee = 0.0010;
89 const amountFee = amount * 0.010;
90 const networkLoadFactor = Math.min(1 + (this.pendingTransactions.length / 10), 2);
91 return (baseFee + amountFee) * networkLoadFactor;
92 }
93
94 getBalance(address: string): number {
95 return this.wallet.getBalance(address);
96 }
97}
Risultato
Hai ora una blockchain funzionante in locale con:
- •Fee dinamiche
- •Wallet con bilanci
- •Mining proof-of-work
- •Transazioni sicure (no double spending)
Conclusione
Questo è solo l’inizio. Puoi estendere il progetto con firme digitali, una rete distribuita, meccanismi di consenso, e magari un’interfaccia frontend per esplorare i blocchi!
Ti interessa vedere anche una versione online o una demo? Visita la mia demo pubblica a questo link!