remove unused specific tracks (#326)

merge ethereum interfaces with generic ones
This commit is contained in:
a10zn8
2023-10-24 15:16:35 +04:00
committed by GitHub
parent 1310c8b7df
commit 788db75b73
34 changed files with 75 additions and 3135 deletions

View File

@@ -22,7 +22,6 @@ import io.emeraldpay.api.proto.Common
import io.emeraldpay.api.proto.ReactorBlockchainGrpc
import io.emeraldpay.dshackle.Chain
import io.emeraldpay.dshackle.ChainValue
import io.emeraldpay.dshackle.SilentException
import io.emeraldpay.dshackle.config.spans.ProviderSpanHandler
import io.micrometer.core.instrument.Counter
import io.micrometer.core.instrument.Metrics
@@ -36,7 +35,6 @@ import org.springframework.stereotype.Service
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
import reactor.core.scheduler.Scheduler
import java.util.Locale
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.TimeUnit
@@ -46,11 +44,8 @@ class BlockchainRpc(
private val nativeCallStream: NativeCallStream,
private val nativeSubscribe: NativeSubscribe,
private val streamHead: StreamHead,
private val trackTx: List<TrackTx>,
private val trackAddress: List<TrackAddress>,
private val describe: Describe,
private val subscribeStatus: SubscribeStatus,
private val estimateFee: EstimateFee,
private val subscribeNodeStatus: SubscribeNodeStatus,
@Qualifier("rpcScheduler")
private val scheduler: Scheduler,
@@ -129,97 +124,6 @@ class BlockchainRpc(
).doOnError { failMetric.increment() }
}
override fun subscribeTxStatus(requestMono: Mono<BlockchainOuterClass.TxStatusRequest>): Flux<BlockchainOuterClass.TxStatus> {
return requestMono.subscribeOn(scheduler).flatMapMany { request ->
val chain = Chain.byId(request.chainValue)
val metrics = chainMetrics.get(chain)
metrics.subscribeTxMetric.increment()
try {
trackTx.find { it.isSupported(chain) }?.let { track ->
track.subscribe(request)
.doOnNext { metrics.subscribeHeadRespMetric.increment() }
.doOnError { failMetric.increment() }
} ?: Flux.error(SilentException.UnsupportedBlockchain(chain))
} catch (t: Throwable) {
log.error("Internal error during Tx Subscription", t)
failMetric.increment()
Flux.error(IllegalStateException("Internal Error"))
}
}
}
override fun subscribeBalance(requestMono: Mono<BlockchainOuterClass.BalanceRequest>): Flux<BlockchainOuterClass.AddressBalance> {
return requestMono.subscribeOn(scheduler).flatMapMany { request ->
val chain = Chain.byId(request.asset.chainValue)
val metrics = chainMetrics.get(chain)
metrics.subscribeBalanceMetric.increment()
val asset = request.asset.code.lowercase(Locale.getDefault())
try {
trackAddress.find { it.isSupported(chain, asset) }?.let { track ->
track.subscribe(request)
.doOnNext { metrics.subscribeBalanceRespMetric.increment() }
.doOnError { failMetric.increment() }
} ?: Flux.error<BlockchainOuterClass.AddressBalance>(SilentException.UnsupportedBlockchain(chain))
.doOnSubscribe {
log.error("Balance for $chain:$asset is not supported")
}
} catch (t: Throwable) {
log.error("Internal error during Balance Subscription", t)
failMetric.increment()
Flux.error(IllegalStateException("Internal Error"))
}
}
}
override fun getBalance(requestMono: Mono<BlockchainOuterClass.BalanceRequest>): Flux<BlockchainOuterClass.AddressBalance> {
return requestMono.subscribeOn(scheduler).flatMapMany { request ->
val chain = Chain.byId(request.asset.chainValue)
val metrics = chainMetrics.get(chain)
metrics.getBalanceMetric.increment()
val asset = request.asset.code.lowercase(Locale.getDefault())
val startTime = System.currentTimeMillis()
try {
trackAddress.find { it.isSupported(chain, asset) }?.let { track ->
track.getBalance(request)
.doOnNext {
metrics.getBalanceRespMetric.record(
System.currentTimeMillis() - startTime,
TimeUnit.MILLISECONDS,
)
}
} ?: Flux.error<BlockchainOuterClass.AddressBalance>(SilentException.UnsupportedBlockchain(chain))
.doOnSubscribe {
log.error("Balance for $chain:$asset is not supported")
}
} catch (t: Throwable) {
log.error("Internal error during Balance Request", t)
failMetric.increment()
Flux.error<BlockchainOuterClass.AddressBalance>(IllegalStateException("Internal Error"))
}
}
}
override fun estimateFee(request: Mono<BlockchainOuterClass.EstimateFeeRequest>): Mono<BlockchainOuterClass.EstimateFeeResponse> {
return request
.subscribeOn(scheduler)
.flatMap {
val chain = Chain.byId(it.chainValue)
val metrics = chainMetrics.get(chain)
metrics.estimateFeeMetric.increment()
val startTime = System.currentTimeMillis()
estimateFee.estimateFee(it).doFinally {
metrics.estimateFeeRespMetric.record(
System.currentTimeMillis() - startTime,
TimeUnit.MILLISECONDS,
)
}
}
.doOnError { t ->
log.error("Internal error during Fee Estimation", t)
failMetric.increment()
}
}
override fun describe(request: Mono<BlockchainOuterClass.DescribeRequest>): Mono<BlockchainOuterClass.DescribeResponse> {
describeMetric.increment()
return describe.describe(request)

View File

@@ -1,38 +0,0 @@
package io.emeraldpay.dshackle.rpc
import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.dshackle.Chain
import io.emeraldpay.dshackle.upstream.ChainFees
import io.emeraldpay.dshackle.upstream.MultistreamHolder
import io.grpc.Status
import io.grpc.StatusException
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Service
import reactor.core.publisher.Mono
@Service
class EstimateFee(
@Autowired private val multistreamHolder: MultistreamHolder,
) {
companion object {
private val log = LoggerFactory.getLogger(EstimateFee::class.java)
}
fun estimateFee(req: BlockchainOuterClass.EstimateFeeRequest): Mono<BlockchainOuterClass.EstimateFeeResponse> {
val chain = Chain.byId(req.chainValue)
val up = multistreamHolder.getUpstream(chain) ?: return Mono.error(
StatusException(
Status.UNAVAILABLE.withDescription("BLOCKCHAIN UNAVAILABLE: ${req.chainValue}"),
),
)
val mode = ChainFees.extractMode(req) ?: return Mono.error(
StatusException(
Status.UNAVAILABLE.withDescription("UNSUPPORTED MODE: ${req.mode.number}"),
),
)
return up.getFeeEstimation()
.estimate(mode, req.blocks)
}
}

View File

@@ -20,6 +20,7 @@ import com.fasterxml.jackson.databind.ObjectMapper
import com.google.protobuf.ByteString
import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.dshackle.BlockchainType
import io.emeraldpay.dshackle.BlockchainType.EVM_POS
import io.emeraldpay.dshackle.Chain
import io.emeraldpay.dshackle.Global
import io.emeraldpay.dshackle.Global.Companion.nullValue
@@ -44,9 +45,6 @@ import io.emeraldpay.dshackle.upstream.MultistreamHolder
import io.emeraldpay.dshackle.upstream.Selector
import io.emeraldpay.dshackle.upstream.calls.DefaultEthereumMethods
import io.emeraldpay.dshackle.upstream.calls.EthereumCallSelector
import io.emeraldpay.dshackle.upstream.ethereum.EthereumLikeMultistream
import io.emeraldpay.dshackle.upstream.ethereum.EthereumMultistream
import io.emeraldpay.dshackle.upstream.ethereum.EthereumPosMultiStream
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcError
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcException
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
@@ -84,17 +82,10 @@ open class NativeCall(
var rpcReaderFactory: RpcReaderFactory = RpcReaderFactory.default()
private val ethereumCallSelectors = EnumMap<Chain, EthereumCallSelector>(Chain::class.java)
companion object {
val casting: Map<BlockchainType, Class<out EthereumLikeMultistream>> = mapOf(
BlockchainType.EVM_POS to EthereumPosMultiStream::class.java,
BlockchainType.EVM_POW to EthereumMultistream::class.java,
)
}
@EventListener
fun onUpstreamChangeEvent(event: UpstreamChangeEvent) {
casting[BlockchainType.from(event.chain)]?.let { cast ->
multistreamHolder.getUpstream(event.chain).let { up ->
multistreamHolder.getUpstream(event.chain).let { up ->
if (BlockchainType.from(up.chain) == EVM_POS) {
ethereumCallSelectors.putIfAbsent(
event.chain,
EthereumCallSelector(up.caches),

View File

@@ -22,9 +22,9 @@ import io.emeraldpay.dshackle.BlockchainType
import io.emeraldpay.dshackle.Chain
import io.emeraldpay.dshackle.Global
import io.emeraldpay.dshackle.SilentException
import io.emeraldpay.dshackle.upstream.Multistream
import io.emeraldpay.dshackle.upstream.MultistreamHolder
import io.emeraldpay.dshackle.upstream.Selector
import io.emeraldpay.dshackle.upstream.ethereum.EthereumLikeMultistream
import io.emeraldpay.dshackle.upstream.ethereum.subscribe.json.HasUpstream
import io.emeraldpay.dshackle.upstream.signature.ResponseSigner
import io.grpc.Status
@@ -67,9 +67,9 @@ open class NativeSubscribe(
/**
* Try to proxy request subscription directly to the upstream dshackle instance.
* If not possible - performs subscription logic on the current instance
* @see EthereumLikeMultistream.tryProxy
* @see EthereumLikeMultistream.tryProxySubscribe
*/
val publisher = getUpstream(chain).tryProxy(matcher, request) ?: run {
val publisher = getUpstream(chain).tryProxySubscribe(matcher, request) ?: run {
val method = request.method
val params: Any? = request.payload?.takeIf { !it.isEmpty }?.let {
objectMapper.readValue(it.newInput(), Map::class.java)
@@ -103,8 +103,8 @@ open class NativeSubscribe(
log.error("Error during subscription to $method, chain $chain, params $params", it)
}
private fun getUpstream(chain: Chain): EthereumLikeMultistream =
multistreamHolder.getUpstream(chain).let { it as EthereumLikeMultistream }
private fun getUpstream(chain: Chain): Multistream =
multistreamHolder.getUpstream(chain)
fun convertToProto(holder: ResponseHolder): NativeSubscribeReplyItem {
if (holder.response is NativeSubscribeReplyItem) {

View File

@@ -1,30 +0,0 @@
/**
* Copyright (c) 2020 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.rpc
import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.dshackle.Chain
import reactor.core.publisher.Flux
/**
* Base interface to tracking balance on a single blockchain
*/
interface TrackAddress {
fun isSupported(chain: Chain, asset: String): Boolean
fun getBalance(request: BlockchainOuterClass.BalanceRequest): Flux<BlockchainOuterClass.AddressBalance>
fun subscribe(request: BlockchainOuterClass.BalanceRequest): Flux<BlockchainOuterClass.AddressBalance>
}

View File

@@ -1,303 +0,0 @@
/**
* Copyright (c) 2020 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.rpc
import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.api.proto.Common
import io.emeraldpay.api.proto.ReactorBlockchainGrpc
import io.emeraldpay.dshackle.BlockchainType
import io.emeraldpay.dshackle.Chain
import io.emeraldpay.dshackle.Defaults
import io.emeraldpay.dshackle.SilentException
import io.emeraldpay.dshackle.startup.UpstreamChangeEvent
import io.emeraldpay.dshackle.upstream.Capability
import io.emeraldpay.dshackle.upstream.MultistreamHolder
import io.emeraldpay.dshackle.upstream.Selector
import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinMultistream
import io.emeraldpay.dshackle.upstream.bitcoin.data.SimpleUnspent
import io.emeraldpay.dshackle.upstream.grpc.BitcoinGrpcUpstream
import org.apache.commons.lang3.StringUtils
import org.bitcoinj.params.MainNetParams
import org.bitcoinj.params.TestNet3Params
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.event.EventListener
import org.springframework.stereotype.Service
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
import java.math.BigInteger
import java.util.concurrent.ConcurrentHashMap
@Service
class TrackBitcoinAddress(
@Autowired private val multistreamHolder: MultistreamHolder,
) : TrackAddress {
companion object {
private val log = LoggerFactory.getLogger(TrackBitcoinAddress::class.java)
}
override fun isSupported(chain: Chain, asset: String): Boolean {
return (asset == "bitcoin" || asset == "btc" || asset == "satoshi") &&
BlockchainType.from(chain) == BlockchainType.BITCOIN && multistreamHolder.isAvailable(chain)
}
/**
* Keep tracking of the current state of local upstreams. True for a chain that has an upstream with balance data.
*/
private val localBalanceAvailable: MutableMap<Chain, Boolean> = ConcurrentHashMap()
/**
* Criteria for a remote grpc upstream that can provide a balance
*/
private val balanceUpstreamMatcher = Selector.MultiMatcher(
listOf(
Selector.GrpcMatcher(),
Selector.CapabilityMatcher(Capability.BALANCE),
),
)
@EventListener
fun onUpstreamChangeEvent(event: UpstreamChangeEvent) {
multistreamHolder.getUpstream(event.chain)?.let { mup ->
val available = mup.getAll().any { up ->
!up.isGrpc() && up.getCapabilities().contains(Capability.BALANCE)
}
setBalanceAvailability(event.chain, available)
}
}
fun setBalanceAvailability(chain: Chain, enabled: Boolean) {
localBalanceAvailable[chain] = enabled
}
/**
* @return true if the current instance has data sources to provide the balance
*/
fun isBalanceAvailable(chain: Chain): Boolean {
return localBalanceAvailable[chain] ?: false
}
fun allAddresses(api: BitcoinMultistream, request: BlockchainOuterClass.BalanceRequest): Flux<String> {
if (!request.hasAddress()) {
return Flux.empty()
}
return when {
request.address.hasAddressXpub() -> {
val xpubAddresses = api.getXpubAddresses()
?: return Flux.error(IllegalStateException("Xpub verification is not available"))
val addressXpub = request.address.addressXpub
if (StringUtils.isEmpty(addressXpub.xpub)) {
return Flux.error(IllegalArgumentException("xpub string is empty"))
}
val xpub = addressXpub.xpub
val start = Math.max(0, addressXpub.start).toInt()
val limit = Math.min(100, Math.max(1, addressXpub.limit)).toInt()
xpubAddresses.activeAddresses(xpub, start, limit)
.map { it.toString() }
.doOnError { t -> log.error("Failed to process xpub. ${t.javaClass}:${t.message}") }
}
request.address.hasAddressSingle() -> {
Flux.just(request.address.addressSingle.address)
}
request.address.hasAddressMulti() -> {
Flux.fromIterable(
request.address.addressMulti.addressesList
.map { addr -> addr.address }
// TODO why sorted?
.sorted(),
)
}
else -> Flux.error(IllegalArgumentException("Unsupported address type"))
}
}
fun requestBalances(
chain: Chain,
api: BitcoinMultistream,
addresses: Flux<String>,
includeUtxo: Boolean,
): Flux<AddressBalance> {
return addresses
.map { Address(chain, it) }
.flatMap { address ->
balanceForAddress(api, address, includeUtxo)
}
}
fun balanceForAddress(api: BitcoinMultistream, address: Address, includeUtxo: Boolean): Mono<AddressBalance> {
return api.getReader()
.listUnspent(address.bitcoinAddress)
.map { unspent ->
totalUnspent(address, includeUtxo, unspent)
}
.switchIfEmpty(
Mono.just(0).map {
AddressBalance(address, BigInteger.ZERO)
},
)
.onErrorResume { t ->
log.error("Failed to get unspent", t)
Mono.empty()
}
}
fun totalUnspent(address: Address, includeUtxo: Boolean, unspent: List<SimpleUnspent>): AddressBalance {
return if (unspent.isEmpty()) {
AddressBalance(address, BigInteger.ZERO)
} else {
unspent.map {
AddressBalance(
address,
BigInteger.valueOf(it.value),
if (includeUtxo) {
listOf(BalanceUtxo(it.txid, it.vout, it.value))
} else {
emptyList()
},
)
}.reduce { a, b -> a.plus(b) }
}
}
fun getBalanceGrpc(api: BitcoinMultistream): Mono<ReactorBlockchainGrpc.ReactorBlockchainStub> {
val ups = api.getApiSource(balanceUpstreamMatcher)
ups.request(1)
return Mono.from(ups)
.map { up ->
up.cast(BitcoinGrpcUpstream::class.java).remote
}
.timeout(Defaults.timeoutInternal, Mono.empty())
.switchIfEmpty(
Mono.fromCallable {
log.warn("No upstream providing balance for ${api.chain}")
}
.then(Mono.error(SilentException.DataUnavailable("BALANCE"))),
)
}
fun getRemoteBalance(
api: BitcoinMultistream,
request: BlockchainOuterClass.BalanceRequest,
): Flux<BlockchainOuterClass.AddressBalance> {
return getBalanceGrpc(api).flatMapMany { remote ->
remote.getBalance(request)
}
}
fun subscribeRemoteBalance(
api: BitcoinMultistream,
request: BlockchainOuterClass.BalanceRequest,
): Flux<BlockchainOuterClass.AddressBalance> {
return getBalanceGrpc(api).flatMapMany { remote ->
remote.subscribeBalance(request)
}
}
override fun getBalance(request: BlockchainOuterClass.BalanceRequest): Flux<BlockchainOuterClass.AddressBalance> {
val chain = Chain.byId(request.asset.chainValue)
val upstream = multistreamHolder.getUpstream(chain)?.cast(BitcoinMultistream::class.java)
?: return Flux.error(SilentException.UnsupportedBlockchain(request.asset.chainValue))
return if (isBalanceAvailable(chain)) {
val addresses = allAddresses(upstream, request)
requestBalances(chain, upstream, addresses, request.includeUtxo)
.map(this@TrackBitcoinAddress::buildResponse)
.doOnError { t ->
log.error("Failed to get balance", t)
}
} else {
getRemoteBalance(upstream, request)
.doOnError { t ->
log.error("Failed to get balance from remote", t)
}
}
}
override fun subscribe(request: BlockchainOuterClass.BalanceRequest): Flux<BlockchainOuterClass.AddressBalance> {
val chain = Chain.byId(request.asset.chainValue)
val upstream = multistreamHolder.getUpstream(chain)?.cast(BitcoinMultistream::class.java)
?: return Flux.error(SilentException.UnsupportedBlockchain(request.asset.chainValue))
if (isBalanceAvailable(chain)) {
val addresses = allAddresses(upstream, request).cache()
val following = upstream.getHead().getFlux()
.flatMap {
requestBalances(chain, upstream, Flux.from(addresses), request.includeUtxo)
}
val last = HashMap<String, BigInteger>()
val result = following
.filter { curr ->
val prev = last[curr.address.address]
// TODO utxo can change without changing balance
val changed = prev == null || curr.balance != prev
if (changed) {
last[curr.address.address] = curr.balance
}
changed
}
return result.map(this@TrackBitcoinAddress::buildResponse)
} else {
return subscribeRemoteBalance(upstream, request)
}
}
private fun buildResponse(address: AddressBalance): BlockchainOuterClass.AddressBalance {
return BlockchainOuterClass.AddressBalance.newBuilder()
.setBalance(address.balance.toString(10))
.setAsset(
Common.Asset.newBuilder()
.setChainValue(address.address.chain.id)
.setCode("BTC"),
)
.setAddress(Common.SingleAddress.newBuilder().setAddress(address.address.address))
.addAllUtxo(
address.utxo.map { utxo ->
BlockchainOuterClass.Utxo.newBuilder()
.setBalance(utxo.value.toString())
.setIndex(utxo.vout.toLong())
.setTxId(utxo.txid)
.build()
},
)
.build()
}
open class AddressBalance(
val address: Address,
var balance: BigInteger = BigInteger.ZERO,
var utxo: List<BalanceUtxo> = emptyList(),
) {
constructor(chain: Chain, address: String, balance: BigInteger) : this(Address(chain, address), balance)
fun plus(other: AddressBalance) = AddressBalance(address, balance + other.balance, utxo.plus(other.utxo))
}
open class BalanceUtxo(val txid: String, val vout: Int, val value: Long)
// TODO use bitcoin class for address
class Address(val chain: Chain, val address: String) {
val network = if (chain == Chain.BITCOIN__MAINNET) {
MainNetParams()
} else {
TestNet3Params()
}
val bitcoinAddress = org.bitcoinj.core.Address.fromString(
network,
address,
)
}
}

View File

@@ -1,177 +0,0 @@
/**
* Copyright (c) 2020 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.rpc
import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.api.proto.Common
import io.emeraldpay.dshackle.BlockchainType
import io.emeraldpay.dshackle.Chain
import io.emeraldpay.dshackle.SilentException
import io.emeraldpay.dshackle.upstream.MultistreamHolder
import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinMultistream
import io.emeraldpay.dshackle.upstream.bitcoin.ExtractBlock
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Service
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
import java.math.BigInteger
import java.time.Duration
import java.time.Instant
import kotlin.math.max
import kotlin.math.min
@Service
class TrackBitcoinTx(
@Autowired private val multistreamHolder: MultistreamHolder,
) : TrackTx {
companion object {
private val log = LoggerFactory.getLogger(TrackBitcoinTx::class.java)
}
override fun isSupported(chain: Chain): Boolean {
return BlockchainType.from(chain) == BlockchainType.BITCOIN && multistreamHolder.isAvailable(chain)
}
override fun subscribe(request: BlockchainOuterClass.TxStatusRequest): Flux<BlockchainOuterClass.TxStatus> {
val chain = Chain.byId(request.chainValue)
val upstream = multistreamHolder.getUpstream(chain)?.cast(BitcoinMultistream::class.java)
?: return Flux.error(SilentException.UnsupportedBlockchain(chain))
val txid = request.txId
val confirmations = max(min(1, request.confirmationLimit), 12)
return subscribe(chain, upstream, txid)
.takeUntil { tx ->
tx.confirmations >= confirmations
}.map(this::asProto)
}
fun subscribe(chain: Chain, upstream: BitcoinMultistream, txid: String): Flux<TxStatus> {
return loadExisting(upstream, txid)
.flatMapMany { status ->
if (status.mined) {
// Head almost always knows the current height, so it can continue with calculating confirmations
// without publishing an empty TxStatus first
continueWithMined(upstream, status)
} else {
loadMempool(upstream, txid)
.flatMapMany { tx ->
val next = if (tx.found) {
untilMined(upstream, tx)
} else {
untilFound(chain, upstream, txid)
}
// fist provide the current status, then updates
Flux.concat(Mono.just(tx), next)
}
}
}
}
fun continueWithMined(upstream: BitcoinMultistream, status: TxStatus): Flux<TxStatus> {
return upstream.getReader().getBlock(status.blockHash!!)
.map { block ->
TxStatus(
status.txid,
true,
ExtractBlock.getHeight(block),
true,
status.blockHash,
ExtractBlock.getTime(block),
ExtractBlock.getDifficulty(block),
)
}.flatMapMany { tx ->
withConfirmations(upstream, tx)
}
}
fun untilFound(chain: Chain, upstream: BitcoinMultistream, txid: String): Flux<TxStatus> {
return Flux.interval(Duration.ofSeconds(1))
.take(Duration.ofMinutes(10))
.flatMap { loadMempool(upstream, txid) }
.skipUntil { it.found }
.flatMap { subscribe(chain, upstream, txid) }
.doOnError { t ->
log.error("Failed to wait until found", t)
}
}
fun untilMined(upstream: BitcoinMultistream, tx: TxStatus): Mono<TxStatus> {
return upstream.getHead().getFlux().flatMap {
loadExisting(upstream, tx.txid)
.filter { it.mined }
}.single()
}
fun withConfirmations(upstream: BitcoinMultistream, tx: TxStatus): Flux<TxStatus> {
return upstream.getHead().getFlux().map {
tx.withHead(it.height)
}
}
fun loadExisting(api: BitcoinMultistream, txid: String): Mono<TxStatus> {
val mined = api.getReader().getTx(txid)
return mined.map {
val block = it["blockhash"] as String?
TxStatus(txid, found = true, mined = block != null, blockHash = block, height = ExtractBlock.getHeight(it))
}
}
fun loadMempool(upstream: BitcoinMultistream, txid: String): Mono<TxStatus> {
val mempool = upstream.getReader().getMempool().get()
return mempool.map {
if (it.contains(txid)) {
TxStatus(txid, found = true, mined = false)
} else {
TxStatus(txid, found = false, mined = false)
}
}
}
private fun asProto(tx: TxStatus): BlockchainOuterClass.TxStatus {
val data = BlockchainOuterClass.TxStatus.newBuilder()
.setTxId(tx.txid)
.setConfirmations(tx.confirmations.toInt())
data.broadcasted = tx.found
val isMined = tx.mined
data.mined = isMined
if (isMined) {
data.setBlock(
Common.BlockInfo.newBuilder()
.setBlockId(tx.blockHash!!.substring(2))
.setTimestamp(tx.blockTime!!.toEpochMilli())
.setHeight(tx.height!!),
)
}
return data.build()
}
class TxStatus(
val txid: String,
val found: Boolean = false,
val height: Long? = null,
val mined: Boolean = false,
val blockHash: String? = null,
val blockTime: Instant? = null,
val blockTotalDifficulty: BigInteger? = null,
val confirmations: Long = 0,
) {
fun withHead(headHeight: Long) =
TxStatus(txid, found, height, mined, blockHash, blockTime, blockTotalDifficulty, headHeight - height!! + 1)
}
}

View File

@@ -1,155 +0,0 @@
/**
* Copyright (c) 2021 EmeraldPay, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.emeraldpay.dshackle.rpc
import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.api.proto.Common
import io.emeraldpay.dshackle.BlockchainType
import io.emeraldpay.dshackle.Chain
import io.emeraldpay.dshackle.SilentException
import io.emeraldpay.dshackle.config.TokensConfig
import io.emeraldpay.dshackle.upstream.MultistreamHolder
import io.emeraldpay.dshackle.upstream.Selector
import io.emeraldpay.dshackle.upstream.ethereum.ERC20Balance
import io.emeraldpay.dshackle.upstream.ethereum.EthereumPosMultiStream
import io.emeraldpay.etherjar.domain.Address
import io.emeraldpay.etherjar.domain.EventId
import io.emeraldpay.etherjar.erc20.ERC20Token
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Service
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
import java.math.BigInteger
import java.util.Locale
import javax.annotation.PostConstruct
@Service
class TrackERC20Address(
@Autowired private val multistreamHolder: MultistreamHolder,
@Autowired private val tokensConfig: TokensConfig,
) : TrackAddress {
companion object {
private val log = LoggerFactory.getLogger(TrackERC20Address::class.java)
}
var erc20Balance: ERC20Balance = ERC20Balance()
private val ethereumAddresses = EthereumAddresses()
private val tokens: MutableMap<TokenId, TokenDefinition> = HashMap()
@PostConstruct
fun init() {
tokensConfig.tokens.forEach { token ->
val chain = token.blockchain!!
val asset = token.name!!.lowercase(Locale.getDefault())
val id = TokenId(chain, asset)
val definition = TokenDefinition(
chain,
asset,
ERC20Token(Address.from(token.address)),
)
tokens[id] = definition
log.info("Enable ERC20 balance for $chain:$asset")
}
}
override fun isSupported(chain: Chain, asset: String): Boolean {
return tokens.containsKey(TokenId(chain, asset.lowercase(Locale.getDefault()))) &&
(BlockchainType.from(chain) == BlockchainType.EVM_POS || BlockchainType.from(chain) == BlockchainType.EVM_POW) && multistreamHolder.isAvailable(chain)
}
override fun getBalance(request: BlockchainOuterClass.BalanceRequest): Flux<BlockchainOuterClass.AddressBalance> {
val chain = Chain.byId(request.asset.chainValue)
val asset = request.asset.code.lowercase(Locale.getDefault())
val tokenDefinition = tokens[TokenId(chain, asset)] ?: return Flux.empty()
return ethereumAddresses.extract(request.address)
.map { TrackedAddress(chain, it, tokenDefinition.token, tokenDefinition.name) }
.flatMap { addr -> getBalance(addr).map(addr::withBalance) }
.map { buildResponse(it) }
}
override fun subscribe(request: BlockchainOuterClass.BalanceRequest): Flux<BlockchainOuterClass.AddressBalance> {
val chain = Chain.byId(request.asset.chainValue)
val asset = request.asset.code.lowercase(Locale.getDefault())
val tokenDefinition = tokens[TokenId(chain, asset)] ?: return Flux.empty()
val logs = getUpstream(chain)
.getEgressSubscription().logs
.create(
listOf(tokenDefinition.token.contract),
listOf(EventId.fromSignature("Transfer", "address", "address", "uint256")),
).connect(Selector.empty)
return ethereumAddresses.extract(request.address)
.map { TrackedAddress(chain, it, tokenDefinition.token, tokenDefinition.name) }
.flatMap { addr ->
val current = getBalance(addr)
val updates = logs
.filter {
it.topics.size >= 3 && (Address.extract(it.topics[1]) == addr.address || Address.extract(it.topics[2]) == addr.address)
}
.distinctUntilChanged {
// check it once per block
it.blockHash
}
.flatMap {
// make sure we use actual balance, don't trust event blindly
getBalance(addr)
}
Flux.concat(current, updates)
.distinctUntilChanged()
.map { addr.withBalance(it) }
}
.map { buildResponse(it) }
}
fun getBalance(addr: TrackedAddress): Mono<BigInteger> {
val upstream = getUpstream(addr.chain)
return erc20Balance.getBalance(upstream, addr.token, addr.address)
}
fun getUpstream(chain: Chain): EthereumPosMultiStream {
return multistreamHolder.getUpstream(chain)?.cast(EthereumPosMultiStream::class.java)
?: throw SilentException.UnsupportedBlockchain(chain)
}
private fun buildResponse(address: TrackedAddress): BlockchainOuterClass.AddressBalance {
return BlockchainOuterClass.AddressBalance.newBuilder()
.setBalance(address.balance!!.toString(10))
.setAsset(
Common.Asset.newBuilder()
.setChainValue(address.chain.id)
.setCode(address.tokenName.uppercase(Locale.getDefault())),
)
.setAddress(Common.SingleAddress.newBuilder().setAddress(address.address.toHex()))
.build()
}
class TrackedAddress(
val chain: Chain,
val address: Address,
val token: ERC20Token,
val tokenName: String,
val balance: BigInteger? = null,
) {
fun withBalance(balance: BigInteger) = TrackedAddress(chain, address, token, tokenName, balance)
}
data class TokenId(val chain: Chain, val name: String)
data class TokenDefinition(val chain: Chain, val name: String, val token: ERC20Token)
}

View File

@@ -1,146 +0,0 @@
/**
* Copyright (c) 2020 EmeraldPay, Inc
* Copyright (c) 2019 ETCDEV GmbH
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.emeraldpay.dshackle.rpc
import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.api.proto.Common
import io.emeraldpay.dshackle.BlockchainType
import io.emeraldpay.dshackle.Chain
import io.emeraldpay.dshackle.Defaults
import io.emeraldpay.dshackle.SilentException
import io.emeraldpay.dshackle.upstream.MultistreamHolder
import io.emeraldpay.dshackle.upstream.ethereum.EthereumPosMultiStream
import io.emeraldpay.etherjar.domain.Address
import io.emeraldpay.etherjar.domain.Wei
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Service
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
import java.util.Locale
@Service
class TrackEthereumAddress(
@Autowired private val multistreamHolder: MultistreamHolder,
) : TrackAddress {
private val log = LoggerFactory.getLogger(TrackEthereumAddress::class.java)
private val ethereumAddresses = EthereumAddresses()
override fun isSupported(chain: Chain, asset: String): Boolean {
return asset == "ether" &&
(BlockchainType.from(chain) == BlockchainType.EVM_POS || BlockchainType.from(chain) == BlockchainType.EVM_POW) && multistreamHolder.isAvailable(chain)
}
override fun getBalance(request: BlockchainOuterClass.BalanceRequest): Flux<BlockchainOuterClass.AddressBalance> {
return initAddress(request)
.flatMap { a -> getBalance(a).map { a.withBalance(it) } }
.map { buildResponse(it) }
}
override fun subscribe(request: BlockchainOuterClass.BalanceRequest): Flux<BlockchainOuterClass.AddressBalance> {
val chain = Chain.byId(request.asset.chainValue)
val head = multistreamHolder.getUpstream(chain)?.getHead()?.getFlux() ?: Flux.empty()
val balances = initAddress(request)
.flatMap { tracked ->
val current = getBalance(tracked)
.map {
tracked.withBalance(it)
}
val updates = head
.flatMap {
getBalance(tracked)
}.map {
tracked.withBalance(it)
}
Flux.concat(current, updates)
.distinctUntilChanged {
it.balance ?: Wei.ZERO
}
}
.doOnError { t ->
if (t is SilentException) {
if (t is SilentException.UnsupportedBlockchain) {
log.warn("Unsupported blockchain: ${t.blockchainId}")
}
log.debug("Failed to process subscription", t)
} else {
log.warn("Failed to process subscription", t)
}
}
return balances.map {
buildResponse(it)
}
}
fun getUpstream(chain: Chain): EthereumPosMultiStream {
return multistreamHolder.getUpstream(chain)?.cast(EthereumPosMultiStream::class.java)
?: throw SilentException.UnsupportedBlockchain(chain)
}
private fun initAddress(request: BlockchainOuterClass.BalanceRequest): Flux<TrackedAddress> {
val chain = Chain.byId(request.asset.chainValue)
if (!multistreamHolder.isAvailable(chain)) {
return Flux.error(SilentException.UnsupportedBlockchain(request.asset.chainValue))
}
if (request.asset.code.lowercase(Locale.getDefault()) != "ether") {
return Flux.error(SilentException("Unsupported asset ${request.asset.code}"))
}
return ethereumAddresses.extract(request.address).map {
TrackedAddress(chain, it)
}
}
private fun createAddress(address: Common.SingleAddress, chain: Chain): TrackedAddress {
val addressParsed = Address.from(address.address)
return TrackedAddress(
chain,
addressParsed,
)
}
fun getBalance(addr: TrackedAddress): Mono<Wei> {
return getUpstream(addr.chain)
.getReader()
.balance()
.read(addr.address)
.timeout(Defaults.timeout)
.map { it.data }
}
private fun buildResponse(address: TrackedAddress): BlockchainOuterClass.AddressBalance {
return BlockchainOuterClass.AddressBalance.newBuilder()
.setBalance(address.balance!!.amount!!.toString(10))
.setAsset(
Common.Asset.newBuilder()
.setChainValue(address.chain.id)
.setCode("ETHER"),
)
.setAddress(Common.SingleAddress.newBuilder().setAddress(address.address.toHex()))
.build()
}
class TrackedAddress(
val chain: Chain,
val address: Address,
val balance: Wei? = null,
) {
fun withBalance(balance: Wei) = TrackedAddress(chain, address, balance)
}
}

View File

@@ -1,390 +0,0 @@
/**
* Copyright (c) 2020 EmeraldPay, Inc
* Copyright (c) 2019 ETCDEV GmbH
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.emeraldpay.dshackle.rpc
import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.api.proto.Common
import io.emeraldpay.dshackle.BlockchainType
import io.emeraldpay.dshackle.Chain
import io.emeraldpay.dshackle.SilentException
import io.emeraldpay.dshackle.data.BlockContainer
import io.emeraldpay.dshackle.data.TxId
import io.emeraldpay.dshackle.upstream.MultistreamHolder
import io.emeraldpay.dshackle.upstream.ethereum.EthereumPosMultiStream
import io.emeraldpay.dshackle.upstream.ethereum.json.BlockJson
import io.emeraldpay.dshackle.upstream.ethereum.json.TransactionJsonSnapshot
import io.emeraldpay.etherjar.domain.BlockHash
import io.emeraldpay.etherjar.domain.TransactionId
import io.emeraldpay.etherjar.rpc.RpcException
import io.emeraldpay.etherjar.rpc.json.TransactionRefJson
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.stereotype.Service
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
import reactor.core.scheduler.Scheduler
import reactor.util.retry.Retry
import java.math.BigInteger
import java.time.Duration
import java.time.Instant
import kotlin.math.max
import kotlin.math.min
@Service
class TrackEthereumTx(
private val multistreamHolder: MultistreamHolder,
@Qualifier("trackTxScheduler")
private val scheduler: Scheduler,
) : TrackTx {
companion object {
private val ZERO_BLOCK = BlockHash.from("0x0000000000000000000000000000000000000000000000000000000000000000")
private val TRACK_TTL = Duration.ofHours(1)
private val NOT_FOUND_TRACK_TTL = Duration.ofMinutes(1)
private val NOT_MINED_TRACK_TTL = NOT_FOUND_TRACK_TTL.multipliedBy(2)
}
private val log = LoggerFactory.getLogger(TrackEthereumTx::class.java)
override fun isSupported(chain: Chain): Boolean {
return (BlockchainType.from(chain) == BlockchainType.EVM_POS || BlockchainType.from(chain) == BlockchainType.EVM_POW) && multistreamHolder.isAvailable(chain)
}
override fun subscribe(request: BlockchainOuterClass.TxStatusRequest): Flux<BlockchainOuterClass.TxStatus> {
val base = prepareTracking(request)
val up = getUpstream(base.chain)
return update(base)
.defaultIfEmpty(base)
.flatMapMany {
Flux.concat(Mono.just(it), subscribe(it, up))
.distinctUntilChanged(TxDetails::status)
.map(this@TrackEthereumTx::asProto)
.subscribeOn(scheduler)
}
.doOnError { t ->
log.error("Subscription error", t)
}
}
fun getUpstream(chain: Chain): EthereumPosMultiStream {
return multistreamHolder.getUpstream(chain)?.cast(EthereumPosMultiStream::class.java)
?: throw SilentException.UnsupportedBlockchain(chain)
}
fun subscribe(base: TxDetails, up: EthereumPosMultiStream): Flux<TxDetails> {
var latestTx = base
val untilFound = Mono.just(latestTx)
.subscribeOn(scheduler)
.map {
// replace with the latest value, it may be already found
latestTx
}
.flatMap { latest ->
if (!latest.status.found) {
update(latest).defaultIfEmpty(latestTx)
} else {
Mono.just(latest)
}
}
.flatMap { received ->
if (!received.status.found) {
Mono.error(SilentException("Retry not found"))
} else {
Mono.just(received)
}
}
.retryWhen(
Retry.fixedDelay(10, Duration.ofSeconds(2)),
)
.onErrorResume { Mono.empty() }
val inBlocks = up.getHead().getFlux()
.subscribeOn(scheduler)
.flatMap { block ->
onNewBlock(latestTx, block)
}
return Flux.merge(untilFound, inBlocks)
.takeUntil(TxDetails::shouldClose)
.doOnNext { newTx ->
latestTx = newTx
}
}
fun onNewBlock(tx: TxDetails, block: BlockContainer): Mono<TxDetails> {
val txid = TxId.from(tx.txid)
if (!tx.status.mined) {
val justMined = block.transactions.contains(txid)
return if (justMined) {
Mono.just(
tx.withStatus(
mined = true,
found = true,
confirmations = 1,
height = block.height,
blockTime = block.timestamp,
blockTotalDifficulty = block.difficulty,
blockHash = BlockHash(block.hash.value),
),
)
} else {
update(tx)
}
} else {
// verify if it's still on chain
// TODO head is supposed to erase block when it was replaced, so can safely recalc here
return update(tx)
}
}
private fun update(tx: TxDetails): Mono<TxDetails> {
val initialStatus = tx.status
val upstream = getUpstream(tx.chain)
return upstream.getReader()
.txByHash().read(tx.txid)
.onErrorResume(RpcException::class.java) { t ->
log.warn("Upstream error, ignoring. {}", t.rpcMessage)
Mono.empty()
}
.flatMap { updateFromBlock(upstream, tx, it) }
.doOnError { t ->
log.error("Failed to load tx block", t)
}
.switchIfEmpty(Mono.just(tx.withStatus(found = false)))
.filter { current ->
initialStatus != current.status || current.shouldClose()
}
}
fun prepareTracking(request: BlockchainOuterClass.TxStatusRequest): TxDetails {
val chain = Chain.byId(request.chainValue)
if (!isSupported(chain)) {
throw SilentException.UnsupportedBlockchain(request.chainValue)
}
val details = TxDetails(
chain,
Instant.now(),
TransactionId.from(request.txId),
min(max(1, request.confirmationLimit), 100),
)
return details
}
fun setBlockDetails(tx: TxDetails, block: BlockJson<TransactionRefJson>): TxDetails {
return if (block.number != null && block.totalDifficulty != null) {
tx.withStatus(
blockTotalDifficulty = block.totalDifficulty,
blockTime = block.timestamp,
)
} else {
tx.withStatus(
mined = false,
)
}
}
private fun loadWeight(tx: TxDetails): Mono<TxDetails> {
val upstream = getUpstream(tx.chain)
if (tx.status.blockHash == null) {
return Mono.empty()
}
return upstream.getReader()
.blocksByHashParsed().read(tx.status.blockHash)
.map { block ->
setBlockDetails(tx, block)
}.doOnError { t ->
log.warn("Failed to update weight", t)
}
}
fun updateFromBlock(upstream: EthereumPosMultiStream, tx: TxDetails, blockTx: TransactionJsonSnapshot): Mono<TxDetails> {
return if (blockTx.blockNumber != null && blockTx.blockHash != null && blockTx.blockHash != ZERO_BLOCK) {
val updated = tx.withStatus(
blockHash = blockTx.blockHash,
height = blockTx.blockNumber,
found = true,
mined = true,
confirmations = 1,
)
upstream.getHead().getFlux().next().map { head ->
val height = updated.status.height
if (height == null || head.height < height) {
updated
} else {
updated.withStatus(
confirmations = head.height - height + 1,
)
}
}.doOnError { t ->
log.error("Unable to load head details", t)
}.flatMap(this::loadWeight)
} else {
Mono.just(
tx.withStatus(
found = true,
mined = false,
),
)
}
}
private fun asProto(tx: TxDetails): BlockchainOuterClass.TxStatus {
val data = BlockchainOuterClass.TxStatus.newBuilder()
.setTxId(tx.txid.toHex())
.setConfirmations(tx.status.confirmations.toInt())
data.broadcasted = tx.status.found
val isMined = tx.status.mined
data.mined = isMined
if (isMined) {
data.setBlock(
Common.BlockInfo.newBuilder()
.setBlockId(tx.status.blockHash!!.toHex().substring(2))
.setTimestamp(tx.status.blockTime!!.toEpochMilli())
.setHeight(tx.status.height!!),
)
}
return data.build()
}
class TxDetails(
val chain: Chain,
val since: Instant,
val txid: TransactionId,
val maxConfirmations: Int,
val status: TxStatus,
) {
constructor(
chain: Chain,
since: Instant,
txid: TransactionId,
maxConfirmations: Int,
) : this(chain, since, txid, maxConfirmations, TxStatus())
fun copy(
since: Instant = this.since,
status: TxStatus = this.status,
) = TxDetails(chain, since, txid, maxConfirmations, status)
fun withStatus(
found: Boolean = this.status.found,
height: Long? = this.status.height,
mined: Boolean = this.status.mined,
blockHash: BlockHash? = this.status.blockHash,
blockTime: Instant? = this.status.blockTime,
blockTotalDifficulty: BigInteger? = this.status.blockTotalDifficulty,
confirmations: Long = this.status.confirmations,
): TxDetails {
return copy(
status = this.status.copy(
found,
height,
mined,
blockHash,
blockTime,
blockTotalDifficulty,
confirmations,
),
)
}
fun shouldClose(): Boolean {
return maxConfirmations <= this.status.confirmations ||
since.isBefore(Instant.now().minus(TRACK_TTL)) ||
(!status.found && since.isBefore(Instant.now().minus(NOT_FOUND_TRACK_TTL))) ||
(!status.mined && since.isBefore(Instant.now().minus(NOT_MINED_TRACK_TTL)))
}
override fun toString(): String {
return "TxDetails(chain=$chain, txid=$txid, status=$status)"
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is TxDetails) return false
if (chain != other.chain) return false
if (since != other.since) return false
if (txid != other.txid) return false
if (maxConfirmations != other.maxConfirmations) return false
if (status != other.status) return false
return true
}
override fun hashCode(): Int {
var result = chain.hashCode()
result = 31 * result + since.hashCode()
result = 31 * result + txid.hashCode()
result = 31 * result + status.hashCode()
return result
}
}
class TxStatus(
val found: Boolean = false,
val height: Long? = null,
val mined: Boolean = false,
val blockHash: BlockHash? = null,
val blockTime: Instant? = null,
val blockTotalDifficulty: BigInteger? = null,
val confirmations: Long = 0,
) {
fun copy(
found: Boolean = this.found,
height: Long? = this.height,
mined: Boolean = this.mined,
blockHash: BlockHash? = this.blockHash,
blockTime: Instant? = this.blockTime,
blockTotalDifficulty: BigInteger? = this.blockTotalDifficulty,
confirmation: Long = this.confirmations,
) = TxStatus(found, height, mined, blockHash, blockTime, blockTotalDifficulty, confirmation)
fun clean() = TxStatus(false, null, false, null, null, null, 0)
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as TxStatus
if (found != other.found) return false
if (height != other.height) return false
if (mined != other.mined) return false
if (blockHash != other.blockHash) return false
if (blockTime != other.blockTime) return false
if (blockTotalDifficulty != other.blockTotalDifficulty) return false
if (confirmations != other.confirmations) return false
return true
}
override fun hashCode(): Int {
var result = found.hashCode()
result = 31 * result + (height?.hashCode() ?: 0)
result = 31 * result + (blockHash?.hashCode() ?: 0)
return result
}
override fun toString(): String {
return "TxStatus(found=$found, height=$height, mined=$mined, blockHash=$blockHash, blockTime=$blockTime, blockTotalDifficulty=$blockTotalDifficulty, confirmations=$confirmations)"
}
}
}

View File

@@ -1,25 +0,0 @@
/**
* Copyright (c) 2020 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.rpc
import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.dshackle.Chain
import reactor.core.publisher.Flux
interface TrackTx {
fun isSupported(chain: Chain): Boolean
fun subscribe(request: BlockchainOuterClass.TxStatusRequest): Flux<BlockchainOuterClass.TxStatus>
}

View File

@@ -0,0 +1,3 @@
package io.emeraldpay.dshackle.upstream
interface CachingReader

View File

@@ -16,6 +16,7 @@
*/
package io.emeraldpay.dshackle.upstream
import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.dshackle.Chain
import io.emeraldpay.dshackle.cache.Caches
import io.emeraldpay.dshackle.cache.CachesEnabled
@@ -198,8 +199,6 @@ abstract class Multistream(
return FilteredApis(chain, upstreams, matcher, i)
}
abstract fun getFeeEstimation(): ChainFees
/**
* Finds an API that leverages caches and other optimizations/transformations of the request.
*/
@@ -475,6 +474,14 @@ abstract class Multistream(
abstract fun makeLagObserver(): HeadLagObserver
open fun tryProxySubscribe(matcher: Selector.Matcher, request: BlockchainOuterClass.NativeSubscribeRequest): Flux<out Any>? = null
abstract fun getCachingReader(): CachingReader?
abstract fun getHead(mather: Selector.Matcher): Head
abstract fun getEnrichedHead(mather: Selector.Matcher): Head
// --------------------------------------------------------------------------------------------------------
class UpstreamStatus(val upstream: Upstream, val status: UpstreamAvailability)

View File

@@ -1,146 +0,0 @@
/**
* Copyright (c) 2021 EmeraldPay, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.emeraldpay.dshackle.upstream.bitcoin
import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.dshackle.upstream.AbstractChainFees
import io.emeraldpay.dshackle.upstream.ChainFees
import org.slf4j.LoggerFactory
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
import java.math.BigDecimal
import java.util.function.Function
class BitcoinFees(
upstreams: BitcoinMultistream,
private val reader: BitcoinReader,
heightLimit: Int,
) : AbstractChainFees<BitcoinFees.TxFee, Map<String, Any>, String, Map<String, Any>>(heightLimit, upstreams, extractTx), ChainFees {
companion object {
private val log = LoggerFactory.getLogger(BitcoinFees::class.java)
private val extractTx = { block: Map<String, Any> ->
block["tx"]
?.let { it as List<String> }
// drop first tx which is a miner reward
?.let { if (it.isEmpty()) it else it.drop(1) }
?: emptyList()
}
}
fun calculateFee(tx: Map<String, Any>): Mono<Long> {
val outAmount = extractVOuts(tx).reduce { acc, l -> (acc ?: 0L) + (l ?: 0L) } ?: 0
val vinsData = tx.get("vin")?.let { it as List<Map<String, Any>> } ?: return Mono.empty()
return Flux.fromIterable(vinsData)
.flatMap {
val txid = it["txid"] as String?
val vout = (it["vout"] as Number?)?.toInt()
if (txid == null || vout == null) {
Mono.empty()
} else {
getTxOutAmount(txid, vout)
}
}
.reduce { t, u -> t + u }
.map { inAmount ->
(inAmount - outAmount).coerceAtLeast(0)
}
}
fun extractSize(tx: Map<String, Any>): Int {
val size: Number = if (tx.containsKey("vsize")) {
tx["vsize"] as Number
} else if (tx.containsKey("size")) {
tx["size"] as Number
} else {
0
}
return size.toInt()
}
fun getTxOutAmount(txid: String, vout: Int): Mono<Long> {
return reader.getTx(txid)
.switchIfEmpty(
Mono.fromCallable { log.warn("No tx $txid") }
.then(Mono.empty()),
)
.flatMap {
extractVOuts(it).let {
if (vout < it.size) {
Mono.justOrEmpty(it[vout])
} else {
Mono.empty()
}
}
}
}
fun extractVOuts(tx: Map<String, Any>): List<Long?> {
val voutsData = tx.get("vout")?.let { it as List<Map<String, Any>> } ?: return emptyList()
return voutsData.map {
val amount = it["value"] ?: return@map null
BigDecimal(amount.toString())
.multiply(BitcoinConst.COIN_DEC)
.longValueExact()
}
}
override fun readFeesAt(height: Long, selector: TxAt<Map<String, Any>, String>): Mono<TxFee> {
return reader.getBlock(height)
.flatMap { block ->
Mono.justOrEmpty(selector.get(block))
.flatMap { txid -> reader.getTx(txid!!) }
.flatMap { tx ->
calculateFee(tx)
.map { fee ->
TxFee(1, fee * 1024 / extractSize(tx))
}
}
}
}
override fun feeAggregation(mode: ChainFees.Mode): Function<Flux<TxFee>, Mono<TxFee>> {
if (mode == ChainFees.Mode.MIN_ALWAYS) {
return Function { src ->
src.reduce { a, b ->
if (a.fee > b.fee) a else b
}
}
}
return Function { src ->
src.reduce { a, b ->
TxFee(a.count + b.count, a.fee + b.fee)
}
}
}
override fun getResponseBuilder(): Function<TxFee, BlockchainOuterClass.EstimateFeeResponse> {
return Function {
val fee = (it.fee / it.count).coerceAtLeast(1)
BlockchainOuterClass.EstimateFeeResponse.newBuilder()
.setBitcoinStd(
BlockchainOuterClass.BitcoinStdFees.newBuilder()
.setSatPerKb(fee),
)
.build()
}
}
// -------
data class TxFee(val count: Int, val fee: Long)
}

View File

@@ -19,7 +19,7 @@ import io.emeraldpay.dshackle.Chain
import io.emeraldpay.dshackle.cache.Caches
import io.emeraldpay.dshackle.config.UpstreamsConfig
import io.emeraldpay.dshackle.reader.JsonRpcReader
import io.emeraldpay.dshackle.upstream.ChainFees
import io.emeraldpay.dshackle.upstream.CachingReader
import io.emeraldpay.dshackle.upstream.DistanceExtractor
import io.emeraldpay.dshackle.upstream.EgressSubscription
import io.emeraldpay.dshackle.upstream.EmptyEgressSubscription
@@ -30,6 +30,7 @@ import io.emeraldpay.dshackle.upstream.Lifecycle
import io.emeraldpay.dshackle.upstream.MergedHead
import io.emeraldpay.dshackle.upstream.Multistream
import io.emeraldpay.dshackle.upstream.Selector
import io.emeraldpay.dshackle.upstream.Selector.Matcher
import io.emeraldpay.dshackle.upstream.Upstream
import io.emeraldpay.dshackle.upstream.calls.DefaultBitcoinMethods
import io.emeraldpay.dshackle.upstream.forkchoice.MostWorkForkChoice
@@ -49,7 +50,6 @@ open class BitcoinMultistream(
private var reader = BitcoinReader(this, head, esplora)
private var addressActiveCheck: AddressActiveCheck? = null
private var xpubAddresses: XpubAddresses? = null
private val feeEstimation = BitcoinFees(this, reader, 6)
private var callRouter: LocalCallRouter = LocalCallRouter(DefaultBitcoinMethods(), reader)
override fun init() {
@@ -64,10 +64,6 @@ open class BitcoinMultistream(
return sourceUpstreams
}
override fun getFeeEstimation(): ChainFees {
return feeEstimation
}
open fun getXpubAddresses(): XpubAddresses? {
return xpubAddresses
}
@@ -133,6 +129,10 @@ open class BitcoinMultistream(
return head
}
override fun getEnrichedHead(mather: Matcher): Head {
TODO("Not yet implemented")
}
override fun getLabels(): Collection<UpstreamsConfig.Labels> {
return sourceUpstreams.flatMap { it.getLabels() }
}
@@ -157,6 +157,14 @@ open class BitcoinMultistream(
return HeadLagObserver(head, sourceUpstreams, DistanceExtractor::extractPowDistance, headScheduler, 3)
}
override fun getCachingReader(): CachingReader? {
return null
}
override fun getHead(mather: Matcher): Head {
return getHead()
}
override fun start() {
super.start()
reader.start()

View File

@@ -33,6 +33,7 @@ import io.emeraldpay.dshackle.reader.Reader
import io.emeraldpay.dshackle.reader.RekeyingReader
import io.emeraldpay.dshackle.reader.SpannedReader
import io.emeraldpay.dshackle.reader.TransformingReader
import io.emeraldpay.dshackle.upstream.CachingReader
import io.emeraldpay.dshackle.upstream.Lifecycle
import io.emeraldpay.dshackle.upstream.Multistream
import io.emeraldpay.dshackle.upstream.calls.CallMethods
@@ -57,7 +58,7 @@ open class EthereumCachingReader(
private val caches: Caches,
callMethodsFactory: Factory<CallMethods>,
private val tracer: Tracer,
) : Lifecycle {
) : Lifecycle, CachingReader {
private val objectMapper: ObjectMapper = Global.objectMapper
private val balanceCache = CurrentBlockCache<Address, Wei>()

View File

@@ -2,6 +2,7 @@ package io.emeraldpay.dshackle.upstream.ethereum
import io.emeraldpay.dshackle.upstream.Capability
import io.emeraldpay.dshackle.upstream.EgressSubscription
import io.emeraldpay.dshackle.upstream.Multistream
import io.emeraldpay.dshackle.upstream.Selector
import io.emeraldpay.dshackle.upstream.ethereum.subscribe.ConnectLogs
import io.emeraldpay.dshackle.upstream.ethereum.subscribe.ConnectNewHeads
@@ -13,7 +14,7 @@ import reactor.core.publisher.Flux
import reactor.core.scheduler.Scheduler
open class EthereumEgressSubscription(
val upstream: EthereumLikeMultistream,
val upstream: Multistream,
val scheduler: Scheduler,
val pendingTxesSource: PendingTxesSource?,
) : EgressSubscription {

View File

@@ -1,25 +0,0 @@
package io.emeraldpay.dshackle.upstream.ethereum
import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.dshackle.upstream.HasEgressSubscription
import io.emeraldpay.dshackle.upstream.Head
import io.emeraldpay.dshackle.upstream.Selector
import io.emeraldpay.dshackle.upstream.Upstream
import reactor.core.publisher.Flux
interface EthereumLikeMultistream : Upstream, HasEgressSubscription {
fun getReader(): EthereumCachingReader
fun getHead(mather: Selector.Matcher): Head
fun getEnrichedHead(mather: Selector.Matcher): Head
/**
* Tries to proxy the native subscribe request to the managed upstreams if
* - any of them matches the matcher criteria
* - all of matching above are gRPC ones
* in this case the upstream dshackle instances can sign the results and they will just proxied as is with original signs
* Otherwise return null
*/
fun tryProxy(matcher: Selector.Matcher, request: BlockchainOuterClass.NativeSubscribeRequest): Flux<out Any>?
}

View File

@@ -23,7 +23,6 @@ import io.emeraldpay.dshackle.config.UpstreamsConfig
import io.emeraldpay.dshackle.data.BlockContainer
import io.emeraldpay.dshackle.reader.JsonRpcReader
import io.emeraldpay.dshackle.reader.Reader
import io.emeraldpay.dshackle.upstream.ChainFees
import io.emeraldpay.dshackle.upstream.DistanceExtractor
import io.emeraldpay.dshackle.upstream.DynamicMergedHead
import io.emeraldpay.dshackle.upstream.EgressSubscription
@@ -55,7 +54,7 @@ open class EthereumMultistream(
caches: Caches,
private val headScheduler: Scheduler,
tracer: Tracer,
) : Multistream(chain, upstreams as MutableList<Upstream>, caches), EthereumLikeMultistream {
) : Multistream(chain, upstreams as MutableList<Upstream>, caches) {
private var head: DynamicMergedHead = DynamicMergedHead(
PriorityForkChoice(),
@@ -69,29 +68,6 @@ open class EthereumMultistream(
private val reader: EthereumCachingReader = EthereumCachingReader(this, this.caches, getMethodsFactory(), tracer)
private var subscribe = EthereumEgressSubscription(this, headScheduler, NoPendingTxes())
private val supportsEIP1559set = setOf(
Chain.ETHEREUM__MAINNET,
Chain.ETHEREUM__GOERLI,
Chain.ETHEREUM__SEPOLIA,
Chain.ARBITRUM__MAINNET,
Chain.OPTIMISM__MAINNET,
Chain.ARBITRUM__GOERLI,
Chain.OPTIMISM__GOERLI,
Chain.POLYGON_ZKEVM__MAINNET,
Chain.POLYGON_ZKEVM__TESTNET,
Chain.ZKSYNC__MAINNET,
Chain.ZKSYNC__TESTNET,
Chain.ARBITRUM_NOVA__MAINNET,
)
private val supportsEIP1559 = supportsEIP1559set.contains(chain)
private val feeEstimation = if (supportsEIP1559) {
EthereumPriorityFees(this, reader, 256)
} else {
EthereumLegacyFees(this, reader, 256)
}
init {
this.init()
}
@@ -155,7 +131,7 @@ open class EthereumMultistream(
return super.isRunning() || reader.isRunning()
}
override fun getReader(): EthereumCachingReader {
override fun getCachingReader(): EthereumCachingReader {
return reader
}
@@ -163,7 +139,7 @@ open class EthereumMultistream(
return head
}
override fun tryProxy(
override fun tryProxySubscribe(
matcher: Selector.Matcher,
request: BlockchainOuterClass.NativeSubscribeRequest,
): Flux<out Any>? =
@@ -236,8 +212,4 @@ open class EthereumMultistream(
)
}
}
override fun getFeeEstimation(): ChainFees {
return feeEstimation
}
}

View File

@@ -19,9 +19,9 @@ import io.emeraldpay.dshackle.data.BlockContainer
import io.emeraldpay.dshackle.data.BlockId
import io.emeraldpay.dshackle.data.TxId
import io.emeraldpay.dshackle.upstream.Head
import io.emeraldpay.dshackle.upstream.Multistream
import io.emeraldpay.dshackle.upstream.Selector
import io.emeraldpay.dshackle.upstream.SubscriptionConnect
import io.emeraldpay.dshackle.upstream.ethereum.EthereumLikeMultistream
import reactor.core.publisher.Flux
import reactor.core.scheduler.Scheduler
import java.time.Duration
@@ -32,7 +32,7 @@ import kotlin.concurrent.read
import kotlin.concurrent.write
class ConnectBlockUpdates(
private val upstream: EthereumLikeMultistream,
private val upstream: Multistream,
private val scheduler: Scheduler,
) : SubscriptionConnect<ConnectBlockUpdates.Update> {

View File

@@ -15,9 +15,9 @@
*/
package io.emeraldpay.dshackle.upstream.ethereum.subscribe
import io.emeraldpay.dshackle.upstream.Multistream
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
import io.emeraldpay.etherjar.hex.Hex32
@@ -27,7 +27,7 @@ import reactor.core.scheduler.Scheduler
import java.util.function.Function
open class ConnectLogs(
upstream: EthereumLikeMultistream,
upstream: Multistream,
private val connectBlockUpdates: ConnectBlockUpdates,
) {
@@ -36,7 +36,7 @@ open class ConnectLogs(
private val TOPIC_COMPARATOR = HexDataComparator()
}
constructor(upstream: EthereumLikeMultistream, scheduler: Scheduler) : this(upstream, ConnectBlockUpdates(upstream, scheduler))
constructor(upstream: Multistream, scheduler: Scheduler) : this(upstream, ConnectBlockUpdates(upstream, scheduler))
private val produceLogs = ProduceLogs(upstream)
fun start(matcher: Selector.Matcher): Flux<LogMessage> {

View File

@@ -15,9 +15,9 @@
*/
package io.emeraldpay.dshackle.upstream.ethereum.subscribe
import io.emeraldpay.dshackle.upstream.Multistream
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 reactor.core.publisher.Flux
import reactor.core.scheduler.Scheduler
@@ -28,7 +28,7 @@ import java.util.concurrent.ConcurrentHashMap
* Connects/reconnects to the upstream to produce NewHeads messages
*/
class ConnectNewHeads(
private val upstream: EthereumLikeMultistream,
private val upstream: Multistream,
private val scheduler: Scheduler,
) : SubscriptionConnect<NewHeadMessage> {

View File

@@ -22,8 +22,8 @@ import io.emeraldpay.dshackle.data.BlockId
import io.emeraldpay.dshackle.data.TxId
import io.emeraldpay.dshackle.reader.Reader
import io.emeraldpay.dshackle.upstream.Multistream
import io.emeraldpay.dshackle.upstream.ethereum.EthereumCachingReader
import io.emeraldpay.dshackle.upstream.ethereum.EthereumDirectReader.Result
import io.emeraldpay.dshackle.upstream.ethereum.EthereumLikeMultistream
import io.emeraldpay.dshackle.upstream.ethereum.subscribe.json.LogMessage
import io.emeraldpay.etherjar.hex.HexData
import io.emeraldpay.etherjar.rpc.json.TransactionReceiptJson
@@ -42,8 +42,8 @@ class ProduceLogs(
private val log = LoggerFactory.getLogger(ProduceLogs::class.java)
}
constructor(upstream: EthereumLikeMultistream) :
this(upstream.getReader().receipts(), (upstream as Multistream).chain)
constructor(upstream: Multistream) :
this((upstream.getCachingReader() as EthereumCachingReader).receipts(), (upstream as Multistream).chain)
private val objectMapper = Global.objectMapper

View File

@@ -23,7 +23,6 @@ import io.emeraldpay.dshackle.config.UpstreamsConfig
import io.emeraldpay.dshackle.data.BlockContainer
import io.emeraldpay.dshackle.reader.JsonRpcReader
import io.emeraldpay.dshackle.reader.Reader
import io.emeraldpay.dshackle.upstream.ChainFees
import io.emeraldpay.dshackle.upstream.DistanceExtractor
import io.emeraldpay.dshackle.upstream.DynamicMergedHead
import io.emeraldpay.dshackle.upstream.EmptyHead
@@ -53,7 +52,7 @@ open class EthereumPosMultiStream(
caches: Caches,
private val headScheduler: Scheduler,
tracer: Tracer,
) : Multistream(chain, upstreams as MutableList<Upstream>, caches), EthereumLikeMultistream {
) : Multistream(chain, upstreams as MutableList<Upstream>, caches) {
private var head: DynamicMergedHead = DynamicMergedHead(
PriorityForkChoice(),
@@ -63,7 +62,6 @@ open class EthereumPosMultiStream(
private val reader: EthereumCachingReader = EthereumCachingReader(this, this.caches, getMethodsFactory(), tracer)
private var subscribe = EthereumEgressSubscription(this, headScheduler, NoPendingTxes())
private val feeEstimation = EthereumPriorityFees(this, reader, 256)
private val filteredHeads: MutableMap<String, Head> =
ConcurrentReferenceHashMap(16, ConcurrentReferenceHashMap.ReferenceType.WEAK)
@@ -112,7 +110,7 @@ open class EthereumPosMultiStream(
start()
}
override fun getReader(): EthereumCachingReader {
override fun getCachingReader(): EthereumCachingReader {
return reader
}
@@ -120,7 +118,7 @@ open class EthereumPosMultiStream(
return head
}
override fun tryProxy(
override fun tryProxySubscribe(
matcher: Selector.Matcher,
request: BlockchainOuterClass.NativeSubscribeRequest,
): Flux<out Any>? =
@@ -204,10 +202,6 @@ open class EthereumPosMultiStream(
}
}
override fun getFeeEstimation(): ChainFees {
return feeEstimation
}
override fun onUpstreamsUpdated() {
super.onUpstreamsUpdated()

View File

@@ -20,7 +20,7 @@ import io.emeraldpay.dshackle.Chain
import io.emeraldpay.dshackle.cache.Caches
import io.emeraldpay.dshackle.config.UpstreamsConfig
import io.emeraldpay.dshackle.reader.JsonRpcReader
import io.emeraldpay.dshackle.upstream.ChainFees
import io.emeraldpay.dshackle.upstream.CachingReader
import io.emeraldpay.dshackle.upstream.DistanceExtractor
import io.emeraldpay.dshackle.upstream.DynamicMergedHead
import io.emeraldpay.dshackle.upstream.EgressSubscription
@@ -29,6 +29,7 @@ import io.emeraldpay.dshackle.upstream.Head
import io.emeraldpay.dshackle.upstream.HeadLagObserver
import io.emeraldpay.dshackle.upstream.Lifecycle
import io.emeraldpay.dshackle.upstream.Multistream
import io.emeraldpay.dshackle.upstream.Selector.Matcher
import io.emeraldpay.dshackle.upstream.Upstream
import io.emeraldpay.dshackle.upstream.forkchoice.PriorityForkChoice
import reactor.core.publisher.Mono
@@ -82,10 +83,22 @@ open class GenericMultistream(
start()
}
override fun getCachingReader(): CachingReader? {
return null
}
override fun getHead(mather: Matcher): Head {
return getHead()
}
override fun getHead(): Head {
return head
}
override fun getEnrichedHead(mather: Matcher): Head {
return getHead()
}
override fun getLabels(): Collection<UpstreamsConfig.Labels> {
return upstreams.flatMap { it.getLabels() }
}
@@ -105,8 +118,4 @@ open class GenericMultistream(
override fun getEgressSubscription(): EgressSubscription {
return EmptyEgressSubscription()
}
override fun getFeeEstimation(): ChainFees {
throw NotImplementedError()
}
}