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)
|
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
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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")
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
@@ -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")
|
||||||
|
|||||||
@@ -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)) {
|
||||||
|
|||||||
@@ -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) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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)) {
|
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
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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:
|
upstreams:
|
||||||
- id: local
|
- id: local
|
||||||
chain: bitcoin
|
chain: bitcoin
|
||||||
|
options:
|
||||||
|
balance: true
|
||||||
connection:
|
connection:
|
||||||
bitcoin:
|
bitcoin:
|
||||||
rpc:
|
rpc:
|
||||||
|
|||||||
Reference in New Issue
Block a user