Skip to content

Commit 5b31c2d

Browse files
authored
Merge branch 'main' into 3838_align_eth_getBalance-param-validation-with-eth-spec
2 parents 02b7f3c + c4b9c0a commit 5b31c2d

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+2380
-141
lines changed
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
2+
name: OpenRPC JSON Updater
3+
4+
on:
5+
push:
6+
branches:
7+
- main
8+
paths:
9+
- 'docs/openrpc.json'
10+
workflow_dispatch:
11+
12+
jobs:
13+
clone-and-build-execution-apis:
14+
runs-on: ubuntu-latest
15+
16+
steps:
17+
- name: Checkout execution-apis repo
18+
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
19+
with:
20+
ref: main
21+
repository: 'ethereum/execution-apis'
22+
path: 'execution-apis'
23+
24+
- name: Use Node.js TLS 20
25+
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
26+
with:
27+
node-version: 20
28+
29+
- name: Install dependencies
30+
run: npm install
31+
working-directory: ./execution-apis
32+
33+
- name: Build project
34+
run: npm run build
35+
working-directory: ./execution-apis
36+
37+
- name: Upload openrpc.json as an artifact
38+
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
39+
with:
40+
name: openrpc
41+
path: ./execution-apis/refs-openrpc.json
42+
43+
update-openrpc:
44+
runs-on: ubuntu-latest
45+
needs: clone-and-build-execution-apis
46+
steps:
47+
- name: Checkout repository
48+
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
49+
with:
50+
ref: 'main'
51+
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
52+
53+
- name: Download openrpc.json artifact
54+
uses: actions/download-artifact@v4
55+
with:
56+
name: openrpc
57+
path: ./downloaded-artifacts/
58+
59+
- name: Copy generated openrpc.json to scripts directory
60+
run: |
61+
mkdir -p scripts/openrpc-json-updater
62+
cp ./downloaded-artifacts/refs-openrpc.json scripts/openrpc-json-updater/original-openrpc.json
63+
64+
- name: Setup Node.js
65+
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
66+
with:
67+
node-version: '22'
68+
69+
- name: Install dependencies
70+
working-directory: scripts/openrpc-json-updater
71+
run: npm install
72+
73+
- name: Generate comparison report
74+
id: generate-report
75+
working-directory: scripts/openrpc-json-updater
76+
run: |
77+
REPORT_OUTPUT=$(node cli.js --original ./original-openrpc.json --modified ../../docs/openrpc.json)
78+
echo "REPORT_OUTPUT<<EOF" >> $GITHUB_ENV
79+
echo "$REPORT_OUTPUT" >> $GITHUB_ENV
80+
echo "EOF" >> $GITHUB_ENV
81+
82+
# This workflow automatically creates PRs when the OpenRPC JSON file differs from the upstream source.
83+
# PRs are only created when actual changes are detected (SKIP_PR=false), ensuring that
84+
# maintainers can review and approve schema updates before they're merged into the main branch.
85+
# This provides a safety mechanism for tracking OpenRPC specification changes over time.
86+
- name: Perform merge
87+
id: merge
88+
working-directory: scripts/openrpc-json-updater
89+
run: |
90+
MERGE_OUTPUT=$(node cli.js --merge --original ./original-openrpc.json --modified ../../docs/openrpc.json)
91+
MERGE_EXIT_CODE=$?
92+
echo "$MERGE_OUTPUT"
93+
94+
if [ $MERGE_EXIT_CODE -eq 0 ]; then
95+
if [[ "$MERGE_OUTPUT" =~ No\ differences\ found\ after\ merge ]]; then
96+
echo "No differences found. Skipping PR creation."
97+
echo "SKIP_PR=true" >> $GITHUB_ENV
98+
exit 0
99+
elif [[ "$MERGE_OUTPUT" == *"Merge completed"* ]]; then
100+
echo "Successfully updated openrpc.json"
101+
echo "SKIP_PR=false" >> $GITHUB_ENV
102+
else
103+
echo "Unexpected output. Output was: $MERGE_OUTPUT"
104+
exit 1
105+
fi
106+
else
107+
echo "Failed to update file. Output was: $MERGE_OUTPUT"
108+
exit 1
109+
fi
110+
111+
- name: Generate unique branch name
112+
id: branch-name
113+
run: |
114+
TIMESTAMP=$(date +%Y%m%d%H%M%S)
115+
UNIQUE_BRANCH="update-openrpc-${TIMESTAMP}"
116+
echo "UNIQUE_BRANCH=${UNIQUE_BRANCH}" >> $GITHUB_ENV
117+
echo "Generated unique branch name: ${UNIQUE_BRANCH}"
118+
119+
- name: Create Pull Request
120+
if: env.SKIP_PR != 'true'
121+
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
122+
with:
123+
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
124+
commit-message: Update OpenRPC JSON
125+
title: 'Update OpenRPC JSON'
126+
body: |
127+
# OpenRPC JSON Update
128+
129+
This PR updates the OpenRPC JSON specification with the latest changes from Ethereum JSON-RPC specification.
130+
131+
## Comparison Report
132+
```
133+
${{ env.REPORT_OUTPUT }}
134+
```
135+
branch: ${{ env.UNIQUE_BRANCH }}
136+
base: 'main'
137+
add-paths: docs/openrpc.json
138+
delete-branch: true

