diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7ab6367..08355f6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,6 +46,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@v3 + with: + submodules: true - name: Install build dependencies (Linux i386) if: runner.os == 'Linux' && matrix.target.cpu == 'i386' diff --git a/.gitignore b/.gitignore index 75ad594..672c808 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,18 @@ node_modules nohup.out hardhat.config.js package-lock.json + +# Individual test executables +all_tests +test_contract_dsl +test_contracts +test_deposit_contract +test_execution_api +test_execution_debug_apis +test_execution_types +test_json_marshalling +test_logs test_null_conversion +test_primitives +test_signed_tx +test_string_decoder diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..0ea3fe2 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "tests/execution-apis"] + path = tests/execution-apis + url = https://github.com/ethereum/execution-apis diff --git a/tests/all_tests.nim b/tests/all_tests.nim index 51d9904..62b8a8a 100644 --- a/tests/all_tests.nim +++ b/tests/all_tests.nim @@ -19,4 +19,5 @@ import test_signed_tx, test_execution_types, test_string_decoder, - test_contract_dsl + test_contract_dsl, + test_execution_api diff --git a/tests/execution-apis b/tests/execution-apis new file mode 160000 index 0000000..cea7eeb --- /dev/null +++ b/tests/execution-apis @@ -0,0 +1 @@ +Subproject commit cea7eeb642052f4c2e03449dc48296def4aafc24 diff --git a/tests/helpers/handlers.nim b/tests/helpers/handlers.nim new file mode 100644 index 0000000..65f01b1 --- /dev/null +++ b/tests/helpers/handlers.nim @@ -0,0 +1,159 @@ +# json-rpc +# Copyright (c) 2024 Status Research & Development GmbH +# Licensed under either of +# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) +# * MIT license ([LICENSE-MIT](LICENSE-MIT)) +# at your option. +# This file may not be copied, modified, or distributed except according to +# those terms. + +import + stint, + eth/common, + json_rpc/rpcserver, + ../../web3/conversions, + ../../web3/eth_api_types, + ../../web3/primitives as w3 + +type + Hash256 = w3.Hash256 + +proc installHandlers*(server: RpcServer) = + server.rpc("eth_syncing") do(x: JsonString, ) -> bool: + return false + + server.rpc("eth_sendRawTransaction") do(x: JsonString, data: seq[byte]) -> TxHash: + let tx = rlp.decode(data, Transaction) + let h = rlpHash(tx) + return TxHash(h.data) + + server.rpc("eth_getTransactionReceipt") do(x: JsonString, data: TxHash) -> ReceiptObject: + var r: ReceiptObject + if x != "-1".JsonString: + r = JrpcConv.decode(x.string, ReceiptObject) + return r + + server.rpc("eth_getTransactionByHash") do(x: JsonString, data: TxHash) -> TransactionObject: + var tx: TransactionObject + if x != "-1".JsonString: + tx = JrpcConv.decode(x.string, TransactionObject) + return tx + + server.rpc("eth_getTransactionByBlockNumberAndIndex") do(x: JsonString, blockId: RtBlockIdentifier, quantity: Quantity) -> TransactionObject: + var tx: TransactionObject + if x != "-1".JsonString: + tx = JrpcConv.decode(x.string, TransactionObject) + return tx + + server.rpc("eth_getTransactionByBlockHashAndIndex") do(x: JsonString, data: Hash256, quantity: Quantity) -> TransactionObject: + var tx: TransactionObject + if x != "-1".JsonString: + tx = JrpcConv.decode(x.string, TransactionObject) + return tx + + server.rpc("eth_getTransactionCount") do(x: JsonString, data: Address, blockId: RtBlockIdentifier) -> Quantity: + if x != "-1".JsonString: + result = JrpcConv.decode(x.string, Quantity) + + server.rpc("eth_getStorageAt") do(x: JsonString, data: Address, slot: UInt256, blockId: RtBlockIdentifier) -> FixedBytes[32]: + if x != "-1".JsonString: + result = JrpcConv.decode(x.string, FixedBytes[32]) + + server.rpc("eth_getProof") do(x: JsonString, address: Address, slots: seq[UInt256], blockId: RtBlockIdentifier) -> ProofResponse: + var p: ProofResponse + if x != "-1".JsonString: + p = JrpcConv.decode(x.string, ProofResponse) + return p + + server.rpc("eth_getCode") do(x: JsonString, data: Address, blockId: RtBlockIdentifier) -> seq[byte]: + if x != "-1".JsonString: + result = JrpcConv.decode(x.string, seq[byte]) + + server.rpc("eth_getBlockTransactionCountByNumber") do(x: JsonString, blockId: RtBlockIdentifier) -> Quantity: + if x != "-1".JsonString: + result = JrpcConv.decode(x.string, Quantity) + + server.rpc("eth_getBlockTransactionCountByHash") do(x: JsonString, data: BlockHash) -> Quantity: + if x != "-1".JsonString: + result = JrpcConv.decode(x.string, Quantity) + + when NimMajor >= 2: + server.rpc("eth_getBlockReceipts") do(x: JsonString, blockId: RtBlockIdentifier) -> JsonString: + # TODO: cannot prove obj is not nil + return x + else: + server.rpc("eth_getBlockReceipts") do(x: JsonString, blockId: RtBlockIdentifier) -> Option[seq[ReceiptObject]]: + if x == "null".JsonString: + var n: Option[seq[ReceiptObject]] + return n + if x != "-1".JsonString: + let r = JrpcConv.decode(x.string, seq[ReceiptObject]) + return some(r) + + server.rpc("eth_getBlockByNumber") do(x: JsonString, blockId: RtBlockIdentifier, fullTransactions: bool) -> BlockObject: + var blk: BlockObject + if x != "-1".JsonString: + blk = JrpcConv.decode(x.string, BlockObject) + return blk + + server.rpc("eth_getBlockByHash") do(x: JsonString, data: BlockHash, fullTransactions: bool) -> BlockObject: + var blk: BlockObject + if x != "-1".JsonString: + blk = JrpcConv.decode(x.string, BlockObject) + return blk + + server.rpc("eth_getBalance") do(x: JsonString, data: Address, blockId: RtBlockIdentifier) -> UInt256: + if x != "-1".JsonString: + result = JrpcConv.decode(x.string, UInt256) + + server.rpc("eth_feeHistory") do(x: JsonString, blockCount: Quantity, newestBlock: RtBlockIdentifier, rewardPercentiles: Option[seq[float64]]) -> FeeHistoryResult: + var fh: FeeHistoryResult + if x != "-1".JsonString: + fh = JrpcConv.decode(x.string, FeeHistoryResult) + return fh + + server.rpc("eth_estimateGas") do(x: JsonString, call: EthCall) -> Quantity: + if x != "-1".JsonString: + result = JrpcConv.decode(x.string, Quantity) + + server.rpc("eth_createAccessList") do(x: JsonString, call: EthCall, blockId: RtBlockIdentifier) -> AccessListResult: + var z: AccessListResult + if x != "-1".JsonString: + z = JrpcConv.decode(x.string, AccessListResult) + return z + + server.rpc("eth_chainId") do(x: JsonString, ) -> Quantity: + if x != "-1".JsonString: + result = JrpcConv.decode(x.string, Quantity) + + server.rpc("eth_call") do(x: JsonString, call: EthCall, blockId: RtBlockIdentifier) -> seq[byte]: + if x != "-1".JsonString: + result = JrpcConv.decode(x.string, seq[byte]) + + server.rpc("eth_blockNumber") do(x: JsonString) -> Quantity: + if x != "-1".JsonString: + result = JrpcConv.decode(x.string, Quantity) + + server.rpc("debug_getRawTransaction") do(x: JsonString, data: TxHash) -> RlpEncodedBytes: + var res: seq[byte] + if x != "-1".JsonString: + res = JrpcConv.decode(x.string, seq[byte]) + return res.RlpEncodedBytes + + server.rpc("debug_getRawReceipts") do(x: JsonString, blockId: RtBlockIdentifier) -> seq[RlpEncodedBytes]: + var res: seq[RlpEncodedBytes] + if x != "-1".JsonString: + res = JrpcConv.decode(x.string, seq[RlpEncodedBytes]) + return res + + server.rpc("debug_getRawHeader") do(x: JsonString, blockId: RtBlockIdentifier) -> RlpEncodedBytes: + var res: seq[byte] + if x != "-1".JsonString: + res = JrpcConv.decode(x.string, seq[byte]) + return res.RlpEncodedBytes + + server.rpc("debug_getRawBlock") do(x: JsonString, blockId: RtBlockIdentifier) -> RlpEncodedBytes: + var res: seq[byte] + if x != "-1".JsonString: + res = JrpcConv.decode(x.string, seq[byte]) + return res.RlpEncodedBytes diff --git a/tests/test_execution_api.nim b/tests/test_execution_api.nim new file mode 100644 index 0000000..216a071 --- /dev/null +++ b/tests/test_execution_api.nim @@ -0,0 +1,110 @@ +import + std/[os, strutils], + pkg/unittest2, + chronos, + json_rpc/[rpcclient, rpcserver], + json_rpc/private/jrpc_sys, + ../web3/conversions, + ./helpers/handlers + +type + TestData = tuple + file: string + input: RequestTx + output: ResponseRx + +const + inputPath = "tests/execution-apis/tests" + +func strip(line: string): string = + return line[3..^1] + +func toTx(req: RequestRx): RequestTx = + RequestTx( + id: Opt.some(req.id), + `method`: req.`method`.get(), + params: req.params.toTx, + ) + +proc extractTest(fileName: string): TestData = + let + lines = readFile(fileName).split("\n") + input = lines[0].strip() + output = lines[1].strip() + + return ( + file: fileName, + input: JrpcSys.decode(input, RequestRx).toTx, + output: JrpcSys.decode(output, ResponseRx), + ) + +proc extractTests(): seq[TestData] = + for fileName in walkDirRec(inputPath): + if fileName.endsWith(".io"): + result.add(fileName.extractTest()) + +proc callWithParams(client: RpcClient, data: TestData): Future[bool] {.async.} = + let res = data.output + + try: + var params = data.input.params + if data.output.result.string.len > 0: + params.positional.insert(data.output.result, 0) + else: + params.positional.insert("-1".JsonString, 0) + + let resJson = await client.call(data.input.`method`, params) + + if res.result.string.len > 0: + let wantVal = JrpcConv.decode(res.result.string, JsonValueRef[string]) + let getVal = JrpcConv.decode(resJson.string, JsonValueRef[string]) + + if wantVal != getVal: + debugEcho data.file + debugEcho "EXPECT: ", res.result + debugEcho "GET: ", resJson.string + return false + + return true + except SerializationError as exc: + debugEcho data.file + debugEcho exc.formatMsg("xxx") + return false + except CatchableError as exc: + if res.error.isSome: + return true + debugEcho data.file + debugEcho exc.msg + return false + +const allowedToFail = [ + "fee-history.io" # float roundtrip not match +] + +suite "Ethereum execution api": + let testCases = extractTests() + if testCases.len < 1: + raise newException(ValueError, "execution_api tests not found, did you clone?") + + var srv = newRpcHttpServer(["127.0.0.1:0"]) + srv.installHandlers() + srv.start() + + for idx, item in testCases: + let input = item.input + let methodName = input.`method` + + test methodName: + let (_, fileName, ext) = splitFile(item.file) + let client = newRpcHttpClient() + waitFor client.connect("http://" & $srv.localAddress()[0]) + let response = waitFor client.callWithParams(item) + let source = fileName & ext + if source in allowedToFail: + check true + else: + check response + waitFor client.close() + + waitFor srv.stop() + waitFor srv.closeWait() diff --git a/tests/test_logs.nim b/tests/test_logs.nim index 5fba3ad..deb693d 100644 --- a/tests/test_logs.nim +++ b/tests/test_logs.nim @@ -33,7 +33,7 @@ contract(LoggerContract): proc MyEvent(sender: Address, number: UInt256) {.event.} proc invoke(value: UInt256) -const LoggerContractCode = "6080604052348015600f57600080fd5b5060bc8061001e6000396000f3fe6080604052348015600f57600080fd5b506004361060285760003560e01c80632b30d2b814602d575b600080fd5b604760048036036020811015604157600080fd5b50356049565b005b604080513381526020810183905281517fdf50c7bb3b25f812aedef81bc334454040e7b27e27de95a79451d663013b7e17929181900390910190a15056fea265627a7a723058202ed7f5086297d2a49fbe359f4e489a007b69eb5077f5c76328bffdb63f164b4b64736f6c63430005090032" +const LoggerContractCode = "6080604052348015600f57600080fd5b5060fb8061001e6000396000f3fe6080604052348015600f57600080fd5b506004361060285760003560e01c80632b30d2b814602d575b600080fd5b605660048036036020811015604157600080fd5b81019080803590602001909291905050506058565b005b7fdf50c7bb3b25f812aedef81bc334454040e7b27e27de95a79451d663013b7e173382604051808373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020018281526020019250505060405180910390a15056fea265627a7a72315820cb9980a67d78ee2e84fedf080db8463ce4a944fccf8b5512448163aaff0aea8964736f6c63430005110032" var contractAddress = Address.fromHex("0xEA255DeA28c84F698Fa195f87fC83D1d4125ef9C") @@ -48,7 +48,6 @@ suite "Logs": # let q = await web3.provider.eth_blockNumber() echo "block: ", uint64(await web3.provider.eth_blockNumber()) - block: # LoggerContract let receipt = await web3.deployContract(LoggerContractCode) contractAddress = receipt.contractAddress.get diff --git a/web3/conversions.nim b/web3/conversions.nim index 49e09de..a061301 100644 --- a/web3/conversions.nim +++ b/web3/conversions.nim @@ -42,6 +42,7 @@ ProofResponse.useDefaultSerializationIn JrpcConv FilterOptions.useDefaultSerializationIn JrpcConv EthSend.useDefaultSerializationIn JrpcConv EthCall.useDefaultSerializationIn JrpcConv +FeeHistoryResult.useDefaultSerializationIn JrpcConv derefType(BlockHeader).useDefaultSerializationIn JrpcConv derefType(BlockObject).useDefaultSerializationIn JrpcConv @@ -290,7 +291,7 @@ proc readValue*(r: var JsonReader[JrpcConv], val: var RtBlockIdentifier) val = RtBlockIdentifier(kind: bidNumber, number: fromHex[uint64](hexStr)) else: val = RtBlockIdentifier(kind: bidAlias, alias: hexStr) - + proc writeValue*(w: var JsonWriter[JrpcConv], v: RtBlockIdentifier) {.gcsafe, raises: [IOError].} = case v.kind diff --git a/web3/eth_api.nim b/web3/eth_api.nim index d229bec..3f78108 100644 --- a/web3/eth_api.nim +++ b/web3/eth_api.nim @@ -35,10 +35,11 @@ createRpcSigsFromNim(RpcClient): proc eth_accounts(): seq[Address] proc eth_blockNumber(): Quantity proc eth_getBalance(data: Address, blockId: BlockIdentifier): UInt256 - proc eth_getStorageAt(data: Address, slot: UInt256, blockId: BlockIdentifier): UInt256 + proc eth_getStorageAt(data: Address, slot: UInt256, blockId: BlockIdentifier): FixedBytes[32] proc eth_getTransactionCount(data: Address, blockId: BlockIdentifier): Quantity proc eth_getBlockTransactionCountByHash(data: BlockHash): Quantity proc eth_getBlockTransactionCountByNumber(blockId: BlockIdentifier): Quantity + proc eth_getBlockReceipts(blockId: BlockIdentifier): Option[seq[ReceiptObject]] proc eth_getUncleCountByBlockHash(data: BlockHash): Quantity proc eth_getUncleCountByBlockNumber(blockId: BlockIdentifier): Quantity proc eth_getCode(data: Address, blockId: BlockIdentifier): seq[byte] @@ -47,7 +48,7 @@ createRpcSigsFromNim(RpcClient): proc eth_sendTransaction(obj: EthSend): TxHash proc eth_sendRawTransaction(data: seq[byte]): TxHash proc eth_call(call: EthCall, blockId: BlockIdentifier): seq[byte] - proc eth_estimateGas(call: EthCall, blockId: BlockIdentifier): Quantity + proc eth_estimateGas(call: EthCall): Quantity proc eth_createAccessList(call: EthCall, blockId: BlockIdentifier): AccessListResult proc eth_getBlockByHash(data: BlockHash, fullTransactions: bool): BlockObject proc eth_getBlockByNumber(blockId: BlockIdentifier, fullTransactions: bool): BlockObject @@ -82,5 +83,15 @@ createRpcSigsFromNim(RpcClient): slots: seq[UInt256], blockId: BlockIdentifier): ProofResponse + proc eth_feeHistory( + blockCount: Quantity, + newestBlock: BlockIdentifier, + rewardPercentiles: Option[seq[float64]]): FeeHistoryResult + + proc debug_getRawBlock(blockId: BlockIdentifier): RlpEncodedBytes + proc debug_getRawHeader(blockId: BlockIdentifier): RlpEncodedBytes + proc debug_getRawReceipts(blockId: BlockIdentifier): seq[RlpEncodedBytes] + proc debug_getRawTransaction(data: TxHash): RlpEncodedBytes + createSingleRpcSig(RpcClient, "eth_getJsonLogs"): proc eth_getLogs(filterOptions: FilterOptions): seq[JsonString] diff --git a/web3/eth_api_types.nim b/web3/eth_api_types.nim index 3c36824..507d40d 100644 --- a/web3/eth_api_types.nim +++ b/web3/eth_api_types.nim @@ -237,6 +237,14 @@ type of bidAlias: alias*: string + FeeHistoryReward* = array[2, Quantity] + + FeeHistoryResult* = object + oldestBlock*: Quantity + baseFeePerGas*: seq[Quantity] + gasUsedRatio*: seq[float64] + reward*: seq[FeeHistoryReward] + {.push raises: [].} func blockId*(n: BlockNumber): RtBlockIdentifier =