@@ -23,6 +23,7 @@ import io.emeraldpay.dshackle.proxy.ProxyServer
|
||||
import io.emeraldpay.dshackle.proxy.ReadRpcJson
|
||||
import io.emeraldpay.dshackle.proxy.WriteRpcJson
|
||||
import io.emeraldpay.dshackle.rpc.NativeCall
|
||||
import io.emeraldpay.dshackle.rpc.NativeSubscribe
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.stereotype.Service
|
||||
@@ -37,6 +38,7 @@ class ProxyStarter(
|
||||
@Autowired private val readRpcJson: ReadRpcJson,
|
||||
@Autowired private val writeRpcJson: WriteRpcJson,
|
||||
@Autowired private val nativeCall: NativeCall,
|
||||
@Autowired private val nativeSubscribe: NativeSubscribe,
|
||||
@Autowired private val tlsSetup: TlsSetup,
|
||||
@Autowired private val accessHandlerHttp: AccessHandlerHttp,
|
||||
// depend on Monitoring, declared here just to ensure it's properly initialized before the Proxy
|
||||
@@ -54,7 +56,7 @@ class ProxyStarter(
|
||||
log.debug("Proxy server is not configured")
|
||||
return
|
||||
}
|
||||
val server = ProxyServer(config, readRpcJson, writeRpcJson, nativeCall, tlsSetup, accessHandlerHttp.factory)
|
||||
val server = ProxyServer(config, readRpcJson, writeRpcJson, nativeCall, nativeSubscribe, tlsSetup, accessHandlerHttp.factory)
|
||||
server.start()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ open class ProxyConfig {
|
||||
}
|
||||
|
||||
var enabled: Boolean = true
|
||||
var websocketEnabled: Boolean = true
|
||||
|
||||
/**
|
||||
* Host to bind server. Default: 127.0.0.1
|
||||
|
||||
86
src/main/kotlin/io/emeraldpay/dshackle/proxy/BaseHandler.kt
Normal file
86
src/main/kotlin/io/emeraldpay/dshackle/proxy/BaseHandler.kt
Normal file
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* 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.proxy
|
||||
|
||||
import io.emeraldpay.api.proto.BlockchainOuterClass
|
||||
import io.emeraldpay.api.proto.Common
|
||||
import io.emeraldpay.dshackle.monitoring.accesslog.AccessHandlerHttp
|
||||
import io.emeraldpay.dshackle.rpc.NativeCall
|
||||
import io.emeraldpay.grpc.Chain
|
||||
import org.reactivestreams.Publisher
|
||||
import org.slf4j.LoggerFactory
|
||||
import reactor.core.publisher.Flux
|
||||
import reactor.core.publisher.Mono
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
abstract class BaseHandler(
|
||||
private val writeRpcJson: WriteRpcJson,
|
||||
private val nativeCall: NativeCall,
|
||||
private val requestMetrics: ProxyServer.RequestMetricsFactory,
|
||||
) {
|
||||
|
||||
companion object {
|
||||
private val log = LoggerFactory.getLogger(BaseHandler::class.java)
|
||||
}
|
||||
|
||||
fun execute(chain: Chain, call: ProxyCall, handler: AccessHandlerHttp.RequestHandler): Publisher<String> {
|
||||
// return empty response for empty request
|
||||
if (call.items.isEmpty()) {
|
||||
return if (call.type == ProxyCall.RpcType.BATCH) {
|
||||
Mono.just("[]")
|
||||
} else {
|
||||
Mono.just("")
|
||||
}
|
||||
}
|
||||
val jsons = execute(chain, call.items, handler)
|
||||
.transform(writeRpcJson.toJsons(call))
|
||||
return if (call.type == ProxyCall.RpcType.SINGLE) {
|
||||
jsons.next()
|
||||
} else {
|
||||
jsons.transform(writeRpcJson.asArray())
|
||||
}
|
||||
}
|
||||
|
||||
fun execute(chain: Chain, items: List<BlockchainOuterClass.NativeCallItem>, handler: AccessHandlerHttp.RequestHandler): Flux<NativeCall.CallResult> {
|
||||
val startTime = System.currentTimeMillis()
|
||||
// during the execution we know only ID of the call, so we use it to find the origin call and associated metrics
|
||||
val metricById = { id: Int ->
|
||||
items.find { it.id == id }?.let { item ->
|
||||
requestMetrics.get(chain, item.method)
|
||||
}
|
||||
}
|
||||
val request = BlockchainOuterClass.NativeCallRequest.newBuilder()
|
||||
.setChain(Common.ChainRef.forNumber(chain.id))
|
||||
.addAllItems(items)
|
||||
.build()
|
||||
handler.onRequest(request)
|
||||
return nativeCall
|
||||
.nativeCallResult(Mono.just(request))
|
||||
.doOnNext {
|
||||
metricById(it.id)?.let { metrics ->
|
||||
metrics.requestMetric.increment()
|
||||
metrics.callMetric.record(System.currentTimeMillis() - startTime, TimeUnit.MILLISECONDS)
|
||||
}
|
||||
handler.onResponse(it)
|
||||
}
|
||||
.doOnError {
|
||||
// when error happened the whole flux is stopped and no result is produced, so we should mark all the requests as failed
|
||||
items.forEach { item ->
|
||||
requestMetrics.get(chain, item.method).errorMetric.increment()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
86
src/main/kotlin/io/emeraldpay/dshackle/proxy/HttpHandler.kt
Normal file
86
src/main/kotlin/io/emeraldpay/dshackle/proxy/HttpHandler.kt
Normal file
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* 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.proxy
|
||||
|
||||
import io.emeraldpay.dshackle.Global
|
||||
import io.emeraldpay.dshackle.config.ProxyConfig
|
||||
import io.emeraldpay.dshackle.monitoring.accesslog.AccessHandlerHttp
|
||||
import io.emeraldpay.dshackle.rpc.NativeCall
|
||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
|
||||
import io.emeraldpay.etherjar.rpc.RpcException
|
||||
import io.emeraldpay.grpc.Chain
|
||||
import io.netty.buffer.ByteBuf
|
||||
import io.netty.buffer.Unpooled
|
||||
import org.reactivestreams.Publisher
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.http.HttpHeaders
|
||||
import reactor.core.publisher.Flux
|
||||
import reactor.core.publisher.Mono
|
||||
import reactor.netty.http.server.HttpServerRequest
|
||||
import reactor.netty.http.server.HttpServerResponse
|
||||
import java.util.function.BiFunction
|
||||
|
||||
/**
|
||||
* Responds to HTTP requests made to the Ethereum Proxy Server
|
||||
*/
|
||||
class HttpHandler(
|
||||
private val readRpcJson: ReadRpcJson,
|
||||
writeRpcJson: WriteRpcJson,
|
||||
nativeCall: NativeCall,
|
||||
private val accessHandler: AccessHandlerHttp.HandlerFactory,
|
||||
requestMetrics: ProxyServer.RequestMetricsFactory,
|
||||
) : BaseHandler(writeRpcJson, nativeCall, requestMetrics) {
|
||||
|
||||
companion object {
|
||||
private val log = LoggerFactory.getLogger(HttpHandler::class.java)
|
||||
}
|
||||
|
||||
fun proxy(routeConfig: ProxyConfig.Route): BiFunction<HttpServerRequest, HttpServerResponse, Publisher<Void>> {
|
||||
return BiFunction { req, resp ->
|
||||
// handle access events
|
||||
val eventHandler = accessHandler.create(req, routeConfig.blockchain)
|
||||
val request = req.receive()
|
||||
.aggregate()
|
||||
.asByteArray()
|
||||
val results = processRequest(routeConfig.blockchain, request, eventHandler)
|
||||
// make sure that the access log handler is closed at the end, so it can render the logs
|
||||
.doFinally { eventHandler.close() }
|
||||
resp.addHeader(HttpHeaders.CONTENT_TYPE, "application/json")
|
||||
.send(results)
|
||||
}
|
||||
}
|
||||
|
||||
fun processRequest(
|
||||
chain: Chain,
|
||||
request: Mono<ByteArray>,
|
||||
handler: AccessHandlerHttp.RequestHandler
|
||||
): Flux<ByteBuf> {
|
||||
return request
|
||||
.map(readRpcJson)
|
||||
.flatMapMany { call ->
|
||||
execute(chain, call, handler)
|
||||
}
|
||||
.onErrorResume(RpcException::class.java) { err ->
|
||||
val id = err.details?.let {
|
||||
if (it is JsonRpcResponse.Id) it else JsonRpcResponse.NumberId(-1)
|
||||
} ?: JsonRpcResponse.NumberId(-1)
|
||||
|
||||
val json = JsonRpcResponse.error(err.code, err.rpcMessage, id)
|
||||
Mono.just(Global.objectMapper.writeValueAsString(json))
|
||||
}
|
||||
.map { Unpooled.wrappedBuffer(it.toByteArray()) }
|
||||
}
|
||||
}
|
||||
@@ -16,36 +16,23 @@
|
||||
*/
|
||||
package io.emeraldpay.dshackle.proxy
|
||||
|
||||
import io.emeraldpay.api.proto.BlockchainOuterClass
|
||||
import io.emeraldpay.api.proto.Common
|
||||
import io.emeraldpay.dshackle.Global
|
||||
import io.emeraldpay.dshackle.TlsSetup
|
||||
import io.emeraldpay.dshackle.config.ProxyConfig
|
||||
import io.emeraldpay.dshackle.monitoring.accesslog.AccessHandlerHttp
|
||||
import io.emeraldpay.dshackle.rpc.NativeCall
|
||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
|
||||
import io.emeraldpay.etherjar.rpc.RpcException
|
||||
import io.emeraldpay.dshackle.rpc.NativeSubscribe
|
||||
import io.emeraldpay.grpc.Chain
|
||||
import io.micrometer.core.instrument.Counter
|
||||
import io.micrometer.core.instrument.Metrics
|
||||
import io.micrometer.core.instrument.Timer
|
||||
import io.netty.buffer.ByteBuf
|
||||
import io.netty.buffer.Unpooled
|
||||
import io.netty.channel.ChannelHandler
|
||||
import io.netty.channel.ChannelHandlerContext
|
||||
import org.reactivestreams.Publisher
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.http.HttpHeaders
|
||||
import reactor.core.publisher.Flux
|
||||
import reactor.core.publisher.Mono
|
||||
import reactor.netty.http.server.HttpServer
|
||||
import reactor.netty.http.server.HttpServerRequest
|
||||
import reactor.netty.http.server.HttpServerResponse
|
||||
import reactor.netty.http.server.HttpServerRoutes
|
||||
import java.util.EnumMap
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.concurrent.locks.ReentrantReadWriteLock
|
||||
import java.util.function.BiFunction
|
||||
import kotlin.concurrent.read
|
||||
import kotlin.concurrent.write
|
||||
|
||||
@@ -57,6 +44,7 @@ class ProxyServer(
|
||||
private val readRpcJson: ReadRpcJson,
|
||||
private val writeRpcJson: WriteRpcJson,
|
||||
private val nativeCall: NativeCall,
|
||||
private val nativeSubscribe: NativeSubscribe,
|
||||
private val tlsSetup: TlsSetup,
|
||||
private val accessHandler: AccessHandlerHttp.HandlerFactory
|
||||
) {
|
||||
@@ -92,6 +80,11 @@ class ProxyServer(
|
||||
StandardRequestMetrics()
|
||||
}
|
||||
|
||||
private val httpHandler = HttpHandler(readRpcJson, writeRpcJson, nativeCall, accessHandler, requestMetrics)
|
||||
private val wsHandler: WebsocketHandler? = if (config.websocketEnabled) {
|
||||
WebsocketHandler(readRpcJson, writeRpcJson, nativeCall, nativeSubscribe, requestMetrics)
|
||||
} else null
|
||||
|
||||
fun start() {
|
||||
if (!config.enabled) {
|
||||
log.debug("Proxy server is not enabled")
|
||||
@@ -116,88 +109,11 @@ class ProxyServer(
|
||||
|
||||
fun setupRoutes(routes: HttpServerRoutes) {
|
||||
config.routes.forEach { routeConfig ->
|
||||
routes.post("/" + routeConfig.id, proxy(routeConfig))
|
||||
}
|
||||
}
|
||||
|
||||
fun execute(chain: Chain, call: ProxyCall, handler: AccessHandlerHttp.RequestHandler): Publisher<String> {
|
||||
// return empty response for empty request
|
||||
if (call.items.isEmpty()) {
|
||||
return if (call.type == ProxyCall.RpcType.BATCH) {
|
||||
Mono.just("[]")
|
||||
} else {
|
||||
Mono.just("")
|
||||
routes.post("/" + routeConfig.id, httpHandler.proxy(routeConfig))
|
||||
if (config.websocketEnabled && wsHandler != null) {
|
||||
routes.ws("/" + routeConfig.id, wsHandler.proxy(routeConfig))
|
||||
}
|
||||
}
|
||||
val startTime = System.currentTimeMillis()
|
||||
// during the execution we know only ID of the call, we use it to find the origin call and associated metrics
|
||||
val metricById = { id: Int ->
|
||||
call.items.find { it.id == id }?.let { item ->
|
||||
requestMetrics.get(chain, item.method)
|
||||
}
|
||||
}
|
||||
val request = BlockchainOuterClass.NativeCallRequest.newBuilder()
|
||||
.setChain(Common.ChainRef.forNumber(chain.id))
|
||||
.addAllItems(call.items)
|
||||
.build()
|
||||
handler.onRequest(request)
|
||||
val jsons = nativeCall
|
||||
.nativeCallResult(Mono.just(request))
|
||||
.doOnNext {
|
||||
metricById(it.id)?.requestMetric?.increment()
|
||||
}
|
||||
.doOnNext {
|
||||
handler.onResponse(it)
|
||||
metricById(it.id)?.callMetric?.record(System.currentTimeMillis() - startTime, TimeUnit.MILLISECONDS)
|
||||
}
|
||||
.doOnError {
|
||||
// when error happened the whole flux is stopped and no result is produced, so we should mark all the requests as failed
|
||||
call.items.forEach { item ->
|
||||
requestMetrics.get(chain, item.method).errorMetric.increment()
|
||||
}
|
||||
}
|
||||
.transform(writeRpcJson.toJsons(call))
|
||||
return if (call.type == ProxyCall.RpcType.SINGLE) {
|
||||
jsons.next()
|
||||
} else {
|
||||
jsons.transform(writeRpcJson.asArray())
|
||||
}
|
||||
}
|
||||
|
||||
fun processRequest(
|
||||
chain: Chain,
|
||||
request: Mono<ByteArray>,
|
||||
handler: AccessHandlerHttp.RequestHandler
|
||||
): Flux<ByteBuf> {
|
||||
return request
|
||||
.map(readRpcJson)
|
||||
.flatMapMany { call ->
|
||||
execute(chain, call, handler)
|
||||
}
|
||||
.onErrorResume(RpcException::class.java) { err ->
|
||||
val id = err.details?.let {
|
||||
if (it is JsonRpcResponse.Id) it else JsonRpcResponse.NumberId(-1)
|
||||
} ?: JsonRpcResponse.NumberId(-1)
|
||||
|
||||
val json = JsonRpcResponse.error(err.code, err.rpcMessage, id)
|
||||
Mono.just(Global.objectMapper.writeValueAsString(json))
|
||||
}
|
||||
.map { Unpooled.wrappedBuffer(it.toByteArray()) }
|
||||
}
|
||||
|
||||
fun proxy(routeConfig: ProxyConfig.Route): BiFunction<HttpServerRequest, HttpServerResponse, Publisher<Void>> {
|
||||
return BiFunction { req, resp ->
|
||||
// handle access events
|
||||
val eventHandler = accessHandler.create(req, routeConfig.blockchain)
|
||||
val request = req.receive()
|
||||
.aggregate()
|
||||
.asByteArray()
|
||||
val results = processRequest(routeConfig.blockchain, request, eventHandler)
|
||||
// make sure that the access log handler is closed at the end, so it can render the logs
|
||||
.doFinally { eventHandler.close() }
|
||||
resp.addHeader(HttpHeaders.CONTENT_TYPE, "application/json")
|
||||
.send(results)
|
||||
}
|
||||
}
|
||||
|
||||
interface RequestMetricsFactory {
|
||||
|
||||
@@ -28,7 +28,6 @@ import org.slf4j.LoggerFactory
|
||||
import org.springframework.stereotype.Service
|
||||
import java.io.IOException
|
||||
import java.util.function.Function
|
||||
import java.util.stream.Collectors
|
||||
|
||||
/**
|
||||
* Reader for JSON RPC request
|
||||
@@ -41,11 +40,11 @@ open class ReadRpcJson : Function<ByteArray, ProxyCall> {
|
||||
private val spaces = " \n\t".toByteArray()
|
||||
}
|
||||
|
||||
private val jsonExtractor: Function<Map<*, *>, RequestJson<Any>>
|
||||
val jsonExtractor: (Map<*, *>) -> RequestJson<Any>
|
||||
private val objectMapper: ObjectMapper = Global.objectMapper
|
||||
|
||||
init {
|
||||
jsonExtractor = Function { json ->
|
||||
jsonExtractor = { json ->
|
||||
if (json["id"] == null) {
|
||||
throw RpcException(RpcResponseError.CODE_INVALID_REQUEST, "ID is not set")
|
||||
}
|
||||
@@ -126,33 +125,11 @@ open class ReadRpcJson : Function<ByteArray, ProxyCall> {
|
||||
* Convert payload to the proxy call details
|
||||
*/
|
||||
override fun apply(data: ByteArray): ProxyCall {
|
||||
val list: MutableList<Map<*, *>>
|
||||
val list: List<Map<*, *>>
|
||||
try {
|
||||
val type = getType(data)
|
||||
if (ProxyCall.RpcType.BATCH == type) {
|
||||
list = objectMapper.readerFor(MutableList::class.java).readValue(data)
|
||||
} else {
|
||||
list = ArrayList(1)
|
||||
val json = objectMapper.readerFor(MutableMap::class.java).readValue<Map<*, *>>(data)
|
||||
list.add(json)
|
||||
}
|
||||
val context = ProxyCall(type)
|
||||
// our internal ids for calls
|
||||
var seq = 0
|
||||
val batch = list.stream()
|
||||
.map<RequestJson<Any>>(jsonExtractor)
|
||||
.map { json ->
|
||||
val id = seq++
|
||||
context.ids[id] = json.id
|
||||
BlockchainOuterClass.NativeCallItem.newBuilder()
|
||||
.setId(id)
|
||||
.setMethod(json.method)
|
||||
.setPayload(ByteString.copyFrom(objectMapper.writeValueAsBytes(json.params)))
|
||||
.build()
|
||||
}
|
||||
.collect(Collectors.toList())
|
||||
context.items.addAll(batch)
|
||||
return context
|
||||
list = extract(type, data)
|
||||
return convertMapToNativeCall(type, list)
|
||||
} catch (e: RpcException) {
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
@@ -160,4 +137,41 @@ open class ReadRpcJson : Function<ByteArray, ProxyCall> {
|
||||
throw RpcException(RpcResponseError.CODE_INVALID_JSON, e.message)
|
||||
}
|
||||
}
|
||||
|
||||
fun extract(type: ProxyCall.RpcType, data: ByteArray): List<Map<*, *>> {
|
||||
return if (ProxyCall.RpcType.BATCH == type) {
|
||||
objectMapper.readerFor(MutableList::class.java).readValue(data)
|
||||
} else {
|
||||
val list = ArrayList<Map<*, *>>(1)
|
||||
val json = objectMapper.readerFor(MutableMap::class.java).readValue<Map<*, *>>(data)
|
||||
list.add(json)
|
||||
list
|
||||
}
|
||||
}
|
||||
|
||||
fun convertMapToNativeCall(type: ProxyCall.RpcType, list: List<Map<*, *>>): ProxyCall {
|
||||
return convertToNativeCall(type, list.map(jsonExtractor))
|
||||
}
|
||||
|
||||
fun convertToNativeCall(type: ProxyCall.RpcType, list: List<RequestJson<Any>>): ProxyCall {
|
||||
val context = ProxyCall(type)
|
||||
val batch = convertToNativeCall(0, context, list)
|
||||
context.items.addAll(batch)
|
||||
return context
|
||||
}
|
||||
|
||||
fun convertToNativeCall(seqStart: Int, context: ProxyCall, items: List<RequestJson<Any>>): List<BlockchainOuterClass.NativeCallItem> {
|
||||
// internal ids for calls
|
||||
var seq = seqStart
|
||||
return items
|
||||
.map { json ->
|
||||
val id = seq++
|
||||
context.ids[id] = json.id
|
||||
BlockchainOuterClass.NativeCallItem.newBuilder()
|
||||
.setId(id)
|
||||
.setMethod(json.method)
|
||||
.setPayload(ByteString.copyFrom(objectMapper.writeValueAsBytes(json.params)))
|
||||
.build()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
155
src/main/kotlin/io/emeraldpay/dshackle/proxy/WebsocketHandler.kt
Normal file
155
src/main/kotlin/io/emeraldpay/dshackle/proxy/WebsocketHandler.kt
Normal file
@@ -0,0 +1,155 @@
|
||||
/**
|
||||
* 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.proxy
|
||||
|
||||
import io.emeraldpay.dshackle.Global
|
||||
import io.emeraldpay.dshackle.config.ProxyConfig
|
||||
import io.emeraldpay.dshackle.monitoring.accesslog.AccessHandlerHttp
|
||||
import io.emeraldpay.dshackle.rpc.NativeCall
|
||||
import io.emeraldpay.dshackle.rpc.NativeSubscribe
|
||||
import io.emeraldpay.etherjar.rpc.json.RequestJson
|
||||
import io.emeraldpay.etherjar.rpc.json.ResponseJson
|
||||
import io.emeraldpay.grpc.Chain
|
||||
import io.netty.buffer.ByteBufInputStream
|
||||
import io.netty.buffer.Unpooled
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import org.reactivestreams.Publisher
|
||||
import org.slf4j.LoggerFactory
|
||||
import reactor.core.publisher.Flux
|
||||
import reactor.core.publisher.Mono
|
||||
import reactor.netty.http.websocket.WebsocketInbound
|
||||
import reactor.netty.http.websocket.WebsocketOutbound
|
||||
import java.util.concurrent.atomic.AtomicLong
|
||||
import java.util.function.BiFunction
|
||||
|
||||
/**
|
||||
* Responds to Websocket requests made to the Ethereum Proxy Server
|
||||
*/
|
||||
class WebsocketHandler(
|
||||
private val readRpcJson: ReadRpcJson,
|
||||
writeRpcJson: WriteRpcJson,
|
||||
nativeCall: NativeCall,
|
||||
private val nativeSubscribe: NativeSubscribe,
|
||||
requestMetrics: ProxyServer.RequestMetricsFactory,
|
||||
) : BaseHandler(writeRpcJson, nativeCall, requestMetrics) {
|
||||
|
||||
companion object {
|
||||
private val log = LoggerFactory.getLogger(WebsocketHandler::class.java)
|
||||
}
|
||||
|
||||
private val subscriptionId = AtomicLong(0)
|
||||
|
||||
fun nextSubscriptionId(): String {
|
||||
val n = subscriptionId.incrementAndGet()
|
||||
return StringUtils.leftPad(n.toString(16), 16, "0")
|
||||
}
|
||||
|
||||
fun proxy(routeConfig: ProxyConfig.Route): BiFunction<WebsocketInbound, WebsocketOutbound, Publisher<Void>> {
|
||||
return BiFunction { req, resp ->
|
||||
val requests: Flux<RequestJson<Any>> = req.aggregateFrames()
|
||||
.receiveFrames()
|
||||
.map { ByteBufInputStream(it.content()).readAllBytes() }
|
||||
.flatMap(this@WebsocketHandler::parseRequest)
|
||||
|
||||
val eventHandler: AccessHandlerHttp.RequestHandler = AccessHandlerHttp.NoOpHandler()
|
||||
val responses = respond(routeConfig.blockchain, requests, eventHandler)
|
||||
.map { Unpooled.wrappedBuffer(it.toByteArray()) }
|
||||
|
||||
resp.send(responses)
|
||||
.then()
|
||||
}
|
||||
}
|
||||
|
||||
fun parseRequest(data: ByteArray): Mono<RequestJson<Any>> {
|
||||
// try to parse JSON call. If received an invalid value just silently ignore it, that's what other Ethereum servers do
|
||||
try {
|
||||
val type = readRpcJson.getType(data)
|
||||
// WS is not supposed to have batches, so ignore them too
|
||||
if (type != ProxyCall.RpcType.SINGLE) {
|
||||
return Mono.empty()
|
||||
}
|
||||
val items = readRpcJson.extract(type, data)
|
||||
if (items.isEmpty()) {
|
||||
//empty should never happen for a SINGLE type of request, but anyway, just return nothing
|
||||
return Mono.empty()
|
||||
}
|
||||
return Mono
|
||||
.just(items.first())
|
||||
.map(readRpcJson.jsonExtractor)
|
||||
.onErrorResume { Mono.empty() }
|
||||
} catch (t: Throwable) {
|
||||
return Mono.empty()
|
||||
}
|
||||
}
|
||||
|
||||
fun respond(blockchain: Chain, requests: Flux<RequestJson<Any>>, eventHandler: AccessHandlerHttp.RequestHandler): Flux<String> {
|
||||
return requests.flatMap { call ->
|
||||
val method = call.method
|
||||
if (method == "eth_subscribe") {
|
||||
val methodParams = splitMethodParams(call.params)
|
||||
if (methodParams != null) {
|
||||
val subscriptionId = nextSubscriptionId()
|
||||
// first need to respond with ID of the subscription, and the following responses would have it in "subscription" param
|
||||
val start = ResponseJson<String, Any>().also {
|
||||
it.id = call.id
|
||||
it.result = subscriptionId
|
||||
}
|
||||
// produce actual responses
|
||||
val responses = nativeSubscribe
|
||||
.subscribe(blockchain, methodParams.first, methodParams.second)
|
||||
.map { event ->
|
||||
WsSubscriptionResponse(params = WsSubscriptionData(event, subscriptionId))
|
||||
}
|
||||
Flux.concat(Mono.just(start), responses)
|
||||
.map { Global.objectMapper.writeValueAsString(it) }
|
||||
} else {
|
||||
Mono.empty()
|
||||
}
|
||||
} else {
|
||||
val proxyCall = readRpcJson.convertToNativeCall(ProxyCall.RpcType.SINGLE, listOf(call))
|
||||
execute(blockchain, proxyCall, eventHandler)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun splitMethodParams(params: List<Any?>): Pair<String, Any?>? {
|
||||
if (params.isEmpty()) {
|
||||
return null
|
||||
}
|
||||
if (params.size == 1) {
|
||||
return Pair(params.first().toString(), null)
|
||||
}
|
||||
if (params.size == 2) {
|
||||
return Pair(params.first().toString(), params[1])
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// classes only to render WebSocket subscription response.
|
||||
// the difference with standard JSON RPC responses that it
|
||||
// (1) it doesn't have id on the top level, but rather as part of params,
|
||||
// and (2) it has the `method` field
|
||||
data class WsSubscriptionResponse(
|
||||
val jsonrpc: String = "2.0",
|
||||
val method: String = "eth_subscription",
|
||||
val params: WsSubscriptionData,
|
||||
)
|
||||
|
||||
data class WsSubscriptionData(
|
||||
val result: Any?,
|
||||
val subscription: String
|
||||
)
|
||||
}
|
||||
@@ -33,7 +33,7 @@ import reactor.core.publisher.Flux
|
||||
import reactor.core.publisher.Mono
|
||||
|
||||
@Service
|
||||
class NativeSubscribe(
|
||||
open class NativeSubscribe(
|
||||
@Autowired private val multistreamHolder: MultistreamHolder
|
||||
) {
|
||||
|
||||
@@ -81,7 +81,7 @@ class NativeSubscribe(
|
||||
}
|
||||
}
|
||||
|
||||
fun subscribe(chain: Chain, method: String, params: Any?): Flux<out Any> {
|
||||
open fun subscribe(chain: Chain, method: String, params: Any?): Flux<out Any> {
|
||||
val up = multistreamHolder.getUpstream(chain) ?: return Flux.error(SilentException.UnsupportedBlockchain(chain))
|
||||
return (up as EthereumMultistream)
|
||||
.getSubscribe()
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* 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.proxy
|
||||
|
||||
import io.emeraldpay.api.proto.BlockchainOuterClass
|
||||
import io.emeraldpay.dshackle.monitoring.accesslog.AccessHandlerHttp
|
||||
import io.emeraldpay.dshackle.rpc.NativeCall
|
||||
import io.emeraldpay.grpc.Chain
|
||||
import org.jetbrains.annotations.NotNull
|
||||
import reactor.core.publisher.Flux
|
||||
import reactor.core.publisher.Mono
|
||||
import spock.lang.Specification
|
||||
|
||||
import java.time.Duration
|
||||
|
||||
class BaseHandlerSpec extends Specification {
|
||||
|
||||
def requestHandler = new AccessHandlerHttp.NoOpHandler()
|
||||
|
||||
def "Return empty for empty single call"() {
|
||||
setup:
|
||||
def handler = new BaseHandlerImpl(new WriteRpcJson(), Stub(NativeCall), Stub(ProxyServer.RequestMetricsFactory))
|
||||
when:
|
||||
def act = Mono.from(handler.execute(Chain.ETHEREUM, new ProxyCall(ProxyCall.RpcType.SINGLE), requestHandler))
|
||||
.block(Duration.ofSeconds(1))
|
||||
then:
|
||||
act == ""
|
||||
}
|
||||
|
||||
def "Return empty array for empty batch call"() {
|
||||
setup:
|
||||
def handler = new BaseHandlerImpl(new WriteRpcJson(), Stub(NativeCall), Stub(ProxyServer.RequestMetricsFactory))
|
||||
when:
|
||||
def act = Mono.from(handler.execute(Chain.ETHEREUM, new ProxyCall(ProxyCall.RpcType.BATCH), requestHandler))
|
||||
.block(Duration.ofSeconds(1))
|
||||
then:
|
||||
act == "[]"
|
||||
}
|
||||
|
||||
def "Execute single call"() {
|
||||
setup:
|
||||
def nativeCall = Mock(NativeCall)
|
||||
def handler = new BaseHandlerImpl(new WriteRpcJson(), nativeCall, Stub(ProxyServer.RequestMetricsFactory))
|
||||
|
||||
def request = BlockchainOuterClass.NativeCallItem.newBuilder()
|
||||
.setMethod("eth_test")
|
||||
.setId(0)
|
||||
.build()
|
||||
def call = new ProxyCall(ProxyCall.RpcType.SINGLE)
|
||||
call.items.add(request)
|
||||
call.ids[0] = 5
|
||||
def response = new NativeCall.CallResult(0, '{"foo": 1}'.bytes, null)
|
||||
when:
|
||||
def act = Flux.from(handler.execute(Chain.ETHEREUM, call, requestHandler))
|
||||
.collectList()
|
||||
.block(Duration.ofSeconds(1))
|
||||
.join("")
|
||||
then:
|
||||
act == '{"jsonrpc":"2.0","id":5,"result":{"foo": 1}}'
|
||||
1 * nativeCall.nativeCallResult(_) >> Flux.fromIterable([response])
|
||||
}
|
||||
|
||||
def "Execute batch call with one item"() {
|
||||
setup:
|
||||
def nativeCall = Mock(NativeCall)
|
||||
def handler = new BaseHandlerImpl(new WriteRpcJson(), nativeCall, Stub(ProxyServer.RequestMetricsFactory))
|
||||
|
||||
def request = BlockchainOuterClass.NativeCallItem.newBuilder()
|
||||
.setMethod("eth_test")
|
||||
.setId(0)
|
||||
.build()
|
||||
def call = new ProxyCall(ProxyCall.RpcType.BATCH)
|
||||
call.items.add(request)
|
||||
call.ids[0] = 5
|
||||
def response = new NativeCall.CallResult(0, '{"foo": 1}'.bytes, null)
|
||||
when:
|
||||
def act = Flux.from(handler.execute(Chain.ETHEREUM, call, requestHandler))
|
||||
.collectList()
|
||||
.block(Duration.ofSeconds(1))
|
||||
.join("")
|
||||
then:
|
||||
act == '[{"jsonrpc":"2.0","id":5,"result":{"foo": 1}}]'
|
||||
1 * nativeCall.nativeCallResult(_) >> Flux.fromIterable([response])
|
||||
}
|
||||
|
||||
class BaseHandlerImpl extends BaseHandler {
|
||||
|
||||
BaseHandlerImpl(@NotNull WriteRpcJson writeRpcJson, @NotNull NativeCall nativeCall, @NotNull ProxyServer.RequestMetricsFactory requestMetrics) {
|
||||
super(writeRpcJson, nativeCall, requestMetrics)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2020 ETCDEV GmbH
|
||||
* Copyright (c) 2020 EmeraldPay, Inc
|
||||
* 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.
|
||||
@@ -19,11 +18,8 @@ package io.emeraldpay.dshackle.proxy
|
||||
import com.google.protobuf.ByteString
|
||||
import io.emeraldpay.api.proto.BlockchainOuterClass
|
||||
import io.emeraldpay.api.proto.Common
|
||||
import io.emeraldpay.dshackle.TlsSetup
|
||||
import io.emeraldpay.dshackle.config.ProxyConfig
|
||||
import io.emeraldpay.dshackle.monitoring.accesslog.AccessHandlerHttp
|
||||
import io.emeraldpay.dshackle.rpc.NativeCall
|
||||
import io.emeraldpay.dshackle.test.TestingCommons
|
||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
|
||||
import io.emeraldpay.etherjar.rpc.RpcException
|
||||
import io.emeraldpay.grpc.Chain
|
||||
@@ -35,64 +31,7 @@ import spock.lang.Specification
|
||||
import java.time.Duration
|
||||
import java.util.function.Function
|
||||
|
||||
class ProxyServerSpec extends Specification {
|
||||
|
||||
def "Uses NativeCall"() {
|
||||
setup:
|
||||
NativeCall nativeCall = Mock(NativeCall)
|
||||
def predefined = { a -> Flux.just("hello") } as Function
|
||||
|
||||
WriteRpcJson writeRpcJson = Mock {
|
||||
1 * toJsons(_) >> predefined
|
||||
}
|
||||
|
||||
ProxyServer server = new ProxyServer(
|
||||
new ProxyConfig(),
|
||||
new ReadRpcJson(),
|
||||
writeRpcJson,
|
||||
nativeCall,
|
||||
new TlsSetup(TestingCommons.fileResolver()),
|
||||
new AccessHandlerHttp.NoOpFactory()
|
||||
)
|
||||
|
||||
def call = new ProxyCall(ProxyCall.RpcType.SINGLE)
|
||||
call.ids[1] = 1
|
||||
call.items.add(
|
||||
BlockchainOuterClass.NativeCallItem.newBuilder()
|
||||
.setMethod("eth_hello")
|
||||
.build()
|
||||
)
|
||||
when:
|
||||
def act = server.execute(Chain.ETHEREUM, call, new AccessHandlerHttp.NoOpHandler())
|
||||
|
||||
then:
|
||||
1 * nativeCall.nativeCallResult(_) >> Flux.just(new NativeCall.CallResult(1, "".bytes, null))
|
||||
StepVerifier.create(act)
|
||||
.expectNext("hello")
|
||||
.expectComplete()
|
||||
.verify(Duration.ofSeconds(1))
|
||||
}
|
||||
|
||||
def "Return error on invalid request"() {
|
||||
setup:
|
||||
ReadRpcJson read = Mock(ReadRpcJson) {
|
||||
1 * apply(_) >> { throw new RpcException(-32123, "test", new JsonRpcResponse.NumberId(4)) }
|
||||
}
|
||||
def server = new ProxyServer(
|
||||
Stub(ProxyConfig),
|
||||
read,
|
||||
Stub(WriteRpcJson), Stub(NativeCall), Stub(TlsSetup),
|
||||
new AccessHandlerHttp.NoOpFactory()
|
||||
)
|
||||
when:
|
||||
def act = server.processRequest(Chain.ETHEREUM, Mono.just("".bytes), new AccessHandlerHttp.NoOpHandler())
|
||||
.map { new String(it.array()) }
|
||||
then:
|
||||
StepVerifier.create(act)
|
||||
.expectNext('{"jsonrpc":"2.0","id":4,"error":{"code":-32123,"message":"test"}}')
|
||||
.expectComplete()
|
||||
.verify(Duration.ofSeconds(1))
|
||||
}
|
||||
class HttpHandlerSpec extends Specification {
|
||||
|
||||
def "Calls access log handler"() {
|
||||
setup:
|
||||
@@ -107,30 +46,78 @@ class ProxyServerSpec extends Specification {
|
||||
.addItems(reqItem)
|
||||
.build()
|
||||
|
||||
|
||||
ReadRpcJson read = Mock(ReadRpcJson) {
|
||||
1 * apply(_) >> new ProxyCall(ProxyCall.RpcType.SINGLE).tap { it.items.add(reqItem) }
|
||||
}
|
||||
NativeCall nativeCall = Mock(NativeCall) {
|
||||
1 * nativeCallResult(_) >> Flux.fromIterable([respItem])
|
||||
}
|
||||
def handler = Mock(AccessHandlerHttp.RequestHandler.class)
|
||||
|
||||
def server = new ProxyServer(
|
||||
Stub(ProxyConfig),
|
||||
read,
|
||||
new WriteRpcJson(),
|
||||
nativeCall,
|
||||
Stub(TlsSetup),
|
||||
new AccessHandlerHttp.NoOpFactory()
|
||||
def accessHandler = Mock(AccessHandlerHttp.RequestHandler)
|
||||
def accessHandlerFactory = Mock(AccessHandlerHttp.HandlerFactory) {
|
||||
_ * it.create(_,) >> accessHandler
|
||||
}
|
||||
def handler = new HttpHandler(
|
||||
new ReadRpcJson(), new WriteRpcJson(),
|
||||
nativeCall, accessHandlerFactory, Stub(ProxyServer.RequestMetricsFactory)
|
||||
)
|
||||
|
||||
when:
|
||||
server.processRequest(Chain.ETHEREUM, Mono.just("".bytes), handler)
|
||||
handler.execute(Chain.ETHEREUM, [reqItem], accessHandler)
|
||||
.blockLast()
|
||||
|
||||
then:
|
||||
1 * handler.onRequest(req)
|
||||
1 * handler.onResponse(respItem)
|
||||
1 * accessHandler.onRequest(req)
|
||||
1 * accessHandler.onResponse(respItem)
|
||||
}
|
||||
|
||||
def "Return error on invalid request"() {
|
||||
setup:
|
||||
ReadRpcJson read = Mock(ReadRpcJson) {
|
||||
1 * apply(_) >> { throw new RpcException(-32123, "test", new JsonRpcResponse.NumberId(4)) }
|
||||
}
|
||||
|
||||
def handler = new HttpHandler(
|
||||
read, new WriteRpcJson(),
|
||||
Stub(NativeCall), Stub(AccessHandlerHttp.HandlerFactory), Stub(ProxyServer.RequestMetricsFactory)
|
||||
)
|
||||
when:
|
||||
|
||||
def act = handler.processRequest(Chain.ETHEREUM, Mono.just("".bytes), new AccessHandlerHttp.NoOpHandler())
|
||||
.map { new String(it.array()) }
|
||||
then:
|
||||
StepVerifier.create(act)
|
||||
.expectNext('{"jsonrpc":"2.0","id":4,"error":{"code":-32123,"message":"test"}}')
|
||||
.expectComplete()
|
||||
.verify(Duration.ofSeconds(1))
|
||||
}
|
||||
|
||||
def "Uses NativeCall"() {
|
||||
setup:
|
||||
NativeCall nativeCall = Mock(NativeCall)
|
||||
def predefined = { a -> Flux.just("hello") } as Function
|
||||
|
||||
WriteRpcJson writeRpcJson = Mock {
|
||||
1 * toJsons(_) >> predefined
|
||||
}
|
||||
|
||||
|
||||
def handler = new HttpHandler(
|
||||
new ReadRpcJson(), writeRpcJson,
|
||||
nativeCall, Stub(AccessHandlerHttp.HandlerFactory), Stub(ProxyServer.RequestMetricsFactory)
|
||||
)
|
||||
|
||||
def call = new ProxyCall(ProxyCall.RpcType.SINGLE)
|
||||
call.ids[1] = 1
|
||||
call.items.add(
|
||||
BlockchainOuterClass.NativeCallItem.newBuilder()
|
||||
.setMethod("eth_hello")
|
||||
.build()
|
||||
)
|
||||
when:
|
||||
def act = handler.execute(Chain.ETHEREUM, call, new AccessHandlerHttp.NoOpHandler())
|
||||
|
||||
then:
|
||||
1 * nativeCall.nativeCallResult(_) >> Flux.just(new NativeCall.CallResult(1, "".bytes, null))
|
||||
StepVerifier.create(act)
|
||||
.expectNext("hello")
|
||||
.expectComplete()
|
||||
.verify(Duration.ofSeconds(1))
|
||||
}
|
||||
}
|
||||
@@ -224,4 +224,12 @@ class ReadRpcJsonSpec extends Specification {
|
||||
t.rpcMessage.toLowerCase() == "params must be an array"
|
||||
t.details == new JsonRpcResponse.NumberId(2)
|
||||
}
|
||||
|
||||
def "Error if json is broken"() {
|
||||
when:
|
||||
reader.apply('{"id":2, "method":"net_peerCount", "params"'.bytes)
|
||||
then:
|
||||
def t = thrown(RpcException)
|
||||
t.code == -32700
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
/**
|
||||
* 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.proxy
|
||||
|
||||
import io.emeraldpay.dshackle.monitoring.accesslog.AccessHandlerHttp
|
||||
import io.emeraldpay.dshackle.rpc.NativeCall
|
||||
import io.emeraldpay.dshackle.rpc.NativeSubscribe
|
||||
import io.emeraldpay.etherjar.rpc.json.RequestJson
|
||||
import io.emeraldpay.grpc.Chain
|
||||
import reactor.core.publisher.Flux
|
||||
import spock.lang.Specification
|
||||
|
||||
import java.time.Duration
|
||||
|
||||
class WebsocketHandlerSpec extends Specification {
|
||||
|
||||
def requestHandler = new AccessHandlerHttp.NoOpHandler()
|
||||
|
||||
def "Parse standard RPC request"() {
|
||||
setup:
|
||||
def handler = new WebsocketHandler(
|
||||
new ReadRpcJson(), Stub(WriteRpcJson), Stub(NativeCall), Stub(NativeSubscribe), Stub(ProxyServer.RequestMetricsFactory)
|
||||
)
|
||||
when:
|
||||
def act = handler.parseRequest('{"id": 5, "jsonrpc": "2.0", "method": "eth_getBlockByNumber", "params": ["0x100001", false]}'.bytes)
|
||||
.block(Duration.ofSeconds(1))
|
||||
|
||||
then:
|
||||
act.id == 5
|
||||
act.method == "eth_getBlockByNumber"
|
||||
act.params == ["0x100001", false]
|
||||
}
|
||||
|
||||
def "Parse to empty an invalid request"() {
|
||||
setup:
|
||||
def handler = new WebsocketHandler(
|
||||
new ReadRpcJson(), Stub(WriteRpcJson), Stub(NativeCall), Stub(NativeSubscribe), Stub(ProxyServer.RequestMetricsFactory)
|
||||
)
|
||||
when:
|
||||
def act = handler.parseRequest('hello world'.bytes)
|
||||
.block(Duration.ofSeconds(1))
|
||||
|
||||
then:
|
||||
act == null
|
||||
}
|
||||
|
||||
def "Parse to empty a batch request"() {
|
||||
setup:
|
||||
def req1 = '{"id": 5, "jsonrpc": "2.0", "method": "eth_getBlockByNumber", "params": ["0x100001", false]}'
|
||||
def handler = new WebsocketHandler(
|
||||
new ReadRpcJson(), Stub(WriteRpcJson), Stub(NativeCall), Stub(NativeSubscribe), Stub(ProxyServer.RequestMetricsFactory)
|
||||
)
|
||||
when:
|
||||
def act = handler.parseRequest("[$req1]".bytes)
|
||||
.block(Duration.ofSeconds(1))
|
||||
|
||||
then:
|
||||
act == null
|
||||
}
|
||||
|
||||
def "Respond to a single call"() {
|
||||
setup:
|
||||
def response = new NativeCall.CallResult(0, '{"foo": 1}'.bytes, null)
|
||||
|
||||
def nativeCall = Mock(NativeCall) {
|
||||
1 * it.nativeCallResult(_) >> Flux.fromIterable([response])
|
||||
}
|
||||
def handler = new WebsocketHandler(
|
||||
new ReadRpcJson(), new WriteRpcJson(), nativeCall, Stub(NativeSubscribe), Stub(ProxyServer.RequestMetricsFactory)
|
||||
)
|
||||
|
||||
def request = new RequestJson("foo_test", [], 2)
|
||||
when:
|
||||
def act = handler.respond(Chain.ETHEREUM, Flux.just(request), requestHandler)
|
||||
.single()
|
||||
.block(Duration.ofSeconds(1))
|
||||
then:
|
||||
act == '{"jsonrpc":"2.0","id":2,"result":{"foo": 1}}'
|
||||
}
|
||||
|
||||
def "Respond to a subscription call"() {
|
||||
setup:
|
||||
def response1 = [foo: 1]
|
||||
def response2 = [foo: 2]
|
||||
|
||||
def nativeSubscribe = Mock(NativeSubscribe) {
|
||||
1 * it.subscribe(Chain.ETHEREUM, "foo_test", null) >> Flux.fromIterable([response1, response2])
|
||||
}
|
||||
def handler = new WebsocketHandler(
|
||||
new ReadRpcJson(), new WriteRpcJson(), Stub(NativeCall), nativeSubscribe, Stub(ProxyServer.RequestMetricsFactory)
|
||||
)
|
||||
|
||||
def request = new RequestJson("eth_subscribe", ["foo_test"], 2)
|
||||
when:
|
||||
def act = handler.respond(Chain.ETHEREUM, Flux.just(request), requestHandler)
|
||||
.collectList()
|
||||
.block(Duration.ofSeconds(1))
|
||||
then:
|
||||
act[0] == '{"jsonrpc":"2.0","id":2,"result":"0000000000000001"}'
|
||||
act[1] == '{"jsonrpc":"2.0","method":"eth_subscription","params":{"result":{"foo":1},"subscription":"0000000000000001"}}'
|
||||
act[2] == '{"jsonrpc":"2.0","method":"eth_subscription","params":{"result":{"foo":2},"subscription":"0000000000000001"}}'
|
||||
act.size() == 3
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user