Skip to content

Commit 725e514

Browse files
authored
Refactor eth_getStorageAt to use new endpoint (#783)
Refactor `eth_getStorageAt` to get the latest state from mirror node endpoint `/contracts/{evmAddress}/state`, when we pass tag `latest`/`pending` or no block at all (from specifications this parameter is not required). Fallback to `/contracts/{evmAddress}/results` endpoint, if a specific block number was passed. Also included in this PR: - Unit and acceptance tests for `eth_getStorageAt`. - Bump acceptance test timeout, because in some occasions is not enough. Signed-off-by: georgi-l95 <[email protected]>
1 parent 977e748 commit 725e514

File tree

7 files changed

+222
-33
lines changed

7 files changed

+222
-33
lines changed

.github/workflows/acceptance-workflow.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ on:
1010
jobs:
1111
acceptance-workflow:
1212
runs-on: ubuntu-latest
13-
timeout-minutes: 35
13+
timeout-minutes: 50
1414
permissions:
1515
contents: write
1616
steps:
@@ -41,5 +41,5 @@ jobs:
4141
uses: nick-fields/retry@v2
4242
with:
4343
max_attempts: 3
44-
timeout_minutes: 15
44+
timeout_minutes: 20
4545
command: npm run acceptancetest:${{ inputs.testfilter }}

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

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,13 +64,15 @@ export class MirrorNodeClient {
6464
private static GET_CONTRACT_RESULT_ENDPOINT = 'contracts/results/';
6565
private static GET_CONTRACT_RESULT_LOGS_ENDPOINT = 'contracts/results/logs';
6666
private static GET_CONTRACT_RESULT_LOGS_BY_ADDRESS_ENDPOINT = `contracts/${MirrorNodeClient.ADDRESS_PLACEHOLDER}/results/logs`;
67+
private static GET_STATE_ENDPOINT = '/state';
6768
private static GET_CONTRACT_RESULTS_ENDPOINT = 'contracts/results';
6869
private static GET_NETWORK_EXCHANGERATE_ENDPOINT = 'network/exchangerate';
6970
private static GET_NETWORK_FEES_ENDPOINT = 'network/fees';
7071
private static GET_TOKENS_ENDPOINT = 'tokens';
7172
private static GET_TRANSACTIONS_ENDPOINT = 'transactions';
7273

7374
private static CONTRACT_RESULT_LOGS_PROPERTY = 'logs';
75+
private static CONTRACT_STATE_PROPERTY = 'state';
7476

7577
private static ORDER = {
7678
ASC: 'asc',
@@ -430,13 +432,27 @@ export class MirrorNodeClient {
430432
requestId);
431433
}
432434

433-
public async getLatestContractResultsByAddress(address: string, blockEndTimestamp: string | undefined, limit: number) {
435+
public async getLatestContractResultsByAddress(address: string, blockEndTimestamp: string | undefined, limit: number, requestId?: string) {
434436
// retrieve the timestamp of the contract
435437
const contractResultsParams: IContractResultsParams = blockEndTimestamp
436438
? { timestamp: `lte:${blockEndTimestamp}` }
437439
: {};
438440
const limitOrderParams: ILimitOrderParams = this.getLimitOrderQueryParam(limit, 'desc');
439-
return this.getContractResultsByAddress(address, contractResultsParams, limitOrderParams);
441+
return this.getContractResultsByAddress(address, contractResultsParams, limitOrderParams, requestId);
442+
}
443+
444+
public async getContractCurrentStateByAddressAndSlot(address: string, slot: string, requestId?: string) {
445+
const limitOrderParams: ILimitOrderParams = this.getLimitOrderQueryParam(constants.MIRROR_NODE_QUERY_LIMIT, constants.ORDER.DESC);
446+
const queryParamObject = {};
447+
448+
this.setQueryParam(queryParamObject, 'slot', slot);
449+
this.setLimitOrderParams(queryParamObject, limitOrderParams);
450+
const queryParams = this.getQueryParams(queryParamObject);
451+
452+
return this.request(`${MirrorNodeClient.GET_CONTRACT_ENDPOINT}${address}${MirrorNodeClient.GET_STATE_ENDPOINT}${queryParams}`,
453+
MirrorNodeClient.GET_STATE_ENDPOINT,
454+
[400, 404],
455+
requestId);
440456
}
441457

442458
getQueryParams(params: object) {

packages/relay/src/lib/eth.ts

Lines changed: 64 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -483,46 +483,103 @@ export class EthImpl implements Eth {
483483
async getStorageAt(address: string, slot: string, blockNumberOrTag?: string | null, requestId?: string) : Promise<string> {
484484
const requestIdPrefix = formatRequestIdMessage(requestId);
485485
this.logger.trace(`${requestIdPrefix} getStorageAt(address=${address}, slot=${slot}, blockNumberOrTag=${blockNumberOrTag})`);
486+
487+
if (EthImpl.blockTagIsLatestOrPending(blockNumberOrTag) || !blockNumberOrTag){
488+
return await this.getCurrentState(address, EthImpl.toHex32Byte(slot), requestId);
489+
} else {
490+
return await this.getStateFromBlock(address, EthImpl.toHex32Byte(slot), blockNumberOrTag, requestId);
491+
}
492+
}
493+
494+
/**
495+
* Returns the current state value filtered by address and slot.
496+
* @param address
497+
* @param slot
498+
* @param requestId
499+
* @returns
500+
*/
501+
private async getCurrentState(address: string, slot: string, requestId?: string) {
502+
const requestIdPrefix = formatRequestIdMessage(requestId);
486503
let result = EthImpl.zeroHex32Byte; // if contract or slot not found then return 32 byte 0
487-
const blockResponse = await this.getHistoricalBlockResponse(blockNumberOrTag, false);
488504

505+
await this.mirrorNodeClient.getContractCurrentStateByAddressAndSlot(address, slot, requestId)
506+
.then(response => {
507+
if(response === null) {
508+
throw predefined.RESOURCE_NOT_FOUND(`Cannot find current state for contract address ${address} at slot=${slot}`);
509+
}
510+
if (response.state.length > 0) {
511+
result = response.state[0].value;
512+
}
513+
})
514+
.catch((e: any) => {
515+
this.logger.error(
516+
e,
517+
`${requestIdPrefix} Failed to retrieve current contract state for address ${address} at slot=${slot}`,
518+
);
519+
throw e;
520+
});
521+
522+
return result;
523+
}
524+
525+
/**
526+
* Returns the state value of contract filtered by address, slot and block number.
527+
* @param address
528+
* @param slot
529+
* @param blockNumberOrTag
530+
* @param requestId
531+
* @returns
532+
*/
533+
private async getStateFromBlock(address: string, slot: string, blockNumberOrTag?: string | null, requestId?: string) {
534+
const requestIdPrefix = formatRequestIdMessage(requestId);
535+
let result = EthImpl.zeroHex32Byte; // if contract or slot not found then return 32 byte 0
536+
537+
const blockResponse = await this.getHistoricalBlockResponse(blockNumberOrTag, false, requestId);
489538
// To save a request to the mirror node for `latest` and `pending` blocks, we directly return null from `getHistoricalBlockResponse`
490539
// But if a block number or `earliest` tag is passed and the mirror node returns `null`, we should throw an error.
491540
if (!EthImpl.blockTagIsLatestOrPending(blockNumberOrTag) && blockResponse == null) {
492541
throw predefined.RESOURCE_NOT_FOUND(`block '${blockNumberOrTag}'.`);
493542
}
494543

495544
const blockEndTimestamp = blockResponse?.timestamp?.to;
496-
const contractResult = await this.mirrorNodeClient.getLatestContractResultsByAddress(address, blockEndTimestamp, 1);
545+
const contractResult = await this.mirrorNodeClient.getLatestContractResultsByAddress(address, blockEndTimestamp, 1, requestId);
497546

498547
if (contractResult?.results?.length > 0) {
499548
// retrieve the contract result details
500549
await this.mirrorNodeClient.getContractResultsDetails(address, contractResult.results[0].timestamp)
501550
.then(contractResultDetails => {
502551
if(contractResultDetails === null) {
503-
throw predefined.RESOURCE_NOT_FOUND(`Contract result details for contract address ${address} at timestamp=${contractResult.results[0].timestamp}`);
552+
throw predefined.RESOURCE_NOT_FOUND(`Contract result details for contract address ${address} at slot ${slot} and timestamp=${contractResult.results[0].timestamp}`);
504553
}
505554
if (EthImpl.isArrayNonEmpty(contractResultDetails.state_changes)) {
506555
// filter the state changes to match slot and return value
507-
const stateChange = contractResultDetails.state_changes.find(stateChange => stateChange.slot === EthImpl.toHex32Byte(slot));
556+
const stateChange = contractResultDetails.state_changes.find(stateChange => stateChange.slot === slot);
508557
if (stateChange) {
509558
result = stateChange.value_written;
510559
}
511-
}
560+
}
512561
})
513562
.catch((e: any) => {
514563
this.logger.error(
515564
e,
516-
`${requestIdPrefix} Failed to retrieve contract result details for contract address ${address} at timestamp=${contractResult.results[0].timestamp}`,
565+
`${requestIdPrefix} Failed to retrieve contract result details for contract address ${address} at slot ${slot} and timestamp=${contractResult.results[0].timestamp}`,
517566
);
518-
519567
throw e;
520568
});
521569
}
522570

523571
return result;
524572
}
525573

574+
/**
575+
* Checks and return correct format from input.
576+
* @param input
577+
* @returns
578+
*/
579+
private static toHex32Byte(input: string): string {
580+
return input.length === 66 ? input : EthImpl.emptyHex + this.prune0x(input).padStart(64, '0');
581+
}
582+
526583
/**
527584
* Gets the balance of an account as of the given block from the mirror node.
528585
* Current implementation does not yet utilize blockNumber
@@ -1123,10 +1180,6 @@ export class EthImpl implements Eth {
11231180
return tag == null || tag === EthImpl.blockLatest || tag === EthImpl.blockPending;
11241181
}
11251182

1126-
private static toHex32Byte(input: string): string {
1127-
return input.length === 66 ? input : EthImpl.emptyHex + this.prune0x(input).padStart(64, '0');
1128-
}
1129-
11301183
/**
11311184
* Translates a block tag into a number. 'latest', 'pending', and null are the
11321185
* most recent block, 'earliest' is 0, numbers become numbers.

packages/relay/src/lib/precheck.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,10 +68,11 @@ export class Precheck {
6868
}
6969

7070
async verifyAccount(tx: Transaction, requestId?: string) {
71+
const requestIdPrefix = formatRequestIdMessage(requestId);
7172
// verify account
7273
const accountInfo = await this.mirrorNodeClient.getAccount(tx.from!, requestId);
7374
if (accountInfo == null) {
74-
this.logger.trace(`${requestId} Failed to retrieve address '${tx.from}' account details from mirror node on verify account precheck for sendRawTransaction(transaction=${JSON.stringify(tx)})`);
75+
this.logger.trace(`${requestIdPrefix} Failed to retrieve address '${tx.from}' account details from mirror node on verify account precheck for sendRawTransaction(transaction=${JSON.stringify(tx)})`);
7576
throw predefined.RESOURCE_NOT_FOUND(`address '${tx.from}'.`);
7677
}
7778

@@ -82,7 +83,8 @@ export class Precheck {
8283
* @param tx
8384
*/
8485
async nonce(tx: Transaction, accountInfoNonce: number, requestId?: string) {
85-
this.logger.trace(`${requestId} Nonce precheck for sendRawTransaction(tx.nonce=${tx.nonce}, accountInfoNonce=${accountInfoNonce})`);
86+
const requestIdPrefix = formatRequestIdMessage(requestId);
87+
this.logger.trace(`${requestIdPrefix} Nonce precheck for sendRawTransaction(tx.nonce=${tx.nonce}, accountInfoNonce=${accountInfoNonce})`);
8688

8789
// @ts-ignore
8890
if (accountInfoNonce > tx.nonce) {

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

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,18 @@ describe('Eth calls using MirrorNode', async function () {
388388
'v': 1
389389
};
390390

391+
const defaultCurrentContractState = {
392+
"state": [
393+
{
394+
'address': contractAddress1,
395+
'contract_id': contractId1,
396+
'timestamp': contractTimestamp1,
397+
'slot': '0x0000000000000000000000000000000000000000000000000000000000000101',
398+
'value': '0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925'
399+
}
400+
]
401+
}
402+
391403
const defaultDetailedContractResults2 = {
392404
...defaultDetailedContractResults, ...{
393405
'timestamp': contractTimestamp2,
@@ -1973,7 +1985,6 @@ describe('Eth calls using MirrorNode', async function () {
19731985
mock.onGet(`contracts/${log.address}`).reply(200, defaultContract);
19741986
}
19751987
const result = await ethImpl.getLogs(null, null, null, null, defaultNullLogTopics);
1976-
console.log(result);
19771988

19781989
expect(result).to.exist;
19791990
expect(result[0].topics.length).to.eq(defaultLogs4[0].topics.length)
@@ -2418,31 +2429,28 @@ describe('Eth calls using MirrorNode', async function () {
24182429

24192430
it('eth_getStorageAt with match with latest block', async function () {
24202431
// mirror node request mocks
2421-
mock.onGet('blocks?limit=1&order=desc').reply(200, {blocks: [defaultBlock]});
2422-
mock.onGet(`contracts/${contractAddress1}/results?limit=1&order=desc`).reply(200, defaultContractResults);
2423-
mock.onGet(`contracts/${contractAddress1}/results/${contractTimestamp1}`).reply(200, defaultDetailedContractResults);
2432+
mock.onGet(`contracts/${contractAddress1}/state?slot=${defaultCurrentContractState.state[0].slot}&limit=100&order=desc`).reply(200, defaultCurrentContractState);
24242433

2425-
const result = await ethImpl.getStorageAt(contractAddress1, defaultDetailedContractResults.state_changes[0].slot, "latest");
2434+
const result = await ethImpl.getStorageAt(contractAddress1, defaultCurrentContractState.state[0].slot, "latest");
24262435
expect(result).to.exist;
24272436
if (result == null) return;
24282437

24292438
// verify slot value
2430-
expect(result).equal(defaultDetailedContractResults.state_changes[0].value_written);
2439+
expect(result).equal(defaultCurrentContractState.state[0].value);
24312440
});
24322441

24332442
// Block number is a required param, this should not work and should be removed when/if validations are added.
24342443
// Instead the relay should return `missing value for required argument <argumentIndex> error`.
24352444
it('eth_getStorageAt with match null block', async function () {
24362445
// mirror node request mocks
2437-
mock.onGet(`contracts/${contractAddress1}/results?limit=1&order=desc`).reply(200, defaultContractResults);
2438-
mock.onGet(`contracts/${contractAddress1}/results/${contractTimestamp1}`).reply(200, defaultDetailedContractResults);
2446+
mock.onGet(`contracts/${contractAddress1}/state?slot=${defaultCurrentContractState.state[0].slot}&limit=100&order=desc`).reply(200, defaultCurrentContractState);
24392447

24402448
const result = await ethImpl.getStorageAt(contractAddress1, defaultDetailedContractResults.state_changes[0].slot);
24412449
expect(result).to.exist;
24422450
if (result == null) return;
24432451

24442452
// verify slot value
2445-
expect(result).equal(defaultDetailedContractResults.state_changes[0].value_written);
2453+
expect(result).equal(defaultCurrentContractState.state[0].value);
24462454
});
24472455

24482456
it('eth_getStorageAt should throw a predefined RESOURCE_NOT_FOUND when block not found', async function () {

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

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,22 @@ describe('MirrorNodeClient', async function () {
355355
'type': 2,
356356
'v': 1
357357
};
358+
359+
const contractAddress = '0x000000000000000000000000000000000000055f';
360+
const contractId = '0.0.5001';
361+
362+
const defaultCurrentContractState = {
363+
"state": [
364+
{
365+
'address': contractAddress,
366+
'contract_id': contractId,
367+
'timestamp': '1653077541.983983199',
368+
'slot': '0x0000000000000000000000000000000000000000000000000000000000000101',
369+
'value': '0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925'
370+
}
371+
]
372+
}
373+
358374
it('`getContractResults` by transactionId', async () => {
359375
const transactionId = '0.0.10-167654-000123456';
360376
mock.onGet(`contracts/results/${transactionId}`).reply(200, detailedContractResult);
@@ -501,6 +517,33 @@ describe('MirrorNodeClient', async function () {
501517
expect(firstResult.index).equal(log.index);
502518
});
503519

520+
it('`getContractCurrentStateByAddressAndSlot`', async () => {
521+
mock.onGet(`contracts/${contractAddress}/state?slot=${defaultCurrentContractState.state[0].slot}&limit=100&order=desc`).reply(200, defaultCurrentContractState);
522+
const result = await mirrorNodeInstance.getContractCurrentStateByAddressAndSlot(contractAddress, defaultCurrentContractState.state[0].slot);
523+
524+
expect(result).to.exist;
525+
expect(result.state).to.exist;
526+
expect(result.state[0].value).to.eq(defaultCurrentContractState.state[0].value);
527+
});
528+
529+
it('`getContractCurrentStateByAddressAndSlot` - incorrect address', async () => {
530+
mock.onGet(`contracts/${contractAddress}/state?slot=${defaultCurrentContractState.state[0].slot}&limit=100&order=desc`).reply(200, defaultCurrentContractState);
531+
try {
532+
expect(await mirrorNodeInstance.getContractCurrentStateByAddressAndSlot(contractAddress+'1', defaultCurrentContractState.state[0].slot)).to.throw();
533+
} catch (error) {
534+
expect(error).to.exist;
535+
}
536+
});
537+
538+
it('`getContractCurrentStateByAddressAndSlot` - incorrect slot', async () => {
539+
mock.onGet(`contracts/${contractAddress}/state?slot=${defaultCurrentContractState.state[0].slot}&limit=100&order=desc`).reply(200, defaultCurrentContractState);
540+
try {
541+
expect(await mirrorNodeInstance.getContractCurrentStateByAddressAndSlot(contractAddress, defaultCurrentContractState.state[0].slot+'1')).to.throw();
542+
} catch (error) {
543+
expect(error).to.exist;
544+
}
545+
});
546+
504547
it('`getContractResultsLogsByAddress` - incorrect address', async () => {
505548
mock.onGet(`contracts/${log.address}/results/logs?limit=100&order=asc`).reply(200, { logs: [log] });
506549

0 commit comments

Comments
 (0)