From 264aca112c9bcc992a93a7f4b13ba6ec61c7fba3 Mon Sep 17 00:00:00 2001 From: Igor Artamonov Date: Mon, 25 Nov 2024 20:04:15 +0000 Subject: [PATCH] problem: eth_simulateV1 returns an array, and there is no standard way to handle this solution: make RpcCall using JavaType instead of Class to support all types of data, including generics and arrays --- .../etherjar/rpc/AbstractRpcTransport.java | 6 +- .../emeraldpay/etherjar/rpc/EthCommands.java | 17 ++-- .../etherjar/rpc/ParityCommands.java | 5 +- .../etherjar/rpc/EthCommandsSpec.groovy | 92 +++++++++++-------- .../etherjar/rpc/NetCommandsSpec.groovy | 6 +- .../etherjar/rpc/ParityCommandsSpec.groovy | 7 +- .../etherjar/rpc/Web3CommandsSpec.groovy | 7 +- .../etherjar/rpc/BatchCallContext.java | 6 +- .../io/emeraldpay/etherjar/rpc/BatchItem.java | 6 +- .../etherjar/rpc/JacksonRpcConverter.java | 42 +++------ .../io/emeraldpay/etherjar/rpc/RpcCall.java | 83 ++++++++++++++--- .../emeraldpay/etherjar/rpc/RpcConverter.java | 6 +- .../io/emeraldpay/etherjar/rpc/TraceList.java | 30 ------ .../rpc/JacksonRpcConverterSpec.groovy | 39 +++++--- .../rpc/json/BlockSimulatedJsonSpec.groovy | 28 ++++++ .../rpc/json/TraceItemJsonSpec.groovy | 22 ++--- 16 files changed, 247 insertions(+), 155 deletions(-) delete mode 100644 etherjar-rpc-json/src/main/java/io/emeraldpay/etherjar/rpc/TraceList.java diff --git a/etherjar-rpc-api/src/main/java/io/emeraldpay/etherjar/rpc/AbstractRpcTransport.java b/etherjar-rpc-api/src/main/java/io/emeraldpay/etherjar/rpc/AbstractRpcTransport.java index 3b1fc93d..26676b99 100644 --- a/etherjar-rpc-api/src/main/java/io/emeraldpay/etherjar/rpc/AbstractRpcTransport.java +++ b/etherjar-rpc-api/src/main/java/io/emeraldpay/etherjar/rpc/AbstractRpcTransport.java @@ -15,6 +15,8 @@ */ package io.emeraldpay.etherjar.rpc; +import com.fasterxml.jackson.databind.JavaType; + import java.io.IOException; import java.io.InputStream; import java.util.*; @@ -46,7 +48,7 @@ public CompletableFuture> execute(List requests = new HashMap<>(items.size()); - Map responseMapping = new HashMap<>(items.size()); + Map responseMapping = new HashMap<>(items.size()); List> rpcRequests = items.stream() .map(item -> { RequestJson request = new RequestJson<>( @@ -89,7 +91,7 @@ protected Function, RpcCallResponse> return (resp) -> { RpcCall call = requests.get(resp.getId()).getCall(); if (call != null) { - ResponseJson castResp = resp.cast(call.getJsonType()); + ResponseJson castResp = resp.cast((Class) call.getJsonType().getRawClass()); return responseJsonConverter.convert(call, castResp); } return null; diff --git a/etherjar-rpc-api/src/main/java/io/emeraldpay/etherjar/rpc/EthCommands.java b/etherjar-rpc-api/src/main/java/io/emeraldpay/etherjar/rpc/EthCommands.java index b33ab32c..93d550e0 100644 --- a/etherjar-rpc-api/src/main/java/io/emeraldpay/etherjar/rpc/EthCommands.java +++ b/etherjar-rpc-api/src/main/java/io/emeraldpay/etherjar/rpc/EthCommands.java @@ -15,6 +15,8 @@ */ package io.emeraldpay.etherjar.rpc; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.type.TypeFactory; import io.emeraldpay.etherjar.domain.*; import io.emeraldpay.etherjar.hex.Hex32; import io.emeraldpay.etherjar.hex.HexData; @@ -23,6 +25,9 @@ public class EthCommands { + private final JavaType blockWithTxJsonType = TypeFactory.defaultInstance().constructParametricType(BlockJson.class, TransactionJson.class); + private final JavaType blockWithTxIdType = TypeFactory.defaultInstance().constructParametricType(BlockJson.class, TransactionRefJson.class); + private final Class> blockWithTxJson = getBlockWithTx(); private final Class> blockWithTxId = getBlockWithRef(); @@ -73,7 +78,7 @@ public RpcCall getBalance(Address address, long block) { */ public RpcCall, BlockJson> getBlock(long blockNumber) { return RpcCall.create("eth_getBlockByNumber", blockWithTxId, HexQuantity.from(blockNumber).toHex(), false) - .withJsonType(blockWithTxId) + .withJsonType(blockWithTxIdType).castJsonType(blockWithTxId) .withResultType(blockWithTxId); } @@ -85,7 +90,7 @@ public RpcCall, BlockJson> get */ public RpcCall, BlockJson> getBlockWithTransactions(long blockNumber) { return RpcCall.create("eth_getBlockByNumber", blockWithTxJson, HexQuantity.from(blockNumber).toHex(), true) - .withJsonType(blockWithTxJson) + .withJsonType(blockWithTxJsonType).castJsonType(blockWithTxJson) .withResultType(blockWithTxJson); } @@ -96,7 +101,7 @@ public RpcCall, BlockJson> getBlockW */ public RpcCall, BlockJson> getBlock(BlockHash hash) { return RpcCall.create("eth_getBlockByHash", blockWithTxId, hash.toHex(), false) - .withJsonType(blockWithTxId) + .withJsonType(blockWithTxIdType).castJsonType(blockWithTxId) .withResultType(blockWithTxId); } @@ -107,7 +112,7 @@ public RpcCall, BlockJson> get */ public RpcCall, BlockJson> getBlockWithTransactions(BlockHash hash) { return RpcCall.create("eth_getBlockByHash", blockWithTxJson, hash.toHex(), true) - .withJsonType(blockWithTxJson) + .withJsonType(blockWithTxJsonType).castJsonType(blockWithTxJson) .withResultType(blockWithTxJson); } @@ -355,8 +360,8 @@ public RpcCall call(TransactionCallJson call, BlockTag block) { * @param block The simulated blocks will be built on top of this. If null, the current block is used. * @return simulated block */ - public RpcCall simulateV1(SimulateJson payload, BlockTag block) { - return RpcCall.create("eth_simulateV1", BlockSimulatedJson.class, payload, block.getCode()); + public RpcCall simulateV1(SimulateJson payload, BlockTag block) { + return RpcCall.create("eth_simulateV1", BlockSimulatedJson.class, payload, block.getCode()).asArray(); } /** diff --git a/etherjar-rpc-api/src/main/java/io/emeraldpay/etherjar/rpc/ParityCommands.java b/etherjar-rpc-api/src/main/java/io/emeraldpay/etherjar/rpc/ParityCommands.java index 90518a1a..97c5e030 100644 --- a/etherjar-rpc-api/src/main/java/io/emeraldpay/etherjar/rpc/ParityCommands.java +++ b/etherjar-rpc-api/src/main/java/io/emeraldpay/etherjar/rpc/ParityCommands.java @@ -16,6 +16,7 @@ package io.emeraldpay.etherjar.rpc; import io.emeraldpay.etherjar.domain.TransactionId; +import io.emeraldpay.etherjar.rpc.json.TraceItemJson; /** * Commands specific for Parity Ethereum @@ -28,7 +29,7 @@ public class ParityCommands { * @param hash hash of the transaction * @return trace list */ - public RpcCall traceTransaction(TransactionId hash) { - return RpcCall.create("trace_transaction", TraceList.class, hash.toHex()); + public RpcCall traceTransaction(TransactionId hash) { + return RpcCall.create("trace_transaction", TraceItemJson.class, hash.toHex()).asArray(); } } diff --git a/etherjar-rpc-api/src/test/groovy/io/emeraldpay/etherjar/rpc/EthCommandsSpec.groovy b/etherjar-rpc-api/src/test/groovy/io/emeraldpay/etherjar/rpc/EthCommandsSpec.groovy index fc2c52b6..dd39a733 100644 --- a/etherjar-rpc-api/src/test/groovy/io/emeraldpay/etherjar/rpc/EthCommandsSpec.groovy +++ b/etherjar-rpc-api/src/test/groovy/io/emeraldpay/etherjar/rpc/EthCommandsSpec.groovy @@ -23,11 +23,14 @@ import io.emeraldpay.etherjar.domain.Wei import io.emeraldpay.etherjar.hex.Hex32 import io.emeraldpay.etherjar.hex.HexData import io.emeraldpay.etherjar.rpc.json.BlockJson +import io.emeraldpay.etherjar.rpc.json.BlockSimulatedJson import io.emeraldpay.etherjar.rpc.json.BlockTag +import io.emeraldpay.etherjar.rpc.json.SimulateJson import io.emeraldpay.etherjar.rpc.json.SyncingJson import io.emeraldpay.etherjar.rpc.json.TransactionCallJson import io.emeraldpay.etherjar.rpc.json.TransactionJson import io.emeraldpay.etherjar.rpc.json.TransactionReceiptJson +import io.emeraldpay.etherjar.rpc.json.TransactionRefJson import spock.lang.Specification class EthCommandsSpec extends Specification { @@ -39,7 +42,7 @@ class EthCommandsSpec extends Specification { then: call.method == "eth_blockNumber" call.params == [] - call.jsonType == String + call.jsonType.rawClass == String call.resultType == Long } @@ -50,7 +53,7 @@ class EthCommandsSpec extends Specification { then: call.method == "eth_getBalance" call.params == ['0xf45c301e123a068badac079d0cff1a9e4ad51911', '0x3e8'] - call.jsonType == String + call.jsonType.rawClass == String call.resultType == Wei when: @@ -59,7 +62,7 @@ class EthCommandsSpec extends Specification { then: call.method == "eth_getBalance" call.params == ['0xf45c301e123a068badac079d0cff1a9e4ad51911', 'latest'] - call.jsonType == String + call.jsonType.rawClass == String call.resultType == Wei } @@ -70,7 +73,8 @@ class EthCommandsSpec extends Specification { then: call.method == "eth_getBlockByNumber" call.params == ['0x3e8', false] - call.jsonType == BlockJson + call.jsonType.rawClass == BlockJson + call.jsonType.containedType(0).rawClass == TransactionRefJson.class call.resultType == BlockJson when: @@ -79,7 +83,8 @@ class EthCommandsSpec extends Specification { then: call.method == "eth_getBlockByNumber" call.params == ['0x3e8', true] - call.jsonType == BlockJson + call.jsonType.rawClass == BlockJson + call.jsonType.containedType(0).rawClass == TransactionJson.class call.resultType == BlockJson when: @@ -88,7 +93,8 @@ class EthCommandsSpec extends Specification { then: call.method == "eth_getBlockByHash" call.params == ['0x4eeb9aa586c63c0f1ce033e6c6fb44b4db1ffe8c5e19c93e2b768c71b5f9cb9c', false] - call.jsonType == BlockJson + call.jsonType.rawClass == BlockJson + call.jsonType.containedType(0).rawClass == TransactionRefJson.class call.resultType == BlockJson when: @@ -97,10 +103,22 @@ class EthCommandsSpec extends Specification { then: call.method == "eth_getBlockByHash" call.params == ['0x4eeb9aa586c63c0f1ce033e6c6fb44b4db1ffe8c5e19c93e2b768c71b5f9cb9c', true] - call.jsonType == BlockJson + call.jsonType.rawClass == BlockJson + call.jsonType.containedType(0).rawClass == TransactionJson.class call.resultType == BlockJson } + def simulateV1() { + when: + def call = Commands.eth().simulateV1(new SimulateJson(), BlockTag.EARLIEST) + + then: + call.method == "eth_simulateV1" + call.jsonType.arrayType + call.jsonType.contentType.rawClass == BlockSimulatedJson.class + call.resultType == BlockSimulatedJson[] + } + def getTransaction() { when: def call = Commands.eth().getTransaction(TransactionId.from("0x18c3ba292e0388fdbcb3789feabc7312fba679f2a7ddc0f5611ce187b32a1d2b")) @@ -108,7 +126,7 @@ class EthCommandsSpec extends Specification { then: call.method == "eth_getTransactionByHash" call.params == ['0x18c3ba292e0388fdbcb3789feabc7312fba679f2a7ddc0f5611ce187b32a1d2b'] - call.jsonType == TransactionJson + call.jsonType.rawClass == TransactionJson call.resultType == TransactionJson when: @@ -117,7 +135,7 @@ class EthCommandsSpec extends Specification { then: call.method == "eth_getTransactionByBlockNumberAndIndex" call.params == ['0x3e8', '0x5'] - call.jsonType == TransactionJson + call.jsonType.rawClass == TransactionJson call.resultType == TransactionJson when: @@ -126,7 +144,7 @@ class EthCommandsSpec extends Specification { then: call.method == "eth_getTransactionByBlockHashAndIndex" call.params == ['0x4eeb9aa586c63c0f1ce033e6c6fb44b4db1ffe8c5e19c93e2b768c71b5f9cb9c', '0x5'] - call.jsonType == TransactionJson + call.jsonType.rawClass == TransactionJson call.resultType == TransactionJson } @@ -137,7 +155,7 @@ class EthCommandsSpec extends Specification { then: call.method == "eth_getTransactionReceipt" call.params == ['0x18c3ba292e0388fdbcb3789feabc7312fba679f2a7ddc0f5611ce187b32a1d2b'] - call.jsonType == TransactionReceiptJson + call.jsonType.rawClass == TransactionReceiptJson call.resultType == TransactionReceiptJson } @@ -148,7 +166,7 @@ class EthCommandsSpec extends Specification { then: call.method == "eth_getTransactionCount" call.params == ['0xf45c301e123a068badac079d0cff1a9e4ad51911', '0x3e8'] - call.jsonType == String + call.jsonType.rawClass == String call.resultType == Long when: @@ -157,7 +175,7 @@ class EthCommandsSpec extends Specification { then: call.method == "eth_getTransactionCount" call.params == ['0xf45c301e123a068badac079d0cff1a9e4ad51911', 'earliest'] - call.jsonType == String + call.jsonType.rawClass == String call.resultType == Long when: @@ -166,7 +184,7 @@ class EthCommandsSpec extends Specification { then: call.method == "eth_getTransactionCount" call.params == ['0xf45c301e123a068badac079d0cff1a9e4ad51911', 'latest'] - call.jsonType == String + call.jsonType.rawClass == String call.resultType == Long when: @@ -175,7 +193,7 @@ class EthCommandsSpec extends Specification { then: call.method == "eth_getBlockTransactionCountByHash" call.params == ['0x4eeb9aa586c63c0f1ce033e6c6fb44b4db1ffe8c5e19c93e2b768c71b5f9cb9c'] - call.jsonType == String + call.jsonType.rawClass == String call.resultType == Long when: @@ -184,7 +202,7 @@ class EthCommandsSpec extends Specification { then: call.method == "eth_getBlockTransactionCountByNumber" call.params == ['0x3e8'] - call.jsonType == String + call.jsonType.rawClass == String call.resultType == Long } @@ -195,7 +213,7 @@ class EthCommandsSpec extends Specification { then: call.method == "eth_getUncleCountByBlockNumber" call.params == ['0x3e8'] - call.jsonType == String + call.jsonType.rawClass == String call.resultType == Long when: @@ -204,7 +222,7 @@ class EthCommandsSpec extends Specification { then: call.method == "eth_getUncleCountByBlockHash" call.params == ['0x4eeb9aa586c63c0f1ce033e6c6fb44b4db1ffe8c5e19c93e2b768c71b5f9cb9c'] - call.jsonType == String + call.jsonType.rawClass == String call.resultType == Long } @@ -215,7 +233,7 @@ class EthCommandsSpec extends Specification { then: call.method == "eth_getUncleByBlockNumberAndIndex" call.params == ['0x3e8', '0x2'] - call.jsonType == BlockJson + call.jsonType.rawClass == BlockJson call.resultType == BlockJson when: @@ -224,7 +242,7 @@ class EthCommandsSpec extends Specification { then: call.method == "eth_getUncleByBlockHashAndIndex" call.params == ['0x4eeb9aa586c63c0f1ce033e6c6fb44b4db1ffe8c5e19c93e2b768c71b5f9cb9c', '0x1'] - call.jsonType == BlockJson + call.jsonType.rawClass == BlockJson call.resultType == BlockJson } @@ -235,7 +253,7 @@ class EthCommandsSpec extends Specification { then: call.method == "eth_getCode" call.params == ['0xf45c301e123a068badac079d0cff1a9e4ad51911', '0x3e8'] - call.jsonType == String + call.jsonType.rawClass == String call.resultType == HexData when: @@ -244,7 +262,7 @@ class EthCommandsSpec extends Specification { then: call.method == "eth_getCode" call.params == ['0xf45c301e123a068badac079d0cff1a9e4ad51911', 'latest'] - call.jsonType == String + call.jsonType.rawClass == String call.resultType == HexData } @@ -255,7 +273,7 @@ class EthCommandsSpec extends Specification { then: call.method == "eth_getWork" call.params == [] - call.jsonType == String[] + call.jsonType.rawClass == String[] call.resultType == HexData[] } @@ -270,7 +288,7 @@ class EthCommandsSpec extends Specification { call.params == ["0x0000000000000001", "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "0xd1fe5700000000000000000000000000d1fe5700000000000000000000000000"] - call.jsonType == Boolean + call.jsonType.rawClass == Boolean call.resultType == Boolean } @@ -284,7 +302,7 @@ class EthCommandsSpec extends Specification { call.method == "eth_submitHashrate" call.params == ["0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "0xd1fe5700000000000000000000000000d1fe5700000000000000000000000000"] - call.jsonType == Boolean + call.jsonType.rawClass == Boolean call.resultType == Boolean } @@ -295,7 +313,7 @@ class EthCommandsSpec extends Specification { then: call.method == "eth_coinbase" call.params == [] - call.jsonType == String + call.jsonType.rawClass == String call.resultType == Address } @@ -306,7 +324,7 @@ class EthCommandsSpec extends Specification { then: call.method == "eth_hashrate" call.params == [] - call.jsonType == String + call.jsonType.rawClass == String call.resultType == Long } @@ -317,7 +335,7 @@ class EthCommandsSpec extends Specification { then: call.method == "eth_mining" call.params == [] - call.jsonType == Boolean + call.jsonType.rawClass == Boolean call.resultType == Boolean } @@ -328,7 +346,7 @@ class EthCommandsSpec extends Specification { then: call.method == "eth_gasPrice" call.params == [] - call.jsonType == String + call.jsonType.rawClass == String call.resultType == Wei } @@ -339,7 +357,7 @@ class EthCommandsSpec extends Specification { then: call.method == "eth_accounts" call.params == [] - call.jsonType == String[] + call.jsonType.rawClass == String[] call.resultType == Address[] } @@ -350,7 +368,7 @@ class EthCommandsSpec extends Specification { then: call.method == "eth_chainId" call.params == [] - call.jsonType == String + call.jsonType.rawClass == String call.resultType == Long } @@ -362,7 +380,7 @@ class EthCommandsSpec extends Specification { then: call.method == "eth_call" call.params == [tx, 'latest'] - call.jsonType == String + call.jsonType.rawClass == String call.resultType == HexData when: @@ -371,7 +389,7 @@ class EthCommandsSpec extends Specification { then: call.method == "eth_call" call.params == [tx, '0xaafcb3'] - call.jsonType == String + call.jsonType.rawClass == String call.resultType == HexData } @@ -383,7 +401,7 @@ class EthCommandsSpec extends Specification { then: call.method == "eth_sendTransaction" call.params == [tx] - call.jsonType == String + call.jsonType.rawClass == String call.resultType == TransactionId when: @@ -392,7 +410,7 @@ class EthCommandsSpec extends Specification { then: call.method == "eth_sendRawTransaction" call.params == ['0x00'] - call.jsonType == String + call.jsonType.rawClass == String call.resultType == TransactionId } @@ -403,7 +421,7 @@ class EthCommandsSpec extends Specification { then: call.method == "eth_sign" call.params == ['0xf45c301e123a068badac079d0cff1a9e4ad51911', '0x1234'] - call.jsonType == String + call.jsonType.rawClass == String call.resultType == HexData } @@ -414,7 +432,7 @@ class EthCommandsSpec extends Specification { then: call.method == "eth_syncing" call.params == [] - call.jsonType == SyncingJson + call.jsonType.rawClass == SyncingJson call.resultType == SyncingJson } } diff --git a/etherjar-rpc-api/src/test/groovy/io/emeraldpay/etherjar/rpc/NetCommandsSpec.groovy b/etherjar-rpc-api/src/test/groovy/io/emeraldpay/etherjar/rpc/NetCommandsSpec.groovy index 3365fffd..c9359afb 100644 --- a/etherjar-rpc-api/src/test/groovy/io/emeraldpay/etherjar/rpc/NetCommandsSpec.groovy +++ b/etherjar-rpc-api/src/test/groovy/io/emeraldpay/etherjar/rpc/NetCommandsSpec.groovy @@ -26,7 +26,7 @@ class NetCommandsSpec extends Specification { then: call.method == "net_version" call.params == [] - call.jsonType == String + call.jsonType.rawClass == String call.resultType == Integer } @@ -37,7 +37,7 @@ class NetCommandsSpec extends Specification { then: call.method == "net_listening" call.params == [] - call.jsonType == Boolean + call.jsonType.rawClass == Boolean call.resultType == Boolean } @@ -48,7 +48,7 @@ class NetCommandsSpec extends Specification { then: call.method == "net_peerCount" call.params == [] - call.jsonType == String + call.jsonType.rawClass == String call.resultType == Integer } diff --git a/etherjar-rpc-api/src/test/groovy/io/emeraldpay/etherjar/rpc/ParityCommandsSpec.groovy b/etherjar-rpc-api/src/test/groovy/io/emeraldpay/etherjar/rpc/ParityCommandsSpec.groovy index c4d84a38..1aab492e 100644 --- a/etherjar-rpc-api/src/test/groovy/io/emeraldpay/etherjar/rpc/ParityCommandsSpec.groovy +++ b/etherjar-rpc-api/src/test/groovy/io/emeraldpay/etherjar/rpc/ParityCommandsSpec.groovy @@ -16,6 +16,7 @@ package io.emeraldpay.etherjar.rpc import io.emeraldpay.etherjar.domain.TransactionId +import io.emeraldpay.etherjar.rpc.json.TraceItemJson import spock.lang.Specification class ParityCommandsSpec extends Specification { @@ -27,7 +28,9 @@ class ParityCommandsSpec extends Specification { then: call.method == "trace_transaction" call.params == ['0x6013af0b08e2f0e8dacdfcf482813c7e1898ec2b3cd21da6d2c16ce9899a2d27'] - call.jsonType == TraceList - call.resultType == TraceList + call.jsonType.isArrayType() + call.jsonType.rawClass == TraceItemJson[].class + call.resultType.isArray() + call.resultType == TraceItemJson[].class } } diff --git a/etherjar-rpc-api/src/test/groovy/io/emeraldpay/etherjar/rpc/Web3CommandsSpec.groovy b/etherjar-rpc-api/src/test/groovy/io/emeraldpay/etherjar/rpc/Web3CommandsSpec.groovy index f9eba23c..40d47558 100644 --- a/etherjar-rpc-api/src/test/groovy/io/emeraldpay/etherjar/rpc/Web3CommandsSpec.groovy +++ b/etherjar-rpc-api/src/test/groovy/io/emeraldpay/etherjar/rpc/Web3CommandsSpec.groovy @@ -15,6 +15,7 @@ */ package io.emeraldpay.etherjar.rpc +import io.emeraldpay.etherjar.hex.Hex32 import io.emeraldpay.etherjar.hex.HexData import spock.lang.Specification @@ -27,7 +28,7 @@ class Web3CommandsSpec extends Specification { then: call.method == "web3_clientVersion" call.params == [] - call.jsonType == String + call.jsonType.rawClass == String call.resultType == String } @@ -38,7 +39,7 @@ class Web3CommandsSpec extends Specification { then: call.method == "web3_sha3" call.params == ["0x00"] - call.jsonType == String - call.resultType == io.emeraldpay.etherjar.hex.Hex32 + call.jsonType.rawClass == String + call.resultType == Hex32 } } diff --git a/etherjar-rpc-json/src/main/java/io/emeraldpay/etherjar/rpc/BatchCallContext.java b/etherjar-rpc-json/src/main/java/io/emeraldpay/etherjar/rpc/BatchCallContext.java index befcc874..b300e857 100644 --- a/etherjar-rpc-json/src/main/java/io/emeraldpay/etherjar/rpc/BatchCallContext.java +++ b/etherjar-rpc-json/src/main/java/io/emeraldpay/etherjar/rpc/BatchCallContext.java @@ -15,12 +15,14 @@ */ package io.emeraldpay.etherjar.rpc; +import com.fasterxml.jackson.databind.JavaType; + import java.util.HashMap; import java.util.Map; public class BatchCallContext { private final Map sourceMapping = new HashMap<>(); - private final Map jsonTypes = new HashMap<>(); + private final Map jsonTypes = new HashMap<>(); private final Map callMapping = new HashMap<>(); public int add(T item) { @@ -31,7 +33,7 @@ public int add(T item) { return current; } - public Map getJsonTypes() { + public Map getJsonTypes() { return jsonTypes; } diff --git a/etherjar-rpc-json/src/main/java/io/emeraldpay/etherjar/rpc/BatchItem.java b/etherjar-rpc-json/src/main/java/io/emeraldpay/etherjar/rpc/BatchItem.java index a2870276..3d8e26d8 100644 --- a/etherjar-rpc-json/src/main/java/io/emeraldpay/etherjar/rpc/BatchItem.java +++ b/etherjar-rpc-json/src/main/java/io/emeraldpay/etherjar/rpc/BatchItem.java @@ -15,6 +15,8 @@ */ package io.emeraldpay.etherjar.rpc; +import com.fasterxml.jackson.databind.JavaType; + import java.util.Objects; public abstract class BatchItem implements AutoCloseable { @@ -27,7 +29,7 @@ protected BatchItem(int id, RpcCall call) { } @SuppressWarnings("unchecked") - public BatchItem cast(Class jsType, Class javaType) { + public BatchItem cast(JavaType jsType, Class javaType) { return (BatchItem) this; } @@ -87,7 +89,7 @@ public boolean read(ResponseJson resp) { } else if (resp.getResult() == null) { onComplete(null); return true; - } else if (!call.getJsonType().isAssignableFrom(resp.getResult().getClass())) { + } else if (!call.getJsonType().getRawClass().isAssignableFrom(resp.getResult().getClass())) { throw new ClassCastException("Expected " + call.getJsonType() + " but received " + resp.getResult().getClass()); } else { onComplete((JS) resp.getResult()); diff --git a/etherjar-rpc-json/src/main/java/io/emeraldpay/etherjar/rpc/JacksonRpcConverter.java b/etherjar-rpc-json/src/main/java/io/emeraldpay/etherjar/rpc/JacksonRpcConverter.java index 0b207b98..83df315b 100644 --- a/etherjar-rpc-json/src/main/java/io/emeraldpay/etherjar/rpc/JacksonRpcConverter.java +++ b/etherjar-rpc-json/src/main/java/io/emeraldpay/etherjar/rpc/JacksonRpcConverter.java @@ -23,6 +23,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.type.TypeFactory; import io.emeraldpay.etherjar.domain.Wei; import io.emeraldpay.etherjar.hex.HexData; import io.emeraldpay.etherjar.rpc.json.*; @@ -83,17 +84,21 @@ public String toJson(List> batch) { } } - @Override public T fromJson(InputStream content, Class target) throws RpcException { + return fromJson(content, objectMapper.getTypeFactory().constructType(target), Integer.class); + } + + @Override + public T fromJson(InputStream content, JavaType target) throws RpcException { return fromJson(content, target, Integer.class); } - @SuppressWarnings("unchecked") public T fromJson(InputStream content, Class target, Class idtype) throws RpcException { - if (TraceList.class.isAssignableFrom(target)) { - return (T) fromJsonList(content, TraceItemJson.class); - } - JavaType type1 = objectMapper.getTypeFactory().constructParametricType(FullResponseJson.class, target, idtype); + return fromJson(content, objectMapper.getTypeFactory().constructType(target), idtype); + } + + public T fromJson(InputStream content, JavaType target, Class idtype) throws RpcException { + JavaType type1 = objectMapper.getTypeFactory().constructParametricType(FullResponseJson.class, target, objectMapper.getTypeFactory().constructType(idtype)); FullResponseJson responseJson; try { responseJson = objectMapper.readerFor(type1).readValue(content); @@ -107,7 +112,7 @@ public T fromJson(InputStream content, Class target, Class idtype) return responseJson.getResult(); } - public T fromJsonResult(InputStream content, Class target) throws RpcException { + public T fromJsonResult(InputStream content, JavaType target) throws RpcException { try { return objectMapper.readerFor(target).readValue(content); } catch (IOException e) { @@ -116,7 +121,7 @@ public T fromJsonResult(InputStream content, Class target) throws RpcExce } @Override - public List> parseBatch(InputStream content, Map targets) throws RpcException { + public List> parseBatch(InputStream content, Map targets) throws RpcException { try { JsonNode nodes = objectMapper.reader().readTree(content); if (!nodes.isArray()) { @@ -133,7 +138,7 @@ public List> parseBatch(InputStream content, Map parsedItem = objectMapper.reader().forType(type1).readValue(resp); parsedBatch.add(parsedItem); @@ -144,23 +149,4 @@ public List> parseBatch(InputStream content, Map List fromJsonList(InputStream content, Class target) throws RpcException { - JavaType dataType = objectMapper.getTypeFactory().constructParametricType(List.class, target); - JavaType idType = objectMapper.getTypeFactory().constructType(Integer.class); - JavaType type2 = objectMapper.getTypeFactory().constructParametricType(FullResponseJson.class, dataType, idType); - FullResponseJson, Object> responseJson = null; - try { - responseJson = objectMapper.readerFor(type2).readValue(content); - } catch (IOException e) { - throw new RpcException(RpcResponseError.CODE_UPSTREAM_INVALID_RESPONSE, e.getMessage()); - } - if (responseJson.hasError()) { - RpcResponseError error = responseJson.getError(); - throw new RpcException(error.getCode(), error.getMessage(), error.getData()); - } - return responseJson.getResult(); - } - - - } diff --git a/etherjar-rpc-json/src/main/java/io/emeraldpay/etherjar/rpc/RpcCall.java b/etherjar-rpc-json/src/main/java/io/emeraldpay/etherjar/rpc/RpcCall.java index d0d42a89..2ca9b887 100644 --- a/etherjar-rpc-json/src/main/java/io/emeraldpay/etherjar/rpc/RpcCall.java +++ b/etherjar-rpc-json/src/main/java/io/emeraldpay/etherjar/rpc/RpcCall.java @@ -15,6 +15,10 @@ */ package io.emeraldpay.etherjar.rpc; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.type.TypeFactory; + +import java.lang.reflect.Array; import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -29,16 +33,13 @@ */ public class RpcCall { - private static Function same() { - return (a) -> a; - } - private final String method; private final List params; - private Class jsonType; + private JavaType jsonType; private Class resultType; private java.util.function.Function converter; + private boolean isArray = false; private RpcCall(String method, List params) { if (method == null) { @@ -64,10 +65,23 @@ private RpcCall(String method, List params) { * @return call definition */ public static RpcCall create(String method, Class type, List params) { + return create(method, TypeFactory.defaultInstance().constructType(type), params); + } + + /** + * + * @param method method name + * @param type data type + * @param params call parameters + * @param data type, same for Java and JSON (i.e. String) + * @return call definition + */ + @SuppressWarnings("unchecked") + public static RpcCall create(String method, JavaType type, List params) { RpcCall call = new RpcCall<>(method, params); - call.jsonType = type; - call.resultType = type; - call.converter = same(); + call.jsonType = TypeFactory.defaultInstance().constructType(type); + call.resultType = (Class) type.getRawClass(); + call.converter = Function.identity(); return call; } @@ -150,7 +164,7 @@ public RpcCall converted(Class resultType, java.util.function.Func */ @SuppressWarnings("unchecked") public RpcCall castJsonType(Class clazz) { - if (this.jsonType == null || clazz.isAssignableFrom(this.jsonType)) { + if (this.jsonType == null || clazz.isAssignableFrom(this.jsonType.getRawClass())) { return (RpcCall) this; } throw new ClassCastException("Value of " + this.jsonType + " is not assignable to " + clazz); @@ -162,6 +176,9 @@ public RpcCall castJsonType(Class clazz) { */ @SuppressWarnings("unchecked") public void setResultType(Class clazz) { + if (isArray) { + throw new IllegalStateException("Cannot change result type after enabling array type"); + } this.resultType = clazz; } @@ -172,11 +189,25 @@ public void setResultType(Class clazz) { * @param JSON data type * @return new call definition */ - @SuppressWarnings("unchecked") public RpcCall withJsonType(Class clazz) { + return withJsonType(TypeFactory.defaultInstance().constructType(clazz)); + } + + /** + * Convert into a new call definition with specified JSON data type + * + * @param jsonType new JSON data type + * @param JSON data type + * @return new call definition + */ + @SuppressWarnings("unchecked") + public RpcCall withJsonType(JavaType jsonType) { + if (isArray) { + throw new IllegalStateException("Cannot change json type after enabling array type"); + } RpcCall copy = new RpcCall<>(this.method, this.params); copy.resultType = this.resultType; - copy.jsonType = clazz; + copy.jsonType = jsonType; copy.converter = (Function) this.converter; return copy; } @@ -190,6 +221,9 @@ public RpcCall withJsonType(Class clazz) { */ @SuppressWarnings("unchecked") public RpcCall withResultType(Class clazz) { + if (isArray) { + throw new IllegalStateException("Cannot change result type after enabling array type"); + } RpcCall copy = new RpcCall<>(this.method, this.params); copy.resultType = clazz; copy.jsonType = this.jsonType; @@ -197,6 +231,27 @@ public RpcCall withResultType(Class clazz) { return copy; } + @SuppressWarnings("unchecked") + public RpcCall asArray() { + RpcCall copy = new RpcCall<>(this.method, this.params); + copy.jsonType = TypeFactory.defaultInstance().constructArrayType(this.jsonType); + copy.resultType = (Class) this.resultType.arrayType(); + if (this.converter != null) { + copy.converter = (values) -> { + if (values == null) { + return null; + } + // it's critical to create an instance of the expected type, otherwise the class type will be Object[] + RES[] result = (RES[]) Array.newInstance(this.resultType, values.length); + for (int i = 0; i < values.length; i++) { + result[i] = this.converter.apply(values[i]); + } + return result; + }; + } + copy.isArray = true; + return copy; + } /** * @@ -227,9 +282,8 @@ public Class getResultType() { * * @return JSON data type */ - @SuppressWarnings("unchecked") - public Class getJsonType() { - return (Class) jsonType; + public JavaType getJsonType() { + return jsonType; } /** @@ -257,6 +311,7 @@ public boolean equals(Object o) { RpcCall call = (RpcCall) o; return method.equals(call.method) && params.equals(call.params) && + Objects.equals(isArray, call.isArray) && Objects.equals(jsonType, call.jsonType) && Objects.equals(resultType, call.resultType); } diff --git a/etherjar-rpc-json/src/main/java/io/emeraldpay/etherjar/rpc/RpcConverter.java b/etherjar-rpc-json/src/main/java/io/emeraldpay/etherjar/rpc/RpcConverter.java index c26f6eab..72ae3da0 100644 --- a/etherjar-rpc-json/src/main/java/io/emeraldpay/etherjar/rpc/RpcConverter.java +++ b/etherjar-rpc-json/src/main/java/io/emeraldpay/etherjar/rpc/RpcConverter.java @@ -17,15 +17,17 @@ package io.emeraldpay.etherjar.rpc; +import com.fasterxml.jackson.databind.JavaType; + import java.io.InputStream; import java.util.List; import java.util.Map; public interface RpcConverter { - T fromJson(InputStream content, Class clazz) throws RpcException; + T fromJson(InputStream content, JavaType clazz) throws RpcException; - List> parseBatch(InputStream content, Map targets) throws RpcException; + List> parseBatch(InputStream content, Map targets) throws RpcException; String toJson(RequestJson request); diff --git a/etherjar-rpc-json/src/main/java/io/emeraldpay/etherjar/rpc/TraceList.java b/etherjar-rpc-json/src/main/java/io/emeraldpay/etherjar/rpc/TraceList.java deleted file mode 100644 index b3677357..00000000 --- a/etherjar-rpc-json/src/main/java/io/emeraldpay/etherjar/rpc/TraceList.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright (c) 2020 EmeraldPay Inc, All Rights Reserved. - * Copyright (c) 2016-2017 Infinitape Inc, All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.emeraldpay.etherjar.rpc; - -import io.emeraldpay.etherjar.rpc.json.TraceItemJson; - -import java.util.List; - -/** - * Workaround to operate List of TraceItemJson (a Class passed to RpcConverter, type is lost in runtime, need a way - * how to distinguish this List from other classes) - */ -public interface TraceList extends List { - -} diff --git a/etherjar-rpc-json/src/test/groovy/io/emeraldpay/etherjar/rpc/JacksonRpcConverterSpec.groovy b/etherjar-rpc-json/src/test/groovy/io/emeraldpay/etherjar/rpc/JacksonRpcConverterSpec.groovy index 6bb288f4..0e7e1d1d 100644 --- a/etherjar-rpc-json/src/test/groovy/io/emeraldpay/etherjar/rpc/JacksonRpcConverterSpec.groovy +++ b/etherjar-rpc-json/src/test/groovy/io/emeraldpay/etherjar/rpc/JacksonRpcConverterSpec.groovy @@ -17,7 +17,8 @@ package io.emeraldpay.etherjar.rpc - +import com.fasterxml.jackson.databind.type.TypeFactory +import io.emeraldpay.etherjar.hex.HexQuantity import io.emeraldpay.etherjar.rpc.json.BlockJson import io.emeraldpay.etherjar.rpc.json.TraceItemJson import io.emeraldpay.etherjar.rpc.json.TransactionJson @@ -38,7 +39,7 @@ class JacksonRpcConverterSpec extends Specification { setup: InputStream json = JacksonEthRpcConverterSpec.classLoader.getResourceAsStream("batch/one-item.json") def target = [ - 1: TransactionJson + 1: TypeFactory.defaultInstance().constructType(TransactionJson) ] when: def act = jacksonRpcConverter.parseBatch(json, target) @@ -55,9 +56,9 @@ class JacksonRpcConverterSpec extends Specification { setup: InputStream json = JacksonEthRpcConverterSpec.classLoader.getResourceAsStream("batch/similar-items.json") def target = [ - 1: BlockJson, - 2: BlockJson, - 3: BlockJson + 1: TypeFactory.defaultInstance().constructType(BlockJson), + 2: TypeFactory.defaultInstance().constructType(BlockJson), + 3: TypeFactory.defaultInstance().constructType(BlockJson) ] when: def act = jacksonRpcConverter.parseBatch(json, target) @@ -81,12 +82,12 @@ class JacksonRpcConverterSpec extends Specification { setup: InputStream json = JacksonEthRpcConverterSpec.classLoader.getResourceAsStream("batch/many-items.json") def target = [ - 1: BlockJson, - 2: TransactionJson, - 3: Boolean, - 4: String, - 5: String, - 6: String + 1: TypeFactory.defaultInstance().constructType(BlockJson), + 2: TypeFactory.defaultInstance().constructType(TransactionJson), + 3: TypeFactory.defaultInstance().constructType(Boolean), + 4: TypeFactory.defaultInstance().constructType(String), + 5: TypeFactory.defaultInstance().constructType(String), + 6: TypeFactory.defaultInstance().constructType(String) ] when: def act = jacksonRpcConverter.parseBatch(json, target) @@ -113,4 +114,20 @@ class JacksonRpcConverterSpec extends Specification { act[5].id == 5 act[5].result == "0x435901" } + + def "Can parse a list"() { + setup: + def rpcCall = RpcCall.create("test", HexQuantity.class).asArray() + def json = '{"id":1, "result": ["0x123", "0x456", "0x789"]}' + when: + def act = jacksonRpcConverter.fromJson(new ByteArrayInputStream(json.getBytes()), rpcCall.jsonType) + then: + act instanceof HexQuantity[] + with(act as HexQuantity[]) { + size() == 3 + it[0] == HexQuantity.from("0x123") + it[1] == HexQuantity.from("0x456") + it[2] == HexQuantity.from("0x789") + } + } } diff --git a/etherjar-rpc-json/src/test/groovy/io/emeraldpay/etherjar/rpc/json/BlockSimulatedJsonSpec.groovy b/etherjar-rpc-json/src/test/groovy/io/emeraldpay/etherjar/rpc/json/BlockSimulatedJsonSpec.groovy index df0b512f..06589a78 100644 --- a/etherjar-rpc-json/src/test/groovy/io/emeraldpay/etherjar/rpc/json/BlockSimulatedJsonSpec.groovy +++ b/etherjar-rpc-json/src/test/groovy/io/emeraldpay/etherjar/rpc/json/BlockSimulatedJsonSpec.groovy @@ -11,6 +11,7 @@ import io.emeraldpay.etherjar.hex.Hex32 import io.emeraldpay.etherjar.hex.HexData import io.emeraldpay.etherjar.hex.HexQuantity import io.emeraldpay.etherjar.rpc.JacksonRpcConverter +import io.emeraldpay.etherjar.rpc.RpcCall import spock.lang.Specification import java.time.Instant @@ -125,4 +126,31 @@ class BlockSimulatedJsonSpec extends Specification { act.calls[1] == block1.calls[1] act == block1 } + + def "Decodes full JSON RPC response"() { + setup: + String json = BlockSimulatedJsonSpec.classLoader.getResourceAsStream("simulate/simulated-1.json").text + String jsonRpc = """ + { + "jsonrpc": "2.0", + "id": 1, + "result": [$json] + } + """ + // as in EthCommands + def rpcCall = RpcCall.create("eth_simulateV1", BlockSimulatedJson.class, "foor", "latest").asArray(); + + when: + def act = jacksonRpcConverter.fromJson(new ByteArrayInputStream(jsonRpc.bytes), rpcCall.jsonType) + + then: + act instanceof BlockSimulatedJson[] + with (act as BlockSimulatedJson[]) { + it[0].calls.size() == block1.calls.size() + it[0].calls.size() == 2 + it[0].calls[0] == block1.calls[0] + it[0].calls[1] == block1.calls[1] + it[0] == block1 + } + } } diff --git a/etherjar-rpc-json/src/test/groovy/io/emeraldpay/etherjar/rpc/json/TraceItemJsonSpec.groovy b/etherjar-rpc-json/src/test/groovy/io/emeraldpay/etherjar/rpc/json/TraceItemJsonSpec.groovy index 6955d8fd..eacc31a4 100644 --- a/etherjar-rpc-json/src/test/groovy/io/emeraldpay/etherjar/rpc/json/TraceItemJsonSpec.groovy +++ b/etherjar-rpc-json/src/test/groovy/io/emeraldpay/etherjar/rpc/json/TraceItemJsonSpec.groovy @@ -17,11 +17,11 @@ package io.emeraldpay.etherjar.rpc.json +import com.fasterxml.jackson.databind.type.TypeFactory import io.emeraldpay.etherjar.domain.Wei import io.emeraldpay.etherjar.hex.HexData import io.emeraldpay.etherjar.rpc.JacksonRpcConverter -import io.emeraldpay.etherjar.rpc.TraceList -import io.emeraldpay.etherjar.rpc.json.TraceItemJson + import spock.lang.Specification import java.text.SimpleDateFormat @@ -40,7 +40,7 @@ class TraceItemJsonSpec extends Specification { InputStream json = TraceItemJsonSpec.classLoader.getResourceAsStream("trace/0x16cb69.json") when: - def act = jacksonRpcConverter.fromJson(json, TraceList.class) + def act = jacksonRpcConverter.fromJson(json, TypeFactory.defaultInstance().constructArrayType(TraceItemJson)) as List then: act.size() == 1 @@ -65,7 +65,7 @@ class TraceItemJsonSpec extends Specification { InputStream json = TraceItemJsonSpec.classLoader.getResourceAsStream("trace/0x19442f.json") when: - def act = jacksonRpcConverter.fromJsonList(json, TraceItemJson.class) + def act = jacksonRpcConverter.fromJson(json, TypeFactory.defaultInstance().constructArrayType(TraceItemJson)) as List then: act.size() == 2 @@ -107,7 +107,7 @@ class TraceItemJsonSpec extends Specification { InputStream json = TraceItemJsonSpec.classLoader.getResourceAsStream("trace/0xdc6c6d.json") when: - def act = jacksonRpcConverter.fromJsonList(json, TraceItemJson.class) + def act = jacksonRpcConverter.fromJson(json, TypeFactory.defaultInstance().constructArrayType(TraceItemJson)) as List then: act.size() == 3 @@ -131,7 +131,7 @@ class TraceItemJsonSpec extends Specification { InputStream json = TraceItemJsonSpec.classLoader.getResourceAsStream("trace/0x5c1969-eth.json") when: - def act = jacksonRpcConverter.fromJsonList(json, TraceItemJson.class) + def act = jacksonRpcConverter.fromJson(json, TypeFactory.defaultInstance().constructArrayType(TraceItemJson)) as List then: act.size() == 1 @@ -155,7 +155,7 @@ class TraceItemJsonSpec extends Specification { InputStream json = TraceItemJsonSpec.classLoader.getResourceAsStream("trace/0x847149.json") when: - def act = jacksonRpcConverter.fromJsonList(json, TraceItemJson.class) + def act = jacksonRpcConverter.fromJson(json, TypeFactory.defaultInstance().constructArrayType(TraceItemJson)) as List then: act.size() == 236 @@ -169,7 +169,7 @@ class TraceItemJsonSpec extends Specification { InputStream json = TraceItemJsonSpec.classLoader.getResourceAsStream("trace/0xb9c321.json") when: - def act = jacksonRpcConverter.fromJsonList(json, TraceItemJson.class) + def act = jacksonRpcConverter.fromJson(json, TypeFactory.defaultInstance().constructArrayType(TraceItemJson)) as List then: act.size() == 2 @@ -185,7 +185,7 @@ class TraceItemJsonSpec extends Specification { InputStream json = TraceItemJsonSpec.classLoader.getResourceAsStream("trace/0x02e508.json") when: - def act = jacksonRpcConverter.fromJsonList(json, TraceItemJson.class) + def act = jacksonRpcConverter.fromJson(json, TypeFactory.defaultInstance().constructArrayType(TraceItemJson)) as List then: act.size() == 2642 @@ -202,7 +202,7 @@ class TraceItemJsonSpec extends Specification { InputStream json = TraceItemJsonSpec.classLoader.getResourceAsStream("trace/0x0cbb36.json") when: - def act = jacksonRpcConverter.fromJson(json, TraceList.class) + def act = jacksonRpcConverter.fromJson(json, TypeFactory.defaultInstance().constructArrayType(TraceItemJson)) as List then: act.size() == 3 @@ -231,7 +231,7 @@ class TraceItemJsonSpec extends Specification { setup: InputStream json = TraceItemJsonSpec.classLoader.getResourceAsStream("trace/0xdc6c6d.json") when: - def act = jacksonRpcConverter.fromJsonList(json, TraceItemJson.class) + def act = jacksonRpcConverter.fromJson(json, TypeFactory.defaultInstance().constructArrayType(TraceItemJson)) as List then: act.size() == 3 act[0].transactionHash.toHex() == '0xdc6c6d169946767dc3448848c1dd82e6286ac939aadeac8450ab959cac7da54d'