problem: doesn't pass error details from upstream

fix: #42
This commit is contained in:
Igor Artamonov
2020-08-10 22:18:54 -04:00
parent 2abe3023b6
commit b5daceeff5
30 changed files with 325 additions and 103 deletions

View File

@@ -117,6 +117,7 @@ dependencies {
testImplementation "io.projectreactor:reactor-test:$reactorVersion" testImplementation "io.projectreactor:reactor-test:$reactorVersion"
testImplementation 'org.objenesis:objenesis:3.1' testImplementation 'org.objenesis:objenesis:3.1'
testImplementation 'org.mock-server:mockserver-netty:5.10' testImplementation 'org.mock-server:mockserver-netty:5.10'
testImplementation "nl.jqno.equalsverifier:equalsverifier:3.3"
} }
compileKotlin { compileKotlin {

View File

@@ -86,7 +86,7 @@ class ProxyServer(
.addAllItems(call.items) .addAllItems(call.items)
.build() .build()
val jsons = nativeCall val jsons = nativeCall
.nativeCall(Mono.just(request)) .nativeCallResult(Mono.just(request))
.transform(writeRpcJson.toJsons(call)) .transform(writeRpcJson.toJsons(call))
return if (call.type == ProxyCall.RpcType.SINGLE) { return if (call.type == ProxyCall.RpcType.SINGLE) {
jsons.next() jsons.next()

View File

@@ -42,7 +42,7 @@ open class WriteRpcJson() {
/** /**
* Convert Dshackle protobuf based responses to JSON RPC formatted as strings * Convert Dshackle protobuf based responses to JSON RPC formatted as strings
*/ */
open fun toJsons(call: ProxyCall): Function<Flux<BlockchainOuterClass.NativeCallReplyItem>, Flux<String>> { open fun toJsons(call: ProxyCall): Function<Flux<NativeCall.CallResult>, Flux<String>> {
return Function { flux -> return Function { flux ->
flux flux
.flatMap { response -> .flatMap { response ->
@@ -70,19 +70,24 @@ open class WriteRpcJson() {
} }
} }
open fun toJson(call: ProxyCall, response: BlockchainOuterClass.NativeCallReplyItem): String? { open fun toJson(call: ProxyCall, response: NativeCall.CallResult): String? {
val id = call.ids[response.id] ?: return null; val id = call.ids[response.id]?.let {
val json = if (response.succeed) { JsonRpcResponse.Id.from(it)
JsonRpcResponse.ok(response.payload.toByteArray(), JsonRpcResponse.Id.from(id)) } ?: 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 { } else {
JsonRpcResponse.error(-32002, response.errorMessage, JsonRpcResponse.Id.from(id)) JsonRpcResponse.ok(response.result!!, id)
} }
return objectMapper.writeValueAsString(json) return objectMapper.writeValueAsString(json)
} }
fun toJson(call: ProxyCall, error: NativeCall.CallFailure): String? { fun toJson(call: ProxyCall, error: NativeCall.CallFailure): String? {
val id = call.ids[error.id] ?: return null; 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) return objectMapper.writeValueAsString(json)
} }

View File

@@ -18,12 +18,15 @@ package io.emeraldpay.dshackle.quorum
import io.emeraldpay.dshackle.upstream.Head import io.emeraldpay.dshackle.upstream.Head
import io.emeraldpay.dshackle.upstream.Upstream 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 io.infinitape.etherjar.rpc.RpcException
open class AlwaysQuorum: CallQuorum { open class AlwaysQuorum: CallQuorum {
private var resolved = false private var resolved = false
private var result: ByteArray? = null private var result: ByteArray? = null
private var rpcError: JsonRpcError? = null
override fun init(head: Head) { override fun init(head: Head) {
} }
@@ -42,10 +45,15 @@ open class AlwaysQuorum: CallQuorum {
return true return true
} }
override fun record(error: RpcException, upstream: Upstream) { override fun record(error: JsonRpcException, upstream: Upstream) {
this.rpcError = error.error
} }
override fun getResult(): ByteArray? { override fun getResult(): ByteArray? {
return result return result
} }
override fun getError(): JsonRpcError? {
return rpcError
}
} }

View File

@@ -18,6 +18,8 @@ package io.emeraldpay.dshackle.quorum
import io.emeraldpay.dshackle.upstream.Head import io.emeraldpay.dshackle.upstream.Head
import io.emeraldpay.dshackle.upstream.Upstream 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.domain.TransactionId
import io.infinitape.etherjar.rpc.RpcException import io.infinitape.etherjar.rpc.RpcException
import io.infinitape.etherjar.rpc.json.BlockJson import io.infinitape.etherjar.rpc.json.BlockJson
@@ -34,8 +36,9 @@ interface CallQuorum {
fun isFailed(): Boolean fun isFailed(): Boolean
fun record(response: ByteArray, upstream: Upstream): Boolean fun record(response: ByteArray, upstream: Upstream): Boolean
fun record(error: RpcException, upstream: Upstream) fun record(error: JsonRpcException, upstream: Upstream)
fun getResult(): ByteArray? fun getResult(): ByteArray?
fun getError(): JsonRpcError?
companion object { companion object {
fun untilResolved(cq: CallQuorum): Predicate<Any> { fun untilResolved(cq: CallQuorum): Predicate<Any> {

View File

@@ -19,6 +19,7 @@ package io.emeraldpay.dshackle.quorum
import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.ObjectMapper
import io.emeraldpay.dshackle.upstream.Head import io.emeraldpay.dshackle.upstream.Head
import io.emeraldpay.dshackle.upstream.Upstream import io.emeraldpay.dshackle.upstream.Upstream
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcException
import io.infinitape.etherjar.rpc.JacksonRpcConverter import io.infinitape.etherjar.rpc.JacksonRpcConverter
import io.infinitape.etherjar.rpc.RpcException import io.infinitape.etherjar.rpc.RpcException
@@ -55,7 +56,7 @@ open class NonEmptyQuorum(
tries++ tries++
} }
override fun record(error: RpcException, upstream: Upstream) { override fun record(error: JsonRpcException, upstream: Upstream) {
tries++ tries++
} }

View File

@@ -18,6 +18,8 @@ package io.emeraldpay.dshackle.quorum
import io.emeraldpay.dshackle.upstream.Head import io.emeraldpay.dshackle.upstream.Head
import io.emeraldpay.dshackle.upstream.Upstream 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 io.infinitape.etherjar.rpc.RpcException
import java.util.concurrent.atomic.AtomicReference import java.util.concurrent.atomic.AtomicReference
@@ -25,6 +27,7 @@ class NotLaggingQuorum(val maxLag: Long = 0): CallQuorum {
private val result: AtomicReference<ByteArray> = AtomicReference() private val result: AtomicReference<ByteArray> = AtomicReference()
private val failed = AtomicReference(false) private val failed = AtomicReference(false)
private var rpcError: JsonRpcError? = null
override fun init(head: Head) { override fun init(head: Head) {
} }
@@ -46,7 +49,8 @@ class NotLaggingQuorum(val maxLag: Long = 0): CallQuorum {
return false 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 val lagging = upstream.getLag() > maxLag
if (!lagging && result.get() == null) { if (!lagging && result.get() == null) {
failed.set(true) failed.set(true)
@@ -56,4 +60,8 @@ class NotLaggingQuorum(val maxLag: Long = 0): CallQuorum {
override fun getResult(): ByteArray { override fun getResult(): ByteArray {
return result.get() return result.get()
} }
override fun getError(): JsonRpcError? {
return rpcError
}
} }

View File

@@ -17,6 +17,7 @@ package io.emeraldpay.dshackle.quorum
import io.emeraldpay.dshackle.reader.Reader import io.emeraldpay.dshackle.reader.Reader
import io.emeraldpay.dshackle.upstream.ApiSource 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.JsonRpcRequest
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
import io.infinitape.etherjar.rpc.RpcException import io.infinitape.etherjar.rpc.RpcException
@@ -56,8 +57,10 @@ class QuorumRpcReader(
val defaultResult: Mono<Result> = Mono.just(quorum).flatMap { q -> val defaultResult: Mono<Result> = Mono.just(quorum).flatMap { q ->
if (q.isFailed()) { if (q.isFailed()) {
//TODO record and return actual error details Mono.error<Result>(
Mono.error<Result>(RpcException(-32000, "Upstream error")) q.getError()?.asException(JsonRpcResponse.IntId(1))
?: RpcException(-32000, "Unknown Upstream error")
)
} else { } else {
log.warn("Empty result for ${key.method} as ${q}") log.warn("Empty result for ${key.method} as ${q}")
Mono.empty<Result>() Mono.empty<Result>()
@@ -74,9 +77,14 @@ class QuorumRpcReader(
.flatMap { response -> .flatMap { response ->
response.requireResult() response.requireResult()
.onErrorResume { err -> .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 // 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 // it it's failed after that, then we don't need more calls, stop api source
if (quorum.isFailed()) { if (quorum.isFailed()) {
apis.resolve() apis.resolve()

View File

@@ -19,6 +19,8 @@ package io.emeraldpay.dshackle.quorum
import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.ObjectMapper
import io.emeraldpay.dshackle.Global import io.emeraldpay.dshackle.Global
import io.emeraldpay.dshackle.upstream.Upstream 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.JacksonRpcConverter
import io.infinitape.etherjar.rpc.RpcException import io.infinitape.etherjar.rpc.RpcException
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
@@ -28,6 +30,7 @@ abstract class ValueAwareQuorum<T>(
): CallQuorum { ): CallQuorum {
private val log = LoggerFactory.getLogger(ValueAwareQuorum::class.java) private val log = LoggerFactory.getLogger(ValueAwareQuorum::class.java)
private var rpcError: JsonRpcError? = null
fun extractValue(response: ByteArray, clazz: Class<T>): T? { fun extractValue(response: ByteArray, clazz: Class<T>): T? {
return Global.objectMapper.readValue(response.inputStream(), clazz) return Global.objectMapper.readValue(response.inputStream(), clazz)
@@ -45,12 +48,16 @@ abstract class ValueAwareQuorum<T>(
return isResolved(); return isResolved();
} }
override fun record(error: RpcException, upstream: Upstream) { override fun record(error: JsonRpcException, upstream: Upstream) {
recordError(null, error.rpcMessage, upstream) this.rpcError = error.error
recordError(null, error.error.message, upstream)
} }
abstract fun recordValue(response: ByteArray, responseValue: T?, upstream: Upstream) abstract fun recordValue(response: ByteArray, responseValue: T?, upstream: Upstream)
abstract fun recordError(response: ByteArray?, errorMessage: String?, upstream: Upstream) abstract fun recordError(response: ByteArray?, errorMessage: String?, upstream: Upstream)
override fun getError(): JsonRpcError? {
return rpcError
}
} }

View File

@@ -25,6 +25,8 @@ import io.emeraldpay.dshackle.upstream.*
import io.emeraldpay.dshackle.quorum.AlwaysQuorum import io.emeraldpay.dshackle.quorum.AlwaysQuorum
import io.emeraldpay.dshackle.quorum.CallQuorum import io.emeraldpay.dshackle.quorum.CallQuorum
import io.emeraldpay.dshackle.quorum.QuorumReaderFactory 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.JsonRpcRequest
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
import io.emeraldpay.grpc.Chain import io.emeraldpay.grpc.Chain
@@ -48,6 +50,12 @@ open class NativeCall(
var quorumReaderFactory: QuorumReaderFactory = QuorumReaderFactory.default() var quorumReaderFactory: QuorumReaderFactory = QuorumReaderFactory.default()
open fun nativeCall(requestMono: Mono<BlockchainOuterClass.NativeCallRequest>): Flux<BlockchainOuterClass.NativeCallReplyItem> { open fun nativeCall(requestMono: Mono<BlockchainOuterClass.NativeCallRequest>): Flux<BlockchainOuterClass.NativeCallReplyItem> {
return nativeCallResult(requestMono)
.map(this::buildResponse)
.onErrorResume(this::processException)
}
open fun nativeCallResult(requestMono: Mono<BlockchainOuterClass.NativeCallRequest>): Flux<CallResult> {
return requestMono.flatMapMany(this::prepareCall) return requestMono.flatMapMany(this::prepareCall)
.map(this::parseParams) .map(this::parseParams)
.parallel() .parallel()
@@ -56,8 +64,6 @@ open class NativeCall(
.doOnError { e -> log.warn("Error during native call: ${e.message}") } .doOnError { e -> log.warn("Error during native call: ${e.message}") }
} }
.sequential() .sequential()
.map(this::buildResponse)
.onErrorResume(this::processException)
} }
fun parseParams(it: CallContext<RawCallDetails>): CallContext<ParsedCallDetails> { fun parseParams(it: CallContext<RawCallDetails>): CallContext<ParsedCallDetails> {
@@ -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 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 { companion object {
fun from(t: Throwable): CallError { fun from(t: Throwable): CallError {
return when (t) { return when (t) {
is RpcException -> CallError(t.code, t.rpcMessage) is JsonRpcException -> CallError(t.id.asInt(), t.error.message, t.error)
is CallFailure -> CallError(t.id, t.reason.message ?: "Upstream Error") is RpcException -> CallError(t.code, t.rpcMessage, null)
else -> CallError(1, t.message ?: "Upstream Error") 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 { 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 { fun fail(id: Int, error: Throwable): CallResult {

View File

@@ -56,7 +56,7 @@ class EthereumRpcHead(
.timeout(Defaults.timeout, Mono.error(Exception("Block number not received"))) .timeout(Defaults.timeout, Mono.error(Exception("Block number not received")))
.flatMap { .flatMap {
if (it.error != null) { if (it.error != null) {
Mono.error(it.error.asException()) Mono.error(it.error.asException(null))
} else { } else {
val value = it.getResultAsProcessedString() val value = it.getResultAsProcessedString()
Mono.just(HexQuantity.from(value)) Mono.just(HexQuantity.from(value))

View File

@@ -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
}
}

View File

@@ -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)
)
}
}
}

View File

@@ -88,7 +88,9 @@ class JsonRpcHttpClient(
return response.response { header, bytes -> return response.response { header, bytes ->
if (header.status().code() != 200) { 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 { } else {
bytes.aggregate().asByteArray() bytes.aggregate().asByteArray()
} }
@@ -101,10 +103,10 @@ class JsonRpcHttpClient(
.flatMap(this@JsonRpcHttpClient::execute) .flatMap(this@JsonRpcHttpClient::execute)
.map(parser::parse) .map(parser::parse)
.onErrorResume { t -> .onErrorResume { t ->
val err = if (t is RpcException) { val err = when (t) {
JsonRpcResponse.error(t.code, t.rpcMessage) is RpcException -> JsonRpcResponse.error(t.code, t.rpcMessage)
} else { is JsonRpcException -> JsonRpcResponse.error(t.error, JsonRpcResponse.IntId(1))
JsonRpcResponse.error(1, t.message ?: t.javaClass.name) else -> JsonRpcResponse.error(1, t.message ?: t.javaClass.name)
} }
Mono.just(err) Mono.just(err)
} }

View File

@@ -19,6 +19,7 @@ import com.fasterxml.jackson.core.JsonFactory
import com.fasterxml.jackson.core.JsonParseException import com.fasterxml.jackson.core.JsonParseException
import com.fasterxml.jackson.core.JsonParser import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.core.JsonToken import com.fasterxml.jackson.core.JsonToken
import io.emeraldpay.dshackle.Global
import io.infinitape.etherjar.rpc.RpcResponseError import io.infinitape.etherjar.rpc.RpcResponseError
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
@@ -35,14 +36,14 @@ class JsonRpcParser() {
val parser: JsonParser = jsonFactory.createParser(json) val parser: JsonParser = jsonFactory.createParser(json)
parser.nextToken() parser.nextToken()
if (parser.currentToken != JsonToken.START_OBJECT) { 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 var nullResponse: JsonRpcResponse? = null
while (parser.nextToken() != JsonToken.END_OBJECT) { while (parser.nextToken() != JsonToken.END_OBJECT) {
val field = parser.currentName val field = parser.currentName
if (field == "jsonrpc" || field == "id") { if (field == "jsonrpc" || field == "id") {
if (!parser.nextToken().isScalarValue) { 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 // just skip the field
} else if (field == "result") { } else if (field == "result") {
@@ -78,12 +79,13 @@ class JsonRpcParser() {
} catch (e: JsonParseException) { } catch (e: JsonParseException) {
log.warn("Failed to parse JSON from upstream: ${e.message}") 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 code = 0
var message = "" var message = ""
var details: Any? = null
while (parser.nextToken() != JsonToken.END_OBJECT) { while (parser.nextToken() != JsonToken.END_OBJECT) {
if (parser.currentToken() == JsonToken.VALUE_NULL) { if (parser.currentToken() == JsonToken.VALUE_NULL) {
@@ -95,9 +97,15 @@ class JsonRpcParser() {
code = parser.intValue code = parser.intValue
} else if (field == "message" && parser.currentToken == JsonToken.VALUE_STRING) { } else if (field == "message" && parser.currentToken == JsonToken.VALUE_STRING) {
message = parser.valueAsString 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 JsonRpcError(code, message, details)
return JsonRpcResponse.ResponseError(code, message)
} }
} }

View File

@@ -18,16 +18,15 @@ package io.emeraldpay.dshackle.upstream.rpcclient
import com.fasterxml.jackson.core.JsonGenerator import com.fasterxml.jackson.core.JsonGenerator
import com.fasterxml.jackson.databind.JsonSerializer import com.fasterxml.jackson.databind.JsonSerializer
import com.fasterxml.jackson.databind.SerializerProvider import com.fasterxml.jackson.databind.SerializerProvider
import io.infinitape.etherjar.rpc.RpcException
import reactor.core.publisher.Mono import reactor.core.publisher.Mono
class JsonRpcResponse( class JsonRpcResponse(
private val result: ByteArray?, private val result: ByteArray?,
val error: ResponseError?, val error: JsonRpcError?,
val id: Id 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 { companion object {
private val NULL_VALUE = "null".toByteArray() private val NULL_VALUE = "null".toByteArray()
@@ -49,12 +48,17 @@ class JsonRpcResponse(
@JvmStatic @JvmStatic
fun error(code: Int, msg: String): JsonRpcResponse { 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 @JvmStatic
fun error(code: Int, msg: String, id: Id): JsonRpcResponse { 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<ByteArray> { fun requireResult(): Mono<ByteArray> {
return if (error != null) { return if (error != null) {
Mono.error(error.asException()) Mono.error(error.asException(id))
} else { } else {
Mono.just(getResult()) Mono.just(getResult())
} }
@@ -96,7 +100,7 @@ class JsonRpcResponse(
fun requireStringResult(): Mono<String> { fun requireStringResult(): Mono<String> {
return if (error != null) { return if (error != null) {
Mono.error(error.asException()) Mono.error(error.asException(id))
} else { } else {
Mono.just(getResultAsProcessedString()) Mono.just(getResultAsProcessedString())
} }
@@ -121,12 +125,6 @@ class JsonRpcResponse(
return result1 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 * JSON RPC wrapper. Makes sure that the id is either Int or String
*/ */
@@ -220,6 +218,13 @@ class JsonRpcResponse(
gen.writeObjectFieldStart("error") gen.writeObjectFieldStart("error")
gen.writeNumberField("code", value.error.code) gen.writeNumberField("code", value.error.code)
gen.writeStringField("message", value.error.message) 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() gen.writeEndObject()
} else { } else {
if (value.result == null) { if (value.result == null) {

View File

@@ -62,7 +62,7 @@ class ProxyServerSpec extends Specification {
def act = server.execute(Common.ChainRef.CHAIN_ETHEREUM, call) def act = server.execute(Common.ChainRef.CHAIN_ETHEREUM, call)
then: then:
1 * nativeCall.nativeCall(_) >> Flux.just(BlockchainOuterClass.NativeCallReplyItem.newBuilder().build()) 1 * nativeCall.nativeCallResult(_) >> Flux.just(new NativeCall.CallResult(1, "".bytes, null))
StepVerifier.create(act) StepVerifier.create(act)
.expectNext("hello") .expectNext("hello")
.expectComplete() .expectComplete()

View File

@@ -18,6 +18,7 @@ package io.emeraldpay.dshackle.proxy
import com.google.protobuf.ByteString import com.google.protobuf.ByteString
import io.emeraldpay.api.proto.BlockchainOuterClass import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.dshackle.rpc.NativeCall
import io.emeraldpay.dshackle.test.TestingCommons import io.emeraldpay.dshackle.test.TestingCommons
import reactor.core.publisher.Flux import reactor.core.publisher.Flux
import spock.lang.Specification import spock.lang.Specification
@@ -86,11 +87,7 @@ class WriteRpcJsonSpec extends Specification {
def call = new ProxyCall(ProxyCall.RpcType.SINGLE) def call = new ProxyCall(ProxyCall.RpcType.SINGLE)
call.ids[1] = 105 call.ids[1] = 105
def data = [ def data = [
BlockchainOuterClass.NativeCallReplyItem.newBuilder() new NativeCall.CallResult(1, '"0x98dbb1"'.bytes, null)
.setId(1)
.setSucceed(true)
.setPayload(ByteString.copyFrom('"0x98dbb1"', 'UTF-8'))
.build()
] ]
when: when:
def act = writer.toJson(call, data[0]) def act = writer.toJson(call, data[0])
@@ -103,11 +100,7 @@ class WriteRpcJsonSpec extends Specification {
def call = new ProxyCall(ProxyCall.RpcType.SINGLE) def call = new ProxyCall(ProxyCall.RpcType.SINGLE)
call.ids[1] = 1 call.ids[1] = 1
def data = [ def data = [
BlockchainOuterClass.NativeCallReplyItem.newBuilder() new NativeCall.CallResult(1, null, new NativeCall.CallError(1, "Internal Error", null))
.setId(1)
.setSucceed(false)
.setErrorMessage("Internal Error")
.build()
] ]
when: when:
def act = writer.toJson(call, data[0]) def act = writer.toJson(call, data[0])
@@ -120,11 +113,7 @@ class WriteRpcJsonSpec extends Specification {
def call = new ProxyCall(ProxyCall.RpcType.SINGLE) def call = new ProxyCall(ProxyCall.RpcType.SINGLE)
call.ids[1] = "aaa" call.ids[1] = "aaa"
def data = [ def data = [
BlockchainOuterClass.NativeCallReplyItem.newBuilder() new NativeCall.CallResult(1, '"0x98dbb1"'.bytes, null)
.setId(1)
.setSucceed(true)
.setPayload(ByteString.copyFrom('"0x98dbb1"', 'UTF-8'))
.build()
] ]
when: when:
def act = writer.toJson(call, data[0]) def act = writer.toJson(call, data[0])
@@ -139,21 +128,9 @@ class WriteRpcJsonSpec extends Specification {
call.ids[2] = 11 call.ids[2] = 11
call.ids[3] = 15 call.ids[3] = 15
def data = [ def data = [
BlockchainOuterClass.NativeCallReplyItem.newBuilder() new NativeCall.CallResult(1, '"0x98dbb1"'.bytes, null),
.setId(1) new NativeCall.CallResult(2, null, new NativeCall.CallError(2, "oops", null)),
.setSucceed(true) new NativeCall.CallResult(3, '{"hash": "0x2484f459dc"}'.bytes, null),
.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(),
] ]
when: when:
def act = Flux.fromIterable(data) def act = Flux.fromIterable(data)

View File

@@ -22,6 +22,7 @@ import io.emeraldpay.dshackle.test.TestingCommons
import io.emeraldpay.dshackle.upstream.Head import io.emeraldpay.dshackle.upstream.Head
import io.emeraldpay.dshackle.upstream.Upstream import io.emeraldpay.dshackle.upstream.Upstream
import io.emeraldpay.dshackle.quorum.BroadcastQuorum import io.emeraldpay.dshackle.quorum.BroadcastQuorum
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcException
import io.infinitape.etherjar.rpc.RpcException import io.infinitape.etherjar.rpc.RpcException
import spock.lang.Specification import spock.lang.Specification
@@ -54,7 +55,7 @@ class BroadcastQuorumSpec extends Specification {
1 * q.recordValue(_, "0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c", _) 1 * q.recordValue(_, "0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c", _)
when: when:
q.record(new RpcException(1, "Nonce too low"), upstream3) q.record(new JsonRpcException(1, "Nonce too low"), upstream3)
then: then:
1 * q.recordError(_, _, _) 1 * q.recordError(_, _, _)
q.isResolved() q.isResolved()
@@ -74,7 +75,7 @@ class BroadcastQuorumSpec extends Specification {
!q.isResolved() !q.isResolved()
when: when:
q.record(new RpcException(1, "Internal error"), upstream1) q.record(new JsonRpcException(1, "Internal error"), upstream1)
then: then:
!q.isResolved() !q.isResolved()
1 * q.recordError(_, _, _) 1 * q.recordError(_, _, _)
@@ -86,7 +87,7 @@ class BroadcastQuorumSpec extends Specification {
1 * q.recordValue(_, "0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c", _) 1 * q.recordValue(_, "0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c", _)
when: when:
q.record(new RpcException(1, "Nonce too low"), upstream3) q.record(new JsonRpcException(1, "Nonce too low"), upstream3)
then: then:
1 * q.recordError(_, _, _) 1 * q.recordError(_, _, _)
q.isResolved() q.isResolved()

View File

@@ -18,6 +18,7 @@ package io.emeraldpay.dshackle.quorum
import io.emeraldpay.dshackle.test.TestingCommons import io.emeraldpay.dshackle.test.TestingCommons
import io.emeraldpay.dshackle.upstream.Head import io.emeraldpay.dshackle.upstream.Head
import io.emeraldpay.dshackle.upstream.Upstream import io.emeraldpay.dshackle.upstream.Upstream
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcException
import io.infinitape.etherjar.rpc.RpcException import io.infinitape.etherjar.rpc.RpcException
import spock.lang.Specification import spock.lang.Specification
@@ -37,19 +38,19 @@ class NonEmptyQuorumSpec extends Specification {
!q.isFailed() !q.isFailed()
when: when:
q.record(new RpcException(1, "Internal"), upstream1) q.record(new JsonRpcException(1, "Internal"), upstream1)
then: then:
!q.isResolved() !q.isResolved()
!q.isFailed() !q.isFailed()
when: when:
q.record(new RpcException(1, "Internal"), upstream2) q.record(new JsonRpcException(1, "Internal"), upstream2)
then: then:
!q.isResolved() !q.isResolved()
!q.isFailed() !q.isFailed()
when: when:
q.record(new RpcException(1, "Internal"), upstream3) q.record(new JsonRpcException(1, "Internal"), upstream3)
then: then:
q.isFailed() q.isFailed()
!q.isResolved() !q.isResolved()
@@ -89,7 +90,7 @@ class NonEmptyQuorumSpec extends Specification {
!q.isFailed() !q.isFailed()
when: when:
q.record(new RpcException(1, "Internal"), upstream1) q.record(new JsonRpcException(1, "Internal"), upstream1)
then: then:
!q.isFailed() !q.isFailed()
!q.isResolved() !q.isResolved()

View File

@@ -22,6 +22,7 @@ import io.emeraldpay.dshackle.test.TestingCommons
import io.emeraldpay.dshackle.upstream.Head import io.emeraldpay.dshackle.upstream.Head
import io.emeraldpay.dshackle.upstream.Upstream import io.emeraldpay.dshackle.upstream.Upstream
import io.emeraldpay.dshackle.quorum.NonceQuorum import io.emeraldpay.dshackle.quorum.NonceQuorum
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcException
import io.infinitape.etherjar.rpc.RpcException import io.infinitape.etherjar.rpc.RpcException
import spock.lang.Specification import spock.lang.Specification
@@ -74,7 +75,7 @@ class NonceQuorumSpec extends Specification {
!q.isResolved() !q.isResolved()
when: when:
q.record(new RpcException(1, "Internal"), upstream1) q.record(new JsonRpcException(1, "Internal"), upstream1)
then: then:
!q.isResolved() !q.isResolved()
1 * q.recordError(_, _, _) 1 * q.recordError(_, _, _)
@@ -113,21 +114,23 @@ class NonceQuorumSpec extends Specification {
!q.isFailed() !q.isFailed()
when: when:
q.record(new RpcException(1, "Internal"), upstream1) q.record(new JsonRpcException(1, "Internal"), upstream1)
then: then:
!q.isResolved() !q.isResolved()
!q.isFailed() !q.isFailed()
when: when:
q.record(new RpcException(1, "Internal"), upstream2) q.record(new JsonRpcException(1, "Internal"), upstream2)
then: then:
!q.isResolved() !q.isResolved()
!q.isFailed() !q.isFailed()
when: when:
q.record(new RpcException(1, "Internal"), upstream3) q.record(new JsonRpcException(1, "Internal"), upstream3)
then: then:
q.isFailed() q.isFailed()
!q.isResolved() !q.isResolved()
q.getError() != null
q.getError().message == "Internal"
} }
} }

View File

@@ -18,6 +18,7 @@ package io.emeraldpay.dshackle.quorum
import io.emeraldpay.dshackle.upstream.Upstream import io.emeraldpay.dshackle.upstream.Upstream
import io.emeraldpay.dshackle.quorum.NotLaggingQuorum import io.emeraldpay.dshackle.quorum.NotLaggingQuorum
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcException
import io.infinitape.etherjar.rpc.RpcException import io.infinitape.etherjar.rpc.RpcException
import spock.lang.Specification import spock.lang.Specification
@@ -74,7 +75,7 @@ class NotLaggingQuorumSpec extends Specification {
def quorum = new NotLaggingQuorum(1) def quorum = new NotLaggingQuorum(1)
when: when:
quorum.record(new RpcException(-100, "test error"), up) quorum.record(new JsonRpcException(-100, "test error"), up)
then: then:
1 * up.getLag() >> 1 1 * up.getLag() >> 1
!quorum.isResolved() !quorum.isResolved()

View File

@@ -21,6 +21,7 @@ import com.google.protobuf.ByteString
import io.emeraldpay.api.proto.BlockchainOuterClass import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.dshackle.Global import io.emeraldpay.dshackle.Global
import io.emeraldpay.dshackle.reader.Reader 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.JsonRpcRequest
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
import io.grpc.stub.StreamObserver import io.grpc.stub.StreamObserver
@@ -60,7 +61,7 @@ class EthereumApiMock implements Reader<JsonRpcRequest, JsonRpcResponse> {
Callable<JsonRpcResponse> call = { Callable<JsonRpcResponse> call = {
def predefined = predefined.find { it.isSame(request.method, request.params) } def predefined = predefined.find { it.isSame(request.method, request.params) }
byte[] result = null byte[] result = null
JsonRpcResponse.ResponseError error = null JsonRpcError error = null
if (predefined != null) { if (predefined != null) {
if (predefined.exception != null) { if (predefined.exception != null) {
predefined.onCalled() predefined.onCalled()
@@ -69,7 +70,7 @@ class EthereumApiMock implements Reader<JsonRpcRequest, JsonRpcResponse> {
} }
if (predefined.result instanceof RpcResponseError) { if (predefined.result instanceof RpcResponseError) {
((RpcResponseError) predefined.result).with { err -> ((RpcResponseError) predefined.result).with { err ->
error = new JsonRpcResponse.ResponseError(err.code, err.message) error = new JsonRpcError(err.code, err.message)
} }
} else { } else {
// ResponseJson json = new ResponseJson<Object, Integer>(id: 1, result: predefined.result) // ResponseJson json = new ResponseJson<Object, Integer>(id: 1, result: predefined.result)
@@ -79,7 +80,7 @@ class EthereumApiMock implements Reader<JsonRpcRequest, JsonRpcResponse> {
predefined.print() predefined.print()
} else { } else {
log.error("Method ${request.method} with ${request.params} is not mocked") 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) return new JsonRpcResponse(result, error)
} as Callable<JsonRpcResponse> } as Callable<JsonRpcResponse>

View File

@@ -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()
}
}

View File

@@ -106,7 +106,7 @@ class JsonRpcHttpClientSpec extends Specification {
then: then:
StepVerifier.create(act) StepVerifier.create(act)
.expectErrorMatches { t -> .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)) .verify(Duration.ofSeconds(1))
} }

View File

@@ -178,6 +178,36 @@ class JsonRpcParserSpec extends Specification {
!act.hasResult() !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"() { def "Handle non-json with producing an error response"() {
setup: setup:
def json = 'NOT JSON' def json = 'NOT JSON'

View File

@@ -90,7 +90,7 @@ class JsonRpcResponseSpec extends Specification {
def "Serialize int id and error"() { def "Serialize int id and error"() {
setup: 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: when:
def act = objectMapper.writeValueAsString(json) def act = objectMapper.writeValueAsString(json)
then: then:
@@ -127,7 +127,7 @@ class JsonRpcResponseSpec extends Specification {
def "Serialize string id and error"() { def "Serialize string id and error"() {
setup: setup:
def json = new JsonRpcResponse(null, def json = new JsonRpcResponse(null,
new JsonRpcResponse.ResponseError(-32041, "Oooops"), new JsonRpcError(-32041, "Oooops"),
new JsonRpcResponse.StringId("9kbo29gkaasf")) new JsonRpcResponse.StringId("9kbo29gkaasf"))
when: when:
def act = objectMapper.writeValueAsString(json) def act = objectMapper.writeValueAsString(json)

View File

@@ -8,19 +8,21 @@ interface CallHandler {
private Object result private Object result
private Integer errorCode private Integer errorCode
private String errorMessage 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.result = result
this.errorCode = errorCode this.errorCode = errorCode
this.errorMessage = errorMessage this.errorMessage = errorMessage
this.errorDetails = errorDetails
} }
static Result ok(Object result) { 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) { static Result error(int errorCode, String errorMessage, Object details = null) {
return new Result(null, errorCode, errorMessage) return new Result(null, errorCode, errorMessage, details)
} }
boolean isResult() { boolean isResult() {
@@ -38,5 +40,9 @@ interface CallHandler {
String getErrorMessage() { String getErrorMessage() {
return errorMessage return errorMessage
} }
Object getErrorDetails() {
return errorDetails
}
} }
} }

View File

@@ -44,6 +44,9 @@ class SimpleUpstream {
code : result.getErrorCode(), code : result.getErrorCode(),
message: result.getErrorMessage() message: result.getErrorMessage()
] ]
if (result.getErrorDetails() != null) {
resultJson["error"]["data"] = result.getErrorDetails()
}
} }
resp.status(200) resp.status(200)
resp.header("content-type", "application/json") resp.header("content-type", "application/json")

View File

@@ -19,6 +19,11 @@ class TestcaseHandler implements CallHandler {
&& params[0].to?.toLowerCase() == "0x542156d51D10Db5acCB99f9Db7e7C91B74E80a2c".toLowerCase()) { && params[0].to?.toLowerCase() == "0x542156d51D10Db5acCB99f9Db7e7C91B74E80a2c".toLowerCase()) {
return Result.error(-32015, "VM execution error.") 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 // https://github.com/emeraldpay/dshackle/issues/43
if (method == "debug_traceTransaction" if (method == "debug_traceTransaction"
&& params[0].toLowerCase() == "0xd949bc0fe1a5d16f4522bc47933554dcc4ada0493ff71ee1973b2410257af9fe".toLowerCase()) { && params[0].toLowerCase() == "0xd949bc0fe1a5d16f4522bc47933554dcc4ada0493ff71ee1973b2410257af9fe".toLowerCase()) {