solution: delegate bitcoin balance request to another upstream, if data is unavailable locally

This commit is contained in:
Igor Artamonov
2020-09-22 23:51:07 -04:00
parent 19717d622b
commit d537ff0461
20 changed files with 276 additions and 33 deletions

View File

@@ -125,6 +125,7 @@ Options (default or as part of upstream config):
provider such as Infura, but disabling it is not recommended for a normal node) provider such as Infura, but disabling it is not recommended for a normal node)
| `min-peers` | 3 | specify minimum amount of connected peers, Dshackle will not use upstream with less than specified number | `min-peers` | 3 | specify minimum amount of connected peers, Dshackle will not use upstream with less than specified number
| `timeout` | 60 | timeout in seconds after which request to the upstream will be discarded (and may be retried on an another upstream) | `timeout` | 60 | timeout in seconds after which request to the upstream will be discarded (and may be retried on an another upstream)
| `balance` | `true` for ethereum, `false` for bitcoin | specify if this node should be used to fetch balance for an address
|=== |===
=== Connection type === Connection type

View File

@@ -103,6 +103,9 @@ cluster:
password: 1a68f20154fc258fe4149c199ad8f281 password: 1a68f20154fc258fe4149c199ad8f281
- id: bitcoin - id: bitcoin
chain: bitcoin chain: bitcoin
options:
# use the node to fetch balances
balance: true
connection: connection:
bitcoin: bitcoin:
rpc: rpc:
@@ -110,6 +113,9 @@ cluster:
basic-auth: basic-auth:
username: bitcoin username: bitcoin
password: e984af45bb888428207c290 password: e984af45bb888428207c290
# uses Esplora index to fetch balances and utxo for an address
esplora:
url: "http://localhost:3001"
- id: remote - id: remote
connection: connection:
grpc: grpc:

View File

@@ -26,7 +26,9 @@ open class SilentException(message: String) : Exception(message) {
/** /**
* Blockchain is not available or not supported by current instance of the Dshackle * Blockchain is not available or not supported by current instance of the Dshackle
*/ */
class UnsupportedBlockchain(val blockchainId: Int): SilentException("Unsupported blockchain $blockchainId") { class UnsupportedBlockchain(val blockchainId: Int) : SilentException("Unsupported blockchain $blockchainId") {
constructor(chain: Chain) : this(chain.id) constructor(chain: Chain) : this(chain.id)
} }
class DataUnavailable(val code: String) : SilentException("Data is unavailable: $code")
} }

View File

