Skip to content

Commit b3864c1

Browse files
authored
Developer mode (#668)
* feat: throw error in getTransactionByHash Signed-off-by: Ivo Yankov <[email protected]> * feat: add env var for dev mode Signed-off-by: Ivo Yankov <[email protected]> * test: add unit tests for reverted tx in getTransactionByHash Signed-off-by: Ivo Yankov <[email protected]> * test: add acceptance tests for dev mode Signed-off-by: Ivo Yankov <[email protected]> * docs: add docs for dev_mode Signed-off-by: Ivo Yankov <[email protected]> Signed-off-by: Ivo Yankov <[email protected]>
1 parent ce99832 commit b3864c1

File tree

14 files changed

+341
-3
lines changed

14 files changed

+341
-3
lines changed

.env.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,4 @@ HBAR_RATE_LIMIT_TINYBAR =
1717
HBAR_RATE_LIMIT_DURATION =
1818
RATE_LIMIT_DISABLED =
1919
ETH_GET_LOGS_BLOCK_RANGE_LIMIT =
20+
DEV_MODE =

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,11 @@ LIMIT_DURATION = 60000
8080
HBAR_RATE_LIMIT_TINYBAR = 6000000000
8181
HBAR_RATE_LIMIT_DURATION = 60000
8282
RATE_LIMIT_DISABLED = false
83+
DEV_MODE = false
8384
```
8485

86+
Note: Read more about `DEV_MODE` [here](docs/dev-mode.md)
87+
8588
The following table highlights some initial configuration values to consider
8689

8790
| Config | Default | Description |

docs/dev-mode.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Development mode
2+
3+
`Developer mode` is intended to be used by users when developing and testing smart contracts. It allows `hardhat-chai-matchers` and other similar libraries to correctly assert revert messages of non-pure contract methods.
4+
It will also be expanded with special settings to speed up the [local node](https://github.com/hashgraph/hedera-local-node).
5+
6+
## Enabling
7+
8+
To enable `dev mode` start the Relay with the environment variable `DEV_MODE` set to `true`.
9+
10+
11+
## Rationale
12+
13+
In the example below `contract.call()` will make the following requests to the JSON RPC Relay: `eth_chainId, eth_estimateGas, eth_sendRawTransaction, eth_getTransactionByHash`
14+
15+
```typescript
16+
await expect(contract.call()).to.be.revertedWith("Some revert message");
17+
```
18+
19+
The asserting method expects to catch an error from any of the called API endpoints. Normally `eth_estimateGas` throws the error if the contract call is about to revert,
20+
but that is currently not possible in the context of Hedera. Instead the error can be thrown by `eth_getTransactionByHash` after the transaction has gone through the consensus nodes.
21+
Since that is not the normal desired behaviour it is only enabled in this mode.

helm-chart/templates/configmap.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,4 @@ data:
2727
HBAR_RATE_LIMIT_DURATION: {{ .Values.config.HBAR_RATE_LIMIT_DURATION }}
2828
ETH_GET_LOGS_BLOCK_RANGE_LIMIT: {{ .Values.config.ETH_GET_LOGS_BLOCK_RANGE_LIMIT }}
2929
RATE_LIMIT_DISABLED: {{ .Values.config.RATE_LIMIT_DISABLED }}
30+
DEV_MODE: {{ .Values.config.DEV_MODE }}

helm-chart/templates/deployment.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,12 @@ spec:
144144
name: {{ include "json-rpc-relay.fullname" . }}
145145
key: RATE_LIMIT_DISABLED
146146
optional: true
147+
- name: DEV_MODE
148+
valueFrom:
149+
configMapKeyRef:
150+
name: {{ include "json-rpc-relay.fullname" . }}
151+
key: DEV_MODE
152+
optional: true
147153
ports:
148154
- containerPort: {{ .Values.ports.containerPort }}
149155
name: {{ .Values.ports.name }}

helm-chart/values.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ config:
111111
HBAR_RATE_LIMIT_DURATION: 60000
112112
ETH_GET_LOGS_BLOCK_RANGE_LIMIT: 1000
113113
RATE_LIMIT_DISABLED: false
114+
DEV_MODE: false
114115

115116
# Enable rolling_restarts if SDK calls fail this is usually due to stale connections that get cycled on restart
116117
rolling_restart:

packages/relay/src/lib/eth.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -960,6 +960,11 @@ export class EthImpl implements Eth {
960960
const rSig = contractResult.r === null ? null : contractResult.r.substring(0, 66);
961961
const sSig = contractResult.s === null ? null : contractResult.s.substring(0, 66);
962962

963+
if (process.env.DEV_MODE && process.env.DEV_MODE === 'true' && contractResult.result === 'CONTRACT_REVERT_EXECUTED') {
964+
let err = predefined.CONTRACT_REVERT(contractResult.error_message);
965+
throw err;
966+
}
967+
963968
return new Transaction({
964969
accessList: undefined, // we don't support access lists, so punt for now
965970
blockHash: contractResult.block_hash.substring(0, 66),

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

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2207,6 +2207,15 @@ describe('Eth', async function () {
22072207
"nonce": 1
22082208
};
22092209

2210+
2211+
const defaultDetailedContractResultByHashReverted = {
2212+
...defaultDetailedContractResultByHash, ...{
2213+
"result": "CONTRACT_REVERT_EXECUTED",
2214+
"status": "0x0",
2215+
"error_message": "0x08c379a000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000013536f6d6520726576657274206d65737361676500000000000000000000000000"
2216+
}
2217+
};
2218+
22102219
const defaultReceipt = {
22112220
"blockHash": "0xd693b532a80fed6392b428604171fb32fdbf953728a3a7ecc7d4062b1652c042",
22122221
"blockNumber": "0x11",
@@ -2541,5 +2550,57 @@ describe('Eth', async function () {
25412550
expect(result).to.exist;
25422551
expect(result.gas).to.eq('0x0');
25432552
});
2553+
2554+
it('returns reverted transactions', async function () {
2555+
mock.onGet(`contracts/results/${defaultTxHash}`).reply(200, defaultDetailedContractResultByHashReverted);
2556+
mock.onGet(`accounts/${defaultFromLongZeroAddress}`).reply(200, {
2557+
evm_address: `${defaultTransaction.from}`
2558+
});
2559+
2560+
const result = await ethImpl.getTransactionByHash(defaultTxHash);
2561+
2562+
expect(result).to.exist;
2563+
if (result == null) return;
2564+
2565+
expect(result.accessList).to.eq(defaultTransaction.accessList);
2566+
expect(result.blockHash).to.eq(defaultTransaction.blockHash);
2567+
expect(result.blockNumber).to.eq(defaultTransaction.blockNumber);
2568+
expect(result.chainId).to.eq(defaultTransaction.chainId);
2569+
expect(result.from).to.eq(defaultTransaction.from);
2570+
expect(result.gas).to.eq(defaultTransaction.gas);
2571+
expect(result.gasPrice).to.eq(defaultTransaction.gasPrice);
2572+
expect(result.hash).to.eq(defaultTransaction.hash);
2573+
expect(result.input).to.eq(defaultTransaction.input);
2574+
expect(result.maxFeePerGas).to.eq(defaultTransaction.maxFeePerGas);
2575+
expect(result.maxPriorityFeePerGas).to.eq(defaultTransaction.maxPriorityFeePerGas);
2576+
expect(result.nonce).to.eq(EthImpl.numberTo0x(defaultTransaction.nonce));
2577+
expect(result.r).to.eq(defaultTransaction.r);
2578+
expect(result.s).to.eq(defaultTransaction.s);
2579+
expect(result.to).to.eq(defaultTransaction.to);
2580+
expect(result.transactionIndex).to.eq(defaultTransaction.transactionIndex);
2581+
expect(result.type).to.eq(EthImpl.numberTo0x(defaultTransaction.type));
2582+
expect(result.v).to.eq(EthImpl.numberTo0x(defaultTransaction.v));
2583+
expect(result.value).to.eq(defaultTransaction.value);
2584+
})
2585+
2586+
it('throws error for reverted transactions when DEV_MODE=true', async function () {
2587+
const initialDevModeValue = process.env.DEV_MODE;
2588+
process.env.DEV_MODE = 'true';
2589+
2590+
mock.onGet(`contracts/results/${defaultTxHash}`).reply(200, defaultDetailedContractResultByHashReverted);
2591+
mock.onGet(`accounts/${defaultFromLongZeroAddress}`).reply(200, {
2592+
evm_address: `${defaultTransaction.from}`
2593+
});
2594+
2595+
try {
2596+
const result = await ethImpl.getTransactionByHash(defaultTxHash);
2597+
expect(true).to.eq(false);
2598+
}
2599+
catch(error) {
2600+
expect(error).to.deep.equal(predefined.CONTRACT_REVERT(defaultDetailedContractResultByHashReverted.error_message));
2601+
}
2602+
2603+
process.env.DEV_MODE = initialDevModeValue;
2604+
})
25442605
});
25452606
});

packages/server/tests/acceptance/rpc.spec.ts

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1302,6 +1302,139 @@ describe('@api RPC Server Acceptance Tests', function () {
13021302
expect(receipt?.revertReason).to.exist;
13031303
expect(receipt.revertReason).to.eq(PAYABLE_METHOD_ERROR_DATA);
13041304
});
1305+
1306+
describe('eth_getTransactionByHash for reverted payable contract calls', async function() {
1307+
const payableMethodsData = [
1308+
{
1309+
data: '0xfe0a3dd7',
1310+
method: 'revertWithNothing',
1311+
message: '',
1312+
errorData: '0x'
1313+
},
1314+
{
1315+
data: '0x0323d234',
1316+
method: 'revertWithString',
1317+
message: 'Some revert message',
1318+
errorData: '0x08c379a000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000013536f6d6520726576657274206d65737361676500000000000000000000000000'
1319+
},
1320+
{
1321+
data: '0x46fc4bb1',
1322+
method: 'revertWithCustomError',
1323+
message: '',
1324+
errorData: '0x0bd3d39c'
1325+
},
1326+
{
1327+
data: '0x33fe3fbd',
1328+
method: 'revertWithPanic',
1329+
message: '',
1330+
errorData: '0x4e487b710000000000000000000000000000000000000000000000000000000000000012'
1331+
}
1332+
];
1333+
const hashes: any = [];
1334+
1335+
beforeEach(async () => {
1336+
requestId = Utils.generateRequestId();
1337+
});
1338+
1339+
before(async function() {
1340+
for (let i = 0; i < payableMethodsData.length; i++) {
1341+
const transaction = {
1342+
// value: ONE_TINYBAR,
1343+
gasLimit: 30000,
1344+
chainId: Number(CHAIN_ID),
1345+
to: reverterEvmAddress,
1346+
nonce: await relay.getAccountNonce(accounts[0].address, requestId),
1347+
gasPrice: await relay.gasPrice(requestId),
1348+
data: payableMethodsData[i].data
1349+
};
1350+
const signedTx = await accounts[0].wallet.signTransaction(transaction);
1351+
const hash = await relay.call('eth_sendRawTransaction', [signedTx], requestId);
1352+
hashes.push(hash);
1353+
1354+
// Wait until receipt is available in mirror node
1355+
await mirrorNode.get(`/contracts/results/${hash}`, requestId);
1356+
}
1357+
});
1358+
1359+
for(let i = 0; i < payableMethodsData.length; i++) {
1360+
it(`Payable method ${payableMethodsData[i].method} returns tx object`, async function () {
1361+
const tx = await relay.call('eth_getTransactionByHash', [hashes[i]], requestId);
1362+
expect(tx).to.exist;
1363+
expect(tx.hash).to.exist;
1364+
expect(tx.hash).to.eq(hashes[i]);
1365+
});
1366+
}
1367+
1368+
describe('DEV_MODE = true', async function () {
1369+
before(async () =>{
1370+
process.env.DEV_MODE = 'true';
1371+
})
1372+
1373+
after(async () =>{
1374+
process.env.DEV_MODE = 'false';
1375+
})
1376+
1377+
for(let i = 0; i < payableMethodsData.length; i++) {
1378+
it(`Payable method ${payableMethodsData[i].method} throws an error`, async function () {
1379+
await relay.callFailing('eth_getTransactionByHash', [hashes[i]], {
1380+
code: -32008,
1381+
message: payableMethodsData[i].message,
1382+
data: payableMethodsData[i].errorData
1383+
}, requestId);
1384+
});
1385+
}
1386+
});
1387+
});
1388+
1389+
describe('eth_call for reverted pure contract calls', async function() {
1390+
beforeEach(async () => {
1391+
requestId = Utils.generateRequestId();
1392+
});
1393+
1394+
const pureMethodsData = [
1395+
{
1396+
data: '0x2dac842f',
1397+
method: 'revertWithNothingPure',
1398+
message: '',
1399+
errorData: '0x'
1400+
},
1401+
{
1402+
data: '0x8b153371',
1403+
method: 'revertWithStringPure',
1404+
message: 'Some revert message',
1405+
errorData: '0x08c379a000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000013536f6d6520726576657274206d65737361676500000000000000000000000000'
1406+
},
1407+
{
1408+
data: '0x35314694',
1409+
method: 'revertWithCustomErrorPure',
1410+
message: '',
1411+
errorData: '0x0bd3d39c'
1412+
},
1413+
{
1414+
data: '0x83889056',
1415+
method: 'revertWithPanicPure',
1416+
message: '',
1417+
errorData: '0x4e487b710000000000000000000000000000000000000000000000000000000000000012'
1418+
}
1419+
];
1420+
1421+
for (let i = 0; i < pureMethodsData.length; i++) {
1422+
it(`Pure method ${pureMethodsData[i].method} returns tx receipt`, async function () {
1423+
const callData = {
1424+
from: accounts[0].address,
1425+
to: reverterEvmAddress,
1426+
gas: 30000,
1427+
data: pureMethodsData[i].data
1428+
};
1429+
1430+
await relay.callFailing('eth_call', [callData], {
1431+
code: -32008,
1432+
message: pureMethodsData[i].message,
1433+
data: pureMethodsData[i].errorData
1434+
}, requestId);
1435+
});
1436+
}
1437+
});
13051438
});
13061439
});
13071440
});

0 commit comments

Comments
 (0)