Skip to content

Commit 7cec5e9

Browse files
AlfredoG87ebadiere
andauthored
feat: Added support for newHeads include transactions. (#2227) (#2242)
feat: Updated documentation. Signed-off-by: ebadiere <[email protected]> Signed-off-by: Alfredo Gutierrez <[email protected]> Co-authored-by: Eric Badiere <[email protected]>
1 parent ce450f2 commit 7cec5e9

File tree

6 files changed

+217
-45
lines changed

6 files changed

+217
-45
lines changed

docs/design/eth-subscribe.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -286,12 +286,13 @@ Add acceptance tests that follow the structure of the existing polling for logs
286286
"extraData": "0xd983010305844765746887676f312e342e328777696e646f7773",
287287
"gasLimit": "0x47e7c4",
288288
"gasUsed": "0x38658",
289+
"hash":"0x50d2fe6747f21334213a8bc2691f02b6338f265e9f39f12c1f98f247e18f33aa",
289290
"logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
290291
"miner": "0xf8b483dba2c3b7176a3da549ad41a48bb3121069",
291292
"nonce": "0x084149998194cc5f",
292293
"number": "0x1348c9",
293294
"parentHash": "0x7736fab79e05dc611604d22470dadad26f56fe494421b5b333de816ce1f25701",
294-
"receiptRoot": "0x2fab35823ad00c7bb388595cb46652fe7886e00660a01e867824d3dceb1c8d36",
295+
"receiptsRoot": "0x2fab35823ad00c7bb388595cb46652fe7886e00660a01e867824d3dceb1c8d36",
295296
"sha3Uncles": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
296297
"stateRoot": "0xb3346685172db67de536d8765c43c31009d0eb3bd9c501c9be3229203f15f378",
297298
"timestamp": "0x56ffeff8",

packages/relay/src/lib/poller.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ export class Poller {
8484

8585
poll.lastPolled = this.latestBlock;
8686
} else if (event === this.NEW_HEADS_EVENT && process.env.WS_NEW_HEADS_ENABLED === 'true') {
87-
data = await this.eth.getBlockByNumber('latest', true);
87+
data = await this.eth.getBlockByNumber('latest', filters?.includeTransactions ?? false);
8888
data.jsonrpc = '2.0';
8989
poll.lastPolled = this.latestBlock;
9090
} else {

packages/relay/src/lib/subscriptionController.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@ export class SubscriptionController {
171171
this.resultsSentToSubscribersCounter.labels('sub.subscriptionId', tag).inc();
172172
sub.connection.send(
173173
JSON.stringify({
174+
jsonrpc: '2.0',
174175
method: 'eth_subscription',
175176
params: subscriptionData,
176177
}),

packages/server/tests/acceptance/ws/subscribe.spec.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
*
33
* Hedera JSON RPC Relay
44
*
5-
* Copyright (C) 2023 Hedera Hashgraph, LLC
5+
* Copyright (C) 2023-2024 Hedera Hashgraph, LLC
66
*
77
* Licensed under the Apache License, Version 2.0 (the "License");
88
* you may not use this file except in compliance with the License.
@@ -29,7 +29,6 @@ import assertions from '../../helpers/assertions';
2929
import { AliasAccount } from '../../clients/servicesClient';
3030
import { predefined, WebSocketError } from '../../../../../packages/relay';
3131
import { ethers } from 'ethers';
32-
import constants from '@hashgraph/json-rpc-relay/dist/lib/constants';
3332
import Assertions from '../../helpers/assertions';
3433
import LogContractJson from '../../contracts/Logs.json';
3534
import Constants from '../../helpers/constants';

packages/server/tests/acceptance/ws/subscribeNewHeads.spec.ts

Lines changed: 203 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -27,49 +27,130 @@ chai.use(solidity);
2727
import { ethers } from 'ethers';
2828
import Assertions from '../../helpers/assertions';
2929
import { predefined } from '@hashgraph/json-rpc-relay';
30+
import { AliasAccount } from '../../clients/servicesClient';
31+
import { numberTo0x } from '../../../../../packages/relay/src/formatters';
32+
import RelayCall from '../../../tests/helpers/constants';
33+
import { Utils } from '../../helpers/utils';
34+
import e from 'express';
3035
const WS_RELAY_URL = `${process.env.WS_RELAY_URL}`;
3136

32-
const createSubscription = (ws, subscriptionId) => {
33-
return new Promise((resolve, reject) => {
34-
ws.on('message', function incoming(data) {
35-
const response = JSON.parse(data);
36-
if (response.id === subscriptionId && response.result) {
37-
console.log(`Subscription ${subscriptionId} successful with ID: ${response.result}`);
38-
resolve(response.result); // Resolve with the subscription ID
39-
} else if (response.method === 'eth_subscription') {
40-
console.log(`Subscription ${subscriptionId} received block:`, response.params.result);
41-
// You can add more logic here to handle incoming blocks
42-
}
43-
});
37+
const ethAddressRegex = /^0x[a-fA-F0-9]*$/;
4438

45-
ws.on('open', function open() {
46-
ws.send(
47-
JSON.stringify({
48-
id: subscriptionId,
49-
jsonrpc: '2.0',
50-
method: 'eth_subscribe',
51-
params: ['newHeads'],
52-
}),
53-
);
54-
});
39+
async function sendTransaction(
40+
ONE_TINYBAR: any,
41+
CHAIN_ID: string | number,
42+
accounts: AliasAccount[],
43+
rpcServer: any,
44+
requestId: any,
45+
mirrorNodeServer: any,
46+
) {
47+
const transaction = {
48+
value: ONE_TINYBAR,
49+
gasLimit: numberTo0x(30000),
50+
chainId: Number(CHAIN_ID),
51+
to: accounts[1].address,
52+
nonce: await rpcServer.getAccountNonce(accounts[0].address, requestId),
53+
maxFeePerGas: await rpcServer.gasPrice(requestId),
54+
};
5555

56-
ws.on('error', (error) => {
57-
reject(error);
58-
});
59-
});
60-
};
56+
const signedTx = await accounts[0].wallet.signTransaction(transaction);
57+
const transactionHash = await rpcServer.sendRawTransaction(signedTx, requestId);
58+
59+
await mirrorNodeServer.get(`/contracts/results/${transactionHash}`, requestId);
60+
61+
return await rpcServer.call(RelayCall.ETH_ENDPOINTS.ETH_GET_TRANSACTION_RECEIPT, [transactionHash], requestId);
62+
}
63+
64+
function verifyResponse(response: any, done: Mocha.Done, webSocket: any, includeTransactions: boolean) {
65+
if (response?.params?.result?.transactions?.length > 0) {
66+
try {
67+
expect(response).to.have.property('jsonrpc', '2.0');
68+
expect(response).to.have.property('method', 'eth_subscription');
69+
expect(response).to.have.property('params');
70+
expect(response.params).to.have.property('result');
71+
expect(response.params.result).to.have.property('difficulty');
72+
expect(response.params.result).to.have.property('extraData');
73+
expect(response.params.result).to.have.property('gasLimit');
74+
expect(response.params.result).to.have.property('gasUsed');
75+
expect(response.params.result).to.have.property('logsBloom');
76+
expect(response.params.result).to.have.property('miner');
77+
expect(response.params.result).to.have.property('nonce');
78+
expect(response.params.result).to.have.property('number');
79+
expect(response.params.result).to.have.property('parentHash');
80+
expect(response.params.result).to.have.property('receiptsRoot');
81+
expect(response.params.result).to.have.property('sha3Uncles');
82+
expect(response.params.result).to.have.property('stateRoot');
83+
expect(response.params.result).to.have.property('timestamp');
84+
expect(response.params.result).to.have.property('transactionsRoot');
85+
expect(response.params.result).to.have.property('hash');
86+
expect(response.params).to.have.property('subscription');
87+
88+
if (includeTransactions) {
89+
expect(response.params.result).to.have.property('transactions');
90+
expect(response.params.result.transactions).to.have.lengthOf(1);
91+
expect(response.params.result.transactions[0]).to.have.property('hash');
92+
expect(response.params.result.transactions[0]).to.have.property('nonce');
93+
expect(response.params.result.transactions[0]).to.have.property('blockHash');
94+
expect(response.params.result.transactions[0]).to.have.property('blockNumber');
95+
expect(response.params.result.transactions[0]).to.have.property('transactionIndex');
96+
expect(response.params.result.transactions[0]).to.have.property('from');
97+
expect(response.params.result.transactions[0]).to.have.property('to');
98+
expect(response.params.result.transactions[0]).to.have.property('value');
99+
expect(response.params.result.transactions[0]).to.have.property('gas');
100+
expect(response.params.result.transactions[0]).to.have.property('gasPrice');
101+
expect(response.params.result.transactions[0]).to.have.property('input');
102+
expect(response.params.result.transactions[0]).to.have.property('v');
103+
expect(response.params.result.transactions[0]).to.have.property('r');
104+
expect(response.params.result.transactions[0]).to.have.property('s');
105+
expect(response.params.result.transactions[0]).to.have.property('type');
106+
expect(response.params.result.transactions[0]).to.have.property('maxFeePerGas');
107+
expect(response.params.result.transactions[0]).to.have.property('maxPriorityFeePerGas');
108+
expect(response.params.result.transactions[0]).to.have.property('chainId');
109+
} else {
110+
expect(response.params.result).to.have.property('transactions');
111+
expect(response.params.result.transactions).to.have.lengthOf(1);
112+
expect(response.params.result.transactions[0]).to.match(ethAddressRegex);
113+
}
114+
done();
115+
} catch (error) {
116+
webSocket.close();
117+
done(error);
118+
}
119+
}
120+
}
61121

62122
describe('@web-socket Acceptance Tests', async function () {
63123
this.timeout(240 * 1000); // 240 seconds
124+
const accounts: AliasAccount[] = [];
125+
const CHAIN_ID = process.env.CHAIN_ID || 0;
126+
const ONE_TINYBAR = Utils.add0xPrefix(Utils.toHex(ethers.parseUnits('1', 10)));
64127

65-
let server;
128+
const defaultGasPrice = numberTo0x(Assertions.defaultGasPrice);
129+
const defaultGasLimit = numberTo0x(3_000_000);
130+
131+
const defaultTransaction = {
132+
value: ONE_TINYBAR,
133+
chainId: Number(CHAIN_ID),
134+
maxPriorityFeePerGas: defaultGasPrice,
135+
maxFeePerGas: defaultGasPrice,
136+
gasLimit: defaultGasLimit,
137+
type: 2,
138+
};
139+
140+
let mirrorNodeServer, requestId, rpcServer, wsServer;
66141

67142
let wsProvider;
68143
let originalWsNewHeadsEnabledValue, originalWsSubcriptionLimitValue;
69144

70145
before(async () => {
71-
const { socketServer } = global;
72-
server = socketServer;
146+
// @ts-ignore
147+
const { servicesNode, socketServer, mirrorNode, relay, logger } = global;
148+
mirrorNodeServer = mirrorNode;
149+
rpcServer = relay;
150+
wsServer = socketServer;
151+
152+
accounts[0] = await servicesNode.createAliasAccount(100, relay.provider, requestId);
153+
accounts[1] = await servicesNode.createAliasAccount(5, relay.provider, requestId);
73154

74155
// cache original ENV values
75156
originalWsNewHeadsEnabledValue = process.env.WS_NEW_HEADS_ENABLED;
@@ -83,15 +164,13 @@ describe('@web-socket Acceptance Tests', async function () {
83164

84165
wsProvider = await new ethers.WebSocketProvider(WS_RELAY_URL);
85166
await new Promise((resolve) => setTimeout(resolve, 1000));
86-
if (server) expect(server._connections).to.equal(1);
87167
});
88168

89169
afterEach(async () => {
90170
if (wsProvider) {
91171
await wsProvider.destroy();
92172
await new Promise((resolve) => setTimeout(resolve, 1000));
93173
}
94-
if (server) expect(server._connections).to.equal(0);
95174
process.env.WS_SUBSCRIPTION_LIMIT = originalWsSubcriptionLimitValue;
96175
});
97176

@@ -150,4 +229,96 @@ describe('@web-socket Acceptance Tests', async function () {
150229
await new Promise((resolve) => setTimeout(resolve, 500));
151230
});
152231
});
232+
233+
describe('Subscriptions for newHeads', async function () {
234+
it('should subscribe to newHeads, include transactions true, and receive a valid JSON RPC response', (done) => {
235+
process.env.WS_NEW_HEADS_ENABLED = 'true';
236+
const webSocket = new WebSocket(WS_RELAY_URL);
237+
const subscriptionId = 1;
238+
webSocket.on('open', function open() {
239+
webSocket.send(
240+
JSON.stringify({
241+
id: subscriptionId,
242+
jsonrpc: '2.0',
243+
method: 'eth_subscribe',
244+
params: ['newHeads', { includeTransactions: true }],
245+
}),
246+
);
247+
});
248+
249+
let responseCounter = 0;
250+
251+
sendTransaction(ONE_TINYBAR, CHAIN_ID, accounts, rpcServer, requestId, mirrorNodeServer);
252+
webSocket.on('message', function incoming(data) {
253+
const response = JSON.parse(data);
254+
255+
responseCounter++;
256+
verifyResponse(response, done, webSocket, true);
257+
if (responseCounter > 1) {
258+
webSocket.close();
259+
done();
260+
}
261+
});
262+
});
263+
264+
it('should subscribe to newHeads, without the "include transactions", and receive a valid JSON RPC response', (done) => {
265+
process.env.WS_NEW_HEADS_ENABLED = 'true';
266+
const webSocket = new WebSocket(WS_RELAY_URL);
267+
const subscriptionId = 1;
268+
webSocket.on('open', function open() {
269+
webSocket.send(
270+
JSON.stringify({
271+
id: subscriptionId,
272+
jsonrpc: '2.0',
273+
method: 'eth_subscribe',
274+
params: ['newHeads'],
275+
}),
276+
);
277+
});
278+
279+
let responseCounter = 0;
280+
281+
sendTransaction(ONE_TINYBAR, CHAIN_ID, accounts, rpcServer, requestId, mirrorNodeServer);
282+
webSocket.on('message', function incoming(data) {
283+
const response = JSON.parse(data);
284+
285+
responseCounter++;
286+
verifyResponse(response, done, webSocket, false);
287+
if (responseCounter > 1) {
288+
webSocket.close();
289+
done();
290+
}
291+
});
292+
});
293+
294+
it('should subscribe to newHeads, with "include transactions false", and receive a valid JSON RPC response', (done) => {
295+
process.env.WS_NEW_HEADS_ENABLED = 'true';
296+
const webSocket = new WebSocket(WS_RELAY_URL);
297+
const subscriptionId = 1;
298+
webSocket.on('open', function open() {
299+
webSocket.send(
300+
JSON.stringify({
301+
id: subscriptionId,
302+
jsonrpc: '2.0',
303+
method: 'eth_subscribe',
304+
params: ['newHeads', { includeTransactions: false }],
305+
}),
306+
);
307+
});
308+
309+
let responseCounter = 0;
310+
311+
sendTransaction(ONE_TINYBAR, CHAIN_ID, accounts, rpcServer, requestId, mirrorNodeServer);
312+
webSocket.on('message', function incoming(data) {
313+
const response = JSON.parse(data);
314+
315+
responseCounter++;
316+
verifyResponse(response, done, webSocket, false);
317+
if (responseCounter > 1) {
318+
webSocket.close();
319+
done();
320+
}
321+
});
322+
});
323+
});
153324
});

packages/ws-server/src/webSocketServer.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -347,15 +347,15 @@ async function handleEthSubscribeLogs(
347347
return { response, subscriptionId };
348348
}
349349

350-
function subscribeToNewHeads(filters: any, response: any, request: any, subscriptionId: any, ctx: any, event: any) {
351-
if (filters !== undefined) {
352-
response = jsonResp(
353-
request.id,
354-
predefined.INVALID_PARAMETER('filters', 'Filters should be undefined for newHeads event'),
355-
undefined,
356-
);
357-
}
358-
subscriptionId = relay.subs()?.subscribe(ctx.websocket, event, 'newHeads');
350+
function subscribeToNewHeads(
351+
filters: any,
352+
response: any,
353+
request: any,
354+
subscriptionId: any,
355+
ctx: any,
356+
event: any,
357+
): { response: any; subscriptionId: any } {
358+
subscriptionId = relay.subs()?.subscribe(ctx.websocket, event, filters);
359359
logger.info(`Subscribed to newHeads, subscriptionId: ${subscriptionId}`);
360360
return { response, subscriptionId };
361361
}

0 commit comments

Comments
 (0)