This commit is contained in:
terminal
2022-08-01 13:33:15 +04:00
19 changed files with 155 additions and 49 deletions

View File

@@ -224,7 +224,11 @@ class UpstreamsConfigReader(
upNode: MappingNode, upNode: MappingNode,
upstream: UpstreamsConfig.Upstream<UpstreamsConfig.GrpcConnection> upstream: UpstreamsConfig.Upstream<UpstreamsConfig.GrpcConnection>
) { ) {
// Dshackle gRPC connection dispatches requests to different upstreams, which may
// be on different blockchains, and each may have different set of labels.
// So the labels and chains assigned to the gRPC connection make no sense.
if (hasAny(upNode, "labels")) { if (hasAny(upNode, "labels")) {
// Actual labels from underlying upstreams are handled by GrpcUpstreamStatus
log.warn("Labels should be not applied to gRPC upstream") log.warn("Labels should be not applied to gRPC upstream")
} }
if (hasAny(upNode, "chain")) { if (hasAny(upNode, "chain")) {

View File

@@ -57,7 +57,7 @@ class TrackBitcoinAddress(
/** /**
* Keep tracking of the current state of local upstreams. True for a chain that has an upstream with balance data. * 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() private val localBalanceAvailable: MutableMap<Chain, Boolean> = ConcurrentHashMap()
/** /**
* Criteria for a remote grpc upstream that can provide a balance * Criteria for a remote grpc upstream that can provide a balance
@@ -72,7 +72,7 @@ class TrackBitcoinAddress(
multistreamHolder.observeChains().subscribe { chain -> multistreamHolder.observeChains().subscribe { chain ->
multistreamHolder.getUpstream(chain)?.let { mup -> multistreamHolder.getUpstream(chain)?.let { mup ->
val available = mup.getAll().any { up -> val available = mup.getAll().any { up ->
!up.isGrpc() && (up.getOptions().providesBalance ?: false) !up.isGrpc() && up.getCapabilities().contains(Capability.BALANCE)
} }
setBalanceAvailability(chain, available) setBalanceAvailability(chain, available)
} }
@@ -80,14 +80,14 @@ class TrackBitcoinAddress(
} }
fun setBalanceAvailability(chain: Chain, enabled: Boolean) { fun setBalanceAvailability(chain: Chain, enabled: Boolean) {
balanceAvailable[chain] = enabled localBalanceAvailable[chain] = enabled
} }
/** /**
* @return true if the current instance has data sources to provide the balance * @return true if the current instance has data sources to provide the balance
*/ */
fun isBalanceAvailable(chain: Chain): Boolean { fun isBalanceAvailable(chain: Chain): Boolean {
return balanceAvailable[chain] ?: false return localBalanceAvailable[chain] ?: false
} }
fun allAddresses(api: BitcoinMultistream, request: BlockchainOuterClass.BalanceRequest): Flux<String> { fun allAddresses(api: BitcoinMultistream, request: BlockchainOuterClass.BalanceRequest): Flux<String> {
@@ -179,10 +179,9 @@ class TrackBitcoinAddress(
} }
.timeout(Defaults.timeoutInternal, Mono.empty()) .timeout(Defaults.timeoutInternal, Mono.empty())
.switchIfEmpty( .switchIfEmpty(
Mono.just(0) Mono.fromCallable {
.doOnNext { log.warn("No upstream providing balance for ${api.chain}")
log.warn("No upstream providing balance for ${api.chain}") }
}
.then(Mono.error(SilentException.DataUnavailable("BALANCE"))) .then(Mono.error(SilentException.DataUnavailable("BALANCE")))
) )
} }

View File

@@ -257,7 +257,7 @@ open class ConfiguredUpstreams(
log.info("Using ALL CHAINS (gRPC) upstream, at ${endpoint.host}:${endpoint.port}") log.info("Using ALL CHAINS (gRPC) upstream, at ${endpoint.host}:${endpoint.port}")
ds.start() ds.start()
.doOnNext { .doOnNext {
log.info("Chain ${it.chain} has ${it.type} through gRPC at ${endpoint.host}:${endpoint.port}") log.info("Chain ${it.chain} ${it.type} through gRPC at ${endpoint.host}:${endpoint.port}. With caps: ${it.upstream.getCapabilities()}")
} }
.subscribe(currentUpstreams::update) .subscribe(currentUpstreams::update)
} }

View File

@@ -26,7 +26,7 @@ import io.emeraldpay.dshackle.upstream.Multistream
import io.emeraldpay.dshackle.upstream.RequestPostprocessor import io.emeraldpay.dshackle.upstream.RequestPostprocessor
import io.emeraldpay.dshackle.upstream.Selector import io.emeraldpay.dshackle.upstream.Selector
import io.emeraldpay.dshackle.upstream.Upstream import io.emeraldpay.dshackle.upstream.Upstream
import io.emeraldpay.dshackle.upstream.ethereum.LocalCallRouter import io.emeraldpay.dshackle.upstream.calls.DefaultBitcoinMethods
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
import io.emeraldpay.grpc.Chain import io.emeraldpay.grpc.Chain
@@ -37,28 +37,34 @@ import reactor.core.publisher.Mono
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
open class BitcoinMultistream( open class BitcoinMultistream(
chain: Chain, chain: Chain,
val upstreams: MutableList<BitcoinUpstream>, private val sourceUpstreams: MutableList<BitcoinUpstream>,
caches: Caches caches: Caches
) : Multistream(chain, upstreams as MutableList<Upstream>, caches, RequestPostprocessor.Empty()), Lifecycle { ) : Multistream(chain, sourceUpstreams as MutableList<Upstream>, caches, RequestPostprocessor.Empty()), Lifecycle {
companion object { companion object {
private val log = LoggerFactory.getLogger(BitcoinMultistream::class.java) private val log = LoggerFactory.getLogger(BitcoinMultistream::class.java)
} }
private var head: Head = EmptyHead() private var head: Head = EmptyHead()
private var esplora = upstreams.find { it.esploraClient != null }?.esploraClient private var esplora = sourceUpstreams.find { it.esploraClient != null }?.esploraClient
private var reader = BitcoinReader(this, head, esplora) private var reader = BitcoinReader(this, head, esplora)
private var addressActiveCheck: AddressActiveCheck? = null private var addressActiveCheck: AddressActiveCheck? = null
private var xpubAddresses: XpubAddresses? = null private var xpubAddresses: XpubAddresses? = null
private val feeEstimation = BitcoinFees(this, reader, 6) private val feeEstimation = BitcoinFees(this, reader, 6)
private var callRouter: LocalCallRouter = LocalCallRouter(DefaultBitcoinMethods(), reader)
override fun init() { override fun init() {
if (upstreams.size > 0) { if (sourceUpstreams.size > 0) {
head = updateHead() head = updateHead()
} }
super.init() super.init()
} }
open val upstreams: List<BitcoinUpstream>
get() {
return sourceUpstreams
}
override fun getFeeEstimation(): ChainFees { override fun getFeeEstimation(): ChainFees {
return feeEstimation return feeEstimation
} }
@@ -75,8 +81,8 @@ open class BitcoinMultistream(
} }
lagObserver?.stop() lagObserver?.stop()
lagObserver = null lagObserver = null
val head = if (upstreams.size == 1) { val head = if (sourceUpstreams.size == 1) {
val upstream = upstreams.first() val upstream = sourceUpstreams.first()
upstream.setLag(0) upstream.setLag(0)
upstream.getHead().apply { upstream.getHead().apply {
if (this is Lifecycle) { if (this is Lifecycle) {
@@ -84,10 +90,10 @@ open class BitcoinMultistream(
} }
} }
} else { } else {
val newHead = MergedHead(upstreams.map { it.getHead() }).apply { val newHead = MergedHead(sourceUpstreams.map { it.getHead() }).apply {
this.start() this.start()
} }
val lagObserver = BitcoinHeadLagObserver(newHead, upstreams) val lagObserver = BitcoinHeadLagObserver(newHead, sourceUpstreams)
this.lagObserver = lagObserver this.lagObserver = lagObserver
lagObserver.start() lagObserver.start()
newHead newHead
@@ -97,7 +103,7 @@ open class BitcoinMultistream(
} }
override fun getRoutedApi(matcher: Selector.Matcher): Mono<Reader<JsonRpcRequest, JsonRpcResponse>> { override fun getRoutedApi(matcher: Selector.Matcher): Mono<Reader<JsonRpcRequest, JsonRpcResponse>> {
return Mono.just(LocalCallRouter(getMethods())) return Mono.just(callRouter)
} }
open fun getReader(): BitcoinReader { open fun getReader(): BitcoinReader {
@@ -106,10 +112,11 @@ open class BitcoinMultistream(
override fun onUpstreamsUpdated() { override fun onUpstreamsUpdated() {
super.onUpstreamsUpdated() super.onUpstreamsUpdated()
esplora = upstreams.find { it.esploraClient != null }?.esploraClient esplora = sourceUpstreams.find { it.esploraClient != null }?.esploraClient
reader = BitcoinReader(this, this.head, esplora) reader = BitcoinReader(this, this.head, esplora)
addressActiveCheck = esplora?.let { AddressActiveCheck(it) } addressActiveCheck = esplora?.let { AddressActiveCheck(it) }
xpubAddresses = addressActiveCheck?.let { XpubAddresses(it) } xpubAddresses = addressActiveCheck?.let { XpubAddresses(it) }
callRouter = LocalCallRouter(getMethods(), reader)
} }
override fun setHead(head: Head) { override fun setHead(head: Head) {
@@ -122,7 +129,7 @@ open class BitcoinMultistream(
} }
override fun getLabels(): Collection<UpstreamsConfig.Labels> { override fun getLabels(): Collection<UpstreamsConfig.Labels> {
return upstreams.flatMap { it.getLabels() } return sourceUpstreams.flatMap { it.getLabels() }
} }
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")

View File

@@ -17,6 +17,7 @@ package io.emeraldpay.dshackle.upstream.bitcoin
import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.ObjectMapper
import io.emeraldpay.dshackle.Global import io.emeraldpay.dshackle.Global
import io.emeraldpay.dshackle.upstream.Capability
import io.emeraldpay.dshackle.upstream.Head import io.emeraldpay.dshackle.upstream.Head
import io.emeraldpay.dshackle.upstream.Selector import io.emeraldpay.dshackle.upstream.Selector
import io.emeraldpay.dshackle.upstream.bitcoin.data.SimpleUnspent import io.emeraldpay.dshackle.upstream.bitcoin.data.SimpleUnspent
@@ -43,6 +44,8 @@ open class BitcoinReader(
private val unspentReader: UnspentReader = if (esploraClient != null) { private val unspentReader: UnspentReader = if (esploraClient != null) {
EsploraUnspentReader(esploraClient, head) EsploraUnspentReader(esploraClient, head)
} else if (upstreams.upstreams.any { it.isGrpc() && it.getCapabilities().contains(Capability.BALANCE) }) {
RemoteUnspentReader(upstreams)
} else { } else {
RpcUnspentReader(upstreams) RpcUnspentReader(upstreams)
} }

View File

@@ -37,7 +37,6 @@ class EsploraUnspentReader(
base.txid, base.txid,
base.vout, base.vout,
base.value, base.value,
head.getCurrentHeight()?.let { base.height - it } ?: 0
) )
} }

View File

@@ -15,12 +15,17 @@
*/ */
package io.emeraldpay.dshackle.upstream.bitcoin package io.emeraldpay.dshackle.upstream.bitcoin
import io.emeraldpay.dshackle.Global
import io.emeraldpay.dshackle.SilentException
import io.emeraldpay.dshackle.reader.Reader import io.emeraldpay.dshackle.reader.Reader
import io.emeraldpay.dshackle.upstream.bitcoin.data.RpcUnspent
import io.emeraldpay.dshackle.upstream.bitcoin.data.SimpleUnspent
import io.emeraldpay.dshackle.upstream.calls.CallMethods import io.emeraldpay.dshackle.upstream.calls.CallMethods
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
import io.emeraldpay.etherjar.rpc.RpcException import io.emeraldpay.etherjar.rpc.RpcException
import io.emeraldpay.etherjar.rpc.RpcResponseError import io.emeraldpay.etherjar.rpc.RpcResponseError
import org.bitcoinj.core.Address
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import reactor.core.publisher.Mono import reactor.core.publisher.Mono
@@ -33,6 +38,7 @@ import reactor.core.publisher.Mono
*/ */
class LocalCallRouter( class LocalCallRouter(
private val methods: CallMethods, private val methods: CallMethods,
private val reader: BitcoinReader,
) : Reader<JsonRpcRequest, JsonRpcResponse> { ) : Reader<JsonRpcRequest, JsonRpcResponse> {
companion object { companion object {
@@ -47,6 +53,39 @@ class LocalCallRouter(
if (!methods.isCallable(key.method)) { if (!methods.isCallable(key.method)) {
return Mono.error(RpcException(RpcResponseError.CODE_METHOD_NOT_EXIST, "Unsupported method")) return Mono.error(RpcException(RpcResponseError.CODE_METHOD_NOT_EXIST, "Unsupported method"))
} }
if (key.method == "listunspent") {
return processUnspentRequest(key)
}
return Mono.empty() return Mono.empty()
} }
/**
*
*/
fun processUnspentRequest(key: JsonRpcRequest): Mono<JsonRpcResponse> {
if (key.params.size < 3) {
return Mono.error(SilentException("Invalid call to unspent. Address is missing"))
}
val addresses = key.params[2]
if (addresses is List<*> && addresses.size > 0) {
val address = addresses[0].toString().let { Address.fromString(null, it) }
return reader.listUnspent(address).map {
val rpc = it.map(convertUnspent(address))
val json = Global.objectMapper.writeValueAsBytes(rpc)
JsonRpcResponse.ok(json, JsonRpcResponse.NumberId(key.id))
}
}
return Mono.error(SilentException("Invalid call to unspent"))
}
fun convertUnspent(address: Address): (SimpleUnspent) -> RpcUnspent {
return { base ->
RpcUnspent(
base.txid,
base.vout,
address.toString(),
base.value,
)
}
}
} }

View File

@@ -0,0 +1,48 @@
package io.emeraldpay.dshackle.upstream.bitcoin
import io.emeraldpay.api.proto.BlockchainOuterClass.BalanceRequest
import io.emeraldpay.dshackle.upstream.Capability
import io.emeraldpay.dshackle.upstream.Selector
import io.emeraldpay.dshackle.upstream.bitcoin.data.SimpleUnspent
import io.emeraldpay.dshackle.upstream.grpc.BitcoinGrpcUpstream
import org.bitcoinj.core.Address
import org.slf4j.LoggerFactory
import reactor.core.publisher.Mono
class RemoteUnspentReader(
val upstreams: BitcoinMultistream
) : UnspentReader {
companion object {
private val log = LoggerFactory.getLogger(RemoteUnspentReader::class.java)
}
private val selector = Selector.LocalAndMatcher(
Selector.GrpcMatcher(),
Selector.CapabilityMatcher(Capability.BALANCE)
)
override fun read(key: Address): Mono<List<SimpleUnspent>> {
val apis = upstreams.getApiSource(selector)
apis.request(1)
return Mono.from(apis)
.map { up ->
up.cast(BitcoinGrpcUpstream::class.java).remote
}
.flatMapMany {
val request = BalanceRequest.newBuilder()
.build()
it.getBalance(request)
}
.map { resp ->
resp.utxoList.map { utxo ->
SimpleUnspent(
utxo.txId,
utxo.index.toInt(),
utxo.balance.toLong(),
)
}
}
.reduce(List<SimpleUnspent>::plus)
}
}

View File

@@ -16,6 +16,8 @@
package io.emeraldpay.dshackle.upstream.bitcoin package io.emeraldpay.dshackle.upstream.bitcoin
import io.emeraldpay.dshackle.Global import io.emeraldpay.dshackle.Global
import io.emeraldpay.dshackle.SilentException
import io.emeraldpay.dshackle.upstream.Capability
import io.emeraldpay.dshackle.upstream.Selector import io.emeraldpay.dshackle.upstream.Selector
import io.emeraldpay.dshackle.upstream.bitcoin.data.RpcUnspent import io.emeraldpay.dshackle.upstream.bitcoin.data.RpcUnspent
import io.emeraldpay.dshackle.upstream.bitcoin.data.SimpleUnspent import io.emeraldpay.dshackle.upstream.bitcoin.data.SimpleUnspent
@@ -38,14 +40,17 @@ class RpcUnspentReader(
base.txid, base.txid,
base.vout, base.vout,
base.amount, base.amount,
base.confirmations
) )
} }
private val selector = Selector.CapabilityMatcher(Capability.BALANCE)
override fun read(key: Address): Mono<List<SimpleUnspent>> { override fun read(key: Address): Mono<List<SimpleUnspent>> {
// docs: https://developer.bitcoin.org/reference/rpc/listunspent.html
//
val address = key.toString() val address = key.toString()
return upstreams.getDirectApi(Selector.empty).flatMap { api -> return upstreams.getDirectApi(selector).flatMap { api ->
api.read(JsonRpcRequest("listunspent", emptyList())) api.read(JsonRpcRequest("listunspent", listOf(1, 9999999, listOf(address))))
.flatMap(JsonRpcResponse::requireResult) .flatMap(JsonRpcResponse::requireResult)
.map { .map {
Global.objectMapper.readerFor(RpcUnspent::class.java).readValues<RpcUnspent>(it).readAll() Global.objectMapper.readerFor(RpcUnspent::class.java).readValues<RpcUnspent>(it).readAll()
@@ -55,6 +60,6 @@ class RpcUnspentReader(
it.address == address it.address == address
}.map(convert) }.map(convert)
} }
} }.switchIfEmpty(Mono.error(SilentException.DataUnavailable("BALANCE")))
} }
} }

View File

@@ -20,5 +20,4 @@ data class RpcUnspent(
val vout: Int, val vout: Int,
val address: String, val address: String,
val amount: Long, val amount: Long,
val confirmations: Long
) )

View File

@@ -30,7 +30,6 @@ class RpcUnspentDeserializer : JsonDeserializer<RpcUnspent>() {
node.get("vout").asInt(), node.get("vout").asInt(),
node.get("address").asText(), node.get("address").asText(),
BigDecimal(node.get("amount").asText()).multiply(BigDecimal.TEN.pow(8)).longValueExact(), BigDecimal(node.get("amount").asText()).multiply(BigDecimal.TEN.pow(8)).longValueExact(),
node.get("confirmations").asLong()
) )
} }
} }

