solution: delegate bitcoin balance request to another upstream, if data is unavailable locally
This commit is contained in:
@@ -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)
|
||||
| `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)
|
||||
| `balance` | `true` for ethereum, `false` for bitcoin | specify if this node should be used to fetch balance for an address
|
||||
|===
|
||||
|
||||
=== Connection type
|
||||
|
||||
@@ -103,6 +103,9 @@ cluster:
|
||||
password: 1a68f20154fc258fe4149c199ad8f281
|
||||
- id: bitcoin
|
||||
chain: bitcoin
|
||||
options:
|
||||
# use the node to fetch balances
|
||||
balance: true
|
||||
connection:
|
||||
bitcoin:
|
||||
rpc:
|
||||
@@ -110,6 +113,9 @@ cluster:
|
||||
basic-auth:
|
||||
username: bitcoin
|
||||
password: e984af45bb888428207c290
|
||||
# uses Esplora index to fetch balances and utxo for an address
|
||||
esplora:
|
||||
url: "http://localhost:3001"
|
||||
- id: remote
|
||||
connection:
|
||||
grpc:
|
||||
|
||||
@@ -26,7 +26,9 @@ open class SilentException(message: String) : Exception(message) {
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
|
||||
class DataUnavailable(val code: String) : SilentException("Data is unavailable: $code")
|
||||
}
|
||||
@@ -30,6 +30,7 @@ class UpstreamsConfig {
|
||||
open class Options {
|
||||
var disableValidation: Boolean? = null
|
||||
var timeout = Defaults.timeout
|
||||
var providesBalance: Boolean? = null
|
||||
|
||||
var minPeers: Int? = 1
|
||||
set(minPeers) {
|
||||
@@ -46,6 +47,7 @@ class UpstreamsConfig {
|
||||
val copy = Options()
|
||||
copy.minPeers = if (this.minPeers != null) this.minPeers else additional.minPeers
|
||||
copy.disableValidation = if (this.disableValidation != null) this.disableValidation else additional.disableValidation
|
||||
copy.providesBalance = if (this.providesBalance != null) this.providesBalance else additional.providesBalance
|
||||
return copy
|
||||
}
|
||||
|
||||
|
||||
@@ -261,6 +261,9 @@ class UpstreamsConfigReader(
|
||||
getValueAsBool(values, "disable-validation")?.let {
|
||||
options.disableValidation = it
|
||||
}
|
||||
getValueAsBool(values, "balance")?.let {
|
||||
options.providesBalance = it
|
||||
}
|
||||
return options
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@ import io.emeraldpay.api.proto.BlockchainOuterClass
|
||||
import io.emeraldpay.api.proto.Common
|
||||
import io.emeraldpay.dshackle.startup.QuorumForLabels
|
||||
import io.emeraldpay.dshackle.upstream.*
|
||||
import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinUpstream
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.stereotype.Service
|
||||
import reactor.core.publisher.Mono
|
||||
@@ -38,6 +37,7 @@ class Describe(
|
||||
multistreamHolder.getUpstream(chain)?.let { chainUpstreams ->
|
||||
val status = subscribeStatus.chainStatus(chain, chainUpstreams.getAll())
|
||||
val targets = chainUpstreams.getMethods().getSupportedMethods()
|
||||
val capabilities: MutableSet<Capability> = mutableSetOf()
|
||||
val chainDescription = BlockchainOuterClass.DescribeChain.newBuilder()
|
||||
.setChain(Common.ChainRef.forNumber(chain.id))
|
||||
.addAllSupportedMethods(targets)
|
||||
@@ -59,8 +59,17 @@ class Describe(
|
||||
})
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,11 +17,16 @@ 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.Defaults
|
||||
import io.emeraldpay.dshackle.SilentException
|
||||
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 io.emeraldpay.grpc.Chain
|
||||
import org.bitcoinj.params.MainNetParams
|
||||
import org.bitcoinj.params.TestNet3Params
|
||||
@@ -31,6 +36,9 @@ 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.util.concurrent.ConcurrentHashMap
|
||||
import javax.annotation.PostConstruct
|
||||
import kotlin.collections.HashMap
|
||||
|
||||
@Service
|
||||
@@ -47,6 +55,42 @@ class TrackBitcoinAddress(
|
||||
&& 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> {
|
||||
if (!request.hasAddress()) {
|
||||
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> {
|
||||
val chain = Chain.byId(request.asset.chainValue)
|
||||
val upstream = multistreamHolder.getUpstream(chain)?.cast(BitcoinMultistream::class.java)
|
||||
?: return Flux.error(SilentException.UnsupportedBlockchain(request.asset.chainValue))
|
||||
val addresses = allAddresses(upstream, request) ?: return Flux.error(SilentException("Unsupported address"))
|
||||
return requestBalances(chain, upstream, addresses, request.includeUtxo)
|
||||
.map(this@TrackBitcoinAddress::buildResponse)
|
||||
return if (isBalanceAvailable(chain)) {
|
||||
val addresses = allAddresses(upstream, request)
|
||||
requestBalances(chain, upstream, addresses, request.includeUtxo)
|
||||
.map(this@TrackBitcoinAddress::buildResponse)
|
||||
} else {
|
||||
getRemoteBalance(upstream, request)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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))
|
||||
val addresses = allAddresses(upstream, request).cache()
|
||||
val following = upstream.getHead().getFlux()
|
||||
.flatMap { block ->
|
||||
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
|
||||
if (isBalanceAvailable(chain)) {
|
||||
val addresses = allAddresses(upstream, request).cache()
|
||||
val following = upstream.getHead().getFlux()
|
||||
.flatMap { block ->
|
||||
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
|
||||
}
|
||||
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 {
|
||||
|
||||
@@ -127,7 +127,7 @@ class CurrentMultistreamHolder(
|
||||
}
|
||||
|
||||
override fun observeChains(): Flux<Chain> {
|
||||
return Flux.merge(
|
||||
return Flux.concat(
|
||||
Flux.fromIterable(getAvailable()),
|
||||
Flux.from(chainsBus)
|
||||
)
|
||||
|
||||
@@ -61,6 +61,7 @@ abstract class Multistream(
|
||||
private var seq = 0
|
||||
protected var lagObserver: HeadLagObserver? = null
|
||||
private var subscription: Disposable? = null
|
||||
private var capabilities: Set<Capability> = emptySet()
|
||||
|
||||
open fun init() {
|
||||
onUpstreamsUpdated()
|
||||
@@ -122,10 +123,18 @@ abstract class Multistream(
|
||||
|
||||
open fun onUpstreamsUpdated() {
|
||||
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
|
||||
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
|
||||
}
|
||||
|
||||
override fun getCapabilities(): Set<Capability> {
|
||||
return this.capabilities
|
||||
}
|
||||
|
||||
override fun isGrpc(): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
fun printStatus() {
|
||||
var height: Long? = null
|
||||
try {
|
||||
|
||||
@@ -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 {
|
||||
return labels.get(name)?.let {
|
||||
labelValue -> values.any { it == labelValue }
|
||||
return labels.get(name)?.let { labelValue ->
|
||||
values.any { it == labelValue }
|
||||
} ?: 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,8 @@ interface Upstream {
|
||||
fun getLabels(): Collection<UpstreamsConfig.Labels>
|
||||
fun getMethods(): CallMethods
|
||||
fun getId(): String
|
||||
fun getCapabilities(): Set<Capability>
|
||||
fun isGrpc(): Boolean
|
||||
|
||||
fun <T : Upstream> cast(selfType: Class<T>): T
|
||||
}
|
||||
@@ -45,6 +45,12 @@ open class BitcoinRpcUpstream(
|
||||
private val head: Head = createHead()
|
||||
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 {
|
||||
return BitcoinRpcHead(
|
||||
directApi,
|
||||
@@ -64,6 +70,14 @@ open class BitcoinRpcUpstream(
|
||||
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 {
|
||||
if (!selfType.isAssignableFrom(this.javaClass)) {
|
||||
throw ClassCastException("Cannot cast ${this.javaClass} to $selfType")
|
||||
|
||||
@@ -38,6 +38,11 @@ open class EthereumRpcUpstream(
|
||||
|
||||
private val head: Head = this.createHead()
|
||||
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) {
|
||||
if (head is CachesEnabled) {
|
||||
@@ -107,6 +112,14 @@ open class EthereumRpcUpstream(
|
||||
return listOf(node.labels)
|
||||
}
|
||||
|
||||
override fun getCapabilities(): Set<Capability> {
|
||||
return capabilities
|
||||
}
|
||||
|
||||
override fun isGrpc(): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
@Suppress("unchecked")
|
||||
override fun <T : Upstream> cast(selfType: Class<T>): T {
|
||||
if (!selfType.isAssignableFrom(this.javaClass)) {
|
||||
|
||||
@@ -22,10 +22,7 @@ import io.emeraldpay.dshackle.config.UpstreamsConfig
|
||||
import io.emeraldpay.dshackle.data.BlockContainer
|
||||
import io.emeraldpay.dshackle.data.BlockId
|
||||
import io.emeraldpay.dshackle.reader.Reader
|
||||
import io.emeraldpay.dshackle.upstream.Head
|
||||
import io.emeraldpay.dshackle.upstream.Selector
|
||||
import io.emeraldpay.dshackle.upstream.Upstream
|
||||
import io.emeraldpay.dshackle.upstream.UpstreamAvailability
|
||||
import io.emeraldpay.dshackle.upstream.*
|
||||
import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinUpstream
|
||||
import io.emeraldpay.dshackle.upstream.bitcoin.ExtractBlock
|
||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcGrpcClient
|
||||
@@ -45,7 +42,7 @@ import java.util.function.Function
|
||||
class BitcoinGrpcUpstream(
|
||||
private val parentId: String,
|
||||
chain: Chain,
|
||||
private val remote: ReactorBlockchainGrpc.ReactorBlockchainStub,
|
||||
val remote: ReactorBlockchainGrpc.ReactorBlockchainStub,
|
||||
private val client: JsonRpcGrpcClient
|
||||
) : BitcoinUpstream(
|
||||
"$parentId/${chain.chainCode}",
|
||||
@@ -92,6 +89,7 @@ class BitcoinGrpcUpstream(
|
||||
private val upstreamStatus = GrpcUpstreamStatus()
|
||||
private val grpcHead = GrpcHead(chain, this, blockConverter, reloadBlock)
|
||||
var timeout = Defaults.timeout
|
||||
private var capabilities: Set<Capability> = emptySet()
|
||||
|
||||
override fun getHead(): Head {
|
||||
return grpcHead
|
||||
@@ -105,6 +103,14 @@ class BitcoinGrpcUpstream(
|
||||
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 {
|
||||
if (!selfType.isAssignableFrom(this.javaClass)) {
|
||||
throw ClassCastException("Cannot cast ${this.javaClass} to $selfType")
|
||||
@@ -126,6 +132,7 @@ class BitcoinGrpcUpstream(
|
||||
|
||||
override fun update(conf: BlockchainOuterClass.DescribeChain) {
|
||||
upstreamStatus.update(conf)
|
||||
this.capabilities = RemoteCapabilities.extract(conf)
|
||||
conf.status?.let { status -> onStatus(status) }
|
||||
}
|
||||
|
||||
|
||||
@@ -90,6 +90,7 @@ open class EthereumGrpcUpstream(
|
||||
private val log = LoggerFactory.getLogger(EthereumGrpcUpstream::class.java)
|
||||
private val upstreamStatus = GrpcUpstreamStatus()
|
||||
private val grpcHead = GrpcHead(chain, this, blockConverter, reloadBlock)
|
||||
private var capabilities: Set<Capability> = emptySet()
|
||||
|
||||
private val defaultReader: Reader<JsonRpcRequest, JsonRpcResponse> = client.forSelector(Selector.empty)
|
||||
var timeout = Defaults.timeout
|
||||
@@ -109,6 +110,7 @@ open class EthereumGrpcUpstream(
|
||||
|
||||
override fun update(conf: BlockchainOuterClass.DescribeChain) {
|
||||
upstreamStatus.update(conf)
|
||||
capabilities = RemoteCapabilities.extract(conf)
|
||||
conf.status?.let { status -> onStatus(status) }
|
||||
}
|
||||
|
||||
@@ -148,4 +150,11 @@ open class EthereumGrpcUpstream(
|
||||
return this as T
|
||||
}
|
||||
|
||||
override fun getCapabilities(): Set<Capability> {
|
||||
return capabilities
|
||||
}
|
||||
|
||||
override fun isGrpc(): Boolean {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -92,6 +92,7 @@ class UpstreamsConfigReaderSpec extends Specification {
|
||||
with(act.upstreams.get(0)) {
|
||||
id == "local"
|
||||
chain == "bitcoin"
|
||||
options == null || options.providesBalance == false
|
||||
connection instanceof UpstreamsConfig.BitcoinConnection
|
||||
with((UpstreamsConfig.BitcoinConnection) connection) {
|
||||
rpc != null
|
||||
@@ -119,6 +120,7 @@ class UpstreamsConfigReaderSpec extends Specification {
|
||||
with(act.upstreams.get(0)) {
|
||||
id == "local"
|
||||
chain == "bitcoin"
|
||||
options.providesBalance == true
|
||||
connection instanceof UpstreamsConfig.BitcoinConnection
|
||||
with((UpstreamsConfig.BitcoinConnection) connection) {
|
||||
rpc != null
|
||||
|
||||
@@ -259,6 +259,7 @@ class TrackBitcoinAddressSpec extends Specification {
|
||||
}
|
||||
MultistreamHolder upstreams = new MultistreamHolderMock(Chain.BITCOIN, upstream)
|
||||
TrackBitcoinAddress track = new TrackBitcoinAddress(upstreams)
|
||||
track.setBalanceAvailability(Chain.BITCOIN, true)
|
||||
|
||||
when:
|
||||
def resp = track.subscribe(BlockchainOuterClass.BalanceRequest.newBuilder()
|
||||
@@ -285,6 +286,5 @@ class TrackBitcoinAddressSpec extends Specification {
|
||||
}
|
||||
.expectComplete()
|
||||
.verify(Duration.ofSeconds(1))
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,8 @@ defaults:
|
||||
upstreams:
|
||||
- id: local
|
||||
chain: bitcoin
|
||||
options:
|
||||
balance: true
|
||||
connection:
|
||||
bitcoin:
|
||||
rpc:
|
||||
|
||||
Reference in New Issue
Block a user