Skip to content

Commit

Permalink
feat: added support for eth_getTransactionReceipt() to the WS server (#…
Browse files Browse the repository at this point in the history
…2274) (#2297)

* fix: updated handleSendingTransactionRequests

Signed-off-by: Logan Nguyen <[email protected]>

* feat: added support for getTransactionReceipt to the WS server

Signed-off-by: Logan Nguyen <[email protected]>

* fix: removed socketId from logs

Signed-off-by: Logan Nguyen <[email protected]>

* fix: removed validateParamsLength method

Signed-off-by: Logan Nguyen <[email protected]>

* fix: reworked handleSendingRequestsToRelay logic

Signed-off-by: Logan Nguyen <[email protected]>

* fix: fixed conflicts with getCode & estimateGas

Signed-off-by: Logan Nguyen <[email protected]>

* fix: disconnect wsProvider after test

Signed-off-by: Logan Nguyen <[email protected]>

* fix: removed timer after connecting and disconnecting wsProvider

Signed-off-by: Logan Nguyen <[email protected]>

Revert "fix: removed timer after connecting and disconnecting wsProvider"

This reverts commit 8cc42b8.

a

Signed-off-by: Logan Nguyen <[email protected]>

---------

Signed-off-by: Logan Nguyen <[email protected]>
  • Loading branch information
quiet-node authored Apr 10, 2024
1 parent 937ad66 commit 7e18c53
Show file tree
Hide file tree
Showing 16 changed files with 295 additions and 223 deletions.
12 changes: 9 additions & 3 deletions packages/server/tests/acceptance/ws/estimateGas.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@
*/

// external resources
import { expect } from 'chai';
import { Contract, ethers, JsonRpcProvider, WebSocketProvider } from 'ethers';
import { AliasAccount } from '../../clients/servicesClient';
import WebSocket from 'ws';
import { expect } from 'chai';
import basicContractJson from '../../contracts/Basic.json';
import { AliasAccount } from '../../clients/servicesClient';
import { Contract, ethers, JsonRpcProvider, WebSocketProvider } from 'ethers';

describe('@release @web-socket eth_estimateGas', async function () {
// @ts-ignore
Expand Down Expand Up @@ -58,6 +58,12 @@ describe('@release @web-socket eth_estimateGas', async function () {
gasPriceDeviation = parseFloat(expectedGas.toString() ?? '0.2');
});

afterEach(async () => {
if (wsProvider) {
await wsProvider.destroy();
}
});

it('@release should execute "eth_estimateGas" for contract call, using a websocket provider', async function () {
const estimatedGas = await wsProvider.estimateGas({
to: `0x${basicContract.contractId.toSolidityAddress()}`,
Expand Down
6 changes: 6 additions & 0 deletions packages/server/tests/acceptance/ws/getCode.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,12 @@ describe('@release @web-socket eth_getCode', async function () {
webSocket = new WebSocket(WS_RELAY_URL);
});

afterEach(async () => {
if (wsProvider) {
await wsProvider.destroy();
}
});

it('should return the code ethers WebSocketProvider', async function () {
await new Promise((resolve) => setTimeout(resolve, 1000));
const codeFromWs = await wsProvider.getCode(`0x${basicContract.contractId.toSolidityAddress()}`);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,11 @@ describe('@release @web-socket eth_getTransactionByHash', async function () {

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));
}
});

Expand Down
117 changes: 117 additions & 0 deletions packages/server/tests/acceptance/ws/getTransactionReceipt.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/*-
*
* 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_getTransactionReceipt', async function () {
const WS_RELAY_URL = `${process.env.WS_RELAY_URL}`;
const METHOD_NAME = 'eth_getTransactionReceipt';
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);
});

afterEach(async () => {
if (wsProvider) {
await wsProvider.destroy();
}
});

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.contractAddress).to.be.eq(expectedTxReceipt.address);
expect(txReceipt.transactionHash).to.be.eq(expectedTxReceipt.hash);
expect(Number(txReceipt.transactionIndex)).to.be.eq(expectedTxReceipt.transaction_index);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,11 @@ describe('@release @web-socket eth_sendRawTransaction', async function () {

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));
}
});

Expand Down
43 changes: 13 additions & 30 deletions packages/ws-server/src/controllers/eth_estimateGas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,25 +18,21 @@
*
*/

import { sendToClient } from '../utils/utils';
import { Relay } from '@hashgraph/json-rpc-relay';
import { validateParamsLength } from '../utils/validators';
import { handleSendingTransactionRequests } from './helpers';
import { handleSendingRequestsToRelay } from './helpers';
import { predefined, Relay } from '@hashgraph/json-rpc-relay';

/**
* Handles the "eth_estimateGas" 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.
* Handles the "eth_estimateGas" method request by estimating the gas needed to execute a transaction.
* Validates the parameters, retrieves the gas estimation using the relay object, 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[]} params - The parameters of the method request.
* @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<any>} 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.
* @throws {JsonRpcError} Throws a JsonRpcError if the method parameters are invalid or an internal error occurs.
*/
export const handleEthEstimateGas = async (
ctx: any,
Expand All @@ -45,40 +41,27 @@ export const handleEthEstimateGas = async (
relay: Relay,
request: any,
method: string,
socketIdPrefix: string,
requestIdPrefix: string,
connectionIdPrefix: string,
) => {
if (params.length !== 1) {
throw predefined.INVALID_PARAMETERS;
}

const TXN = params[0];
const TAG = JSON.stringify({ method, txn: TXN });

validateParamsLength(
ctx,
params,
method,
TAG,
logger,
sendToClient,
1,
socketIdPrefix,
requestIdPrefix,
connectionIdPrefix,
);

logger.info(
`${connectionIdPrefix} ${requestIdPrefix} ${socketIdPrefix}: Retrieving estimated gas with txn=${TXN} for tag=${TAG}`,
);
logger.info(`${connectionIdPrefix} ${requestIdPrefix}: Retrieving gas estimation for tag=${TAG}`);

return handleSendingTransactionRequests(
await handleSendingRequestsToRelay(
ctx,
TAG,
params,
[TXN, requestIdPrefix],
relay,
logger,
request,
method,
'estimateGas',
socketIdPrefix,
requestIdPrefix,
connectionIdPrefix,
);
Expand Down
46 changes: 15 additions & 31 deletions packages/ws-server/src/controllers/eth_getCode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,25 +18,21 @@
*
*/

import { sendToClient } from '../utils/utils';
import { Relay } from '@hashgraph/json-rpc-relay';
import { validateParamsLength } from '../utils/validators';
import { handleSendingTransactionRequests } from './helpers';
import { handleSendingRequestsToRelay } from './helpers';
import { predefined, Relay } from '@hashgraph/json-rpc-relay';

/**
* Handles the "eth_getCode" 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.
* Handles the "eth_getCode" method request by retrieving the code at a specific address on the Hedera network.
* Validates the parameters, retrieves the contract code using the relay object, 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[]} params - The parameters of the method request, expecting an address and a block parameter.
* @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<any>} 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.
* @throws {JsonRpcError} Throws a JsonRpcError if the method parameters are invalid or an internal error occurs.
*/
export const handleEthGetCode = async (
ctx: any,
Expand All @@ -45,40 +41,28 @@ export const handleEthGetCode = async (
relay: Relay,
request: any,
method: string,
socketIdPrefix: string,
requestIdPrefix: string,
connectionIdPrefix: string,
) => {
const ADDRESS = params[0];
const TAG = JSON.stringify({ method, address: ADDRESS });
if (params.length !== 2) {
throw predefined.INVALID_PARAMETERS;
}

validateParamsLength(
ctx,
params,
method,
TAG,
logger,
sendToClient,
2,
socketIdPrefix,
requestIdPrefix,
connectionIdPrefix,
);
const ADDRESS = params[0];
const BLOCK_TAG = params[1];
const TAG = JSON.stringify({ method, address: ADDRESS, block: BLOCK_TAG });

logger.info(
`${connectionIdPrefix} ${requestIdPrefix} ${socketIdPrefix}: Retrieving code with address=${ADDRESS} for tag=${TAG}`,
);
logger.info(`${connectionIdPrefix} ${requestIdPrefix}: Retrieving contract code for tag=${TAG}`);

return handleSendingTransactionRequests(
await handleSendingRequestsToRelay(
ctx,
TAG,
params,
[ADDRESS, BLOCK_TAG, requestIdPrefix],
relay,
logger,
request,
method,
'getCode',
socketIdPrefix,
requestIdPrefix,
connectionIdPrefix,
);
Expand Down
Loading

0 comments on commit 7e18c53

Please sign in to comment.