ws subscription refactoring

added support of pending transactions subscription
This commit is contained in:
a10zn8
2022-12-21 17:44:56 +04:00
parent 7bc4acb376
commit e0fdfa4420
64 changed files with 2051 additions and 310 deletions

View File

@@ -25,8 +25,10 @@ import io.emeraldpay.dshackle.upstream.bitcoin.data.EsploraUnspent
import io.emeraldpay.dshackle.upstream.bitcoin.data.EsploraUnspentDeserializer
import io.emeraldpay.dshackle.upstream.bitcoin.data.RpcUnspent
import io.emeraldpay.dshackle.upstream.bitcoin.data.RpcUnspentDeserializer
import io.emeraldpay.dshackle.upstream.ethereum.subscribe.json.TransactionIdSerializer
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
import io.emeraldpay.etherjar.domain.TransactionId
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.TimeZone
@@ -84,6 +86,7 @@ class Global {
private fun createObjectMapper(): ObjectMapper {
val module = SimpleModule("EmeraldDshackle", Version(1, 0, 0, null, null, null))
module.addSerializer(JsonRpcResponse::class.java, JsonRpcResponse.ResponseJsonSerializer())
module.addSerializer(TransactionId::class.java, TransactionIdSerializer())
module.addDeserializer(EsploraUnspent::class.java, EsploraUnspentDeserializer())
module.addDeserializer(RpcUnspent::class.java, RpcUnspentDeserializer())

View File

@@ -0,0 +1,101 @@
package io.emeraldpay.dshackle.commons
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.util.backoff.BackOff
import org.springframework.util.backoff.BackOffExecution
import org.springframework.util.backoff.ExponentialBackOff
import org.springframework.util.backoff.FixedBackOff
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
import java.time.Duration
/**
* A flux holder that reconnects to it on failure taking into account a back off strategy
*/
class DurableFlux<T>(
private val provider: () -> Flux<T>,
private val errorBackOff: BackOff,
private val log: Logger,
) {
companion object {
private val defaultLog = LoggerFactory.getLogger(DurableFlux::class.java)
@JvmStatic
fun newBuilder(): Builder<*> {
return Builder<Any>()
}
}
private var messagesSinceStart = 0
private var errorBackOffExecution = errorBackOff.start()
fun connect(): Flux<T> {
return provider.invoke()
.doOnNext {
if (messagesSinceStart == 0) {
errorBackOffExecution = errorBackOff.start()
}
messagesSinceStart++
}
.doOnSubscribe {
messagesSinceStart = 0
}
.onErrorResume { t ->
val backoff = errorBackOffExecution.nextBackOff()
if (backoff != BackOffExecution.STOP) {
log.warn("Connection closed with ${t.message}. Reconnecting in ${backoff}ms")
connect().delaySubscription(Duration.ofMillis(backoff))
} else {
log.warn("Connection closed with ${t.message}. Not reconnecting")
Mono.error(t)
}
}
}
class Builder<T> {
private var provider: (() -> Flux<T>)? = null
protected var errorBackOff: BackOff = FixedBackOff(1_000, Long.MAX_VALUE)
protected var log: Logger = DurableFlux.defaultLog
@Suppress("UNCHECKED_CAST")
fun <X> using(provider: () -> Flux<X>): Builder<X> {
this.provider = provider as () -> Flux<T>
return this as Builder<X>
}
fun backoffOnError(time: Duration): Builder<T> {
errorBackOff = FixedBackOff(time.toMillis(), Long.MAX_VALUE)
return this
}
fun backoffOnError(time: Duration, multiplier: Double, max: Duration? = null): Builder<T> {
errorBackOff = ExponentialBackOff(time.toMillis(), multiplier).also {
if (max != null) {
it.maxInterval = max.toMillis()
}
}
return this
}
fun backoffOnError(backOff: BackOff): Builder<T> {
errorBackOff = backOff
return this
}
fun logTo(log: Logger): Builder<T> {
this.log = log
return this
}
fun build(): DurableFlux<T> {
if (provider == null) {
throw IllegalStateException("No provider for original Flux")
}
return DurableFlux(provider!!, errorBackOff, log)
}
}
}

View File

@@ -0,0 +1,152 @@
package io.emeraldpay.dshackle.commons
import org.apache.commons.collections4.iterators.UnmodifiableIterator
import org.slf4j.LoggerFactory
import java.time.Duration
import java.util.LinkedList
import java.util.TreeSet
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock
/**
* A naive implementation of a Set with a limit for elements and an expiration time. Supposed to be used a filter for uniqueness.
* Internally it uses a TreeSet and a journal of added elements, which is used ot remove elements when they expire or the list grows too large.
* It's tread safe, but may be suboptimal to use in multithreaded scenario because of internal locks.
*/
class ExpiringSet<T>(
ttl: Duration,
comparator: Comparator<T>,
val limit: Int,
) : MutableSet<T> {
companion object {
private val log = LoggerFactory.getLogger(ExpiringSet::class.java)
}
private val tree = TreeSet<T>(comparator)
private val lock = ReentrantLock()
private val journal = LinkedList<JournalItem<T>>()
private var count = 0
private val ttl = ttl.toMillis()
data class JournalItem<T>(
val since: Long = System.currentTimeMillis(),
val value: T
) {
fun isExpired(ttl: Long): Boolean {
return System.currentTimeMillis() > since + ttl
}
}
override val size: Int
get() = count
override fun clear() {
lock.withLock {
tree.clear()
journal.clear()
count = 0
}
}
override fun addAll(elements: Collection<T>): Boolean {
var changed = false
elements.forEach {
changed = changed || add(it)
}
return changed
}
override fun add(element: T): Boolean {
lock.withLock {
val added = tree.add(element)
if (added) {
journal.offer(JournalItem(value = element))
count++
shrink()
}
return added
}
}
override fun isEmpty(): Boolean {
return count == 0
}
override fun iterator(): MutableIterator<T> {
// not mutable
return UnmodifiableIterator.unmodifiableIterator(tree.iterator())
}
override fun retainAll(elements: Collection<T>): Boolean {
lock.withLock {
var changed = false
val iter = tree.iterator()
while (iter.hasNext()) {
val next = iter.next()
if (!elements.contains(next)) {
changed = true
iter.remove()
count--
}
}
return changed
}
}
override fun removeAll(elements: Collection<T>): Boolean {
var changed = false
elements.forEach {
changed = changed || remove(it)
}
return changed
}
override fun remove(element: T): Boolean {
lock.withLock {
val changed = tree.remove(element)
if (changed) {
count--
}
return changed
}
}
override fun containsAll(elements: Collection<T>): Boolean {
return elements.all { contains(it) }
}
override fun contains(element: T): Boolean {
lock.withLock {
return tree.contains(element)
}
}
fun shrink() {
lock.withLock {
val iter = journal.iterator()
val removeAtLeast = (count - limit).coerceAtLeast(0)
var removed = 0
var stop = false
while (!stop && iter.hasNext()) {
val next = iter.next()
val overflow = removeAtLeast > removed
val expired = next.isExpired(ttl)
if (overflow || expired) {
iter.remove()
if (tree.remove(next.value)) {
removed++
}
}
// we always delete expired elements so don't stop on that
if (!expired) {
// but if we already deleted all non-expired element (i.e., started because it grew too large)
// then we stop as soon as we don't have any overflow
stop = !overflow
}
}
count -= removed
}
}
}

View File

@@ -0,0 +1,67 @@
package io.emeraldpay.dshackle.commons
import org.slf4j.LoggerFactory
import reactor.core.publisher.Flux
import java.util.concurrent.atomic.AtomicLong
import java.util.concurrent.locks.ReentrantReadWriteLock
import kotlin.concurrent.read
import kotlin.concurrent.write
/**
* A flux holder that that creates it only if requested. Keeps it for the following calls, so all the following calls will
* reuse it. Forgets as soon as it completes/cancelled, so it will be recreated again if needed.
*/
class SharedFluxHolder<T>(
/**
* Provider for the flux. Note that it can be called multiple times but only one is used at the same time.
* I.e., if there is a few calls because of a thread-race only one is kept.
* But once it's completed a new one may be created if requested.
*/
private val provider: () -> Flux<T>
) {
companion object {
private val log = LoggerFactory.getLogger(SharedFluxHolder::class.java)
}
private val ids = AtomicLong()
private val lock = ReentrantReadWriteLock()
private var current: Holder<T>? = null
fun get(): Flux<T> {
lock.read {
if (current != null) {
return current!!.flux
}
}
// The following doesn't consume resources because it's just create a Flux without actual subscription
// So even for the case of a thread race it's okay to create many. B/c only one is going to be kept as `current` and subscribed
val id = ids.incrementAndGet()
val created = Holder(
provider.invoke()
.share()
.doFinally { onClose(id) },
id
)
lock.write {
if (current != null) {
return current!!.flux
}
current = created
}
return created.flux
}
private fun onClose(id: Long) {
lock.write {
if (current?.id == id) {
current = null
}
}
}
data class Holder<T>(
val flux: Flux<T>,
val id: Long,
)
}

View File

@@ -69,7 +69,7 @@ open class NativeSubscribe(
* If not possible - performs subscription logic on the current instance
* @see EthereumLikeMultistream.tryProxy
*/
val publisher = getUpstream(chain)?.tryProxy(matcher, request) ?: run {
val publisher = getUpstream(chain).tryProxy(matcher, request) ?: run {
val method = request.method
val params: Any? = request.payload?.takeIf { !it.isEmpty }?.let {
objectMapper.readValue(it.newInput(), Map::class.java)
@@ -97,12 +97,10 @@ open class NativeSubscribe(
}
open fun subscribe(chain: Chain, method: String, params: Any?, matcher: Selector.Matcher): Flux<out Any> =
getUpstream(chain)?.getSubscribe()?.subscribe(method, params, matcher)
?: Flux.error(SilentException.UnsupportedBlockchain(chain))
getUpstream(chain).getSubscriptionApi().subscribe(method, params, matcher)
private fun getUpstream(chain: Chain): EthereumLikeMultistream? =
multistreamHolder.getUpstream(chain)
?.let { it as EthereumLikeMultistream }
private fun getUpstream(chain: Chain): EthereumLikeMultistream =
multistreamHolder.getUpstream(chain).let { it as EthereumLikeMultistream }
fun convertToProto(holder: ResponseHolder): NativeSubscribeReplyItem {
if (holder.response is NativeSubscribeReplyItem) {

View File

@@ -88,12 +88,11 @@ class TrackERC20Address(
val asset = request.asset.code.lowercase(Locale.getDefault())
val tokenDefinition = tokens[TokenId(chain, asset)] ?: return Flux.empty()
val logs = getUpstream(chain)
.getSubscribe().logs
.start(
.getSubscriptionApi().logs
.create(
listOf(tokenDefinition.token.contract),
listOf(EventId.fromSignature("Transfer", "address", "address", "uint256")),
Selector.empty
)
).connect(Selector.empty)
return ethereumAddresses.extract(request.address)
.map { TrackedAddress(chain, it, tokenDefinition.token, tokenDefinition.name) }

View File

@@ -11,6 +11,7 @@ interface BlockValidator {
}
companion object {
@JvmField
val ALWAYS_VALID = AlwaysValid()
}
}

View File

@@ -0,0 +1,27 @@
/**
* Copyright (c) 2022 EmeraldPay, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.emeraldpay.dshackle.upstream
open class NoUpstreamSubscriptions : UpstreamSubscriptions {
companion object {
val DEFAULT = NoUpstreamSubscriptions()
}
override fun <T> get(method: String): SubscriptionConnect<T>? {
return null
}
}

View File

@@ -0,0 +1,26 @@
/**
* Copyright (c) 2022 EmeraldPay, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.emeraldpay.dshackle.upstream
import reactor.core.publisher.Flux
/**
* Note that T is supposed to be serializable as JSON
*/
interface SubscriptionConnect<T> {
fun connect(matcher: Selector.Matcher): Flux<T>
}

View File

@@ -0,0 +1,24 @@
/**
* Copyright (c) 2022 EmeraldPay, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.emeraldpay.dshackle.upstream
/**
* Subscriptions available on the current upstream
*/
interface UpstreamSubscriptions {
fun <T> get(method: String): SubscriptionConnect<T>?
}

View File

@@ -29,7 +29,7 @@ import org.slf4j.LoggerFactory
import reactor.core.publisher.Mono
open class DefaultEthereumHead(
private val upstreamId: String,
protected val upstreamId: String,
forkChoice: ForkChoice,
blockValidator: BlockValidator
) : Head, AbstractHead(forkChoice, blockValidator, 60_000, upstreamId) {

View File

@@ -8,7 +8,7 @@ import reactor.core.publisher.Flux
interface EthereumLikeMultistream : Upstream {
fun getReader(): EthereumCachingReader
fun getSubscribe(): EthereumSubscribe
fun getSubscriptionApi(): EthereumSubscriptionApi
fun getHead(mather: Selector.Matcher): Head

View File

@@ -23,6 +23,9 @@ import io.emeraldpay.dshackle.config.UpstreamsConfig
import io.emeraldpay.dshackle.reader.Reader
import io.emeraldpay.dshackle.upstream.*
import io.emeraldpay.dshackle.upstream.Lifecycle
import io.emeraldpay.dshackle.upstream.ethereum.subscribe.AggregatedPendingTxes
import io.emeraldpay.dshackle.upstream.ethereum.subscribe.NoPendingTxes
import io.emeraldpay.dshackle.upstream.ethereum.subscribe.PendingTxesSource
import io.emeraldpay.dshackle.upstream.forkchoice.MostWorkForkChoice
import io.emeraldpay.dshackle.upstream.grpc.GrpcUpstream
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
@@ -48,7 +51,7 @@ open class EthereumMultistream(
ConcurrentReferenceHashMap(16, ConcurrentReferenceHashMap.ReferenceType.WEAK)
private val reader: EthereumCachingReader = EthereumCachingReader(this, this.caches, getMethodsFactory())
private val subscribe = EthereumSubscribe(this)
private var subscribe = EthereumSubscriptionApi(this, NoPendingTxes())
private val supportsEIP1559 = when (chain) {
Chain.ETHEREUM, Chain.TESTNET_ROPSTEN, Chain.TESTNET_GOERLI, Chain.TESTNET_RINKEBY -> true
@@ -68,6 +71,24 @@ open class EthereumMultistream(
super.init()
}
override fun onUpstreamsUpdated() {
super.onUpstreamsUpdated()
val pendingTxes: PendingTxesSource = upstreams
.mapNotNull {
it.getUpstreamSubscriptions().getPendingTxes()
}.let {
if (it.isEmpty()) {
NoPendingTxes()
} else if (it.size == 1) {
it.first()
} else {
AggregatedPendingTxes(it)
}
}
subscribe = EthereumSubscriptionApi(this, pendingTxes)
}
override fun start() {
super.start()
reader.start()
@@ -158,7 +179,7 @@ open class EthereumMultistream(
return Mono.just(LocalCallRouter(reader, getMethods(), getHead(), localEnabled))
}
override fun getSubscribe(): EthereumSubscribe {
override fun getSubscriptionApi(): EthereumSubscriptionApi {
return subscribe
}

View File

@@ -69,6 +69,11 @@ open class EthereumRpcUpstream(
.subscribe(this::setStatus)
}
}
override fun getUpstreamSubscriptions(): EthereumUpstreamSubscriptions {
return connector.getUpstreamSubscriptions()
}
override fun getHead(): Head {
return connector.getHead()
}

View File

@@ -4,17 +4,24 @@ import io.emeraldpay.dshackle.upstream.Selector
import io.emeraldpay.dshackle.upstream.ethereum.subscribe.ConnectLogs
import io.emeraldpay.dshackle.upstream.ethereum.subscribe.ConnectNewHeads
import io.emeraldpay.dshackle.upstream.ethereum.subscribe.ConnectSyncing
import io.emeraldpay.dshackle.upstream.ethereum.subscribe.PendingTxesSource
import io.emeraldpay.etherjar.domain.Address
import io.emeraldpay.etherjar.hex.Hex32
import org.slf4j.LoggerFactory
import reactor.core.publisher.Flux
open class EthereumSubscribe(
val upstream: EthereumLikeMultistream
open class EthereumSubscriptionApi(
val upstream: EthereumLikeMultistream,
val pendingTxesSource: PendingTxesSource
) {
companion object {
private val log = LoggerFactory.getLogger(EthereumSubscribe::class.java)
private val log = LoggerFactory.getLogger(EthereumSubscriptionApi::class.java)
const val METHOD_NEW_HEADS = "newHeads"
const val METHOD_LOGS = "logs"
const val METHOD_SYNCING = "syncing"
const val METHOD_PENDING_TXES = "newPendingTransactions"
}
private val newHeads = ConnectNewHeads(upstream)
@@ -23,10 +30,10 @@ open class EthereumSubscribe(
@Suppress("UNCHECKED_CAST")
open fun subscribe(method: String, params: Any?, matcher: Selector.Matcher): Flux<out Any> {
if (method == "newHeads") {
if (method == METHOD_NEW_HEADS) {
return newHeads.connect(matcher)
}
if (method == "logs") {
if (method == METHOD_LOGS) {
val paramsMap = try {
if (params != null && Map::class.java.isAssignableFrom(params.javaClass)) {
readLogsRequest(params as Map<String, Any?>)
@@ -36,10 +43,13 @@ open class EthereumSubscribe(
} catch (t: Throwable) {
return Flux.error(UnsupportedOperationException("Invalid parameter for $method. Error: ${t.message}"))
}
return logs.start(paramsMap.address, paramsMap.topics, matcher)
return logs.create(paramsMap.address, paramsMap.topics).connect(matcher)
}
if (method == "syncing") {
return syncing.connect()
if (method == METHOD_SYNCING) {
return syncing.connect(matcher)
}
if (method == METHOD_PENDING_TXES) {
return pendingTxesSource.connect(matcher)
}
return Flux.error(UnsupportedOperationException("Method $method is not supported"))
}

View File

@@ -44,4 +44,6 @@ abstract class EthereumUpstream(
override fun getLabels(): Collection<UpstreamsConfig.Labels> {
return node?.let { listOf(it.labels) } ?: emptyList()
}
abstract fun getUpstreamSubscriptions(): EthereumUpstreamSubscriptions
}

View File

@@ -0,0 +1,24 @@
/**
* Copyright (c) 2022 EmeraldPay, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.emeraldpay.dshackle.upstream.ethereum
import io.emeraldpay.dshackle.upstream.UpstreamSubscriptions
import io.emeraldpay.dshackle.upstream.ethereum.subscribe.PendingTxesSource
interface EthereumUpstreamSubscriptions : UpstreamSubscriptions {
fun getPendingTxes(): PendingTxesSource?
}

View File

@@ -58,11 +58,11 @@ class EthereumWsFactory(
)
}
fun create(upstream: DefaultUpstream?, validator: EthereumUpstreamValidator?): WsConnection {
fun create(upstream: DefaultUpstream?): WsConnectionImpl {
require(upstream == null || upstream.getId() == id) {
"Creating instance for different upstream. ${upstream?.getId()} != id"
}
return WsConnection(id, uri, origin, basicAuth, metrics, upstream, validator).also { ws ->
return WsConnectionImpl(id, uri, origin, basicAuth, metrics, upstream).also { ws ->
config?.frameSize?.let {
ws.frameSize = it
}

View File

@@ -16,19 +16,32 @@
*/
package io.emeraldpay.dshackle.upstream.ethereum
import io.emeraldpay.dshackle.Defaults
import io.emeraldpay.dshackle.Global
import io.emeraldpay.dshackle.SilentException
import io.emeraldpay.dshackle.data.BlockContainer
import io.emeraldpay.dshackle.reader.Reader
import io.emeraldpay.dshackle.upstream.BlockValidator
import io.emeraldpay.dshackle.upstream.Lifecycle
import io.emeraldpay.dshackle.upstream.forkchoice.ForkChoice
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcWsClient
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
import io.emeraldpay.etherjar.rpc.json.BlockJson
import io.emeraldpay.etherjar.rpc.json.TransactionRefJson
import org.slf4j.LoggerFactory
import reactor.core.Disposable
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
import reactor.core.scheduler.Schedulers
import reactor.retry.Repeat
import java.time.Duration
class EthereumWsHead(
private val ws: WsConnection,
upstreamId: String,
forkChoice: ForkChoice,
blockValidator: BlockValidator
blockValidator: BlockValidator,
private val api: Reader<JsonRpcRequest, JsonRpcResponse>,
private val wsSubscriptions: WsSubscriptions,
) : DefaultEthereumHead(upstreamId, forkChoice, blockValidator), Lifecycle {
private val log = LoggerFactory.getLogger(EthereumWsHead::class.java)
@@ -44,12 +57,53 @@ class EthereumWsHead(
this.subscription?.dispose()
val heads = Flux.merge(
// get the current block, not just wait for the next update
getLatestBlock(JsonRpcWsClient(ws)),
ws.getBlocksFlux()
getLatestBlock(api),
listenNewHeads()
)
this.subscription = super.follow(heads)
}
fun listenNewHeads(): Flux<BlockContainer> {
return wsSubscriptions.subscribe("newHeads")
.map {
Global.objectMapper.readValue(it, BlockJson::class.java) as BlockJson<TransactionRefJson>
}
.flatMap { block ->
// newHeads returns incomplete blocks, i.e. without some fields and without transaction hashes,
// so we need to fetch the full block data
if (block.difficulty == null || block.transactions == null) {
// TODO do we really need this ?
enhanceRealBlock(block)
} else {
Mono.just(BlockContainer.from(block))
}
}
}
fun enhanceRealBlock(block: BlockJson<TransactionRefJson>): Mono<BlockContainer> {
return Mono.just(block.hash)
.flatMap { hash ->
api.read(JsonRpcRequest("eth_getBlockByHash", listOf(hash.toHex(), false)))
.flatMap { resp ->
if (resp.isNull()) {
Mono.error(SilentException("Received null for block $hash"))
} else {
Mono.just(resp)
}
}
.flatMap(JsonRpcResponse::requireResult)
.map { BlockContainer.fromEthereumJson(it, upstreamId) }
.subscribeOn(Schedulers.boundedElastic())
.timeout(Defaults.timeoutInternal, Mono.empty())
}.repeatWhenEmpty { n ->
Repeat.times<Any>(5)
.exponentialBackoff(Duration.ofMillis(50), Duration.ofMillis(500))
.apply(n)
}
.timeout(Defaults.timeout, Mono.empty())
.onErrorResume { Mono.empty() }
}
override fun stop() {
super.stop()
subscription?.dispose()

View File

@@ -0,0 +1,31 @@
/**
* Copyright (c) 2022 EmeraldPay, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.emeraldpay.dshackle.upstream.ethereum
import io.emeraldpay.dshackle.upstream.NoUpstreamSubscriptions
import io.emeraldpay.dshackle.upstream.ethereum.subscribe.PendingTxesSource
class NoEthereumUpstreamSubscriptions : NoUpstreamSubscriptions(), EthereumUpstreamSubscriptions {
companion object {
@JvmStatic
val DEFAULT = NoEthereumUpstreamSubscriptions()
}
override fun getPendingTxes(): PendingTxesSource? {
return null
}
}

View File

@@ -17,20 +17,17 @@ package io.emeraldpay.dshackle.upstream.ethereum
import io.emeraldpay.dshackle.Defaults
import io.emeraldpay.dshackle.Global
import io.emeraldpay.dshackle.SilentException
import io.emeraldpay.dshackle.config.AuthConfig
import io.emeraldpay.dshackle.data.BlockContainer
import io.emeraldpay.dshackle.upstream.DefaultUpstream
import io.emeraldpay.dshackle.upstream.UpstreamAvailability
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcError
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcException
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcWsMessage
import io.emeraldpay.dshackle.upstream.rpcclient.ResponseWSParser
import io.emeraldpay.dshackle.upstream.rpcclient.RpcMetrics
import io.emeraldpay.etherjar.rpc.RpcResponseError
import io.emeraldpay.etherjar.rpc.json.BlockJson
import io.emeraldpay.etherjar.rpc.json.TransactionRefJson
import io.netty.buffer.ByteBuf
import io.netty.buffer.ByteBufInputStream
import io.netty.buffer.Unpooled
@@ -51,7 +48,6 @@ import reactor.netty.http.client.HttpClient
import reactor.netty.http.client.WebsocketClientSpec
import reactor.netty.http.websocket.WebsocketInbound
import reactor.netty.http.websocket.WebsocketOutbound
import reactor.retry.Repeat
import reactor.util.function.Tuples
import java.net.URI
import java.time.Duration
@@ -60,26 +56,22 @@ import java.util.Base64
import java.util.concurrent.Executors
import java.util.concurrent.ScheduledFuture
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger
open class WsConnection(
open class WsConnectionImpl(
private val id: String,
private val uri: URI,
private val origin: URI,
private val basicAuth: AuthConfig.ClientBasicAuth?,
private val rpcMetrics: RpcMetrics?,
private val upstream: DefaultUpstream?,
private val validator: EthereumUpstreamValidator?
) : AutoCloseable {
companion object {
private val log = LoggerFactory.getLogger(WsConnection::class.java)
private val log = LoggerFactory.getLogger(WsConnectionImpl::class.java)
private const val IDS_START = 100
private const val START_REQUEST =
"{\"jsonrpc\":\"2.0\", \"method\":\"eth_subscribe\", \"id\":\"blocks\", \"params\":[\"newHeads\"]}"
// WebSocket Frame limit.
// Default is 65_536, but Geth responds with larger frames,
@@ -108,26 +100,22 @@ open class WsConnection(
private val resetBackoffExecutor = Executors.newScheduledThreadPool(2)
private var resetBackoffTask: ScheduledFuture<Unit>? = null
private val blocks = Sinks
private val messages = Sinks
.many()
.multicast()
.directBestEffort<BlockContainer>()
.directBestEffort<ResponseWSParser.WsResponse>()
private var rpcSend = Sinks
.many()
.unicast()
.onBackpressureBuffer<JsonRpcRequest>()
private val rpcReceive = Sinks
.many()
.multicast()
.directBestEffort<JsonRpcResponse>()
private val disconnects = Sinks
.many()
.multicast()
.directBestEffort<Instant>()
private val sendIdSeq = AtomicInteger(IDS_START)
private val sendExecutor = Executors.newFixedThreadPool(
1.coerceAtLeast(Runtime.getRuntime().availableProcessors() / 2)
)
private val sendExecutor = Executors.newSingleThreadExecutor()
private var keepConnection = true
private var connection: Disposable? = null
private val reconnecting = AtomicBoolean(false)
@@ -183,7 +171,6 @@ open class WsConnection(
private fun connectInternal() {
log.info("Connecting to WebSocket: $uri")
log.info("Available processors: ${Runtime.getRuntime().availableProcessors()}")
connection?.dispose()
connection = HttpClient.create()
.resolver(DefaultAddressResolverGroup.INSTANCE)
@@ -236,6 +223,7 @@ open class WsConnection(
}
fun handle(inbound: WebsocketInbound, outbound: WebsocketOutbound): Publisher<Void> {
var read = false
val consumer = inbound
.aggregateFrames(msgSizeLimit)
.receiveFrames()
@@ -244,11 +232,17 @@ open class WsConnection(
.flatMap {
try {
val msg = parser.parse(it)
if (msg.type == ResponseWSParser.Type.SUBSCRIPTION) {
onSubscription(msg)
} else {
onRpc(msg)
if (!read) {
if (msg.error != null) {
log.warn("Received error ${msg.error.code} from $uri: ${msg.error.message}")
} else {
// restart backoff only after a successful read from the connection,
// otherwise it may restart it even if the connection is faulty or responds only with error
currentBackOff = reconnectBackoff.start()
read = true
}
}
onMessage(msg)
} catch (t: Throwable) {
log.warn("Failed to process WS message. ${t.javaClass}: ${t.message}")
Mono.empty()
@@ -265,117 +259,57 @@ open class WsConnection(
val calls = rpcSend
.asFlux()
.map {
Unpooled.wrappedBuffer(it.toJson())
Unpooled.wrappedBuffer(Global.objectMapper.writeValueAsBytes(it))
}
return outbound.send(
Flux.merge(
startWhenValidated(),
calls.subscribeOn(Schedulers.boundedElastic()),
consumer.then(Mono.empty<ByteBuf>()).subscribeOn(Schedulers.boundedElastic())
)
)
}
/**
* Starts subscriptions ('newHeads') when the upstream is fully validated. If upstream is invalid it breaks flow with an Error.
* I.e., the first requests are made from a Validator and when it returns OK the Connection continues with other stuff.
*/
fun startWhenValidated(): Publisher<ByteBuf> {
val start = Mono.just(START_REQUEST).map {
Unpooled.wrappedBuffer(it.toByteArray())
}
return if (validator != null) {
validator.validate()
.timeout(
Defaults.timeoutInternal,
Mono.fromCallable { log.warn("Not received a validation result from $uri") }.then(Mono.error(TimeoutException()))
)
.flatMap {
if (it == UpstreamAvailability.OK) {
start
} else {
tryReconnectLater()
Mono.error(IllegalStateException("Upstream $uri is not ready"))
}
}
} else {
start
}
}
fun onRpc(msg: ResponseWSParser.WsResponse): Mono<Void> {
return if (msg.id.isNumber()) {
val resp = JsonRpcResponse(
msg.value,
msg.error,
msg.id,
null
)
Mono.fromCallable {
val status = rpcReceive.tryEmitNext(resp)
if (status.isFailure) {
if (status == Sinks.EmitResult.FAIL_ZERO_SUBSCRIBER) {
log.debug("No subscribers to WS response")
} else {
log.warn("Failed to proceed with a RPC message: $status")
}
}
}.then()
} else {
// it's a response to the newHeads subscription, just ignore it
Mono.empty<Void>()
}
}
fun onSubscription(msg: ResponseWSParser.WsResponse): Mono<Void> {
if (msg.error != null) {
return Mono.error(IllegalStateException("Received error from WS upstream: ${msg.error.message}"))
}
// we always expect an answer to the `newHeads`, since we are not initiating any other subscriptions
fun onMessage(msg: ResponseWSParser.WsResponse): Mono<Void> {
return Mono.fromCallable {
Global.objectMapper.readValue(msg.value, BlockJson::class.java) as BlockJson<TransactionRefJson>
}.flatMap { onNewHeads(it) }.then()
val status = messages.tryEmitNext(msg)
if (status.isFailure) {
if (status == Sinks.EmitResult.FAIL_ZERO_SUBSCRIBER) {
log.debug("No subscribers to WS response")
} else {
log.warn("Failed to proceed with a WS message: $status")
}
}
}.then()
}
fun onNewHeads(block: BlockJson<TransactionRefJson>): Mono<Void> {
// newHeads returns incomplete blocks, i.e. without some fields and without transaction hashes,
// so we need to fetch the full block data
return if (block.difficulty == null || block.transactions == null) {
Mono.just(block.hash)
.flatMap { hash ->
call(JsonRpcRequest("eth_getBlockByHash", listOf(hash.toHex(), false)))
.flatMap { resp ->
if (resp.isNull()) {
Mono.error(SilentException("Received null for block $hash"))
} else {
Mono.just(resp)
}
}
.flatMap(JsonRpcResponse::requireResult)
.map { BlockContainer.fromEthereumJson(it, id) }
.subscribeOn(Schedulers.boundedElastic())
.timeout(Defaults.timeoutInternal, Mono.empty())
}.repeatWhenEmpty { n ->
Repeat.times<Any>(5)
.exponentialBackoff(Duration.ofMillis(50), Duration.ofMillis(500))
.apply(n)
}
.timeout(Defaults.timeout, Mono.empty())
.onErrorResume { Mono.empty() }
.doOnNext {
blocks.tryEmitNext(it)
}
.then()
} else {
Mono.fromCallable {
blocks.tryEmitNext(BlockContainer.from(block, id))
}.then()
}
open fun getRpcResponses(): Flux<JsonRpcResponse> {
return Flux.from(messages.asFlux())
.publishOn(Schedulers.boundedElastic())
.filter {
it.type == ResponseWSParser.Type.RPC
}
.map { msg ->
JsonRpcResponse(
msg.value, msg.error, msg.id, null
)
}
}
fun call(originalRequest: JsonRpcRequest): Mono<JsonRpcResponse> {
open fun getSubscribeResponses(): Flux<JsonRpcWsMessage> {
return Flux.from(messages.asFlux())
.publishOn(Schedulers.boundedElastic())
.filter {
it.type == ResponseWSParser.Type.SUBSCRIPTION
}
.map { msg ->
JsonRpcWsMessage(
msg.value, msg.error, msg.id.asString(),
)
}
}
open fun callRpc(originalRequest: JsonRpcRequest): Mono<JsonRpcResponse> {
return Mono.fromCallable {
val startTime = System.nanoTime()
// use an internal id sequence, to avoid id conflicts with user calls
@@ -387,10 +321,13 @@ open class WsConnection(
}
}
fun sendRpc(request: JsonRpcRequest) {
private fun sendRpc(request: JsonRpcRequest) {
// submit to upstream in a separate thread, to free current thread (needs for subscription, etc)
sendExecutor.execute {
rpcSend.emitNext(request) { _, res -> res == Sinks.EmitResult.FAIL_NON_SERIALIZED }
val result = rpcSend.tryEmitNext(request)
if (result.isFailure) {
log.warn("Failed to send RPC request: $result")
}
}
}
@@ -405,7 +342,7 @@ open class WsConnection(
false
)
val response = Flux.from(rpcReceive.asFlux())
val response = Flux.from(getRpcResponses())
.doOnSubscribe { sendRpc(request) }
.filter { resp -> resp.id.asNumber() == expectedId }
.take(Defaults.timeout)
@@ -432,10 +369,6 @@ open class WsConnection(
.switchIfEmpty(Mono.error(noResponse))
}
fun getBlocksFlux(): Flux<BlockContainer> {
return this.blocks.asFlux()
}
override fun close() {
log.info("Closing connection to WebSocket $uri")
keepConnection = false

View File

@@ -0,0 +1,41 @@
/**
* Copyright (c) 2022 EmeraldPay, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.emeraldpay.dshackle.upstream.ethereum
import reactor.core.publisher.Flux
/**
* A JSON-RPC Subscription client.
* In general, it's a Websocket extension for JSON RPC that allows multiple responses to the same method.
*
* Example:
*
* <pre><code>
* > {"id": 1, "method": "eth_subscribe", "params": ["newPendingTransactions"]}
* > {"jsonrpc":"2.0","id":1,"result":"0xcff45d00e77e8e050d919daf284516c8"}
* > {"jsonrpc":"2.0","method":"eth_subscription","params":{"subscription":"0xcff45d00e77e8e050d919daf284516c8","result":"0xa61bab14fc9720ea8725622688c2f964666d7c2afdae38af7dad53f12f242d5c"}}
* > {"jsonrpc":"2.0","method":"eth_subscription","params":{"subscription":"0xcff45d00e77e8e050d919daf284516c8","result":"0x911548eb0f3bf353a54e03a3506c7c3e747470d6c201f03babbc07ff6e14cd6e"}}
* > {"jsonrpc":"2.0","method":"eth_subscription","params":{"subscription":"0xcff45d00e77e8e050d919daf284516c8","result":"0x67f22a3b441ea312306f97694ca8159f8d6faaccf0f5ce6442c84b13991f1d23"}}
* </code></pre>
*
* */
interface WsSubscriptions {
/**
* Subscribe on remote
*/
fun subscribe(method: String): Flux<ByteArray>
}

View File

@@ -0,0 +1,54 @@
/**
* Copyright (c) 2022 EmeraldPay, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.emeraldpay.dshackle.upstream.ethereum
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcException
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
import org.slf4j.LoggerFactory
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
import java.util.concurrent.atomic.AtomicLong
import java.util.concurrent.atomic.AtomicReference
class WsSubscriptionsImpl(
val conn: WsConnectionImpl,
) : WsSubscriptions {
companion object {
private val log = LoggerFactory.getLogger(WsSubscriptionsImpl::class.java)
}
private val ids = AtomicLong(1)
override fun subscribe(method: String): Flux<ByteArray> {
val subscriptionId = AtomicReference("")
val messages = conn.getSubscribeResponses()
.filter { it.subscriptionId == subscriptionId.get() }
.filter { it.result != null } // should never happen
.map { it.result!! }
return conn.callRpc(JsonRpcRequest("eth_subscribe", listOf(method), ids.incrementAndGet()))
.flatMapMany {
if (it.hasError()) {
log.warn("Failed to establish ETH Subscription: ${it.error?.message}")
Mono.error(JsonRpcException(it.id, it.error!!))
} else {
subscriptionId.set(it.getResultAsProcessedString())
messages
}
}
}
}

View File

@@ -3,6 +3,7 @@ package io.emeraldpay.dshackle.upstream.ethereum.connectors
import io.emeraldpay.dshackle.reader.Reader
import io.emeraldpay.dshackle.upstream.Head
import io.emeraldpay.dshackle.upstream.Lifecycle
import io.emeraldpay.dshackle.upstream.ethereum.EthereumUpstreamSubscriptions
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
@@ -10,4 +11,6 @@ interface EthereumConnector : Lifecycle {
fun getHead(): Head
fun getApi(): Reader<JsonRpcRequest, JsonRpcResponse>
fun getUpstreamSubscriptions(): EthereumUpstreamSubscriptions
}

View File

@@ -29,7 +29,7 @@ open class EthereumConnectorFactory(
override fun create(upstream: DefaultUpstream, validator: EthereumUpstreamValidator, chain: Chain): EthereumConnector {
if (wsFactory != null && !preferHttp) {
return EthereumWsConnector(wsFactory, upstream, validator, forkChoice, blockValidator)
return EthereumWsConnector(wsFactory, upstream, forkChoice, blockValidator)
}
if (httpFactory == null) {
throw java.lang.IllegalArgumentException("Can't create rpc connector if no http factory set")

View File

@@ -7,10 +7,7 @@ import io.emeraldpay.dshackle.upstream.BlockValidator
import io.emeraldpay.dshackle.upstream.Head
import io.emeraldpay.dshackle.upstream.Lifecycle
import io.emeraldpay.dshackle.upstream.MergedHead
import io.emeraldpay.dshackle.upstream.ethereum.EthereumRpcHead
import io.emeraldpay.dshackle.upstream.ethereum.EthereumWsFactory
import io.emeraldpay.dshackle.upstream.ethereum.EthereumWsHead
import io.emeraldpay.dshackle.upstream.ethereum.WsConnection
import io.emeraldpay.dshackle.upstream.ethereum.*
import io.emeraldpay.dshackle.upstream.forkchoice.AlwaysForkChoice
import io.emeraldpay.dshackle.upstream.forkchoice.ForkChoice
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
@@ -25,7 +22,7 @@ class EthereumRpcConnector(
forkChoice: ForkChoice,
blockValidator: BlockValidator
) : EthereumConnector, CachesEnabled {
private val conn: WsConnection?
private val conn: WsConnectionImpl?
private val head: Head
companion object {
@@ -35,15 +32,16 @@ class EthereumRpcConnector(
init {
if (wsFactory != null) {
// do not set upstream to the WS, since it doesn't control the RPC upstream
conn = wsFactory.create(null, null)
val wsHead = EthereumWsHead(conn, id, AlwaysForkChoice(), blockValidator)
// receive bew blocks through WebSockets, but also periodically verify with RPC in case if WS failed
val rpcHead = EthereumRpcHead(directReader, AlwaysForkChoice(), id, blockValidator, Duration.ofSeconds(30))
conn = wsFactory.create(null)
val subscriptions = WsSubscriptionsImpl(conn)
val wsHead = EthereumWsHead(id, AlwaysForkChoice(), blockValidator, getApi(), subscriptions)
// receive all new blocks through WebSockets, but also periodically verify with RPC in case if WS failed
val rpcHead = EthereumRpcHead(getApi(), AlwaysForkChoice(), id, blockValidator, Duration.ofSeconds(30))
head = MergedHead(listOf(rpcHead, wsHead), forkChoice, "Merged for $id")
} else {
conn = null
log.warn("Setting up connector for $id upstream with RPC-only access, less effective than WS+RPC")
head = EthereumRpcHead(directReader, forkChoice, id, blockValidator)
head = EthereumRpcHead(getApi(), forkChoice, id, blockValidator)
}
}
@@ -78,6 +76,10 @@ class EthereumRpcConnector(
return directReader
}
override fun getUpstreamSubscriptions(): EthereumUpstreamSubscriptions {
return NoEthereumUpstreamSubscriptions.DEFAULT
}
override fun getHead(): Head {
return head
}

View File

@@ -4,10 +4,8 @@ import io.emeraldpay.dshackle.reader.Reader
import io.emeraldpay.dshackle.upstream.BlockValidator
import io.emeraldpay.dshackle.upstream.DefaultUpstream
import io.emeraldpay.dshackle.upstream.Head
import io.emeraldpay.dshackle.upstream.ethereum.EthereumUpstreamValidator
import io.emeraldpay.dshackle.upstream.ethereum.EthereumWsFactory
import io.emeraldpay.dshackle.upstream.ethereum.EthereumWsHead
import io.emeraldpay.dshackle.upstream.ethereum.WsConnection
import io.emeraldpay.dshackle.upstream.ethereum.*
import io.emeraldpay.dshackle.upstream.ethereum.subscribe.EthereumWsSubscriptions
import io.emeraldpay.dshackle.upstream.forkchoice.ForkChoice
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
@@ -16,18 +14,20 @@ import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcWsClient
class EthereumWsConnector(
wsFactory: EthereumWsFactory,
upstream: DefaultUpstream,
validator: EthereumUpstreamValidator,
forkChoice: ForkChoice,
blockValidator: BlockValidator
) : EthereumConnector {
private val conn: WsConnection
private val conn: WsConnectionImpl
private val api: Reader<JsonRpcRequest, JsonRpcResponse>
private val head: EthereumWsHead
private val subscriptions: EthereumUpstreamSubscriptions
init {
conn = wsFactory.create(upstream, validator)
head = EthereumWsHead(conn, upstream.getId(), forkChoice, blockValidator)
conn = wsFactory.create(upstream)
api = JsonRpcWsClient(conn)
val wsSubscriptions = WsSubscriptionsImpl(conn)
head = EthereumWsHead(upstream.getId(), forkChoice, blockValidator, api, wsSubscriptions)
subscriptions = EthereumWsSubscriptions(wsSubscriptions)
}
override fun start() {
@@ -48,6 +48,10 @@ class EthereumWsConnector(
return api
}
override fun getUpstreamSubscriptions(): EthereumUpstreamSubscriptions {
return subscriptions
}
override fun getHead(): Head {
return head
}

View File

@@ -0,0 +1,45 @@
/**
* Copyright (c) 2022 EmeraldPay, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.emeraldpay.dshackle.upstream.ethereum.subscribe
import io.emeraldpay.dshackle.commons.ExpiringSet
import io.emeraldpay.dshackle.upstream.Selector
import io.emeraldpay.etherjar.domain.TransactionId
import io.emeraldpay.etherjar.hex.HexDataComparator
import org.slf4j.LoggerFactory
import reactor.core.publisher.Flux
import java.time.Duration
class AggregatedPendingTxes(
private val sources: List<PendingTxesSource>
) : PendingTxesSource {
companion object {
private val log = LoggerFactory.getLogger(AggregatedPendingTxes::class.java)
}
private val track = ExpiringSet<TransactionId>(
Duration.ofSeconds(30),
HexDataComparator() as Comparator<TransactionId>,
10_000
)
override fun connect(matcher: Selector.Matcher): Flux<TransactionId> {
return Flux.merge(
sources.map { it.connect(matcher) } // todo check
).filter(track::add)
}
}

View File

@@ -20,6 +20,7 @@ import io.emeraldpay.dshackle.data.BlockId
import io.emeraldpay.dshackle.data.TxId
import io.emeraldpay.dshackle.upstream.Head
import io.emeraldpay.dshackle.upstream.Selector
import io.emeraldpay.dshackle.upstream.SubscriptionConnect
import io.emeraldpay.dshackle.upstream.ethereum.EthereumLikeMultistream
import org.slf4j.LoggerFactory
import reactor.core.publisher.Flux
@@ -33,7 +34,7 @@ import kotlin.concurrent.write
class ConnectBlockUpdates(
private val upstream: EthereumLikeMultistream
) {
) : SubscriptionConnect<ConnectBlockUpdates.Update> {
companion object {
private val log = LoggerFactory.getLogger(ConnectBlockUpdates::class.java)
@@ -49,7 +50,7 @@ class ConnectBlockUpdates(
private val connected: MutableMap<String, Flux<Update>> = ConcurrentHashMap()
fun connect() = connect(Selector.empty)
fun connect(matcher: Selector.Matcher): Flux<Update> {
override fun connect(matcher: Selector.Matcher): Flux<Update> {
return connected.computeIfAbsent(matcher.describeInternal()) { key ->
extract(upstream.getHead(matcher))
.publishOn(Schedulers.boundedElastic())

View File

@@ -16,6 +16,7 @@
package io.emeraldpay.dshackle.upstream.ethereum.subscribe
import io.emeraldpay.dshackle.upstream.Selector
import io.emeraldpay.dshackle.upstream.SubscriptionConnect
import io.emeraldpay.dshackle.upstream.ethereum.EthereumLikeMultistream
import io.emeraldpay.dshackle.upstream.ethereum.subscribe.json.LogMessage
import io.emeraldpay.etherjar.domain.Address
@@ -45,14 +46,18 @@ open class ConnectLogs(
return produceLogs.produce(connectBlockUpdates.connect(matcher))
}
open fun start(addresses: List<Address>, topics: List<Hex32>, matcher: Selector.Matcher): Flux<LogMessage> {
// shortcut to the whole output if we don't have any filters
if (addresses.isEmpty() && topics.isEmpty()) {
return start(matcher)
open fun create(addresses: List<Address>, topics: List<Hex32>): SubscriptionConnect<LogMessage> {
return object : SubscriptionConnect<LogMessage> {
override fun connect(matcher: Selector.Matcher): Flux<LogMessage> {
// shortcut to the whole output if we don't have any filters
if (addresses.isEmpty() && topics.isEmpty()) {
return start(matcher)
}
// filtered output
return start(matcher)
.transform(filtered(addresses, topics))
}
}
// filtered output
return start(matcher)
.transform(filtered(addresses, topics))
}
fun filtered(addresses: List<Address>, topics: List<Hex32>): Function<Flux<LogMessage>, Flux<LogMessage>> {

View File

@@ -16,6 +16,7 @@
package io.emeraldpay.dshackle.upstream.ethereum.subscribe
import io.emeraldpay.dshackle.upstream.Selector
import io.emeraldpay.dshackle.upstream.SubscriptionConnect
import io.emeraldpay.dshackle.upstream.ethereum.EthereumLikeMultistream
import io.emeraldpay.dshackle.upstream.ethereum.subscribe.json.NewHeadMessage
import org.slf4j.LoggerFactory
@@ -29,7 +30,7 @@ import java.util.concurrent.ConcurrentHashMap
*/
class ConnectNewHeads(
private val upstream: EthereumLikeMultistream
) {
) : SubscriptionConnect<NewHeadMessage> {
companion object {
private val log = LoggerFactory.getLogger(ConnectNewHeads::class.java)
@@ -37,7 +38,7 @@ class ConnectNewHeads(
private val connected: MutableMap<String, Flux<NewHeadMessage>> = ConcurrentHashMap()
fun connect(matcher: Selector.Matcher): Flux<NewHeadMessage> =
override fun connect(matcher: Selector.Matcher): Flux<NewHeadMessage> =
connected.computeIfAbsent(matcher.describeInternal()) { key ->
ProduceNewHeads(upstream.getHead(matcher))
.start()

View File

@@ -15,6 +15,8 @@
*/
package io.emeraldpay.dshackle.upstream.ethereum.subscribe
import io.emeraldpay.dshackle.upstream.Selector
import io.emeraldpay.dshackle.upstream.SubscriptionConnect
import io.emeraldpay.dshackle.upstream.UpstreamAvailability
import io.emeraldpay.dshackle.upstream.ethereum.EthereumLikeMultistream
import org.slf4j.LoggerFactory
@@ -25,7 +27,7 @@ import kotlin.concurrent.withLock
class ConnectSyncing(
private val upstream: EthereumLikeMultistream
) {
) : SubscriptionConnect<Boolean> {
companion object {
private val log = LoggerFactory.getLogger(ConnectSyncing::class.java)
@@ -34,7 +36,7 @@ class ConnectSyncing(
private var connected: Flux<Boolean>? = null
private val connectLock = ReentrantLock()
fun connect(): Flux<Boolean> {
override fun connect(matcher: Selector.Matcher): Flux<Boolean> {
val current = connected
if (current != null) {
return current

View File

@@ -0,0 +1,42 @@
/**
* Copyright (c) 2022 EmeraldPay, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.emeraldpay.dshackle.upstream.ethereum.subscribe
import io.emeraldpay.dshackle.commons.DurableFlux
import io.emeraldpay.dshackle.commons.SharedFluxHolder
import io.emeraldpay.dshackle.upstream.Selector
import io.emeraldpay.dshackle.upstream.SubscriptionConnect
import io.emeraldpay.etherjar.domain.TransactionId
import reactor.core.publisher.Flux
import java.time.Duration
abstract class DefaultPendingTxesSource : SubscriptionConnect<TransactionId>, PendingTxesSource {
private val connectionSource = DurableFlux
.newBuilder()
.using(::createConnection)
.backoffOnError(Duration.ofMillis(100), 1.5, Duration.ofSeconds(60))
.build()
private val holder = SharedFluxHolder<TransactionId>(
connectionSource::connect
)
override fun connect(matcher: Selector.Matcher): Flux<TransactionId> {
return holder.get()
}
abstract fun createConnection(): Flux<TransactionId>
}

View File

@@ -0,0 +1,59 @@
/**
* Copyright (c) 2022 EmeraldPay, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.emeraldpay.dshackle.upstream.ethereum.subscribe
import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.api.proto.BlockchainOuterClass.NativeSubscribeReplyItem
import io.emeraldpay.api.proto.BlockchainOuterClass.NativeSubscribeRequest
import io.emeraldpay.api.proto.ReactorBlockchainGrpc
import io.emeraldpay.dshackle.Chain
import io.emeraldpay.dshackle.upstream.ethereum.EthereumSubscriptionApi
import io.emeraldpay.etherjar.domain.TransactionId
import reactor.core.publisher.Flux
class DshacklePendingTxesSource(
private val blockchain: Chain,
private val conn: ReactorBlockchainGrpc.ReactorBlockchainStub,
) : PendingTxesSource, DefaultPendingTxesSource() {
private val request = NativeSubscribeRequest.newBuilder()
.setChainValue(blockchain.id)
.setMethod(EthereumSubscriptionApi.METHOD_PENDING_TXES)
.build()
var available = false
override fun createConnection(): Flux<TransactionId> {
if (!available) {
return Flux.empty()
}
return conn
.nativeSubscribe(request)
.map(::readResponse)
.map(TransactionId::from)
}
fun readResponse(resp: NativeSubscribeReplyItem): String {
// comes as a string, so cut off the quotes
return resp.payload.substring(1, resp.payload.size() - 1).toStringUtf8()
}
fun update(conf: BlockchainOuterClass.DescribeChain) {
available = conf.supportedMethodsList.any {
it == EthereumSubscriptionApi.METHOD_PENDING_TXES
}
}
}

View File

@@ -0,0 +1,47 @@
/**
* Copyright (c) 2022 EmeraldPay, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.emeraldpay.dshackle.upstream.ethereum.subscribe
import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.api.proto.ReactorBlockchainGrpc
import io.emeraldpay.dshackle.Chain
import io.emeraldpay.dshackle.upstream.SubscriptionConnect
import io.emeraldpay.dshackle.upstream.UpstreamSubscriptions
import io.emeraldpay.dshackle.upstream.ethereum.EthereumSubscriptionApi
import io.emeraldpay.dshackle.upstream.ethereum.EthereumUpstreamSubscriptions
class EthereumDshackleSubscriptions(
blockchain: Chain,
conn: ReactorBlockchainGrpc.ReactorBlockchainStub,
) : UpstreamSubscriptions, EthereumUpstreamSubscriptions {
private val pendingTxes = DshacklePendingTxesSource(blockchain, conn)
override fun <T> get(method: String): SubscriptionConnect<T>? {
if (method == EthereumSubscriptionApi.METHOD_PENDING_TXES) {
return pendingTxes as SubscriptionConnect<T>
}
return null
}
fun update(conf: BlockchainOuterClass.DescribeChain) {
pendingTxes.update(conf)
}
override fun getPendingTxes(): PendingTxesSource? {
return pendingTxes
}
}

View File

@@ -0,0 +1,45 @@
/**
* Copyright (c) 2022 EmeraldPay, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.emeraldpay.dshackle.upstream.ethereum.subscribe
import io.emeraldpay.dshackle.upstream.SubscriptionConnect
import io.emeraldpay.dshackle.upstream.UpstreamSubscriptions
import io.emeraldpay.dshackle.upstream.ethereum.EthereumSubscriptionApi
import io.emeraldpay.dshackle.upstream.ethereum.EthereumUpstreamSubscriptions
import io.emeraldpay.dshackle.upstream.ethereum.WsSubscriptions
import org.slf4j.LoggerFactory
class EthereumWsSubscriptions(
private val conn: WsSubscriptions
) : UpstreamSubscriptions, EthereumUpstreamSubscriptions {
companion object {
private val log = LoggerFactory.getLogger(EthereumWsSubscriptions::class.java)
}
private val pendingTxes = WebsocketPendingTxes(conn)
override fun <T> get(method: String): SubscriptionConnect<T>? {
if (method == EthereumSubscriptionApi.METHOD_PENDING_TXES) {
return pendingTxes as SubscriptionConnect<T>
}
return null
}
override fun getPendingTxes(): PendingTxesSource? {
return pendingTxes
}
}

View File

@@ -0,0 +1,34 @@
/**
* Copyright (c) 2022 EmeraldPay, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.emeraldpay.dshackle.upstream.ethereum.subscribe
import io.emeraldpay.dshackle.upstream.Selector
import io.emeraldpay.etherjar.domain.TransactionId
import org.slf4j.LoggerFactory
import reactor.core.publisher.Flux
class NoPendingTxes : PendingTxesSource {
companion object {
private val log = LoggerFactory.getLogger(NoPendingTxes::class.java)
val DEFAULT = NoPendingTxes()
}
override fun connect(matched: Selector.Matcher): Flux<TransactionId> {
return Flux.empty()
}
}

View File

@@ -0,0 +1,25 @@
/**
* Copyright (c) 2022 EmeraldPay, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.emeraldpay.dshackle.upstream.ethereum.subscribe
import io.emeraldpay.dshackle.upstream.SubscriptionConnect
import io.emeraldpay.etherjar.domain.TransactionId
/**
* A source to subscribe to newPendingTransactions.
* When using a Websocket RPC on a node, it produces hashes of new transactions received.
*/
interface PendingTxesSource : SubscriptionConnect<TransactionId>

View File

@@ -0,0 +1,47 @@
/**
* Copyright (c) 2022 EmeraldPay, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.emeraldpay.dshackle.upstream.ethereum.subscribe
import io.emeraldpay.dshackle.upstream.SubscriptionConnect
import io.emeraldpay.dshackle.upstream.ethereum.EthereumSubscriptionApi
import io.emeraldpay.dshackle.upstream.ethereum.WsSubscriptions
import io.emeraldpay.etherjar.domain.TransactionId
import org.slf4j.LoggerFactory
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
import java.time.Duration
class WebsocketPendingTxes(
private val wsSubscriptions: WsSubscriptions
) : DefaultPendingTxesSource(), SubscriptionConnect<TransactionId> {
companion object {
private val log = LoggerFactory.getLogger(WebsocketPendingTxes::class.java)
}
override fun createConnection(): Flux<TransactionId> {
return wsSubscriptions.subscribe(EthereumSubscriptionApi.METHOD_PENDING_TXES)
.timeout(Duration.ofSeconds(60), Mono.empty())
.map {
// comes as a JS string, i.e., within quotes
val value = ByteArray(it.size - 2)
System.arraycopy(it, 1, value, 0, value.size)
TransactionId.from(String(value))
}
.doOnError { t -> log.warn("Invalid pending transaction", t) }
.onErrorResume { Mono.empty() }
}
}

View File

@@ -0,0 +1,37 @@
/**
* Copyright (c) 2022 EmeraldPay, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.emeraldpay.dshackle.upstream.ethereum.subscribe.json
import com.fasterxml.jackson.core.JsonGenerator
import com.fasterxml.jackson.databind.JsonSerializer
import com.fasterxml.jackson.databind.SerializerProvider
import io.emeraldpay.etherjar.domain.TransactionId
import org.slf4j.LoggerFactory
class TransactionIdSerializer : JsonSerializer<TransactionId>() {
companion object {
private val log = LoggerFactory.getLogger(TransactionIdSerializer::class.java)
}
override fun serialize(value: TransactionId?, gen: JsonGenerator, serializers: SerializerProvider) {
if (value == null) {
gen.writeNull()
return
}
gen.writeString(value.toHex())
}
}

View File

@@ -29,6 +29,9 @@ import io.emeraldpay.dshackle.upstream.MergedHead
import io.emeraldpay.dshackle.upstream.Multistream
import io.emeraldpay.dshackle.upstream.Selector
import io.emeraldpay.dshackle.upstream.Upstream
import io.emeraldpay.dshackle.upstream.ethereum.subscribe.AggregatedPendingTxes
import io.emeraldpay.dshackle.upstream.ethereum.subscribe.NoPendingTxes
import io.emeraldpay.dshackle.upstream.ethereum.subscribe.PendingTxesSource
import io.emeraldpay.dshackle.upstream.forkchoice.PriorityForkChoice
import io.emeraldpay.dshackle.upstream.grpc.GrpcUpstream
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
@@ -52,8 +55,8 @@ open class EthereumPosMultiStream(
private var head: Head? = null
private val reader: EthereumCachingReader = EthereumCachingReader(this, this.caches, getMethodsFactory())
private var subscribe = EthereumSubscriptionApi(this, NoPendingTxes())
private val feeEstimation = EthereumPriorityFees(this, reader, 256)
private val subscribe = EthereumSubscribe(this)
private val filteredHeads: MutableMap<String, Head> =
ConcurrentReferenceHashMap(16, ConcurrentReferenceHashMap.ReferenceType.WEAK)
@@ -157,7 +160,7 @@ open class EthereumPosMultiStream(
return Mono.just(LocalCallRouter(reader, getMethods(), getHead(), localEnabled))
}
override fun getSubscribe(): EthereumSubscribe {
override fun getSubscriptionApi(): EthereumSubscriptionApi {
return subscribe
}
@@ -182,4 +185,22 @@ open class EthereumPosMultiStream(
override fun getFeeEstimation(): ChainFees {
return feeEstimation
}
override fun onUpstreamsUpdated() {
super.onUpstreamsUpdated()
val pendingTxes: PendingTxesSource = upstreams
.mapNotNull {
it.getUpstreamSubscriptions().getPendingTxes()
}.let {
if (it.isEmpty()) {
NoPendingTxes()
} else if (it.size == 1) {
it.first()
} else {
AggregatedPendingTxes(it)
}
}
subscribe = EthereumSubscriptionApi(this, pendingTxes)
}
}

View File

@@ -98,4 +98,8 @@ open class EthereumPosRpcUpstream(
}
return this as T
}
override fun getUpstreamSubscriptions(): EthereumUpstreamSubscriptions {
return connector.getUpstreamSubscriptions()
}
}

View File

@@ -44,4 +44,6 @@ abstract class EthereumPosUpstream(
override fun getLabels(): Collection<UpstreamsConfig.Labels> {
return node?.let { listOf(it.labels) } ?: emptyList()
}
abstract fun getUpstreamSubscriptions(): EthereumUpstreamSubscriptions
}

View File

@@ -17,7 +17,7 @@
package io.emeraldpay.dshackle.upstream.grpc
import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.api.proto.ReactorBlockchainGrpc
import io.emeraldpay.api.proto.ReactorBlockchainGrpc.ReactorBlockchainStub
import io.emeraldpay.dshackle.Chain
import io.emeraldpay.dshackle.Defaults
import io.emeraldpay.dshackle.config.UpstreamsConfig
@@ -32,6 +32,8 @@ import io.emeraldpay.dshackle.upstream.Upstream
import io.emeraldpay.dshackle.upstream.UpstreamAvailability
import io.emeraldpay.dshackle.upstream.calls.CallMethods
import io.emeraldpay.dshackle.upstream.ethereum.EthereumUpstream
import io.emeraldpay.dshackle.upstream.ethereum.EthereumUpstreamSubscriptions
import io.emeraldpay.dshackle.upstream.ethereum.subscribe.EthereumDshackleSubscriptions
import io.emeraldpay.dshackle.upstream.forkchoice.MostWorkForkChoice
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcGrpcClient
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
@@ -53,7 +55,7 @@ open class EthereumGrpcUpstream(
hash: Byte,
role: UpstreamsConfig.UpstreamRole,
private val chain: Chain,
private val remote: ReactorBlockchainGrpc.ReactorBlockchainStub,
private val remote: ReactorBlockchainStub,
private val client: JsonRpcGrpcClient,
overrideLabels: UpstreamsConfig.Labels?
) : EthereumUpstream(
@@ -107,8 +109,9 @@ open class EthereumGrpcUpstream(
private val defaultReader: Reader<JsonRpcRequest, JsonRpcResponse> = client.getReader()
var timeout = Defaults.timeout
private val ethereumSubscriptions = EthereumDshackleSubscriptions(chain, remote)
override fun getBlockchainApi(): ReactorBlockchainGrpc.ReactorBlockchainStub {
override fun getBlockchainApi(): ReactorBlockchainStub {
return remote
}
@@ -141,6 +144,10 @@ open class EthereumGrpcUpstream(
return upstreamStatus.getLabels()
}
override fun getUpstreamSubscriptions(): EthereumUpstreamSubscriptions {
return ethereumSubscriptions
}
override fun getMethods(): CallMethods {
return upstreamStatus.getCallMethods()
}

View File

@@ -32,6 +32,8 @@ import io.emeraldpay.dshackle.upstream.Upstream
import io.emeraldpay.dshackle.upstream.UpstreamAvailability
import io.emeraldpay.dshackle.upstream.calls.CallMethods
import io.emeraldpay.dshackle.upstream.ethereum.EthereumPosUpstream
import io.emeraldpay.dshackle.upstream.ethereum.EthereumUpstreamSubscriptions
import io.emeraldpay.dshackle.upstream.ethereum.subscribe.EthereumDshackleSubscriptions
import io.emeraldpay.dshackle.upstream.forkchoice.NoChoiceWithPriorityForkChoice
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcGrpcClient
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
@@ -107,6 +109,7 @@ open class EthereumPosGrpcUpstream(
private val defaultReader: Reader<JsonRpcRequest, JsonRpcResponse> = client.getReader()
var timeout = Defaults.timeout
private val ethereumSubscriptions = EthereumDshackleSubscriptions(chain, remote)
override fun start() {
}
@@ -174,4 +177,8 @@ open class EthereumPosGrpcUpstream(
override fun isGrpc(): Boolean {
return true
}
override fun getUpstreamSubscriptions(): EthereumUpstreamSubscriptions {
return ethereumSubscriptions
}
}

View File

@@ -16,12 +16,12 @@
package io.emeraldpay.dshackle.upstream.rpcclient
import io.emeraldpay.dshackle.reader.Reader
import io.emeraldpay.dshackle.upstream.ethereum.WsConnection
import io.emeraldpay.dshackle.upstream.ethereum.WsConnectionImpl
import io.emeraldpay.etherjar.rpc.RpcResponseError
import reactor.core.publisher.Mono
class JsonRpcWsClient(
private val ws: WsConnection
private val ws: WsConnectionImpl
) : Reader<JsonRpcRequest, JsonRpcResponse> {
override fun read(key: JsonRpcRequest): Mono<JsonRpcResponse> {
@@ -36,6 +36,6 @@ class JsonRpcWsClient(
)
)
}
return ws.call(key)
return ws.callRpc(key)
}
}

View File

@@ -0,0 +1,22 @@
/**
* Copyright (c) 2022 EmeraldPay, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.emeraldpay.dshackle.upstream.rpcclient
class JsonRpcWsMessage(
val result: ByteArray?,
val error: JsonRpcError?,
val subscriptionId: String,
)