diff --git a/build.gradle b/build.gradle index e9d958b10..d543989b8 100644 --- a/build.gradle +++ b/build.gradle @@ -117,6 +117,7 @@ dependencies { testImplementation "io.projectreactor:reactor-test:$reactorVersion" testImplementation 'org.objenesis:objenesis:3.1' testImplementation 'org.mock-server:mockserver-netty:5.10' + testImplementation "nl.jqno.equalsverifier:equalsverifier:3.3" } compileKotlin { diff --git a/src/main/kotlin/io/emeraldpay/dshackle/proxy/ProxyServer.kt b/src/main/kotlin/io/emeraldpay/dshackle/proxy/ProxyServer.kt index c57a3b18e..013cd2b5e 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/proxy/ProxyServer.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/proxy/ProxyServer.kt @@ -86,7 +86,7 @@ class ProxyServer( .addAllItems(call.items) .build() val jsons = nativeCall - .nativeCall(Mono.just(request)) + .nativeCallResult(Mono.just(request)) .transform(writeRpcJson.toJsons(call)) return if (call.type == ProxyCall.RpcType.SINGLE) { jsons.next() diff --git a/src/main/kotlin/io/emeraldpay/dshackle/proxy/WriteRpcJson.kt b/src/main/kotlin/io/emeraldpay/dshackle/proxy/WriteRpcJson.kt index d97870d87..865ff9785 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/proxy/WriteRpcJson.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/proxy/WriteRpcJson.kt @@ -42,7 +42,7 @@ open class WriteRpcJson() { /** * Convert Dshackle protobuf based responses to JSON RPC formatted as strings */ - open fun toJsons(call: ProxyCall): Function, Flux> { + open fun toJsons(call: ProxyCall): Function, Flux> { return Function { flux -> flux .flatMap { response -> @@ -70,19 +70,24 @@ open class WriteRpcJson() { } } - open fun toJson(call: ProxyCall, response: BlockchainOuterClass.NativeCallReplyItem): String? { - val id = call.ids[response.id] ?: return null; - val json = if (response.succeed) { - JsonRpcResponse.ok(response.payload.toByteArray(), JsonRpcResponse.Id.from(id)) + open fun toJson(call: ProxyCall, response: NativeCall.CallResult): String? { + val id = call.ids[response.id]?.let { + JsonRpcResponse.Id.from(it) + } ?: return null; + val json = if (response.isError()) { + val error = response.error!! + error.upstreamError?.let { upstreamError -> + JsonRpcResponse.error(upstreamError, id) + } ?: JsonRpcResponse.error(-32002, error.message, id) } else { - JsonRpcResponse.error(-32002, response.errorMessage, JsonRpcResponse.Id.from(id)) + JsonRpcResponse.ok(response.result!!, id) } return objectMapper.writeValueAsString(json) } fun toJson(call: ProxyCall, error: NativeCall.CallFailure): String? { val id = call.ids[error.id] ?: return null; - val json = JsonRpcResponse.error(-32002, error.reason.message ?: "", JsonRpcResponse.Id.from(id)) + val json = JsonRpcResponse.error(-32003, error.reason.message ?: "", JsonRpcResponse.Id.from(id)) return objectMapper.writeValueAsString(json) } diff --git a/src/main/kotlin/io/emeraldpay/dshackle/quorum/AlwaysQuorum.kt b/src/main/kotlin/io/emeraldpay/dshackle/quorum/AlwaysQuorum.kt index 3887db3b1..1c21cd240 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/quorum/AlwaysQuorum.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/quorum/AlwaysQuorum.kt @@ -18,12 +18,15 @@ package io.emeraldpay.dshackle.quorum import io.emeraldpay.dshackle.upstream.Head import io.emeraldpay.dshackle.upstream.Upstream +import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcError +import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcException import io.infinitape.etherjar.rpc.RpcException open class AlwaysQuorum: CallQuorum { private var resolved = false private var result: ByteArray? = null + private var rpcError: JsonRpcError? = null override fun init(head: Head) { } @@ -42,10 +45,15 @@ open class AlwaysQuorum: CallQuorum { return true } - override fun record(error: RpcException, upstream: Upstream) { + override fun record(error: JsonRpcException, upstream: Upstream) { + this.rpcError = error.error } override fun getResult(): ByteArray? { return result } + + override fun getError(): JsonRpcError? { + return rpcError + } } \ No newline at end of file diff --git a/src/main/kotlin/io/emeraldpay/dshackle/quorum/CallQuorum.kt b/src/main/kotlin/io/emeraldpay/dshackle/quorum/CallQuorum.kt index e6d3ae724..b8b55a73f 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/quorum/CallQuorum.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/quorum/CallQuorum.kt @@ -18,6 +18,8 @@ package io.emeraldpay.dshackle.quorum import io.emeraldpay.dshackle.upstream.Head import io.emeraldpay.dshackle.upstream.Upstream +import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcError +import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcException import io.infinitape.etherjar.domain.TransactionId import io.infinitape.etherjar.rpc.RpcException import io.infinitape.etherjar.rpc.json.BlockJson @@ -34,8 +36,9 @@ interface CallQuorum { fun isFailed(): Boolean fun record(response: ByteArray, upstream: Upstream): Boolean - fun record(error: RpcException, upstream: Upstream) + fun record(error: JsonRpcException, upstream: Upstream) fun getResult(): ByteArray? + fun getError(): JsonRpcError? companion object { fun untilResolved(cq: CallQuorum): Predicate { diff --git a/src/main/kotlin/io/emeraldpay/dshackle/quorum/NonEmptyQuorum.kt b/src/main/kotlin/io/emeraldpay/dshackle/quorum/NonEmptyQuorum.kt index 4752c71de..50072d72f 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/quorum/NonEmptyQuorum.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/quorum/NonEmptyQuorum.kt @@ -19,6 +19,7 @@ package io.emeraldpay.dshackle.quorum import com.fasterxml.jackson.databind.ObjectMapper import io.emeraldpay.dshackle.upstream.Head import io.emeraldpay.dshackle.upstream.Upstream +import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcException import io.infinitape.etherjar.rpc.JacksonRpcConverter import io.infinitape.etherjar.rpc.RpcException @@ -55,7 +56,7 @@ open class NonEmptyQuorum( tries++ } - override fun record(error: RpcException, upstream: Upstream) { + override fun record(error: JsonRpcException, upstream: Upstream) { tries++ } diff --git a/src/main/kotlin/io/emeraldpay/dshackle/quorum/NotLaggingQuorum.kt b/src/main/kotlin/io/emeraldpay/dshackle/quorum/NotLaggingQuorum.kt index b56da4570..771136adc 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/quorum/NotLaggingQuorum.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/quorum/NotLaggingQuorum.kt @@ -18,6 +18,8 @@ package io.emeraldpay.dshackle.quorum import io.emeraldpay.dshackle.upstream.Head import io.emeraldpay.dshackle.upstream.Upstream +import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcError +import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcException import io.infinitape.etherjar.rpc.RpcException import java.util.concurrent.atomic.AtomicReference @@ -25,6 +27,7 @@ class NotLaggingQuorum(val maxLag: Long = 0): CallQuorum { private val result: AtomicReference = AtomicReference() private val failed = AtomicReference(false) + private var rpcError: JsonRpcError? = null override fun init(head: Head) { } @@ -46,7 +49,8 @@ class NotLaggingQuorum(val maxLag: Long = 0): CallQuorum { return false } - override fun record(error: RpcException, upstream: Upstream) { + override fun record(error: JsonRpcException, upstream: Upstream) { + this.rpcError = error.error val lagging = upstream.getLag() > maxLag if (!lagging && result.get() == null) { failed.set(true) @@ -56,4 +60,8 @@ class NotLaggingQuorum(val maxLag: Long = 0): CallQuorum { override fun getResult(): ByteArray { return result.get() } + + override fun getError(): JsonRpcError? { + return rpcError + } } \ No newline at end of file diff --git a/src/main/kotlin/io/emeraldpay/dshackle/quorum/QuorumRpcReader.kt b/src/main/kotlin/io/emeraldpay/dshackle/quorum/QuorumRpcReader.kt index 2757809e3..978824a9b 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/quorum/QuorumRpcReader.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/quorum/QuorumRpcReader.kt @@ -17,6 +17,7 @@ package io.emeraldpay.dshackle.quorum import io.emeraldpay.dshackle.reader.Reader import io.emeraldpay.dshackle.upstream.ApiSource +import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcException import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse import io.infinitape.etherjar.rpc.RpcException @@ -56,8 +57,10 @@ class QuorumRpcReader( val defaultResult: Mono = Mono.just(quorum).flatMap { q -> if (q.isFailed()) { - //TODO record and return actual error details - Mono.error(RpcException(-32000, "Upstream error")) + Mono.error( + q.getError()?.asException(JsonRpcResponse.IntId(1)) + ?: RpcException(-32000, "Unknown Upstream error") + ) } else { log.warn("Empty result for ${key.method} as ${q}") Mono.empty() @@ -74,9 +77,14 @@ class QuorumRpcReader( .flatMap { response -> response.requireResult() .onErrorResume { err -> - if (err is RpcException) { + if (err is RpcException || err is JsonRpcException) { // on error notify quorum, it may use error message or other details - quorum.record(err, api) + val cleanErr: JsonRpcException = when (err) { + is RpcException -> JsonRpcException.from(err) + is JsonRpcException -> err + else -> throw IllegalStateException("Cannot convert from exception", err) + } + quorum.record(cleanErr, api) // it it's failed after that, then we don't need more calls, stop api source if (quorum.isFailed()) { apis.resolve() diff --git a/src/main/kotlin/io/emeraldpay/dshackle/quorum/ValueAwareQuorum.kt b/src/main/kotlin/io/emeraldpay/dshackle/quorum/ValueAwareQuorum.kt index 938dd8eff..5e3710183 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/quorum/ValueAwareQuorum.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/quorum/ValueAwareQuorum.kt @@ -19,6 +19,8 @@ package io.emeraldpay.dshackle.quorum import com.fasterxml.jackson.databind.ObjectMapper import io.emeraldpay.dshackle.Global import io.emeraldpay.dshackle.upstream.Upstream +import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcError +import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcException import io.infinitape.etherjar.rpc.JacksonRpcConverter import io.infinitape.etherjar.rpc.RpcException import org.slf4j.LoggerFactory @@ -28,6 +30,7 @@ abstract class ValueAwareQuorum( ): CallQuorum { private val log = LoggerFactory.getLogger(ValueAwareQuorum::class.java) + private var rpcError: JsonRpcError? = null fun extractValue(response: ByteArray, clazz: Class): T? { return Global.objectMapper.readValue(response.inputStream(), clazz) @@ -45,12 +48,16 @@ abstract class ValueAwareQuorum( return isResolved(); } - override fun record(error: RpcException, upstream: Upstream) { - recordError(null, error.rpcMessage, upstream) + override fun record(error: JsonRpcException, upstream: Upstream) { + this.rpcError = error.error + recordError(null, error.error.message, upstream) } abstract fun recordValue(response: ByteArray, responseValue: T?, upstream: Upstream) abstract fun recordError(response: ByteArray?, errorMessage: String?, upstream: Upstream) + override fun getError(): JsonRpcError? { + return rpcError + } } \ No newline at end of file diff --git a/src/main/kotlin/io/emeraldpay/dshackle/rpc/NativeCall.kt b/src/main/kotlin/io/emeraldpay/dshackle/rpc/NativeCall.kt index cfb2820c7..eaf3448a0 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/rpc/NativeCall.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/rpc/NativeCall.kt @@ -25,6 +25,8 @@ import io.emeraldpay.dshackle.upstream.* import io.emeraldpay.dshackle.quorum.AlwaysQuorum import io.emeraldpay.dshackle.quorum.CallQuorum import io.emeraldpay.dshackle.quorum.QuorumReaderFactory +import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcError +import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcException import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse import io.emeraldpay.grpc.Chain @@ -48,6 +50,12 @@ open class NativeCall( var quorumReaderFactory: QuorumReaderFactory = QuorumReaderFactory.default() open fun nativeCall(requestMono: Mono): Flux { + return nativeCallResult(requestMono) + .map(this::buildResponse) + .onErrorResume(this::processException) + } + + open fun nativeCallResult(requestMono: Mono): Flux { return requestMono.flatMapMany(this::prepareCall) .map(this::parseParams) .parallel() @@ -56,8 +64,6 @@ open class NativeCall( .doOnError { e -> log.warn("Error during native call: ${e.message}") } } .sequential() - .map(this::buildResponse) - .onErrorResume(this::processException) } fun parseParams(it: CallContext): CallContext { @@ -201,13 +207,14 @@ open class NativeCall( open class CallFailure(val id: Int, val reason: Throwable) : Exception("Failed to call $id: ${reason.message}") - open class CallError(val id: Int, val message: String) { + open class CallError(val id: Int, val message: String, val upstreamError: JsonRpcError?) { companion object { fun from(t: Throwable): CallError { return when (t) { - is RpcException -> CallError(t.code, t.rpcMessage) - is CallFailure -> CallError(t.id, t.reason.message ?: "Upstream Error") - else -> CallError(1, t.message ?: "Upstream Error") + is JsonRpcException -> CallError(t.id.asInt(), t.error.message, t.error) + is RpcException -> CallError(t.code, t.rpcMessage, null) + is CallFailure -> CallError(t.id, t.reason.message ?: "Upstream Error", null) + else -> CallError(1, t.message ?: "Upstream Error", null) } } } @@ -220,7 +227,7 @@ open class NativeCall( } fun fail(id: Int, errorCore: Int, errorMessage: String): CallResult { - return CallResult(id, null, CallError(errorCore, errorMessage)) + return CallResult(id, null, CallError(errorCore, errorMessage, null)) } fun fail(id: Int, error: Throwable): CallResult { diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumRpcHead.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumRpcHead.kt index e0d1135dd..20c7a35e0 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumRpcHead.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumRpcHead.kt @@ -56,7 +56,7 @@ class EthereumRpcHead( .timeout(Defaults.timeout, Mono.error(Exception("Block number not received"))) .flatMap { if (it.error != null) { - Mono.error(it.error.asException()) + Mono.error(it.error.asException(null)) } else { val value = it.getResultAsProcessedString() Mono.just(HexQuantity.from(value)) diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/rpcclient/JsonRpcError.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/rpcclient/JsonRpcError.kt new file mode 100644 index 000000000..28d155c0e --- /dev/null +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/rpcclient/JsonRpcError.kt @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2020 EmeraldPay, Inc + * + * 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.dshackle.upstream.rpcclient + +import io.infinitape.etherjar.rpc.RpcException + +class JsonRpcError(val code: Int, val message: String, val details: Any?) { + + constructor(code: Int, message: String) : this(code, message, null) + + companion object { + @JvmStatic + fun from(err: RpcException): JsonRpcError { + return JsonRpcError( + err.code, err.rpcMessage, err.details + ) + } + } + + fun asException(id: JsonRpcResponse.Id?): JsonRpcException { + return JsonRpcException(id ?: JsonRpcResponse.IntId(-1), this) + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is JsonRpcError) return false + + if (code != other.code) return false + if (message != other.message) return false + if (details != other.details) return false + + return true + } + + override fun hashCode(): Int { + var result = code + result = 31 * result + message.hashCode() + result = 31 * result + (details?.hashCode() ?: 0) + return result + } + + +} \ No newline at end of file diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/rpcclient/JsonRpcException.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/rpcclient/JsonRpcException.kt new file mode 100644 index 000000000..a68e53d46 --- /dev/null +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/rpcclient/JsonRpcException.kt @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2020 EmeraldPay, Inc + * + * 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.dshackle.upstream.rpcclient + +import io.infinitape.etherjar.rpc.RpcException + +class JsonRpcException( + val id: JsonRpcResponse.Id, + val error: JsonRpcError +) : Exception(error.message) { + + constructor(id: Int, message: String) : this(JsonRpcResponse.IntId(id), JsonRpcError(-32005, message)) + + companion object { + fun from(err: RpcException): JsonRpcException { + val id = err.details?.let { + if (it is JsonRpcResponse.Id) { + it + } else { + JsonRpcResponse.IntId(-3) + } + } ?: JsonRpcResponse.IntId(-4) + return JsonRpcException( + id, JsonRpcError.from(err) + ) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/rpcclient/JsonRpcHttpClient.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/rpcclient/JsonRpcHttpClient.kt index d8ff18ee3..ce603fe6f 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/rpcclient/JsonRpcHttpClient.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/rpcclient/JsonRpcHttpClient.kt @@ -88,7 +88,9 @@ class JsonRpcHttpClient( return response.response { header, bytes -> if (header.status().code() != 200) { - Mono.error(RpcException(RpcResponseError.CODE_UPSTREAM_INVALID_RESPONSE, "HTTP Code: ${header.status().code()}")) + Mono.error(JsonRpcException(JsonRpcResponse.IntId(-2), + JsonRpcError(RpcResponseError.CODE_UPSTREAM_INVALID_RESPONSE, "HTTP Code: ${header.status().code()}")) + ) } else { bytes.aggregate().asByteArray() } @@ -101,10 +103,10 @@ class JsonRpcHttpClient( .flatMap(this@JsonRpcHttpClient::execute) .map(parser::parse) .onErrorResume { t -> - val err = if (t is RpcException) { - JsonRpcResponse.error(t.code, t.rpcMessage) - } else { - JsonRpcResponse.error(1, t.message ?: t.javaClass.name) + val err = when (t) { + is RpcException -> JsonRpcResponse.error(t.code, t.rpcMessage) + is JsonRpcException -> JsonRpcResponse.error(t.error, JsonRpcResponse.IntId(1)) + else -> JsonRpcResponse.error(1, t.message ?: t.javaClass.name) } Mono.just(err) } diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/rpcclient/JsonRpcParser.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/rpcclient/JsonRpcParser.kt index b19894bd7..8a45db7e9 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/rpcclient/JsonRpcParser.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/rpcclient/JsonRpcParser.kt @@ -19,6 +19,7 @@ import com.fasterxml.jackson.core.JsonFactory import com.fasterxml.jackson.core.JsonParseException import com.fasterxml.jackson.core.JsonParser import com.fasterxml.jackson.core.JsonToken +import io.emeraldpay.dshackle.Global import io.infinitape.etherjar.rpc.RpcResponseError import org.slf4j.LoggerFactory @@ -35,14 +36,14 @@ class JsonRpcParser() { val parser: JsonParser = jsonFactory.createParser(json) parser.nextToken() if (parser.currentToken != JsonToken.START_OBJECT) { - return JsonRpcResponse(null, JsonRpcResponse.ResponseError(RpcResponseError.CODE_UPSTREAM_INVALID_RESPONSE, "Invalid JSON")) + return JsonRpcResponse(null, JsonRpcError(RpcResponseError.CODE_UPSTREAM_INVALID_RESPONSE, "Invalid JSON")) } var nullResponse: JsonRpcResponse? = null while (parser.nextToken() != JsonToken.END_OBJECT) { val field = parser.currentName if (field == "jsonrpc" || field == "id") { if (!parser.nextToken().isScalarValue) { - return JsonRpcResponse(null, JsonRpcResponse.ResponseError(RpcResponseError.CODE_UPSTREAM_INVALID_RESPONSE, "Invalid JSON (id/type)")) + return JsonRpcResponse(null, JsonRpcError(RpcResponseError.CODE_UPSTREAM_INVALID_RESPONSE, "Invalid JSON (id or jsonrpc value)")) } // just skip the field } else if (field == "result") { @@ -78,12 +79,13 @@ class JsonRpcParser() { } catch (e: JsonParseException) { log.warn("Failed to parse JSON from upstream: ${e.message}") } - return JsonRpcResponse(null, JsonRpcResponse.ResponseError(RpcResponseError.CODE_UPSTREAM_INVALID_RESPONSE, "Invalid JSON structure")) + return JsonRpcResponse(null, JsonRpcError(RpcResponseError.CODE_UPSTREAM_INVALID_RESPONSE, "Invalid JSON structure")) } - fun readError(parser: JsonParser): JsonRpcResponse.ResponseError? { + fun readError(parser: JsonParser): JsonRpcError? { var code = 0 var message = "" + var details: Any? = null while (parser.nextToken() != JsonToken.END_OBJECT) { if (parser.currentToken() == JsonToken.VALUE_NULL) { @@ -95,9 +97,15 @@ class JsonRpcParser() { code = parser.intValue } else if (field == "message" && parser.currentToken == JsonToken.VALUE_STRING) { message = parser.valueAsString + } else if (field == "data") { + when (val value = parser.nextToken()) { + JsonToken.VALUE_NULL -> details = null + JsonToken.VALUE_STRING -> details = parser.valueAsString + JsonToken.START_OBJECT -> details = Global.objectMapper.readValue(parser, java.util.Map::class.java) + else -> log.warn("Unsupported error data type $value") + } } } - - return JsonRpcResponse.ResponseError(code, message) + return JsonRpcError(code, message, details) } } \ No newline at end of file diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/rpcclient/JsonRpcResponse.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/rpcclient/JsonRpcResponse.kt index 05d94de19..7c6012a25 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/rpcclient/JsonRpcResponse.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/rpcclient/JsonRpcResponse.kt @@ -18,16 +18,15 @@ package io.emeraldpay.dshackle.upstream.rpcclient import com.fasterxml.jackson.core.JsonGenerator import com.fasterxml.jackson.databind.JsonSerializer import com.fasterxml.jackson.databind.SerializerProvider -import io.infinitape.etherjar.rpc.RpcException import reactor.core.publisher.Mono class JsonRpcResponse( private val result: ByteArray?, - val error: ResponseError?, + val error: JsonRpcError?, val id: Id ) { - constructor(result: ByteArray?, error: ResponseError?) : this(result, error, IntId(0)) + constructor(result: ByteArray?, error: JsonRpcError?) : this(result, error, IntId(0)) companion object { private val NULL_VALUE = "null".toByteArray() @@ -49,12 +48,17 @@ class JsonRpcResponse( @JvmStatic fun error(code: Int, msg: String): JsonRpcResponse { - return JsonRpcResponse(null, ResponseError(code, msg)) + return JsonRpcResponse(null, JsonRpcError(code, msg)) + } + + @JvmStatic + fun error(error: JsonRpcError, id: Id): JsonRpcResponse { + return JsonRpcResponse(null, error, id) } @JvmStatic fun error(code: Int, msg: String, id: Id): JsonRpcResponse { - return JsonRpcResponse(null, ResponseError(code, msg), id) + return JsonRpcResponse(null, JsonRpcError(code, msg), id) } } @@ -88,7 +92,7 @@ class JsonRpcResponse( fun requireResult(): Mono { return if (error != null) { - Mono.error(error.asException()) + Mono.error(error.asException(id)) } else { Mono.just(getResult()) } @@ -96,7 +100,7 @@ class JsonRpcResponse( fun requireStringResult(): Mono { return if (error != null) { - Mono.error(error.asException()) + Mono.error(error.asException(id)) } else { Mono.just(getResultAsProcessedString()) } @@ -121,12 +125,6 @@ class JsonRpcResponse( return result1 } - class ResponseError(val code: Int, val message: String) { - fun asException(): RpcException { - return RpcException(code, message) - } - } - /** * JSON RPC wrapper. Makes sure that the id is either Int or String */ @@ -220,6 +218,13 @@ class JsonRpcResponse( gen.writeObjectFieldStart("error") gen.writeNumberField("code", value.error.code) gen.writeStringField("message", value.error.message) + value.error.details?.let { details -> + when (details) { + is String -> gen.writeStringField("data", details) + is Number -> gen.writeNumberField("data", details.toInt()) + else -> gen.writeObjectField("data", details) + } + } gen.writeEndObject() } else { if (value.result == null) { diff --git a/src/test/groovy/io/emeraldpay/dshackle/proxy/ProxyServerSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/proxy/ProxyServerSpec.groovy index 630530d78..4b09c3b43 100644 --- a/src/test/groovy/io/emeraldpay/dshackle/proxy/ProxyServerSpec.groovy +++ b/src/test/groovy/io/emeraldpay/dshackle/proxy/ProxyServerSpec.groovy @@ -62,7 +62,7 @@ class ProxyServerSpec extends Specification { def act = server.execute(Common.ChainRef.CHAIN_ETHEREUM, call) then: - 1 * nativeCall.nativeCall(_) >> Flux.just(BlockchainOuterClass.NativeCallReplyItem.newBuilder().build()) + 1 * nativeCall.nativeCallResult(_) >> Flux.just(new NativeCall.CallResult(1, "".bytes, null)) StepVerifier.create(act) .expectNext("hello") .expectComplete() diff --git a/src/test/groovy/io/emeraldpay/dshackle/proxy/WriteRpcJsonSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/proxy/WriteRpcJsonSpec.groovy index 091eb6e59..a4f1d13de 100644 --- a/src/test/groovy/io/emeraldpay/dshackle/proxy/WriteRpcJsonSpec.groovy +++ b/src/test/groovy/io/emeraldpay/dshackle/proxy/WriteRpcJsonSpec.groovy @@ -18,6 +18,7 @@ package io.emeraldpay.dshackle.proxy import com.google.protobuf.ByteString import io.emeraldpay.api.proto.BlockchainOuterClass +import io.emeraldpay.dshackle.rpc.NativeCall import io.emeraldpay.dshackle.test.TestingCommons import reactor.core.publisher.Flux import spock.lang.Specification @@ -86,11 +87,7 @@ class WriteRpcJsonSpec extends Specification { def call = new ProxyCall(ProxyCall.RpcType.SINGLE) call.ids[1] = 105 def data = [ - BlockchainOuterClass.NativeCallReplyItem.newBuilder() - .setId(1) - .setSucceed(true) - .setPayload(ByteString.copyFrom('"0x98dbb1"', 'UTF-8')) - .build() + new NativeCall.CallResult(1, '"0x98dbb1"'.bytes, null) ] when: def act = writer.toJson(call, data[0]) @@ -103,11 +100,7 @@ class WriteRpcJsonSpec extends Specification { def call = new ProxyCall(ProxyCall.RpcType.SINGLE) call.ids[1] = 1 def data = [ - BlockchainOuterClass.NativeCallReplyItem.newBuilder() - .setId(1) - .setSucceed(false) - .setErrorMessage("Internal Error") - .build() + new NativeCall.CallResult(1, null, new NativeCall.CallError(1, "Internal Error", null)) ] when: def act = writer.toJson(call, data[0]) @@ -120,11 +113,7 @@ class WriteRpcJsonSpec extends Specification { def call = new ProxyCall(ProxyCall.RpcType.SINGLE) call.ids[1] = "aaa" def data = [ - BlockchainOuterClass.NativeCallReplyItem.newBuilder() - .setId(1) - .setSucceed(true) - .setPayload(ByteString.copyFrom('"0x98dbb1"', 'UTF-8')) - .build() + new NativeCall.CallResult(1, '"0x98dbb1"'.bytes, null) ] when: def act = writer.toJson(call, data[0]) @@ -139,21 +128,9 @@ class WriteRpcJsonSpec extends Specification { call.ids[2] = 11 call.ids[3] = 15 def data = [ - BlockchainOuterClass.NativeCallReplyItem.newBuilder() - .setId(1) - .setSucceed(true) - .setPayload(ByteString.copyFrom('"0x98dbb1"', 'UTF-8')) - .build(), - BlockchainOuterClass.NativeCallReplyItem.newBuilder() - .setId(2) - .setSucceed(false) - .setErrorMessage("oops") - .build(), - BlockchainOuterClass.NativeCallReplyItem.newBuilder() - .setId(3) - .setSucceed(true) - .setPayload(ByteString.copyFrom('{"hash": "0x2484f459dc"}', 'UTF-8')) - .build(), + new NativeCall.CallResult(1, '"0x98dbb1"'.bytes, null), + new NativeCall.CallResult(2, null, new NativeCall.CallError(2, "oops", null)), + new NativeCall.CallResult(3, '{"hash": "0x2484f459dc"}'.bytes, null), ] when: def act = Flux.fromIterable(data) diff --git a/src/test/groovy/io/emeraldpay/dshackle/quorum/BroadcastQuorumSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/quorum/BroadcastQuorumSpec.groovy index 9c6a1c395..126a0616a 100644 --- a/src/test/groovy/io/emeraldpay/dshackle/quorum/BroadcastQuorumSpec.groovy +++ b/src/test/groovy/io/emeraldpay/dshackle/quorum/BroadcastQuorumSpec.groovy @@ -22,6 +22,7 @@ import io.emeraldpay.dshackle.test.TestingCommons import io.emeraldpay.dshackle.upstream.Head import io.emeraldpay.dshackle.upstream.Upstream import io.emeraldpay.dshackle.quorum.BroadcastQuorum +import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcException import io.infinitape.etherjar.rpc.RpcException import spock.lang.Specification @@ -54,7 +55,7 @@ class BroadcastQuorumSpec extends Specification { 1 * q.recordValue(_, "0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c", _) when: - q.record(new RpcException(1, "Nonce too low"), upstream3) + q.record(new JsonRpcException(1, "Nonce too low"), upstream3) then: 1 * q.recordError(_, _, _) q.isResolved() @@ -74,7 +75,7 @@ class BroadcastQuorumSpec extends Specification { !q.isResolved() when: - q.record(new RpcException(1, "Internal error"), upstream1) + q.record(new JsonRpcException(1, "Internal error"), upstream1) then: !q.isResolved() 1 * q.recordError(_, _, _) @@ -86,7 +87,7 @@ class BroadcastQuorumSpec extends Specification { 1 * q.recordValue(_, "0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c", _) when: - q.record(new RpcException(1, "Nonce too low"), upstream3) + q.record(new JsonRpcException(1, "Nonce too low"), upstream3) then: 1 * q.recordError(_, _, _) q.isResolved() diff --git a/src/test/groovy/io/emeraldpay/dshackle/quorum/NonEmptyQuorumSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/quorum/NonEmptyQuorumSpec.groovy index a140408d4..9384b4f4b 100644 --- a/src/test/groovy/io/emeraldpay/dshackle/quorum/NonEmptyQuorumSpec.groovy +++ b/src/test/groovy/io/emeraldpay/dshackle/quorum/NonEmptyQuorumSpec.groovy @@ -18,6 +18,7 @@ package io.emeraldpay.dshackle.quorum import io.emeraldpay.dshackle.test.TestingCommons import io.emeraldpay.dshackle.upstream.Head import io.emeraldpay.dshackle.upstream.Upstream +import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcException import io.infinitape.etherjar.rpc.RpcException import spock.lang.Specification @@ -37,19 +38,19 @@ class NonEmptyQuorumSpec extends Specification { !q.isFailed() when: - q.record(new RpcException(1, "Internal"), upstream1) + q.record(new JsonRpcException(1, "Internal"), upstream1) then: !q.isResolved() !q.isFailed() when: - q.record(new RpcException(1, "Internal"), upstream2) + q.record(new JsonRpcException(1, "Internal"), upstream2) then: !q.isResolved() !q.isFailed() when: - q.record(new RpcException(1, "Internal"), upstream3) + q.record(new JsonRpcException(1, "Internal"), upstream3) then: q.isFailed() !q.isResolved() @@ -89,7 +90,7 @@ class NonEmptyQuorumSpec extends Specification { !q.isFailed() when: - q.record(new RpcException(1, "Internal"), upstream1) + q.record(new JsonRpcException(1, "Internal"), upstream1) then: !q.isFailed() !q.isResolved() diff --git a/src/test/groovy/io/emeraldpay/dshackle/quorum/NonceQuorumSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/quorum/NonceQuorumSpec.groovy index 374389ce8..259fb1e83 100644 --- a/src/test/groovy/io/emeraldpay/dshackle/quorum/NonceQuorumSpec.groovy +++ b/src/test/groovy/io/emeraldpay/dshackle/quorum/NonceQuorumSpec.groovy @@ -22,6 +22,7 @@ import io.emeraldpay.dshackle.test.TestingCommons import io.emeraldpay.dshackle.upstream.Head import io.emeraldpay.dshackle.upstream.Upstream import io.emeraldpay.dshackle.quorum.NonceQuorum +import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcException import io.infinitape.etherjar.rpc.RpcException import spock.lang.Specification @@ -74,7 +75,7 @@ class NonceQuorumSpec extends Specification { !q.isResolved() when: - q.record(new RpcException(1, "Internal"), upstream1) + q.record(new JsonRpcException(1, "Internal"), upstream1) then: !q.isResolved() 1 * q.recordError(_, _, _) @@ -113,21 +114,23 @@ class NonceQuorumSpec extends Specification { !q.isFailed() when: - q.record(new RpcException(1, "Internal"), upstream1) + q.record(new JsonRpcException(1, "Internal"), upstream1) then: !q.isResolved() !q.isFailed() when: - q.record(new RpcException(1, "Internal"), upstream2) + q.record(new JsonRpcException(1, "Internal"), upstream2) then: !q.isResolved() !q.isFailed() when: - q.record(new RpcException(1, "Internal"), upstream3) + q.record(new JsonRpcException(1, "Internal"), upstream3) then: q.isFailed() !q.isResolved() + q.getError() != null + q.getError().message == "Internal" } } diff --git a/src/test/groovy/io/emeraldpay/dshackle/quorum/NotLaggingQuorumSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/quorum/NotLaggingQuorumSpec.groovy index 3b8c68b70..9e335aa87 100644 --- a/src/test/groovy/io/emeraldpay/dshackle/quorum/NotLaggingQuorumSpec.groovy +++ b/src/test/groovy/io/emeraldpay/dshackle/quorum/NotLaggingQuorumSpec.groovy @@ -18,6 +18,7 @@ package io.emeraldpay.dshackle.quorum import io.emeraldpay.dshackle.upstream.Upstream import io.emeraldpay.dshackle.quorum.NotLaggingQuorum +import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcException import io.infinitape.etherjar.rpc.RpcException import spock.lang.Specification @@ -74,7 +75,7 @@ class NotLaggingQuorumSpec extends Specification { def quorum = new NotLaggingQuorum(1) when: - quorum.record(new RpcException(-100, "test error"), up) + quorum.record(new JsonRpcException(-100, "test error"), up) then: 1 * up.getLag() >> 1 !quorum.isResolved() diff --git a/src/test/groovy/io/emeraldpay/dshackle/test/EthereumApiMock.groovy b/src/test/groovy/io/emeraldpay/dshackle/test/EthereumApiMock.groovy index 11fc75d11..51bf4f2eb 100644 --- a/src/test/groovy/io/emeraldpay/dshackle/test/EthereumApiMock.groovy +++ b/src/test/groovy/io/emeraldpay/dshackle/test/EthereumApiMock.groovy @@ -21,6 +21,7 @@ import com.google.protobuf.ByteString import io.emeraldpay.api.proto.BlockchainOuterClass import io.emeraldpay.dshackle.Global import io.emeraldpay.dshackle.reader.Reader +import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcError import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse import io.grpc.stub.StreamObserver @@ -60,7 +61,7 @@ class EthereumApiMock implements Reader { Callable call = { def predefined = predefined.find { it.isSame(request.method, request.params) } byte[] result = null - JsonRpcResponse.ResponseError error = null + JsonRpcError error = null if (predefined != null) { if (predefined.exception != null) { predefined.onCalled() @@ -69,7 +70,7 @@ class EthereumApiMock implements Reader { } if (predefined.result instanceof RpcResponseError) { ((RpcResponseError) predefined.result).with { err -> - error = new JsonRpcResponse.ResponseError(err.code, err.message) + error = new JsonRpcError(err.code, err.message) } } else { // ResponseJson json = new ResponseJson(id: 1, result: predefined.result) @@ -79,7 +80,7 @@ class EthereumApiMock implements Reader { predefined.print() } else { log.error("Method ${request.method} with ${request.params} is not mocked") - error = new JsonRpcResponse.ResponseError(-32601, "Method ${request.method} with ${request.params} is not mocked") + error = new JsonRpcError(-32601, "Method ${request.method} with ${request.params} is not mocked") } return new JsonRpcResponse(result, error) } as Callable diff --git a/src/test/groovy/io/emeraldpay/dshackle/upstream/rpcclient/JsonRpcErrorSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/upstream/rpcclient/JsonRpcErrorSpec.groovy new file mode 100644 index 000000000..da35ebf3a --- /dev/null +++ b/src/test/groovy/io/emeraldpay/dshackle/upstream/rpcclient/JsonRpcErrorSpec.groovy @@ -0,0 +1,34 @@ +package io.emeraldpay.dshackle.upstream.rpcclient + +import io.infinitape.etherjar.rpc.RpcException +import nl.jqno.equalsverifier.EqualsVerifier +import nl.jqno.equalsverifier.Warning +import spock.lang.Specification + +class JsonRpcErrorSpec extends Specification { + + def "Build from RpcException"() { + when: + def act = JsonRpcError.from(new RpcException(-32123, "test test")) + then: + act.code == -32123 + act.message == "test test" + act.details == null + } + + def "Build from RpcException with details"() { + when: + def act = JsonRpcError.from(new RpcException(-32123, "test test", "foo bar")) + then: + act.code == -32123 + act.message == "test test" + act.details == "foo bar" + } + + def "Equals"() { + when: + def v = EqualsVerifier.forClass(JsonRpcError) + then: + v.verify() + } +} diff --git a/src/test/groovy/io/emeraldpay/dshackle/upstream/rpcclient/JsonRpcHttpClientSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/upstream/rpcclient/JsonRpcHttpClientSpec.groovy index db9a92b2d..6b8126b68 100644 --- a/src/test/groovy/io/emeraldpay/dshackle/upstream/rpcclient/JsonRpcHttpClientSpec.groovy +++ b/src/test/groovy/io/emeraldpay/dshackle/upstream/rpcclient/JsonRpcHttpClientSpec.groovy @@ -106,7 +106,7 @@ class JsonRpcHttpClientSpec extends Specification { then: StepVerifier.create(act) .expectErrorMatches { t -> - t instanceof RpcException && t.code == RpcResponseError.CODE_UPSTREAM_INVALID_RESPONSE + t instanceof JsonRpcException && t.error.code == RpcResponseError.CODE_UPSTREAM_INVALID_RESPONSE } .verify(Duration.ofSeconds(1)) } diff --git a/src/test/groovy/io/emeraldpay/dshackle/upstream/rpcclient/JsonRpcParserSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/upstream/rpcclient/JsonRpcParserSpec.groovy index 11fae9078..1f872f361 100644 --- a/src/test/groovy/io/emeraldpay/dshackle/upstream/rpcclient/JsonRpcParserSpec.groovy +++ b/src/test/groovy/io/emeraldpay/dshackle/upstream/rpcclient/JsonRpcParserSpec.groovy @@ -178,6 +178,36 @@ class JsonRpcParserSpec extends Specification { !act.hasResult() } + def "Parse error with data"() { + setup: + // 0 8 16 32 + def json = '{"jsonrpc": "2.0", "id": 1, "result": null, "error": {"code": -1111, "message": "test", "data": "just data"}}' + when: + def act = parser.parse(json.getBytes()) + then: + act.error != null + act.error.code == -1111 + act.error.message == "test" + act.error.details == "just data" + act.hasError() + !act.hasResult() + } + + def "Parse error with data struct"() { + setup: + // 0 8 16 32 + def json = '{"jsonrpc": "2.0", "id": 1, "result": null, "error": {"code": -1111, "message": "test", "data": {"foo": "just data", "bar": 1}}}' + when: + def act = parser.parse(json.getBytes()) + then: + act.error != null + act.error.code == -1111 + act.error.message == "test" + act.error.details == [foo: "just data", bar: 1] + act.hasError() + !act.hasResult() + } + def "Handle non-json with producing an error response"() { setup: def json = 'NOT JSON' diff --git a/src/test/groovy/io/emeraldpay/dshackle/upstream/rpcclient/JsonRpcResponseSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/upstream/rpcclient/JsonRpcResponseSpec.groovy index adaefa30d..05416079c 100644 --- a/src/test/groovy/io/emeraldpay/dshackle/upstream/rpcclient/JsonRpcResponseSpec.groovy +++ b/src/test/groovy/io/emeraldpay/dshackle/upstream/rpcclient/JsonRpcResponseSpec.groovy @@ -90,7 +90,7 @@ class JsonRpcResponseSpec extends Specification { def "Serialize int id and error"() { setup: - def json = new JsonRpcResponse(null, new JsonRpcResponse.ResponseError(-32041, "Oooops"), new JsonRpcResponse.IntId(101)) + def json = new JsonRpcResponse(null, new JsonRpcError(-32041, "Oooops"), new JsonRpcResponse.IntId(101)) when: def act = objectMapper.writeValueAsString(json) then: @@ -127,7 +127,7 @@ class JsonRpcResponseSpec extends Specification { def "Serialize string id and error"() { setup: def json = new JsonRpcResponse(null, - new JsonRpcResponse.ResponseError(-32041, "Oooops"), + new JsonRpcError(-32041, "Oooops"), new JsonRpcResponse.StringId("9kbo29gkaasf")) when: def act = objectMapper.writeValueAsString(json) diff --git a/testing/simple-upstream/src/main/groovy/testing/CallHandler.groovy b/testing/simple-upstream/src/main/groovy/testing/CallHandler.groovy index 7835a0ece..2eb2366fc 100644 --- a/testing/simple-upstream/src/main/groovy/testing/CallHandler.groovy +++ b/testing/simple-upstream/src/main/groovy/testing/CallHandler.groovy @@ -8,19 +8,21 @@ interface CallHandler { private Object result private Integer errorCode private String errorMessage + private Object errorDetails - Result(Object result, Integer errorCode, String errorMessage) { + Result(Object result, Integer errorCode, String errorMessage, Object errorDetails) { this.result = result this.errorCode = errorCode this.errorMessage = errorMessage + this.errorDetails = errorDetails } static Result ok(Object result) { - return new Result(result, null, null) + return new Result(result, null, null, null) } - static Result error(int errorCode, String errorMessage) { - return new Result(null, errorCode, errorMessage) + static Result error(int errorCode, String errorMessage, Object details = null) { + return new Result(null, errorCode, errorMessage, details) } boolean isResult() { @@ -38,5 +40,9 @@ interface CallHandler { String getErrorMessage() { return errorMessage } + + Object getErrorDetails() { + return errorDetails + } } } \ No newline at end of file diff --git a/testing/simple-upstream/src/main/groovy/testing/SimpleUpstream.groovy b/testing/simple-upstream/src/main/groovy/testing/SimpleUpstream.groovy index ef0db7023..31ff7cced 100644 --- a/testing/simple-upstream/src/main/groovy/testing/SimpleUpstream.groovy +++ b/testing/simple-upstream/src/main/groovy/testing/SimpleUpstream.groovy @@ -44,6 +44,9 @@ class SimpleUpstream { code : result.getErrorCode(), message: result.getErrorMessage() ] + if (result.getErrorDetails() != null) { + resultJson["error"]["data"] = result.getErrorDetails() + } } resp.status(200) resp.header("content-type", "application/json") diff --git a/testing/simple-upstream/src/main/groovy/testing/TestcaseHandler.groovy b/testing/simple-upstream/src/main/groovy/testing/TestcaseHandler.groovy index c19637441..4f5630528 100644 --- a/testing/simple-upstream/src/main/groovy/testing/TestcaseHandler.groovy +++ b/testing/simple-upstream/src/main/groovy/testing/TestcaseHandler.groovy @@ -19,6 +19,11 @@ class TestcaseHandler implements CallHandler { && params[0].to?.toLowerCase() == "0x542156d51D10Db5acCB99f9Db7e7C91B74E80a2c".toLowerCase()) { return Result.error(-32015, "VM execution error.") } + // https://github.com/emeraldpay/dshackle/issues/35 (second) + if (method == "eth_call" + && params[0].to?.toLowerCase() == "0x8ee2a5aca4f88cb8c757b8593d0734855dcc0eba".toLowerCase()) { + return Result.error(-32015, "VM execution error.", "revert: SafeMath: division by zero") + } // https://github.com/emeraldpay/dshackle/issues/43 if (method == "debug_traceTransaction" && params[0].toLowerCase() == "0xd949bc0fe1a5d16f4522bc47933554dcc4ada0493ff71ee1973b2410257af9fe".toLowerCase()) {