solution: Ethereum WS connection fetches block through the same WS channel, instead of HTTP RPC
This commit is contained in:
@@ -27,13 +27,34 @@ import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
|
||||
import io.grpc.stub.StreamObserver
|
||||
import io.emeraldpay.etherjar.rpc.RpcResponseError
|
||||
import io.emeraldpay.etherjar.rpc.json.ResponseJson
|
||||
import io.netty.buffer.ByteBuf
|
||||
import io.netty.buffer.ByteBufAllocator
|
||||
import io.netty.buffer.ByteBufInputStream
|
||||
import io.netty.buffer.Unpooled
|
||||
import io.netty.handler.codec.http.HttpHeaders
|
||||
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame
|
||||
import io.netty.handler.codec.http.websocketx.WebSocketCloseStatus
|
||||
import io.netty.handler.codec.http.websocketx.WebSocketFrame
|
||||
import org.jetbrains.annotations.NotNull
|
||||
import org.reactivestreams.Publisher
|
||||
import org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
import reactor.core.publisher.Flux
|
||||
import reactor.core.publisher.Mono
|
||||
import reactor.core.publisher.Sinks
|
||||
import reactor.netty.ByteBufFlux
|
||||
import reactor.netty.Connection
|
||||
import reactor.netty.NettyInbound
|
||||
import reactor.netty.NettyOutbound
|
||||
import reactor.netty.http.websocket.WebsocketInbound
|
||||
import reactor.netty.http.websocket.WebsocketOutbound
|
||||
import reactor.util.annotation.Nullable
|
||||
|
||||
import java.time.Duration
|
||||
import java.util.concurrent.Callable
|
||||
import java.util.function.BiFunction
|
||||
import java.util.function.Consumer
|
||||
import java.util.function.Predicate
|
||||
|
||||
class EthereumApiMock implements Reader<JsonRpcRequest, JsonRpcResponse> {
|
||||
|
||||
@@ -57,7 +78,7 @@ class EthereumApiMock implements Reader<JsonRpcRequest, JsonRpcResponse> {
|
||||
}
|
||||
|
||||
@Override
|
||||
Mono<JsonRpcResponse> read(JsonRpcRequest request) {
|
||||
Mono<JsonRpcResponse> read(JsonRpcRequest request, boolean required = true) {
|
||||
Callable<JsonRpcResponse> call = {
|
||||
def predefined = predefined.find { it.isSame(request.method, request.params) }
|
||||
byte[] result = null
|
||||
@@ -65,7 +86,7 @@ class EthereumApiMock implements Reader<JsonRpcRequest, JsonRpcResponse> {
|
||||
if (predefined != null) {
|
||||
if (predefined.exception != null) {
|
||||
predefined.onCalled()
|
||||
predefined.print()
|
||||
predefined.print(request.id)
|
||||
throw predefined.exception
|
||||
}
|
||||
if (predefined.result instanceof RpcResponseError) {
|
||||
@@ -77,12 +98,15 @@ class EthereumApiMock implements Reader<JsonRpcRequest, JsonRpcResponse> {
|
||||
result = objectMapper.writeValueAsBytes(predefined.result)
|
||||
}
|
||||
predefined.onCalled()
|
||||
predefined.print()
|
||||
predefined.print(request.id)
|
||||
} else {
|
||||
log.error("Method ${request.method} with ${request.params} is not mocked")
|
||||
if (!required) {
|
||||
return null
|
||||
}
|
||||
error = new JsonRpcError(-32601, "Method ${request.method} with ${request.params} is not mocked")
|
||||
}
|
||||
return new JsonRpcResponse(result, error)
|
||||
return new JsonRpcResponse(result, error, JsonRpcResponse.Id.from(request.id))
|
||||
} as Callable<JsonRpcResponse>
|
||||
return Mono.fromCallable(call)
|
||||
}
|
||||
@@ -104,6 +128,10 @@ class EthereumApiMock implements Reader<JsonRpcRequest, JsonRpcResponse> {
|
||||
responseObserver.onCompleted()
|
||||
}
|
||||
|
||||
WebsocketApi asWebsocket() {
|
||||
return new WebsocketApi(this)
|
||||
}
|
||||
|
||||
class PredefinedResponse {
|
||||
String method
|
||||
List params
|
||||
@@ -132,8 +160,202 @@ class EthereumApiMock implements Reader<JsonRpcRequest, JsonRpcResponse> {
|
||||
}
|
||||
}
|
||||
|
||||
void print() {
|
||||
println "Execute API: $method ${params ? params : '_'} >> $result"
|
||||
void print(int id) {
|
||||
println "Execute API: $id $method ${params ? params : '_'} >> $result"
|
||||
}
|
||||
}
|
||||
|
||||
class WebsocketApi {
|
||||
private final EthereumApiMock api
|
||||
|
||||
private Sinks.Many<JsonRpcResponse> responses = Sinks
|
||||
.many()
|
||||
.unicast()
|
||||
.onBackpressureBuffer()
|
||||
private Sinks.Many<String> jsonResponses = Sinks
|
||||
.many()
|
||||
.unicast()
|
||||
.onBackpressureBuffer()
|
||||
private WebsocketOutboundMock outbound
|
||||
private WebsocketInboundMock inbound
|
||||
|
||||
WebsocketApi(EthereumApiMock api) {
|
||||
this.api = api
|
||||
outbound = new WebsocketOutboundMock(api, responses)
|
||||
inbound = new WebsocketInboundMock(responses.asFlux(), jsonResponses.asFlux())
|
||||
}
|
||||
|
||||
boolean send(String json) {
|
||||
jsonResponses.tryEmitNext(json).success
|
||||
}
|
||||
|
||||
WebsocketOutbound getOutbound() {
|
||||
return outbound
|
||||
}
|
||||
|
||||
WebsocketInbound getInbound() {
|
||||
return inbound
|
||||
}
|
||||
}
|
||||
|
||||
class WebsocketInboundMock implements WebsocketInbound {
|
||||
|
||||
private final Flux<JsonRpcResponse> responses
|
||||
private final Flux<String> jsonResponses
|
||||
|
||||
WebsocketInboundMock(Flux<JsonRpcResponse> responses, Flux<String> jsonResponses) {
|
||||
this.responses = responses
|
||||
this.jsonResponses = jsonResponses
|
||||
}
|
||||
|
||||
@Override
|
||||
String selectedSubprotocol() {
|
||||
throw new UnsupportedOperationException()
|
||||
}
|
||||
|
||||
@Override
|
||||
HttpHeaders headers() {
|
||||
throw new UnsupportedOperationException()
|
||||
}
|
||||
|
||||
@Override
|
||||
Mono<WebSocketCloseStatus> receiveCloseStatus() {
|
||||
return Mono.empty()
|
||||
}
|
||||
|
||||
@Override
|
||||
ByteBufFlux receive() {
|
||||
throw new UnsupportedOperationException()
|
||||
}
|
||||
|
||||
@Override
|
||||
Flux<?> receiveObject() {
|
||||
throw new UnsupportedOperationException()
|
||||
}
|
||||
|
||||
@Override
|
||||
NettyInbound withConnection(Consumer<? super Connection> withConnection) {
|
||||
return this
|
||||
}
|
||||
|
||||
@Override
|
||||
Flux<WebSocketFrame> receiveFrames() {
|
||||
return Flux.merge(
|
||||
jsonResponses,
|
||||
responses.map {
|
||||
Global.objectMapper.writeValueAsString(it)
|
||||
})
|
||||
.map {
|
||||
println("WS server->client msg: $it")
|
||||
new TextWebSocketFrame(it)
|
||||
}
|
||||
.doOnError { t ->
|
||||
t.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class WebsocketOutboundMock implements WebsocketOutbound {
|
||||
|
||||
private final EthereumApiMock api
|
||||
private final Sinks.Many<JsonRpcResponse> responses
|
||||
|
||||
WebsocketOutboundMock(EthereumApiMock api, Sinks.Many<JsonRpcResponse> responses) {
|
||||
this.api = api
|
||||
this.responses = responses
|
||||
}
|
||||
|
||||
@Override
|
||||
String selectedSubprotocol() {
|
||||
throw new UnsupportedOperationException()
|
||||
}
|
||||
|
||||
@Override
|
||||
ByteBufAllocator alloc() {
|
||||
throw new UnsupportedOperationException()
|
||||
}
|
||||
|
||||
private void handle(Publisher<ByteBuf> dataStream) {
|
||||
Flux.from(dataStream)
|
||||
.map { it ->
|
||||
Global.objectMapper.readValue(new ByteBufInputStream(it), JsonRpcRequest)
|
||||
}
|
||||
.flatMap { JsonRpcRequest request ->
|
||||
api.read(request, false)
|
||||
}
|
||||
.doOnNext {
|
||||
def status = responses.tryEmitNext(it)
|
||||
if (status.isFailure()) {
|
||||
println("Failed to send through mock: $status")
|
||||
}
|
||||
}
|
||||
.subscribe()
|
||||
}
|
||||
|
||||
@Override
|
||||
NettyOutbound send(Publisher<? extends ByteBuf> dataStream) {
|
||||
handle(dataStream)
|
||||
return this
|
||||
}
|
||||
|
||||
@Override
|
||||
NettyOutbound send(Publisher<? extends ByteBuf> dataStream, Predicate<ByteBuf> predicate) {
|
||||
handle(dataStream)
|
||||
return this
|
||||
}
|
||||
|
||||
@Override
|
||||
NettyOutbound sendObject(Publisher<?> dataStream, Predicate<Object> predicate) {
|
||||
def msgs = Flux.from(dataStream)
|
||||
.cast(TextWebSocketFrame)
|
||||
.map {
|
||||
Unpooled.wrappedBuffer(it.text().bytes)
|
||||
}
|
||||
handle(msgs)
|
||||
return this
|
||||
}
|
||||
|
||||
@Override
|
||||
NettyOutbound sendObject(Object message) {
|
||||
return this
|
||||
}
|
||||
|
||||
@Override
|
||||
def <S> NettyOutbound sendUsing(Callable<? extends S> sourceInput, BiFunction<? super Connection, ? super S, ?> mappedInput, Consumer<? super S> sourceCleanup) {
|
||||
return this
|
||||
}
|
||||
|
||||
@Override
|
||||
NettyOutbound withConnection(Consumer<? super Connection> withConnection) {
|
||||
return this
|
||||
}
|
||||
|
||||
@Override
|
||||
Mono<Void> sendClose() {
|
||||
return Mono.fromCallable {
|
||||
responses.tryEmitComplete()
|
||||
}.then()
|
||||
}
|
||||
|
||||
@Override
|
||||
Mono<Void> sendClose(int rsv) {
|
||||
return Mono.fromCallable {
|
||||
responses.tryEmitComplete()
|
||||
}.then()
|
||||
}
|
||||
|
||||
@Override
|
||||
Mono<Void> sendClose(int statusCode, @Nullable String reasonText) {
|
||||
return Mono.fromCallable {
|
||||
responses.tryEmitComplete()
|
||||
}.then()
|
||||
}
|
||||
|
||||
@Override
|
||||
Mono<Void> sendClose(int rsv, int statusCode, @Nullable String reasonText) {
|
||||
return Mono.fromCallable {
|
||||
responses.tryEmitComplete()
|
||||
}.then()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,11 +15,15 @@
|
||||
*/
|
||||
package io.emeraldpay.dshackle.upstream.ethereum
|
||||
|
||||
import io.emeraldpay.dshackle.cache.BlocksMemCache
|
||||
import io.emeraldpay.dshackle.Global
|
||||
import io.emeraldpay.dshackle.data.BlockContainer
|
||||
import io.emeraldpay.dshackle.test.TestingCommons
|
||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
|
||||
import io.emeraldpay.etherjar.domain.BlockHash
|
||||
import io.emeraldpay.etherjar.domain.TransactionId
|
||||
import io.emeraldpay.etherjar.rpc.RpcResponseError
|
||||
import io.emeraldpay.etherjar.rpc.json.BlockJson
|
||||
import io.emeraldpay.etherjar.rpc.json.TransactionJson
|
||||
import io.emeraldpay.etherjar.rpc.json.TransactionRefJson
|
||||
import reactor.core.publisher.Flux
|
||||
import reactor.test.StepVerifier
|
||||
@@ -34,7 +38,6 @@ class EthereumWsFactorySpec extends Specification {
|
||||
def "Fetch block"() {
|
||||
setup:
|
||||
def wsf = new EthereumWsFactory(new URI("http://localhost"), new URI("http://localhost"))
|
||||
def blocksCache = Mock(BlocksMemCache)
|
||||
|
||||
def block = new BlockJson<TransactionRefJson>()
|
||||
block.number = 100
|
||||
@@ -44,20 +47,98 @@ class EthereumWsFactorySpec extends Specification {
|
||||
block.uncles = []
|
||||
block.totalDifficulty = BigInteger.ONE
|
||||
|
||||
def headBlock = block.copy().tap {
|
||||
it.transactions = null
|
||||
}
|
||||
|
||||
def apiMock = TestingCommons.api()
|
||||
def upstream = TestingCommons.upstream(apiMock)
|
||||
def ws = wsf.create(upstream)
|
||||
def wsApiMock = apiMock.asWebsocket()
|
||||
def ws = wsf.create()
|
||||
|
||||
apiMock.answerOnce("eth_getBlockByHash", ["0x3ec2ebf5d0ec474d0ac6bc50d2770d8409ad76e119968e7919f85d5ec8915200", false], block)
|
||||
|
||||
when:
|
||||
def act = Flux.from(ws.getFlux())
|
||||
Flux.from(ws.handle(wsApiMock.inbound, wsApiMock.outbound)).subscribe()
|
||||
def act = Flux.from(ws.getBlocksFlux())
|
||||
|
||||
then:
|
||||
StepVerifier.create(act)
|
||||
.then { ws.onNewBlock(block) }
|
||||
.then { ws.onNewHeads(headBlock).subscribe() }
|
||||
.expectNext(BlockContainer.from(block))
|
||||
.thenCancel()
|
||||
.verify(Duration.ofSeconds(1))
|
||||
}
|
||||
|
||||
def "Makes a RPC call"() {
|
||||
setup:
|
||||
def wsf = new EthereumWsFactory(new URI("http://localhost"), new URI("http://localhost"))
|
||||
def apiMock = TestingCommons.api()
|
||||
def wsApiMock = apiMock.asWebsocket()
|
||||
def ws = wsf.create()
|
||||
|
||||
def tx = new TransactionJson().tap {
|
||||
hash = TransactionId.from("0x3ec2ebf5d0ec474d0ac6bc50d2770d8409ad76e119968e7919f85d5ec8915200")
|
||||
}
|
||||
apiMock.answerOnce("eth_getTransactionByHash", ["0x3ec2ebf5d0ec474d0ac6bc50d2770d8409ad76e119968e7919f85d5ec8915200"], tx)
|
||||
|
||||
when:
|
||||
Flux.from(ws.handle(wsApiMock.inbound, wsApiMock.outbound)).subscribe()
|
||||
def act = ws.call(new JsonRpcRequest("eth_getTransactionByHash", ["0x3ec2ebf5d0ec474d0ac6bc50d2770d8409ad76e119968e7919f85d5ec8915200"], 15))
|
||||
|
||||
then:
|
||||
StepVerifier.create(act)
|
||||
.expectNextMatches {
|
||||
it.id.asNumber() == 15L && Global.objectMapper.readValue(it.result, TransactionJson) == tx
|
||||
}
|
||||
.expectComplete()
|
||||
.verify(Duration.ofSeconds(1))
|
||||
}
|
||||
|
||||
def "Makes a RPC call - return null"() {
|
||||
setup:
|
||||
def wsf = new EthereumWsFactory(new URI("http://localhost"), new URI("http://localhost"))
|
||||
def apiMock = TestingCommons.api()
|
||||
def wsApiMock = apiMock.asWebsocket()
|
||||
def ws = wsf.create()
|
||||
|
||||
apiMock.answerOnce("eth_getTransactionByHash", ["0x3ec2ebf5d0ec474d0ac6bc50d2770d8409ad76e119968e7919f85d5ec8915200"], null)
|
||||
|
||||
when:
|
||||
Flux.from(ws.handle(wsApiMock.inbound, wsApiMock.outbound)).subscribe()
|
||||
def act = ws.call(new JsonRpcRequest("eth_getTransactionByHash", ["0x3ec2ebf5d0ec474d0ac6bc50d2770d8409ad76e119968e7919f85d5ec8915200"], 15))
|
||||
|
||||
then:
|
||||
StepVerifier.create(act)
|
||||
.expectNextMatches {
|
||||
it.id.asNumber() == 15L &&
|
||||
it.resultAsRawString == 'null'
|
||||
}
|
||||
.expectComplete()
|
||||
.verify(Duration.ofSeconds(1))
|
||||
}
|
||||
|
||||
def "Makes a RPC call - return error"() {
|
||||
setup:
|
||||
def wsf = new EthereumWsFactory(new URI("http://localhost"), new URI("http://localhost"))
|
||||
def apiMock = TestingCommons.api()
|
||||
def wsApiMock = apiMock.asWebsocket()
|
||||
def ws = wsf.create()
|
||||
|
||||
apiMock.answerOnce("eth_getTransactionByHash", ["0x3ec2ebf5d0ec474d0ac6bc50d2770d8409ad76e119968e7919f85d5ec8915200"],
|
||||
new RpcResponseError(RpcResponseError.CODE_METHOD_NOT_EXIST, "test"))
|
||||
|
||||
when:
|
||||
Flux.from(ws.handle(wsApiMock.inbound, wsApiMock.outbound)).subscribe()
|
||||
def act = ws.call(new JsonRpcRequest("eth_getTransactionByHash", ["0x3ec2ebf5d0ec474d0ac6bc50d2770d8409ad76e119968e7919f85d5ec8915200"], 15))
|
||||
|
||||
then:
|
||||
StepVerifier.create(act)
|
||||
.expectNextMatches {
|
||||
it.id.asNumber() == 15L &&
|
||||
it.error != null &&
|
||||
it.error.code == RpcResponseError.CODE_METHOD_NOT_EXIST && it.error.message == "test"
|
||||
}
|
||||
.expectComplete()
|
||||
.verify(Duration.ofSeconds(1))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,19 +18,9 @@ package io.emeraldpay.dshackle.upstream.rpcclient
|
||||
import io.emeraldpay.etherjar.rpc.RpcResponseError
|
||||
import spock.lang.Specification
|
||||
|
||||
class JsonRpcParserSpec extends Specification {
|
||||
class ResponseRpcParserSpec extends Specification {
|
||||
|
||||
JsonRpcParser parser = new JsonRpcParser()
|
||||
|
||||
def "Parse just result"() {
|
||||
setup:
|
||||
def json = '{"result": "Hello world!"}'
|
||||
when:
|
||||
def act = parser.parse(json.getBytes())
|
||||
then:
|
||||
act.error == null
|
||||
new String(act.result) == '"Hello world!"'
|
||||
}
|
||||
ResponseRpcParser parser = new ResponseRpcParser()
|
||||
|
||||
def "Parse string response"() {
|
||||
setup:
|
||||
@@ -178,6 +168,19 @@ class JsonRpcParserSpec extends Specification {
|
||||
!act.hasResult()
|
||||
}
|
||||
|
||||
def "Parse error with no result field"() {
|
||||
setup:
|
||||
def json = '{"jsonrpc": "2.0", "id": 1, "error": {"code": -1111, "message": "test"}}'
|
||||
when:
|
||||
def act = parser.parse(json.getBytes())
|
||||
then:
|
||||
act.error != null
|
||||
act.error.code == -1111
|
||||
act.error.message == "test"
|
||||
act.hasError()
|
||||
!act.hasResult()
|
||||
}
|
||||
|
||||
def "Parse error with data"() {
|
||||
setup:
|
||||
// 0 8 16 32
|
||||
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* Copyright (c) 2021 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 spock.lang.Specification
|
||||
|
||||
class ResponseWSParserSpec extends Specification {
|
||||
|
||||
ResponseWSParser parser = new ResponseWSParser()
|
||||
|
||||
def "Parse subscription response"() {
|
||||
setup:
|
||||
def msg = "{\n" +
|
||||
" \"id\": \"blocks\", \n" +
|
||||
" \"jsonrpc\": \"2.0\", \n" +
|
||||
" \"result\": \"0x9cef478923ff08bf67fde6c64013158d\"\n" +
|
||||
"}"
|
||||
when:
|
||||
def act = parser.parse(msg.bytes)
|
||||
then:
|
||||
act.type == ResponseWSParser.Type.RPC
|
||||
act.id.asString() == "blocks"
|
||||
act.error == null
|
||||
act.value == "\"0x9cef478923ff08bf67fde6c64013158d\"".bytes
|
||||
}
|
||||
|
||||
def "Parse newHeads event"() {
|
||||
setup:
|
||||
def msg = "{\n" +
|
||||
" \"jsonrpc\": \"2.0\",\n" +
|
||||
" \"method\": \"eth_subscription\",\n" +
|
||||
" \"params\": {\n" +
|
||||
" \"result\": {\n" +
|
||||
" \"difficulty\": \"0x15d9223a23aa\",\n" +
|
||||
" \"extraData\": \"0xd983010305844765746887676f312e342e328777696e646f7773\",\n" +
|
||||
" \"gasLimit\": \"0x47e7c4\",\n" +
|
||||
" \"gasUsed\": \"0x38658\",\n" +
|
||||
" \"logsBloom\": \"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\",\n" +
|
||||
" \"miner\": \"0xf8b483dba2c3b7176a3da549ad41a48bb3121069\",\n" +
|
||||
" \"nonce\": \"0x084149998194cc5f\",\n" +
|
||||
" \"number\": \"0x1348c9\",\n" +
|
||||
" \"parentHash\": \"0x7736fab79e05dc611604d22470dadad26f56fe494421b5b333de816ce1f25701\",\n" +
|
||||
" \"receiptRoot\": \"0x2fab35823ad00c7bb388595cb46652fe7886e00660a01e867824d3dceb1c8d36\",\n" +
|
||||
" \"sha3Uncles\": \"0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347\",\n" +
|
||||
" \"stateRoot\": \"0xb3346685172db67de536d8765c43c31009d0eb3bd9c501c9be3229203f15f378\",\n" +
|
||||
" \"timestamp\": \"0x56ffeff8\",\n" +
|
||||
" \"transactionsRoot\": \"0x0167ffa60e3ebc0b080cdb95f7c0087dd6c0e61413140e39d94d3468d7c9689f\"\n" +
|
||||
" },\n" +
|
||||
" \"subscription\": \"0x9ce59a13059e417087c02d3236a0b1cc\"\n" +
|
||||
" }\n" +
|
||||
"}"
|
||||
when:
|
||||
def act = parser.parse(msg.bytes)
|
||||
then:
|
||||
act.type == ResponseWSParser.Type.SUBSCRIPTION
|
||||
act.id.asString() == "0x9ce59a13059e417087c02d3236a0b1cc"
|
||||
act.error == null
|
||||
with(new String(act.value)) {
|
||||
it.length() > 0
|
||||
it.startsWith("{")
|
||||
it.endsWith("}")
|
||||
it.contains("\"difficulty\": \"0x15d9223a23aa\"")
|
||||
}
|
||||
}
|
||||
|
||||
def "Parse RPC with error"() {
|
||||
setup:
|
||||
def msg = "{\"jsonrpc\":\"2.0\",\"id\":151,\"error\":{\"code\":-32602,\"message\":\"invalid blocknumber\"}}"
|
||||
when:
|
||||
def act = parser.parse(msg.bytes)
|
||||
then:
|
||||
act.type == ResponseWSParser.Type.RPC
|
||||
act.id.asNumber() == 151L
|
||||
act.error != null
|
||||
act.value == null
|
||||
with(act.error) {
|
||||
it.code == -32602
|
||||
it.message == "invalid blocknumber"
|
||||
}
|
||||
}
|
||||
|
||||
def "Parse RPC with null result"() {
|
||||
setup:
|
||||
def msg = "{\"jsonrpc\":\"2.0\",\"id\":100,\"result\":null}"
|
||||
when:
|
||||
def act = parser.parse(msg.bytes)
|
||||
then:
|
||||
act.type == ResponseWSParser.Type.RPC
|
||||
act.id.asNumber() == 100L
|
||||
act.error == null
|
||||
new String(act.value) == "null"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user