packages/relay/src/lib/decorators/cache.decorator.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@ const shouldSkipCachingForSingleParams = (args: IArgument[], params: CacheSingle
4949
return true;
5050
}
5151

52-
// do not cache optional parameters like 'blockNumber' on 'eth_getStorageAt'
52+
// do not cache when a parameter is missing or undefined
53+
// this handles cases where optional parameters are not provided
5354
if (!Object.prototype.hasOwnProperty.call(args, item.index) || args[item.index] === undefined) {
5455
return true;
5556
}

packages/relay/src/lib/eth.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -671,15 +671,15 @@ export class EthImpl implements Eth {
671671
*
672672
* @param {string} address - The Ethereum address to get the storage value from
673673
* @param {string} slot - The storage slot to get the value from
674-
* @param {string | null} blockNumberOrTagOrHash - The block number or tag or hash to get the storage value from
674+
* @param {string} blockNumberOrTagOrHash - The block number or tag or hash to get the storage value from
675675
* @param {RequestDetails} requestDetails - The request details for logging and tracking
676676
* @returns {Promise<string>} A promise that resolves to the storage value as a hexadecimal string
677677
*/
678678
@rpcMethod
679679
@rpcParamValidationRules({
680680
0: { type: ParamType.ADDRESS, required: true },
681681
1: { type: ParamType.HEX64, required: true },
682-
2: { type: ParamType.BLOCK_NUMBER_OR_HASH, required: false },
682+
2: { type: ParamType.BLOCK_NUMBER_OR_HASH, required: true },
683683
})
684684
@rpcParamLayoutConfig(RPC_LAYOUT.custom((params) => [params[0], params[1], params[2]]))
685685
@cache(CacheService.getInstance(CACHE_LEVEL.L1), {
@@ -688,7 +688,7 @@ export class EthImpl implements Eth {
688688
async getStorageAt(
689689
address: string,
690690
slot: string,
691-
blockNumberOrTagOrHash: string | null,
691+
blockNumberOrTagOrHash: string,
692692
requestDetails: RequestDetails,
693693
): Promise<string> {
694694
return this.contractService.getStorageAt(address, slot, blockNumberOrTagOrHash, requestDetails);
@@ -903,7 +903,7 @@ export class EthImpl implements Eth {
903903
* @rpcParamValidationRules Applies JSON-RPC parameter validation according to the API specification
904904
*
905905
* @param {string} address - The account address for which to retrieve the transaction count.
906-
* @param {string | null} blockNumOrTag - Possible values are 'earliest', 'pending', 'latest', or a block hash in hexadecimal format.
906+
* @param {string} blockNumOrTag - Possible values are 'earliest', 'pending', 'latest', or a block hash in hexadecimal format.
907907
* @param {RequestDetails} requestDetails - The details of the request for logging and tracking.
908908
* @returns {Promise<string | JsonRpcError>} A promise that resolves to the transaction count in hexadecimal format or a JsonRpcError.
909909
*/
@@ -917,7 +917,7 @@ export class EthImpl implements Eth {
917917
})
918918
async getTransactionCount(
919919
address: string,
920-
blockNumOrTag: string | null,
920+
blockNumOrTag: string,
921921
requestDetails: RequestDetails,
922922
): Promise<string | JsonRpcError> {
923923
return this.accountService.getTransactionCount(address, blockNumOrTag, requestDetails);

packages/relay/src/lib/factories/transactionReceiptFactory.ts

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ interface IRegularTransactionReceiptParams {
3030
from: string;
3131
logs: Log[];
3232
receiptResponse: any;
33-
to: string;
33+
to: string | null;
3434
}
3535

3636
/**
@@ -68,16 +68,44 @@ class TransactionReceiptFactory {
6868
/**
6969
* Creates a regular transaction receipt from mirror node contract result data
7070
*
71+
* Handles the correction of transaction receipt `to` field for contract creation transactions.
72+
*
73+
* This logic addresses a discrepancy between Hedera and standard Ethereum behavior regarding
74+
* the `to` field in transaction receipts. When a smart contract is deployed:
75+
*
76+
* 1. In standard Ethereum JSON-RPC, if the original transaction had a null `to` field
77+
* (contract creation), the transaction receipt also reports a null `to` field.
78+
*
79+
* 2. Hedera Mirror Node, however, automatically populates the `to` field with the
80+
* address of the newly created contract.
81+
*
82+
* The code checks if a contract was directly created by the transaction (rather than created by
83+
* another contract) by checking if the contract's ID appears in the `created_contract_ids` array.
84+
* If so, it resets the `to` field to null to match standard Ethereum JSON-RPC behavior.
85+
*
86+
* This ensures compatibility with Ethereum tooling that expects standard transaction receipt formats.
87+
* The handling covers various scenarios:
88+
*
89+
* - Direct contract deployment (empty `to` field)
90+
* - Contract creation via factory contracts
91+
* - Method calls that don't create contracts
92+
* - Transactions with populated `to` fields that create child contracts
93+
*
7194
* @param params Parameters required to create a regular transaction receipt
7295
* @param resolveEvmAddressFn Function to resolve EVM addresses
7396
* @returns {ITransactionReceipt} Transaction receipt for the regular transaction
7497
*/
7598
public static createRegularReceipt(params: IRegularTransactionReceiptParams): ITransactionReceipt {
76-
const { receiptResponse, effectiveGas, from, logs, to } = params;
99+
const { receiptResponse, effectiveGas, from, logs } = params;
100+
let { to } = params;
77101

78102
// Determine contract address if it exists
79103
const contractAddress = TransactionReceiptFactory.getContractAddressFromReceipt(receiptResponse);
80104

105+
if (receiptResponse.created_contract_ids.includes(receiptResponse.contract_id)) {
106+
to = null;
107+
}
108+
81109
// Create the receipt object
82110
const receipt: ITransactionReceipt = {
83111
blockHash: toHash32(receiptResponse.block_hash),

packages/relay/src/lib/services/ethService/accountService/AccountService.ts

Lines changed: 16 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -296,12 +296,12 @@ export class AccountService implements IAccountService {
296296
* Queries mirror node for best effort and falls back to consensus node for contracts until HIP 729 is implemented.
297297
*
298298
* @param {string} address The account address
299-
* @param {string | null} blockNumOrTag Possible values are earliest/pending/latest or hex
299+
* @param {string} blockNumOrTag Possible values are earliest/pending/latest or hex
300300
* @param {RequestDetails} requestDetails The request details for logging and tracking
301301
*/
302302
public async getTransactionCount(
303303
address: string,
304-
blockNumOrTag: string | null,
304+
blockNumOrTag: string,
305305
requestDetails: RequestDetails,
306306
): Promise<string | JsonRpcError> {
307307
const requestIdPrefix = requestDetails.formattedRequestId;
@@ -320,26 +320,21 @@ export class AccountService implements IAccountService {
320320
}
321321

322322
const blockNum = Number(blockNumOrTag);
323-
if (blockNumOrTag) {
324-
if (blockNum === 0 || blockNum === 1) {
325-
// previewnet and testnet bug have a genesis blockNumber of 1 but non system account were yet to be created
326-
return constants.ZERO_HEX;
327-
} else if (this.common.blockTagIsLatestOrPending(blockNumOrTag)) {
328-
// if latest or pending, get latest ethereumNonce from mirror node account API
329-
nonceCount = await this.getAccountLatestEthereumNonce(address, requestDetails);
330-
} else if (blockNumOrTag === constants.BLOCK_EARLIEST) {
331-
nonceCount = await this.getAccountNonceForEarliestBlock(requestDetails);
332-
} else if (!isNaN(blockNum) && blockNumOrTag.length != constants.BLOCK_HASH_LENGTH && blockNum > 0) {
333-
nonceCount = await this.getAccountNonceForHistoricBlock(address, blockNum, requestDetails);
334-
} else if (blockNumOrTag.length == constants.BLOCK_HASH_LENGTH && blockNumOrTag.startsWith(constants.EMPTY_HEX)) {
335-
nonceCount = await this.getAccountNonceForHistoricBlock(address, blockNumOrTag, requestDetails);
336-
} else {
337-
// return a '-39001: Unknown block' error per api-spec
338-
throw predefined.UNKNOWN_BLOCK();
339-
}
340-
} else {
341-
// if no block consideration, get latest ethereumNonce from mirror node if account or from consensus node is contract until HIP 729 is implemented
323+
if (blockNum === 0 || blockNum === 1) {
324+
// previewnet and testnet bug have a genesis blockNumber of 1 but non system account were yet to be created
325+
return constants.ZERO_HEX;
326+
} else if (this.common.blockTagIsLatestOrPending(blockNumOrTag)) {
327+
// if latest or pending, get latest ethereumNonce from mirror node account API
342328
nonceCount = await this.getAccountLatestEthereumNonce(address, requestDetails);
329+
} else if (blockNumOrTag === constants.BLOCK_EARLIEST) {
330+
nonceCount = await this.getAccountNonceForEarliestBlock(requestDetails);
331+
} else if (!isNaN(blockNum) && blockNumOrTag.length != constants.BLOCK_HASH_LENGTH && blockNum > 0) {
332+
nonceCount = await this.getAccountNonceForHistoricBlock(address, blockNum, requestDetails);
333+
} else if (blockNumOrTag.length == constants.BLOCK_HASH_LENGTH && blockNumOrTag.startsWith(constants.EMPTY_HEX)) {
334+
nonceCount = await this.getAccountNonceForHistoricBlock(address, blockNumOrTag, requestDetails);
335+
} else {
336+
// return a '-39001: Unknown block' error per api-spec
337+
throw predefined.UNKNOWN_BLOCK();
343338
}
344339

345340
const cacheTtl =

packages/relay/src/lib/services/ethService/accountService/IAccountService.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { RequestDetails } from '../../../types';
66
export interface IAccountService {
77
getTransactionCount: (
88
address: string,
9-
blockNumOrTag: string | null,
9+
blockNumOrTag: string,
1010
requestDetails: RequestDetails,
1111
) => Promise<string | JsonRpcError>;
1212

packages/relay/src/lib/services/ethService/blockService/BlockService.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,11 +168,13 @@ export class BlockService implements IBlockService {
168168
}
169169
return null;
170170
}
171+
171172
contractResult.logs = logsByHash.get(contractResult.hash) || [];
172173
const [from, to] = await Promise.all([
173174
this.common.resolveEvmAddress(contractResult.from, requestDetails),
174-
this.common.resolveEvmAddress(contractResult.to, requestDetails),
175+
contractResult.to === null ? null : this.common.resolveEvmAddress(contractResult.to, requestDetails),
175176
]);
177+
176178
const transactionReceiptParams: IRegularTransactionReceiptParams = {
177179
effectiveGas,
178180
from,

packages/relay/src/lib/services/ethService/contractService/ContractService.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -288,14 +288,14 @@ export class ContractService implements IContractService {
288288
*
289289
* @param {string} address - The address of the storage
290290
* @param {string} slot - The slot index (hex string)
291-
* @param {string | null} blockNumberOrTagOrHash - Block number, tag, or hash
291+
* @param {string} blockNumberOrTagOrHash - Block number, tag, or hash
292292
* @param {RequestDetails} requestDetails - The request details for logging and tracking
293293
* @returns {Promise<string>} The value at the given storage position
294294
*/
295295
public async getStorageAt(
296296
address: string,
297297
slot: string,
298-
blockNumberOrTagOrHash: string | null,
298+
blockNumberOrTagOrHash: string,
299299
requestDetails: RequestDetails,
300300
): Promise<string> {
301301
const requestIdPrefix = requestDetails.formattedRequestId;

packages/relay/src/lib/services/ethService/contractService/IContractService.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export interface IContractService {
3535
getStorageAt: (
3636
address: string,
3737
slot: string,
38-
blockNumberOrTagOrHash: string | null,
38+
blockNumberOrTagOrHash: string,
3939
requestDetails: RequestDetails,
4040
) => Promise<string>;
4141

packages/relay/src/lib/services/ethService/transactionService/ITransactionService.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import { JsonRpcError } from '../../../errors/JsonRpcError';
44
import { Transaction } from '../../../model';
5-
import { RequestDetails } from '../../../types';
5+
import { ITransactionReceipt, RequestDetails } from '../../../types';
66

77
export interface ITransactionService {
88
getTransactionByBlockHashAndIndex(
@@ -19,7 +19,7 @@ export interface ITransactionService {
1919

2020
getTransactionByHash(hash: string, requestDetails: RequestDetails): Promise<Transaction | null>;
2121

22-
getTransactionReceipt(hash: string, requestDetails: RequestDetails): Promise<any>;
22+
getTransactionReceipt(hash: string, requestDetails: RequestDetails): Promise<ITransactionReceipt | null>;
2323

2424
sendRawTransaction(transaction: string, requestDetails: RequestDetails): Promise<string | JsonRpcError>;
2525

0 commit comments

Comments
 (0)