diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..96fab4f --- /dev/null +++ b/.gitignore @@ -0,0 +1,38 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# Dependencies +node_modules +.pnp +.pnp.js + +# Local env files +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# Testing +coverage + +# Turbo +.turbo + +# Vercel +.vercel + +# Build Outputs +.next/ +out/ +build +dist + + +# Debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Misc +.DS_Store +*.pem diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..e69de29 diff --git a/apps/central/package.json b/apps/central/package.json new file mode 100644 index 0000000..dd82d3c --- /dev/null +++ b/apps/central/package.json @@ -0,0 +1,20 @@ +{ + "name": "central", + "version": "1.0.0", + "description": "", + "main": "dist/index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "dev": "tsc && node dist/index.js" + }, + "author": "", + "dependencies": { + "@repo/types": "", + "@repo/typescript-config": "", + "bs58": "^6.0.0", + "ws": "^8.18.0" + }, + "devDependencies": { + "@types/ws": "^8.5.12" + } +} diff --git a/apps/central/src/index.ts b/apps/central/src/index.ts new file mode 100644 index 0000000..457e322 --- /dev/null +++ b/apps/central/src/index.ts @@ -0,0 +1,144 @@ +import { WebSocketServer, WebSocket } from "ws"; +import url from "url"; + +const wss = new WebSocketServer({ port: 8080 }); +let miners: { + ws: WebSocket; + minerAddress: number; +}[] = []; + +wss.on("connection", (ws: WebSocket, req) => { + //@ts-ignore + const minerAddress = url.parse(req.url, true).query["publicKey"] as string; + + console.log("Here", minerAddress); + + if (!minerAddress) { + console.error("Miner Address Missing"); + ws.close(); + return; + } + + let miner = { + ws, + minerAddress: Date.now(), + }; + miners.push(miner); + console.log("New miner connected."); + + // Handle incoming messages from miners + ws.on("message", (message: string) => { + handleIncomingMessage(message, miner); + }); + + // Handle miner disconnect + ws.on("close", () => { + miners = miners.filter((miner) => miner.ws !== ws); + console.log("Miner disconnected."); + }); +}); + +function handleIncomingMessage( + message: string, + miner: { + ws: WebSocket; + minerAddress: number; + }, +) { + const data = JSON.parse(message); + + switch (data.type) { + case "new_block": + // Broadcast the new block to all miners + console.log("Received new block. Broadcasting to all miners."); + broadcast(message, miner); + break; + + case "transaction": + // Broadcast the transaction to all miners + console.log("Received new transaction. Broadcasting to all miners."); + broadcast(message, miner); + break; + + case "sync_chain": + console.log("Received sync chain request"); + const minersToRequest = miners.filter(m => m.minerAddress !== miner.minerAddress); + const requestSyncFromMiner = (index: number) => { + if (index >= minersToRequest.length) { + console.log("No miners available to fulfill sync request"); + return; + } + + const targetMiner = minersToRequest[index]; + if (!targetMiner) { + console.log("Miner not found. Trying next miner."); + requestSyncFromMiner(index + 1); + return; + } + targetMiner.ws.send(JSON.stringify({ + type: 'sync_chain', + blockIndex: data.blockIndex + })); + + const responseHandler = (response: string) => { + const parsedResponse = JSON.parse(response); + if (parsedResponse.type === 'sync_chain_response') { + if (parsedResponse.error) { + console.log(`Sync request failed for miner ${targetMiner.minerAddress}. Trying next miner.`); + requestSyncFromMiner(index + 1); + } else { + console.log(`Received sync response from miner ${targetMiner.minerAddress}. Forwarding to requester.`); + miner.ws.send(response); + targetMiner.ws.removeListener('message', responseHandler); + } + } + }; + + targetMiner.ws.on('message', responseHandler); + + // Set a timeout in case the miner doesn't respond + setTimeout(() => { + if (targetMiner.ws.listenerCount('message') > 0) { + console.log(`Sync request timed out for miner ${targetMiner.minerAddress}. Trying next miner.`); + targetMiner.ws.removeListener('message', responseHandler); + requestSyncFromMiner(index + 1); + } + }, 5000); // 5 second timeout + }; + + requestSyncFromMiner(0); + break; + + case "request_missing_blocks": + // Handle the request for missing blocks + handleMissingBlocksRequest(data.latestKnownHash, miner); + break; + + default: + console.log("Unknown message type:", data.type); + } +} + +console.log("Central WebSocket server running on port 8080."); + +function broadcast( + message: string, + fromMiner: { + ws: WebSocket; + minerAddress: number; + }, +) { + miners.map((miner) => { + if (miner.minerAddress != fromMiner.minerAddress) { + miner.ws.send(message); + } + }); +} + +function handleMissingBlocksRequest( + latestKnownHash: string, + miner: { + ws: WebSocket; + minerAddress: number; + }, +) {} diff --git a/apps/central/tsconfig.json b/apps/central/tsconfig.json new file mode 100644 index 0000000..1738383 --- /dev/null +++ b/apps/central/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "@repo/typescript-config/base.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src" + }, + "include": ["src"], + "exclude": ["node_modules", "dist"] + } \ No newline at end of file diff --git a/apps/miner/package.json b/apps/miner/package.json new file mode 100644 index 0000000..8ac52ee --- /dev/null +++ b/apps/miner/package.json @@ -0,0 +1,17 @@ +{ + "name": "miner", + "version": "1.0.0", + "description": "", + "main": "dist/index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "dev": "tsc && node dist/index.js" + }, + "author": "", + "license": "ISC", + "dependencies": { + "@repo/types": "", + "@repo/typescript-config": "", + "bs58": "^6.0.0" + } +} diff --git a/apps/miner/src/Block.ts b/apps/miner/src/Block.ts new file mode 100644 index 0000000..dc92e53 --- /dev/null +++ b/apps/miner/src/Block.ts @@ -0,0 +1,47 @@ +import Transaction from "@repo/types/transaction" +import * as crypto from 'crypto'; + +class Block { + timestamp: number; + transactions: Transaction[]; + previousHash: string; + hash: string; + nonce: number; + blockIndex: number; + + constructor(transactions: Transaction[],previousHash: string,blockIndex: number, timestamp?: number,nonce?: number) { + this.timestamp = timestamp ? timestamp : Date.now(); + this.transactions = transactions; + this.previousHash = previousHash; + this.nonce = nonce ? nonce : 0; + this.hash = this.calculateHash(); + this.blockIndex = blockIndex; + } + + calculateHash(): string { + const data = this.previousHash + this.timestamp + JSON.stringify(this.transactions) + this.nonce; + return crypto.createHash('sha256').update(data).digest('hex'); + } + + mineBlock(difficulty: number): void { + while(this.hash.substring(0,difficulty) !== Array(difficulty + 1).join("0")) { + this.nonce++; + this.hash = this.calculateHash(); + } + console.log(`Block mined: ${this.hash}`); + console.log('') + } + + hasValidTransactions(): boolean { + for (const tx of this.transactions) { + if (!tx.isValid()) { + return false; + } + } + return true; + } + +} + + +export default Block; \ No newline at end of file diff --git a/apps/miner/src/BlockChain.ts b/apps/miner/src/BlockChain.ts new file mode 100644 index 0000000..3a29bb6 --- /dev/null +++ b/apps/miner/src/BlockChain.ts @@ -0,0 +1,283 @@ +import Transaction from "@repo/types/transaction" +import Block from "./Block"; +import WsHandler from "./WsHandler"; + +class BlockChain { + chain: Block[]; + difficulty: number; + pendingTransactions: Transaction[]; + miningReward: number; + + static instance: BlockChain; + + static getInstance(): BlockChain { + if (!BlockChain.instance) { + BlockChain.instance = new BlockChain(); + } + return BlockChain.instance; + } + + private constructor() { + this.chain = [this.createGenesisBlock()]; + this.difficulty = 3; // Number of leading zeros required in the hash + this.pendingTransactions = []; + this.miningReward = 50; // Reward for mining a block + } + + createGenesisBlock(): Block { + const initialBlock = new Block([],"0",0); + initialBlock.timestamp = 1609459200000; + initialBlock.hash = initialBlock.calculateHash(); + return initialBlock; + } + + addTransaction(transaction: Transaction) { + this.pendingTransactions.push(transaction); + } + + minePendingTransactions(minerAddress: string): void { + + this.pendingTransactions = this.pendingTransactions.filter((txn) => { + if(!txn.isValid()) { + console.log("Invalid Transactions", txn.signature); + return false; + } + return true; + }); + + if(this.pendingTransactions.length === 0) { + console.log("No Transactions to mine !!"); + return; + } + + const block = new Block(this.pendingTransactions, this.getLatestBlock().hash,this.chain.length); + block.mineBlock(this.difficulty); + + console.log('Block successfully mined !'); + this.chain.push(block); + + // Update the block to central server, further forwarding to other miners + WsHandler.getInstance().send(JSON.stringify({ + 'type': 'new_block', + block, + minerAddress + })); + + this.pendingTransactions = []; + + //TODO: Reward Transactions for miners + // const rewardTxn = new Transaction(null,minerAddress,this.miningReward); + + // this.pendingTransactions = [ + // rewardTxn + // ]; + + // // This transaction is just present on this miner, so should be send it to other miners too + + // WsHandler.getInstance().send(JSON.stringify({ + // type: 'transaction', + // transaction: rewardTxn, + // minerAddress + // })); + + } + + getLatestBlock(): Block { + return this.chain[this.chain.length - 1]!; + } + + receiveBlock(block: Block): void { + if (this.validateBlock(block)) { + if (block.previousHash === this.getLatestBlock().hash) { + // This block extends our current chain + this.addBlockToChain(block); + } else if (this.chain.length >= 2 && block.previousHash === this.chain[this.chain.length - 2]!.hash) { + // This block is competing with our latest block + this.handleCompetingBlock(block); + } else if(this.getLatestBlock().blockIndex < block.blockIndex) { + // This block is part of a longer chain + console.log("Received a block which is part of a longer chain"); + this.handleLongerChain(block); + } else if(this.getLatestBlock().blockIndex > block.blockIndex) { + // This block is part of a shorter chain} + console.log("Received a block which is part of a shorter chain !! Ignoring it"); + } + } else { + console.error("Invalid block Received %d", block.hash); + } + } + + handleCompetingBlock(block: Block) { + const currentBlock = this.getLatestBlock(); + if(block.timestamp < currentBlock.timestamp || + ((block.timestamp == currentBlock.timestamp) && (block.hash < currentBlock.hash)) + ) { + console.log('Updating latest block with competing block' + block.hash); + this.chain[this.chain.length - 1] = block; + + const exclusiveTransactionsInCurrentBlock = currentBlock.transactions.filter((transaction) => { + return !block.transactions.some((txn) => txn.calculateHash() === transaction.calculateHash()) + }) + + // Update the mempool with the exclusive transactions + this.pendingTransactions = [ + ...this.pendingTransactions, + ...exclusiveTransactionsInCurrentBlock + ]; + + this.updatePendingTransactions(block.transactions); + } + } + + handleLongerChain(block: Block) { + // Need to sync the blockchain and get the longer chain + const currentBlockIndex = this.getLatestBlock().blockIndex; + const newBlockIndex = block.blockIndex; + + if (newBlockIndex > currentBlockIndex) { + + const blocksToRequest = (block.blockIndex - this.getLatestBlock().blockIndex) * 2; + + // Request the longer chain from other miners\ + + + const deregisterHandler = WsHandler.getInstance().registerHandler('sync_chain_response', (data: string) => { + console.log("Sync Chain Response Received"); + const response = JSON.parse(data); + + const receivedChain = response.chain; + const syncedBlocks: Block[] = [];; + + for(const block of receivedChain) { + const txnArray: Transaction[] = []; + if(block.transactions) { + for(let i = 0; i < block.transactions.length; i++) { + if(block.transactions[i]) { + const txn = new Transaction(block.transactions[i].fromAddress,block.transactions[i].toAddress,block.transactions[i].amount,block.transactions[i].signature); + txnArray.push(txn); + } + } + } + const recvBlock = new Block(txnArray,block.previousHash,block.blockIndex,block.timestamp,block.nonce); + syncedBlocks.push(recvBlock); + } + + + // const syncedBlocks: Block[] = receivedChainBlocks.map((block: Record) => { + // const txnArray: Transaction[] = []; + // if(block.transactions) { + // for(let i = 0; i < block.transactions.length; i++) { + // if(block.transactions[i]) { + // const txn = new Transaction(block.transactions[i].fromAddress,block.transactions[i].toAddress,block.transactions[i].amount,block.transactions[i].signature); + // txnArray.push(txn); + // } + // } + // } + // const recvBlock = new Block(txnArray,block.previousHash,block.timestamp,block.nonce); + // return recvBlock; + // }); + + console.log("Synced Blocks",syncedBlocks); + + if(syncedBlocks.length == 0 ) { + console.error("Received an empty chain from other miner"); + return; + } + + let divergenceIndex = -1; + + for(let i = this.chain.length - 1; i >= 0; i--) { + if(syncedBlocks.find((blck) => blck.hash === this.chain[i]!.hash)) { + divergenceIndex = i; + break; + } + } + + if(divergenceIndex == -1) { + console.error("Received a chain which is not extending our current chain"); + // TODO: Request a Full sync for the chain + return; + } + + + // Remove the out of sync blocks from our chain + const removedOutOfSyncBlocks = this.chain.slice(divergenceIndex + 1); + + const removedTransactions = removedOutOfSyncBlocks.flatMap((blck) => blck.transactions); + + this.pendingTransactions = [ + ...this.pendingTransactions, + ...removedTransactions + ]; + + // all the already done transaction which are presenty in the above pending transactions will be + // removed when we finaly add the synced blocks to our chain + + const blocksToAdd = syncedBlocks.slice(syncedBlocks.indexOf(syncedBlocks.find(b => b.hash === this.getLatestBlock().hash)!) + 1); + + for(const block of blocksToAdd) { + if(this.validateBlock(block) && block.previousHash === this.getLatestBlock().hash) { + this.addBlockToChain(block); + this.updatePendingTransactions(block.transactions); + } else { + console.error("Invalid block received from other miner"); + break; + } + } + + console.log("Chain synced successfully"); + + // One time callback, hence deregister after complete + deregisterHandler(); + }); + + WsHandler.getInstance().send(JSON.stringify({ + type: 'sync_chain', + blockIndex: Math.max(0, block.blockIndex - blocksToRequest), + })); + + + + } else { + console.log("Received a block which is part of a shorter chain !! Ignoring it"); + } + } + + addBlockToChain(block: Block): void { + this.chain.push(block); + console.log(`Added new block to chain: ${block.hash}`); + + // Remove transactions in this block from pending transactions + this.updatePendingTransactions(block.transactions); + } + + + validateBlock(block: Block) { + // if(block.previousHash !== this.getLatestBlock().hash) { + // console.error("Invalid previous hash"); + // return false; + // } + if(!block.hasValidTransactions()) { + console.error("Invalid transactions in block"); + return false; + } + if(block.hash !== block.calculateHash()) { + console.error("Invalid hash"); + return false; + } + return true; + } + + addBlock(newBlock: Block) { + this.chain.push(newBlock); + } + + updatePendingTransactions(confirmedTransactions: Transaction[]) { + this.pendingTransactions = this.pendingTransactions.filter((transaction) => { + return !confirmedTransactions.some((txn) => txn.calculateHash() === transaction.calculateHash()); + }); + } + +} + +export default BlockChain; \ No newline at end of file diff --git a/apps/miner/src/WsHandler.ts b/apps/miner/src/WsHandler.ts new file mode 100644 index 0000000..e2fc2db --- /dev/null +++ b/apps/miner/src/WsHandler.ts @@ -0,0 +1,155 @@ +let bs58 = require('bs58') +import WebSocket from 'ws'; +import BlockChain from './BlockChain'; +import Transaction from '@repo/types/transaction'; +import Block from './Block'; +import {minerPublicKey} from './index' + +class WsHandler { + + private socket: WebSocket; + private registerdCallBacks: Mapvoid)[]>; + + // private static URL = "ws://localhost:8080" + "?publicKey=" + bs58.encode(Buffer.from(minerPublicKey.export({ type: 'pkcs1', format: 'pem' }).toString())); + + private static instance: WsHandler | null = null; + + static getInstance(): WsHandler { + if (!WsHandler.instance) { + let URL = "ws://localhost:8080" + "?publicKey=" + bs58.default.encode(Buffer.from(minerPublicKey.export({ type: 'pkcs1', format: 'pem' }).toString())); + WsHandler.instance = new WsHandler(URL); + } + return WsHandler.instance; + } + + private constructor(url: string) { + this.socket = new WebSocket(url); + + this.registerdCallBacks = new Map(); + + this.socket.on('open', () => { + console.log('Connected to websocket server'); + }); + + this.socket.on('message', (data) => { + console.log('Received message:', data); + // Handle the received message here + this.handleIncomingMessage(data.toString()); + }); + + this.socket.on('close', () => { + console.log('Disconnected from websocket server'); + }); + + this.socket.on('error', (error) => { + console.error('Websocket error:', error); + }); + } + + // Add your custom handlers here + + private handleIncomingMessage = (message: string) => { + + const data = JSON.parse(message); + + switch (data.type) { + case 'new_block': + // Broadcast the new block to all miners + const block = data.block; + console.log('Received new block'); + const txnArray: Transaction[] = []; + if(block.transactions) { + for(let i = 0; i < block.transactions.length; i++) { + if(block.transactions[i]) { + const txn = new Transaction(block.transactions[i].fromAddress,block.transactions[i].toAddress,block.transactions[i].amount,block.transactions[i].signature); + txnArray.push(txn); + } + } + } + const recvBlock = new Block(txnArray,block.previousHash,block.blockIndex,block.timestamp,block.nonce); + BlockChain.getInstance().receiveBlock(recvBlock); + break; + + case 'transaction': + // Broadcast the transaction to all miners + console.log('Received new transaction.'); + const transaction = data.transaction; + const txn = new Transaction(transaction.fromAddress,transaction.toAddress,transaction.amount,transaction.signature); + console.log(transaction); + if(!txn.isValid()) { + console.error('Invalid Transaction !! Rejected ❌'); + } else { + console.log('Transaction Queued ⏱️'); + BlockChain.getInstance().addTransaction(txn); + } + break; + + case 'latest_block_hash': + console.log("Requested the latest block hash"); + const blockHash = BlockChain.getInstance().getLatestBlock().hash; + this.socket.send(JSON.stringify({ + blockHash + })); + break; + + case 'sync_chain': + console.log("Sync Chain Request Received"); + try { + const blockIndex = data.blockIndex; + const requestedChain = BlockChain.getInstance().chain.slice(blockIndex); + this.socket.send(JSON.stringify({ + type: 'sync_chain_response', + chain: requestedChain + })); + } catch (error) { + console.log("Error in Sync Chain Request Received"); + this.socket.send(JSON.stringify({ + type: 'sync_chain_response', + error: "Invalid Block Index" + })); + } + break; + + case 'sync_chain_response': + console.log("Synced Chain Received",this.registerdCallBacks.get('sync_chain_response')); + this.registerdCallBacks.get('sync_chain_response')?.map(cb => cb(message)); + break; + + + case 'request_missing_blocks': + // Handle the request for missing blocks + break; + + default: + console.log('Unknown message type:', data.type); + } + } + + registerHandler(event: string, callBack: ((data: string) => void)) { + let callBacks = this.registerdCallBacks.get(event); + if(!callBacks) { + callBacks = []; + } + callBacks.push(callBack); + this.registerdCallBacks.set(event,callBacks); + return () => { + let callBacks = this.registerdCallBacks.get(event); + if(!callBacks) { + callBacks = []; + } + this.registerdCallBacks.set(event,callBacks.filter(cb => cb !== callBack)); + } + } + + send(message: string) { + this.socket.send(message); + } + + close() { + this.socket.close(); + } +} + + + +export default WsHandler; \ No newline at end of file diff --git a/apps/miner/src/index.ts b/apps/miner/src/index.ts new file mode 100644 index 0000000..0acbfe2 --- /dev/null +++ b/apps/miner/src/index.ts @@ -0,0 +1,36 @@ +import Blockchain from './BlockChain'; +import Transaction from '@repo/types/transaction'; +import * as crypto from 'crypto'; +import WsHandler from './WsHandler'; + +// Generate key pairs for Miner +export const { publicKey: minerPublicKey, privateKey: minerPrivateKey } = crypto.generateKeyPairSync('rsa', { + modulusLength: 2048, +}); + +// Create a new blockchain instance +const myBlockchain = Blockchain.getInstance(); + +console.log(JSON.stringify(myBlockchain, null, 2)); + +// Create a new WebSocket instance +const webSocketHandler = WsHandler.getInstance(); + +if(!webSocketHandler) { + throw new Error("Can't connect to central server"); +} + +setInterval(() => { + // Mine pending transactions + myBlockchain.minePendingTransactions(minerPublicKey.export({ type: 'pkcs1', format: 'pem' }).toString()); +},1000); + + +setInterval(() => { + console.log('\nCurrent Blockchain Status:'); + console.log(getBlockchainStatus()); +}, 60000); // Log every 60 seconds + +function getBlockchainStatus() { + return JSON.stringify(myBlockchain, null, 2); +} \ No newline at end of file diff --git a/apps/miner/src/sendDemoTransactions.ts b/apps/miner/src/sendDemoTransactions.ts new file mode 100644 index 0000000..1050da8 --- /dev/null +++ b/apps/miner/src/sendDemoTransactions.ts @@ -0,0 +1,34 @@ +import WebSocket from 'ws'; +import Transaction from '@repo/types/transaction'; +import * as crypto from 'crypto'; + + +// Create a WebSocket connection to the server +const ws = new WebSocket('ws://localhost:8080?publicKey="jasn'); + +// Event handler for when the connection is established +ws.on('open', () => { + + const senderKeyPair = crypto.generateKeyPairSync('rsa', { + modulusLength: 2048, + }); + + const receiverKeyPair = crypto.generateKeyPairSync('rsa',{ + modulusLength: 2048, + }) + + // Create a demo transaction object + const transaction = new Transaction( + senderKeyPair.publicKey.export({ type: 'pkcs1', format: 'pem' }).toString(), + receiverKeyPair.publicKey.export({ type: 'pkcs1', format: 'pem' }).toString(), + 10 + ); + + transaction.signTransaction(senderKeyPair.privateKey); + + // Send the transaction to the server + ws.send(JSON.stringify({ + type: 'transaction', + transaction + })); +}); diff --git a/apps/miner/tsconfig.json b/apps/miner/tsconfig.json new file mode 100644 index 0000000..68384b4 --- /dev/null +++ b/apps/miner/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "@repo/typescript-config/base.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src" + }, + "include": ["src"], + "exclude": ["node_modules", "dist"] +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..2a200c2 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,326 @@ +{ + "name": "bitcoin-server", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "bitcoin-server", + "workspaces": [ + "apps/*", + "packages/*" + ], + "devDependencies": { + "prettier": "^3.2.5", + "turbo": "^2.0.14", + "typescript": "^5.4.5" + }, + "engines": { + "node": ">=18" + } + }, + "apps/central": { + "version": "1.0.0", + "dependencies": { + "@repo/types": "", + "@repo/typescript-config": "", + "bs58": "^6.0.0", + "ws": "^8.18.0" + }, + "devDependencies": { + "@types/ws": "^8.5.12" + } + }, + "apps/docs": { + "version": "0.1.0", + "extraneous": true, + "dependencies": { + "@repo/ui": "*", + "next": "15.0.0-rc.0", + "react": "19.0.0-rc-f994737d14-20240522", + "react-dom": "19.0.0-rc-f994737d14-20240522" + }, + "devDependencies": { + "@repo/eslint-config": "*", + "@repo/typescript-config": "*", + "@types/node": "^20", + "@types/react": "^18", + "@types/react-dom": "^18", + "eslint": "^8", + "eslint-config-next": "15.0.0-rc.0", + "typescript": "^5" + } + }, + "apps/miner": { + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@repo/types": "", + "@repo/typescript-config": "", + "bs58": "^6.0.0" + } + }, + "apps/web": { + "version": "0.1.0", + "extraneous": true, + "dependencies": { + "@repo/ui": "*", + "next": "15.0.0-rc.0", + "react": "19.0.0-rc-f994737d14-20240522", + "react-dom": "19.0.0-rc-f994737d14-20240522" + }, + "devDependencies": { + "@repo/eslint-config": "*", + "@repo/typescript-config": "*", + "@types/node": "^20", + "@types/react": "^18", + "@types/react-dom": "^18", + "eslint": "^8", + "eslint-config-next": "15.0.0-rc.0", + "typescript": "^5" + } + }, + "node_modules/@repo/types": { + "resolved": "packages/types", + "link": true + }, + "node_modules/@repo/typescript-config": { + "resolved": "packages/typescript-config", + "link": true + }, + "node_modules/@types/node": { + "version": "22.4.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.4.0.tgz", + "integrity": "sha512-49AbMDwYUz7EXxKU/r7mXOsxwFr4BYbvB7tWYxVuLdb2ibd30ijjXINSMAHiEEZk5PCRBmW1gUeisn2VMKt3cQ==", + "dev": true, + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/@types/ws": { + "version": "8.5.12", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.12.tgz", + "integrity": "sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/base-x": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-5.0.0.tgz", + "integrity": "sha512-sMW3VGSX1QWVFA6l8U62MLKz29rRfpTlYdCqLdpLo1/Yd4zZwSbnUaDfciIAowAqvq7YFnWq9hrhdg1KYgc1lQ==" + }, + "node_modules/bs58": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-6.0.0.tgz", + "integrity": "sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==", + "dependencies": { + "base-x": "^5.0.0" + } + }, + "node_modules/central": { + "resolved": "apps/central", + "link": true + }, + "node_modules/miner": { + "resolved": "apps/miner", + "link": true + }, + "node_modules/prettier": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", + "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/turbo": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/turbo/-/turbo-2.0.14.tgz", + "integrity": "sha512-00JjdCMD/cpsjP0Izkjcm8Oaor5yUCfDwODtaLb+WyblyadkaDEisGhy3Dbd5az9n+5iLSPiUgf+WjPbns6MRg==", + "dev": true, + "bin": { + "turbo": "bin/turbo" + }, + "optionalDependencies": { + "turbo-darwin-64": "2.0.14", + "turbo-darwin-arm64": "2.0.14", + "turbo-linux-64": "2.0.14", + "turbo-linux-arm64": "2.0.14", + "turbo-windows-64": "2.0.14", + "turbo-windows-arm64": "2.0.14" + } + }, + "node_modules/turbo-darwin-64": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/turbo-darwin-64/-/turbo-darwin-64-2.0.14.tgz", + "integrity": "sha512-kwfDmjNwlNfvtrvT29+ZBg5n1Wvxl891bFHchMJyzMoR0HOE9N1NSNdSZb9wG3e7sYNIu4uDkNk+VBEqJW0HzQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/turbo-darwin-arm64": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/turbo-darwin-arm64/-/turbo-darwin-arm64-2.0.14.tgz", + "integrity": "sha512-m3LXYEshCx3wc4ZClM6gb01KYpFmtjQ9IBF3A7ofjb6ahux3xlYZJZ3uFCLAGHuvGLuJ3htfiPbwlDPTdknqqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/turbo-linux-64": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/turbo-linux-64/-/turbo-linux-64-2.0.14.tgz", + "integrity": "sha512-7vBzCPdoTtR92SNn2JMgj1FlMmyonGmpMaQdgAB1OVYtuQ6NVGoh7/lODfaILqXjpvmFSVbpBIDrKOT6EvcprQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/turbo-linux-arm64": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/turbo-linux-arm64/-/turbo-linux-arm64-2.0.14.tgz", + "integrity": "sha512-jwH+c0bfjpBf26K/tdEFatmnYyXwGROjbr6bZmNcL8R+IkGAc/cglL+OToqJnQZTgZvH7uDGbeSyUo7IsHyjuA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/turbo-windows-64": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/turbo-windows-64/-/turbo-windows-64-2.0.14.tgz", + "integrity": "sha512-w9/XwkHSzvLjmioo6cl3S1yRfI6swxsV1j1eJwtl66JM4/pn0H2rBa855R0n7hZnmI6H5ywLt/nLt6Ae8RTDmw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/turbo-windows-arm64": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/turbo-windows-arm64/-/turbo-windows-arm64-2.0.14.tgz", + "integrity": "sha512-XaQlyYk+Rf4xS5XWCo8XCMIpssgGGy8blzLfolN6YBp4baElIWMlkLZHDbGyiFmCbNf9I9gJI64XGRG+LVyyjA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/typescript": { + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", + "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.19.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.6.tgz", + "integrity": "sha512-e/vggGopEfTKSvj4ihnOLTsqhrKRN3LeO6qSN/GxohhuRv8qH9bNQ4B8W7e/vFL+0XTnmHPB4/kegunZGA4Org==", + "dev": true + }, + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "packages/eslint-config": { + "name": "@repo/eslint-config", + "version": "0.0.0", + "extraneous": true, + "devDependencies": { + "@typescript-eslint/eslint-plugin": "^7.1.0", + "@typescript-eslint/parser": "^7.1.0", + "@vercel/style-guide": "^5.2.0", + "eslint-config-prettier": "^9.1.0", + "eslint-config-turbo": "^2.0.0", + "eslint-plugin-only-warn": "^1.1.0", + "typescript": "^5.3.3" + } + }, + "packages/types": { + "name": "@repo/types", + "version": "1.0.0", + "license": "ISC", + "devDependencies": { + "@repo/typescript-config": "*", + "typescript": "latest" + } + }, + "packages/typescript-config": { + "name": "@repo/typescript-config", + "version": "0.0.0", + "license": "MIT" + }, + "packages/ui": { + "name": "@repo/ui", + "version": "0.0.0", + "extraneous": true, + "dependencies": { + "react": "^18.2.0" + }, + "devDependencies": { + "@repo/eslint-config": "*", + "@repo/typescript-config": "*", + "@turbo/gen": "^1.12.4", + "@types/eslint": "^8.56.5", + "@types/node": "^20.11.24", + "@types/react": "^18.2.61", + "@types/react-dom": "^18.2.19", + "eslint": "^8.57.0", + "typescript": "^5.3.3" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..4d82c63 --- /dev/null +++ b/package.json @@ -0,0 +1,23 @@ +{ + "name": "bitcoin-server", + "private": true, + "scripts": { + "build": "turbo build", + "dev": "turbo dev", + "lint": "turbo lint", + "format": "prettier --write \"**/*.{ts,tsx,md}\"" + }, + "devDependencies": { + "prettier": "^3.2.5", + "turbo": "^2.0.14", + "typescript": "^5.4.5" + }, + "engines": { + "node": ">=18" + }, + "packageManager": "npm@10.5.0", + "workspaces": [ + "apps/*", + "packages/*" + ] +} diff --git a/packages/types/package.json b/packages/types/package.json new file mode 100644 index 0000000..7fcedea --- /dev/null +++ b/packages/types/package.json @@ -0,0 +1,22 @@ +{ + "name": "@repo/types", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "dev": "tsc --watch", + "build": "tsc" + }, + "exports": { + "./transaction": { + "types": "./src/Transaction.ts", + "default": "./dist/Transaction.js" + } + }, + "devDependencies": { + "@repo/typescript-config": "*", + "typescript": "latest" + }, + "author": "", + "license": "ISC" +} diff --git a/packages/types/src/Block.ts b/packages/types/src/Block.ts new file mode 100644 index 0000000..e69de29 diff --git a/packages/types/src/Blockchain.ts b/packages/types/src/Blockchain.ts new file mode 100644 index 0000000..e69de29 diff --git a/packages/types/src/Transaction.ts b/packages/types/src/Transaction.ts new file mode 100644 index 0000000..8ee7585 --- /dev/null +++ b/packages/types/src/Transaction.ts @@ -0,0 +1,45 @@ +import * as crypto from 'crypto'; + +class Transaction { + fromAddress: string | null; + toAddress: string; + amount: number; + signature: string | null; + + constructor(fromAddress: string | null, toAddress: string, amount: number, signature?: string) { + this.fromAddress = fromAddress; + this.toAddress = toAddress; + this.amount = amount; + this.signature = signature ?? null; + } + + calculateHash() { + return crypto.createHash('sha256').update(this.fromAddress + this.toAddress + this.amount).digest('hex'); + } + + signTransaction(signingKey: crypto.KeyObject) { + + if (this.fromAddress === null) return; // Don't sign mining rewards + + const hashTx = this.calculateHash(); + const sign = crypto.createSign('SHA256'); + sign.update(hashTx).end(); + this.signature = sign.sign(signingKey,'hex') + } + + isValid(): boolean { + console.log(this.fromAddress, "||" , this.fromAddress == null); + if (this.fromAddress == null) return true; // Mining rewards don't need to be signed + + if(!this.signature || this.signature.length == 0) { + return false; + } + const publicKey = crypto.createPublicKey(this.fromAddress); + const verify = crypto.createVerify('SHA256'); + verify.update(this.calculateHash()); + return verify.verify(publicKey,this.signature,'hex'); + } + +} + +export default Transaction; \ No newline at end of file diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts new file mode 100644 index 0000000..a925a5f --- /dev/null +++ b/packages/types/src/index.ts @@ -0,0 +1 @@ +export * from './Transaction' \ No newline at end of file diff --git a/packages/types/tsconfig.json b/packages/types/tsconfig.json new file mode 100644 index 0000000..68384b4 --- /dev/null +++ b/packages/types/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "@repo/typescript-config/base.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src" + }, + "include": ["src"], + "exclude": ["node_modules", "dist"] +} \ No newline at end of file diff --git a/packages/typescript-config/base.json b/packages/typescript-config/base.json new file mode 100644 index 0000000..0f80cfd --- /dev/null +++ b/packages/typescript-config/base.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "display": "Default", + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "esModuleInterop": true, + "incremental": false, + "isolatedModules": true, + "lib": ["es2022", "DOM", "DOM.Iterable"], + "module": "NodeNext", + "moduleDetection": "force", + "moduleResolution": "NodeNext", + "noUncheckedIndexedAccess": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "strict": true, + "target": "ES2022" + } +} diff --git a/packages/typescript-config/nextjs.json b/packages/typescript-config/nextjs.json new file mode 100644 index 0000000..44f4289 --- /dev/null +++ b/packages/typescript-config/nextjs.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "display": "Next.js", + "extends": "./base.json", + "compilerOptions": { + "plugins": [{ "name": "next" }], + "module": "ESNext", + "moduleResolution": "Bundler", + "allowJs": true, + "jsx": "preserve", + "noEmit": true + } +} diff --git a/packages/typescript-config/package.json b/packages/typescript-config/package.json new file mode 100644 index 0000000..27c0e60 --- /dev/null +++ b/packages/typescript-config/package.json @@ -0,0 +1,9 @@ +{ + "name": "@repo/typescript-config", + "version": "0.0.0", + "private": true, + "license": "MIT", + "publishConfig": { + "access": "public" + } +} diff --git a/packages/typescript-config/react-library.json b/packages/typescript-config/react-library.json new file mode 100644 index 0000000..44924d9 --- /dev/null +++ b/packages/typescript-config/react-library.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "display": "React Library", + "extends": "./base.json", + "compilerOptions": { + "jsx": "react-jsx" + } +} diff --git a/turbo.json b/turbo.json new file mode 100644 index 0000000..807e324 --- /dev/null +++ b/turbo.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://turbo.build/schema.json", + "ui": "tui", + "tasks": { + "build": { + "dependsOn": ["^build"], + "inputs": ["$TURBO_DEFAULT$", ".env*"], + "outputs": [".next/**", "!.next/cache/**"] + }, + "lint": { + "dependsOn": ["^lint"] + }, + "dev": { + "cache": false, + "persistent": true + } + } +}