View File

@@ -19,5 +19,4 @@ data class SimpleUnspent(
val txid: String, val txid: String,
val vout: Int, val vout: Int,
val value: Long, val value: Long,
val confirmations: Long
) )

View File

@@ -99,6 +99,10 @@ class BitcoinGrpcUpstream(
var timeout = Defaults.timeout var timeout = Defaults.timeout
private var capabilities: Set<Capability> = emptySet() private var capabilities: Set<Capability> = emptySet()
override fun getBlockchainApi(): ReactorBlockchainGrpc.ReactorBlockchainStub {
return remote
}
override fun getHead(): Head { override fun getHead(): Head {
return grpcHead return grpcHead
} }

View File

@@ -103,6 +103,10 @@ open class EthereumGrpcUpstream(
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
override fun getBlockchainApi(): ReactorBlockchainGrpc.ReactorBlockchainStub {
return remote
}
override fun start() { override fun start() {
} }

View File

@@ -16,12 +16,16 @@
package io.emeraldpay.dshackle.upstream.grpc package io.emeraldpay.dshackle.upstream.grpc
import io.emeraldpay.api.proto.BlockchainOuterClass import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.api.proto.ReactorBlockchainGrpc
import io.emeraldpay.dshackle.upstream.Upstream
interface GrpcUpstream { interface GrpcUpstream : Upstream {
/** /**
* Update the configuration of the upstream with the new data. * Update the configuration of the upstream with the new data.
* Called on the first creation, and each time a new state received from upstream * Called on the first creation, and each time a new state received from upstream
*/ */
fun update(conf: BlockchainOuterClass.DescribeChain) fun update(conf: BlockchainOuterClass.DescribeChain)
fun getBlockchainApi(): ReactorBlockchainGrpc.ReactorBlockchainStub
} }

View File

@@ -91,7 +91,7 @@ class GrpcUpstreams(
client.describe(BlockchainOuterClass.DescribeRequest.newBuilder().build()) client.describe(BlockchainOuterClass.DescribeRequest.newBuilder().build())
}.onErrorContinue { t, _ -> }.onErrorContinue { t, _ ->
if (ExceptionUtils.indexOfType(t, ConnectException::class.java) >= 0) { if (ExceptionUtils.indexOfType(t, ConnectException::class.java) >= 0) {
log.warn("gRPC upstream $host:$port is unavailable") log.warn("gRPC upstream $host:$port is unavailable. (${t.javaClass}: ${t.message})")
known.values.forEach { known.values.forEach {
it.setStatus(UpstreamAvailability.UNAVAILABLE) it.setStatus(UpstreamAvailability.UNAVAILABLE)
} }

View File

@@ -51,7 +51,7 @@ class TrackBitcoinAddressSpec extends Specification {
def "Correct sum for single"() { def "Correct sum for single"() {
setup: setup:
def unspents = [ def unspents = [
new SimpleUnspent("f14b222e652c58d11435fa9172ddea000c6f5e20e6b715eb940fc28d1c4adeef", 0, 100L, 123L) new SimpleUnspent("f14b222e652c58d11435fa9172ddea000c6f5e20e6b715eb940fc28d1c4adeef", 0, 100L)
] ]
TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(MultistreamHolder)) TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(MultistreamHolder))
def address = new TrackBitcoinAddress.Address( def address = new TrackBitcoinAddress.Address(
@@ -68,8 +68,8 @@ class TrackBitcoinAddressSpec extends Specification {
def "Correct sum for few"() { def "Correct sum for few"() {
setup: setup:
def unspents = [ def unspents = [
new SimpleUnspent("f14b222e652c58d11435fa9172ddea000c6f5e20e6b715eb940fc28d1c4adeef", 0, 100L, 123L), new SimpleUnspent("f14b222e652c58d11435fa9172ddea000c6f5e20e6b715eb940fc28d1c4adeef", 0, 100L),
new SimpleUnspent("17d1c4adf14b222e652c58d11435fa9ee2ddea000c6f5e20e6b715eb940fc28f", 0, 123L, 123L), new SimpleUnspent("17d1c4adf14b222e652c58d11435fa9ee2ddea000c6f5e20e6b715eb940fc28f", 0, 123L),
] ]
TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(MultistreamHolder)) TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(MultistreamHolder))
def address = new TrackBitcoinAddress.Address( def address = new TrackBitcoinAddress.Address(
@@ -120,8 +120,8 @@ class TrackBitcoinAddressSpec extends Specification {
def "Correct sum for few with utxo"() { def "Correct sum for few with utxo"() {
setup: setup:
def unspents = [ def unspents = [
new SimpleUnspent("f14b222e652c58d11435fa9172ddea000c6f5e20e6b715eb940fc28d1c4adeef", 0, 100L, 123L), new SimpleUnspent("f14b222e652c58d11435fa9172ddea000c6f5e20e6b715eb940fc28d1c4adeef", 0, 100L),
new SimpleUnspent("17d1c4adf14b222e652c58d11435fa9ee2ddea000c6f5e20e6b715eb940fc28f", 0, 123L, 123L), new SimpleUnspent("17d1c4adf14b222e652c58d11435fa9ee2ddea000c6f5e20e6b715eb940fc28f", 0, 123L),
] ]
TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(MultistreamHolder)) TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(MultistreamHolder))
def address = new TrackBitcoinAddress.Address( def address = new TrackBitcoinAddress.Address(
@@ -282,7 +282,7 @@ class TrackBitcoinAddressSpec extends Specification {
2 * listUnspent(_) >>> [ 2 * listUnspent(_) >>> [
Mono.just([]), Mono.just([]),
Mono.just([ Mono.just([
new SimpleUnspent("f14b222e652c58d11435fa9172ddea000c6f5e20e6b715eb940fc28d1c4adeef", 0, 1230000L, 123L) new SimpleUnspent("f14b222e652c58d11435fa9172ddea000c6f5e20e6b715eb940fc28d1c4adeef", 0, 1230000L)
]) ])
] ]
} }

View File

@@ -34,7 +34,8 @@ class BitcoinReaderSpec extends Specification {
api.answerOnce("getblockhash", [100000], "000000000003ba27aa200b1cecaad478d2b00432346c3f1f3986da1afd33e506") api.answerOnce("getblockhash", [100000], "000000000003ba27aa200b1cecaad478d2b00432346c3f1f3986da1afd33e506")
api.answerOnce("getblock", ["000000000003ba27aa200b1cecaad478d2b00432346c3f1f3986da1afd33e506"], block) api.answerOnce("getblock", ["000000000003ba27aa200b1cecaad478d2b00432346c3f1f3986da1afd33e506"], block)
def ups = Mock(BitcoinMultistream) { def ups = Mock(BitcoinMultistream) {
_ * it.getDirectApi(_) >> Mono.just(api) _ * getDirectApi(_) >> Mono.just(api)
_ * upstreams >> []
} }
def reader = new BitcoinReader(ups, Stub(Head), null) def reader = new BitcoinReader(ups, Stub(Head), null)

View File

@@ -29,7 +29,7 @@ class RpcUnspentReaderSpec extends Specification {
setup: setup:
def json = this.class.getClassLoader().getResourceAsStream("bitcoin/unspent-one-addr.json").bytes def json = this.class.getClassLoader().getResourceAsStream("bitcoin/unspent-one-addr.json").bytes
def rpcReader = Mock(Reader) { def rpcReader = Mock(Reader) {
1 * read(new JsonRpcRequest("listunspent", [])) >> Mono.just(JsonRpcResponse.ok(json)) 1 * read(new JsonRpcRequest("listunspent", [1, 9999999, ["1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK"]])) >> Mono.just(JsonRpcResponse.ok(json))
} }
def upstreams = Mock(BitcoinMultistream) { def upstreams = Mock(BitcoinMultistream) {
1 * getDirectApi(_) >> Mono.just(rpcReader) 1 * getDirectApi(_) >> Mono.just(rpcReader)
@@ -45,19 +45,16 @@ class RpcUnspentReaderSpec extends Specification {
with(act[0]) { with(act[0]) {
txid == "e0f946c8f971b25cdffa64eed71d886019e437c0bf6a1b280584c0be5d1b5409" txid == "e0f946c8f971b25cdffa64eed71d886019e437c0bf6a1b280584c0be5d1b5409"
vout == 29 vout == 29
confirmations == 2010
value == 1230030 value == 1230030
} }
with(act[1]) { with(act[1]) {
txid == "66e1e4d14ed6f454d2fda036f35cba423274ecdf5d46deb93f172c412a0f650d" txid == "66e1e4d14ed6f454d2fda036f35cba423274ecdf5d46deb93f172c412a0f650d"
vout == 83 vout == 83
confirmations == 4963
value == 756339 value == 756339
} }
with(act[35]) { with(act[35]) {
txid == "f14b222e652c58d11435fa9172ddea000c6f5e20e6b715eb940fc28d1c4adeef" txid == "f14b222e652c58d11435fa9172ddea000c6f5e20e6b715eb940fc28d1c4adeef"
vout == 58 vout == 58
confirmations == 2139
value == 1105047 value == 1105047
} }
} }
@@ -66,7 +63,7 @@ class RpcUnspentReaderSpec extends Specification {
setup: setup:
def json = this.class.getClassLoader().getResourceAsStream("bitcoin/unspent-two-addr.json").bytes def json = this.class.getClassLoader().getResourceAsStream("bitcoin/unspent-two-addr.json").bytes
def rpcReader = Mock(Reader) { def rpcReader = Mock(Reader) {
1 * read(new JsonRpcRequest("listunspent", [])) >> Mono.just(JsonRpcResponse.ok(json)) 1 * read(new JsonRpcRequest("listunspent", [1, 9999999, ["35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP"]])) >> Mono.just(JsonRpcResponse.ok(json))
} }
def upstreams = Mock(BitcoinMultistream) { def upstreams = Mock(BitcoinMultistream) {
1 * getDirectApi(_) >> Mono.just(rpcReader) 1 * getDirectApi(_) >> Mono.just(rpcReader)
@@ -83,19 +80,16 @@ class RpcUnspentReaderSpec extends Specification {
txid == "8ad0d954a01eeb4f2c62d58d291699af847f9c8df43b775c27ffe8a5f76eba00" txid == "8ad0d954a01eeb4f2c62d58d291699af847f9c8df43b775c27ffe8a5f76eba00"
vout == 1 vout == 1
value == 216465 value == 216465
confirmations == 2583
} }
with(act[11]) { with(act[11]) {
txid == "777671a46b30b068052a73387e036bc8515cd3ba6adf9be4c70dfc0699f67c09" txid == "777671a46b30b068052a73387e036bc8515cd3ba6adf9be4c70dfc0699f67c09"
vout == 0 vout == 0
confirmations == 13705
value == 307906 value == 307906
} }
// cat src/test/resources/bitcoin/unspent-two-addr.json | jq '[.[] | select(.address == "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP")] | .[211]' // cat src/test/resources/bitcoin/unspent-two-addr.json | jq '[.[] | select(.address == "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP")] | .[211]'
with(act[211]) { with(act[211]) {
txid == "f20727393b0a586a3062a615fb71f43ec21c24258c3c6ec546fee5cbc1fa2ba7" txid == "f20727393b0a586a3062a615fb71f43ec21c24258c3c6ec546fee5cbc1fa2ba7"
vout == 0 vout == 0
confirmations == 21890
value == 50000000000 value == 50000000000
} }
} }
@@ -104,7 +98,7 @@ class RpcUnspentReaderSpec extends Specification {
setup: setup:
def json = this.class.getClassLoader().getResourceAsStream("bitcoin/unspent-two-addr.json").bytes def json = this.class.getClassLoader().getResourceAsStream("bitcoin/unspent-two-addr.json").bytes
def rpcReader = Mock(Reader) { def rpcReader = Mock(Reader) {
1 * read(new JsonRpcRequest("listunspent", [])) >> Mono.just(JsonRpcResponse.ok(json)) 1 * read(_) >> Mono.just(JsonRpcResponse.ok(json))
} }
def upstreams = Mock(BitcoinMultistream) { def upstreams = Mock(BitcoinMultistream) {
1 * getDirectApi(_) >> Mono.just(rpcReader) 1 * getDirectApi(_) >> Mono.just(rpcReader)
@@ -122,13 +116,11 @@ class RpcUnspentReaderSpec extends Specification {
txid == "e0f946c8f971b25cdffa64eed71d886019e437c0bf6a1b280584c0be5d1b5409" txid == "e0f946c8f971b25cdffa64eed71d886019e437c0bf6a1b280584c0be5d1b5409"
vout == 29 vout == 29
value == 1230030 value == 1230030
confirmations == 2030
} }
// cat src/test/resources/bitcoin/unspent-two-addr.json | jq '[.[] | select(.address == "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK")] | .[35]' // cat src/test/resources/bitcoin/unspent-two-addr.json | jq '[.[] | select(.address == "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK")] | .[35]'
with(act[35]) { with(act[35]) {
txid == "f14b222e652c58d11435fa9172ddea000c6f5e20e6b715eb940fc28d1c4adeef" txid == "f14b222e652c58d11435fa9172ddea000c6f5e20e6b715eb940fc28d1c4adeef"
vout == 58 vout == 58
confirmations == 2159
value == 1105047 value == 1105047
} }
} }