├── .gitignore ├── LICENSE ├── README.md ├── app.js ├── config └── nodes.json ├── jest.config.js ├── multiple.js ├── package-lock.json ├── package.json └── src ├── controllers ├── __tests__ │ └── blockchain.integration.test.js └── blockchain.js └── models ├── __tests__ ├── block.test.js ├── blockchain.test.js ├── nodes.test.js ├── transaction.test.js └── transactions.test.js ├── block.js ├── blockchain.js ├── nodes.js ├── transaction.js └── transactions.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | nodes.prod.json 3 | storage -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 amarukensei 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Simple Blockchain in Node.js 2 | 3 | This is a very simple blockchain implementation in Node.js. 4 | 5 | It is just a proof of concept so as to understand how a blockchain may be created, including decentralized and distributed ledger concept. 6 | 7 | 8 | ## Getting Started 9 | 10 | ### Prerequisites 11 | 12 | You need node.js and npm package manager to be installed. I developed this code using node v12.10.0, but should work with previous versions. 13 | 14 | ### Installation 15 | 16 | Go into the code root and install all the packages. 17 | 18 | ```sh 19 | $ npm install 20 | ``` 21 | 22 | ### Node list 23 | 24 | Not mandatory, but it is recommended to have a list of nodes for having the distributed ledger mode. 25 | 26 | There is already a default list of nodes set in `config/nodes.json`, but you may change it as you wish. 27 | 28 | ``` 29 | [ 30 | "http://0.0.0.0:4000", 31 | "http://0.0.0.0:4001", 32 | "http://0.0.0.0:4002" 33 | ] 34 | ``` 35 | 36 | ### Running one instance 37 | 38 | Use this for starting one single instance (a node). 39 | 40 | ```sh 41 | $ node app 42 | ``` 43 | 44 | 45 | ### Running multiple nodes 46 | 47 | Use this if you want to have multiple instances in the same machine. Nodes defined in the config file above will be accessed and keep updated. 48 | 49 | This is a convinient way for playing around with distributed ledger quickly. 50 | 51 | ```sh 52 | $ node multiple 53 | ``` 54 | 55 | ## API 56 | 57 | POST requests need to have `Content-Type` as `application/json` 58 | 59 | ### GET /nodes 60 | 61 | Returns a list of registered nodes. 62 | 63 | ```sh 64 | $ curl http://0.0.0.0:4000/nodes 65 | ``` 66 | 67 | Response: 68 | 69 | ``` 70 | [ 71 | "http://0.0.0.0:4000", 72 | "http://0.0.0.0:4001", 73 | "http://0.0.0.0:4002" 74 | ] 75 | ``` 76 | 77 | ### POST /transaction 78 | 79 | Add a transaction to the blockchain transaction queue. This needs to be mined afterwards so as to be added to the blockchain. 80 | 81 | The body of the request should contain a json string with these keys: `from`, `to` and `amount`. 82 | 83 | ```sh 84 | $ curl --header "Content-Type: application/json" \ 85 | --request POST \ 86 | --data '{"from":"bd748a5a5479649cfd83132d3be99d0c1a2ebadc1e4c405e","to":"3be24b8dccf3c0a171c76b092e2a95f6e9d387eac6b647f1","amount": 1}' \ 87 | curl http://0.0.0.0:4000/transaction 88 | ``` 89 | 90 | I all went fine you should be getting a response like: 91 | 92 | ``` 93 | { 94 | "success": 1 95 | } 96 | ``` 97 | 98 | ### GET /transactions 99 | 100 | Returns all pending transactions waiting to be mined. 101 | 102 | ```sh 103 | $ curl http://0.0.0.0:4000/transactions 104 | ``` 105 | 106 | Response: 107 | 108 | ``` 109 | [ 110 | { 111 | "from": "bd748a5a5479649cfd83132d3be99d0c1a2ebadc1e4c405e", 112 | "to": "3be24b8dccf3c0a171c76b092e2a95f6e9d387eac6b647f1", 113 | "amount": 1, 114 | "timestamp": 1569590821 115 | } 116 | ] 117 | ``` 118 | 119 | ### GET /mine 120 | 121 | This will mine (process) all the pending transactions and add the result into a block and then to the blockchain itself. 122 | 123 | In this version, once the mining process is done, the blockchain data will be broadcasted instanly to other registered nodes (if any). 124 | 125 | This is a decentralized and distributed ledger, so data should remain the same everywhere. 126 | 127 | ```sh 128 | $ curl http://0.0.0.0:4000/mine 129 | ``` 130 | 131 | Returns the mined block with all its data and related transactions. 132 | 133 | ``` 134 | { 135 | "index": 1, 136 | "previousHash": "00002818703517bab21046d807a3fc0284b8a05979ce48baa40ed2eeeadd3b92", 137 | "hash": "000089aef2e4516c72ef4c29f9490471cf20b7fcc7819bb000dc2d8b27281268", 138 | "timestamp": 1569590961, 139 | "nonce": 279, 140 | "transactions": [ 141 | { 142 | "from": "bd748a5a5479649cfd83132d3be99d0c1a2ebadc1e4c405e", 143 | "to": "3be24b8dccf3c0a171c76b092e2a95f6e9d387eac6b647f1", 144 | "amount": 1, 145 | "timestamp": 1569590821 146 | } 147 | ] 148 | } 149 | ``` 150 | 151 | ### GET /blockchain 152 | 153 | Returns the whole data of the blockchain. (Note that for a small blockchain this is doable, but not for a big one.) 154 | 155 | 156 | ```sh 157 | $ curl http://0.0.0.0:4000/blockchain 158 | ``` 159 | 160 | Response: 161 | 162 | ``` 163 | [ 164 | { 165 | "index": 0, 166 | "previousHash": "0000000000000000", 167 | "hash": "00002818703517bab21046d807a3fc0284b8a05979ce48baa40ed2eeeadd3b92", 168 | "timestamp": 1568323235, 169 | "nonce": 4190, 170 | "transactions": [] 171 | }, 172 | { 173 | "index": 1, 174 | "previousHash": "00002818703517bab21046d807a3fc0284b8a05979ce48baa40ed2eeeadd3b92", 175 | "hash": "000089aef2e4516c72ef4c29f9490471cf20b7fcc7819bb000dc2d8b27281268", 176 | "timestamp": 1569590961, 177 | "nonce": 279, 178 | "transactions": [ 179 | { 180 | "from": "bd748a5a5479649cfd83132d3be99d0c1a2ebadc1e4c405e", 181 | "to": "3be24b8dccf3c0a171c76b092e2a95f6e9d387eac6b647f1", 182 | "amount": 1, 183 | "timestamp": 1569590821 184 | } 185 | ] 186 | } 187 | ] 188 | ``` 189 | 190 | Note the genesis block. That is the very first block created which gets added by default when the blockchains is initialized. 191 | 192 | 193 | ### GET /blockchain/1 194 | 195 | Returns a block specified by index id. 196 | 197 | ```sh 198 | $ http://0.0.0.0:4000/blockchain/1 199 | ``` 200 | 201 | Response: 202 | 203 | ``` 204 | { 205 | "index": 1, 206 | "previousHash": "00002818703517bab21046d807a3fc0284b8a05979ce48baa40ed2eeeadd3b92", 207 | "hash": "000089aef2e4516c72ef4c29f9490471cf20b7fcc7819bb000dc2d8b27281268", 208 | "timestamp": 1569590961, 209 | "nonce": 279, 210 | "transactions": [ 211 | { 212 | "from": "bd748a5a5479649cfd83132d3be99d0c1a2ebadc1e4c405e", 213 | "to": "3be24b8dccf3c0a171c76b092e2a95f6e9d387eac6b647f1", 214 | "amount": 1, 215 | "timestamp": 1569590821 216 | } 217 | ] 218 | } 219 | ``` 220 | 221 | ### GET /blockchain/last-index 222 | 223 | Returns the index of the last inseted block in the blockchain. 224 | 225 | ```sh 226 | $ http://0.0.0.0:4000/blockchain/last-index 227 | ``` 228 | 229 | Response: 230 | 231 | ``` 232 | 1 233 | ``` 234 | 235 | ## Author 236 | 237 | Bernardino Todolí López - [Taula Consulting](http://www.taula-consulting.com/en/) 238 | 239 | ## License 240 | 241 | This project is licensed under the MIT License. 242 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | 2 | const blockchainController = require('./src/controllers/blockchain'); 3 | const express = require('express'); 4 | const bodyParser = require('body-parser'); 5 | 6 | // Load env vars 7 | const url = process.env.URL || '0.0.0.0'; 8 | const port = process.env.PORT || 4000; 9 | 10 | // Init express 11 | let app = express(); 12 | app.use(bodyParser.json()); 13 | 14 | let listener = app.listen(port, url, function() { 15 | console.log('Server started at ' + listener.address().address + ':' + listener.address().port); 16 | }); 17 | 18 | // API 19 | let controller = new blockchainController(url, port); 20 | 21 | app.get('/resolve', controller.resolve.bind(controller)); 22 | app.get('/nodes', controller.getNodes.bind(controller)); 23 | app.post('/transaction', controller.postTransaction.bind(controller)); 24 | app.get('/transactions', controller.getTransactions.bind(controller)); 25 | app.get('/mine', controller.mine.bind(controller)); 26 | app.get('/blockchain/last-index', controller.getBlockLastIndex.bind(controller)); 27 | app.get('/blockchain/:idx', controller.getBlockByIndex.bind(controller)); 28 | app.get('/blockchain', controller.getBlockchain.bind(controller)); 29 | -------------------------------------------------------------------------------- /config/nodes.json: -------------------------------------------------------------------------------- 1 | [ 2 | "http://0.0.0.0:4000", 3 | "http://0.0.0.0:4001", 4 | "http://0.0.0.0:4002" 5 | ] -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: 'node', 3 | testMatch: ['**/__tests__/**/*.js?(x)', '**/?(*.)+(spec|test).js?(x)'], 4 | }; 5 | -------------------------------------------------------------------------------- /multiple.js: -------------------------------------------------------------------------------- 1 | const blockchainController = require('./src/controllers/blockchain'); 2 | const express = require('express'); 3 | const bodyParser = require('body-parser'); 4 | const nodes = require('./config/nodes.json'); 5 | 6 | // Create an instance per node in the list to mimic distributed and decentralized nodes 7 | nodes.forEach( node => { 8 | let idx = node.lastIndexOf('//'); 9 | let idx2 = node.lastIndexOf(':'); 10 | let url = node.substring(idx+2, idx2); 11 | let port = node.substring(idx2+1); 12 | 13 | // Init express 14 | let app = express(); 15 | app.use(bodyParser.json()); 16 | 17 | let listener = app.listen(port, url, function() { 18 | console.log('Server started at ' + listener.address().address + ':' + listener.address().port); 19 | }); 20 | 21 | // API 22 | let controller = new blockchainController(url, port); 23 | app.get('/resolve', controller.resolve.bind(controller)); 24 | app.get('/nodes', controller.getNodes.bind(controller)); 25 | app.post('/transaction', controller.postTransaction.bind(controller)); 26 | app.get('/transactions', controller.getTransactions.bind(controller)); 27 | app.get('/mine', controller.mine.bind(controller)); 28 | app.get('/blockchain/last-index', controller.getBlockLastIndex.bind(controller)); 29 | app.get('/blockchain/:idx', controller.getBlockByIndex.bind(controller)); 30 | app.get('/blockchain', controller.getBlockchain.bind(controller)); 31 | }); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Blockchain", 3 | "version": "1.0.0", 4 | "description": "Simple implementation of a blockchain", 5 | "main": "app.js", 6 | "scripts": { 7 | "start": "node app.js", 8 | "test": "jest" 9 | }, 10 | "author": "Bernardino Todolí López", 11 | "license": "MIT", 12 | "dependencies": { 13 | "body-parser": "^1.18.3", 14 | "express": "^4.16.3", 15 | "js-sha256": "^0.9.0", 16 | "node-fetch": "^3.1.1", 17 | "node-persist": "^3.0.5" 18 | }, 19 | "devDependencies": { 20 | "jest": "^29.7.0", 21 | "supertest": "^7.1.1" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/controllers/__tests__/blockchain.integration.test.js: -------------------------------------------------------------------------------- 1 | const request = require('supertest'); 2 | const app = require('../../../app'); // Assuming app.js exports the express app 3 | const storage = require('node-persist'); 4 | const fs = require('fs'); 5 | const path = require('path'); 6 | 7 | // --- Global Mocks --- 8 | // Mock node-persist 9 | let mockStorageData = {}; 10 | jest.mock('node-persist', () => ({ 11 | create: jest.fn().mockReturnThis(), 12 | init: jest.fn(async () => { 13 | // console.log('Mock node-persist init called'); 14 | // Optionally clear mockStorageData here if init implies a fresh start for a storage dir 15 | }), 16 | getItem: jest.fn(async (key) => { 17 | // console.log(`Mock getItem: ${key} -> ${JSON.stringify(mockStorageData[key])}`); 18 | return mockStorageData[key] !== undefined ? JSON.parse(JSON.stringify(mockStorageData[key])) : undefined; 19 | }), 20 | setItem: jest.fn(async (key, value) => { 21 | // console.log(`Mock setItem: ${key} -> ${JSON.stringify(value)}`); 22 | mockStorageData[key] = JSON.parse(JSON.stringify(value)); // Store a copy 23 | }), 24 | clear: jest.fn(async () => { 25 | // console.log('Mock clear called'); 26 | mockStorageData = {}; 27 | }), 28 | // Add any other methods used by the application 29 | })); 30 | 31 | // Mock nodes.json content 32 | const nodesFilePath = path.join(__dirname, '../../../src/config/nodes.json'); // Adjust path as needed 33 | 34 | describe('Blockchain API Integration Tests', () => { 35 | let originalNodesJsonContent; 36 | 37 | beforeAll(async () => { 38 | // Save original nodes.json if it exists, then create a mock one 39 | if (fs.existsSync(nodesFilePath)) { 40 | originalNodesJsonContent = fs.readFileSync(nodesFilePath, 'utf8'); 41 | } 42 | // Create a default mock nodes.json for tests 43 | fs.writeFileSync(nodesFilePath, JSON.stringify(['http://localhost:3001', 'http://localhost:3002'])); 44 | 45 | // Initialize the app (which in turn initializes blockchain, nodes etc.) 46 | // This ensures our mocks are in place before the app fully sets up. 47 | // Note: app.js might need to re-require its dependencies if they were cached before mocks applied. 48 | // If `app` is already configured at the top-level import, this is fine. 49 | // await app.ready(); // If your app has an explicit ready signal after async setup 50 | }); 51 | 52 | afterAll(async () => { 53 | // Restore original nodes.json 54 | if (originalNodesJsonContent) { 55 | fs.writeFileSync(nodesFilePath, originalNodesJsonContent); 56 | } else { 57 | fs.unlinkSync(nodesFilePath); // Remove if it didn't exist before 58 | } 59 | }); 60 | 61 | beforeEach(async () => { 62 | // Reset mock storage before each test to ensure test isolation for blockchain state 63 | mockStorageData = {}; 64 | // The blockchain instance within the app needs to be "reset". 65 | // This is the tricky part with integration tests. 66 | // If the blockchain is a singleton initialized once, we need a way to reset its state. 67 | // One way: re-initialize the relevant part of the app or the controller. 68 | // For now, clearing mockStorageData and re-initializing the blockchain 69 | // by directly calling its internal load/reset methods might be needed if the app 70 | // doesn't expose a reset mechanism. 71 | // The current setup relies on app.js re-initializing its blockchain on each test run 72 | // IF it's structured to do so (e.g. if server is restarted, or if app export is a factory). 73 | // Given app is imported once, its blockchain instance is likely a singleton. 74 | // We'll assume for now that clearing mockStorageData is enough for the blockchain 75 | // to re-create genesis block etc., as its constructor loads from storage. 76 | // We also need to reset the transactions list in the singleton Transactions instance in the app. 77 | // This might require a dedicated endpoint or a way to access and reset it. 78 | // For now, we'll test sequentially and manage state via API calls. 79 | 80 | // A simple way to reset transactions for now (assuming they are in memory or reset by mining) 81 | // This doesn't reset the blockchain itself, only pending transactions. 82 | const allTransactions = await request(app).get('/transactions'); 83 | if (allTransactions.body.length > 0) { 84 | // Mine them away to clear pending transactions 85 | await request(app).get('/mine'); 86 | } 87 | // Ensure blockchain is at a known state (e.g. only genesis block) 88 | // This is harder without an explicit reset. We will rely on mockStorageData clearing. 89 | // The app's blockchain instance will call `storage.getItem('blocks')` on startup (or first access) 90 | // If it's undefined (due to mockStorageData = {}), it *should* create a genesis block. 91 | }); 92 | 93 | describe('GET /nodes', () => { 94 | test('should return 200 and a list of nodes from mocked nodes.json', async () => { 95 | const response = await request(app).get('/nodes'); 96 | expect(response.status).toBe(200); 97 | expect(response.body).toEqual(['http://localhost:3001', 'http://localhost:3002']); 98 | }); 99 | }); 100 | 101 | describe('POST /transaction', () => { 102 | test('Success: should add a transaction and return success', async () => { 103 | const transactionData = { from: 'wallet1', to: 'wallet2', amount: 100 }; 104 | const response = await request(app) 105 | .post('/transaction') 106 | .send(transactionData); 107 | expect(response.status).toBe(200); // Assuming 200 for successful JSON response 108 | expect(response.body).toEqual({ success: 1 }); 109 | 110 | // Verify by getting transactions 111 | const transactionsResponse = await request(app).get('/transactions'); 112 | expect(transactionsResponse.body).toHaveLength(1); 113 | expect(transactionsResponse.body[0]).toMatchObject(transactionData); 114 | }); 115 | 116 | test('Failure (invalid data): should return 406 for missing "from"', async () => { 117 | const transactionData = { to: 'wallet2', amount: 100 }; // "from" is missing 118 | const response = await request(app) 119 | .post('/transaction') 120 | .send(transactionData); 121 | expect(response.status).toBe(406); 122 | expect(response.body).toHaveProperty('error'); 123 | expect(response.body.error).toBe('Transaction "from" is mandatory'); 124 | }); 125 | }); 126 | 127 | describe('GET /transactions', () => { 128 | test('should initially return an empty array', async () => { 129 | // Need to ensure transactions are clear for this test. 130 | // Assuming beforeEach clears them or previous tests leave them clear. 131 | // For robustness, explicitly clear here if a mechanism exists. 132 | // For now, relying on mining from previous tests or clean state. 133 | // Let's clear mockStorageData for transactions specifically 134 | mockStorageData['transactions'] = []; // Assuming transactions are also stored 135 | 136 | const response = await request(app).get('/transactions'); 137 | expect(response.status).toBe(200); 138 | expect(response.body).toEqual([]); 139 | }); 140 | 141 | test('should return transactions after they are added', async () => { 142 | const transactionData = { from: 'wA', to: 'wB', amount: 50 }; 143 | await request(app).post('/transaction').send(transactionData); 144 | 145 | const response = await request(app).get('/transactions'); 146 | expect(response.status).toBe(200); 147 | expect(response.body).toHaveLength(1); 148 | expect(response.body[0]).toMatchObject(transactionData); 149 | }); 150 | }); 151 | 152 | describe('GET /mine', () => { 153 | beforeEach(async () => { 154 | // Clear pending transactions by attempting to mine if any exist 155 | // This ensures a clean slate for each mine test. 156 | const pending = await request(app).get('/transactions'); 157 | if (pending.body.length > 0) { 158 | await request(app).get('/mine'); 159 | } 160 | }); 161 | 162 | test('With pending transactions: should mine a block and return it', async () => { 163 | const tx1 = { from: 'minerWallet', to: 'recipient1', amount: 10 }; 164 | await request(app).post('/transaction').send(tx1); 165 | 166 | const mineResponse = await request(app).get('/mine'); 167 | expect(mineResponse.status).toBe(200); 168 | expect(mineResponse.body).toHaveProperty('index'); 169 | expect(mineResponse.body).toHaveProperty('hash'); 170 | expect(mineResponse.body).toHaveProperty('previousHash'); 171 | expect(mineResponse.body.transactions).toHaveLength(1); 172 | expect(mineResponse.body.transactions[0]).toMatchObject(tx1); 173 | 174 | // Verify transactions are cleared 175 | const transactionsResponse = await request(app).get('/transactions'); 176 | expect(transactionsResponse.body).toEqual([]); 177 | 178 | // Verify blockchain includes the new block 179 | const blockchainResponse = await request(app).get('/blockchain'); 180 | expect(blockchainResponse.body.length).toBeGreaterThanOrEqual(1); // Genesis + mined 181 | const lastBlock = blockchainResponse.body.pop(); 182 | expect(lastBlock.transactions[0]).toMatchObject(tx1); 183 | }); 184 | 185 | test('Without pending transactions: should return 500 error', async () => { 186 | // Ensure no transactions (covered by beforeEach and verified here) 187 | const currentTransactions = await request(app).get('/transactions'); 188 | expect(currentTransactions.body).toEqual([]); 189 | 190 | const mineResponse = await request(app).get('/mine'); 191 | expect(mineResponse.status).toBe(500); 192 | expect(mineResponse.body).toEqual({ error: 'No transactions to be mined' }); 193 | }); 194 | }); 195 | 196 | describe('GET /blockchain', () => { 197 | test('should return the blockchain (initially genesis block)', async () => { 198 | // Need to ensure a "fresh" blockchain state for this test, 199 | // meaning only the genesis block from the mocked storage. 200 | // Clearing mockStorageData in a general beforeEach helps. 201 | // The app's blockchain instance should re-load and create genesis. 202 | 203 | // To be certain, let's force a "reset" of the mock storage for blocks. 204 | // This simulates a fresh start for the blockchain. 205 | mockStorageData['blocks'] = undefined; 206 | // The app's Blockchain constructor should call loadBlocks, find no blocks, 207 | // and create a genesis block. We need to allow this async operation to complete. 208 | // This is a bit hand-wavy without explicit app reset. 209 | // Await a short time for potential async init in app, or ensure app.ready() if exists. 210 | // For now, we assume the next request will see the initialized chain. 211 | 212 | const response = await request(app).get('/blockchain'); 213 | expect(response.status).toBe(200); 214 | expect(response.body).toBeInstanceOf(Array); 215 | expect(response.body).toHaveLength(1); // Only genesis block 216 | expect(response.body[0].index).toBe(0); 217 | expect(response.body[0].previousHash).toBe('0'); // Standard previousHash for genesis 218 | }); 219 | 220 | test('should include new blocks after mining', async () => { 221 | // Add a transaction 222 | await request(app).post('/transaction').send({ from: 'testFrom', to: 'testTo', amount: 5 }); 223 | // Mine the block 224 | await request(app).get('/mine'); 225 | 226 | const response = await request(app).get('/blockchain'); 227 | expect(response.status).toBe(200); 228 | expect(response.body).toHaveLength(2); // Genesis + 1 mined block 229 | expect(response.body[1].transactions).toHaveLength(1); 230 | expect(response.body[1].transactions[0].from).toBe('testFrom'); 231 | }); 232 | }); 233 | 234 | describe('GET /block/:idx', () => { 235 | beforeAll(async () => { 236 | // Ensure there's more than just genesis. Mine a block. 237 | // This beforeAll for the describe block ensures blocks exist for these tests. 238 | // Need to clear transactions first. 239 | let pending = await request(app).get('/transactions'); 240 | if (pending.body.length > 0) await request(app).get('/mine'); 241 | 242 | await request(app).post('/transaction').send({ from: 'idxTest', to: 'receiver', amount: 1 }); 243 | const mineResult = await request(app).get('/mine'); 244 | if (mineResult.status !== 200) console.error("Pre-test mining failed:", mineResult.body); 245 | }); 246 | 247 | test('Valid index: should return the correct block', async () => { 248 | // Test for genesis block 249 | let response = await request(app).get('/block/0'); 250 | expect(response.status).toBe(200); 251 | expect(response.body.index).toBe(0); 252 | 253 | // Test for the mined block (index 1) 254 | response = await request(app).get('/block/1'); 255 | expect(response.status).toBe(200); 256 | expect(response.body.index).toBe(1); 257 | expect(response.body.transactions[0].from).toBe('idxTest'); 258 | }); 259 | 260 | test('Invalid index (out of bounds): should return 200 and empty array', async () => { 261 | const response = await request(app).get('/block/999'); // Assuming 999 is out of bounds 262 | expect(response.status).toBe(200); 263 | expect(response.body).toEqual([]); 264 | }); 265 | 266 | test('Invalid index (non-numeric): should return 200 and empty array (or 404)', async () => { 267 | // Behavior depends on Express routing and controller's `getBlockByIndex` parsing. 268 | // Current `getBlockByIndex` if `idx` becomes NaN might result in `[]`. 269 | // If Express route doesn't match `/block/abc` to `/:idx` where `idx` is expected numeric, it's 404. 270 | // Let's assume it hits the endpoint and `getBlockByIndex` handles it. 271 | const response = await request(app).get('/block/abc'); 272 | // If route param coercion fails and it doesn't reach the handler, status could be 400/404. 273 | // If it reaches `getBlockByIndex` and `parseInt('abc')` is `NaN`, it returns `[]`. 274 | expect(response.status).toBe(200); // As per current model implementation 275 | expect(response.body).toEqual([]); 276 | }); 277 | }); 278 | 279 | describe('GET /block/last/index', () => { 280 | test('should return the index of the last block', async () => { 281 | // Initial state (genesis block) 282 | // This test might be flaky depending on when other tests run and add blocks. 283 | // Forcing a "reset" of the blockchain to only genesis for this specific check. 284 | mockStorageData['blocks'] = undefined; 285 | // Await for app to re-init its blockchain (conceptual, may need delay or app hook) 286 | // This is very hard to guarantee without an app reset. 287 | // Let's assume the state from previous /block/:idx tests which added one block. 288 | // So, last index should be 1. 289 | 290 | const blockchainState = await request(app).get('/blockchain'); 291 | const expectedLastIndex = blockchainState.body.length - 1; 292 | 293 | 294 | const response = await request(app).get('/block/last/index'); 295 | expect(response.status).toBe(200); 296 | expect(response.body).toBe(expectedLastIndex); // e.g., 1 if genesis + 1 mined block 297 | }); 298 | }); 299 | 300 | describe('GET /resolve', () => { 301 | test('should return 200 and an array (possibly empty or with sync results)', async () => { 302 | // This is a basic smoke test. 303 | // The actual logic of resolve is complex and unit-tested in Nodes model. 304 | // Here, we just check if the endpoint is reachable and returns without error. 305 | // The response will depend on the mocked nodes.json and how Nodes.resolve behaves with it. 306 | // Given our mocked nodes.json, it will attempt to fetch from them. 307 | // We need to mock `node-fetch` for the Nodes class if it's not already done at a higher level. 308 | 309 | // For this integration test, we haven't mocked `node-fetch` used by `Nodes.js`. 310 | // So, this will make actual HTTP requests to localhost:3001/3002 if not careful. 311 | // This highlights a deeper need for controlling external calls in integration tests. 312 | 313 | // Simplification: If `nodes.json` was empty, resolve would do nothing. 314 | // Let's write nodes.json to be empty for this specific test. 315 | fs.writeFileSync(nodesFilePath, JSON.stringify([])); 316 | // Re-trigger app's node list loading (difficult without app reset) 317 | // Assuming for now the Nodes instance in app re-reads it or is new. 318 | 319 | const response = await request(app).get('/resolve'); 320 | expect(response.status).toBe(200); 321 | // With an empty nodes list, the resolve method in Nodes.js typically returns an empty array or a specific message. 322 | // The controller sends back `res.send(nodes.resolve(res, blockchain));` 323 | // If `nodes.list` is empty, `nodes.resolve` returns `res.send([])` 324 | // So, response.body should be [] 325 | expect(response.body).toEqual([]); 326 | 327 | // Restore nodes.json for other tests if needed, or rely on afterAll. 328 | fs.writeFileSync(nodesFilePath, JSON.stringify(['http://localhost:3001', 'http://localhost:3002'])); 329 | }); 330 | }); 331 | 332 | }); 333 | -------------------------------------------------------------------------------- /src/controllers/blockchain.js: -------------------------------------------------------------------------------- 1 | const Transactions = require('../models/transactions'); 2 | const Blockchain = require('../models/blockchain'); 3 | const Nodes = require('../models/nodes'); 4 | 5 | class BlockchainController { 6 | constructor(url, port) { 7 | this.blockchain = new Blockchain(url, port); 8 | this.nodes = new Nodes(url, port); 9 | this.transactions = new Transactions(); 10 | } 11 | 12 | resolve(req, res) { 13 | this.nodes.resolve(res, this.blockchain); 14 | } 15 | 16 | getNodes(req, res) { 17 | res.json(this.nodes.list); 18 | } 19 | 20 | postTransaction(req, res) { 21 | this.transactions.add(req, res); 22 | } 23 | 24 | getTransactions(req, res) { 25 | res.json(this.transactions.get()); 26 | } 27 | 28 | mine(req, res) { 29 | res.json(this.blockchain.mine(this.transactions, res)); 30 | } 31 | 32 | getBlockchain(req, res) { 33 | res.json(this.blockchain.blocks); 34 | } 35 | 36 | getBlockByIndex(req, res) { 37 | res.json(this.blockchain.getBlockByIndex(req.params.idx)); 38 | } 39 | 40 | getBlockLastIndex(req, res) { 41 | res.json(this.blockchain.getBlockLastIndex()); 42 | } 43 | } 44 | 45 | module.exports = BlockchainController; -------------------------------------------------------------------------------- /src/models/__tests__/block.test.js: -------------------------------------------------------------------------------- 1 | const Block = require('../block'); 2 | 3 | describe('Block', () => { 4 | let timestamp; 5 | let previousHash; 6 | let transactions; 7 | let nonce; 8 | let hash; 9 | let block; 10 | 11 | beforeEach(() => { 12 | timestamp = Date.now(); 13 | previousHash = 'previous-hash'; 14 | transactions = { 15 | list: [{ id: 'tx1' }, { id: 'tx2' }], 16 | reset: jest.fn(), 17 | }; 18 | nonce = 0; 19 | hash = 'test-hash'; // Assuming hash is calculated or passed externally 20 | block = new Block(timestamp, previousHash, transactions, nonce, hash); 21 | }); 22 | 23 | test('constructor initializes properties correctly', () => { 24 | expect(block.timestamp).toBe(timestamp); 25 | expect(block.previousHash).toBe(previousHash); 26 | expect(block.transactions).toEqual([{ id: 'tx1' }, { id: 'tx2' }]); 27 | expect(block.nonce).toBe(nonce); 28 | expect(block.hash).toBe(hash); 29 | expect(transactions.reset).toHaveBeenCalledTimes(1); 30 | }); 31 | 32 | describe('key getter', () => { 33 | test('produces a string', () => { 34 | expect(typeof block.key).toBe('string'); 35 | }); 36 | 37 | test('changes when nonce changes', () => { 38 | const initialKey = block.key; 39 | block.nonce = 1; 40 | expect(block.key).not.toBe(initialKey); 41 | }); 42 | 43 | test('changes when transactions change', () => { 44 | const initialKey = block.key; 45 | block.transactions = [{ id: 'tx3' }]; 46 | expect(block.key).not.toBe(initialKey); 47 | }); 48 | }); 49 | 50 | describe('addTransactions', () => { 51 | let newTransactions; 52 | 53 | beforeEach(() => { 54 | newTransactions = { 55 | list: [{ id: 'tx3' }, { id: 'tx4' }], 56 | reset: jest.fn(), 57 | }; 58 | // Reset the transactions in the block for a clean test 59 | block.transactions = []; 60 | // Reset the mock for the initial transactions object 61 | transactions.reset.mockClear(); 62 | }); 63 | 64 | test('adds transactions from input and calls reset', () => { 65 | block.addTransactions(newTransactions); 66 | expect(block.transactions).toEqual([{ id: 'tx3' }, { id: 'tx4' }]); 67 | expect(newTransactions.reset).toHaveBeenCalledTimes(1); 68 | // Ensure the original transactions.reset was not called again 69 | expect(transactions.reset).not.toHaveBeenCalled(); 70 | }); 71 | 72 | test('does not add transactions if input list is empty', () => { 73 | const emptyTransactions = { 74 | list: [], 75 | reset: jest.fn(), 76 | }; 77 | block.addTransactions(emptyTransactions); 78 | expect(block.transactions).toEqual([]); 79 | expect(emptyTransactions.reset).toHaveBeenCalledTimes(1); 80 | }); 81 | }); 82 | }); 83 | -------------------------------------------------------------------------------- /src/models/__tests__/blockchain.test.js: -------------------------------------------------------------------------------- 1 | const Blockchain = require('../blockchain'); 2 | const Block = require('../block'); 3 | const Nodes = require('../nodes'); 4 | const storage = require('node-persist'); 5 | const sha256 = require('js-sha256'); 6 | 7 | // Mock dependencies 8 | jest.mock('../block'); 9 | jest.mock('../nodes'); 10 | jest.mock('js-sha256'); 11 | 12 | // Improved mock for node-persist 13 | const mockStorage = { 14 | init: jest.fn().mockResolvedValue(undefined), 15 | getItem: jest.fn().mockResolvedValue(undefined), // Default to resolve with undefined 16 | setItem: jest.fn().mockResolvedValue(undefined), 17 | clear: jest.fn().mockResolvedValue(undefined), // if used 18 | // Add other methods if Blockchain uses them, e.g., length, key, etc. 19 | }; 20 | jest.mock('node-persist', () => ({ 21 | create: jest.fn().mockReturnValue(mockStorage), // create() returns our mockStorage object 22 | // Static methods like `რედაქტირება` or `რედაქტირებაSync` would be mocked here if used directly, 23 | // but Blockchain seems to use an instance via create().config(). 24 | })); 25 | 26 | // Actual storage instance used by Blockchain class will be our mockStorage. 27 | // We can refer to `mockStorage.init`, `mockStorage.getItem` etc. in tests. 28 | 29 | describe('Blockchain', () => { 30 | let blockchain; // Will hold Blockchain instance 31 | let mockTransactions; 32 | let mockRes; 33 | 34 | beforeEach(async () => { // Made beforeEach async to handle async blockchain instantiation 35 | // Reset all general mocks 36 | Block.mockClear(); 37 | Nodes.mockClear(); // Assuming Nodes has been mocked and its methods are jest.fn() 38 | sha256.mockClear(); 39 | 40 | // Clear mocks on our mockStorage object 41 | mockStorage.init.mockClear(); 42 | mockStorage.getItem.mockClear(); 43 | mockStorage.setItem.mockClear(); 44 | mockStorage.clear.mockClear(); 45 | 46 | // Set default behaviors for storage methods for each test 47 | // (can be overridden in specific tests if needed) 48 | mockStorage.getItem.mockImplementation(async (key) => { 49 | if (key === 'blocks') return undefined; // Default: no blocks 50 | if (key === 'transactions') return undefined; // Default: no pending transactions 51 | return undefined; 52 | }); 53 | 54 | // Initialize blockchain here. Constructor calls async loadBlocks(). 55 | // The Blockchain constructor itself isn't async, but it triggers async operations. 56 | blockchain = new Blockchain(); 57 | // Ensure async operations triggered by constructor (like loadBlocks) complete. 58 | // Blockchain.js needs to expose a promise for this, or tests need to account for it. 59 | // For now, we assume `loadBlocksPromise` or similar exists, or that subsequent awaits in tests handle it. 60 | // If Blockchain class has a promise like `this.loadBlocksPromise = this.loadBlocks();` 61 | // then we can do: await blockchain.loadBlocksPromise; 62 | // Based on the original test, it seems `await blockchain.loadBlocks()` was called manually. 63 | // Let's assume the constructor handles loadBlocks internally and we might need to wait if structure changed. 64 | // If Blockchain constructor now internally awaits loadBlocks, then `new Blockchain()` might return a promise implicitly. 65 | // However, standard JS constructors don't return promises. 66 | // We will rely on the fact that storage calls are awaited inside Blockchain methods. 67 | 68 | // Common mock objects 69 | mockTransactions = { 70 | list: [{ id: 'tx1' }], 71 | reset: jest.fn(), // Assuming Transactions has a reset method 72 | }; 73 | mockRes = { 74 | status: jest.fn().mockReturnThis(), 75 | json: jest.fn(), 76 | send: jest.fn(), // Used by some methods 77 | }; 78 | }); 79 | 80 | describe('constructor', () => { 81 | test('Scenario 1: No existing blocks in storage', async () => { 82 | // getItem is configured in beforeEach to return undefined for 'blocks' and 'transactions' 83 | // Re-initialize blockchain to ensure constructor logic with these mocks is tested. 84 | blockchain = new Blockchain(); 85 | // The constructor should call loadBlocks, which in turn might call addBlock for genesis. 86 | // We need to await the completion of these async operations. 87 | // A common pattern is for the class to expose a promise that resolves when init is done. 88 | // If not, we might need a small delay or rely on internal awaits in Blockchain. 89 | // For now, assume Blockchain's constructor internally handles this, 90 | // and its methods correctly await storage operations. 91 | 92 | // Let's use a small delay to allow async operations in constructor to complete. 93 | // This is not ideal, a dedicated promise from Blockchain would be better. 94 | await new Promise(resolve => setTimeout(resolve, 0)); 95 | 96 | 97 | expect(mockStorage.init).toHaveBeenCalledTimes(1); 98 | // getItem would be called for 'blocks' and 'transactions' by loadBlocks 99 | expect(mockStorage.getItem).toHaveBeenCalledWith('blocks'); 100 | expect(mockStorage.getItem).toHaveBeenCalledWith('transactions'); 101 | 102 | // A genesis block should be created if mockStorage.getItem('blocks') was undefined 103 | expect(Block).toHaveBeenCalledTimes(1); // For the genesis block 104 | expect(blockchain.blocks).toHaveLength(1); 105 | expect(blockchain.blocks[0]).toBeInstanceOf(Block); 106 | // setItem is called by addBlock (which is called for genesis) 107 | expect(mockStorage.setItem).toHaveBeenCalledWith('blocks', blockchain.blocks); 108 | // setItem might also be called for transactions if they are initialized 109 | // expect(mockStorage.setItem).toHaveBeenCalledWith('transactions', []); 110 | }); 111 | 112 | test('Scenario 2: Existing blocks in storage', async () => { 113 | const existingBlocksData = [ 114 | { index: 0, previousHash: '0', timestamp: Date.now(), transactions: [], nonce: 0, hash: 'hash0' }, 115 | { index: 1, previousHash: 'hash0', timestamp: Date.now(), transactions: [], nonce: 1, hash: 'hash1' }, 116 | ]; 117 | mockStorage.getItem.mockImplementation(async (key) => { 118 | if (key === 'blocks') return existingBlocksData; 119 | if (key === 'transactions') return []; // Or some existing transactions 120 | return undefined; 121 | }); 122 | 123 | Block.mockClear(); // Clear any calls from previous tests or beforeEach setup 124 | 125 | blockchain = new Blockchain(); // Re-initialize with the new mock for getItem 126 | await new Promise(resolve => setTimeout(resolve, 0)); // Allow async loadBlocks to complete 127 | 128 | expect(mockStorage.init).toHaveBeenCalledTimes(1); 129 | expect(mockStorage.getItem).toHaveBeenCalledWith('blocks'); 130 | 131 | expect(blockchain.blocks).toEqual(existingBlocksData); // Blocks should be loaded 132 | expect(Block).not.toHaveBeenCalled(); // No NEW Block instances should be made if loaded from storage 133 | // setItem should not be called for 'blocks' if they were just loaded and not changed 134 | expect(mockStorage.setItem).not.toHaveBeenCalledWith('blocks', expect.any(Array)); 135 | }); 136 | }); 137 | 138 | describe('addBlock(block)', () => { 139 | let mockBlockInstance; 140 | 141 | beforeEach(() => { 142 | // Create a fresh mock Block instance for each addBlock test 143 | // This represents the block *to be added* 144 | mockBlockInstance = new Block(); // This is a mock Block instance 145 | mockBlockInstance.key = 'test-key'; // Mock key for hash generation 146 | mockBlockInstance.nonce = 0; // Mock nonce for hash generation 147 | // Mock methods or properties on this specific instance if needed 148 | // e.g., mockBlockInstance.addTransactions = jest.fn(); 149 | 150 | // Ensure blockchain.blocks is clean for specific scenarios 151 | blockchain.blocks = []; 152 | sha256.mockReturnValue('dummy-hash'); // Ensure generateHash works 153 | }); 154 | 155 | test('Scenario 1: Adding the first block (genesis)', async () => { 156 | // The block passed to addBlock is assumed to be a new block, possibly genesis 157 | // For genesis, previousHash and hash are set by addBlock. 158 | 159 | await blockchain.addBlock(mockBlockInstance); 160 | 161 | expect(mockBlockInstance.previousHash).toBe("0000000000000000"); 162 | // Verify hash generation was called 163 | expect(sha256).toHaveBeenCalled(); // generateHash was called 164 | expect(mockBlockInstance.hash).toBe('dummy-hash'); // generateHash assigned the hash 165 | expect(blockchain.blocks).toHaveLength(1); 166 | expect(blockchain.blocks[0]).toBe(mockBlockInstance); 167 | expect(mockStorage.setItem).toHaveBeenCalledWith('blocks', blockchain.blocks); 168 | }); 169 | 170 | test('Scenario 2: Adding a subsequent block', async () => { 171 | const previousBlock = new Block(); // Mock existing block 172 | previousBlock.hash = 'previous-block-hash'; 173 | blockchain.blocks = [previousBlock]; // Setup existing chain 174 | 175 | await blockchain.addBlock(mockBlockInstance); 176 | 177 | expect(mockBlockInstance.previousHash).toBe(previousBlock.hash); 178 | expect(sha256).toHaveBeenCalled(); 179 | expect(mockBlockInstance.hash).toBe('dummy-hash'); 180 | expect(blockchain.blocks).toHaveLength(2); 181 | expect(blockchain.blocks[1]).toBe(mockBlockInstance); 182 | expect(mockStorage.setItem).toHaveBeenCalledWith('blocks', blockchain.blocks); 183 | }); 184 | }); 185 | 186 | describe('getNextBlock(transactions)', () => { 187 | let mockPreviousBlock; 188 | let mockNewBlockInstance; 189 | 190 | beforeEach(() => { 191 | mockPreviousBlock = new Block(); // Mock instance of Block 192 | mockPreviousBlock.index = 0; 193 | mockPreviousBlock.hash = 'prev-hash'; 194 | 195 | // Mock getPreviousBlock to return our controlled block 196 | blockchain.getPreviousBlock = jest.fn().mockReturnValue(mockPreviousBlock); 197 | 198 | // Mock generateHash to return a predictable hash 199 | blockchain.generateHash = jest.fn().mockReturnValue('next-block-hash'); 200 | 201 | // When `new Block(...)` is called inside getNextBlock, it should return our mock instance 202 | mockNewBlockInstance = new Block(); // This is the block getNextBlock will "create" 203 | mockNewBlockInstance.addTransactions = jest.fn(); // Mock its methods 204 | Block.mockImplementation(() => mockNewBlockInstance); 205 | 206 | 207 | }); 208 | 209 | test('should create and return a new block with correct properties', () => { 210 | const resultBlock = blockchain.getNextBlock(mockTransactions); 211 | 212 | expect(Block).toHaveBeenCalledTimes(1); // A new Block was instantiated 213 | // Check constructor arguments for the new block if necessary, e.g. 214 | // expect(Block).toHaveBeenCalledWith(expect.any(Number), mockPreviousBlock.hash, ???); 215 | // This depends on how Block is constructed and what getNextBlock passes. 216 | // Based on typical blockchain logic: 217 | // new Block(timestamp, previousHash, transactions (handled by addTransactions), nonce, hash) 218 | // Nonce and hash are set by generateHash. 219 | // The actual Block constructor in the code takes: timestamp, previousHash, transactions (raw), nonce, hash 220 | 221 | expect(resultBlock).toBe(mockNewBlockInstance); // Returns the created mock instance 222 | expect(resultBlock.addTransactions).toHaveBeenCalledWith(mockTransactions); 223 | expect(resultBlock.index).toBe(mockPreviousBlock.index + 1); 224 | expect(resultBlock.previousHash).toBe(mockPreviousBlock.hash); 225 | expect(blockchain.generateHash).toHaveBeenCalledWith(resultBlock); 226 | expect(resultBlock.hash).toBe('next-block-hash'); 227 | }); 228 | }); 229 | 230 | describe('getPreviousBlock()', () => { 231 | test('should return the last block in the chain', () => { 232 | const block1 = { index: 0 }; 233 | const block2 = { index: 1 }; 234 | blockchain.blocks = [block1, block2]; 235 | expect(blockchain.getPreviousBlock()).toBe(block2); 236 | }); 237 | 238 | test('should return undefined if chain is empty (though constructor adds genesis)', () => { 239 | blockchain.blocks = []; // Force empty 240 | expect(blockchain.getPreviousBlock()).toBeUndefined(); 241 | }); 242 | }); 243 | 244 | describe('generateHash(block)', () => { 245 | let mockBlockForKey; 246 | 247 | beforeEach(() => { 248 | mockBlockForKey = { // Not a Block instance, just an object with key and nonce 249 | key: 'test_data_for_hashing', 250 | nonce: 0, 251 | }; 252 | // Reset sha256 mock for specific call counting per test 253 | sha256.mockClear(); 254 | }); 255 | 256 | test('should call js-sha256 until hash starts with "000"', () => { 257 | sha256 258 | .mockReturnValueOnce('123hash') 259 | .mockReturnValueOnce('012hash') 260 | .mockReturnValueOnce('000hash_success'); 261 | 262 | const hash = blockchain.generateHash(mockBlockForKey); 263 | 264 | expect(sha256).toHaveBeenCalledTimes(3); 265 | expect(sha256).toHaveBeenNthCalledWith(1, mockBlockForKey.key + 0); 266 | expect(sha256).toHaveBeenNthCalledWith(2, mockBlockForKey.key + 1); 267 | expect(sha256).toHaveBeenNthCalledWith(3, mockBlockForKey.key + 2); 268 | expect(mockBlockForKey.nonce).toBe(2); // Nonce incremented until success 269 | expect(hash).toBe('000hash_success'); 270 | }); 271 | 272 | test('should handle block without a key property gracefully (or throw error)', () => { 273 | // Based on current implementation, it would be `undefined + nonce` leading to `NaN` in string context 274 | // then sha256 would hash "NaN0", "NaN1" etc. This is probably not intended. 275 | // For now, let's test current behavior. 276 | const blockWithoutKey = { nonce: 0 }; 277 | sha256.mockReturnValueOnce('000_hash_for_nan'); 278 | 279 | const hash = blockchain.generateHash(blockWithoutKey); 280 | 281 | expect(sha256).toHaveBeenCalledWith('undefined0'); // Or "NaN0" depending on JS coercion 282 | expect(blockWithoutKey.nonce).toBe(0); 283 | expect(hash).toBe('000_hash_for_nan'); 284 | }); 285 | }); 286 | 287 | describe('mine(transactions, res)', () => { 288 | let mockNewBlock; 289 | 290 | beforeEach(() => { 291 | mockNewBlock = new Block(); // A mock block that getNextBlock will return 292 | mockNewBlock.hash = 'mined-block-hash'; // Give it some identifiable property 293 | 294 | blockchain.getNextBlock = jest.fn().mockReturnValue(mockNewBlock); 295 | blockchain.addBlock = jest.fn().mockResolvedValue(undefined); // Simulate async addBlock 296 | 297 | // Mock Nodes instance and its broadcast method 298 | // blockchain.nodes is an instance of the mocked Nodes class. 299 | // So we need to ensure its broadcast method is a mock. 300 | // The Nodes mock should handle this if its methods are jest.fn() 301 | // If blockchain.nodes was instantiated with `new Nodes()`, and Nodes is jest.mocked: 302 | // then blockchain.nodes.broadcast should already be a jest.fn(). 303 | // Let's verify by ensuring Nodes.mock.instances[0].broadcast exists if an instance was made. 304 | if (Nodes.mock.instances.length > 0) { 305 | Nodes.mock.instances[0].broadcast = jest.fn(); 306 | blockchain.nodes = Nodes.mock.instances[0]; // ensure our blockchain uses this instance 307 | } else { 308 | // If constructor didn't make one, create a manual one for the test 309 | const mockNodesInstance = new Nodes(); 310 | mockNodesInstance.broadcast = jest.fn(); 311 | blockchain.nodes = mockNodesInstance; 312 | } 313 | }); 314 | 315 | test('Success case: should mine block and broadcast', async () => { 316 | mockTransactions.list = [{ id: 'tx1' }]; // Ensure transactions exist 317 | 318 | const result = await blockchain.mine(mockTransactions, mockRes); 319 | 320 | expect(blockchain.getNextBlock).toHaveBeenCalledWith(mockTransactions); 321 | expect(blockchain.addBlock).toHaveBeenCalledWith(mockNewBlock); 322 | expect(blockchain.nodes.broadcast).toHaveBeenCalledTimes(1); 323 | expect(result).toBe(mockNewBlock); 324 | expect(mockRes.status).not.toHaveBeenCalled(); 325 | expect(mockRes.json).not.toHaveBeenCalled(); // Or check for specific success response if any 326 | }); 327 | 328 | test('Failure case: no transactions to be mined', async () => { 329 | mockTransactions.list = []; // No transactions 330 | 331 | const result = await blockchain.mine(mockTransactions, mockRes); 332 | 333 | expect(mockRes.status).toHaveBeenCalledWith(500); 334 | // The actual implementation sends {error: ...} via res.send, not res.json 335 | // And it doesn't return the error object from the function itself, but undefined. 336 | // Let's adjust based on the actual code's behavior for mine: 337 | // It calls `res.send({error: ...})` and returns nothing in case of error. 338 | // If successful, it returns the block. 339 | 340 | // The original code does: `return res.status(500).send({error: ...})` 341 | // which means the function would return the result of `res.send(...)` 342 | // Let's assume `res.send` returns `res` for chaining or `undefined`. 343 | // For testing, we care that `res.send` was called with the error. 344 | expect(mockRes.send).toHaveBeenCalledWith({ error: 'No transactions to be mined' }); 345 | 346 | // The function should effectively return undefined or what res.send returns 347 | // If it returns the res object: expect(result).toBe(mockRes); 348 | // If it returns what res.send returns (e.g. undefined): expect(result).toBeUndefined(); 349 | // Given `return res.status(500).send(...)`, it returns the result of `send`. 350 | // We'll assume `send` returns `res` for now. If not, the test for `result` might need adjustment. 351 | // Let's check if `res.send` was called, which is more robust. 352 | 353 | expect(blockchain.getNextBlock).not.toHaveBeenCalled(); 354 | expect(blockchain.addBlock).not.toHaveBeenCalled(); 355 | expect(blockchain.nodes.broadcast).not.toHaveBeenCalled(); 356 | }); 357 | }); 358 | 359 | describe('updateBlocks(blocks, transactions)', () => { // Added transactions based on impl. 360 | test('should replace this.blocks and this.transactions, and save blocks', async () => { 361 | const newBlocksArray = [{ index: 0, hash: 'new_genesis' }]; 362 | const newTransactionsArray = [{id: 'new_tx'}]; 363 | 364 | // Mock blockchain's current transactions if the method also updates them 365 | blockchain.transactions = { list: [], reset: jest.fn() }; 366 | 367 | 368 | await blockchain.updateBlocks(newBlocksArray, newTransactionsArray); 369 | 370 | expect(blockchain.blocks).toBe(newBlocksArray); 371 | expect(blockchain.transactions.list).toBe(newTransactionsArray); 372 | 373 | expect(mockStorage.setItem).toHaveBeenCalledWith('blocks', newBlocksArray); 374 | expect(mockStorage.setItem).toHaveBeenCalledWith('transactions', newTransactionsArray); 375 | }); 376 | }); 377 | 378 | describe('getBlockByIndex(idx)', () => { 379 | beforeEach(() => { 380 | blockchain.blocks = [ 381 | { index: 0, data: 'genesis' }, 382 | { index: 1, data: 'block1' }, 383 | { index: 2, data: 'block2' }, 384 | ]; 385 | }); 386 | 387 | test('should return the block at a valid index', () => { 388 | expect(blockchain.getBlockByIndex(1)).toEqual({ index: 1, data: 'block1' }); 389 | }); 390 | 391 | test('should return empty array for an index too high', () => { 392 | expect(blockchain.getBlockByIndex(5)).toEqual([]); // As per current code snippet 393 | }); 394 | 395 | test('should return empty array for a negative index', () => { 396 | expect(blockchain.getBlockByIndex(-1)).toEqual([]); // As per current code snippet 397 | }); 398 | test('should return empty array for non-numeric index', () => { 399 | expect(blockchain.getBlockByIndex("abc")).toEqual([]); 400 | }); 401 | }); 402 | 403 | describe('getBlockLastIndex()', () => { 404 | test('should return the last index when blocks exist', () => { 405 | blockchain.blocks = [{ index: 0 }, { index: 1 }, { index: 2 }]; 406 | expect(blockchain.getBlockLastIndex()).toBe(2); 407 | }); 408 | 409 | test('should return -1 when no blocks exist', () => { 410 | blockchain.blocks = []; 411 | expect(blockchain.getBlockLastIndex()).toBe(-1); 412 | }); 413 | }); 414 | }); 415 | -------------------------------------------------------------------------------- /src/models/__tests__/nodes.test.js: -------------------------------------------------------------------------------- 1 | const Nodes = require('../nodes'); 2 | const fetch = require('node-fetch'); 3 | const fs = require('fs'); 4 | const Blockchain = require('../blockchain'); // Though we mock it, it's good to have the actual path 5 | 6 | // Mock external dependencies 7 | jest.mock('node-fetch'); 8 | jest.mock('fs'); 9 | jest.mock('../blockchain'); // Mock the Blockchain class 10 | 11 | // No need to mock a global config file for Nodes.js, as url and port are passed to constructor. 12 | 13 | describe('Nodes', () => { 14 | let nodesInstance; 15 | let mockRes; 16 | 17 | beforeEach(() => { 18 | // Reset mocks before each test 19 | fetch.mockReset(); 20 | fs.readFileSync.mockReset(); 21 | Blockchain.mockClear(); // Clear all instances and calls to constructor and all methods. 22 | // We will also mock specific Blockchain instances' methods as needed. 23 | 24 | mockRes = { 25 | send: jest.fn(), 26 | status: jest.fn().mockReturnThis(), 27 | json: jest.fn(), // Added for completeness, though not directly in Nodes 28 | }; 29 | }); 30 | 31 | describe('constructor', () => { 32 | test('should populate this.list correctly, excluding the current node URL', () => { 33 | const nodesJsonContent = JSON.stringify([ 34 | 'http://localhost:3000', // Current node 35 | 'http://localhost:3001', 36 | 'http://localhost:3002', 37 | ]); 38 | fs.readFileSync.mockReturnValue(nodesJsonContent); 39 | 40 | // Provide URL and port to the constructor as Nodes.js expects 41 | nodesInstance = new Nodes('http://localhost', '3000'); 42 | 43 | // Verify that Nodes.js tries to read the correct nodes.json path 44 | // process.env.NODE_ENV is usually 'test' in Jest environment 45 | const expectedNodesPath = require('path').resolve(__dirname, '../../config/nodes.json'); 46 | expect(fs.readFileSync).toHaveBeenCalledWith(expectedNodesPath, 'utf8'); 47 | expect(nodesInstance.list).toEqual([ 48 | 'http://localhost:3001', 49 | 'http://localhost:3002', 50 | ]); 51 | }); 52 | 53 | test('should result in an empty list if nodes.json is empty', () => { 54 | fs.readFileSync.mockReturnValue(JSON.stringify([])); 55 | nodesInstance = new Nodes('http://localhost', '3000'); 56 | expect(nodesInstance.list).toEqual([]); 57 | }); 58 | 59 | test('should result in an empty list if nodes.json only contains the current node URL', () => { 60 | fs.readFileSync.mockReturnValue(JSON.stringify(['http://localhost:3000'])); 61 | nodesInstance = new Nodes('http://localhost', '3000'); 62 | expect(nodesInstance.list).toEqual([]); 63 | }); 64 | 65 | test('should handle errors when nodes.json is not found (though fs mock makes this tricky)', () => { 66 | fs.readFileSync.mockImplementation(() => { 67 | throw new Error('File not found'); 68 | }); 69 | // Expect constructor to not throw but initialize with empty list or handle gracefully 70 | expect(() => { 71 | nodesInstance = new Nodes('http://localhost', '3000'); 72 | }).not.toThrow(); 73 | expect(nodesInstance.list).toEqual([]); 74 | }); 75 | }); 76 | 77 | describe('resolve(res, blockchain)', () => { 78 | let mockBlockchain; 79 | 80 | beforeEach(() => { 81 | // Setup a default nodesInstance for resolve tests 82 | fs.readFileSync.mockReturnValue(JSON.stringify(['http://localhost:3001', 'http://localhost:3002'])); 83 | // Provide URL and port to the constructor 84 | nodesInstance = new Nodes('http://localhost', '3000'); 85 | 86 | // Create a mock Blockchain instance for each test 87 | mockBlockchain = new Blockchain(); // Blockchain is mocked, this creates a mocked instance 88 | mockBlockchain.updateBlocks = jest.fn(); 89 | // Mock the blocks getter. We need to use jest.spyOn for getters/setters on mocked objects 90 | // or define it directly if the mock is simple. 91 | // Here, we'll assume 'blocks' is a property that can be set for simplicity with jest.mock. 92 | // If it were a getter, it'd be: jest.spyOn(mockBlockchain, 'blocks', 'get').mockReturnValue([...]); 93 | mockBlockchain.blocks = [{ index: 0, hash: 'genesis' }]; // Current chain 94 | }); 95 | 96 | test('Scenario 1: Current chain is shorter, should sync and send status', async () => { 97 | const longerChain = [{ index: 0 }, { index: 1, hash: 'new_block' }]; 98 | fetch.mockResolvedValueOnce({ // Mock response for first node in list (node1) 99 | ok: true, 100 | json: async () => ({ blocks: longerChain, transactions: [] }), 101 | }); 102 | fetch.mockResolvedValueOnce({ // Mock response for second node (node2) - shorter chain 103 | ok: true, 104 | json: async () => ({ blocks: [{ index: 0 }], transactions: [] }), 105 | }); 106 | 107 | 108 | await nodesInstance.resolve(mockRes, mockBlockchain); 109 | 110 | expect(fetch).toHaveBeenCalledWith('http://localhost:3001/blockchain'); 111 | expect(fetch).toHaveBeenCalledWith('http://localhost:3002/blockchain'); 112 | expect(mockBlockchain.updateBlocks).toHaveBeenCalledTimes(1); 113 | expect(mockBlockchain.updateBlocks).toHaveBeenCalledWith(longerChain, []); // Ensure transactions are also passed 114 | expect(mockRes.send).toHaveBeenCalledWith(expect.arrayContaining([ 115 | { synced: 'http://localhost:3001' }, 116 | { noaction: 'http://localhost:3002' } 117 | ])); 118 | expect(mockRes.status).not.toHaveBeenCalled(); 119 | }); 120 | 121 | test('Scenario 2: Current chain is longer or same length, no sync, send status', async () => { 122 | const shorterChain = [{ index: 0 }]; // Same as current mockBlockchain.blocks 123 | fetch.mockResolvedValue({ // Mock response for all nodes 124 | ok: true, 125 | json: async () => ({ blocks: shorterChain, transactions: [] }), 126 | }); 127 | 128 | await nodesInstance.resolve(mockRes, mockBlockchain); 129 | 130 | expect(fetch).toHaveBeenCalledTimes(nodesInstance.list.length); 131 | expect(mockBlockchain.updateBlocks).not.toHaveBeenCalled(); 132 | expect(mockRes.send).toHaveBeenCalledWith(expect.arrayContaining([ 133 | { noaction: 'http://localhost:3001' }, 134 | { noaction: 'http://localhost:3002' } 135 | ])); 136 | expect(mockRes.status).not.toHaveBeenCalled(); 137 | }); 138 | 139 | test('Scenario 3: Node fetch error, send error status for that node', async () => { 140 | fetch.mockRejectedValueOnce(new Error('Network error')); // node1 fails 141 | fetch.mockResolvedValueOnce({ // node2 is fine, shorter chain 142 | ok: true, 143 | json: async () => ({ blocks: [{ index: 0 }], transactions: [] }), 144 | }); 145 | 146 | await nodesInstance.resolve(mockRes, mockBlockchain); 147 | 148 | expect(fetch).toHaveBeenCalledTimes(nodesInstance.list.length); 149 | expect(mockBlockchain.updateBlocks).not.toHaveBeenCalled(); 150 | expect(mockRes.send).toHaveBeenCalledWith(expect.arrayContaining([ 151 | { error: 'Failed to reach node http://localhost:3001 or node is not responding correctly.' }, 152 | { noaction: 'http://localhost:3002' } 153 | ])); 154 | expect(mockRes.status).not.toHaveBeenCalledWith(500); // Not all nodes failed 155 | }); 156 | 157 | test('Scenario 3b: Node returns non-ok response, send error status', async () => { 158 | fetch.mockResolvedValueOnce({ ok: false, status: 500 }); // node1 returns server error 159 | fetch.mockResolvedValueOnce({ // node2 is fine, shorter chain 160 | ok: true, 161 | json: async () => ({ blocks: [{ index: 0 }], transactions: [] }), 162 | }); 163 | 164 | await nodesInstance.resolve(mockRes, mockBlockchain); 165 | expect(mockRes.send).toHaveBeenCalledWith(expect.arrayContaining([ 166 | { error: 'Failed to reach node http://localhost:3001 or node is not responding correctly.' }, 167 | { noaction: 'http://localhost:3002' } 168 | ])); 169 | expect(mockRes.status).not.toHaveBeenCalledWith(500); 170 | }); 171 | 172 | 173 | test('Scenario 3c: All nodes fail, set status to 500 and send errors', async () => { 174 | fetch.mockRejectedValue(new Error('Network error for all')); // All nodes fail 175 | 176 | await nodesInstance.resolve(mockRes, mockBlockchain); 177 | 178 | expect(fetch).toHaveBeenCalledTimes(nodesInstance.list.length); 179 | expect(mockBlockchain.updateBlocks).not.toHaveBeenCalled(); 180 | expect(mockRes.send).toHaveBeenCalledWith(expect.arrayContaining([ 181 | { error: 'Failed to reach node http://localhost:3001 or node is not responding correctly.' }, 182 | { error: 'Failed to reach node http://localhost:3002 or node is not responding correctly.' } 183 | ])); 184 | expect(mockRes.status).toHaveBeenCalledWith(500); 185 | }); 186 | 187 | test('Scenario 4: Multiple nodes with mixed responses', async () => { 188 | const longerChain = [{ index: 0 }, { index: 1, hash: 'new_block_node1' }]; 189 | const muchLongerChain = [{ index: 0 }, { index: 1 }, {index: 2, hash: 'new_block_node3'}]; 190 | fs.readFileSync.mockReturnValue(JSON.stringify([ 191 | 'http://localhost:3001', 'http://localhost:3002', 'http://localhost:3003', 'http://localhost:3004' 192 | ])); 193 | // Provide URL and port to the constructor 194 | nodesInstance = new Nodes('http://localhost', '3000'); // Re-initialize with more nodes 195 | 196 | fetch.mockResolvedValueOnce({ // node1: longer chain 197 | ok: true, 198 | json: async () => ({ blocks: longerChain, transactions: ['tx1'] }), 199 | }); 200 | fetch.mockRejectedValueOnce(new Error('Network error for node2')); // node2: fails 201 | fetch.mockResolvedValueOnce({ // node3: much longer chain 202 | ok: true, 203 | json: async () => ({ blocks: muchLongerChain, transactions: ['tx2', 'tx3'] }), 204 | }); 205 | fetch.mockResolvedValueOnce({ // node4: shorter chain 206 | ok: true, 207 | json: async () => ({ blocks: [{ index: 0 }], transactions: [] }), 208 | }); 209 | 210 | mockBlockchain.blocks = [{ index: 0, hash: 'initial_genesis' }]; 211 | 212 | 213 | await nodesInstance.resolve(mockRes, mockBlockchain); 214 | 215 | expect(fetch.mock.calls[0][0]).toBe('http://localhost:3001/blockchain'); 216 | expect(fetch.mock.calls[1][0]).toBe('http://localhost:3002/blockchain'); 217 | expect(fetch.mock.calls[2][0]).toBe('http://localhost:3003/blockchain'); 218 | expect(fetch.mock.calls[3][0]).toBe('http://localhost:3004/blockchain'); 219 | 220 | // The longest chain (from node3) should be used for update 221 | expect(mockBlockchain.updateBlocks).toHaveBeenCalledTimes(1); 222 | expect(mockBlockchain.updateBlocks).toHaveBeenCalledWith(muchLongerChain, ['tx2', 'tx3']); 223 | 224 | expect(mockRes.send).toHaveBeenCalledWith(expect.arrayContaining([ 225 | { synced: 'http://localhost:3001' }, // This is technically true, it was longer than original 226 | { error: 'Failed to reach node http://localhost:3002 or node is not responding correctly.' }, 227 | { synced: 'http://localhost:3003' }, // This is the one that provided the final longest chain 228 | { noaction: 'http://localhost:3004' } 229 | ])); 230 | expect(mockRes.status).not.toHaveBeenCalledWith(500); 231 | }); 232 | }); 233 | 234 | describe('broadcast()', () => { 235 | beforeEach(() => { 236 | // Setup a default nodesInstance for broadcast tests 237 | fs.readFileSync.mockReturnValue(JSON.stringify(['http://localhost:3001', 'http://localhost:3002'])); 238 | // Provide URL and port to the constructor 239 | nodesInstance = new Nodes('http://localhost', '3000'); 240 | global.console = { log: jest.fn() }; // Mock console.log 241 | }); 242 | 243 | afterEach(() => { 244 | // Restore console.log 245 | global.console = require('console'); 246 | }); 247 | 248 | test('should call fetch for each node in this.list with /resolve endpoint', async () => { 249 | fetch.mockResolvedValue({ // Mock a generic successful response for /resolve 250 | ok: true, 251 | json: async () => ({ message: 'Resolved' }), 252 | }); 253 | 254 | await nodesInstance.broadcast(); 255 | 256 | expect(fetch).toHaveBeenCalledTimes(nodesInstance.list.length); 257 | expect(fetch).toHaveBeenCalledWith('http://localhost:3001/resolve', { method: 'POST' }); 258 | expect(fetch).toHaveBeenCalledWith('http://localhost:3002/resolve', { method: 'POST' }); 259 | }); 260 | 261 | test('should log responses from each node', async () => { 262 | fetch.mockResolvedValueOnce({ 263 | ok: true, 264 | json: async () => ({ node: 'node1 response' }), 265 | }); 266 | fetch.mockResolvedValueOnce({ 267 | ok: true, 268 | json: async () => ({ node: 'node2 response' }), 269 | }); 270 | 271 | await nodesInstance.broadcast(); 272 | 273 | expect(console.log).toHaveBeenCalledWith({ node: 'node1 response' }); 274 | expect(console.log).toHaveBeenCalledWith({ node: 'node2 response' }); 275 | }); 276 | 277 | test('should log errors if fetch fails for a node', async () => { 278 | fetch.mockResolvedValueOnce({ // Successful for node1 279 | ok: true, 280 | json: async () => ({ node: 'node1 success' }), 281 | }); 282 | fetch.mockRejectedValueOnce(new Error('Node2 network error')); // Fails for node2 283 | 284 | await nodesInstance.broadcast(); 285 | 286 | expect(console.log).toHaveBeenCalledWith({ node: 'node1 success' }); 287 | expect(console.log).toHaveBeenCalledWith(new Error('Node2 network error')); 288 | }); 289 | 290 | test('should log errors if fetch response is not ok', async () => { 291 | fetch.mockResolvedValueOnce({ // node1 ok 292 | ok: true, 293 | json: async () => ({ node: 'node1 success' }), 294 | }); 295 | fetch.mockResolvedValueOnce({ // node2 not ok 296 | ok: false, 297 | status: 500, 298 | json: async () => ({ error: 'node2 server error' }) 299 | }); 300 | 301 | await nodesInstance.broadcast(); 302 | 303 | expect(console.log).toHaveBeenCalledWith({ node: 'node1 success' }); 304 | // The actual code logs the response object, not a custom error string here. 305 | expect(console.log).toHaveBeenCalledWith(expect.objectContaining({ ok: false, status: 500})); 306 | }); 307 | }); 308 | }); 309 | -------------------------------------------------------------------------------- /src/models/__tests__/transaction.test.js: -------------------------------------------------------------------------------- 1 | const Transaction = require('../transaction'); 2 | 3 | describe('Transaction', () => { 4 | describe('constructor', () => { 5 | test('should create a transaction with valid arguments', () => { 6 | const from = 'address1'; 7 | const to = 'address2'; 8 | const amount = 100; 9 | const transaction = new Transaction(from, to, amount); 10 | 11 | expect(transaction.from).toBe(from); 12 | expect(transaction.to).toBe(to); 13 | expect(transaction.amount).toBe(amount); 14 | expect(typeof transaction.timestamp).toBe('number'); 15 | }); 16 | 17 | test('should throw an error if "from" is missing', () => { 18 | expect(() => new Transaction(undefined, 'address2', 100)).toThrow('Transaction "from" is mandatory'); 19 | }); 20 | 21 | test('should throw an error if "to" is missing', () => { 22 | expect(() => new Transaction('address1', undefined, 100)).toThrow('Transaction "to" is mandatory'); 23 | }); 24 | 25 | test('should throw an error if "amount" is missing', () => { 26 | expect(() => new Transaction('address1', 'address2', undefined)).toThrow('Transaction "amount" is mandatory and must be a number'); 27 | }); 28 | 29 | test('should throw an error if "amount" is not a number', () => { 30 | expect(() => new Transaction('address1', 'address2', 'not-a-number')).toThrow('Transaction "amount" is mandatory and must be a number'); 31 | }); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /src/models/__tests__/transactions.test.js: -------------------------------------------------------------------------------- 1 | const Transactions = require('../transactions'); 2 | const Transaction = require('../transaction'); 3 | 4 | // Mock the Transaction class to control its behavior 5 | jest.mock('../transaction'); 6 | 7 | describe('Transactions', () => { 8 | let transactions; 9 | let mockReq; 10 | let mockRes; 11 | 12 | beforeEach(() => { 13 | transactions = new Transactions(); 14 | // Reset the mock before each test to clear previous calls and instances 15 | Transaction.mockClear(); 16 | 17 | mockReq = { 18 | body: {}, 19 | }; 20 | mockRes = { 21 | json: jest.fn(), 22 | status: jest.fn().mockReturnThis(), // Ensure status can be chained (though not strictly needed here) 23 | }; 24 | }); 25 | 26 | describe('constructor', () => { 27 | test('should initialize an empty list of transactions', () => { 28 | expect(transactions.list).toEqual([]); 29 | }); 30 | }); 31 | 32 | describe('add(req, res)', () => { 33 | describe('Success case', () => { 34 | test('should create a new Transaction, add it to the list, and send success response', () => { 35 | mockReq.body = { from: 'address1', to: 'address2', amount: 100 }; 36 | const mockTransactionInstance = { 37 | from: 'address1', 38 | to: 'address2', 39 | amount: 100, 40 | timestamp: Date.now(), 41 | }; 42 | // Configure the mock Transaction constructor to return our mock instance 43 | Transaction.mockImplementation(() => mockTransactionInstance); 44 | 45 | transactions.add(mockReq, mockRes); 46 | 47 | expect(Transaction).toHaveBeenCalledTimes(1); 48 | expect(Transaction).toHaveBeenCalledWith('address1', 'address2', 100); 49 | expect(transactions.list).toHaveLength(1); 50 | expect(transactions.list[0]).toBe(mockTransactionInstance); 51 | expect(mockRes.json).toHaveBeenCalledWith({ success: 1 }); 52 | expect(mockRes.status).not.toHaveBeenCalled(); 53 | }); 54 | }); 55 | 56 | describe('Failure case (Transaction creation throws error)', () => { 57 | test('should not add to list, set status to 406, and send error response', () => { 58 | mockReq.body = { from: 'address1', amount: 100 }; // Missing 'to' 59 | const errorMessage = 'Transaction "to" is mandatory'; 60 | // Configure the mock Transaction constructor to throw an error 61 | Transaction.mockImplementation(() => { 62 | throw new Error(errorMessage); 63 | }); 64 | 65 | transactions.add(mockReq, mockRes); 66 | 67 | expect(Transaction).toHaveBeenCalledTimes(1); 68 | expect(Transaction).toHaveBeenCalledWith('address1', undefined, 100); 69 | expect(transactions.list).toHaveLength(0); 70 | expect(mockRes.status).toHaveBeenCalledWith(406); 71 | expect(mockRes.json).toHaveBeenCalledWith({ error: errorMessage }); 72 | }); 73 | }); 74 | }); 75 | 76 | describe('get()', () => { 77 | test('should return the current list of transactions', () => { 78 | const mockTx1 = { id: 'tx1' }; 79 | const mockTx2 = { id: 'tx2' }; 80 | transactions.list = [mockTx1, mockTx2]; 81 | 82 | const result = transactions.get(); 83 | expect(result).toEqual([mockTx1, mockTx2]); 84 | }); 85 | 86 | test('should return an empty list if no transactions exist', () => { 87 | const result = transactions.get(); 88 | expect(result).toEqual([]); 89 | }); 90 | }); 91 | 92 | describe('reset()', () => { 93 | test('should clear the list of transactions', () => { 94 | transactions.list = [{ id: 'tx1' }, { id: 'tx2' }]; // Add some dummy transactions 95 | transactions.reset(); 96 | expect(transactions.list).toEqual([]); 97 | }); 98 | 99 | test('should not throw an error if the list is already empty', () => { 100 | expect(() => transactions.reset()).not.toThrow(); 101 | expect(transactions.list).toEqual([]); 102 | }); 103 | }); 104 | }); 105 | -------------------------------------------------------------------------------- /src/models/block.js: -------------------------------------------------------------------------------- 1 | class Block { 2 | constructor() { 3 | this.index = 0; 4 | this.previousHash = ''; 5 | this.hash = ''; 6 | this.timestamp = Math.floor(+new Date() / 1000); 7 | this.nonce = 0; 8 | this.transactions = []; 9 | } 10 | 11 | get key() { 12 | return JSON.stringify(this.transactions) + this.index + this.previousHash + this.nonce; 13 | } 14 | 15 | addTransactions(transactions) { 16 | transactions.list.forEach(transaction => { 17 | this.transactions.push(transaction); 18 | }); 19 | transactions.reset(); 20 | } 21 | 22 | } 23 | 24 | module.exports = Block; -------------------------------------------------------------------------------- /src/models/blockchain.js: -------------------------------------------------------------------------------- 1 | const sha256 = require('js-sha256'); 2 | const Block = require('./block'); 3 | const nodePersist = require('node-persist'); 4 | const crypto = require('crypto'); 5 | const Nodes = require('./nodes'); 6 | 7 | class Blockchain { 8 | constructor(url, port) { 9 | this.blocks = []; 10 | this.nodes = new Nodes(url, port); 11 | 12 | (async () => { 13 | this.storage = nodePersist.create({ 14 | dir: __dirname + '/../../storage/' + crypto.createHash('md5').update(url+port).digest("hex") 15 | }); 16 | await this.storage.init(); 17 | 18 | let blocks = await this.storage.getItem('blocks'); 19 | this.blocks = typeof blocks != 'undefined' ? blocks : []; 20 | 21 | if (this.blocks.length == 0) { 22 | let genesisBlock = new Block(); // initial block 23 | this.addBlock(genesisBlock); 24 | } 25 | })(); 26 | } 27 | 28 | addBlock(block) { 29 | if (this.blocks.length == 0) { 30 | block.previousHash = "0000000000000000"; 31 | block.hash = this.generateHash(block); 32 | } 33 | 34 | this.blocks.push(block); 35 | 36 | (async () => { 37 | await this.storage.setItem('blocks', this.blocks); 38 | })(); 39 | } 40 | 41 | getNextBlock(transactions) { 42 | let block = new Block(); 43 | let previousBlock = this.getPreviousBlock(); 44 | 45 | block.addTransactions(transactions); 46 | block.index = previousBlock.index + 1; 47 | block.previousHash = previousBlock.hash; 48 | block.hash = this.generateHash(block); 49 | 50 | return block; 51 | } 52 | 53 | getPreviousBlock() { 54 | return this.blocks[this.blocks.length - 1]; 55 | } 56 | 57 | generateHash(block) { 58 | let hash = sha256(block.key); 59 | 60 | while (!hash.startsWith('000')) { 61 | block.nonce++; 62 | hash = sha256(block.key); 63 | } 64 | 65 | return hash; 66 | } 67 | 68 | mine(transactions, res) { 69 | if (transactions.list.length == 0) { 70 | res.status(500); 71 | return {error: 'No transactions to be mined'}; 72 | } 73 | 74 | let block = this.getNextBlock(transactions); 75 | this.addBlock(block); 76 | this.nodes.broadcast(); 77 | 78 | return block; 79 | } 80 | 81 | updateBlocks(blocks) { 82 | this.blocks = blocks; 83 | 84 | (async () => { 85 | await this.storage.setItem('blocks', this.blocks); 86 | })(); 87 | } 88 | 89 | getBlockByIndex(idx) { 90 | let foundBlock = []; 91 | 92 | if (idx<=this.blocks.length) { 93 | this.blocks.forEach( (block) => { 94 | if (idx == block.index) { 95 | foundBlock = block; 96 | return; 97 | } 98 | }); 99 | } 100 | 101 | return foundBlock; 102 | } 103 | 104 | getBlockLastIndex() { 105 | return this.blocks.length-1; 106 | } 107 | } 108 | 109 | module.exports = Blockchain; -------------------------------------------------------------------------------- /src/models/nodes.js: -------------------------------------------------------------------------------- 1 | const fetch = (...args) => import('node-fetch').then(({default: fetch}) => fetch(...args)); 2 | const Blockchain = require('./blockchain'); 3 | 4 | class Nodes { 5 | constructor(url, port) { 6 | const nodes = require(process.env.NODE_ENV=='production' ? '../../config/nodes.prod.json' : '../../config/nodes.json'); 7 | const currentURL = url + ':' + port; 8 | this.list = []; 9 | 10 | for(let i in nodes) 11 | if (nodes[i].indexOf(currentURL) == -1) 12 | this.list.push(nodes[i]); 13 | } 14 | 15 | resolve(res, blockchain) { 16 | let completed = 0; 17 | let nNodes = this.list.length; 18 | let response = []; 19 | let errorCount = 0; 20 | 21 | this.list.forEach(function(node) { 22 | fetch(node + '/blockchain') 23 | .then(function(resp) { 24 | return resp.json(); 25 | }) 26 | .then(function(respBlockchain) { 27 | if (blockchain.blocks.length < respBlockchain.length) { 28 | blockchain.updateBlocks(respBlockchain); 29 | response.push({synced: node}); 30 | } else { 31 | response.push({noaction: node}); 32 | } 33 | 34 | if (++completed == nNodes) { 35 | if (errorCount == nNodes) 36 | res.status(500); 37 | res.send(response); 38 | } 39 | }) 40 | .catch(function(error) { 41 | ++errorCount; 42 | //response.push({error: 'Failed to reach node at ' + node}) 43 | response.push({error: error.message}) 44 | if (++completed == nNodes) { 45 | if (errorCount == nNodes) 46 | res.status(500); 47 | res.send(response); 48 | } 49 | }); 50 | }); 51 | } 52 | 53 | broadcast() { 54 | this.list.forEach(function(node) { 55 | fetch(node + '/resolve') 56 | .then(function(resp) { 57 | return resp.json(); 58 | }) 59 | .then(function(resp) { 60 | console.log(node, resp) 61 | }) 62 | .catch(function(error) { 63 | console.log(node, error); 64 | }); 65 | }); 66 | } 67 | } 68 | 69 | module.exports = Nodes; -------------------------------------------------------------------------------- /src/models/transaction.js: -------------------------------------------------------------------------------- 1 | class Transaction { 2 | constructor(from, to, amount) { 3 | if (!from) { 4 | throw new Error('Transaction "from" is mandatory'); 5 | } 6 | if (!to) { 7 | throw new Error('Transaction "to" is mandatory'); 8 | } 9 | if (amount === undefined || typeof amount !== 'number' || isNaN(amount)) { 10 | throw new Error('Transaction "amount" is mandatory and must be a number'); 11 | } 12 | 13 | this.from = from; 14 | this.to = to; 15 | this.amount = amount; 16 | this.timestamp = Math.floor(+new Date() / 1000); 17 | } 18 | } 19 | 20 | module.exports = Transaction; -------------------------------------------------------------------------------- /src/models/transactions.js: -------------------------------------------------------------------------------- 1 | const Transaction = require('./transaction'); 2 | 3 | class Transactions { 4 | constructor() { 5 | this.list = []; 6 | } 7 | 8 | add(req, res) { 9 | let response = ''; 10 | 11 | try { 12 | let tx = new Transaction(req.body.from, req.body.to, req.body.amount); 13 | this.list.push(tx); 14 | response = {'success': 1}; 15 | 16 | } catch(ex) { 17 | res.status(406); 18 | response = {'error': ex.message}; 19 | } 20 | 21 | res.json(response); 22 | } 23 | 24 | get() { 25 | return this.list; 26 | } 27 | 28 | reset() { 29 | this.list = []; 30 | } 31 | } 32 | 33 | module.exports = Transactions; --------------------------------------------------------------------------------