Skip to content

Commit 4b403c9

Browse files
authored
implement eth_getStorageAt method. initial pass (#433)
* implement eth_getStorageAt method. initial pass Signed-off-by: lukelee-sl <[email protected]> * address SonarCloud issues Signed-off-by: lukelee-sl <[email protected]> * reduce cognitive complexity Signed-off-by: lukelee-sl <[email protected]> * make block number optional Signed-off-by: lukelee-sl <[email protected]> * address code review comments Signed-off-by: lukelee-sl <[email protected]> * address sonarcloud checks Signed-off-by: lukelee-sl <[email protected]> * Merge remote changes Signed-off-by: lukelee-sl <[email protected]> * fix code merging issue Signed-off-by: lukelee-sl <[email protected]> * remove unneeded function Signed-off-by: lukelee-sl <[email protected]> * fix typo Signed-off-by: lukelee-sl <[email protected]> * fix typo Signed-off-by: lukelee-sl <[email protected]> * address code review comments Signed-off-by: lukelee-sl <[email protected]> * use static constants for pending, latest and earliest Signed-off-by: lukelee-sl <[email protected]> * update limit Signed-off-by: lukelee-sl <[email protected]>
1 parent 6674394 commit 4b403c9

File tree

7 files changed

+198
-38
lines changed

7 files changed

+198
-38
lines changed

packages/relay/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ export interface Eth {
7272

7373
getLogs(blockHash: string|null, fromBlock: string|null, toBlock: string|null, address: string|null, topics: any[]|null, requestId?: string): Promise<Log[]>;
7474

75-
getStorageAt(address: string, slot: string, blockNumber: string|null, requestId?: string): JsonRpcError;
75+
getStorageAt(address: string, slot: string, blockNumber: string|null, requestId?: string): Promise<string>;
7676

7777
getTransactionByBlockHashAndIndex(hash: string, index: number, requestId?: string): Promise<Transaction | null>;
7878

packages/relay/src/lib/clients/mirrorNodeClient.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,15 @@ export class MirrorNodeClient {
344344
.replace(MirrorNodeClient.TIMESTAMP_PLACEHOLDER, timestamp);
345345
}
346346

347+
public async getLatestContractResultsByAddress(address: string, blockEndTimestamp: string | undefined, limit: number) {
348+
// retrieve the timestamp of the contract
349+
const contractResultsParams: IContractResultsParams = blockEndTimestamp
350+
? { timestamp: `lte:${blockEndTimestamp}` }
351+
: {};
352+
const limitOrderParams: ILimitOrderParams = this.getLimitOrderQueryParam(limit, 'desc');
353+
return this.getContractResultsByAddress(address, contractResultsParams, limitOrderParams);
354+
}
355+
347356
getQueryParams(params: object) {
348357
let paramString = '';
349358
for (const [key, value] of Object.entries(params)) {

packages/relay/src/lib/errors/JsonRpcError.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,4 +111,9 @@ export const predefined = {
111111
code: -32010,
112112
message: 'Request timeout. Please try again.'
113113
}),
114+
'RESOURCE_NOT_FOUND': new JsonRpcError({
115+
name: 'Resource not found',
116+
code: -32001,
117+
message: 'Requested resource not found'
118+
}),
114119
};

packages/relay/src/lib/eth.ts

Lines changed: 80 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,10 @@ export class EthImpl implements Eth {
6969
static ethGetTransactionCount = 'eth_getTransactionCount';
7070
static ethSendRawTransaction = 'eth_sendRawTransaction';
7171

72+
// block constants
73+
static blockLatest = 'latest';
74+
static blockEarliest = 'earliest';
75+
static blockPending = 'pending';
7276

7377
/**
7478
* The sdk client use for connecting to both the consensus nodes and mirror node. The account
@@ -139,7 +143,7 @@ export class EthImpl implements Eth {
139143
const requestIdPrefix = formatRequestIdMessage(requestId);
140144
this.logger.trace(`${requestIdPrefix} feeHistory(blockCount=${blockCount}, newestBlock=${newestBlock}, rewardPercentiles=${rewardPercentiles})`);
141145
try {
142-
const latestBlockNumber = await this.translateBlockTag('latest', requestId);
146+
const latestBlockNumber = await this.translateBlockTag(EthImpl.blockLatest, requestId);
143147
const newestBlockNumber = await this.translateBlockTag(newestBlock, requestId);
144148

145149
if (newestBlockNumber > latestBlockNumber) {
@@ -441,10 +445,43 @@ export class EthImpl implements Eth {
441445
return predefined.UNSUPPORTED_METHOD;
442446
}
443447

444-
getStorageAt(address: string, slot: string, blockNumber: string | null, requestId?: string): JsonRpcError {
448+
/**
449+
* Gets the value from a storage position at the given Ethereum address.
450+
*
451+
* @param address
452+
* @param slot
453+
* @param blockNumberOrTag
454+
*/
455+
async getStorageAt(address: string, slot: string, blockNumberOrTag?: string | null, requestId?: string) : Promise<string> {
445456
const requestIdPrefix = formatRequestIdMessage(requestId);
446-
this.logger.trace(`${requestIdPrefix} getStorageAt(address=${address}, slot=${slot}, blockNumber=${blockNumber})`, address, slot, blockNumber);
447-
return predefined.UNSUPPORTED_METHOD;
457+
let result = EthImpl.zeroHex32Byte; // if contract or slot not found then return 32 byte 0
458+
const blockResponse = await this.getHistoricalBlockResponse(blockNumberOrTag, false);
459+
const blockEndTimestamp = blockResponse?.timestamp?.to;
460+
const contractResult = await this.mirrorNodeClient.getLatestContractResultsByAddress(address, blockEndTimestamp, 1);
461+
462+
if (contractResult?.results?.length > 0) {
463+
// retrieve the contract result details
464+
await this.mirrorNodeClient.getContractResultsDetails(address, contractResult.results[0].timestamp)
465+
.then( contractResultDetails => {
466+
if(contractResultDetails && contractResultDetails.state_changes) {
467+
// loop through the state changes to match slot and return value
468+
for (const stateChange of contractResultDetails.state_changes) {
469+
if(stateChange.slot === slot) {
470+
result = stateChange.value_written;
471+
}
472+
}
473+
}
474+
})
475+
.catch( (e: any) => {
476+
this.logger.error(
477+
e,
478+
`${requestIdPrefix} Failed to retrieve contract result details for contract address ${address} at timestamp=${contractResult.results[0].timestamp}`,
479+
);
480+
throw e;
481+
});
482+
}
483+
484+
return result;
448485
}
449486

450487
/**
@@ -913,9 +950,9 @@ export class EthImpl implements Eth {
913950
* @private
914951
*/
915952
private async translateBlockTag(tag: string | null, requestId?: string): Promise<number> {
916-
if (tag === null || tag === 'latest' || tag === 'pending') {
953+
if (tag === null || tag === EthImpl.blockLatest || tag === EthImpl.blockPending) {
917954
return Number(await this.blockNumber(requestId));
918-
} else if (tag === 'earliest') {
955+
} else if (tag === EthImpl.blockEarliest) {
919956
return 0;
920957
} else {
921958
return Number(tag);
@@ -932,25 +969,8 @@ export class EthImpl implements Eth {
932969
* @param blockHashOrNumber
933970
* @param showDetails
934971
*/
935-
private async getBlock(blockHashOrNumber: string, showDetails: boolean, requestId?: string): Promise<Block | null> {
936-
let blockResponse: any;
937-
if (blockHashOrNumber == null || blockHashOrNumber == 'latest' || blockHashOrNumber == 'pending') {
938-
const blockPromise = this.mirrorNodeClient.getLatestBlock(requestId);
939-
const blockAnswer = await blockPromise;
940-
blockResponse = blockAnswer.blocks[0];
941-
} else if (blockHashOrNumber == 'earliest') {
942-
blockResponse = await this.mirrorNodeClient.getBlock(0, requestId);
943-
} else if (blockHashOrNumber.length < 32) {
944-
// anything less than 32 characters is treated as a number
945-
blockResponse = await this.mirrorNodeClient.getBlock(Number(blockHashOrNumber), requestId);
946-
} else {
947-
blockResponse = await this.mirrorNodeClient.getBlock(blockHashOrNumber, requestId);
948-
}
949-
950-
if (_.isNil(blockResponse) || blockResponse.hash === undefined) {
951-
// block not found
952-
return null;
953-
}
972+
private async getBlock(blockHashOrNumber: string, showDetails: boolean, requestId?: string ): Promise<Block | null> {
973+
const blockResponse = await this.getHistoricalBlockResponse(blockHashOrNumber, true);
954974

955975
const timestampRange = blockResponse.timestamp;
956976
const timestampRangeParams = [`gte:${timestampRange.from}`, `lte:${timestampRange.to}`];
@@ -1016,6 +1036,41 @@ export class EthImpl implements Eth {
10161036
});
10171037
}
10181038

1039+
/**
1040+
* returns the block response
1041+
* otherwise return undefined.
1042+
*
1043+
* @param blockNumberOrTag
1044+
* @param returnLatest
1045+
*/
1046+
private async getHistoricalBlockResponse(blockNumberOrTag?: string | null, returnLatest?: boolean): Promise<any | null> {
1047+
let blockResponse: any;
1048+
// Determine if the latest block should be returned and if not then just return null
1049+
if (!returnLatest &&
1050+
(blockNumberOrTag == null || blockNumberOrTag === EthImpl.blockLatest || blockNumberOrTag === EthImpl.blockPending)) {
1051+
return null;
1052+
}
1053+
1054+
if (blockNumberOrTag == null || blockNumberOrTag === EthImpl.blockLatest || blockNumberOrTag === EthImpl.blockPending) {
1055+
const blockPromise = this.mirrorNodeClient.getLatestBlock();
1056+
const blockAnswer = await blockPromise;
1057+
blockResponse = blockAnswer.blocks[0];
1058+
} else if (blockNumberOrTag == EthImpl.blockEarliest) {
1059+
blockResponse = await this.mirrorNodeClient.getBlock(0);
1060+
} else if (blockNumberOrTag.length < 32) {
1061+
// anything less than 32 characters is treated as a number
1062+
blockResponse = await this.mirrorNodeClient.getBlock(Number(blockNumberOrTag));
1063+
} else {
1064+
blockResponse = await this.mirrorNodeClient.getBlock(blockNumberOrTag);
1065+
}
1066+
if (_.isNil(blockResponse) || blockResponse.hash === undefined) {
1067+
// block not found.
1068+
throw predefined.RESOURCE_NOT_FOUND;
1069+
}
1070+
1071+
return blockResponse;
1072+
}
1073+
10191074
private static getTransactionCountFromBlockResponse(block: any) {
10201075
if (block === null || block.count === undefined) {
10211076
// block not found

packages/relay/tests/lib/eth.spec.ts

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1498,6 +1498,79 @@ describe('Eth calls using MirrorNode', async function () {
14981498
expect(hasError).to.be.true;
14991499
});
15001500
});
1501+
1502+
describe('eth_getStorageAt', async function() {
1503+
it('eth_getStorageAt with match with block', async function () {
1504+
// mirror node request mocks
1505+
mock.onGet(`blocks/${blockNumber}`).reply(200, defaultBlock);
1506+
mock.onGet(`contracts/${contractAddress1}/results?timestamp=lte:${defaultBlock.timestamp.to}&limit=1&order=desc`).reply(200, defaultContractResults);
1507+
mock.onGet(`contracts/${contractAddress1}/results/${contractTimestamp1}`).reply(200, defaultDetailedContractResults);
1508+
1509+
const result = await ethImpl.getStorageAt(contractAddress1, defaultDetailedContractResults.state_changes[0].slot, EthImpl.numberTo0x(blockNumber));
1510+
expect(result).to.exist;
1511+
if (result == null) return;
1512+
1513+
// verify slot value
1514+
expect(result).equal(defaultDetailedContractResults.state_changes[0].value_written);
1515+
});
1516+
1517+
it('eth_getStorageAt with match with latest block', async function () {
1518+
// mirror node request mocks
1519+
mock.onGet(`contracts/${contractAddress1}/results?limit=1&order=desc`).reply(200, defaultContractResults);
1520+
mock.onGet(`contracts/${contractAddress1}/results/${contractTimestamp1}`).reply(200, defaultDetailedContractResults);
1521+
1522+
const result = await ethImpl.getStorageAt(contractAddress1, defaultDetailedContractResults.state_changes[0].slot, "latest");
1523+
expect(result).to.exist;
1524+
if (result == null) return;
1525+
1526+
// verify slot value
1527+
expect(result).equal(defaultDetailedContractResults.state_changes[0].value_written);
1528+
});
1529+
1530+
it('eth_getStorageAt with match null block', async function () {
1531+
// mirror node request mocks
1532+
mock.onGet(`contracts/${contractAddress1}/results?limit=1&order=desc`).reply(200, defaultContractResults);
1533+
mock.onGet(`contracts/${contractAddress1}/results/${contractTimestamp1}`).reply(200, defaultDetailedContractResults);
1534+
1535+
const result = await ethImpl.getStorageAt(contractAddress1, defaultDetailedContractResults.state_changes[0].slot);
1536+
expect(result).to.exist;
1537+
if (result == null) return;
1538+
1539+
// verify slot value
1540+
expect(result).equal(defaultDetailedContractResults.state_changes[0].value_written);
1541+
});
1542+
1543+
it('eth_getStorageAt should throw a predefined NO_SUITABLE_PEERS when block not found', async function () {
1544+
1545+
let hasError = false;
1546+
try {
1547+
mock.onGet(`blocks/${blockNumber}`).reply(200, null);
1548+
const result = await ethImpl.getStorageAt(contractAddress1, defaultDetailedContractResults.state_changes[0].slot, EthImpl.numberTo0x(blockNumber));
1549+
} catch (e: any) {
1550+
hasError = true;
1551+
expect(e.code).to.equal(-32001);
1552+
expect(e.name).to.equal('Resource not found');
1553+
}
1554+
expect(hasError).to.be.true;
1555+
});
1556+
1557+
it('eth_getStorageAt should throw error when contract not found', async function () {
1558+
// mirror node request mocks
1559+
mock.onGet(`blocks/${blockNumber}`).reply(200, defaultBlock);
1560+
mock.onGet(`contracts/${contractAddress1}/results?timestamp=lte:${defaultBlock.timestamp.to}&limit=1&order=desc`).reply(200, defaultContractResults);
1561+
mock.onGet(`contracts/${contractAddress1}/results/${contractTimestamp1}`).reply(404, detailedContractResultNotFound);
1562+
1563+
let hasError = false;
1564+
try {
1565+
const result = await ethImpl.getStorageAt(contractAddress1, defaultDetailedContractResults.state_changes[0].slot, EthImpl.numberTo0x(blockNumber));
1566+
} catch (e: any) {
1567+
hasError = true;
1568+
expect(e.statusCode).to.equal(404);
1569+
expect(e.message).to.equal("Request failed with status code 404");
1570+
}
1571+
expect(hasError).to.be.true;
1572+
});
1573+
});
15011574
});
15021575

15031576
describe('Eth', async function () {
@@ -1684,7 +1757,6 @@ describe('Eth', async function () {
16841757
'sendTransaction',
16851758
'protocolVersion',
16861759
'coinbase',
1687-
'getStorageAt',
16881760
];
16891761

16901762
unsupportedMethods.forEach(method => {

packages/relay/tests/lib/mirrorNodeClient.spec.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,36 @@ describe('MirrorNodeClient', async function () {
414414
expect(firstResult.to).equal(contractResult.to);
415415
});
416416

417+
it('`getLatestContractResultsByAddress` by address no timestamp', async () => {
418+
const address = '0x0000000000000000000000000000000000001f41';
419+
mock.onGet(`contracts/${address}/results?limit=1&order=desc`).reply(200, { results: [contractResult], links: { next: null } });
420+
421+
const result = await mirrorNodeInstance.getLatestContractResultsByAddress(address, undefined, 1);
422+
expect(result).to.exist;
423+
expect(result.links).to.exist;
424+
expect(result.links.next).to.equal(null);
425+
expect(result.results.length).to.gt(0);
426+
const firstResult = result.results[0];
427+
expect(firstResult.contract_id).equal(detailedContractResult.contract_id);
428+
expect(firstResult.function_parameters).equal(contractResult.function_parameters);
429+
expect(firstResult.to).equal(contractResult.to);
430+
});
431+
432+
it('`getLatestContractResultsByAddress` by address with timestamp, limit 2', async () => {
433+
const address = '0x0000000000000000000000000000000000001f41';
434+
mock.onGet(`contracts/${address}/results?timestamp=lte:987654.000123456&limit=2&order=desc`).reply(200, { results: [contractResult], links: { next: null } });
435+
436+
const result = await mirrorNodeInstance.getLatestContractResultsByAddress(address, "987654.000123456", 2);
437+
expect(result).to.exist;
438+
expect(result.links).to.exist;
439+
expect(result.links.next).to.equal(null);
440+
expect(result.results.length).to.gt(0);
441+
const firstResult = result.results[0];
442+
expect(firstResult.contract_id).equal(detailedContractResult.contract_id);
443+
expect(firstResult.function_parameters).equal(contractResult.function_parameters);
444+
expect(firstResult.to).equal(contractResult.to);
445+
});
446+
417447
const log = {
418448
'address': '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',
419449
'bloom': '0x549358c4c2e573e02410ef7b5a5ffa5f36dd7398',

packages/server/tests/integration/server.spec.ts

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -316,17 +316,6 @@ describe('RPC Server', async function() {
316316

317317
BaseTest.unsupportedJsonRpcMethodChecks(res);
318318
});
319-
320-
it('should execute "eth_getStorageAt"', async function() {
321-
const res = await this.testClient.post('/', {
322-
'id': '2',
323-
'jsonrpc': '2.0',
324-
'method': 'eth_getStorageAt',
325-
'params': [null]
326-
});
327-
328-
BaseTest.unsupportedJsonRpcMethodChecks(res);
329-
});
330319
});
331320

332321
class BaseTest {

0 commit comments

Comments
 (0)