diff --git a/package-lock.json b/package-lock.json index dd03da8c..6ef4c287 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,13 +15,14 @@ "@hapi/sntp": "^4.0.0", "@shardus/archiver-discovery": "1.1.0", "@shardus/crypto-utils": "4.1.3", - "axios": "1.6.1", + "axios": "^1.6.1", "better-sqlite3": "7.6.2", "body-parser": "1.19.0", "connect": "3.7.0", "cookie-parser": "1.4.6", "cors": "2.8.5", "eth-rpc-errors": "4.0.3", + "ethereumjs-tx": "^2.1.2", "ethereumjs-util": "7.1.3", "execa": "^5.1.1", "express": "4.17.2", @@ -4863,6 +4864,49 @@ "setimmediate": "^1.0.5" } }, + "node_modules/ethereumjs-common": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/ethereumjs-common/-/ethereumjs-common-1.5.2.tgz", + "integrity": "sha512-hTfZjwGX52GS2jcVO6E2sx4YuFnf0Fhp5ylo4pEPhEffNln7vS59Hr5sLnp3/QCazFLluuBZ+FZ6J5HTp0EqCA==", + "deprecated": "New package name format for new versions: @ethereumjs/common. Please update." + }, + "node_modules/ethereumjs-tx": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ethereumjs-tx/-/ethereumjs-tx-2.1.2.tgz", + "integrity": "sha512-zZEK1onCeiORb0wyCXUvg94Ve5It/K6GD1K+26KfFKodiBiS6d9lfCXlUKGBBdQ+bv7Day+JK0tj1K+BeNFRAw==", + "deprecated": "New package name format for new versions: @ethereumjs/tx. Please update.", + "dependencies": { + "ethereumjs-common": "^1.5.0", + "ethereumjs-util": "^6.0.0" + } + }, + "node_modules/ethereumjs-tx/node_modules/@types/bn.js": { + "version": "4.11.6", + "resolved": "https://registry.npmjs.org/@types/bn.js/-/bn.js-4.11.6.tgz", + "integrity": "sha512-pqr857jrp2kPuO9uRjZ3PwnJTjoQy+fcdxvBTvHm6dkmEL9q+hDD/2j/0ELOBPtPnS8LjCX0gI9nbl8lVkadpg==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/ethereumjs-tx/node_modules/bn.js": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" + }, + "node_modules/ethereumjs-tx/node_modules/ethereumjs-util": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-6.2.1.tgz", + "integrity": "sha512-W2Ktez4L01Vexijrm5EB6w7dg4n/TgpoYU4avuT5T3Vmnw/eCRtiBrJfQYS/DCSvDIOLn2k57GcHdeBcgVxAqw==", + "dependencies": { + "@types/bn.js": "^4.11.3", + "bn.js": "^4.11.0", + "create-hash": "^1.1.2", + "elliptic": "^6.5.2", + "ethereum-cryptography": "^0.1.3", + "ethjs-util": "0.1.6", + "rlp": "^2.2.3" + } + }, "node_modules/ethereumjs-util": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-7.1.3.tgz", @@ -4878,6 +4922,19 @@ "node": ">=10.0.0" } }, + "node_modules/ethjs-util": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/ethjs-util/-/ethjs-util-0.1.6.tgz", + "integrity": "sha512-CUnVOQq7gSpDHZVVrQW8ExxUETWrnrvXYvYz55wOU8Uj4VCgw56XC2B/fVqQN+f7gmrnRHSLVnFAwsCuNwji8w==", + "dependencies": { + "is-hex-prefixed": "1.0.0", + "strip-hex-prefix": "1.0.0" + }, + "engines": { + "node": ">=6.5.0", + "npm": ">=3" + } + }, "node_modules/eventemitter2": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-5.0.1.tgz", @@ -6266,6 +6323,15 @@ "node": ">=0.10.0" } }, + "node_modules/is-hex-prefixed": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-hex-prefixed/-/is-hex-prefixed-1.0.0.tgz", + "integrity": "sha512-WvtOiug1VFrE9v1Cydwm+FnXd3+w9GaeVUss5W4v/SLy3UW00vP+6iNF2SdnfiBoLy4bTqVdkftNGTUeOFVsbA==", + "engines": { + "node": ">=6.5.0", + "npm": ">=3" + } + }, "node_modules/is-lambda": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", @@ -9988,6 +10054,18 @@ "node": ">=6" } }, + "node_modules/strip-hex-prefix": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-hex-prefix/-/strip-hex-prefix-1.0.0.tgz", + "integrity": "sha512-q8d4ue7JGEiVcypji1bALTos+0pWtyGlivAWyPuTkHzuTCJqrK9sWxYQZUq6Nq3cuyv3bm734IhHvHtGGURU6A==", + "dependencies": { + "is-hex-prefixed": "1.0.0" + }, + "engines": { + "node": ">=6.5.0", + "npm": ">=3" + } + }, "node_modules/strip-indent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", diff --git a/package.json b/package.json index c05bbe1c..de6a86a9 100644 --- a/package.json +++ b/package.json @@ -25,13 +25,14 @@ "@hapi/sntp": "^4.0.0", "@shardus/archiver-discovery": "1.1.0", "@shardus/crypto-utils": "4.1.3", - "axios": "1.6.1", + "axios": "^1.6.1", "better-sqlite3": "7.6.2", "body-parser": "1.19.0", "connect": "3.7.0", "cookie-parser": "1.4.6", "cors": "2.8.5", "eth-rpc-errors": "4.0.3", + "ethereumjs-tx": "^2.1.2", "ethereumjs-util": "7.1.3", "execa": "^5.1.1", "express": "4.17.2", diff --git a/src/__tests__/integration/ethCall.test.ts b/src/__tests__/integration/ethCall.test.ts new file mode 100644 index 00000000..0b6984f5 --- /dev/null +++ b/src/__tests__/integration/ethCall.test.ts @@ -0,0 +1,123 @@ +import request from 'supertest'; +import { extendedServer } from '../../server'; + +describe('JSON-RPC Methods - eth_call', () => { + it('should execute eth_call and return the result', async () => { + const response = await request(extendedServer) + .post('/') + .send({ + jsonrpc: '2.0', + id: 1, + method: 'eth_call', + params: [{ + from: '0x1923A1Eb8e4dA49604aFfd34De1B478580cf8698', + to: '0xC5223533feB845fD28717A7813a72af4df5f2751', + gas: '0x5208', + gasPrice: '0x09184e72a000', + value: '0x0', + data: '0x70a08231000000000000000000000000d46e8dd67c5d32be8058bb8eb970870f07244567' + }, 'latest'] + }); + + expect(response.status).toBe(200); + expect(response.body).toBeDefined(); + expect(response.body.jsonrpc).toBe('2.0'); + expect(response.body.id).toBe(1); + expect(response.body.result).toBeDefined(); + expect(response.body.result).toMatch(/^0x[0-9a-fA-F]*$/); + }); + + it('should return an error if jsonrpc property is missing', async () => { + const response = await request(extendedServer) + .post('/') + .send({ + id: 1, + method: 'eth_call', + params: [{ + from: '0x1923A1Eb8e4dA49604aFfd34De1B478580cf8698', + to: '0xC5223533feB845fD28717A7813a72af4df5f2751', + gas: '0x5208', + gasPrice: '0x09184e72a000', + value: '0x0', + data: '0x70a08231000000000000000000000000d46e8dd67c5d32be8058bb8eb970870f07244567' + }, 'latest'] + }); + + expect(response.status).toBe(200); + expect(response.body).toBeDefined(); + expect(response.body.jsonrpc).toBe('2.0'); + expect(response.body.id).toBe(null); + expect(response.body.error).toBeDefined(); + expect(response.body.error.code).toBe(-32600); + expect(response.body.error.message).toBe('Invalid request'); + }); + + it('should return no response if id property is missing', async () => { + const response = await request(extendedServer) + .post('/') + .send({ + jsonrpc: '2.0', + method: 'eth_call', + params: [{ + from: '0x1923A1Eb8e4dA49604aFfd34De1B478580cf8698', + to: '0xC5223533feB845fD28717A7813a72af4df5f2751', + gas: '0x5208', + gasPrice: '0x09184e72a000', + value: '0x0', + data: '0x70a08231000000000000000000000000d46e8dd67c5d32be8058bb8eb970870f07244567' + }, 'latest'] + }); + + expect(response.status).toBe(204); + }); + + it('should return an error if to property is missing', async () => { + const response = await request(extendedServer) + .post('/') + .send({ + jsonrpc: '2.0', + id: 1, + method: 'eth_call', + params: [{ + from: '0x1923A1Eb8e4dA49604aFfd34De1B478580cf8698', + gas: '0x5208', + gasPrice: '0x09184e72a000', + value: '0x0', + data: '0x70a08231000000000000000000000000d46e8dd67c5d32be8058bb8eb970870f07244567' + }, 'latest'] + }); + + expect(response.status).toBe(200); + expect(response.body).toBeDefined(); + expect(response.body.jsonrpc).toBe('2.0'); + expect(response.body.id).toBe(1); + expect(response.body.error).toBeDefined(); + expect(response.body.error.code).toBe(-32602); + expect(response.body.error.message).toBe(`Invalid params: 'to' or 'data' not provided`); + }); + + it('should return an error if data property is not provided', async () => { + const response = await request(extendedServer) + .post('/') + .send({ + jsonrpc: '2.0', + id: 1, + method: 'eth_call', + params: [{ + from: '0x1923A1Eb8e4dA49604aFfd34De1B478580cf8698', + to: '0xC5223533feB845fD28717A7813a72af4df5f2751', + gas: '0x5208', + gasPrice: '0x09184e72a000', + value: '0x0' + }, 'latest'] + }); + + expect(response.status).toBe(200); + expect(response.body).toBeDefined(); + expect(response.body.jsonrpc).toBe('2.0'); + expect(response.body.id).toBe(1); + expect(response.body.error).toBeDefined(); + expect(response.body.error.code).toBe(-32602); + expect(response.body.error.message).toBe(`Invalid params: 'to' or 'data' not provided`); + }); +}); diff --git a/src/__tests__/integration/ethGetCode.test.ts b/src/__tests__/integration/ethGetCode.test.ts new file mode 100644 index 00000000..c4e1037b --- /dev/null +++ b/src/__tests__/integration/ethGetCode.test.ts @@ -0,0 +1,73 @@ +import request from 'supertest'; +import { extendedServer } from '../../server'; + +describe('JSON-RPC Methods - eth_getCode', () => { + const testAddress = '0x6d1f44b11eb29b8bcbb4f7e15be7e4ebdd0a9cc5'; + + it('should return the code at the given address for the latest block', async () => { + const response = await request(extendedServer) + .post('/') + .send({ + jsonrpc: '2.0', + id: 1, + method: 'eth_getCode', + params: [testAddress, 'latest'] + }); + + expect(response.status).toBe(200); + expect(response.body).toBeDefined(); + expect(response.body.jsonrpc).toBe('2.0'); + expect(response.body.id).toBe(1); + expect(response.body.result).toMatch(/^0x[0-9a-fA-F]*$/); + console.log('Code:', response.body.result); + }); + + it('should return an error if jsonrpc property is missing', async () => { + const response = await request(extendedServer) + .post('/') + .send({ + id: 1, + method: 'eth_getCode', + params: [testAddress, 'latest'] + }); + + expect(response.status).toBe(200); + expect(response.body).toBeDefined(); + expect(response.body.jsonrpc).toBe('2.0'); + expect(response.body.id).toBe(null); + expect(response.body.error).toBeDefined(); + expect(response.body.error.code).toBe(-32600); + expect(response.body.error.message).toBe('Invalid request'); + }); + + it('should return no response if id property is missing', async () => { + const response = await request(extendedServer) + .post('/') + .send({ + jsonrpc: '2.0', + method: 'eth_getCode', + params: [testAddress, 'latest'] + }); + + expect(response.status).toBe(204); + }); + + // it('should return an error if block parameter is invalid', async () => { + // const response = await request(extendedServer) + // .post('/') + // .send({ + // jsonrpc: '2.0', + // id: 1, + // method: 'eth_getCode', + // params: [testAddress, 'invalidBlock'] + // }); + + // expect(response.status).toBe(200); + // expect(response.body).toBeDefined(); + // expect(response.body.jsonrpc).toBe('2.0'); + // expect(response.body.id).toBe(1); + // expect(response.body.error).toBeDefined(); + // expect(response.body.error.code).toBe(-32602); + // expect(response.body.error.message).toBe('Invalid params'); + // }); +}); diff --git a/src/__tests__/integration/ethGetUncleCountByBlockHash.test.ts b/src/__tests__/integration/ethGetUncleCountByBlockHash.test.ts new file mode 100644 index 00000000..a1c3fec5 --- /dev/null +++ b/src/__tests__/integration/ethGetUncleCountByBlockHash.test.ts @@ -0,0 +1,67 @@ +import request from 'supertest'; +import { extendedServer } from '../../server'; + +describe('JSON-RPC Methods - eth_getUncleCountByBlockHash', () => { + let blockHash: any; + + // Fetch a block hash before running the tests + beforeAll(async () => { + const response = await request(extendedServer) + .post('/') + .send({ + jsonrpc: '2.0', + id: 1, + method: 'eth_getBlockByNumber', + params: ['latest', false] + }); + + blockHash = response.body.result.hash; + }); + + it('should return the number of uncles for a valid block hash', async () => { + const response = await request(extendedServer) + .post('/') + .send({ + jsonrpc: '2.0', + id: 1, + method: 'eth_getUncleCountByBlockHash', + params: [blockHash] + }); + + expect(response.status).toBe(200); + expect(response.body).toBeDefined(); + expect(response.body.jsonrpc).toBe('2.0'); + expect(response.body.id).toBe(1); + expect(response.body.result).toMatch(/^0x[0-9a-fA-F]+$/); + }); + + it('should return an error if jsonrpc property is missing', async () => { + const response = await request(extendedServer) + .post('/') + .send({ + id: 1, + method: 'eth_getUncleCountByBlockHash', + params: [blockHash] + }); + + expect(response.status).toBe(200); + expect(response.body).toBeDefined(); + expect(response.body.jsonrpc).toBe('2.0'); + expect(response.body.id).toBe(null); + expect(response.body.error).toBeDefined(); + expect(response.body.error.code).toBe(-32600); + expect(response.body.error.message).toBe('Invalid request'); + }); + + it('should return no response if id property is missing', async () => { + const response = await request(extendedServer) + .post('/') + .send({ + jsonrpc: '2.0', + method: 'eth_getUncleCountByBlockHash', + params: [blockHash] + }); + + expect(response.status).toBe(204); + }); +}); diff --git a/src/__tests__/integration/ethGetUncleCountByBlockNumber.test.ts b/src/__tests__/integration/ethGetUncleCountByBlockNumber.test.ts new file mode 100644 index 00000000..75efcd57 --- /dev/null +++ b/src/__tests__/integration/ethGetUncleCountByBlockNumber.test.ts @@ -0,0 +1,68 @@ +import request from 'supertest'; +import { extendedServer } from '../../server'; + +describe('JSON-RPC Methods - eth_getUncleCountByBlockNumber', () => { + let blockNumber: any; + + // Fetch the latest block number before running the tests + beforeAll(async () => { + const response = await request(extendedServer) + .post('/') + .send({ + jsonrpc: '2.0', + id: 1, + method: 'eth_blockNumber', + params: [] + }); + + blockNumber = response.body.result; + }); + + it('should return the number of uncles for a valid block number', async () => { + const response = await request(extendedServer) + .post('/') + .send({ + jsonrpc: '2.0', + id: 1, + method: 'eth_getUncleCountByBlockNumber', + params: [blockNumber] + }); + + expect(response.status).toBe(200); + expect(response.body).toBeDefined(); + expect(response.body.jsonrpc).toBe('2.0'); + expect(response.body.id).toBe(1); + expect(response.body.result).toMatch(/^0x[0-9a-fA-F]+$/); + }); + + it('should return an error if jsonrpc property is missing', async () => { + const response = await request(extendedServer) + .post('/') + .send({ + id: 1, + method: 'eth_getUncleCountByBlockNumber', + params: [blockNumber] + }); + + expect(response.status).toBe(200); + expect(response.body).toBeDefined(); + expect(response.body.jsonrpc).toBe('2.0'); + expect(response.body.id).toBe(null); + expect(response.body.error).toBeDefined(); + expect(response.body.error.code).toBe(-32600); + expect(response.body.error.message).toBe('Invalid request'); + }); + + it('should return no response if id property is missing', async () => { + const response = await request(extendedServer) + .post('/') + .send({ + jsonrpc: '2.0', + method: 'eth_getUncleCountByBlockNumber', + params: [blockNumber] + }); + + expect(response.status).toBe(204); + }); + +}); diff --git a/src/__tests__/integration/ethSendRawTransaction.test.ts b/src/__tests__/integration/ethSendRawTransaction.test.ts new file mode 100644 index 00000000..4219644e --- /dev/null +++ b/src/__tests__/integration/ethSendRawTransaction.test.ts @@ -0,0 +1,52 @@ +import request from 'supertest'; +import { extendedServer } from '../../server'; + +describe('JSON-RPC Methods - eth_sendRawTransaction', () => { + it('should send a valid raw transaction and return the transaction hash', async () => { + const response = await request(extendedServer) + .post('/') + .send({ + jsonrpc: '2.0', + id: 1, + method: 'eth_sendRawTransaction', + params: ['0xf86c808609184e72a00082520894c5223533feb845fd28717a7813a72af4df5f2751872386f26fc100008026a0126b583b8b05b1b2a3b548fd08553769d365b833ca980d4d7ccf6ba84458353ea031470ab76cd14c1725c58eb947a69a285186714894d632655718a35fa0435231'] // This is a valid raw transaction, replace with your own + }); + + expect(response.status).toBe(200); + expect(response.body).toBeDefined(); + expect(response.body.jsonrpc).toBe('2.0'); + expect(response.body.id).toBe(1); + expect(response.body.result).toMatch(/^0x[0-9a-fA-F]+$/); + console.log('Transaction hash:', response.body.result); + }); + + it('should return an error if jsonrpc property is missing', async () => { + const response = await request(extendedServer) + .post('/') + .send({ + id: 1, + method: 'eth_sendRawTransaction', + params: ['0xf86c808609184e72a00082520894c5223533feb845fd28717a7813a72af4df5f2751872386f26fc100008026a0126b583b8b05b1b2a3b548fd08553769d365b833ca980d4d7ccf6ba84458353ea031470ab76cd14c1725c58eb947a69a285186714894d632655718a35fa0435231'] // This is a valid raw transaction, replace with your own + }); + + expect(response.status).toBe(200); + expect(response.body).toBeDefined(); + expect(response.body.jsonrpc).toBe('2.0'); + expect(response.body.id).toBe(null); + expect(response.body.error).toBeDefined(); + expect(response.body.error.code).toBe(-32600); + expect(response.body.error.message).toBe('Invalid request'); + }); + + it('should return no response if id property is missing', async () => { + const response = await request(extendedServer) + .post('/') + .send({ + jsonrpc: '2.0', + method: 'eth_sendRawTransaction', + params: ['0xf86c808609184e72a00082520894c5223533feb845fd28717a7813a72af4df5f2751872386f26fc100008026a0126b583b8b05b1b2a3b548fd08553769d365b833ca980d4d7ccf6ba84458353ea031470ab76cd14c1725c58eb947a69a285186714894d632655718a35fa0435231'] // This is a valid raw transaction, replace with your own + }); + expect(response.status).toBe(204); + }); +}); + diff --git a/src/__tests__/integration/ethSendTransaction.test.ts b/src/__tests__/integration/ethSendTransaction.test.ts new file mode 100644 index 00000000..eef87446 --- /dev/null +++ b/src/__tests__/integration/ethSendTransaction.test.ts @@ -0,0 +1,116 @@ +import request from 'supertest'; +import { extendedServer } from '../../server'; +const { Transaction } = require('ethereumjs-tx'); + +// Helper function to make JSON-RPC calls +async function jsonRpcRequest(method: any, params: any) { + try { + const response = await request(extendedServer) + .post('/') + .send({ + jsonrpc: '2.0', + id: 1, + method: method, + params: params + }); + return response.body; + } catch (error) { + console.error(`Error in JSON-RPC request: ${error}`); + throw error; + } +} + +describe('JSON-RPC Methods - eth_sendTransaction', () => { + describe('eth_sendTransaction', () => { + it('should send a transaction and return the transaction hash', async () => { + // Step 1: Get the nonce + const nonceResult = await jsonRpcRequest('eth_getTransactionCount', ['0x1923A1Eb8e4dA49604aFfd34De1B478580cf8698', 'latest']); + const nonce = nonceResult.result; + console.log(`Nonce: ${nonce}`); + + // Step 2: Create the transaction object + const txParams = { + nonce: nonce, + gasPrice: '0x09184e72a000', // 20 Gwei + gasLimit: '0x5208', // 21000 + to: '0xC5223533feB845fD28717A7813a72af4df5F2751', + value: '0x2386f26fc10000', // 0.01 Ether in hex + data: '0x', // Empty data field + chainId: 8082, + }; + + // Step 3: Create a new transaction and sign it + const tx = new Transaction(txParams); + const senderPrivateKey = Buffer.from('TEST_WITH_YOUR_PRIVATE_KEY', 'hex'); + tx.sign(senderPrivateKey); + + // Step 4: Serialize the transaction + const serializedTx = tx.serialize(); + const rawTx = '0x' + serializedTx.toString('hex'); + console.log(`Raw Transaction: ${rawTx}`); + + // Step 5: Send the signed transaction + const response = await request(extendedServer) + .post('/') + .send({ + jsonrpc: '2.0', + id: 1, + method: 'eth_sendRawTransaction', + params: [rawTx] + }); + + expect(response.status).toBe(200); + expect(response.body).toHaveProperty('result'); + console.log('Transaction hash:', response.body.result); + }); + + it('should return an error if jsonrpc property is missing', async () => { + const response = await request(extendedServer) + .post('/') + .send({ + id: 2, + method: 'eth_sendTransaction', + params: [ + { + from: '0x1923A1Eb8e4dA49604aFfd34De1B478580cf8698', + to: '0xC5223533feB845fD28717A7813a72af4df5F2751', + gas: '0x5208', + gasPrice: '0x09184e72a000', + value: '0x2386f26fc10000', + data: '0x' + } + ] + }); + + expect(response.status).toBe(200); + expect(response.body).toBeDefined(); + expect(response.body.jsonrpc).toBe('2.0'); + expect(response.body.id).toBe(null); + expect(response.body.error).toBeDefined(); + expect(response.body.error.code).toBe(-32600); + expect(response.body.error.message).toBe('Invalid request'); + }); + + it('should return no response if id property is missing', async () => { + const response = await request(extendedServer) + .post('/') + .send({ + jsonrpc: '2.0', + method: 'eth_sendTransaction', + params: [ + { + from: '0x1923A1Eb8e4dA49604aFfd34De1B478580cf8698', + to: '0xC5223533feB845fD28717A7813a72af4df5F2751', + gas: '0x5208', + gasPrice: '0x09184e72a000', + value: '0x2386f26fc10000', + data: '0x' + } + ] + }); + + expect(response.status).toBe(204); + }); + }); + +}); \ No newline at end of file