ws subscription refactoring
added support of pending transactions subscription
This commit is contained in:
@@ -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())
|
||||
|
||||
101
src/main/kotlin/io/emeraldpay/dshackle/commons/DurableFlux.kt
Normal file
101
src/main/kotlin/io/emeraldpay/dshackle/commons/DurableFlux.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
152
src/main/kotlin/io/emeraldpay/dshackle/commons/ExpiringSet.kt
Normal file
152
src/main/kotlin/io/emeraldpay/dshackle/commons/ExpiringSet.kt
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) }
|
||||
|
||||
@@ -11,6 +11,7 @@ interface BlockValidator {
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmField
|
||||
val ALWAYS_VALID = AlwaysValid()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
}
|
||||
@@ -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>?
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -69,6 +69,11 @@ open class EthereumRpcUpstream(
|
||||
.subscribe(this::setStatus)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getUpstreamSubscriptions(): EthereumUpstreamSubscriptions {
|
||||
return connector.getUpstreamSubscriptions()
|
||||
}
|
||||
|
||||
override fun getHead(): Head {
|
||||
return connector.getHead()
|
||||
}
|
||||
|
||||
@@ -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"))
|
||||
}
|
||||
@@ -44,4 +44,6 @@ abstract class EthereumUpstream(
|
||||
override fun getLabels(): Collection<UpstreamsConfig.Labels> {
|
||||
return node?.let { listOf(it.labels) } ?: emptyList()
|
||||
}
|
||||
|
||||
abstract fun getUpstreamSubscriptions(): EthereumUpstreamSubscriptions
|
||||
}
|
||||
|
||||
@@ -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?
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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>
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
|
||||
@@ -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>> {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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() }
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,4 +98,8 @@ open class EthereumPosRpcUpstream(
|
||||
}
|
||||
return this as T
|
||||
}
|
||||
|
||||
override fun getUpstreamSubscriptions(): EthereumUpstreamSubscriptions {
|
||||
return connector.getUpstreamSubscriptions()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,4 +44,6 @@ abstract class EthereumPosUpstream(
|
||||
override fun getLabels(): Collection<UpstreamsConfig.Labels> {
|
||||
return node?.let { listOf(it.labels) } ?: emptyList()
|
||||
}
|
||||
|
||||
abstract fun getUpstreamSubscriptions(): EthereumUpstreamSubscriptions
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
Reference in New Issue
Block a user