merge
This commit is contained in:
@@ -247,7 +247,11 @@ class UpstreamsConfigReader(
|
||||
upNode: MappingNode,
|
||||
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")) {
|
||||
// Actual labels from underlying upstreams are handled by GrpcUpstreamStatus
|
||||
log.warn("Labels should be not applied to gRPC upstream")
|
||||
}
|
||||
if (hasAny(upNode, "chain")) {
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
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
|
||||
@@ -72,7 +72,7 @@ class TrackBitcoinAddress(
|
||||
multistreamHolder.observeChains().subscribe { chain ->
|
||||
multistreamHolder.getUpstream(chain)?.let { mup ->
|
||||
val available = mup.getAll().any { up ->
|
||||
!up.isGrpc() && (up.getOptions().providesBalance ?: false)
|
||||
!up.isGrpc() && up.getCapabilities().contains(Capability.BALANCE)
|
||||
}
|
||||
setBalanceAvailability(chain, available)
|
||||
}
|
||||
@@ -80,14 +80,14 @@ class TrackBitcoinAddress(
|
||||
}
|
||||
|
||||
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
|
||||
*/
|
||||
fun isBalanceAvailable(chain: Chain): Boolean {
|
||||
return balanceAvailable[chain] ?: false
|
||||
return localBalanceAvailable[chain] ?: false
|
||||
}
|
||||
|
||||
fun allAddresses(api: BitcoinMultistream, request: BlockchainOuterClass.BalanceRequest): Flux<String> {
|
||||
@@ -179,10 +179,9 @@ class TrackBitcoinAddress(
|
||||
}
|
||||
.timeout(Defaults.timeoutInternal, Mono.empty())
|
||||
.switchIfEmpty(
|
||||
Mono.just(0)
|
||||
.doOnNext {
|
||||
log.warn("No upstream providing balance for ${api.chain}")
|
||||
}
|
||||
Mono.fromCallable {
|
||||
log.warn("No upstream providing balance for ${api.chain}")
|
||||
}
|
||||
.then(Mono.error(SilentException.DataUnavailable("BALANCE")))
|
||||
)
|
||||
}
|
||||
|
||||
@@ -263,7 +263,7 @@ open class ConfiguredUpstreams(
|
||||
log.info("Using ALL CHAINS (gRPC) upstream, at ${endpoint.host}:${endpoint.port}")
|
||||
ds.start()
|
||||
.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)
|
||||
}
|
||||
|
||||
@@ -26,8 +26,9 @@ import io.emeraldpay.dshackle.upstream.Multistream
|
||||
import io.emeraldpay.dshackle.upstream.RequestPostprocessor
|
||||
import io.emeraldpay.dshackle.upstream.Selector
|
||||
import io.emeraldpay.dshackle.upstream.Upstream
|
||||
import io.emeraldpay.dshackle.upstream.ethereum.LocalCallRouter
|
||||
import io.emeraldpay.dshackle.upstream.bitcoin.LocalCallRouter
|
||||
import io.emeraldpay.dshackle.upstream.forkchoice.MostWorkForkChoice
|
||||
import io.emeraldpay.dshackle.upstream.calls.DefaultBitcoinMethods
|
||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
|
||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
|
||||
import io.emeraldpay.grpc.Chain
|
||||
@@ -38,28 +39,34 @@ import reactor.core.publisher.Mono
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
open class BitcoinMultistream(
|
||||
chain: Chain,
|
||||
val upstreams: MutableList<BitcoinUpstream>,
|
||||
private val sourceUpstreams: MutableList<BitcoinUpstream>,
|
||||
caches: Caches
|
||||
) : Multistream(chain, upstreams as MutableList<Upstream>, caches, RequestPostprocessor.Empty()), Lifecycle {
|
||||
) : Multistream(chain, sourceUpstreams as MutableList<Upstream>, caches, RequestPostprocessor.Empty()), Lifecycle {
|
||||
|
||||
companion object {
|
||||
private val log = LoggerFactory.getLogger(BitcoinMultistream::class.java)
|
||||
}
|
||||
|
||||
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 addressActiveCheck: AddressActiveCheck? = null
|
||||
private var xpubAddresses: XpubAddresses? = null
|
||||
private val feeEstimation = BitcoinFees(this, reader, 6)
|
||||
private var callRouter: LocalCallRouter = LocalCallRouter(DefaultBitcoinMethods(), reader)
|
||||
|
||||
override fun init() {
|
||||
if (upstreams.size > 0) {
|
||||
if (sourceUpstreams.size > 0) {
|
||||
head = updateHead()
|
||||
}
|
||||
super.init()
|
||||
}
|
||||
|
||||
open val upstreams: List<BitcoinUpstream>
|
||||
get() {
|
||||
return sourceUpstreams
|
||||
}
|
||||
|
||||
override fun getFeeEstimation(): ChainFees {
|
||||
return feeEstimation
|
||||
}
|
||||
@@ -76,8 +83,8 @@ open class BitcoinMultistream(
|
||||
}
|
||||
lagObserver?.stop()
|
||||
lagObserver = null
|
||||
val head = if (upstreams.size == 1) {
|
||||
val upstream = upstreams.first()
|
||||
val head = if (sourceUpstreams.size == 1) {
|
||||
val upstream = sourceUpstreams.first()
|
||||
upstream.setLag(0)
|
||||
upstream.getHead().apply {
|
||||
if (this is Lifecycle) {
|
||||
@@ -85,10 +92,10 @@ open class BitcoinMultistream(
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val newHead = MergedHead(upstreams.map { it.getHead() }, MostWorkForkChoice()).apply {
|
||||
this.start()
|
||||
}
|
||||
val lagObserver = BitcoinHeadLagObserver(newHead, upstreams)
|
||||
val newHead = MergedHead(sourceUpstreams.map { it.getHead() }, MostWorkForkChoice()).apply {
|
||||
this.start()
|
||||
}
|
||||
val lagObserver = BitcoinHeadLagObserver(newHead, sourceUpstreams)
|
||||
this.lagObserver = lagObserver
|
||||
lagObserver.start()
|
||||
newHead
|
||||
@@ -98,7 +105,7 @@ open class BitcoinMultistream(
|
||||
}
|
||||
|
||||
override fun getRoutedApi(matcher: Selector.Matcher): Mono<Reader<JsonRpcRequest, JsonRpcResponse>> {
|
||||
return Mono.just(LocalCallRouter(getMethods()))
|
||||
return Mono.just(callRouter)
|
||||
}
|
||||
|
||||
open fun getReader(): BitcoinReader {
|
||||
@@ -107,10 +114,11 @@ open class BitcoinMultistream(
|
||||
|
||||
override fun onUpstreamsUpdated() {
|
||||
super.onUpstreamsUpdated()
|
||||
esplora = upstreams.find { it.esploraClient != null }?.esploraClient
|
||||
esplora = sourceUpstreams.find { it.esploraClient != null }?.esploraClient
|
||||
reader = BitcoinReader(this, this.head, esplora)
|
||||
addressActiveCheck = esplora?.let { AddressActiveCheck(it) }
|
||||
xpubAddresses = addressActiveCheck?.let { XpubAddresses(it) }
|
||||
callRouter = LocalCallRouter(getMethods(), reader)
|
||||
}
|
||||
|
||||
override fun setHead(head: Head) {
|
||||
@@ -123,7 +131,7 @@ open class BitcoinMultistream(
|
||||
}
|
||||
|
||||
override fun getLabels(): Collection<UpstreamsConfig.Labels> {
|
||||
return upstreams.flatMap { it.getLabels() }
|
||||
return sourceUpstreams.flatMap { it.getLabels() }
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
|
||||
@@ -17,6 +17,7 @@ package io.emeraldpay.dshackle.upstream.bitcoin
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import io.emeraldpay.dshackle.Global
|
||||
import io.emeraldpay.dshackle.upstream.Capability
|
||||
import io.emeraldpay.dshackle.upstream.Head
|
||||
import io.emeraldpay.dshackle.upstream.Selector
|
||||
import io.emeraldpay.dshackle.upstream.bitcoin.data.SimpleUnspent
|
||||
@@ -43,6 +44,8 @@ open class BitcoinReader(
|
||||
|
||||
private val unspentReader: UnspentReader = if (esploraClient != null) {
|
||||
EsploraUnspentReader(esploraClient, head)
|
||||
} else if (upstreams.upstreams.any { it.isGrpc() && it.getCapabilities().contains(Capability.BALANCE) }) {
|
||||
RemoteUnspentReader(upstreams)
|
||||
} else {
|
||||
RpcUnspentReader(upstreams)
|
||||
}
|
||||
|
||||
@@ -37,7 +37,6 @@ class EsploraUnspentReader(
|
||||
base.txid,
|
||||
base.vout,
|
||||
base.value,
|
||||
head.getCurrentHeight()?.let { base.height - it } ?: 0
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -15,12 +15,17 @@
|
||||
*/
|
||||
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.upstream.bitcoin.data.RpcUnspent
|
||||
import io.emeraldpay.dshackle.upstream.bitcoin.data.SimpleUnspent
|
||||
import io.emeraldpay.dshackle.upstream.calls.CallMethods
|
||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
|
||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
|
||||
import io.emeraldpay.etherjar.rpc.RpcException
|
||||
import io.emeraldpay.etherjar.rpc.RpcResponseError
|
||||
import org.bitcoinj.core.Address
|
||||
import org.slf4j.LoggerFactory
|
||||
import reactor.core.publisher.Mono
|
||||
|
||||
@@ -33,6 +38,7 @@ import reactor.core.publisher.Mono
|
||||
*/
|
||||
class LocalCallRouter(
|
||||
private val methods: CallMethods,
|
||||
private val reader: BitcoinReader,
|
||||
) : Reader<JsonRpcRequest, JsonRpcResponse> {
|
||||
|
||||
companion object {
|
||||
@@ -47,6 +53,39 @@ class LocalCallRouter(
|
||||
if (!methods.isCallable(key.method)) {
|
||||
return Mono.error(RpcException(RpcResponseError.CODE_METHOD_NOT_EXIST, "Unsupported method"))
|
||||
}
|
||||
if (key.method == "listunspent") {
|
||||
return processUnspentRequest(key)
|
||||
}
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,8 @@
|
||||
package io.emeraldpay.dshackle.upstream.bitcoin
|
||||
|
||||
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.bitcoin.data.RpcUnspent
|
||||
import io.emeraldpay.dshackle.upstream.bitcoin.data.SimpleUnspent
|
||||
@@ -38,14 +40,17 @@ class RpcUnspentReader(
|
||||
base.txid,
|
||||
base.vout,
|
||||
base.amount,
|
||||
base.confirmations
|
||||
)
|
||||
}
|
||||
|
||||
private val selector = Selector.CapabilityMatcher(Capability.BALANCE)
|
||||
|
||||
override fun read(key: Address): Mono<List<SimpleUnspent>> {
|
||||
// docs: https://developer.bitcoin.org/reference/rpc/listunspent.html
|
||||
//
|
||||
val address = key.toString()
|
||||
return upstreams.getDirectApi(Selector.empty).flatMap { api ->
|
||||
api.read(JsonRpcRequest("listunspent", emptyList()))
|
||||
return upstreams.getDirectApi(selector).flatMap { api ->
|
||||
api.read(JsonRpcRequest("listunspent", listOf(1, 9999999, listOf(address))))
|
||||
.flatMap(JsonRpcResponse::requireResult)
|
||||
.map {
|
||||
Global.objectMapper.readerFor(RpcUnspent::class.java).readValues<RpcUnspent>(it).readAll()
|
||||
@@ -55,6 +60,6 @@ class RpcUnspentReader(
|
||||
it.address == address
|
||||
}.map(convert)
|
||||
}
|
||||
}
|
||||
}.switchIfEmpty(Mono.error(SilentException.DataUnavailable("BALANCE")))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,5 +20,4 @@ data class RpcUnspent(
|
||||
val vout: Int,
|
||||
val address: String,
|
||||
val amount: Long,
|
||||
val confirmations: Long
|
||||
)
|
||||
|
||||
@@ -30,7 +30,6 @@ class RpcUnspentDeserializer : JsonDeserializer<RpcUnspent>() {
|
||||
node.get("vout").asInt(),
|
||||
node.get("address").asText(),
|
||||
BigDecimal(node.get("amount").asText()).multiply(BigDecimal.TEN.pow(8)).longValueExact(),
|
||||
node.get("confirmations").asLong()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,5 +19,4 @@ data class SimpleUnspent(
|
||||
val txid: String,
|
||||
val vout: Int,
|
||||
val value: Long,
|
||||
val confirmations: Long
|
||||
)
|
||||
|
||||
@@ -100,6 +100,10 @@ class BitcoinGrpcUpstream(
|
||||
var timeout = Defaults.timeout
|
||||
private var capabilities: Set<Capability> = emptySet()
|
||||
|
||||
override fun getBlockchainApi(): ReactorBlockchainGrpc.ReactorBlockchainStub {
|
||||
return remote
|
||||
}
|
||||
|
||||
override fun getHead(): Head {
|
||||
return grpcHead
|
||||
}
|
||||
|
||||
@@ -100,6 +100,10 @@ open class EthereumGrpcUpstream(
|
||||
private val defaultReader: Reader<JsonRpcRequest, JsonRpcResponse> = client.forSelector(Selector.empty)
|
||||
var timeout = Defaults.timeout
|
||||
|
||||
override fun getBlockchainApi(): ReactorBlockchainGrpc.ReactorBlockchainStub {
|
||||
return remote
|
||||
}
|
||||
|
||||
override fun start() {
|
||||
}
|
||||
|
||||
|
||||
@@ -16,12 +16,16 @@
|
||||
package io.emeraldpay.dshackle.upstream.grpc
|
||||
|
||||
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.
|
||||
* Called on the first creation, and each time a new state received from upstream
|
||||
*/
|
||||
fun update(conf: BlockchainOuterClass.DescribeChain)
|
||||
|
||||
fun getBlockchainApi(): ReactorBlockchainGrpc.ReactorBlockchainStub
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@ class GrpcUpstreams(
|
||||
client.describe(BlockchainOuterClass.DescribeRequest.newBuilder().build())
|
||||
}.onErrorContinue { t, _ ->
|
||||
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 {
|
||||
it.setStatus(UpstreamAvailability.UNAVAILABLE)
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ class TrackBitcoinAddressSpec extends Specification {
|
||||
def "Correct sum for single"() {
|
||||
setup:
|
||||
def unspents = [
|
||||
new SimpleUnspent("f14b222e652c58d11435fa9172ddea000c6f5e20e6b715eb940fc28d1c4adeef", 0, 100L, 123L)
|
||||
new SimpleUnspent("f14b222e652c58d11435fa9172ddea000c6f5e20e6b715eb940fc28d1c4adeef", 0, 100L)
|
||||
]
|
||||
TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(MultistreamHolder))
|
||||
def address = new TrackBitcoinAddress.Address(
|
||||
@@ -68,8 +68,8 @@ class TrackBitcoinAddressSpec extends Specification {
|
||||
def "Correct sum for few"() {
|
||||
setup:
|
||||
def unspents = [
|
||||
new SimpleUnspent("f14b222e652c58d11435fa9172ddea000c6f5e20e6b715eb940fc28d1c4adeef", 0, 100L, 123L),
|
||||
new SimpleUnspent("17d1c4adf14b222e652c58d11435fa9ee2ddea000c6f5e20e6b715eb940fc28f", 0, 123L, 123L),
|
||||
new SimpleUnspent("f14b222e652c58d11435fa9172ddea000c6f5e20e6b715eb940fc28d1c4adeef", 0, 100L),
|
||||
new SimpleUnspent("17d1c4adf14b222e652c58d11435fa9ee2ddea000c6f5e20e6b715eb940fc28f", 0, 123L),
|
||||
]
|
||||
TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(MultistreamHolder))
|
||||
def address = new TrackBitcoinAddress.Address(
|
||||
@@ -120,8 +120,8 @@ class TrackBitcoinAddressSpec extends Specification {
|
||||
def "Correct sum for few with utxo"() {
|
||||
setup:
|
||||
def unspents = [
|
||||
new SimpleUnspent("f14b222e652c58d11435fa9172ddea000c6f5e20e6b715eb940fc28d1c4adeef", 0, 100L, 123L),
|
||||
new SimpleUnspent("17d1c4adf14b222e652c58d11435fa9ee2ddea000c6f5e20e6b715eb940fc28f", 0, 123L, 123L),
|
||||
new SimpleUnspent("f14b222e652c58d11435fa9172ddea000c6f5e20e6b715eb940fc28d1c4adeef", 0, 100L),
|
||||
new SimpleUnspent("17d1c4adf14b222e652c58d11435fa9ee2ddea000c6f5e20e6b715eb940fc28f", 0, 123L),
|
||||
]
|
||||
TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(MultistreamHolder))
|
||||
def address = new TrackBitcoinAddress.Address(
|
||||
@@ -282,7 +282,7 @@ class TrackBitcoinAddressSpec extends Specification {
|
||||
2 * listUnspent(_) >>> [
|
||||
Mono.just([]),
|
||||
Mono.just([
|
||||
new SimpleUnspent("f14b222e652c58d11435fa9172ddea000c6f5e20e6b715eb940fc28d1c4adeef", 0, 1230000L, 123L)
|
||||
new SimpleUnspent("f14b222e652c58d11435fa9172ddea000c6f5e20e6b715eb940fc28d1c4adeef", 0, 1230000L)
|
||||
])
|
||||
]
|
||||
}
|
||||
|
||||
@@ -34,7 +34,8 @@ class BitcoinReaderSpec extends Specification {
|
||||
api.answerOnce("getblockhash", [100000], "000000000003ba27aa200b1cecaad478d2b00432346c3f1f3986da1afd33e506")
|
||||
api.answerOnce("getblock", ["000000000003ba27aa200b1cecaad478d2b00432346c3f1f3986da1afd33e506"], block)
|
||||
def ups = Mock(BitcoinMultistream) {
|
||||
_ * it.getDirectApi(_) >> Mono.just(api)
|
||||
_ * getDirectApi(_) >> Mono.just(api)
|
||||
_ * upstreams >> []
|
||||
}
|
||||
def reader = new BitcoinReader(ups, Stub(Head), null)
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ class RpcUnspentReaderSpec extends Specification {
|
||||
setup:
|
||||
def json = this.class.getClassLoader().getResourceAsStream("bitcoin/unspent-one-addr.json").bytes
|
||||
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) {
|
||||
1 * getDirectApi(_) >> Mono.just(rpcReader)
|
||||
@@ -45,19 +45,16 @@ class RpcUnspentReaderSpec extends Specification {
|
||||
with(act[0]) {
|
||||
txid == "e0f946c8f971b25cdffa64eed71d886019e437c0bf6a1b280584c0be5d1b5409"
|
||||
vout == 29
|
||||
confirmations == 2010
|
||||
value == 1230030
|
||||
}
|
||||
with(act[1]) {
|
||||
txid == "66e1e4d14ed6f454d2fda036f35cba423274ecdf5d46deb93f172c412a0f650d"
|
||||
vout == 83
|
||||
confirmations == 4963
|
||||
value == 756339
|
||||
}
|
||||
with(act[35]) {
|
||||
txid == "f14b222e652c58d11435fa9172ddea000c6f5e20e6b715eb940fc28d1c4adeef"
|
||||
vout == 58
|
||||
confirmations == 2139
|
||||
value == 1105047
|
||||
}
|
||||
}
|
||||
@@ -66,7 +63,7 @@ class RpcUnspentReaderSpec extends Specification {
|
||||
setup:
|
||||
def json = this.class.getClassLoader().getResourceAsStream("bitcoin/unspent-two-addr.json").bytes
|
||||
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) {
|
||||
1 * getDirectApi(_) >> Mono.just(rpcReader)
|
||||
@@ -83,19 +80,16 @@ class RpcUnspentReaderSpec extends Specification {
|
||||
txid == "8ad0d954a01eeb4f2c62d58d291699af847f9c8df43b775c27ffe8a5f76eba00"
|
||||
vout == 1
|
||||
value == 216465
|
||||
confirmations == 2583
|
||||
}
|
||||
with(act[11]) {
|
||||
txid == "777671a46b30b068052a73387e036bc8515cd3ba6adf9be4c70dfc0699f67c09"
|
||||
vout == 0
|
||||
confirmations == 13705
|
||||
value == 307906
|
||||
}
|
||||
// cat src/test/resources/bitcoin/unspent-two-addr.json | jq '[.[] | select(.address == "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP")] | .[211]'
|
||||
with(act[211]) {
|
||||
txid == "f20727393b0a586a3062a615fb71f43ec21c24258c3c6ec546fee5cbc1fa2ba7"
|
||||
vout == 0
|
||||
confirmations == 21890
|
||||
value == 50000000000
|
||||
}
|
||||
}
|
||||
@@ -104,7 +98,7 @@ class RpcUnspentReaderSpec extends Specification {
|
||||
setup:
|
||||
def json = this.class.getClassLoader().getResourceAsStream("bitcoin/unspent-two-addr.json").bytes
|
||||
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) {
|
||||
1 * getDirectApi(_) >> Mono.just(rpcReader)
|
||||
@@ -122,13 +116,11 @@ class RpcUnspentReaderSpec extends Specification {
|
||||
txid == "e0f946c8f971b25cdffa64eed71d886019e437c0bf6a1b280584c0be5d1b5409"
|
||||
vout == 29
|
||||
value == 1230030
|
||||
confirmations == 2030
|
||||
}
|
||||
// cat src/test/resources/bitcoin/unspent-two-addr.json | jq '[.[] | select(.address == "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK")] | .[35]'
|
||||
with(act[35]) {
|
||||
txid == "f14b222e652c58d11435fa9172ddea000c6f5e20e6b715eb940fc28d1c4adeef"
|
||||
vout == 58
|
||||
confirmations == 2159
|
||||
value == 1105047
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user