@@ -30,6 +30,7 @@ class UpstreamsConfig {
open class Options { open class Options {
var disableValidation: Boolean? = null var disableValidation: Boolean? = null
var timeout = Defaults.timeout var timeout = Defaults.timeout
var providesBalance: Boolean? = null
var minPeers: Int? = 1 var minPeers: Int? = 1
set(minPeers) { set(minPeers) {
@@ -46,6 +47,7 @@ class UpstreamsConfig {
val copy = Options() val copy = Options()
copy.minPeers = if (this.minPeers != null) this.minPeers else additional.minPeers copy.minPeers = if (this.minPeers != null) this.minPeers else additional.minPeers
copy.disableValidation = if (this.disableValidation != null) this.disableValidation else additional.disableValidation copy.disableValidation = if (this.disableValidation != null) this.disableValidation else additional.disableValidation
copy.providesBalance = if (this.providesBalance != null) this.providesBalance else additional.providesBalance
return copy return copy
} }

View File

@@ -261,6 +261,9 @@ class UpstreamsConfigReader(
getValueAsBool(values, "disable-validation")?.let { getValueAsBool(values, "disable-validation")?.let {
options.disableValidation = it options.disableValidation = it
} }
getValueAsBool(values, "balance")?.let {
options.providesBalance = it
}
return options return options
} }

View File

@@ -20,7 +20,6 @@ import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.api.proto.Common import io.emeraldpay.api.proto.Common
import io.emeraldpay.dshackle.startup.QuorumForLabels import io.emeraldpay.dshackle.startup.QuorumForLabels
import io.emeraldpay.dshackle.upstream.* import io.emeraldpay.dshackle.upstream.*
import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinUpstream
import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import reactor.core.publisher.Mono import reactor.core.publisher.Mono
@@ -38,6 +37,7 @@ class Describe(
multistreamHolder.getUpstream(chain)?.let { chainUpstreams -> multistreamHolder.getUpstream(chain)?.let { chainUpstreams ->
val status = subscribeStatus.chainStatus(chain, chainUpstreams.getAll()) val status = subscribeStatus.chainStatus(chain, chainUpstreams.getAll())
val targets = chainUpstreams.getMethods().getSupportedMethods() val targets = chainUpstreams.getMethods().getSupportedMethods()
val capabilities: MutableSet<Capability> = mutableSetOf()
val chainDescription = BlockchainOuterClass.DescribeChain.newBuilder() val chainDescription = BlockchainOuterClass.DescribeChain.newBuilder()
.setChain(Common.ChainRef.forNumber(chain.id)) .setChain(Common.ChainRef.forNumber(chain.id))
.addAllSupportedMethods(targets) .addAllSupportedMethods(targets)
@@ -59,8 +59,17 @@ class Describe(
}) })
chainDescription.addNodes(nodeDetails) chainDescription.addNodes(nodeDetails)
} }
capabilities.addAll(up.getCapabilities())
} }
} }
chainDescription.addAllCapabilities(
capabilities.map {
when (it) {
Capability.RPC -> BlockchainOuterClass.Capabilities.CAP_CALLS
Capability.BALANCE -> BlockchainOuterClass.Capabilities.CAP_BALANCE
}
}
)
resp.addChains(chainDescription.build()) resp.addChains(chainDescription.build())
} }
} }

View File

