solution: refactor upstreams to make them blockchain specific
This commit is contained in:
41
src/main/kotlin/io/emeraldpay/dshackle/BlockchainType.kt
Normal file
41
src/main/kotlin/io/emeraldpay/dshackle/BlockchainType.kt
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2020 ETCDEV GmbH
|
||||||
|
*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
import io.emeraldpay.grpc.Chain
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type of the blockchain, architecture-wise
|
||||||
|
*/
|
||||||
|
enum class BlockchainType {
|
||||||
|
|
||||||
|
ETHEREUM,
|
||||||
|
BITCOIN,
|
||||||
|
OTHER;
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@JvmStatic
|
||||||
|
fun fromBlockchain(blockchain: Chain): BlockchainType {
|
||||||
|
if (blockchain == Chain.ETHEREUM || blockchain == Chain.ETHEREUM_CLASSIC || blockchain == Chain.TESTNET_KOVAN || blockchain == Chain.TESTNET_MORDEN) {
|
||||||
|
return ETHEREUM
|
||||||
|
}
|
||||||
|
if (blockchain == Chain.BITCOIN || blockchain == Chain.TESTNET_BITCOIN) {
|
||||||
|
return BITCOIN
|
||||||
|
}
|
||||||
|
return OTHER
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@
|
|||||||
package io.emeraldpay.dshackle.config
|
package io.emeraldpay.dshackle.config
|
||||||
|
|
||||||
import io.emeraldpay.dshackle.Defaults
|
import io.emeraldpay.dshackle.Defaults
|
||||||
|
import java.lang.ClassCastException
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.collections.ArrayList
|
import kotlin.collections.ArrayList
|
||||||
@@ -72,6 +73,14 @@ class UpstreamsConfig {
|
|||||||
var connection: T? = null
|
var connection: T? = null
|
||||||
val labels = Labels()
|
val labels = Labels()
|
||||||
var methods: Methods? = null
|
var methods: Methods? = null
|
||||||
|
|
||||||
|
@Suppress("unchecked")
|
||||||
|
fun <Z : UpstreamConnection> cast(type: Class<Z>): Upstream<Z> {
|
||||||
|
if (connection == null || type.isAssignableFrom(connection!!.javaClass)) {
|
||||||
|
return this as Upstream<Z>
|
||||||
|
}
|
||||||
|
throw ClassCastException("Cannot cast ${connection?.javaClass} to $type")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
open class UpstreamConnection
|
open class UpstreamConnection
|
||||||
|
|||||||
@@ -34,13 +34,13 @@ open class AlwaysQuorum: CallQuorum {
|
|||||||
return resolved
|
return resolved
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun record(response: ByteArray, upstream: Upstream): Boolean {
|
override fun record(response: ByteArray, upstream: Upstream<*, *>): Boolean {
|
||||||
result = response
|
result = response
|
||||||
resolved = true
|
resolved = true
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun record(error: RpcException, upstream: Upstream) {
|
override fun record(error: RpcException, upstream: Upstream<*, *>) {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getResult(): ByteArray? {
|
override fun getResult(): ByteArray? {
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ open class BroadcastQuorum(
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun recordValue(response: ByteArray, responseValue: String?, upstream: Upstream) {
|
override fun recordValue(response: ByteArray, responseValue: String?, upstream: Upstream<*, *>) {
|
||||||
calls++
|
calls++
|
||||||
if (txid == null && responseValue != null) {
|
if (txid == null && responseValue != null) {
|
||||||
txid = responseValue
|
txid = responseValue
|
||||||
@@ -50,7 +50,7 @@ open class BroadcastQuorum(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun recordError(response: ByteArray?, errorMessage: String?, upstream: Upstream) {
|
override fun recordError(response: ByteArray?, errorMessage: String?, upstream: Upstream<*, *>) {
|
||||||
// can be "message: known transaction: TXID", "Transaction with the same hash was already imported" or "message: Nonce too low"
|
// can be "message: known transaction: TXID", "Transaction with the same hash was already imported" or "message: Nonce too low"
|
||||||
calls++
|
calls++
|
||||||
if (result == null) {
|
if (result == null) {
|
||||||
|
|||||||
@@ -30,8 +30,8 @@ interface CallQuorum {
|
|||||||
fun init(head: Head<BlockJson<TransactionRefJson>>)
|
fun init(head: Head<BlockJson<TransactionRefJson>>)
|
||||||
|
|
||||||
fun isResolved(): Boolean
|
fun isResolved(): Boolean
|
||||||
fun record(response: ByteArray, upstream: Upstream): Boolean
|
fun record(response: ByteArray, upstream: Upstream<*, *>): Boolean
|
||||||
fun record(error: RpcException, upstream: Upstream)
|
fun record(error: RpcException, upstream: Upstream<*, *>)
|
||||||
fun getResult(): ByteArray?
|
fun getResult(): ByteArray?
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -41,8 +41,8 @@ interface CallQuorum {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun asReducer(): BiFunction<CallQuorum, Tuple2<ByteArray, Upstream>, CallQuorum> {
|
fun asReducer(): BiFunction<CallQuorum, Tuple2<ByteArray, Upstream<*, *>>, CallQuorum> {
|
||||||
return BiFunction<CallQuorum, Tuple2<ByteArray, Upstream>, CallQuorum> { a, b ->
|
return BiFunction<CallQuorum, Tuple2<ByteArray, Upstream<*, *>>, CallQuorum> { a, b ->
|
||||||
a.record(b.t1, b.t2)
|
a.record(b.t1, b.t2)
|
||||||
return@BiFunction a
|
return@BiFunction a
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ open class NonEmptyQuorum(
|
|||||||
return result != null || tries >= maxTries
|
return result != null || tries >= maxTries
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun recordValue(response: ByteArray, responseValue: Any?, upstream: Upstream) {
|
override fun recordValue(response: ByteArray, responseValue: Any?, upstream: Upstream<*, *>) {
|
||||||
tries++
|
tries++
|
||||||
if (responseValue != null) {
|
if (responseValue != null) {
|
||||||
result = response
|
result = response
|
||||||
@@ -49,10 +49,10 @@ open class NonEmptyQuorum(
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun recordError(response: ByteArray?, errorMessage: String?, upstream: Upstream) {
|
override fun recordError(response: ByteArray?, errorMessage: String?, upstream: Upstream<*, *>) {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun record(error: RpcException, upstream: Upstream) {
|
override fun record(error: RpcException, upstream: Upstream<*, *>) {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -46,7 +46,7 @@ open class NonceQuorum(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun recordValue(response: ByteArray, responseValue: String?, upstream: Upstream) {
|
override fun recordValue(response: ByteArray, responseValue: String?, upstream: Upstream<*, *>) {
|
||||||
val value = responseValue?.let { str ->
|
val value = responseValue?.let { str ->
|
||||||
HexQuantity.from(str).value.toLong()
|
HexQuantity.from(str).value.toLong()
|
||||||
}
|
}
|
||||||
@@ -65,11 +65,11 @@ open class NonceQuorum(
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun recordError(response: ByteArray?, errorMessage: String?, upstream: Upstream) {
|
override fun recordError(response: ByteArray?, errorMessage: String?, upstream: Upstream<*, *>) {
|
||||||
errors++
|
errors++
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun record(error: RpcException, upstream: Upstream) {
|
override fun record(error: RpcException, upstream: Upstream<*, *>) {
|
||||||
errors++
|
errors++
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ class NotLaggingQuorum(val maxLag: Long = 0): CallQuorum {
|
|||||||
return result.get() != null
|
return result.get() != null
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun record(response: ByteArray, upstream: Upstream): Boolean {
|
override fun record(response: ByteArray, upstream: Upstream<*, *>): Boolean {
|
||||||
val lagging = upstream.getLag() > maxLag
|
val lagging = upstream.getLag() > maxLag
|
||||||
if (!lagging) {
|
if (!lagging) {
|
||||||
result.set(response)
|
result.set(response)
|
||||||
@@ -43,7 +43,7 @@ class NotLaggingQuorum(val maxLag: Long = 0): CallQuorum {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun record(error: RpcException, upstream: Upstream) {
|
override fun record(error: RpcException, upstream: Upstream<*, *>) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ abstract class ValueAwareQuorum<T>(
|
|||||||
return jacksonRpcConverter.fromJson(response.inputStream(), clazz)
|
return jacksonRpcConverter.fromJson(response.inputStream(), clazz)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun record(response: ByteArray, upstream: Upstream): Boolean {
|
override fun record(response: ByteArray, upstream: Upstream<*, *>): Boolean {
|
||||||
try {
|
try {
|
||||||
val value = extractValue(response, clazz)
|
val value = extractValue(response, clazz)
|
||||||
recordValue(response, value, upstream)
|
recordValue(response, value, upstream)
|
||||||
@@ -43,12 +43,12 @@ abstract class ValueAwareQuorum<T>(
|
|||||||
return isResolved();
|
return isResolved();
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun record(error: RpcException, upstream: Upstream) {
|
override fun record(error: RpcException, upstream: Upstream<*, *>) {
|
||||||
recordError(null, error.rpcMessage, upstream)
|
recordError(null, error.rpcMessage, 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<*, *>)
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2019 ETCDEV GmbH
|
|
||||||
*
|
|
||||||
* 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.reader
|
|
||||||
|
|
||||||
import io.emeraldpay.dshackle.upstream.Selector
|
|
||||||
import io.emeraldpay.dshackle.upstream.Upstream
|
|
||||||
import io.infinitape.etherjar.domain.BlockHash
|
|
||||||
import io.infinitape.etherjar.domain.TransactionId
|
|
||||||
import io.infinitape.etherjar.rpc.Commands
|
|
||||||
import io.infinitape.etherjar.rpc.json.BlockJson
|
|
||||||
import io.infinitape.etherjar.rpc.json.TransactionRefJson
|
|
||||||
import reactor.core.publisher.Mono
|
|
||||||
import reactor.retry.Repeat
|
|
||||||
import java.time.Duration
|
|
||||||
|
|
||||||
class BlockApiReader(
|
|
||||||
val upstream: Upstream
|
|
||||||
): Reader<BlockHash, BlockJson<TransactionRefJson>> {
|
|
||||||
|
|
||||||
override fun read(key: BlockHash): Mono<BlockJson<TransactionRefJson>> {
|
|
||||||
return Mono.just(key)
|
|
||||||
.flatMap {
|
|
||||||
upstream.getApi(Selector.empty)
|
|
||||||
.flatMap { api -> api.executeAndConvert(Commands.eth().getBlock(it)) }
|
|
||||||
}.repeatWhenEmpty { n ->
|
|
||||||
Repeat.times<Any>(3)
|
|
||||||
.exponentialBackoff(Duration.ofMillis(100), Duration.ofMillis(500))
|
|
||||||
.apply(n)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -18,7 +18,8 @@ package io.emeraldpay.dshackle.rpc
|
|||||||
import io.emeraldpay.api.proto.BlockchainOuterClass
|
import io.emeraldpay.api.proto.BlockchainOuterClass
|
||||||
import io.emeraldpay.api.proto.Common
|
import io.emeraldpay.api.proto.Common
|
||||||
import io.emeraldpay.api.proto.ReactorBlockchainGrpc
|
import io.emeraldpay.api.proto.ReactorBlockchainGrpc
|
||||||
import io.grpc.stub.StreamObserver
|
import io.emeraldpay.dshackle.BlockchainType
|
||||||
|
import io.emeraldpay.grpc.Chain
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import org.springframework.beans.factory.annotation.Autowired
|
import org.springframework.beans.factory.annotation.Autowired
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
@@ -29,8 +30,8 @@ import reactor.core.publisher.Mono
|
|||||||
class BlockchainRpc(
|
class BlockchainRpc(
|
||||||
@Autowired private val nativeCall: NativeCall,
|
@Autowired private val nativeCall: NativeCall,
|
||||||
@Autowired private val streamHead: StreamHead,
|
@Autowired private val streamHead: StreamHead,
|
||||||
@Autowired private val trackTx: TrackTx,
|
@Autowired private val trackEthereumTx: TrackEthereumTx,
|
||||||
@Autowired private val trackAddress: TrackAddress,
|
@Autowired private val trackEthereumAddress: TrackEthereumAddress,
|
||||||
@Autowired private val describe: Describe,
|
@Autowired private val describe: Describe,
|
||||||
@Autowired private val subscribeStatus: SubscribeStatus
|
@Autowired private val subscribeStatus: SubscribeStatus
|
||||||
): ReactorBlockchainGrpc.BlockchainImplBase() {
|
): ReactorBlockchainGrpc.BlockchainImplBase() {
|
||||||
@@ -46,15 +47,15 @@ class BlockchainRpc(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun subscribeTxStatus(request: Mono<BlockchainOuterClass.TxStatusRequest>): Flux<BlockchainOuterClass.TxStatus> {
|
override fun subscribeTxStatus(request: Mono<BlockchainOuterClass.TxStatusRequest>): Flux<BlockchainOuterClass.TxStatus> {
|
||||||
return trackTx.add(request)
|
return trackEthereumTx.add(request)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun subscribeBalance(request: Mono<BlockchainOuterClass.BalanceRequest>): Flux<BlockchainOuterClass.AddressBalance> {
|
override fun subscribeBalance(request: Mono<BlockchainOuterClass.BalanceRequest>): Flux<BlockchainOuterClass.AddressBalance> {
|
||||||
return trackAddress.subscribe(request)
|
return trackEthereumAddress.subscribe(request)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getBalance(request: Mono<BlockchainOuterClass.BalanceRequest>): Flux<BlockchainOuterClass.AddressBalance> {
|
override fun getBalance(request: Mono<BlockchainOuterClass.BalanceRequest>): Flux<BlockchainOuterClass.AddressBalance> {
|
||||||
return trackAddress.getBalance(request)
|
return trackEthereumAddress.getBalance(request)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun describe(request: Mono<BlockchainOuterClass.DescribeRequest>): Mono<BlockchainOuterClass.DescribeResponse> {
|
override fun describe(request: Mono<BlockchainOuterClass.DescribeRequest>): Mono<BlockchainOuterClass.DescribeResponse> {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import io.emeraldpay.api.proto.Common
|
|||||||
import io.emeraldpay.dshackle.startup.QuorumForLabels
|
import io.emeraldpay.dshackle.startup.QuorumForLabels
|
||||||
import io.emeraldpay.dshackle.upstream.*
|
import io.emeraldpay.dshackle.upstream.*
|
||||||
import io.emeraldpay.dshackle.upstream.ethereum.EthereumUpstream
|
import io.emeraldpay.dshackle.upstream.ethereum.EthereumUpstream
|
||||||
import io.emeraldpay.dshackle.upstream.grpc.GrpcUpstream
|
import io.emeraldpay.dshackle.upstream.grpc.EthereumGrpcUpstream
|
||||||
import org.springframework.beans.factory.annotation.Autowired
|
import org.springframework.beans.factory.annotation.Autowired
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
import reactor.core.publisher.Mono
|
import reactor.core.publisher.Mono
|
||||||
@@ -47,7 +47,7 @@ class Describe(
|
|||||||
val nodes = QuorumForLabels()
|
val nodes = QuorumForLabels()
|
||||||
if (up is EthereumUpstream) {
|
if (up is EthereumUpstream) {
|
||||||
nodes.add(up.node)
|
nodes.add(up.node)
|
||||||
} else if (up is GrpcUpstream) {
|
} else if (up is EthereumGrpcUpstream) {
|
||||||
nodes.add(up.getNodes())
|
nodes.add(up.getNodes())
|
||||||
}
|
}
|
||||||
nodes.getAll().forEach { node ->
|
nodes.getAll().forEach { node ->
|
||||||
|
|||||||
@@ -18,12 +18,16 @@ package io.emeraldpay.dshackle.rpc
|
|||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
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.BlockchainType
|
||||||
import io.emeraldpay.dshackle.SilentException
|
import io.emeraldpay.dshackle.SilentException
|
||||||
import io.emeraldpay.dshackle.upstream.*
|
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.upstream.ethereum.EthereumApi
|
||||||
import io.emeraldpay.grpc.Chain
|
import io.emeraldpay.grpc.Chain
|
||||||
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.TransactionRefJson
|
||||||
import org.apache.commons.lang3.StringUtils
|
import org.apache.commons.lang3.StringUtils
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import org.springframework.beans.factory.annotation.Autowired
|
import org.springframework.beans.factory.annotation.Autowired
|
||||||
@@ -84,13 +88,17 @@ open class NativeCall(
|
|||||||
if (chain == Chain.UNSPECIFIED) {
|
if (chain == Chain.UNSPECIFIED) {
|
||||||
return Flux.error(CallFailure(0, SilentException.UnsupportedBlockchain(request.chain.number)))
|
return Flux.error(CallFailure(0, SilentException.UnsupportedBlockchain(request.chain.number)))
|
||||||
}
|
}
|
||||||
|
if (BlockchainType.fromBlockchain(chain) != BlockchainType.ETHEREUM) {
|
||||||
|
return Flux.error(CallFailure(0, SilentException.UnsupportedBlockchain(request.chain.number)))
|
||||||
|
}
|
||||||
|
|
||||||
val upstream = upstreams.getUpstream(chain)
|
val upstream = upstreams.getUpstream(chain)
|
||||||
?: return Flux.error(CallFailure(0, SilentException.UnsupportedBlockchain(chain)))
|
?: return Flux.error(CallFailure(0, SilentException.UnsupportedBlockchain(chain)))
|
||||||
|
|
||||||
return prepareCall(request, upstream)
|
return prepareCall(request, upstream as AggregatedUpstream<EthereumApi, BlockJson<TransactionRefJson>>)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun prepareCall(request: BlockchainOuterClass.NativeCallRequest, upstream: AggregatedUpstream): Flux<CallContext<RawCallDetails>> {
|
fun prepareCall(request: BlockchainOuterClass.NativeCallRequest, upstream: AggregatedUpstream<EthereumApi, BlockJson<TransactionRefJson>>): Flux<CallContext<RawCallDetails>> {
|
||||||
return request.itemsList.toFlux().map {
|
return request.itemsList.toFlux().map {
|
||||||
val method = it.method
|
val method = it.method
|
||||||
val params = it.payload.toStringUtf8()
|
val params = it.payload.toStringUtf8()
|
||||||
@@ -197,7 +205,7 @@ open class NativeCall(
|
|||||||
}
|
}
|
||||||
|
|
||||||
open class CallContext<T>(val id: Int,
|
open class CallContext<T>(val id: Int,
|
||||||
val upstream: AggregatedUpstream,
|
val upstream: AggregatedUpstream<EthereumApi, BlockJson<TransactionRefJson>>,
|
||||||
val matcher: Selector.Matcher,
|
val matcher: Selector.Matcher,
|
||||||
val callQuorum: CallQuorum,
|
val callQuorum: CallQuorum,
|
||||||
val payload: T) {
|
val payload: T) {
|
||||||
@@ -205,7 +213,7 @@ open class NativeCall(
|
|||||||
return CallContext(id, upstream, matcher, callQuorum, payload)
|
return CallContext(id, upstream, matcher, callQuorum, payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getApis(): ApiSource {
|
fun getApis(): ApiSource<EthereumApi> {
|
||||||
return upstream.getApis(matcher)
|
return upstream.getApis(matcher)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,9 +18,9 @@ package io.emeraldpay.dshackle.rpc
|
|||||||
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.api.proto.Common
|
import io.emeraldpay.api.proto.Common
|
||||||
|
import io.emeraldpay.dshackle.BlockchainType
|
||||||
import io.emeraldpay.dshackle.upstream.Upstreams
|
import io.emeraldpay.dshackle.upstream.Upstreams
|
||||||
import io.emeraldpay.grpc.Chain
|
import io.emeraldpay.grpc.Chain
|
||||||
import io.infinitape.etherjar.domain.TransactionId
|
|
||||||
import io.infinitape.etherjar.rpc.json.BlockJson
|
import io.infinitape.etherjar.rpc.json.BlockJson
|
||||||
import io.infinitape.etherjar.rpc.json.TransactionRefJson
|
import io.infinitape.etherjar.rpc.json.TransactionRefJson
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
@@ -44,11 +44,25 @@ class StreamHead(
|
|||||||
?: return@flatMapMany Flux.error<BlockchainOuterClass.ChainHead>(Exception("Unavailable chain: $chain"))
|
?: return@flatMapMany Flux.error<BlockchainOuterClass.ChainHead>(Exception("Unavailable chain: $chain"))
|
||||||
up.getHead()
|
up.getHead()
|
||||||
.getFlux()
|
.getFlux()
|
||||||
.map { asProto(chain, it) }
|
.map { asProto(chain, it!!) }
|
||||||
|
.onErrorContinue { t, _ ->
|
||||||
|
log.warn("Head error: ${t.message}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun asProto(chain: Chain, block: BlockJson<TransactionRefJson>): BlockchainOuterClass.ChainHead {
|
fun asProto(chain: Chain, block: Any): BlockchainOuterClass.ChainHead {
|
||||||
|
if (BlockchainType.fromBlockchain(chain) == BlockchainType.ETHEREUM) {
|
||||||
|
if (BlockJson::class.java.isAssignableFrom(block.javaClass)) {
|
||||||
|
return asEthereumProto(chain, block as BlockJson<TransactionRefJson>)
|
||||||
|
} else {
|
||||||
|
throw IllegalArgumentException("Invalid block type: ${block.javaClass}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw IllegalArgumentException("Unsupported blockchain ${chain}")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun asEthereumProto(chain: Chain, block: BlockJson<TransactionRefJson>): BlockchainOuterClass.ChainHead {
|
||||||
return BlockchainOuterClass.ChainHead.newBuilder()
|
return BlockchainOuterClass.ChainHead.newBuilder()
|
||||||
.setChainValue(chain.id)
|
.setChainValue(chain.id)
|
||||||
.setHeight(block.number)
|
.setHeight(block.number)
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ class SubscribeStatus(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun chainStatus(chain: Chain, ups: List<Upstream>): BlockchainOuterClass.ChainStatus {
|
fun chainStatus(chain: Chain, ups: List<Upstream<*, *>>): BlockchainOuterClass.ChainStatus {
|
||||||
val available = ups.map { u ->
|
val available = ups.map { u ->
|
||||||
u.getStatus()
|
u.getStatus()
|
||||||
}.min() ?: UpstreamAvailability.UNAVAILABLE
|
}.min() ?: UpstreamAvailability.UNAVAILABLE
|
||||||
@@ -59,6 +59,6 @@ class SubscribeStatus(
|
|||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
class ChainSubscription(val chain: Chain, val up: AggregatedUpstream, val avail: UpstreamAvailability)
|
class ChainSubscription(val chain: Chain, val up: AggregatedUpstream<*, *>, val avail: UpstreamAvailability)
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -17,15 +17,20 @@ package io.emeraldpay.dshackle.rpc
|
|||||||
|
|
||||||
import io.emeraldpay.api.proto.BlockchainOuterClass
|
import io.emeraldpay.api.proto.BlockchainOuterClass
|
||||||
import io.emeraldpay.api.proto.Common
|
import io.emeraldpay.api.proto.Common
|
||||||
|
import io.emeraldpay.dshackle.BlockchainType
|
||||||
import io.emeraldpay.dshackle.Defaults
|
import io.emeraldpay.dshackle.Defaults
|
||||||
import io.emeraldpay.dshackle.SilentException
|
import io.emeraldpay.dshackle.SilentException
|
||||||
|
import io.emeraldpay.dshackle.upstream.AggregatedUpstream
|
||||||
import io.emeraldpay.dshackle.upstream.Selector
|
import io.emeraldpay.dshackle.upstream.Selector
|
||||||
import io.emeraldpay.dshackle.upstream.Upstreams
|
import io.emeraldpay.dshackle.upstream.Upstreams
|
||||||
|
import io.emeraldpay.dshackle.upstream.ethereum.EthereumApi
|
||||||
import io.emeraldpay.grpc.Chain
|
import io.emeraldpay.grpc.Chain
|
||||||
import io.infinitape.etherjar.domain.Address
|
import io.infinitape.etherjar.domain.Address
|
||||||
import io.infinitape.etherjar.domain.Wei
|
import io.infinitape.etherjar.domain.Wei
|
||||||
import io.infinitape.etherjar.rpc.Commands
|
import io.infinitape.etherjar.rpc.Commands
|
||||||
|
import io.infinitape.etherjar.rpc.json.BlockJson
|
||||||
import io.infinitape.etherjar.rpc.json.BlockTag
|
import io.infinitape.etherjar.rpc.json.BlockTag
|
||||||
|
import io.infinitape.etherjar.rpc.json.TransactionRefJson
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import org.springframework.beans.factory.annotation.Autowired
|
import org.springframework.beans.factory.annotation.Autowired
|
||||||
import org.springframework.scheduling.annotation.Scheduled
|
import org.springframework.scheduling.annotation.Scheduled
|
||||||
@@ -43,12 +48,12 @@ import java.util.concurrent.atomic.AtomicLong
|
|||||||
import javax.annotation.PostConstruct
|
import javax.annotation.PostConstruct
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
class TrackAddress(
|
class TrackEthereumAddress(
|
||||||
@Autowired private val upstreams: Upstreams,
|
@Autowired private val upstreams: Upstreams,
|
||||||
@Autowired private val upstreamScheduler: Scheduler
|
@Autowired private val upstreamScheduler: Scheduler
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private val log = LoggerFactory.getLogger(TrackAddress::class.java)
|
private val log = LoggerFactory.getLogger(TrackEthereumAddress::class.java)
|
||||||
private val clients = HashMap<Chain, ConcurrentLinkedQueue<TrackedAddress>>()
|
private val clients = HashMap<Chain, ConcurrentLinkedQueue<TrackedAddress>>()
|
||||||
private val seq = AtomicLong(0)
|
private val seq = AtomicLong(0)
|
||||||
|
|
||||||
@@ -94,6 +99,9 @@ class TrackAddress(
|
|||||||
|
|
||||||
private fun initializeSimple(request: BlockchainOuterClass.BalanceRequest): Flux<SimpleAddress> {
|
private fun initializeSimple(request: BlockchainOuterClass.BalanceRequest): Flux<SimpleAddress> {
|
||||||
val chain = Chain.byId(request.asset.chainValue)
|
val chain = Chain.byId(request.asset.chainValue)
|
||||||
|
if (BlockchainType.fromBlockchain(chain) != BlockchainType.ETHEREUM) {
|
||||||
|
return Flux.error(SilentException.UnsupportedBlockchain(request.asset.chainValue))
|
||||||
|
}
|
||||||
if (!upstreams.isAvailable(chain)) {
|
if (!upstreams.isAvailable(chain)) {
|
||||||
return Flux.error(SilentException.UnsupportedBlockchain(request.asset.chainValue))
|
return Flux.error(SilentException.UnsupportedBlockchain(request.asset.chainValue))
|
||||||
}
|
}
|
||||||
@@ -122,6 +130,10 @@ class TrackAddress(
|
|||||||
|
|
||||||
fun subscribe(requestMono: Mono<BlockchainOuterClass.BalanceRequest>): Flux<BlockchainOuterClass.AddressBalance> {
|
fun subscribe(requestMono: Mono<BlockchainOuterClass.BalanceRequest>): Flux<BlockchainOuterClass.AddressBalance> {
|
||||||
return requestMono.flatMapMany { request ->
|
return requestMono.flatMapMany { request ->
|
||||||
|
val chain = Chain.byId(request.asset.chainValue)
|
||||||
|
if (BlockchainType.fromBlockchain(chain) != BlockchainType.ETHEREUM) {
|
||||||
|
return@flatMapMany Flux.error<BlockchainOuterClass.AddressBalance>(SilentException.UnsupportedBlockchain(request.asset.chainValue))
|
||||||
|
}
|
||||||
val bus = TopicProcessor.create<BlockchainOuterClass.AddressBalance>()
|
val bus = TopicProcessor.create<BlockchainOuterClass.AddressBalance>()
|
||||||
initializeSubscription(request, bus)
|
initializeSubscription(request, bus)
|
||||||
.flatMap { tracked ->
|
.flatMap { tracked ->
|
||||||
@@ -175,7 +187,8 @@ class TrackAddress(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getBalance(addr: SimpleAddress): Mono<Wei> {
|
fun getBalance(addr: SimpleAddress): Mono<Wei> {
|
||||||
val up = upstreams.getUpstream(addr.chain) ?: return Mono.error(SilentException.UnsupportedBlockchain(addr.chain))
|
val up = upstreams.getUpstream(addr.chain) as AggregatedUpstream<EthereumApi, BlockJson<TransactionRefJson>>?
|
||||||
|
?: return Mono.error(SilentException.UnsupportedBlockchain(addr.chain))
|
||||||
return up.getApi(Selector.empty)
|
return up.getApi(Selector.empty)
|
||||||
.flatMap { api -> api.executeAndConvert(Commands.eth().getBalance(addr.address, BlockTag.LATEST)) }
|
.flatMap { api -> api.executeAndConvert(Commands.eth().getBalance(addr.address, BlockTag.LATEST)) }
|
||||||
.timeout(Defaults.timeout)
|
.timeout(Defaults.timeout)
|
||||||
@@ -18,10 +18,13 @@ package io.emeraldpay.dshackle.rpc
|
|||||||
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.api.proto.Common
|
import io.emeraldpay.api.proto.Common
|
||||||
|
import io.emeraldpay.dshackle.BlockchainType
|
||||||
import io.emeraldpay.dshackle.SilentException
|
import io.emeraldpay.dshackle.SilentException
|
||||||
|
import io.emeraldpay.dshackle.upstream.AggregatedUpstream
|
||||||
import io.emeraldpay.dshackle.upstream.Selector
|
import io.emeraldpay.dshackle.upstream.Selector
|
||||||
import io.emeraldpay.dshackle.upstream.Upstream
|
import io.emeraldpay.dshackle.upstream.Upstream
|
||||||
import io.emeraldpay.dshackle.upstream.Upstreams
|
import io.emeraldpay.dshackle.upstream.Upstreams
|
||||||
|
import io.emeraldpay.dshackle.upstream.ethereum.EthereumApi
|
||||||
import io.emeraldpay.grpc.Chain
|
import io.emeraldpay.grpc.Chain
|
||||||
import io.infinitape.etherjar.domain.BlockHash
|
import io.infinitape.etherjar.domain.BlockHash
|
||||||
import io.infinitape.etherjar.domain.TransactionId
|
import io.infinitape.etherjar.domain.TransactionId
|
||||||
@@ -40,7 +43,6 @@ import reactor.core.publisher.TopicProcessor
|
|||||||
import reactor.core.publisher.toFlux
|
import reactor.core.publisher.toFlux
|
||||||
import reactor.core.scheduler.Scheduler
|
import reactor.core.scheduler.Scheduler
|
||||||
import reactor.util.function.Tuples
|
import reactor.util.function.Tuples
|
||||||
import java.lang.Exception
|
|
||||||
import java.math.BigInteger
|
import java.math.BigInteger
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
@@ -53,7 +55,7 @@ import kotlin.math.max
|
|||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
class TrackTx(
|
class TrackEthereumTx(
|
||||||
@Autowired private val upstreams: Upstreams,
|
@Autowired private val upstreams: Upstreams,
|
||||||
@Autowired private val upstreamScheduler: Scheduler
|
@Autowired private val upstreamScheduler: Scheduler
|
||||||
) {
|
) {
|
||||||
@@ -67,7 +69,7 @@ class TrackTx(
|
|||||||
private val PING_PERIOD = Duration.ofMinutes(5)
|
private val PING_PERIOD = Duration.ofMinutes(5)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val log = LoggerFactory.getLogger(TrackTx::class.java)
|
private val log = LoggerFactory.getLogger(TrackEthereumTx::class.java)
|
||||||
private val clients = HashMap<Chain, ConcurrentLinkedQueue<TrackedTx>>()
|
private val clients = HashMap<Chain, ConcurrentLinkedQueue<TrackedTx>>()
|
||||||
private val seq = AtomicLong(0)
|
private val seq = AtomicLong(0)
|
||||||
|
|
||||||
@@ -142,6 +144,9 @@ class TrackTx(
|
|||||||
|
|
||||||
fun prepareTracking(request: BlockchainOuterClass.TxStatusRequest): TxDetails {
|
fun prepareTracking(request: BlockchainOuterClass.TxStatusRequest): TxDetails {
|
||||||
val chain = Chain.byId(request.chainValue)
|
val chain = Chain.byId(request.chainValue)
|
||||||
|
if (BlockchainType.fromBlockchain(chain) != BlockchainType.ETHEREUM) {
|
||||||
|
throw SilentException.UnsupportedBlockchain(request.chainValue)
|
||||||
|
}
|
||||||
if (!clients.containsKey(chain)) {
|
if (!clients.containsKey(chain)) {
|
||||||
throw SilentException.UnsupportedBlockchain(chain)
|
throw SilentException.UnsupportedBlockchain(chain)
|
||||||
}
|
}
|
||||||
@@ -208,7 +213,7 @@ class TrackTx(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun loadWeight(tx: TxDetails): Mono<TxDetails> {
|
private fun loadWeight(tx: TxDetails): Mono<TxDetails> {
|
||||||
val upstream = upstreams.getUpstream(tx.chain)
|
val upstream = upstreams.getUpstream(tx.chain) as AggregatedUpstream<EthereumApi, BlockJson<TransactionRefJson>>?
|
||||||
?: return Mono.error(SilentException.UnsupportedBlockchain(tx.chain))
|
?: return Mono.error(SilentException.UnsupportedBlockchain(tx.chain))
|
||||||
return upstream.getApi(Selector.empty)
|
return upstream.getApi(Selector.empty)
|
||||||
.flatMap { api -> api.executeAndConvert(Commands.eth().getBlock(tx.status.blockHash)) }
|
.flatMap { api -> api.executeAndConvert(Commands.eth().getBlock(tx.status.blockHash)) }
|
||||||
@@ -219,7 +224,7 @@ class TrackTx(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateFromBlock(upstream: Upstream, tx: TxDetails, it: TransactionJson): Mono<TxDetails> {
|
fun updateFromBlock(upstream: Upstream<EthereumApi, BlockJson<TransactionRefJson>>, tx: TxDetails, it: TransactionJson): Mono<TxDetails> {
|
||||||
return if (it.blockNumber != null && it.blockHash != null && it.blockHash != ZERO_BLOCK) {
|
return if (it.blockNumber != null && it.blockHash != null && it.blockHash != ZERO_BLOCK) {
|
||||||
val updated = tx.withStatus(
|
val updated = tx.withStatus(
|
||||||
blockHash = it.blockHash,
|
blockHash = it.blockHash,
|
||||||
@@ -250,7 +255,8 @@ class TrackTx(
|
|||||||
|
|
||||||
private fun checkForUpdate(tx: TxDetails): Mono<TxDetails> {
|
private fun checkForUpdate(tx: TxDetails): Mono<TxDetails> {
|
||||||
val initialStatus = tx.status
|
val initialStatus = tx.status
|
||||||
val upstream = upstreams.getUpstream(tx.chain) ?: return Mono.error(SilentException.UnsupportedBlockchain(tx.chain))
|
val upstream = upstreams.getUpstream(tx.chain) as AggregatedUpstream<EthereumApi, BlockJson<TransactionRefJson>>?
|
||||||
|
?: return Mono.error(SilentException.UnsupportedBlockchain(tx.chain))
|
||||||
val execution = upstream.getApi(Selector.empty)
|
val execution = upstream.getApi(Selector.empty)
|
||||||
.flatMap { api -> api.executeAndConvert(Commands.eth().getTransaction(tx.txid)) }
|
.flatMap { api -> api.executeAndConvert(Commands.eth().getTransaction(tx.txid)) }
|
||||||
return execution
|
return execution
|
||||||
@@ -16,9 +16,9 @@
|
|||||||
package io.emeraldpay.dshackle.startup
|
package io.emeraldpay.dshackle.startup
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
|
import io.emeraldpay.dshackle.BlockchainType
|
||||||
import io.emeraldpay.dshackle.FileResolver
|
import io.emeraldpay.dshackle.FileResolver
|
||||||
import io.emeraldpay.dshackle.config.UpstreamsConfig
|
import io.emeraldpay.dshackle.config.UpstreamsConfig
|
||||||
import io.emeraldpay.dshackle.config.UpstreamsConfigReader
|
|
||||||
import io.emeraldpay.dshackle.upstream.CurrentUpstreams
|
import io.emeraldpay.dshackle.upstream.CurrentUpstreams
|
||||||
import io.emeraldpay.dshackle.upstream.calls.ManagedCallMethods
|
import io.emeraldpay.dshackle.upstream.calls.ManagedCallMethods
|
||||||
import io.emeraldpay.dshackle.upstream.ethereum.DirectEthereumApi
|
import io.emeraldpay.dshackle.upstream.ethereum.DirectEthereumApi
|
||||||
@@ -27,16 +27,13 @@ import io.emeraldpay.dshackle.upstream.ethereum.EthereumWs
|
|||||||
import io.emeraldpay.dshackle.upstream.grpc.GrpcUpstreams
|
import io.emeraldpay.dshackle.upstream.grpc.GrpcUpstreams
|
||||||
import io.emeraldpay.grpc.Chain
|
import io.emeraldpay.grpc.Chain
|
||||||
import io.infinitape.etherjar.rpc.http.ReactorHttpRpcClient
|
import io.infinitape.etherjar.rpc.http.ReactorHttpRpcClient
|
||||||
import org.apache.commons.lang3.StringUtils
|
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import org.springframework.beans.factory.annotation.Autowired
|
import org.springframework.beans.factory.annotation.Autowired
|
||||||
import org.springframework.core.env.Environment
|
|
||||||
import org.springframework.stereotype.Repository
|
import org.springframework.stereotype.Repository
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import javax.annotation.PostConstruct
|
import javax.annotation.PostConstruct
|
||||||
import kotlin.collections.HashMap
|
import kotlin.collections.HashMap
|
||||||
import kotlin.system.exitProcess
|
|
||||||
|
|
||||||
@Repository
|
@Repository
|
||||||
open class ConfiguredUpstreams(
|
open class ConfiguredUpstreams(
|
||||||
@@ -59,21 +56,26 @@ open class ConfiguredUpstreams(
|
|||||||
|
|
||||||
@PostConstruct
|
@PostConstruct
|
||||||
fun start() {
|
fun start() {
|
||||||
|
log.debug("Starting upstreams")
|
||||||
val defaultOptions = buildDefaultOptions(config)
|
val defaultOptions = buildDefaultOptions(config)
|
||||||
config.upstreams.forEach { up ->
|
config.upstreams.forEach { up ->
|
||||||
|
log.debug("Start upstream ${up.id}")
|
||||||
if (up.connection is UpstreamsConfig.GrpcConnection) {
|
if (up.connection is UpstreamsConfig.GrpcConnection) {
|
||||||
val options = up.options ?: UpstreamsConfig.Options()
|
val options = up.options ?: UpstreamsConfig.Options()
|
||||||
buildGrpcUpstream(up as UpstreamsConfig.Upstream<UpstreamsConfig.GrpcConnection>, options)
|
buildGrpcUpstream(up.cast(UpstreamsConfig.GrpcConnection::class.java), options)
|
||||||
} else {
|
} else {
|
||||||
val chain = chainNames[up.chain]
|
val chain = chainNames[up.chain]
|
||||||
if (chain == null) {
|
if (chain == null) {
|
||||||
log.error("Chain not supported: ${up.chain}")
|
log.error("Chain is unknown: ${up.chain}")
|
||||||
|
return@forEach
|
||||||
|
}
|
||||||
|
if (BlockchainType.fromBlockchain(chain) != BlockchainType.ETHEREUM) {
|
||||||
|
log.error("Chain is unsupported: ${up.chain}")
|
||||||
return@forEach
|
return@forEach
|
||||||
}
|
}
|
||||||
val options = (up.options ?: UpstreamsConfig.Options())
|
val options = (up.options ?: UpstreamsConfig.Options())
|
||||||
.merge(defaultOptions[chain] ?: UpstreamsConfig.Options.getDefaults())
|
.merge(defaultOptions[chain] ?: UpstreamsConfig.Options.getDefaults())
|
||||||
buildEthereumUpstream(up as UpstreamsConfig.Upstream<UpstreamsConfig.EthereumConnection>, chain, options)
|
buildEthereumUpstream(up.cast(UpstreamsConfig.EthereumConnection::class.java), chain, options)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,15 +20,38 @@ import io.emeraldpay.dshackle.cache.CachesEnabled
|
|||||||
import io.emeraldpay.dshackle.upstream.Upstream
|
import io.emeraldpay.dshackle.upstream.Upstream
|
||||||
import io.emeraldpay.grpc.Chain
|
import io.emeraldpay.grpc.Chain
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An update event to the list of currently available upstreams.
|
||||||
|
*/
|
||||||
class UpstreamChange(
|
class UpstreamChange(
|
||||||
|
/**
|
||||||
|
* Target blockchain
|
||||||
|
*/
|
||||||
val chain: Chain,
|
val chain: Chain,
|
||||||
val upstream: Upstream,
|
/**
|
||||||
|
* Corresponding upstream
|
||||||
|
*/
|
||||||
|
val upstream: Upstream<*, *>,
|
||||||
|
/**
|
||||||
|
* Type of the change
|
||||||
|
*/
|
||||||
val type: ChangeType
|
val type: ChangeType
|
||||||
): CachesEnabled {
|
): CachesEnabled {
|
||||||
|
|
||||||
enum class ChangeType {
|
enum class ChangeType {
|
||||||
|
/**
|
||||||
|
* Upstream just added
|
||||||
|
*/
|
||||||
ADDED,
|
ADDED,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upstream become available after being temporally off
|
||||||
|
*/
|
||||||
REVALIDATED,
|
REVALIDATED,
|
||||||
STALE,
|
|
||||||
|
/**
|
||||||
|
* Upstream is removed (it still doesn't mean it wouldn't return again after some reconfiguration)
|
||||||
|
*/
|
||||||
REMOVED,
|
REMOVED,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,19 +31,22 @@ import java.util.concurrent.locks.ReentrantLock
|
|||||||
import java.util.function.Predicate
|
import java.util.function.Predicate
|
||||||
import kotlin.concurrent.withLock
|
import kotlin.concurrent.withLock
|
||||||
|
|
||||||
abstract class AggregatedUpstream(
|
/**
|
||||||
|
* Aggregation of multiple upstreams responding to a single blockchain
|
||||||
|
*/
|
||||||
|
abstract class AggregatedUpstream<U : UpstreamApi, B>(
|
||||||
private val objectMapper: ObjectMapper,
|
private val objectMapper: ObjectMapper,
|
||||||
val caches: Caches
|
val caches: Caches
|
||||||
): Upstream, Lifecycle {
|
) : Upstream<U, B>, Lifecycle {
|
||||||
|
|
||||||
private var cacheSubscription: Disposable? = null
|
private var cacheSubscription: Disposable? = null
|
||||||
var cache: CachingEthereumApi = CachingEthereumApi.empty()
|
var cache: CachingEthereumApi = CachingEthereumApi.empty()
|
||||||
private val reconfigLock = ReentrantLock()
|
private val reconfigLock = ReentrantLock()
|
||||||
private var callMethods: CallMethods? = null
|
private var callMethods: CallMethods? = null
|
||||||
|
|
||||||
abstract fun getAll(): List<Upstream>
|
abstract fun getAll(): List<Upstream<U, B>>
|
||||||
abstract fun addUpstream(upstream: Upstream)
|
abstract fun addUpstream(upstream: Upstream<U, B>)
|
||||||
abstract fun getApis(matcher: Selector.Matcher): ApiSource
|
abstract fun getApis(matcher: Selector.Matcher): ApiSource<U>
|
||||||
|
|
||||||
fun onUpstreamsUpdated() {
|
fun onUpstreamsUpdated() {
|
||||||
reconfigLock.withLock {
|
reconfigLock.withLock {
|
||||||
@@ -78,24 +81,6 @@ abstract class AggregatedUpstream(
|
|||||||
return callMethods ?: throw IllegalStateException("Methods are not initialized yet")
|
return callMethods ?: throw IllegalStateException("Methods are not initialized yet")
|
||||||
}
|
}
|
||||||
|
|
||||||
class UpstreamStatus(val upstream: Upstream, val status: UpstreamAvailability, val ts: Instant = Instant.now())
|
|
||||||
|
|
||||||
class FilterBestAvailability(): Predicate<UpstreamStatus> {
|
|
||||||
private val lastRef = AtomicReference<UpstreamStatus>()
|
|
||||||
|
|
||||||
override fun test(t: UpstreamStatus): Boolean {
|
|
||||||
val last = lastRef.get()
|
|
||||||
val changed = last == null
|
|
||||||
|| t.status > last.status
|
|
||||||
|| (last.upstream == t.upstream && t.status != last.status)
|
|
||||||
|| last.ts.isBefore(Instant.now() - Duration.ofSeconds(60))
|
|
||||||
if (changed) {
|
|
||||||
lastRef.set(t)
|
|
||||||
}
|
|
||||||
return changed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun start() {
|
override fun start() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,4 +99,24 @@ abstract class AggregatedUpstream(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class UpstreamStatus<H>(val upstream: Upstream<UpstreamApi, H>, val status: UpstreamAvailability, val ts: Instant = Instant.now())
|
||||||
|
|
||||||
|
class FilterBestAvailability() : Predicate<UpstreamStatus<*>> {
|
||||||
|
private val lastRef = AtomicReference<UpstreamStatus<*>>()
|
||||||
|
|
||||||
|
override fun test(t: UpstreamStatus<*>): Boolean {
|
||||||
|
val last = lastRef.get()
|
||||||
|
val changed = last == null
|
||||||
|
|| t.status > last.status
|
||||||
|
|| (last.upstream == t.upstream && t.status != last.status)
|
||||||
|
|| last.ts.isBefore(Instant.now() - Duration.ofSeconds(60))
|
||||||
|
if (changed) {
|
||||||
|
lastRef.set(t)
|
||||||
|
}
|
||||||
|
return changed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -18,7 +18,7 @@ package io.emeraldpay.dshackle.upstream
|
|||||||
import io.emeraldpay.dshackle.upstream.ethereum.DirectEthereumApi
|
import io.emeraldpay.dshackle.upstream.ethereum.DirectEthereumApi
|
||||||
import org.reactivestreams.Publisher
|
import org.reactivestreams.Publisher
|
||||||
|
|
||||||
interface ApiSource: Publisher<DirectEthereumApi> {
|
interface ApiSource<U : UpstreamApi> : Publisher<U> {
|
||||||
|
|
||||||
fun resolve()
|
fun resolve()
|
||||||
fun request(tries: Int)
|
fun request(tries: Int)
|
||||||
|
|||||||
@@ -17,38 +17,34 @@ package io.emeraldpay.dshackle.upstream
|
|||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
import io.emeraldpay.dshackle.cache.Caches
|
import io.emeraldpay.dshackle.cache.Caches
|
||||||
import io.emeraldpay.dshackle.config.UpstreamsConfig
|
|
||||||
import io.emeraldpay.dshackle.upstream.ethereum.DirectEthereumApi
|
|
||||||
import io.emeraldpay.dshackle.upstream.ethereum.EthereumHead
|
|
||||||
import io.emeraldpay.dshackle.upstream.ethereum.EthereumHeadMerge
|
|
||||||
import io.emeraldpay.grpc.Chain
|
import io.emeraldpay.grpc.Chain
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import org.springframework.context.Lifecycle
|
import org.springframework.context.Lifecycle
|
||||||
import reactor.core.Disposable
|
import reactor.core.Disposable
|
||||||
import reactor.core.publisher.Mono
|
import reactor.core.publisher.Mono
|
||||||
import java.lang.IllegalStateException
|
|
||||||
import java.time.Duration
|
|
||||||
|
|
||||||
open class ChainUpstreams (
|
/**
|
||||||
|
* General interface to upstream(s) to a single chain
|
||||||
|
*/
|
||||||
|
abstract class ChainUpstreams<U : UpstreamApi, B>(
|
||||||
val chain: Chain,
|
val chain: Chain,
|
||||||
private val upstreams: MutableList<Upstream>,
|
private val upstreams: MutableList<Upstream<U, B>>,
|
||||||
caches: Caches,
|
caches: Caches,
|
||||||
objectMapper: ObjectMapper
|
objectMapper: ObjectMapper
|
||||||
) : AggregatedUpstream(objectMapper, caches), Lifecycle {
|
) : AggregatedUpstream<U, B>(objectMapper, caches), Lifecycle {
|
||||||
|
|
||||||
private val log = LoggerFactory.getLogger(ChainUpstreams::class.java)
|
private val log = LoggerFactory.getLogger(ChainUpstreams::class.java)
|
||||||
private var seq = 0
|
private var seq = 0
|
||||||
private var head: EthereumHead? = null
|
protected var lagObserver: HeadLagObserver<U, B>? = null
|
||||||
private var lagObserver: HeadLagObserver? = null
|
|
||||||
private var subscription: Disposable? = null
|
private var subscription: Disposable? = null
|
||||||
|
|
||||||
init {
|
open fun init() {
|
||||||
if (upstreams.size > 0) {
|
onUpstreamsUpdated()
|
||||||
head = updateHead()
|
|
||||||
onUpstreamsUpdated()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
abstract fun updateHead(): Head<B>
|
||||||
|
abstract fun setHead(head: Head<B>)
|
||||||
|
|
||||||
override fun getId(): String {
|
override fun getId(): String {
|
||||||
return "!all:${chain.chainCode}"
|
return "!all:${chain.chainCode}"
|
||||||
}
|
}
|
||||||
@@ -68,7 +64,7 @@ open class ChainUpstreams (
|
|||||||
super.stop()
|
super.stop()
|
||||||
subscription?.dispose()
|
subscription?.dispose()
|
||||||
subscription = null
|
subscription = null
|
||||||
head?.let {
|
getHead().let {
|
||||||
if (it is Lifecycle) {
|
if (it is Lifecycle) {
|
||||||
it.stop()
|
it.stop()
|
||||||
}
|
}
|
||||||
@@ -76,50 +72,24 @@ open class ChainUpstreams (
|
|||||||
lagObserver?.stop()
|
lagObserver?.stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun updateHead(): EthereumHead {
|
override fun getAll(): List<Upstream<U, B>> {
|
||||||
head?.let {
|
|
||||||
if (it is Lifecycle) {
|
|
||||||
it.stop()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
lagObserver?.stop()
|
|
||||||
lagObserver = null
|
|
||||||
val head = if (upstreams.size == 1) {
|
|
||||||
val upstream = upstreams.first()
|
|
||||||
upstream.setLag(0)
|
|
||||||
upstream.getHead()
|
|
||||||
} else {
|
|
||||||
val newHead = EthereumHeadMerge(upstreams.map { it.getHead() }).apply {
|
|
||||||
this.start()
|
|
||||||
}
|
|
||||||
val lagObserver = HeadLagObserver(newHead, upstreams).apply {
|
|
||||||
this.start()
|
|
||||||
}
|
|
||||||
this.lagObserver = lagObserver
|
|
||||||
newHead
|
|
||||||
}
|
|
||||||
onHeadUpdated(head)
|
|
||||||
return head
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getAll(): List<Upstream> {
|
|
||||||
return upstreams
|
return upstreams
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun addUpstream(upstream: Upstream) {
|
override fun addUpstream(upstream: Upstream<U, B>) {
|
||||||
upstreams.add(upstream)
|
upstreams.add(upstream)
|
||||||
head = updateHead()
|
setHead(updateHead())
|
||||||
onUpstreamsUpdated()
|
onUpstreamsUpdated()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeUpstream(id: String) {
|
fun removeUpstream(id: String) {
|
||||||
if (upstreams.removeIf { it.getId() == id }) {
|
if (upstreams.removeIf { it.getId() == id }) {
|
||||||
head = updateHead()
|
setHead(updateHead())
|
||||||
onUpstreamsUpdated()
|
onUpstreamsUpdated()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getApis(matcher: Selector.Matcher): ApiSource {
|
override fun getApis(matcher: Selector.Matcher): ApiSource<U> {
|
||||||
val i = seq++
|
val i = seq++
|
||||||
if (seq >= Int.MAX_VALUE / 2) {
|
if (seq >= Int.MAX_VALUE / 2) {
|
||||||
seq = 0
|
seq = 0
|
||||||
@@ -127,15 +97,11 @@ open class ChainUpstreams (
|
|||||||
return FilteredApis(upstreams, matcher, i)
|
return FilteredApis(upstreams, matcher, i)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getApi(matcher: Selector.Matcher): Mono<DirectEthereumApi> {
|
override fun getApi(matcher: Selector.Matcher): Mono<U> {
|
||||||
val apis = getApis(matcher)
|
val apis = getApis(matcher)
|
||||||
apis.request(1)
|
apis.request(1)
|
||||||
return Mono.from(apis)
|
return Mono.from(apis)
|
||||||
.switchIfEmpty(Mono.error<DirectEthereumApi>(Exception("No API available")))
|
.switchIfEmpty(Mono.error<U>(Exception("No API available")))
|
||||||
}
|
|
||||||
|
|
||||||
override fun getHead(): EthereumHead {
|
|
||||||
return head!!
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setLag(lag: Long) {
|
override fun setLag(lag: Long) {
|
||||||
@@ -145,28 +111,5 @@ open class ChainUpstreams (
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getLabels(): Collection<UpstreamsConfig.Labels> {
|
abstract fun printStatus()
|
||||||
return upstreams.flatMap { it.getLabels() }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun printStatus() {
|
|
||||||
var height: Long? = null
|
|
||||||
try {
|
|
||||||
height = head!!.getFlux().next().block(Duration.ofSeconds(1))?.number
|
|
||||||
} catch (e: IllegalStateException) {
|
|
||||||
//timout
|
|
||||||
} catch (e: Exception) {
|
|
||||||
log.warn("Head processing error: ${e.javaClass} ${e.message}")
|
|
||||||
}
|
|
||||||
val statuses = upstreams.map { it.getStatus() }
|
|
||||||
.groupBy { it }
|
|
||||||
.map { "${it.key.name}/${it.value.size}" }
|
|
||||||
.joinToString(",")
|
|
||||||
val lag = upstreams.map { it.getLag() }
|
|
||||||
.joinToString(", ")
|
|
||||||
|
|
||||||
log.info("State of ${chain.chainCode}: height=${height ?: '?'}, status=$statuses, lag=[$lag]")
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -16,13 +16,17 @@
|
|||||||
package io.emeraldpay.dshackle.upstream
|
package io.emeraldpay.dshackle.upstream
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
import io.emeraldpay.dshackle.cache.Caches
|
|
||||||
import io.emeraldpay.dshackle.cache.CachesEnabled
|
import io.emeraldpay.dshackle.cache.CachesEnabled
|
||||||
import io.emeraldpay.dshackle.cache.CachesFactory
|
import io.emeraldpay.dshackle.cache.CachesFactory
|
||||||
import io.emeraldpay.dshackle.startup.UpstreamChange
|
import io.emeraldpay.dshackle.startup.UpstreamChange
|
||||||
import io.emeraldpay.dshackle.upstream.calls.CallMethods
|
import io.emeraldpay.dshackle.upstream.calls.CallMethods
|
||||||
import io.emeraldpay.dshackle.upstream.calls.QuorumBasedMethods
|
import io.emeraldpay.dshackle.upstream.calls.DefaultEthereumMethods
|
||||||
|
import io.emeraldpay.dshackle.upstream.ethereum.EthereumApi
|
||||||
|
import io.emeraldpay.dshackle.upstream.ethereum.EthereumChainUpstreams
|
||||||
|
import io.emeraldpay.dshackle.upstream.ethereum.EthereumUpstream
|
||||||
import io.emeraldpay.grpc.Chain
|
import io.emeraldpay.grpc.Chain
|
||||||
|
import io.infinitape.etherjar.rpc.json.BlockJson
|
||||||
|
import io.infinitape.etherjar.rpc.json.TransactionRefJson
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import org.springframework.beans.factory.annotation.Autowired
|
import org.springframework.beans.factory.annotation.Autowired
|
||||||
import org.springframework.scheduling.annotation.Scheduled
|
import org.springframework.scheduling.annotation.Scheduled
|
||||||
@@ -42,22 +46,23 @@ class CurrentUpstreams(
|
|||||||
|
|
||||||
private val log = LoggerFactory.getLogger(CurrentUpstreams::class.java)
|
private val log = LoggerFactory.getLogger(CurrentUpstreams::class.java)
|
||||||
|
|
||||||
private val chainMapping = ConcurrentHashMap<Chain, ChainUpstreams>()
|
private val chainMapping = ConcurrentHashMap<Chain, ChainUpstreams<*, *>>()
|
||||||
private val chainsBus = TopicProcessor.create<Chain>()
|
private val chainsBus = TopicProcessor.create<Chain>()
|
||||||
private val callTargets = HashMap<Chain, QuorumBasedMethods>()
|
private val callTargets = HashMap<Chain, CallMethods>()
|
||||||
private val updateLock = ReentrantLock()
|
private val updateLock = ReentrantLock()
|
||||||
|
|
||||||
fun update(change: UpstreamChange) {
|
fun update(change: UpstreamChange) {
|
||||||
updateLock.withLock {
|
updateLock.withLock {
|
||||||
val chain = change.chain
|
val chain = change.chain
|
||||||
val up = change.upstream
|
val up = change.upstream
|
||||||
val current = chainMapping[chain]
|
.cast(EthereumUpstream::class.java, EthereumApi::class.java, BlockJson::class.java) as Upstream<EthereumApi, BlockJson<TransactionRefJson>>
|
||||||
|
val current = chainMapping[chain] as ChainUpstreams<EthereumApi, BlockJson<TransactionRefJson>>?
|
||||||
if (change.type == UpstreamChange.ChangeType.REMOVED) {
|
if (change.type == UpstreamChange.ChangeType.REMOVED) {
|
||||||
current?.removeUpstream(up.getId())
|
current?.removeUpstream(up.getId())
|
||||||
log.info("Upstream ${change.upstream.getId()} with chain $chain has been removed")
|
log.info("Upstream ${change.upstream.getId()} with chain $chain has been removed")
|
||||||
} else {
|
} else {
|
||||||
if (current == null) {
|
if (current == null) {
|
||||||
val created = ChainUpstreams(chain, ArrayList<Upstream>(), cachesFactory.getCaches(chain), objectMapper)
|
val created = EthereumChainUpstreams(chain, ArrayList(), cachesFactory.getCaches(chain), objectMapper)
|
||||||
if (up is CachesEnabled) {
|
if (up is CachesEnabled) {
|
||||||
up.setCaches(created.caches)
|
up.setCaches(created.caches)
|
||||||
}
|
}
|
||||||
@@ -79,7 +84,7 @@ class CurrentUpstreams(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getUpstream(chain: Chain): AggregatedUpstream? {
|
override fun getUpstream(chain: Chain): AggregatedUpstream<*, *>? {
|
||||||
return chainMapping[chain]
|
return chainMapping[chain]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,8 +108,8 @@ class CurrentUpstreams(
|
|||||||
return callTargets[chain] ?: return setupDefaultMethods(chain)
|
return callTargets[chain] ?: return setupDefaultMethods(chain)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setupDefaultMethods(chain: Chain): QuorumBasedMethods {
|
fun setupDefaultMethods(chain: Chain): CallMethods {
|
||||||
val created = QuorumBasedMethods(objectMapper, chain)
|
val created = DefaultEthereumMethods(objectMapper, chain)
|
||||||
callTargets[chain] = created
|
callTargets[chain] = created
|
||||||
return created
|
return created
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,10 +19,10 @@ import reactor.core.publisher.Flux
|
|||||||
import reactor.core.publisher.TopicProcessor
|
import reactor.core.publisher.TopicProcessor
|
||||||
import java.util.concurrent.atomic.AtomicReference
|
import java.util.concurrent.atomic.AtomicReference
|
||||||
|
|
||||||
abstract class DefaultUpstream(
|
abstract class DefaultUpstream<U : UpstreamApi, B>(
|
||||||
defaultLag: Long,
|
defaultLag: Long,
|
||||||
defaultAvail: UpstreamAvailability
|
defaultAvail: UpstreamAvailability
|
||||||
) : Upstream {
|
) : Upstream<U, B> {
|
||||||
|
|
||||||
constructor() : this(Long.MAX_VALUE, UpstreamAvailability.UNAVAILABLE)
|
constructor() : this(Long.MAX_VALUE, UpstreamAvailability.UNAVAILABLE)
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,6 @@
|
|||||||
*/
|
*/
|
||||||
package io.emeraldpay.dshackle.upstream
|
package io.emeraldpay.dshackle.upstream
|
||||||
|
|
||||||
import io.emeraldpay.dshackle.upstream.ethereum.DirectEthereumApi
|
|
||||||
import org.reactivestreams.Subscriber
|
import org.reactivestreams.Subscriber
|
||||||
import reactor.core.publisher.EmitterProcessor
|
import reactor.core.publisher.EmitterProcessor
|
||||||
import reactor.core.publisher.Flux
|
import reactor.core.publisher.Flux
|
||||||
@@ -26,28 +25,28 @@ import kotlin.math.pow
|
|||||||
import kotlin.math.roundToLong
|
import kotlin.math.roundToLong
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
|
|
||||||
class FilteredApis(
|
class FilteredApis<U : UpstreamApi>(
|
||||||
allUpstreams: List<Upstream>,
|
allUpstreams: List<Upstream<U, *>>,
|
||||||
private val matcher: Selector.Matcher,
|
private val matcher: Selector.Matcher,
|
||||||
pos: Int,
|
pos: Int,
|
||||||
private val repeatLimit: Long,
|
private val repeatLimit: Long,
|
||||||
jitter: Int
|
jitter: Int
|
||||||
): ApiSource {
|
) : ApiSource<U> {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val DEFAULT_DELAY_STEP = 100
|
private const val DEFAULT_DELAY_STEP = 100
|
||||||
private const val MAX_WAIT_MILLIS = 5000L
|
private const val MAX_WAIT_MILLIS = 5000L
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(allUpstreams: List<Upstream>,
|
constructor(allUpstreams: List<Upstream<U, *>>,
|
||||||
matcher: Selector.Matcher,
|
matcher: Selector.Matcher,
|
||||||
pos: Int): this(allUpstreams, matcher, pos, 10, 7)
|
pos: Int) : this(allUpstreams, matcher, pos, 10, 7)
|
||||||
|
|
||||||
constructor(allUpstreams: List<Upstream>,
|
constructor(allUpstreams: List<Upstream<U, *>>,
|
||||||
matcher: Selector.Matcher): this(allUpstreams, matcher, 0, 10, 10)
|
matcher: Selector.Matcher) : this(allUpstreams, matcher, 0, 10, 10)
|
||||||
|
|
||||||
private val delay: Int
|
private val delay: Int
|
||||||
private val upstreams: List<Upstream>
|
private val upstreams: List<Upstream<UpstreamApi, *>>
|
||||||
|
|
||||||
private val control = EmitterProcessor.create<Boolean>(32, false)
|
private val control = EmitterProcessor.create<Boolean>(32, false)
|
||||||
|
|
||||||
@@ -75,18 +74,18 @@ class FilteredApis(
|
|||||||
return Duration.ofMillis(time)
|
return Duration.ofMillis(time)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun subscribe(subscriber: Subscriber<in DirectEthereumApi>) {
|
override fun subscribe(subscriber: Subscriber<in U>) {
|
||||||
val first = Flux.fromIterable(upstreams)
|
val first = Flux.fromIterable(upstreams)
|
||||||
val retries = (1 until repeatLimit).map { r ->
|
val retries = (1 until repeatLimit).map { r ->
|
||||||
Flux.fromIterable(upstreams).delaySubscription(waitDuration(r))
|
Flux.fromIterable(upstreams).delaySubscription(waitDuration(r))
|
||||||
}.let { Flux.concat(it) }
|
}.let { Flux.concat(it) }
|
||||||
|
|
||||||
Flux.concat(first, retries)
|
Flux.concat(first, retries)
|
||||||
.filter(Upstream::isAvailable)
|
.filter(Upstream<UpstreamApi, *>::isAvailable)
|
||||||
.filter(matcher::matches)
|
.filter(matcher::matches)
|
||||||
.flatMap { it.getApi(matcher) }
|
.flatMap { it.getApi(matcher) }
|
||||||
.zipWith(control).map { it.t1 }
|
.zipWith(control).map { it.t1 }
|
||||||
.subscribe(subscriber)
|
.subscribe(subscriber as Subscriber<in UpstreamApi>)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun resolve() {
|
override fun resolve() {
|
||||||
|
|||||||
@@ -18,6 +18,6 @@ package io.emeraldpay.dshackle.upstream
|
|||||||
import reactor.core.publisher.Flux
|
import reactor.core.publisher.Flux
|
||||||
import reactor.core.publisher.Mono
|
import reactor.core.publisher.Mono
|
||||||
|
|
||||||
interface Head<T> {
|
interface Head<out T> {
|
||||||
fun getFlux(): Flux<T>
|
fun getFlux(): Flux<out T>
|
||||||
}
|
}
|
||||||
@@ -15,23 +15,21 @@
|
|||||||
*/
|
*/
|
||||||
package io.emeraldpay.dshackle.upstream
|
package io.emeraldpay.dshackle.upstream
|
||||||
|
|
||||||
import io.emeraldpay.dshackle.upstream.ethereum.EthereumHead
|
|
||||||
import io.infinitape.etherjar.domain.TransactionId
|
|
||||||
import io.infinitape.etherjar.rpc.json.BlockJson
|
|
||||||
import io.infinitape.etherjar.rpc.json.TransactionRefJson
|
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import org.springframework.context.Lifecycle
|
import org.springframework.context.Lifecycle
|
||||||
import reactor.core.Disposable
|
import reactor.core.Disposable
|
||||||
import reactor.core.publisher.Flux
|
import reactor.core.publisher.Flux
|
||||||
import reactor.core.publisher.toFlux
|
|
||||||
import reactor.util.function.Tuple2
|
import reactor.util.function.Tuple2
|
||||||
import reactor.util.function.Tuples
|
import reactor.util.function.Tuples
|
||||||
import java.time.Duration
|
|
||||||
|
|
||||||
class HeadLagObserver (
|
/**
|
||||||
private val master: EthereumHead,
|
* Observer group of upstreams and defined a distance in blocks (lag) between a leader (best height/difficulty) and
|
||||||
private val followers: Collection<Upstream>
|
* other upstreams.
|
||||||
): Lifecycle {
|
*/
|
||||||
|
abstract class HeadLagObserver<A : UpstreamApi, B>(
|
||||||
|
private val master: Head<B>,
|
||||||
|
private val followers: Collection<Upstream<A, B>>
|
||||||
|
) : Lifecycle {
|
||||||
|
|
||||||
private val log = LoggerFactory.getLogger(HeadLagObserver::class.java)
|
private val log = LoggerFactory.getLogger(HeadLagObserver::class.java)
|
||||||
|
|
||||||
@@ -50,7 +48,7 @@ class HeadLagObserver (
|
|||||||
current = null
|
current = null
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun subscription(): Flux<Unit> {
|
fun subscription(): Flux<Unit> {
|
||||||
return master.getFlux()
|
return master.getFlux()
|
||||||
.flatMap(this::probeFollowers)
|
.flatMap(this::probeFollowers)
|
||||||
.map { item ->
|
.map { item ->
|
||||||
@@ -58,38 +56,29 @@ class HeadLagObserver (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun probeFollowers(top: BlockJson<TransactionRefJson>): Flux<Tuple2<Long, Upstream>> {
|
fun probeFollowers(top: B): Flux<Tuple2<Long, Upstream<A, B>>> {
|
||||||
return followers.toFlux()
|
return Flux.fromIterable(followers)
|
||||||
.parallel(followers.size)
|
.parallel(followers.size)
|
||||||
.flatMap { mapLagging(top, it, getCurrentBlocks(it)) }
|
.flatMap { mapLagging(top, it, getCurrentBlocks(it)) }
|
||||||
.sequential()
|
.sequential()
|
||||||
.onErrorContinue { t, _ -> log.warn("Failed to update lagging distance", t) }
|
.onErrorContinue { t, _ -> log.warn("Failed to update lagging distance", t) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getCurrentBlocks(up: Upstream): Flux<BlockJson<TransactionRefJson>> {
|
abstract fun getCurrentBlocks(up: Upstream<A, B>): Flux<B>
|
||||||
val head = up.getHead()
|
|
||||||
return head.getFlux().take(Duration.ofSeconds(1))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun mapLagging(top: BlockJson<TransactionRefJson>, up: Upstream, blocks: Flux<BlockJson<TransactionRefJson>>): Flux<Tuple2<Long, Upstream>> {
|
fun mapLagging(top: B, up: Upstream<A, B>, blocks: Flux<B>): Flux<Tuple2<Long, Upstream<A, B>>> {
|
||||||
return blocks
|
return blocks
|
||||||
.map { extractDistance(top, it) }
|
.map { extractDistance(top, it) }
|
||||||
.takeUntil{ lag -> lag <= 0L }
|
.takeUntil { lag -> lag <= 0L }
|
||||||
.map { Tuples.of(it, up) }
|
.map { Tuples.of(it, up) }
|
||||||
.doOnError { t ->
|
.doOnError { t ->
|
||||||
log.warn("Failed to find distance for $up", t)
|
log.warn("Failed to find distance for $up", t)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun extractDistance(top: BlockJson<TransactionRefJson>, curr: BlockJson<TransactionRefJson>): Long {
|
abstract fun extractDistance(top: B, curr: B): Long
|
||||||
return when {
|
|
||||||
curr.number > top.number -> if (curr.totalDifficulty >= top.totalDifficulty) 0 else forkDistance(top, curr)
|
|
||||||
curr.number == top.number -> if (curr.totalDifficulty == top.totalDifficulty) 0 else forkDistance(top, curr)
|
|
||||||
else -> top.number - curr.number
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun forkDistance(top: BlockJson<TransactionRefJson>, curr: BlockJson<TransactionRefJson>): Long {
|
fun forkDistance(top: B, curr: B): Long {
|
||||||
//TODO look for common ancestor? though it may be a corruption
|
//TODO look for common ancestor? though it may be a corruption
|
||||||
return 6
|
return 6
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -95,13 +95,13 @@ class Selector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface Matcher {
|
interface Matcher {
|
||||||
fun matches(up: Upstream): Boolean
|
fun matches(up: Upstream<UpstreamApi, *>): Boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
class MultiMatcher(
|
class MultiMatcher(
|
||||||
private val matchers: Collection<Matcher>
|
private val matchers: Collection<Matcher>
|
||||||
): Matcher {
|
): Matcher {
|
||||||
override fun matches(up: Upstream): Boolean {
|
override fun matches(up: Upstream<UpstreamApi, *>): Boolean {
|
||||||
return matchers.all { it.matches(up) }
|
return matchers.all { it.matches(up) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,21 +113,22 @@ class Selector {
|
|||||||
class MethodMatcher(
|
class MethodMatcher(
|
||||||
val method: String
|
val method: String
|
||||||
): Matcher {
|
): Matcher {
|
||||||
override fun matches(up: Upstream): Boolean {
|
override fun matches(up: Upstream<UpstreamApi, *>): Boolean {
|
||||||
return up.getMethods().isAllowed(method)
|
return up.getMethods().isAllowed(method)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class LabelSelectorMatcher: Matcher {
|
abstract class LabelSelectorMatcher: Matcher {
|
||||||
override fun matches(up: Upstream): Boolean {
|
override fun matches(up: Upstream<UpstreamApi, *>): Boolean {
|
||||||
return up.getLabels().any(this::matches)
|
return up.getLabels().any(this::matches)
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract fun matches(labels: UpstreamsConfig.Labels): Boolean
|
abstract fun matches(labels: UpstreamsConfig.Labels): Boolean
|
||||||
abstract fun asProto(): BlockchainOuterClass.Selector?
|
abstract fun asProto(): BlockchainOuterClass.Selector?
|
||||||
}
|
}
|
||||||
|
|
||||||
class EmptyMatcher: Matcher {
|
class EmptyMatcher: Matcher {
|
||||||
override fun matches(up: Upstream): Boolean {
|
override fun matches(up: Upstream<UpstreamApi, *>): Boolean {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -142,7 +143,7 @@ class Selector {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun matches(up: Upstream): Boolean {
|
override fun matches(up: Upstream<UpstreamApi, *>): Boolean {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,16 +22,18 @@ import io.emeraldpay.dshackle.upstream.ethereum.EthereumHead
|
|||||||
import reactor.core.publisher.Flux
|
import reactor.core.publisher.Flux
|
||||||
import reactor.core.publisher.Mono
|
import reactor.core.publisher.Mono
|
||||||
|
|
||||||
interface Upstream {
|
interface Upstream<out T : UpstreamApi, out B> {
|
||||||
fun isAvailable(): Boolean
|
fun isAvailable(): Boolean
|
||||||
fun getStatus(): UpstreamAvailability
|
fun getStatus(): UpstreamAvailability
|
||||||
fun observeStatus(): Flux<UpstreamAvailability>
|
fun observeStatus(): Flux<UpstreamAvailability>
|
||||||
fun getHead(): EthereumHead
|
fun getHead(): Head<B>
|
||||||
fun getApi(matcher: Selector.Matcher): Mono<DirectEthereumApi>
|
fun getApi(matcher: Selector.Matcher): Mono<out T>
|
||||||
fun getOptions(): UpstreamsConfig.Options
|
fun getOptions(): UpstreamsConfig.Options
|
||||||
fun setLag(lag: Long)
|
fun setLag(lag: Long)
|
||||||
fun getLag(): Long
|
fun getLag(): Long
|
||||||
fun getLabels(): Collection<UpstreamsConfig.Labels>
|
fun getLabels(): Collection<UpstreamsConfig.Labels>
|
||||||
fun getMethods(): CallMethods
|
fun getMethods(): CallMethods
|
||||||
fun getId(): String
|
fun getId(): String
|
||||||
|
|
||||||
|
fun <T : Upstream<TA, BA>, TA : UpstreamApi, BA> cast(selfType: Class<T>, upstreamType: Class<TA>, blockType: Class<BA>): T
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2020 ETCDEV GmbH
|
||||||
|
*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
import reactor.core.publisher.Mono
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A general interface to make a request to an Upstream API
|
||||||
|
*/
|
||||||
|
interface UpstreamApi {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param id an internal uniq id, if multiple requests are made in batch
|
||||||
|
* @param method JSON RPC method name
|
||||||
|
* @param params JSON RPC parameters, must be serializable into a JSON array
|
||||||
|
*/
|
||||||
|
fun execute(id: Int, method: String, params: List<Any>): Mono<ByteArray>
|
||||||
|
|
||||||
|
}
|
||||||
@@ -20,7 +20,7 @@ import io.emeraldpay.grpc.Chain
|
|||||||
import reactor.core.publisher.Flux
|
import reactor.core.publisher.Flux
|
||||||
|
|
||||||
interface Upstreams {
|
interface Upstreams {
|
||||||
fun getUpstream(chain: Chain): AggregatedUpstream?
|
fun getUpstream(chain: Chain): AggregatedUpstream<*, *>?
|
||||||
fun getAvailable(): List<Chain>
|
fun getAvailable(): List<Chain>
|
||||||
fun observeChains(): Flux<Chain>
|
fun observeChains(): Flux<Chain>
|
||||||
fun getDefaultMethods(chain: Chain): CallMethods
|
fun getDefaultMethods(chain: Chain): CallMethods
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import java.util.*
|
|||||||
* Default configuration for Ethereum based RPC. Defines optimal Quorum strategies for different methods, and provides
|
* Default configuration for Ethereum based RPC. Defines optimal Quorum strategies for different methods, and provides
|
||||||
* hardcoded results for base methods, such as `net_version`, `web3_clientVersion` and similar
|
* hardcoded results for base methods, such as `net_version`, `web3_clientVersion` and similar
|
||||||
*/
|
*/
|
||||||
class QuorumBasedMethods(
|
class DefaultEthereumMethods(
|
||||||
private val objectMapper: ObjectMapper,
|
private val objectMapper: ObjectMapper,
|
||||||
private val chain: Chain
|
private val chain: Chain
|
||||||
) : CallMethods {
|
) : CallMethods {
|
||||||
@@ -102,6 +102,7 @@ class QuorumBasedMethods(
|
|||||||
override fun isAllowed(method: String): Boolean {
|
override fun isAllowed(method: String): Boolean {
|
||||||
return allowedMethods.contains(method)
|
return allowedMethods.contains(method)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun isHardcoded(method: String): Boolean {
|
override fun isHardcoded(method: String): Boolean {
|
||||||
return hardcodedMethods.contains(method)
|
return hardcodedMethods.contains(method)
|
||||||
}
|
}
|
||||||
@@ -17,23 +17,24 @@ package io.emeraldpay.dshackle.upstream.ethereum
|
|||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
import io.emeraldpay.dshackle.upstream.Upstream
|
import io.emeraldpay.dshackle.upstream.Upstream
|
||||||
|
import io.emeraldpay.dshackle.upstream.UpstreamApi
|
||||||
import io.infinitape.etherjar.rpc.*
|
import io.infinitape.etherjar.rpc.*
|
||||||
|
import io.infinitape.etherjar.rpc.json.BlockJson
|
||||||
|
import io.infinitape.etherjar.rpc.json.TransactionRefJson
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import reactor.core.publisher.Mono
|
import reactor.core.publisher.Mono
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
|
||||||
abstract class EthereumApi(
|
abstract class EthereumApi(
|
||||||
objectMapper: ObjectMapper
|
objectMapper: ObjectMapper
|
||||||
) {
|
) : UpstreamApi {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val log = LoggerFactory.getLogger(EthereumApi::class.java)
|
private val log = LoggerFactory.getLogger(EthereumApi::class.java)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val jacksonRpcConverter = JacksonRpcConverter(objectMapper)
|
private val jacksonRpcConverter = JacksonRpcConverter(objectMapper)
|
||||||
var upstream: Upstream? = null
|
var upstream: Upstream<EthereumApi, BlockJson<TransactionRefJson>>? = null
|
||||||
|
|
||||||
abstract fun execute(id: Int, method: String, params: List<Any>): Mono<ByteArray>
|
|
||||||
|
|
||||||
fun <JS, RS> execute(rpcCall: RpcCall<JS, RS>): Mono<ByteArray> {
|
fun <JS, RS> execute(rpcCall: RpcCall<JS, RS>): Mono<ByteArray> {
|
||||||
return execute(0, rpcCall.method, rpcCall.params as List<Any>)
|
return execute(0, rpcCall.method, rpcCall.params as List<Any>)
|
||||||
|
|||||||
@@ -0,0 +1,127 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2020 ETCDEV GmbH
|
||||||
|
*
|
||||||
|
* 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.ethereum
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
|
import io.emeraldpay.dshackle.cache.Caches
|
||||||
|
import io.emeraldpay.dshackle.config.UpstreamsConfig
|
||||||
|
import io.emeraldpay.dshackle.upstream.*
|
||||||
|
import io.emeraldpay.grpc.Chain
|
||||||
|
import io.infinitape.etherjar.rpc.json.BlockJson
|
||||||
|
import io.infinitape.etherjar.rpc.json.TransactionJson
|
||||||
|
import io.infinitape.etherjar.rpc.json.TransactionRefJson
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import org.springframework.context.Lifecycle
|
||||||
|
import java.lang.IllegalStateException
|
||||||
|
import java.time.Duration
|
||||||
|
|
||||||
|
class EthereumChainUpstreams(
|
||||||
|
chain: Chain,
|
||||||
|
val upstreams: MutableList<EthereumUpstream>,
|
||||||
|
caches: Caches,
|
||||||
|
objectMapper: ObjectMapper
|
||||||
|
) : ChainUpstreams<EthereumApi, BlockJson<TransactionRefJson>>(chain, upstreams as MutableList<Upstream<EthereumApi, BlockJson<TransactionRefJson>>>, caches, objectMapper) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val log = LoggerFactory.getLogger(EthereumChainUpstreams::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var head: EthereumHead? = null
|
||||||
|
|
||||||
|
init {
|
||||||
|
this.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun init() {
|
||||||
|
if (upstreams.size > 0) {
|
||||||
|
head = updateHead()
|
||||||
|
}
|
||||||
|
super.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getHead(): EthereumHead {
|
||||||
|
return head!!
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setHead(head: Head<BlockJson<TransactionRefJson>>) {
|
||||||
|
this.head = head as EthereumHead
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun updateHead(): EthereumHead {
|
||||||
|
head?.let {
|
||||||
|
if (it is Lifecycle) {
|
||||||
|
it.stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lagObserver?.stop()
|
||||||
|
lagObserver = null
|
||||||
|
val head = if (upstreams.size == 1) {
|
||||||
|
val upstream = upstreams.first()
|
||||||
|
upstream.setLag(0)
|
||||||
|
upstream.getHead()
|
||||||
|
} else {
|
||||||
|
val newHead = EthereumHeadMerge(upstreams.map { it.getHead() }).apply {
|
||||||
|
this.start()
|
||||||
|
}
|
||||||
|
val lagObserver = EthereumHeadLagObserver(newHead, upstreams).apply {
|
||||||
|
this.start()
|
||||||
|
}
|
||||||
|
this.lagObserver = lagObserver
|
||||||
|
newHead
|
||||||
|
}
|
||||||
|
onHeadUpdated(head)
|
||||||
|
return head
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getLabels(): Collection<UpstreamsConfig.Labels> {
|
||||||
|
return upstreams.flatMap { it.getLabels() }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun printStatus() {
|
||||||
|
var height: Long? = null
|
||||||
|
try {
|
||||||
|
height = getHead().getFlux().next().block(Duration.ofSeconds(1))?.number
|
||||||
|
} catch (e: IllegalStateException) {
|
||||||
|
//timout
|
||||||
|
} catch (e: Exception) {
|
||||||
|
log.warn("Head processing error: ${e.javaClass} ${e.message}")
|
||||||
|
}
|
||||||
|
val statuses = upstreams.map { it.getStatus() }
|
||||||
|
.groupBy { it }
|
||||||
|
.map { "${it.key.name}/${it.value.size}" }
|
||||||
|
.joinToString(",")
|
||||||
|
val lag = upstreams.map { it.getLag() }
|
||||||
|
.joinToString(", ")
|
||||||
|
|
||||||
|
log.info("State of ${chain.chainCode}: height=${height ?: '?'}, status=$statuses, lag=[$lag]")
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
override fun <T : Upstream<TA, BA>, TA : UpstreamApi, BA> cast(selfType: Class<T>, upstreamType: Class<TA>, blockType: Class<BA>): T {
|
||||||
|
if (!selfType.isAssignableFrom(this.javaClass)) {
|
||||||
|
throw ClassCastException("Cannot cast ${this.javaClass} to $selfType")
|
||||||
|
}
|
||||||
|
if (!upstreamType.isAssignableFrom(EthereumApi::class.java)) {
|
||||||
|
throw ClassCastException("Cannot cast ${EthereumApi::class.java} to $upstreamType")
|
||||||
|
}
|
||||||
|
if (!blockType.isAssignableFrom(BlockJson::class.java)) {
|
||||||
|
throw ClassCastException("Cannot cast ${BlockJson::class.java} to $blockType")
|
||||||
|
}
|
||||||
|
return this as T
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2020 ETCDEV GmbH
|
||||||
|
*
|
||||||
|
* 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.ethereum
|
||||||
|
|
||||||
|
import io.emeraldpay.dshackle.upstream.HeadLagObserver
|
||||||
|
import io.emeraldpay.dshackle.upstream.Upstream
|
||||||
|
import io.infinitape.etherjar.rpc.json.BlockJson
|
||||||
|
import io.infinitape.etherjar.rpc.json.TransactionRefJson
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import reactor.core.publisher.Flux
|
||||||
|
import java.time.Duration
|
||||||
|
|
||||||
|
class EthereumHeadLagObserver(
|
||||||
|
master: EthereumHead,
|
||||||
|
followers: Collection<Upstream<EthereumApi, BlockJson<TransactionRefJson>>>
|
||||||
|
) : HeadLagObserver<EthereumApi, BlockJson<TransactionRefJson>>(master, followers) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val log = LoggerFactory.getLogger(EthereumHeadLagObserver::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getCurrentBlocks(up: Upstream<EthereumApi, BlockJson<TransactionRefJson>>): Flux<BlockJson<TransactionRefJson>> {
|
||||||
|
val head = up.getHead()
|
||||||
|
return Flux.from(head.getFlux()).take(Duration.ofSeconds(1))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun extractDistance(top: BlockJson<TransactionRefJson>, curr: BlockJson<TransactionRefJson>): Long {
|
||||||
|
return when {
|
||||||
|
curr.number > top.number -> if (curr.totalDifficulty >= top.totalDifficulty) 0 else forkDistance(top, curr)
|
||||||
|
curr.number == top.number -> if (curr.totalDifficulty == top.totalDifficulty) 0 else forkDistance(top, curr)
|
||||||
|
else -> top.number - curr.number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,6 +23,8 @@ import io.emeraldpay.dshackle.upstream.*
|
|||||||
import io.emeraldpay.dshackle.upstream.calls.CallMethods
|
import io.emeraldpay.dshackle.upstream.calls.CallMethods
|
||||||
import io.emeraldpay.dshackle.upstream.calls.DirectCallMethods
|
import io.emeraldpay.dshackle.upstream.calls.DirectCallMethods
|
||||||
import io.emeraldpay.grpc.Chain
|
import io.emeraldpay.grpc.Chain
|
||||||
|
import io.infinitape.etherjar.rpc.json.BlockJson
|
||||||
|
import io.infinitape.etherjar.rpc.json.TransactionRefJson
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import org.springframework.context.Lifecycle
|
import org.springframework.context.Lifecycle
|
||||||
import reactor.core.Disposable
|
import reactor.core.Disposable
|
||||||
@@ -37,9 +39,9 @@ open class EthereumUpstream(
|
|||||||
private val options: UpstreamsConfig.Options,
|
private val options: UpstreamsConfig.Options,
|
||||||
val node: QuorumForLabels.QuorumItem,
|
val node: QuorumForLabels.QuorumItem,
|
||||||
private val targets: CallMethods
|
private val targets: CallMethods
|
||||||
): DefaultUpstream(), CachesEnabled, Lifecycle {
|
) : DefaultUpstream<EthereumApi, BlockJson<TransactionRefJson>>(), Upstream<EthereumApi, BlockJson<TransactionRefJson>>, CachesEnabled, Lifecycle {
|
||||||
|
|
||||||
constructor(id: String, chain: Chain, api: DirectEthereumApi): this(id, chain, api, null,
|
constructor(id: String, chain: Chain, api: DirectEthereumApi) : this(id, chain, api, null,
|
||||||
UpstreamsConfig.Options.getDefaults(), QuorumForLabels.QuorumItem(1, UpstreamsConfig.Labels()),
|
UpstreamsConfig.Options.getDefaults(), QuorumForLabels.QuorumItem(1, UpstreamsConfig.Labels()),
|
||||||
DirectCallMethods())
|
DirectCallMethods())
|
||||||
|
|
||||||
@@ -133,4 +135,18 @@ open class EthereumUpstream(
|
|||||||
return targets
|
return targets
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("unchecked")
|
||||||
|
override fun <T : Upstream<TA, BA>, TA : UpstreamApi, BA> cast(selfType: Class<T>, upstreamType: Class<TA>, blockType: Class<BA>): T {
|
||||||
|
if (!selfType.isAssignableFrom(this.javaClass)) {
|
||||||
|
throw ClassCastException("Cannot cast ${this.javaClass} to $selfType")
|
||||||
|
}
|
||||||
|
if (!upstreamType.isAssignableFrom(EthereumApi::class.java)) {
|
||||||
|
throw ClassCastException("Cannot cast ${EthereumApi::class.java} to $upstreamType")
|
||||||
|
}
|
||||||
|
if (!blockType.isAssignableFrom(BlockJson::class.java)) {
|
||||||
|
throw ClassCastException("Cannot cast ${BlockJson::class.java} to $blockType")
|
||||||
|
}
|
||||||
|
return this as T
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -30,6 +30,7 @@ import io.emeraldpay.dshackle.upstream.calls.CallMethods
|
|||||||
import io.emeraldpay.dshackle.upstream.calls.DirectCallMethods
|
import io.emeraldpay.dshackle.upstream.calls.DirectCallMethods
|
||||||
import io.emeraldpay.dshackle.upstream.ethereum.DefaultEthereumHead
|
import io.emeraldpay.dshackle.upstream.ethereum.DefaultEthereumHead
|
||||||
import io.emeraldpay.dshackle.upstream.ethereum.DirectEthereumApi
|
import io.emeraldpay.dshackle.upstream.ethereum.DirectEthereumApi
|
||||||
|
import io.emeraldpay.dshackle.upstream.ethereum.EthereumApi
|
||||||
import io.emeraldpay.dshackle.upstream.ethereum.EthereumHead
|
import io.emeraldpay.dshackle.upstream.ethereum.EthereumHead
|
||||||
import io.emeraldpay.grpc.Chain
|
import io.emeraldpay.grpc.Chain
|
||||||
import io.infinitape.etherjar.domain.BlockHash
|
import io.infinitape.etherjar.domain.BlockHash
|
||||||
@@ -51,16 +52,16 @@ import java.util.concurrent.atomic.AtomicReference
|
|||||||
import java.util.function.Function
|
import java.util.function.Function
|
||||||
import kotlin.collections.ArrayList
|
import kotlin.collections.ArrayList
|
||||||
|
|
||||||
open class GrpcUpstream(
|
open class EthereumGrpcUpstream(
|
||||||
private val parentId: String,
|
private val parentId: String,
|
||||||
private val chain: Chain,
|
private val chain: Chain,
|
||||||
private val blockchainStub: ReactorBlockchainGrpc.ReactorBlockchainStub,
|
private val blockchainStub: ReactorBlockchainGrpc.ReactorBlockchainStub,
|
||||||
private val objectMapper: ObjectMapper,
|
private val objectMapper: ObjectMapper,
|
||||||
private val rpcClient: ReactorEmeraldClient
|
private val rpcClient: ReactorEmeraldClient
|
||||||
): DefaultUpstream(), CachesEnabled, Lifecycle {
|
) : DefaultUpstream<EthereumApi, BlockJson<TransactionRefJson>>(), CachesEnabled, Lifecycle {
|
||||||
|
|
||||||
private var allLabels: Collection<UpstreamsConfig.Labels> = ArrayList<UpstreamsConfig.Labels>()
|
private var allLabels: Collection<UpstreamsConfig.Labels> = ArrayList<UpstreamsConfig.Labels>()
|
||||||
private val log = LoggerFactory.getLogger(GrpcUpstream::class.java)
|
private val log = LoggerFactory.getLogger(EthereumGrpcUpstream::class.java)
|
||||||
private var caches: Caches? = null
|
private var caches: Caches? = null
|
||||||
|
|
||||||
private val options = UpstreamsConfig.Options.getDefaults()
|
private val options = UpstreamsConfig.Options.getDefaults()
|
||||||
@@ -214,4 +215,18 @@ open class GrpcUpstream(
|
|||||||
this.caches = caches
|
this.caches = caches
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
override fun <T : Upstream<TA, BA>, TA : UpstreamApi, BA> cast(selfType: Class<T>, upstreamType: Class<TA>, blockType: Class<BA>): T {
|
||||||
|
if (!selfType.isAssignableFrom(this.javaClass)) {
|
||||||
|
throw ClassCastException("Cannot cast ${this.javaClass} to $selfType")
|
||||||
|
}
|
||||||
|
if (!upstreamType.isAssignableFrom(EthereumApi::class.java)) {
|
||||||
|
throw ClassCastException("Cannot cast ${EthereumApi::class.java} to $upstreamType")
|
||||||
|
}
|
||||||
|
if (!blockType.isAssignableFrom(BlockJson::class.java)) {
|
||||||
|
throw ClassCastException("Cannot cast ${BlockJson::class.java} to $blockType")
|
||||||
|
}
|
||||||
|
return this as T
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -18,10 +18,10 @@ package io.emeraldpay.dshackle.upstream.grpc
|
|||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
import io.emeraldpay.api.proto.BlockchainOuterClass
|
import io.emeraldpay.api.proto.BlockchainOuterClass
|
||||||
import io.emeraldpay.api.proto.ReactorBlockchainGrpc
|
import io.emeraldpay.api.proto.ReactorBlockchainGrpc
|
||||||
|
import io.emeraldpay.dshackle.BlockchainType
|
||||||
import io.emeraldpay.dshackle.Defaults
|
import io.emeraldpay.dshackle.Defaults
|
||||||
import io.emeraldpay.dshackle.FileResolver
|
import io.emeraldpay.dshackle.FileResolver
|
||||||
import io.emeraldpay.dshackle.config.AuthConfig
|
import io.emeraldpay.dshackle.config.AuthConfig
|
||||||
import io.emeraldpay.dshackle.config.UpstreamsConfig
|
|
||||||
import io.emeraldpay.dshackle.upstream.UpstreamAvailability
|
import io.emeraldpay.dshackle.upstream.UpstreamAvailability
|
||||||
import io.emeraldpay.dshackle.startup.UpstreamChange
|
import io.emeraldpay.dshackle.startup.UpstreamChange
|
||||||
import io.emeraldpay.grpc.Chain
|
import io.emeraldpay.grpc.Chain
|
||||||
@@ -54,7 +54,7 @@ class GrpcUpstreams(
|
|||||||
var timeout = Defaults.timeout
|
var timeout = Defaults.timeout
|
||||||
|
|
||||||
private var client: ReactorBlockchainGrpc.ReactorBlockchainStub? = null
|
private var client: ReactorBlockchainGrpc.ReactorBlockchainStub? = null
|
||||||
private val known = HashMap<Chain, GrpcUpstream>()
|
private val known = HashMap<Chain, EthereumGrpcUpstream>()
|
||||||
private val lock = ReentrantLock()
|
private val lock = ReentrantLock()
|
||||||
private var grpcTransport: ReactorEmeraldClient? = null
|
private var grpcTransport: ReactorEmeraldClient? = null
|
||||||
|
|
||||||
@@ -117,7 +117,7 @@ class GrpcUpstreams(
|
|||||||
}.map { chainDetails ->
|
}.map { chainDetails ->
|
||||||
val chain = Chain.byId(chainDetails.chain.number)
|
val chain = Chain.byId(chainDetails.chain.number)
|
||||||
val up = getOrCreate(chain)
|
val up = getOrCreate(chain)
|
||||||
(up.upstream as GrpcUpstream).init(chainDetails)
|
(up.upstream as EthereumGrpcUpstream).init(chainDetails)
|
||||||
up
|
up
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,10 +155,13 @@ class GrpcUpstreams(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getOrCreate(chain: Chain): UpstreamChange {
|
fun getOrCreate(chain: Chain): UpstreamChange {
|
||||||
|
if (BlockchainType.fromBlockchain(chain) != BlockchainType.ETHEREUM) {
|
||||||
|
throw IllegalArgumentException("Unsupported blockchain: $chain")
|
||||||
|
}
|
||||||
lock.withLock {
|
lock.withLock {
|
||||||
val current = known[chain]
|
val current = known[chain]
|
||||||
return if (current == null) {
|
return if (current == null) {
|
||||||
val created = GrpcUpstream(id, chain, client!!, objectMapper, grpcTransport!!.copyForChain(chain))
|
val created = EthereumGrpcUpstream(id, chain, client!!, objectMapper, grpcTransport!!.copyForChain(chain))
|
||||||
created.timeout = this.timeout
|
created.timeout = this.timeout
|
||||||
known[chain] = created
|
known[chain] = created
|
||||||
created.start()
|
created.start()
|
||||||
@@ -169,7 +172,7 @@ class GrpcUpstreams(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun get(chain: Chain): GrpcUpstream {
|
fun get(chain: Chain): EthereumGrpcUpstream {
|
||||||
return known[chain]!!
|
return known[chain]!!
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package io.emeraldpay.dshackle
|
||||||
|
|
||||||
|
import io.emeraldpay.grpc.Chain
|
||||||
|
import spock.lang.Specification
|
||||||
|
|
||||||
|
class BlockchainTypeSpec extends Specification {
|
||||||
|
|
||||||
|
def "Correct type for ethereum"() {
|
||||||
|
expect:
|
||||||
|
BlockchainType.fromBlockchain(chain) == BlockchainType.ETHEREUM
|
||||||
|
where:
|
||||||
|
chain << [Chain.ETHEREUM, Chain.ETHEREUM_CLASSIC, Chain.TESTNET_KOVAN, Chain.TESTNET_MORDEN]
|
||||||
|
}
|
||||||
|
|
||||||
|
def "Correct type for bitcoin"() {
|
||||||
|
expect:
|
||||||
|
BlockchainType.fromBlockchain(chain) == BlockchainType.BITCOIN
|
||||||
|
where:
|
||||||
|
chain << [Chain.BITCOIN, Chain.TESTNET_BITCOIN]
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -22,6 +22,7 @@ import io.emeraldpay.dshackle.test.EthereumUpstreamMock
|
|||||||
import io.emeraldpay.dshackle.test.UpstreamsMock
|
import io.emeraldpay.dshackle.test.UpstreamsMock
|
||||||
import io.emeraldpay.dshackle.upstream.ethereum.DirectEthereumApi
|
import io.emeraldpay.dshackle.upstream.ethereum.DirectEthereumApi
|
||||||
import io.emeraldpay.dshackle.upstream.Upstream
|
import io.emeraldpay.dshackle.upstream.Upstream
|
||||||
|
import io.emeraldpay.dshackle.upstream.ethereum.EthereumUpstream
|
||||||
import io.emeraldpay.grpc.Chain
|
import io.emeraldpay.grpc.Chain
|
||||||
import io.infinitape.etherjar.domain.BlockHash
|
import io.infinitape.etherjar.domain.BlockHash
|
||||||
import io.infinitape.etherjar.domain.TransactionId
|
import io.infinitape.etherjar.domain.TransactionId
|
||||||
@@ -38,7 +39,7 @@ class StreamHeadSpec extends Specification {
|
|||||||
|
|
||||||
def "Errors on unavailable chain"() {
|
def "Errors on unavailable chain"() {
|
||||||
setup:
|
setup:
|
||||||
def upstreams = new UpstreamsMock(Chain.ETHEREUM, Stub(Upstream))
|
def upstreams = new UpstreamsMock(Chain.ETHEREUM, Stub(EthereumUpstream))
|
||||||
def streamHead = new StreamHead(upstreams)
|
def streamHead = new StreamHead(upstreams)
|
||||||
when:
|
when:
|
||||||
def flux = streamHead.add(
|
def flux = streamHead.add(
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ import spock.lang.Specification
|
|||||||
|
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
|
|
||||||
class TrackAddressSpec extends Specification {
|
class TrackEthereumAddressSpec extends Specification {
|
||||||
|
|
||||||
def chain = Common.ChainRef.CHAIN_ETHEREUM
|
def chain = Common.ChainRef.CHAIN_ETHEREUM
|
||||||
def address1 = "0xe2c8fa8120d813cd0b5e6add120295bf20cfa09f"
|
def address1 = "0xe2c8fa8120d813cd0b5e6add120295bf20cfa09f"
|
||||||
@@ -59,7 +59,7 @@ class TrackAddressSpec extends Specification {
|
|||||||
def apiMock = TestingCommons.api(Stub(ReactorRpcClient))
|
def apiMock = TestingCommons.api(Stub(ReactorRpcClient))
|
||||||
def upstreamMock = TestingCommons.upstream(apiMock)
|
def upstreamMock = TestingCommons.upstream(apiMock)
|
||||||
Upstreams upstreams = new UpstreamsMock(Chain.ETHEREUM, upstreamMock)
|
Upstreams upstreams = new UpstreamsMock(Chain.ETHEREUM, upstreamMock)
|
||||||
TrackAddress trackAddress = new TrackAddress(upstreams, Schedulers.immediate())
|
TrackEthereumAddress trackAddress = new TrackEthereumAddress(upstreams, Schedulers.immediate())
|
||||||
trackAddress.init()
|
trackAddress.init()
|
||||||
|
|
||||||
apiMock.answer("eth_getBalance", ["0xe2c8fa8120d813cd0b5e6add120295bf20cfa09f", "latest"], "0x499602D2")
|
apiMock.answer("eth_getBalance", ["0xe2c8fa8120d813cd0b5e6add120295bf20cfa09f", "latest"], "0x499602D2")
|
||||||
@@ -101,7 +101,7 @@ class TrackAddressSpec extends Specification {
|
|||||||
def apiMock = TestingCommons.api(Stub(ReactorRpcClient))
|
def apiMock = TestingCommons.api(Stub(ReactorRpcClient))
|
||||||
def upstreamMock = TestingCommons.upstream(apiMock)
|
def upstreamMock = TestingCommons.upstream(apiMock)
|
||||||
Upstreams upstreams = new UpstreamsMock(Chain.ETHEREUM, upstreamMock)
|
Upstreams upstreams = new UpstreamsMock(Chain.ETHEREUM, upstreamMock)
|
||||||
TrackAddress trackAddress = new TrackAddress(upstreams, Schedulers.immediate())
|
TrackEthereumAddress trackAddress = new TrackEthereumAddress(upstreams, Schedulers.immediate())
|
||||||
trackAddress.init()
|
trackAddress.init()
|
||||||
|
|
||||||
apiMock.answerOnce("eth_getBalance", ["0xe2c8fa8120d813cd0b5e6add120295bf20cfa09f", "latest"], "0x499602D2")
|
apiMock.answerOnce("eth_getBalance", ["0xe2c8fa8120d813cd0b5e6add120295bf20cfa09f", "latest"], "0x499602D2")
|
||||||
@@ -36,7 +36,7 @@ import spock.lang.Specification
|
|||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
|
|
||||||
class TrackTxSpec extends Specification {
|
class TrackEthereumTxSpec extends Specification {
|
||||||
|
|
||||||
def chain = Common.ChainRef.CHAIN_ETHEREUM
|
def chain = Common.ChainRef.CHAIN_ETHEREUM
|
||||||
def txId = "0xba61ce4672751fd6086a9ac2b55547a5555af17535b6c0334ede2ecb6d64070a"
|
def txId = "0xba61ce4672751fd6086a9ac2b55547a5555af17535b6c0334ede2ecb6d64070a"
|
||||||
@@ -45,8 +45,8 @@ class TrackTxSpec extends Specification {
|
|||||||
def "Gives details for an old transaction"() {
|
def "Gives details for an old transaction"() {
|
||||||
setup:
|
setup:
|
||||||
def req = BlockchainOuterClass.TxStatusRequest.newBuilder()
|
def req = BlockchainOuterClass.TxStatusRequest.newBuilder()
|
||||||
.setChain(chain)
|
.setChain(chain)
|
||||||
.setConfirmationLimit(6)
|
.setConfirmationLimit(6)
|
||||||
.setTxId(txId)
|
.setTxId(txId)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
@@ -91,7 +91,7 @@ class TrackTxSpec extends Specification {
|
|||||||
def apiMock = TestingCommons.api(Stub(ReactorRpcClient))
|
def apiMock = TestingCommons.api(Stub(ReactorRpcClient))
|
||||||
def upstreamMock = TestingCommons.upstream(apiMock)
|
def upstreamMock = TestingCommons.upstream(apiMock)
|
||||||
Upstreams upstreams = new UpstreamsMock(Chain.ETHEREUM, upstreamMock)
|
Upstreams upstreams = new UpstreamsMock(Chain.ETHEREUM, upstreamMock)
|
||||||
TrackTx trackTx = new TrackTx(upstreams, Schedulers.immediate())
|
TrackEthereumTx trackTx = new TrackEthereumTx(upstreams, Schedulers.immediate())
|
||||||
trackTx.init()
|
trackTx.init()
|
||||||
|
|
||||||
apiMock.answer("eth_getTransactionByHash", [txId], txJson)
|
apiMock.answer("eth_getTransactionByHash", [txId], txJson)
|
||||||
@@ -123,7 +123,7 @@ class TrackTxSpec extends Specification {
|
|||||||
def apiMock = TestingCommons.api(Stub(ReactorRpcClient))
|
def apiMock = TestingCommons.api(Stub(ReactorRpcClient))
|
||||||
def upstreamMock = TestingCommons.upstream(apiMock)
|
def upstreamMock = TestingCommons.upstream(apiMock)
|
||||||
Upstreams upstreams = new UpstreamsMock(Chain.ETHEREUM, upstreamMock)
|
Upstreams upstreams = new UpstreamsMock(Chain.ETHEREUM, upstreamMock)
|
||||||
TrackTx trackTx = new TrackTx(upstreams, Schedulers.immediate())
|
TrackEthereumTx trackTx = new TrackEthereumTx(upstreams, Schedulers.immediate())
|
||||||
trackTx.init()
|
trackTx.init()
|
||||||
|
|
||||||
apiMock.answer("eth_getTransactionByHash", [txId], null)
|
apiMock.answer("eth_getTransactionByHash", [txId], null)
|
||||||
@@ -180,7 +180,7 @@ class TrackTxSpec extends Specification {
|
|||||||
def apiMock = TestingCommons.api(Stub(ReactorRpcClient))
|
def apiMock = TestingCommons.api(Stub(ReactorRpcClient))
|
||||||
def upstreamMock = TestingCommons.upstream(apiMock)
|
def upstreamMock = TestingCommons.upstream(apiMock)
|
||||||
Upstreams upstreams = new UpstreamsMock(Chain.ETHEREUM, upstreamMock)
|
Upstreams upstreams = new UpstreamsMock(Chain.ETHEREUM, upstreamMock)
|
||||||
TrackTx trackTx = new TrackTx(upstreams, Schedulers.immediate())
|
TrackEthereumTx trackTx = new TrackEthereumTx(upstreams, Schedulers.immediate())
|
||||||
trackTx.init()
|
trackTx.init()
|
||||||
|
|
||||||
apiMock.answerOnce("eth_getTransactionByHash", [txId], null)
|
apiMock.answerOnce("eth_getTransactionByHash", [txId], null)
|
||||||
@@ -279,7 +279,7 @@ class TrackTxSpec extends Specification {
|
|||||||
def apiMock = TestingCommons.api(Stub(ReactorRpcClient))
|
def apiMock = TestingCommons.api(Stub(ReactorRpcClient))
|
||||||
def upstreamMock = TestingCommons.upstream(apiMock)
|
def upstreamMock = TestingCommons.upstream(apiMock)
|
||||||
Upstreams upstreams = new UpstreamsMock(Chain.ETHEREUM, upstreamMock)
|
Upstreams upstreams = new UpstreamsMock(Chain.ETHEREUM, upstreamMock)
|
||||||
TrackTx trackTx = new TrackTx(upstreams, Schedulers.immediate())
|
TrackEthereumTx trackTx = new TrackEthereumTx(upstreams, Schedulers.immediate())
|
||||||
trackTx.init()
|
trackTx.init()
|
||||||
|
|
||||||
apiMock.answerOnce("eth_getTransactionByHash", [txId], null)
|
apiMock.answerOnce("eth_getTransactionByHash", [txId], null)
|
||||||
@@ -320,7 +320,7 @@ class TrackTxSpec extends Specification {
|
|||||||
def apiMock = TestingCommons.api(Stub(ReactorRpcClient))
|
def apiMock = TestingCommons.api(Stub(ReactorRpcClient))
|
||||||
def upstreamMock = TestingCommons.upstream(apiMock)
|
def upstreamMock = TestingCommons.upstream(apiMock)
|
||||||
Upstreams upstreams = new UpstreamsMock(Chain.ETHEREUM, upstreamMock)
|
Upstreams upstreams = new UpstreamsMock(Chain.ETHEREUM, upstreamMock)
|
||||||
TrackTx trackTx = new TrackTx(upstreams, Schedulers.immediate())
|
TrackEthereumTx trackTx = new TrackEthereumTx(upstreams, Schedulers.immediate())
|
||||||
trackTx.init()
|
trackTx.init()
|
||||||
|
|
||||||
def req = BlockchainOuterClass.TxStatusRequest.newBuilder()
|
def req = BlockchainOuterClass.TxStatusRequest.newBuilder()
|
||||||
@@ -341,7 +341,7 @@ class TrackTxSpec extends Specification {
|
|||||||
def apiMock = TestingCommons.api(Stub(ReactorRpcClient))
|
def apiMock = TestingCommons.api(Stub(ReactorRpcClient))
|
||||||
def upstreamMock = TestingCommons.upstream(apiMock)
|
def upstreamMock = TestingCommons.upstream(apiMock)
|
||||||
Upstreams upstreams = new UpstreamsMock(Chain.ETHEREUM, upstreamMock)
|
Upstreams upstreams = new UpstreamsMock(Chain.ETHEREUM, upstreamMock)
|
||||||
TrackTx trackTx = new TrackTx(upstreams, Schedulers.immediate())
|
TrackEthereumTx trackTx = new TrackEthereumTx(upstreams, Schedulers.immediate())
|
||||||
trackTx.init()
|
trackTx.init()
|
||||||
|
|
||||||
def req = BlockchainOuterClass.TxStatusRequest.newBuilder()
|
def req = BlockchainOuterClass.TxStatusRequest.newBuilder()
|
||||||
@@ -18,7 +18,7 @@ package io.emeraldpay.dshackle.test
|
|||||||
import io.emeraldpay.dshackle.config.UpstreamsConfig
|
import io.emeraldpay.dshackle.config.UpstreamsConfig
|
||||||
import io.emeraldpay.dshackle.upstream.calls.CallMethods
|
import io.emeraldpay.dshackle.upstream.calls.CallMethods
|
||||||
import io.emeraldpay.dshackle.startup.QuorumForLabels
|
import io.emeraldpay.dshackle.startup.QuorumForLabels
|
||||||
import io.emeraldpay.dshackle.upstream.calls.QuorumBasedMethods
|
import io.emeraldpay.dshackle.upstream.calls.DefaultEthereumMethods
|
||||||
import io.emeraldpay.dshackle.upstream.ethereum.DirectEthereumApi
|
import io.emeraldpay.dshackle.upstream.ethereum.DirectEthereumApi
|
||||||
import io.emeraldpay.dshackle.upstream.ethereum.EthereumHead
|
import io.emeraldpay.dshackle.upstream.ethereum.EthereumHead
|
||||||
import io.emeraldpay.dshackle.upstream.ethereum.EthereumUpstream
|
import io.emeraldpay.dshackle.upstream.ethereum.EthereumUpstream
|
||||||
@@ -33,11 +33,11 @@ class EthereumUpstreamMock extends EthereumUpstream {
|
|||||||
EthereumHeadMock ethereumHeadMock = new EthereumHeadMock()
|
EthereumHeadMock ethereumHeadMock = new EthereumHeadMock()
|
||||||
|
|
||||||
EthereumUpstreamMock(@NotNull Chain chain, @NotNull DirectEthereumApi api) {
|
EthereumUpstreamMock(@NotNull Chain chain, @NotNull DirectEthereumApi api) {
|
||||||
this(chain, api, new QuorumBasedMethods(TestingCommons.objectMapper(), chain))
|
this(chain, api, new DefaultEthereumMethods(TestingCommons.objectMapper(), chain))
|
||||||
}
|
}
|
||||||
|
|
||||||
EthereumUpstreamMock(@NotNull String id, @NotNull Chain chain, @NotNull DirectEthereumApi api) {
|
EthereumUpstreamMock(@NotNull String id, @NotNull Chain chain, @NotNull DirectEthereumApi api) {
|
||||||
this(id, chain, api, new QuorumBasedMethods(TestingCommons.objectMapper(), chain))
|
this(id, chain, api, new DefaultEthereumMethods(TestingCommons.objectMapper(), chain))
|
||||||
}
|
}
|
||||||
|
|
||||||
EthereumUpstreamMock(@NotNull Chain chain, @NotNull DirectEthereumApi api, CallMethods methods) {
|
EthereumUpstreamMock(@NotNull Chain chain, @NotNull DirectEthereumApi api, CallMethods methods) {
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import io.emeraldpay.dshackle.upstream.AggregatedUpstream
|
|||||||
import io.emeraldpay.dshackle.upstream.ChainUpstreams
|
import io.emeraldpay.dshackle.upstream.ChainUpstreams
|
||||||
import io.emeraldpay.dshackle.upstream.calls.DirectCallMethods
|
import io.emeraldpay.dshackle.upstream.calls.DirectCallMethods
|
||||||
import io.emeraldpay.dshackle.upstream.ethereum.DirectEthereumApi
|
import io.emeraldpay.dshackle.upstream.ethereum.DirectEthereumApi
|
||||||
|
import io.emeraldpay.dshackle.upstream.ethereum.EthereumChainUpstreams
|
||||||
import io.emeraldpay.dshackle.upstream.ethereum.EthereumUpstream
|
import io.emeraldpay.dshackle.upstream.ethereum.EthereumUpstream
|
||||||
import io.emeraldpay.grpc.Chain
|
import io.emeraldpay.grpc.Chain
|
||||||
import io.infinitape.etherjar.rpc.JacksonRpcConverter
|
import io.infinitape.etherjar.rpc.JacksonRpcConverter
|
||||||
@@ -75,7 +76,7 @@ class TestingCommons {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static AggregatedUpstream aggregatedUpstream(EthereumUpstream up) {
|
static AggregatedUpstream aggregatedUpstream(EthereumUpstream up) {
|
||||||
return new ChainUpstreams(Chain.ETHEREUM, [up], Caches.default(), objectMapper())
|
return new EthereumChainUpstreams(Chain.ETHEREUM, [up], Caches.default(), objectMapper())
|
||||||
}
|
}
|
||||||
|
|
||||||
static CachesFactory emptyCaches() {
|
static CachesFactory emptyCaches() {
|
||||||
|
|||||||
@@ -18,16 +18,17 @@ package io.emeraldpay.dshackle.test
|
|||||||
import io.emeraldpay.dshackle.cache.Caches
|
import io.emeraldpay.dshackle.cache.Caches
|
||||||
import io.emeraldpay.dshackle.upstream.AggregatedUpstream
|
import io.emeraldpay.dshackle.upstream.AggregatedUpstream
|
||||||
import io.emeraldpay.dshackle.upstream.ChainUpstreams
|
import io.emeraldpay.dshackle.upstream.ChainUpstreams
|
||||||
import io.emeraldpay.dshackle.upstream.calls.QuorumBasedMethods
|
import io.emeraldpay.dshackle.upstream.calls.DefaultEthereumMethods
|
||||||
import io.emeraldpay.dshackle.upstream.Upstream
|
import io.emeraldpay.dshackle.upstream.Upstream
|
||||||
import io.emeraldpay.dshackle.upstream.Upstreams
|
import io.emeraldpay.dshackle.upstream.Upstreams
|
||||||
|
import io.emeraldpay.dshackle.upstream.ethereum.EthereumChainUpstreams
|
||||||
import io.emeraldpay.grpc.Chain
|
import io.emeraldpay.grpc.Chain
|
||||||
import org.jetbrains.annotations.NotNull
|
import org.jetbrains.annotations.NotNull
|
||||||
import reactor.core.publisher.Flux
|
import reactor.core.publisher.Flux
|
||||||
|
|
||||||
class UpstreamsMock implements Upstreams {
|
class UpstreamsMock implements Upstreams {
|
||||||
|
|
||||||
private Map<Chain, QuorumBasedMethods> target = [:]
|
private Map<Chain, DefaultEthereumMethods> target = [:]
|
||||||
private Map<Chain, AggregatedUpstream> upstreams = [:]
|
private Map<Chain, AggregatedUpstream> upstreams = [:]
|
||||||
|
|
||||||
UpstreamsMock(Chain chain, Upstream up) {
|
UpstreamsMock(Chain chain, Upstream up) {
|
||||||
@@ -40,7 +41,7 @@ class UpstreamsMock implements Upstreams {
|
|||||||
|
|
||||||
AggregatedUpstream addUpstream(@NotNull Chain chain, @NotNull Upstream up) {
|
AggregatedUpstream addUpstream(@NotNull Chain chain, @NotNull Upstream up) {
|
||||||
if (!upstreams.containsKey(chain)) {
|
if (!upstreams.containsKey(chain)) {
|
||||||
upstreams[chain] = new ChainUpstreams(chain, [up], Caches.default(), TestingCommons.objectMapper())
|
upstreams[chain] = new EthereumChainUpstreams(chain, [up], Caches.default(), TestingCommons.objectMapper())
|
||||||
} else {
|
} else {
|
||||||
upstreams[chain].addUpstream(up)
|
upstreams[chain].addUpstream(up)
|
||||||
}
|
}
|
||||||
@@ -63,9 +64,9 @@ class UpstreamsMock implements Upstreams {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
QuorumBasedMethods getDefaultMethods(@NotNull Chain chain) {
|
DefaultEthereumMethods getDefaultMethods(@NotNull Chain chain) {
|
||||||
if (target[chain] == null) {
|
if (target[chain] == null) {
|
||||||
QuorumBasedMethods targets = new QuorumBasedMethods(TestingCommons.objectMapper(), chain)
|
DefaultEthereumMethods targets = new DefaultEthereumMethods(TestingCommons.objectMapper(), chain)
|
||||||
target[chain] = targets
|
target[chain] = targets
|
||||||
}
|
}
|
||||||
return target[chain]
|
return target[chain]
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import io.emeraldpay.dshackle.test.EthereumUpstreamMock
|
|||||||
import io.emeraldpay.dshackle.test.TestingCommons
|
import io.emeraldpay.dshackle.test.TestingCommons
|
||||||
import io.emeraldpay.dshackle.upstream.calls.DirectCallMethods
|
import io.emeraldpay.dshackle.upstream.calls.DirectCallMethods
|
||||||
import io.emeraldpay.dshackle.upstream.ethereum.DirectEthereumApi
|
import io.emeraldpay.dshackle.upstream.ethereum.DirectEthereumApi
|
||||||
|
import io.emeraldpay.dshackle.upstream.ethereum.EthereumChainUpstreams
|
||||||
import io.emeraldpay.grpc.Chain
|
import io.emeraldpay.grpc.Chain
|
||||||
import spock.lang.Specification
|
import spock.lang.Specification
|
||||||
|
|
||||||
@@ -30,7 +31,7 @@ class AggregatedUpstreamSpec extends Specification {
|
|||||||
setup:
|
setup:
|
||||||
def up1 = new EthereumUpstreamMock("test1", Chain.ETHEREUM, Stub(DirectEthereumApi), new DirectCallMethods(["eth_test1", "eth_test2"]))
|
def up1 = new EthereumUpstreamMock("test1", Chain.ETHEREUM, Stub(DirectEthereumApi), new DirectCallMethods(["eth_test1", "eth_test2"]))
|
||||||
def up2 = new EthereumUpstreamMock("test1", Chain.ETHEREUM, Stub(DirectEthereumApi), new DirectCallMethods(["eth_test2", "eth_test3"]))
|
def up2 = new EthereumUpstreamMock("test1", Chain.ETHEREUM, Stub(DirectEthereumApi), new DirectCallMethods(["eth_test2", "eth_test3"]))
|
||||||
def aggr = new ChainUpstreams(Chain.ETHEREUM, [up1, up2], Caches.default(), TestingCommons.objectMapper())
|
def aggr = new EthereumChainUpstreams(Chain.ETHEREUM, [up1, up2], Caches.default(), TestingCommons.objectMapper())
|
||||||
when:
|
when:
|
||||||
aggr.onUpstreamsUpdated()
|
aggr.onUpstreamsUpdated()
|
||||||
def act = aggr.getMethods()
|
def act = aggr.getMethods()
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import io.emeraldpay.dshackle.config.UpstreamsConfig
|
|||||||
import io.emeraldpay.dshackle.startup.QuorumForLabels
|
import io.emeraldpay.dshackle.startup.QuorumForLabels
|
||||||
import io.emeraldpay.dshackle.test.EthereumApiStub
|
import io.emeraldpay.dshackle.test.EthereumApiStub
|
||||||
import io.emeraldpay.dshackle.test.TestingCommons
|
import io.emeraldpay.dshackle.test.TestingCommons
|
||||||
import io.emeraldpay.dshackle.upstream.calls.QuorumBasedMethods
|
import io.emeraldpay.dshackle.upstream.calls.DefaultEthereumMethods
|
||||||
import io.emeraldpay.dshackle.upstream.ethereum.DirectEthereumApi
|
import io.emeraldpay.dshackle.upstream.ethereum.DirectEthereumApi
|
||||||
import io.emeraldpay.dshackle.upstream.ethereum.EthereumUpstream
|
import io.emeraldpay.dshackle.upstream.ethereum.EthereumUpstream
|
||||||
import io.emeraldpay.dshackle.upstream.ethereum.EthereumWs
|
import io.emeraldpay.dshackle.upstream.ethereum.EthereumWs
|
||||||
@@ -35,7 +35,7 @@ class FilteredApisSpec extends Specification {
|
|||||||
|
|
||||||
def rpcClient = Stub(ReactorRpcClient)
|
def rpcClient = Stub(ReactorRpcClient)
|
||||||
def objectMapper = TestingCommons.objectMapper()
|
def objectMapper = TestingCommons.objectMapper()
|
||||||
def ethereumTargets = new QuorumBasedMethods(objectMapper, Chain.ETHEREUM)
|
def ethereumTargets = new DefaultEthereumMethods(objectMapper, Chain.ETHEREUM)
|
||||||
|
|
||||||
def "Verifies labels"() {
|
def "Verifies labels"() {
|
||||||
setup:
|
setup:
|
||||||
|
|||||||
@@ -13,9 +13,10 @@
|
|||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package io.emeraldpay.dshackle.upstream
|
package io.emeraldpay.dshackle.upstream.ethereum
|
||||||
|
|
||||||
import io.emeraldpay.dshackle.upstream.ethereum.EthereumHead
|
import io.emeraldpay.dshackle.upstream.HeadLagObserver
|
||||||
|
import io.emeraldpay.dshackle.upstream.Upstream
|
||||||
import io.infinitape.etherjar.rpc.json.BlockJson
|
import io.infinitape.etherjar.rpc.json.BlockJson
|
||||||
import reactor.core.publisher.Flux
|
import reactor.core.publisher.Flux
|
||||||
import reactor.core.publisher.TopicProcessor
|
import reactor.core.publisher.TopicProcessor
|
||||||
@@ -25,7 +26,7 @@ import spock.lang.Specification
|
|||||||
|
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
|
|
||||||
class HeadLagObserverSpec extends Specification {
|
class EthereumHeadLagObserverSpec extends Specification {
|
||||||
|
|
||||||
def "Updates lag distance"() {
|
def "Updates lag distance"() {
|
||||||
setup:
|
setup:
|
||||||
@@ -64,7 +65,7 @@ class HeadLagObserverSpec extends Specification {
|
|||||||
1 * up2.setLag(1)
|
1 * up2.setLag(1)
|
||||||
1 * up2.setLag(0)
|
1 * up2.setLag(0)
|
||||||
|
|
||||||
HeadLagObserver observer = new HeadLagObserver(master, [up1, up2])
|
HeadLagObserver observer = new EthereumHeadLagObserver(master, [up1, up2])
|
||||||
when:
|
when:
|
||||||
def act = observer.subscription().take(Duration.ofMillis(1200))
|
def act = observer.subscription().take(Duration.ofMillis(1200))
|
||||||
|
|
||||||
@@ -78,7 +79,7 @@ class HeadLagObserverSpec extends Specification {
|
|||||||
def "Probes until there is no difference"() {
|
def "Probes until there is no difference"() {
|
||||||
setup:
|
setup:
|
||||||
EthereumHead master = Mock()
|
EthereumHead master = Mock()
|
||||||
HeadLagObserver observer = new HeadLagObserver(master, [])
|
HeadLagObserver observer = new EthereumHeadLagObserver(master, [])
|
||||||
Upstream up = Mock()
|
Upstream up = Mock()
|
||||||
|
|
||||||
def blocks = [100, 101, 102].collect { i ->
|
def blocks = [100, 101, 102].collect { i ->
|
||||||
@@ -103,7 +104,7 @@ class HeadLagObserverSpec extends Specification {
|
|||||||
def "Correct distance"() {
|
def "Correct distance"() {
|
||||||
setup:
|
setup:
|
||||||
EthereumHead master = Mock()
|
EthereumHead master = Mock()
|
||||||
HeadLagObserver observer = new HeadLagObserver(master, [])
|
HeadLagObserver observer = new EthereumHeadLagObserver(master, [])
|
||||||
expect:
|
expect:
|
||||||
def top = new BlockJson().with {
|
def top = new BlockJson().with {
|
||||||
it.number = topHeight
|
it.number = topHeight
|
||||||
@@ -34,7 +34,7 @@ import spock.lang.Specification
|
|||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
import java.util.concurrent.CompletableFuture
|
import java.util.concurrent.CompletableFuture
|
||||||
|
|
||||||
class GrpcUpstreamSpec extends Specification {
|
class EthereumGrpcUpstreamSpec extends Specification {
|
||||||
|
|
||||||
MockServer mockServer = new MockServer()
|
MockServer mockServer = new MockServer()
|
||||||
ObjectMapper objectMapper = TestingCommons.objectMapper()
|
ObjectMapper objectMapper = TestingCommons.objectMapper()
|
||||||
@@ -70,7 +70,7 @@ class GrpcUpstreamSpec extends Specification {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
def transport = ReactorEmeraldClient.newBuilder().connectUsing(client.channel).build()
|
def transport = ReactorEmeraldClient.newBuilder().connectUsing(client.channel).build()
|
||||||
def upstream = new GrpcUpstream("test", chain, client, objectMapper, transport)
|
def upstream = new EthereumGrpcUpstream("test", chain, client, objectMapper, transport)
|
||||||
upstream.setLag(0)
|
upstream.setLag(0)
|
||||||
upstream.init(BlockchainOuterClass.DescribeChain.newBuilder()
|
upstream.init(BlockchainOuterClass.DescribeChain.newBuilder()
|
||||||
.addAllSupportedMethods(["eth_getBlockByHash"])
|
.addAllSupportedMethods(["eth_getBlockByHash"])
|
||||||
@@ -126,7 +126,7 @@ class GrpcUpstreamSpec extends Specification {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
def transport = ReactorEmeraldClient.newBuilder().connectUsing(client.channel).build()
|
def transport = ReactorEmeraldClient.newBuilder().connectUsing(client.channel).build()
|
||||||
def upstream = new GrpcUpstream("test", Chain.ETHEREUM, client, objectMapper, transport)
|
def upstream = new EthereumGrpcUpstream("test", Chain.ETHEREUM, client, objectMapper, transport)
|
||||||
upstream.setLag(0)
|
upstream.setLag(0)
|
||||||
upstream.init(BlockchainOuterClass.DescribeChain.newBuilder()
|
upstream.init(BlockchainOuterClass.DescribeChain.newBuilder()
|
||||||
.addAllSupportedMethods(["eth_getBlockByHash"])
|
.addAllSupportedMethods(["eth_getBlockByHash"])
|
||||||
@@ -186,7 +186,7 @@ class GrpcUpstreamSpec extends Specification {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
def transport = ReactorEmeraldClient.newBuilder().connectUsing(client.channel).build()
|
def transport = ReactorEmeraldClient.newBuilder().connectUsing(client.channel).build()
|
||||||
def upstream = new GrpcUpstream("test", chain, client, objectMapper, transport)
|
def upstream = new EthereumGrpcUpstream("test", chain, client, objectMapper, transport)
|
||||||
upstream.setLag(0)
|
upstream.setLag(0)
|
||||||
upstream.init(BlockchainOuterClass.DescribeChain.newBuilder()
|
upstream.init(BlockchainOuterClass.DescribeChain.newBuilder()
|
||||||
.addAllSupportedMethods(["eth_getBlockByHash"])
|
.addAllSupportedMethods(["eth_getBlockByHash"])
|
||||||
Reference in New Issue
Block a user