From 967056ce08c9e964a5c6dcf440ffbfd4f4e3dc2c Mon Sep 17 00:00:00 2001 From: Logan Nguyen Date: Fri, 5 Apr 2024 17:32:29 -0400 Subject: [PATCH] feat: added support for getTransactionByHash endpoint in the WS server (#2273) (#2295) * refactor: refactored validateParamsLength Signed-off-by: Logan Nguyen * feat: added support for getTransactionByHas endpoint in the WS server Signed-off-by: Logan Nguyen * refactor: refactored sending requests method Signed-off-by: Logan Nguyen --------- Signed-off-by: Logan Nguyen --- .../ws/getTransactionByHash.spec.ts | 120 ++++++++++++++++++ .../src/controllers/eth_getTransaction.ts | 85 +++++++++++++ .../src/controllers/eth_sendRawTransaction.ts | 61 ++++----- packages/ws-server/src/controllers/helpers.ts | 81 ++++++++++++ packages/ws-server/src/controllers/index.ts | 3 +- packages/ws-server/src/utils/constants.ts | 1 + packages/ws-server/src/utils/validators.ts | 36 ++++++ packages/ws-server/src/webSocketServer.ts | 21 ++- 8 files changed, 372 insertions(+), 36 deletions(-) create mode 100644 packages/server/tests/acceptance/ws/getTransactionByHash.spec.ts create mode 100644 packages/ws-server/src/controllers/eth_getTransaction.ts create mode 100644 packages/ws-server/src/controllers/helpers.ts diff --git a/packages/server/tests/acceptance/ws/getTransactionByHash.spec.ts b/packages/server/tests/acceptance/ws/getTransactionByHash.spec.ts new file mode 100644 index 0000000000..cc01e058b9 --- /dev/null +++ b/packages/server/tests/acceptance/ws/getTransactionByHash.spec.ts @@ -0,0 +1,120 @@ +/*- + * + * Hedera JSON RPC Relay + * + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +// external resources +import { expect } from 'chai'; +import { ethers, WebSocketProvider } from 'ethers'; +import RelayClient from '../../clients/relayClient'; +import MirrorClient from '../../clients/mirrorClient'; +import { AliasAccount } from '../../clients/servicesClient'; +import { numberTo0x } from '@hashgraph/json-rpc-relay/src/formatters'; +import { ONE_TINYBAR_IN_WEI_HEX } from '@hashgraph/json-rpc-relay/tests/lib/eth/eth-config'; + +describe('@release @web-socket eth_getTransactionByHash', async function () { + const WS_RELAY_URL = `${process.env.WS_RELAY_URL}`; + const METHOD_NAME = 'eth_getTransactionByHash'; + const CHAIN_ID = process.env.CHAIN_ID || '0x12a'; + const INVALID_PARAMS = [['hedera', 'hbar'], [], ['websocket', 'rpc', 'invalid']]; + const INVALID_TX_HASH = ['0xhbar', '0xHedera', '', 66, 'abc', true, false, 39]; + + let accounts: AliasAccount[] = []; + let mirrorNodeServer: MirrorClient, requestId: string, relayClient: RelayClient, wsProvider: WebSocketProvider; + + before(async () => { + // @ts-ignore + const { servicesNode, mirrorNode, relay } = global; + + mirrorNodeServer = mirrorNode; + relayClient = relay; + + accounts[0] = await servicesNode.createAliasAccount(100, relay.provider, requestId); + accounts[1] = await servicesNode.createAliasAccount(5, relay.provider, requestId); + }); + + beforeEach(async () => { + wsProvider = new ethers.WebSocketProvider(WS_RELAY_URL); + await new Promise((resolve) => setTimeout(resolve, 1000)); + }); + + afterEach(async () => { + if (wsProvider) { + await wsProvider.destroy(); + await new Promise((resolve) => setTimeout(resolve, 1000)); + } + }); + + for (const params of INVALID_PARAMS) { + it(`Should throw predefined.INVALID_PARAMETERS if the request's params variable is invalid (params.length !== 1). params=[${params}]`, async () => { + try { + await wsProvider.send(METHOD_NAME, params); + expect(true).to.eq(false); + } catch (error) { + expect(error.error).to.exist; + expect(error.error.code).to.eq(-32602); + expect(error.error.name).to.eq('Invalid parameters'); + expect(error.error.message).to.eq('Invalid params'); + } + }); + } + + for (const txHash of INVALID_TX_HASH) { + it(`Should handle invalid data correctly. txHash = ${txHash}`, async () => { + try { + const res = await wsProvider.send(METHOD_NAME, [txHash]); + if (txHash === '') { + expect(res).to.be.null; + } else { + expect(true).to.eq(false); + } + } catch (error) { + expect(error.error.code).to.eq(-32603); + expect(error.error.name).to.eq(`Internal error`); + expect(error.error.message).to.eq( + 'Error invoking RPC: "Invalid Transaction id. Please use \\"shard.realm.num-sss-nnn\\" format where sss are seconds and nnn are nanoseconds"', + ); + } + }); + } + + it('Should handle valid data correctly', async () => { + const tx = { + value: ONE_TINYBAR_IN_WEI_HEX, + gasLimit: numberTo0x(30000), + chainId: Number(CHAIN_ID), + to: accounts[1].address, + nonce: await relayClient.getAccountNonce(accounts[0].address, requestId), + maxFeePerGas: await relayClient.gasPrice(requestId), + }; + + const signedTx = await accounts[0].wallet.signTransaction(tx); + const txHash = await relayClient.sendRawTransaction(signedTx, requestId); + const expectedTxReceipt = await mirrorNodeServer.get(`/contracts/results/${txHash}`); + + const txReceipt = await wsProvider.send(METHOD_NAME, [txHash]); + + expect(txReceipt.from).to.be.eq(accounts[0].address); + expect(txReceipt.to).to.be.eq(accounts[1].address); + expect(txReceipt.blockHash).to.be.eq(expectedTxReceipt.block_hash.slice(0, 66)); + expect(txReceipt.hash).to.be.eq(expectedTxReceipt.hash); + expect(txReceipt.r).to.be.eq(expectedTxReceipt.r); + expect(txReceipt.s).to.be.eq(expectedTxReceipt.s); + expect(Number(txReceipt.v)).to.be.eq(expectedTxReceipt.v); + }); +}); diff --git a/packages/ws-server/src/controllers/eth_getTransaction.ts b/packages/ws-server/src/controllers/eth_getTransaction.ts new file mode 100644 index 0000000000..ea0840d621 --- /dev/null +++ b/packages/ws-server/src/controllers/eth_getTransaction.ts @@ -0,0 +1,85 @@ +/* - + * + * Hedera JSON RPC Relay + * + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import { sendToClient } from '../utils/utils'; +import { Relay } from '@hashgraph/json-rpc-relay'; +import { validateParamsLength } from '../utils/validators'; +import { handleSendingTransactionRequests } from './helpers'; + +/** + * Handles the "eth_getTransactionByHash" method request by retrieving transaction details from the Hedera network. + * Validates the parameters, retrieves the transaction details, and sends the response back to the client. + * @param {any} ctx - The context object containing information about the WebSocket connection. + * @param {any[]} params - The parameters of the method request, expecting a single parameter: the transaction hash. + * @param {any} logger - The logger object for logging messages and events. + * @param {Relay} relay - The relay object for interacting with the Hedera network. + * @param {any} request - The request object received from the client. + * @param {string} method - The JSON-RPC method associated with the request. + * @param {string} socketIdPrefix - The prefix for the socket ID. + * @param {string} requestIdPrefix - The prefix for the request ID. + * @param {string} connectionIdPrefix - The prefix for the connection ID. + * @returns {Promise} Returns a promise that resolves with the JSON-RPC response to the client. + * @throws {JsonRpcError} Throws a JsonRpcError if there is an issue with the parameters or an internal error occurs. + */ +export const handleEthGetTransactionByHash = async ( + ctx: any, + params: any, + logger: any, + relay: Relay, + request: any, + method: string, + socketIdPrefix: string, + requestIdPrefix: string, + connectionIdPrefix: string, +) => { + const TX_HASH = params[0]; + const TAG = JSON.stringify({ method, signedTx: TX_HASH }); + + validateParamsLength( + ctx, + params, + method, + TAG, + logger, + sendToClient, + 1, + socketIdPrefix, + requestIdPrefix, + connectionIdPrefix, + ); + + logger.info( + `${connectionIdPrefix} ${requestIdPrefix} ${socketIdPrefix}: Retrieving transaction info with txHash=${TX_HASH} for tag=${TAG}`, + ); + + return handleSendingTransactionRequests( + ctx, + TAG, + TX_HASH, + relay, + logger, + request, + method, + 'getTransactionByHash', + socketIdPrefix, + requestIdPrefix, + connectionIdPrefix, + ); +}; diff --git a/packages/ws-server/src/controllers/eth_sendRawTransaction.ts b/packages/ws-server/src/controllers/eth_sendRawTransaction.ts index 71343da10c..f6bc29dbb4 100644 --- a/packages/ws-server/src/controllers/eth_sendRawTransaction.ts +++ b/packages/ws-server/src/controllers/eth_sendRawTransaction.ts @@ -20,8 +20,8 @@ import { sendToClient } from '../utils/utils'; import { Relay } from '@hashgraph/json-rpc-relay'; -import { predefined } from '@hashgraph/json-rpc-relay'; -import jsonResp from '@hashgraph/json-rpc-server/dist/koaJsonRpc/lib/RpcResponse'; +import { validateParamsLength } from '../utils/validators'; +import { handleSendingTransactionRequests } from './helpers'; /** * Handles the "eth_sendRawTransaction" method request by submitting a raw transaction to the Websocket server. @@ -52,40 +52,33 @@ export const handleEthSendRawTransaction = async ( const SIGNED_TX = params[0]; const TAG = JSON.stringify({ method, signedTx: SIGNED_TX }); - if (params.length !== 1) { - const ERR_MSG = 'INVALID PARAMETERS'; - logger.error(`${connectionIdPrefix} ${requestIdPrefix} ${socketIdPrefix}: Invalid parameters ${params}`); - sendToClient(ctx.websocket, method, ERR_MSG, TAG, logger, socketIdPrefix, requestIdPrefix, connectionIdPrefix); - throw predefined.INVALID_PARAMETERS; - } - + validateParamsLength( + ctx, + params, + method, + TAG, + logger, + sendToClient, + 1, + socketIdPrefix, + requestIdPrefix, + connectionIdPrefix, + ); logger.info( `${connectionIdPrefix} ${requestIdPrefix} ${socketIdPrefix}: Submitting raw transaction with signedTx=${SIGNED_TX} for tag=${TAG}`, ); - try { - const txRes = await relay.eth().sendRawTransaction(SIGNED_TX, requestIdPrefix); - if (txRes) { - sendToClient(ctx.websocket, method, txRes, TAG, logger, socketIdPrefix, requestIdPrefix, connectionIdPrefix); - } else { - logger.error( - `${connectionIdPrefix} ${requestIdPrefix} ${socketIdPrefix}: Fail to retrieve result for tag=${TAG}`, - ); - } - - return jsonResp(request.id, null, txRes); - } catch (error: any) { - sendToClient( - ctx.websocket, - method, - JSON.stringify(error.message || error), - TAG, - logger, - socketIdPrefix, - requestIdPrefix, - connectionIdPrefix, - ); - - throw predefined.INTERNAL_ERROR(JSON.stringify(error.message || error)); - } + return handleSendingTransactionRequests( + ctx, + TAG, + SIGNED_TX, + relay, + logger, + request, + method, + 'sendRawTransaction', + socketIdPrefix, + requestIdPrefix, + connectionIdPrefix, + ); }; diff --git a/packages/ws-server/src/controllers/helpers.ts b/packages/ws-server/src/controllers/helpers.ts new file mode 100644 index 0000000000..00678eb6a9 --- /dev/null +++ b/packages/ws-server/src/controllers/helpers.ts @@ -0,0 +1,81 @@ +/* - + * + * Hedera JSON RPC Relay + * + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +import { sendToClient } from '../utils/utils'; +import { Relay } from '@hashgraph/json-rpc-relay'; +import { predefined } from '@hashgraph/json-rpc-relay'; +import jsonResp from '@hashgraph/json-rpc-server/dist/koaJsonRpc/lib/RpcResponse'; + +/** + * Handles sending transaction-related requests to the Hedera network, such as sending raw transactions or getting transaction information. + * Executes the specified Hedera RPC call endpoint with the provided argument, retrieves the response, and sends it back to the client. + * @param {any} ctx - The context object containing information about the WebSocket connection. + * @param {string} tag - A tag used for logging and identifying the message. + * @param {string} arg - The argument required for the Hedera RPC call. + * @param {Relay} relay - The relay object for interacting with the Hedera network. + * @param {any} logger - The logger object for logging messages and events. + * @param {any} request - The request object received from the client. + * @param {string} method - The JSON-RPC method associated with the request. + * @param {string} rpcCallEndpoint - The Hedera RPC call endpoint to execute. + * @param {string} socketIdPrefix - The prefix for the socket ID. + * @param {string} requestIdPrefix - The prefix for the request ID. + * @param {string} connectionIdPrefix - The prefix for the connection ID. + * @returns {Promise} Returns a promise that resolves with the JSON-RPC response to the client. + * @throws {JsonRpcError} Throws a JsonRpcError if there is an issue with the Hedera RPC call or an internal error occurs. + */ +export const handleSendingTransactionRequests = async ( + ctx: any, + tag: string, + arg: string, + relay: Relay, + logger: any, + request: any, + method: string, + rpcCallEndpoint: string, + socketIdPrefix: string, + requestIdPrefix: string, + connectionIdPrefix: string, +): Promise => { + try { + const txRes = await relay.eth()[rpcCallEndpoint](arg, requestIdPrefix); + + if (txRes) { + sendToClient(ctx.websocket, method, txRes, tag, logger, socketIdPrefix, requestIdPrefix, connectionIdPrefix); + } else { + logger.error( + `${connectionIdPrefix} ${requestIdPrefix} ${socketIdPrefix}: Fail to retrieve result for tag=${tag}`, + ); + } + + return jsonResp(request.id, null, txRes); + } catch (error: any) { + sendToClient( + ctx.websocket, + method, + JSON.stringify(error.message || error), + tag, + logger, + socketIdPrefix, + requestIdPrefix, + connectionIdPrefix, + ); + + throw predefined.INTERNAL_ERROR(JSON.stringify(error.message || error)); + } +}; diff --git a/packages/ws-server/src/controllers/index.ts b/packages/ws-server/src/controllers/index.ts index e09a40683e..cac9ce29f0 100644 --- a/packages/ws-server/src/controllers/index.ts +++ b/packages/ws-server/src/controllers/index.ts @@ -20,6 +20,7 @@ import { handleEthSubsribe } from './eth_subscribe'; import { handleEthUnsubscribe } from './eth_unscribe'; +import { handleEthGetTransactionByHash } from './eth_getTransaction'; import { handleEthSendRawTransaction } from './eth_sendRawTransaction'; -export { handleEthUnsubscribe, handleEthSubsribe, handleEthSendRawTransaction }; +export { handleEthUnsubscribe, handleEthSubsribe, handleEthSendRawTransaction, handleEthGetTransactionByHash }; diff --git a/packages/ws-server/src/utils/constants.ts b/packages/ws-server/src/utils/constants.ts index 3cba469f33..79bab83d7a 100644 --- a/packages/ws-server/src/utils/constants.ts +++ b/packages/ws-server/src/utils/constants.ts @@ -58,5 +58,6 @@ export const WS_CONSTANTS = { ETH_UNSUBSCRIBE: 'eth_unsubscribe', ETH_CHAIN_ID: 'eth_chainId', ETH_SEND_RAW_TRANSACTION: 'eth_sendRawTransaction', + ETH_GET_TRANSACTION_BY_HASH: 'eth_getTransactionByHash', }, }; diff --git a/packages/ws-server/src/utils/validators.ts b/packages/ws-server/src/utils/validators.ts index e728864bcb..074488228a 100644 --- a/packages/ws-server/src/utils/validators.ts +++ b/packages/ws-server/src/utils/validators.ts @@ -80,3 +80,39 @@ export const validateSubscribeEthLogsParams = async ( } } }; + +/** + * Validates the length of parameters received in a JSON-RPC method request. + * If the length does not match the expected length, logs an error, sends an error response to the client, + * and throws an exception for invalid parameters. + * @param {any} ctx - The context object containing information about the WebSocket connection. + * @param {any[]} params - The parameters of the method request. + * @param {string} method - The JSON-RPC method associated with the request. + * @param {string} tag - A tag used for logging and identifying the message. + * @param {any} logger - The logger object for logging messages and events. + * @param {any} sendToClient - The function for sending responses to the client. + * @param {number} expectedLength - The expected length of parameters for the method request. + * @param {string} socketIdPrefix - The prefix for the socket ID. + * @param {string} requestIdPrefix - The prefix for the request ID. + * @param {string} connectionIdPrefix - The prefix for the connection ID. + * @throws {JsonRpcError} Throws a JsonRpcError if the length of parameters does not match the expected length. + */ +export const validateParamsLength = ( + ctx: any, + params: any, + method: string, + tag: string, + logger: any, + sendToClient: any, + expectedLength: number, + socketIdPrefix: string, + requestIdPrefix: string, + connectionIdPrefix: string, +) => { + if (params.length !== expectedLength) { + const ERR_MSG = 'INVALID PARAMETERS'; + logger.error(`${connectionIdPrefix} ${requestIdPrefix} ${socketIdPrefix}: Invalid parameters ${params}`); + sendToClient(ctx.websocket, method, ERR_MSG, tag, logger, socketIdPrefix, requestIdPrefix, connectionIdPrefix); + throw predefined.INVALID_PARAMETERS; + } +}; diff --git a/packages/ws-server/src/webSocketServer.ts b/packages/ws-server/src/webSocketServer.ts index f7eb4e8725..65ea7480c0 100644 --- a/packages/ws-server/src/webSocketServer.ts +++ b/packages/ws-server/src/webSocketServer.ts @@ -34,7 +34,12 @@ import KoaJsonRpc from '@hashgraph/json-rpc-server/dist/koaJsonRpc'; import jsonResp from '@hashgraph/json-rpc-server/dist/koaJsonRpc/lib/RpcResponse'; import { generateMethodsCounter, generateMethodsCounterById } from './utils/counters'; import { type Relay, RelayImpl, predefined, JsonRpcError } from '@hashgraph/json-rpc-relay'; -import { handleEthSendRawTransaction, handleEthSubsribe, handleEthUnsubscribe } from './controllers'; +import { + handleEthGetTransactionByHash, + handleEthSendRawTransaction, + handleEthSubsribe, + handleEthUnsubscribe, +} from './controllers'; const register = new Registry(); const pingInterval = Number(process.env.WS_PING_INTERVAL || 1000); @@ -169,6 +174,20 @@ app.ws.use(async (ctx) => { connectionIdPrefix, ); break; + + case WS_CONSTANTS.METHODS.ETH_GET_TRANSACTION_BY_HASH: + response = await handleEthGetTransactionByHash( + ctx, + params, + logger, + relay, + request, + method, + socketIdPrefix, + requestIdPrefix, + connectionIdPrefix, + ); + break; default: response = jsonResp(request.id, DEFAULT_ERROR, null); }