@@ -17,11 +17,16 @@ package io.emeraldpay.dshackle.rpc
import io.emeraldpay.api.proto.BlockchainOuterClass import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.api.proto.Common import io.emeraldpay.api.proto.Common
import io.emeraldpay.api.proto.ReactorBlockchainGrpc
import io.emeraldpay.dshackle.BlockchainType import io.emeraldpay.dshackle.BlockchainType
import io.emeraldpay.dshackle.Defaults
import io.emeraldpay.dshackle.SilentException import io.emeraldpay.dshackle.SilentException
import io.emeraldpay.dshackle.upstream.Capability
import io.emeraldpay.dshackle.upstream.MultistreamHolder 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.BitcoinMultistream
import io.emeraldpay.dshackle.upstream.bitcoin.data.SimpleUnspent import io.emeraldpay.dshackle.upstream.bitcoin.data.SimpleUnspent
import io.emeraldpay.dshackle.upstream.grpc.BitcoinGrpcUpstream
import io.emeraldpay.grpc.Chain import io.emeraldpay.grpc.Chain
import org.bitcoinj.params.MainNetParams import org.bitcoinj.params.MainNetParams
import org.bitcoinj.params.TestNet3Params import org.bitcoinj.params.TestNet3Params
@@ -31,6 +36,9 @@ import org.springframework.stereotype.Service
import reactor.core.publisher.Flux import reactor.core.publisher.Flux
import reactor.core.publisher.Mono import reactor.core.publisher.Mono
import java.math.BigInteger import java.math.BigInteger
import java.time.Duration
import java.util.concurrent.ConcurrentHashMap
import javax.annotation.PostConstruct
import kotlin.collections.HashMap import kotlin.collections.HashMap
@Service @Service
@@ -47,6 +55,42 @@ class TrackBitcoinAddress(
&& BlockchainType.fromBlockchain(chain) == BlockchainType.BITCOIN && multistreamHolder.isAvailable(chain) && BlockchainType.fromBlockchain(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 balanceAvailable: MutableMap<Chain, Boolean> = ConcurrentHashMap()
/**
* Criteria for a remote grpc upstream that can provide a balance
*/
private val balanceUpstreamMatcher = Selector.LocalAndMatcher(
Selector.GrpcMatcher(),
Selector.CapabilityMatcher(Capability.BALANCE)
)
@PostConstruct
fun listenChains() {
multistreamHolder.observeChains().subscribe { chain ->
multistreamHolder.getUpstream(chain)?.let { mup ->
val available = mup.getAll().any { up ->
!up.isGrpc() && (up.getOptions().providesBalance ?: false)
}
setBalanceAvailability(chain, available)
}
}
}
fun setBalanceAvailability(chain: Chain, enabled: Boolean) {
balanceAvailable[chain] = enabled
}
/**
* @return true if the current instance has data sources to provide the balance
*/
fun isBalanceAvailable(chain: Chain): Boolean {
return balanceAvailable[chain] ?: false
}
fun allAddresses(api: BitcoinMultistream, request: BlockchainOuterClass.BalanceRequest): Flux<String> { fun allAddresses(api: BitcoinMultistream, request: BlockchainOuterClass.BalanceRequest): Flux<String> {
if (!request.hasAddress()) { if (!request.hasAddress()) {
return Flux.empty() return Flux.empty()
@@ -117,38 +161,74 @@ class TrackBitcoinAddress(
} }
} }
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.just(0)
.doOnNext {
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> { override fun getBalance(request: BlockchainOuterClass.BalanceRequest): Flux<BlockchainOuterClass.AddressBalance> {
val chain = Chain.byId(request.asset.chainValue) val chain = Chain.byId(request.asset.chainValue)
val upstream = multistreamHolder.getUpstream(chain)?.cast(BitcoinMultistream::class.java) val upstream = multistreamHolder.getUpstream(chain)?.cast(BitcoinMultistream::class.java)
?: return Flux.error(SilentException.UnsupportedBlockchain(request.asset.chainValue)) ?: return Flux.error(SilentException.UnsupportedBlockchain(request.asset.chainValue))
val addresses = allAddresses(upstream, request) ?: return Flux.error(SilentException("Unsupported address")) return if (isBalanceAvailable(chain)) {
return requestBalances(chain, upstream, addresses, request.includeUtxo) val addresses = allAddresses(upstream, request)
.map(this@TrackBitcoinAddress::buildResponse) requestBalances(chain, upstream, addresses, request.includeUtxo)
.map(this@TrackBitcoinAddress::buildResponse)
} else {
getRemoteBalance(upstream, request)
}
} }
override fun subscribe(request: BlockchainOuterClass.BalanceRequest): Flux<BlockchainOuterClass.AddressBalance> { override fun subscribe(request: BlockchainOuterClass.BalanceRequest): Flux<BlockchainOuterClass.AddressBalance> {
val chain = Chain.byId(request.asset.chainValue) val chain = Chain.byId(request.asset.chainValue)
val upstream = multistreamHolder.getUpstream(chain)?.cast(BitcoinMultistream::class.java) val upstream = multistreamHolder.getUpstream(chain)?.cast(BitcoinMultistream::class.java)
?: return Flux.error(SilentException.UnsupportedBlockchain(request.asset.chainValue)) ?: return Flux.error(SilentException.UnsupportedBlockchain(request.asset.chainValue))
val addresses = allAddresses(upstream, request).cache() if (isBalanceAvailable(chain)) {
val following = upstream.getHead().getFlux() val addresses = allAddresses(upstream, request).cache()
.flatMap { block -> val following = upstream.getHead().getFlux()
requestBalances(chain, upstream, Flux.from(addresses), request.includeUtxo) .flatMap { block ->
} requestBalances(chain, upstream, Flux.from(addresses), request.includeUtxo)
val last = HashMap<String, BigInteger>() }
val result = following val last = HashMap<String, BigInteger>()
.filter { curr -> val result = following
val prev = last[curr.address.address] .filter { curr ->
//TODO utxo can change without changing balance val prev = last[curr.address.address]
val changed = prev == null || curr.balance != prev //TODO utxo can change without changing balance
if (changed) { val changed = prev == null || curr.balance != prev
last[curr.address.address] = curr.balance if (changed) {
last[curr.address.address] = curr.balance
}
changed
} }
changed
}
return result.map(this@TrackBitcoinAddress::buildResponse) return result.map(this@TrackBitcoinAddress::buildResponse)
} else {
return subscribeRemoteBalance(upstream, request)
}
} }
private fun buildResponse(address: AddressBalance): BlockchainOuterClass.AddressBalance { private fun buildResponse(address: AddressBalance): BlockchainOuterClass.AddressBalance {

View File

@@ -127,7 +127,7 @@ class CurrentMultistreamHolder(
} }
override fun observeChains(): Flux<Chain> { override fun observeChains(): Flux<Chain> {
return Flux.merge( return Flux.concat(
Flux.fromIterable(getAvailable()), Flux.fromIterable(getAvailable()),
Flux.from(chainsBus) Flux.from(chainsBus)
) )

View File

@@ -61,6 +61,7 @@ abstract class Multistream(
private var seq = 0 private var seq = 0
protected var lagObserver: HeadLagObserver? = null protected var lagObserver: HeadLagObserver? = null
private var subscription: Disposable? = null private var subscription: Disposable? = null
private var capabilities: Set<Capability> = emptySet()
open fun init() { open fun init() {
onUpstreamsUpdated() onUpstreamsUpdated()
@@ -122,10 +123,18 @@ abstract class Multistream(
open fun onUpstreamsUpdated() { open fun onUpstreamsUpdated() {
reconfigLock.withLock { reconfigLock.withLock {
getAll().map { it.getMethods() }.let { val upstreams = getAll()
upstreams.map { it.getMethods() }.let {
//TODO made list of uniq instances, and then if only one, just use it directly //TODO made list of uniq instances, and then if only one, just use it directly
callMethods = AggregatedCallMethods(it) callMethods = AggregatedCallMethods(it)
} }
capabilities = if (upstreams.isEmpty()) {
emptySet()
} else {
upstreams.map { up ->
up.getCapabilities()
}.reduce { acc, curr -> acc + curr }
}
} }
} }
@@ -211,6 +220,14 @@ abstract class Multistream(
return 0 return 0
} }
override fun getCapabilities(): Set<Capability> {
return this.capabilities
}
override fun isGrpc(): Boolean {
return false
}
fun printStatus() { fun printStatus() {
var height: Long? = null var height: Long? = null
try { try {

View File

@@ -149,10 +149,18 @@ class Selector {
} }
} }
class LabelMatcher(val name: String, val values: Collection<String>): LabelSelectorMatcher() { class LocalAndMatcher(vararg val matchers: Matcher) : Matcher {
override fun matches(up: Upstream): Boolean {
return matchers.all { it.matches(up) }
}
}
class LabelMatcher(val name: String, val values: Collection<String>) : LabelSelectorMatcher() {
override fun matches(labels: UpstreamsConfig.Labels): Boolean { override fun matches(labels: UpstreamsConfig.Labels): Boolean {
return labels.get(name)?.let { return labels.get(name)?.let { labelValue ->
labelValue -> values.any { it == labelValue } values.any { it == labelValue }
} ?: false } ?: false
} }
@@ -221,4 +229,15 @@ class Selector {
} }
} }
class CapabilityMatcher(val capability: Capability) : Matcher {
override fun matches(up: Upstream): Boolean {
return up.getCapabilities().contains(capability)
}
}
class GrpcMatcher() : Matcher {
override fun matches(up: Upstream): Boolean {
return up.isGrpc()
}
}
} }

View File

@@ -37,6 +37,8 @@ interface Upstream {
fun getLabels(): Collection<UpstreamsConfig.Labels> fun getLabels(): Collection<UpstreamsConfig.Labels>
fun getMethods(): CallMethods fun getMethods(): CallMethods
fun getId(): String fun getId(): String
fun getCapabilities(): Set<Capability>
fun isGrpc(): Boolean
fun <T : Upstream> cast(selfType: Class<T>): T fun <T : Upstream> cast(selfType: Class<T>): T
} }

View File

@@ -45,6 +45,12 @@ open class BitcoinRpcUpstream(
private val head: Head = createHead() private val head: Head = createHead()
private var validatorSubscription: Disposable? = null private var validatorSubscription: Disposable? = null
private val capabilities = if (options.providesBalance == true) {
setOf(Capability.RPC, Capability.BALANCE)
} else {
setOf(Capability.RPC)
}
private fun createHead(): Head { private fun createHead(): Head {
return BitcoinRpcHead( return BitcoinRpcHead(
directApi, directApi,
@@ -64,6 +70,14 @@ open class BitcoinRpcUpstream(
return listOf(UpstreamsConfig.Labels()) return listOf(UpstreamsConfig.Labels())
} }
override fun getCapabilities(): Set<Capability> {
return capabilities;
}
override fun isGrpc(): Boolean {
return false
}
override fun <T : Upstream> cast(selfType: Class<T>): T { override fun <T : Upstream> cast(selfType: Class<T>): T {
if (!selfType.isAssignableFrom(this.javaClass)) { if (!selfType.isAssignableFrom(this.javaClass)) {
throw ClassCastException("Cannot cast ${this.javaClass} to $selfType") throw ClassCastException("Cannot cast ${this.javaClass} to $selfType")

View File

@@ -38,6 +38,11 @@ open class EthereumRpcUpstream(
private val head: Head = this.createHead() private val head: Head = this.createHead()
private var validatorSubscription: Disposable? = null private var validatorSubscription: Disposable? = null
private val capabilities = if (options.providesBalance != false) {
setOf(Capability.RPC, Capability.BALANCE)
} else {
setOf(Capability.RPC)
}
override fun setCaches(caches: Caches) { override fun setCaches(caches: Caches) {
if (head is CachesEnabled) { if (head is CachesEnabled) {
@@ -107,6 +112,14 @@ open class EthereumRpcUpstream(
return listOf(node.labels) return listOf(node.labels)
} }
override fun getCapabilities(): Set<Capability> {
return capabilities
}
override fun isGrpc(): Boolean {
return false
}
@Suppress("unchecked") @Suppress("unchecked")
override fun <T : Upstream> cast(selfType: Class<T>): T { override fun <T : Upstream> cast(selfType: Class<T>): T {
if (!selfType.isAssignableFrom(this.javaClass)) { if (!selfType.isAssignableFrom(this.javaClass)) {

View File

@@ -22,10 +22,7 @@ import io.emeraldpay.dshackle.config.UpstreamsConfig
import io.emeraldpay.dshackle.data.BlockContainer import io.emeraldpay.dshackle.data.BlockContainer
import io.emeraldpay.dshackle.data.BlockId import io.emeraldpay.dshackle.data.BlockId
import io.emeraldpay.dshackle.reader.Reader import io.emeraldpay.dshackle.reader.Reader
import io.emeraldpay.dshackle.upstream.Head import io.emeraldpay.dshackle.upstream.*
import io.emeraldpay.dshackle.upstream.Selector
import io.emeraldpay.dshackle.upstream.Upstream
import io.emeraldpay.dshackle.upstream.UpstreamAvailability
import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinUpstream import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinUpstream
import io.emeraldpay.dshackle.upstream.bitcoin.ExtractBlock import io.emeraldpay.dshackle.upstream.bitcoin.ExtractBlock
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcGrpcClient import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcGrpcClient
@@ -45,7 +42,7 @@ import java.util.function.Function
class BitcoinGrpcUpstream( class BitcoinGrpcUpstream(
private val parentId: String, private val parentId: String,
chain: Chain, chain: Chain,
private val remote: ReactorBlockchainGrpc.ReactorBlockchainStub, val remote: ReactorBlockchainGrpc.ReactorBlockchainStub,
private val client: JsonRpcGrpcClient private val client: JsonRpcGrpcClient
) : BitcoinUpstream( ) : BitcoinUpstream(
"$parentId/${chain.chainCode}", "$parentId/${chain.chainCode}",
@@ -92,6 +89,7 @@ class BitcoinGrpcUpstream(
private val upstreamStatus = GrpcUpstreamStatus() private val upstreamStatus = GrpcUpstreamStatus()
private val grpcHead = GrpcHead(chain, this, blockConverter, reloadBlock) private val grpcHead = GrpcHead(chain, this, blockConverter, reloadBlock)
var timeout = Defaults.timeout var timeout = Defaults.timeout
private var capabilities: Set<Capability> = emptySet()
override fun getHead(): Head { override fun getHead(): Head {
return grpcHead return grpcHead
@@ -105,6 +103,14 @@ class BitcoinGrpcUpstream(
return upstreamStatus.getLabels() return upstreamStatus.getLabels()
} }
override fun getCapabilities(): Set<Capability> {
return capabilities
}
override fun isGrpc(): Boolean {
return true
}
override fun <T : Upstream> cast(selfType: Class<T>): T { override fun <T : Upstream> cast(selfType: Class<T>): T {
if (!selfType.isAssignableFrom(this.javaClass)) { if (!selfType.isAssignableFrom(this.javaClass)) {
throw ClassCastException("Cannot cast ${this.javaClass} to $selfType") throw ClassCastException("Cannot cast ${this.javaClass} to $selfType")
@@ -126,6 +132,7 @@ class BitcoinGrpcUpstream(
override fun update(conf: BlockchainOuterClass.DescribeChain) { override fun update(conf: BlockchainOuterClass.DescribeChain) {
upstreamStatus.update(conf) upstreamStatus.update(conf)
this.capabilities = RemoteCapabilities.extract(conf)
conf.status?.let { status -> onStatus(status) } conf.status?.let { status -> onStatus(status) }
} }

View File

@@ -90,6 +90,7 @@ open class EthereumGrpcUpstream(
private val log = LoggerFactory.getLogger(EthereumGrpcUpstream::class.java) private val log = LoggerFactory.getLogger(EthereumGrpcUpstream::class.java)
private val upstreamStatus = GrpcUpstreamStatus() private val upstreamStatus = GrpcUpstreamStatus()
private val grpcHead = GrpcHead(chain, this, blockConverter, reloadBlock) private val grpcHead = GrpcHead(chain, this, blockConverter, reloadBlock)
private var capabilities: Set<Capability> = emptySet()
private val defaultReader: Reader<JsonRpcRequest, JsonRpcResponse> = client.forSelector(Selector.empty) private val defaultReader: Reader<JsonRpcRequest, JsonRpcResponse> = client.forSelector(Selector.empty)
var timeout = Defaults.timeout var timeout = Defaults.timeout
@@ -109,6 +110,7 @@ open class EthereumGrpcUpstream(
override fun update(conf: BlockchainOuterClass.DescribeChain) { override fun update(conf: BlockchainOuterClass.DescribeChain) {
upstreamStatus.update(conf) upstreamStatus.update(conf)
capabilities = RemoteCapabilities.extract(conf)
conf.status?.let { status -> onStatus(status) } conf.status?.let { status -> onStatus(status) }
} }
@@ -148,4 +150,11 @@ open class EthereumGrpcUpstream(
return this as T return this as T
} }
override fun getCapabilities(): Set<Capability> {
return capabilities
}
override fun isGrpc(): Boolean {
return true
}
} }

View File

@@ -0,0 +1,23 @@
package io.emeraldpay.dshackle.upstream.grpc
import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.dshackle.upstream.Capability
class RemoteCapabilities {
companion object {
@JvmStatic
fun extract(conf: BlockchainOuterClass.DescribeChain): Set<Capability> {
return conf.capabilitiesList?.let { values ->
values.mapNotNull { value ->
when {
BlockchainOuterClass.Capabilities.CAP_BALANCE == value -> Capability.BALANCE
BlockchainOuterClass.Capabilities.CAP_CALLS == value -> Capability.RPC
else -> null
}
}.toSet()
} ?: emptySet()
}
}
}

View File

@@ -92,6 +92,7 @@ class UpstreamsConfigReaderSpec extends Specification {
with(act.upstreams.get(0)) { with(act.upstreams.get(0)) {
id == "local" id == "local"
chain == "bitcoin" chain == "bitcoin"
options == null || options.providesBalance == false
connection instanceof UpstreamsConfig.BitcoinConnection connection instanceof UpstreamsConfig.BitcoinConnection
with((UpstreamsConfig.BitcoinConnection) connection) { with((UpstreamsConfig.BitcoinConnection) connection) {
rpc != null rpc != null
@@ -119,6 +120,7 @@ class UpstreamsConfigReaderSpec extends Specification {
with(act.upstreams.get(0)) { with(act.upstreams.get(0)) {
id == "local" id == "local"
chain == "bitcoin" chain == "bitcoin"
options.providesBalance == true
connection instanceof UpstreamsConfig.BitcoinConnection connection instanceof UpstreamsConfig.BitcoinConnection
with((UpstreamsConfig.BitcoinConnection) connection) { with((UpstreamsConfig.BitcoinConnection) connection) {
rpc != null rpc != null

View File

@@ -259,6 +259,7 @@ class TrackBitcoinAddressSpec extends Specification {
} }
MultistreamHolder upstreams = new MultistreamHolderMock(Chain.BITCOIN, upstream) MultistreamHolder upstreams = new MultistreamHolderMock(Chain.BITCOIN, upstream)
TrackBitcoinAddress track = new TrackBitcoinAddress(upstreams) TrackBitcoinAddress track = new TrackBitcoinAddress(upstreams)
track.setBalanceAvailability(Chain.BITCOIN, true)
when: when:
def resp = track.subscribe(BlockchainOuterClass.BalanceRequest.newBuilder() def resp = track.subscribe(BlockchainOuterClass.BalanceRequest.newBuilder()
@@ -285,6 +286,5 @@ class TrackBitcoinAddressSpec extends Specification {
} }
.expectComplete() .expectComplete()
.verify(Duration.ofSeconds(1)) .verify(Duration.ofSeconds(1))
} }
} }

View File

@@ -0,0 +1,32 @@
package io.emeraldpay.dshackle.upstream.grpc
import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.dshackle.upstream.Capability
import spock.lang.Specification
class RemoteCapabilitiesSpec extends Specification {
def "parse from remote - all"() {
setup:
def remote = BlockchainOuterClass.DescribeChain.newBuilder()
.addCapabilities(BlockchainOuterClass.Capabilities.CAP_CALLS)
.addCapabilities(BlockchainOuterClass.Capabilities.CAP_NONE)
.addCapabilities(BlockchainOuterClass.Capabilities.CAP_BALANCE)
.build()
when:
def act = RemoteCapabilities.extract(remote)
then:
act == [Capability.BALANCE, Capability.RPC].toSet()
}
def "parse from remote - only call"() {
setup:
def remote = BlockchainOuterClass.DescribeChain.newBuilder()
.addCapabilities(BlockchainOuterClass.Capabilities.CAP_CALLS)
.build()
when:
def act = RemoteCapabilities.extract(remote)
then:
act == [Capability.RPC].toSet()
}
}

View File

@@ -9,6 +9,8 @@ defaults:
upstreams: upstreams:
- id: local - id: local
chain: bitcoin chain: bitcoin
options:
balance: true
connection: connection:
bitcoin: bitcoin:
rpc: rpc: