From ba996a8d2bbd5bb56e9ab4ce1ad3120d7a5af2db Mon Sep 17 00:00:00 2001 From: Igor Artamonov Date: Sat, 18 Apr 2020 22:26:30 -0400 Subject: [PATCH 01/13] solution: initial implementation for Bitcoin upstreams --- build.gradle | 3 + .../dshackle/config/UpstreamsConfig.kt | 5 + .../dshackle/config/UpstreamsConfigReader.kt | 26 +- .../io/emeraldpay/dshackle/data/BlockId.kt | 12 +- .../io/emeraldpay/dshackle/data/HashId.kt | 6 +- .../io/emeraldpay/dshackle/data/TxId.kt | 10 +- .../io/emeraldpay/dshackle/rpc/StreamHead.kt | 2 +- .../dshackle/startup/ConfiguredUpstreams.kt | 51 +- .../dshackle/upstream/AbstractHead.kt | 52 + .../dshackle/upstream/AggregatedUpstream.kt | 3 +- .../dshackle/upstream/CachingEthereumApi.kt | 6 +- .../dshackle/upstream/ChainUpstreams.kt | 22 +- .../dshackle/upstream/CurrentUpstreams.kt | 75 +- .../dshackle/upstream/DefaultUpstream.kt | 25 +- .../EmptyEthereumHead.kt => EmptyHead.kt} | 5 +- .../EthereumHeadMerge.kt => MergedHead.kt} | 8 +- .../dshackle/upstream/bitcoin/BitcoinApi.kt | 44 + .../upstream/bitcoin/BitcoinChainUpstreams.kt | 79 + .../upstream/bitcoin/BitcoinRpcClient.kt | 54 + .../upstream/bitcoin/BitcoinRpcHead.kt | 63 + .../upstream/bitcoin/BitcoinUpstream.kt | 91 + .../bitcoin/BitcoinUpstreamValidator.kt | 47 + .../upstream/bitcoin/DefaultBitcoinMethods.kt | 10 + .../dshackle/upstream/bitcoin/ExtractBlock.kt | 40 + .../upstream/calls/DirectCallMethods.kt | 6 +- .../upstream/ethereum/DefaultEthereumHead.kt | 47 +- .../ethereum/EthereumChainUpstreams.kt | 34 +- .../upstream/ethereum/EthereumHead.kt | 24 - .../ethereum/EthereumHeadLagObserver.kt | 3 +- .../upstream/ethereum/EthereumUpstream.kt | 40 +- .../EthereumUpstreamValidator.kt} | 11 +- .../upstream/grpc/EthereumGrpcUpstream.kt | 21 +- .../dshackle/cache/BlocksMemCacheSpec.groovy | 6 +- .../dshackle/cache/HeightCacheSpec.groovy | 14 +- .../dshackle/cache/TxMemCacheSpec.groovy | 14 +- .../dshackle/data/BlockIdSpec.groovy | 30 + .../emeraldpay/dshackle/data/TxIdSpec.groovy | 29 + .../dshackle/test/EthereumHeadMock.groovy | 4 +- .../dshackle/test/EthereumUpstreamMock.groovy | 6 +- ...ockServer.groovy => MockGrpcServer.groovy} | 6 +- .../upstream/CachingEthereumApiSpec.groovy | 17 +- .../upstream/bitcoin/BitcoinApiSpec.groovy | 108 ++ .../bitcoin/BitcoinRpcClientSpec.groovy | 86 + .../bitcoin/BitcoinRpcHeadSpec.groovy | 95 ++ .../upstream/bitcoin/ExtractBlockSpec.groovy | 24 + .../EthereumHeadLagObserverSpec.groovy | 11 +- .../grpc/EthereumGrpcUpstreamSpec.groovy | 4 +- src/test/resources/bitcoin/block-626472.json | 1509 +++++++++++++++++ 48 files changed, 2640 insertions(+), 248 deletions(-) create mode 100644 src/main/kotlin/io/emeraldpay/dshackle/upstream/AbstractHead.kt rename src/main/kotlin/io/emeraldpay/dshackle/upstream/{ethereum/EmptyEthereumHead.kt => EmptyHead.kt} (87%) rename src/main/kotlin/io/emeraldpay/dshackle/upstream/{ethereum/EthereumHeadMerge.kt => MergedHead.kt} (87%) create mode 100644 src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinApi.kt create mode 100644 src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinChainUpstreams.kt create mode 100644 src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinRpcClient.kt create mode 100644 src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinRpcHead.kt create mode 100644 src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinUpstream.kt create mode 100644 src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinUpstreamValidator.kt create mode 100644 src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/DefaultBitcoinMethods.kt create mode 100644 src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/ExtractBlock.kt delete mode 100644 src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumHead.kt rename src/main/kotlin/io/emeraldpay/dshackle/upstream/{UpstreamValidator.kt => ethereum/EthereumUpstreamValidator.kt} (89%) create mode 100644 src/test/groovy/io/emeraldpay/dshackle/data/BlockIdSpec.groovy create mode 100644 src/test/groovy/io/emeraldpay/dshackle/data/TxIdSpec.groovy rename src/test/groovy/io/emeraldpay/dshackle/test/{MockServer.groovy => MockGrpcServer.groovy} (90%) create mode 100644 src/test/groovy/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinApiSpec.groovy create mode 100644 src/test/groovy/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinRpcClientSpec.groovy create mode 100644 src/test/groovy/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinRpcHeadSpec.groovy create mode 100644 src/test/groovy/io/emeraldpay/dshackle/upstream/bitcoin/ExtractBlockSpec.groovy create mode 100644 src/test/resources/bitcoin/block-626472.json diff --git a/build.gradle b/build.gradle index 19582c0d..a216236a 100644 --- a/build.gradle +++ b/build.gradle @@ -84,6 +84,7 @@ dependencies { compile "io.infinitape:etherjar-rpc-ws:$etherjarVersion" compile "io.infinitape:etherjar-rpc-emerald:$etherjarVersion" compile "io.infinitape:etherjar-tx:$etherjarVersion" + compile 'org.bitcoinj:bitcoinj-core:0.15.8' compile 'org.apache.httpcomponents:httpmime:4.5.8' compile 'org.apache.httpcomponents:httpclient:4.5.8' @@ -108,9 +109,11 @@ dependencies { testCompile "org.codehaus.groovy:groovy:$groovyVersion" testCompile 'cglib:cglib-nodep:3.2.12' testCompile "org.spockframework:spock-core:$spockVersion" + testCompile 'de.jodamob.kotlin:kotlin-runner-spock:0.3.1' testCompile "io.grpc:grpc-testing:${grpcVersion}" testCompile "io.projectreactor:reactor-test:$reactorVersion" testCompile 'org.objenesis:objenesis:3.0.1' + testCompile 'org.mock-server:mockserver-netty:5.10' } compileKotlin { diff --git a/src/main/kotlin/io/emeraldpay/dshackle/config/UpstreamsConfig.kt b/src/main/kotlin/io/emeraldpay/dshackle/config/UpstreamsConfig.kt index 40db6ea4..f9957d8b 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/config/UpstreamsConfig.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/config/UpstreamsConfig.kt @@ -96,6 +96,10 @@ class UpstreamsConfig { var ws: WsEndpoint? = null } + class BitcoinConnection : UpstreamConnection() { + var rpc: HttpEndpoint? = null + } + class HttpEndpoint(val url: URI) { var basicAuth: AuthConfig.ClientBasicAuth? = null var tls: AuthConfig.ClientTlsAuth? = null @@ -124,6 +128,7 @@ class UpstreamsConfig { enum class UpstreamType private constructor(vararg code: String) { ETHEREUM_JSON_RPC("ethereum"), + BITCOIN_JSON_RPC("bitcoin"), DSHACKLE("dshackle", "grpc"), UNKNOWN("unknown"); diff --git a/src/main/kotlin/io/emeraldpay/dshackle/config/UpstreamsConfigReader.kt b/src/main/kotlin/io/emeraldpay/dshackle/config/UpstreamsConfigReader.kt index a8d8a2a7..08f9df5b 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/config/UpstreamsConfigReader.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/config/UpstreamsConfigReader.kt @@ -87,7 +87,7 @@ class UpstreamsConfigReader( val connConfigNode = getMapping(connNode, "ethereum")!! val upstream = UpstreamsConfig.Upstream() readUpstreamCommon(upNode, upstream) - readUpstreamEthereum(upNode, upstream) + readUpstreamStandard(upNode, upstream) if (isValid(upstream)) { config.upstreams.add(upstream) val connection = UpstreamsConfig.EthereumConnection() @@ -113,6 +113,26 @@ class UpstreamsConfigReader( } else { log.error("Upstream at #0 has invalid configuration") } + } else if (hasAny(connNode, "bitcoin")) { + val connConfigNode = getMapping(connNode, "bitcoin")!! + val upstream = UpstreamsConfig.Upstream() + readUpstreamCommon(upNode, upstream) + readUpstreamStandard(upNode, upstream) + if (isValid(upstream)) { + config.upstreams.add(upstream) + val connection = UpstreamsConfig.BitcoinConnection() + upstream.connection = connection + getMapping(connConfigNode, "rpc")?.let { node -> + getValueAsString(node, "url")?.let { url -> + val http = UpstreamsConfig.HttpEndpoint(URI(url)) + connection.rpc = http + http.basicAuth = authConfigReader.readClientBasicAuth(node) + http.tls = authConfigReader.readClientTls(node) + } + } + } else { + log.error("Upstream at #0 has invalid configuration") + } } else if (hasAny(connNode, "grpc")) { val connConfigNode = getMapping(connNode, "grpc")!! val upstream = UpstreamsConfig.Upstream() @@ -162,13 +182,13 @@ class UpstreamsConfigReader( } } - internal fun readUpstreamEthereum(upNode: MappingNode, upstream: UpstreamsConfig.Upstream) { + internal fun readUpstreamStandard(upNode: MappingNode, upstream: UpstreamsConfig.Upstream<*>) { upstream.chain = getValueAsString(upNode, "chain") if (hasAny(upNode, "labels")) { getMapping(upNode, "labels")?.let { labels -> labels.value.stream() .filter { n -> n.keyNode is ScalarNode && n.valueNode is ScalarNode } - .map { n -> Tuples.of((n.keyNode as ScalarNode).value, (n.valueNode as ScalarNode).value)} + .map { n -> Tuples.of((n.keyNode as ScalarNode).value, (n.valueNode as ScalarNode).value) } .map { kv -> Tuples.of(kv.t1.trim(), kv.t2.trim()) } .filter { kv -> StringUtils.isNotEmpty(kv.t1) && StringUtils.isNotEmpty(kv.t2) } .forEach { kv -> diff --git a/src/main/kotlin/io/emeraldpay/dshackle/data/BlockId.kt b/src/main/kotlin/io/emeraldpay/dshackle/data/BlockId.kt index 671730b7..3a5ae533 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/data/BlockId.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/data/BlockId.kt @@ -15,7 +15,9 @@ */ package io.emeraldpay.dshackle.data +import io.infinitape.etherjar.domain.BlockHash import io.infinitape.etherjar.rpc.json.BlockJson +import org.bouncycastle.util.encoders.Hex class BlockId( value: ByteArray @@ -23,7 +25,7 @@ class BlockId( companion object { @JvmStatic - fun from(hash: io.infinitape.etherjar.domain.BlockHash): BlockId { + fun from(hash: BlockHash): BlockId { return BlockId(hash.bytes) } @@ -34,7 +36,13 @@ class BlockId( @JvmStatic fun from(id: String): BlockId { - return from(io.infinitape.etherjar.domain.BlockHash.from(id)) + val clean = if (id.startsWith("0x")) { + id.substring(2) + } else { + id + } + val bytes = Hex.decode(clean) + return BlockId(bytes) } } diff --git a/src/main/kotlin/io/emeraldpay/dshackle/data/HashId.kt b/src/main/kotlin/io/emeraldpay/dshackle/data/HashId.kt index 206b45dc..ab4d2eb9 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/data/HashId.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/data/HashId.kt @@ -28,11 +28,9 @@ open class HashId( } fun toHex(): String { - val hex = CharArray(value.size * 2 + 2) - hex[0] = '0' - hex[1] = 'x' + val hex = CharArray(value.size * 2) var i = 0 - var j = 2 + var j = 0 while (i < value.size) { hex[j++] = HEX_DIGITS[0xF0 and value[i].toInt() ushr 4] hex[j++] = HEX_DIGITS[0x0F and value[i].toInt()] diff --git a/src/main/kotlin/io/emeraldpay/dshackle/data/TxId.kt b/src/main/kotlin/io/emeraldpay/dshackle/data/TxId.kt index 1dd1b976..e4f3d061 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/data/TxId.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/data/TxId.kt @@ -17,6 +17,8 @@ package io.emeraldpay.dshackle.data import io.infinitape.etherjar.domain.TransactionId import io.infinitape.etherjar.rpc.json.TransactionJson +import org.bouncycastle.util.encoders.Hex +import java.math.BigInteger class TxId( value: ByteArray @@ -35,7 +37,13 @@ class TxId( @JvmStatic fun from(id: String): TxId { - return from(TransactionId.from(id)) + val clean = if (id.startsWith("0x")) { + id.substring(2) + } else { + id + } + val bytes = Hex.decode(clean) + return TxId(bytes) } } } \ No newline at end of file diff --git a/src/main/kotlin/io/emeraldpay/dshackle/rpc/StreamHead.kt b/src/main/kotlin/io/emeraldpay/dshackle/rpc/StreamHead.kt index a10c07ea..cf533b86 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/rpc/StreamHead.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/rpc/StreamHead.kt @@ -65,7 +65,7 @@ class StreamHead( .setHeight(block.height) .setTimestamp(block.timestamp!!.toEpochMilli()) .setWeight(ByteString.copyFrom(block.difficulty.toByteArray())) - .setBlockId(block.hash.toHex().substring(2)) + .setBlockId(block.hash.toHex()) .build() } diff --git a/src/main/kotlin/io/emeraldpay/dshackle/startup/ConfiguredUpstreams.kt b/src/main/kotlin/io/emeraldpay/dshackle/startup/ConfiguredUpstreams.kt index de3ade53..5d1ac9fb 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/startup/ConfiguredUpstreams.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/startup/ConfiguredUpstreams.kt @@ -20,6 +20,9 @@ import io.emeraldpay.dshackle.BlockchainType import io.emeraldpay.dshackle.FileResolver import io.emeraldpay.dshackle.config.UpstreamsConfig import io.emeraldpay.dshackle.upstream.CurrentUpstreams +import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinApi +import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinRpcClient +import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinUpstream import io.emeraldpay.dshackle.upstream.calls.ManagedCallMethods import io.emeraldpay.dshackle.upstream.ethereum.DirectEthereumApi import io.emeraldpay.dshackle.upstream.ethereum.EthereumUpstream @@ -32,6 +35,7 @@ import org.springframework.beans.factory.annotation.Autowired import org.springframework.stereotype.Repository import java.net.URI import java.util.* +import java.util.concurrent.atomic.AtomicInteger import javax.annotation.PostConstruct import kotlin.collections.HashMap @@ -44,6 +48,7 @@ open class ConfiguredUpstreams( ) { private val log = LoggerFactory.getLogger(ConfiguredUpstreams::class.java) + private var seq = AtomicInteger(0) private val chainNames = mapOf( "ethereum" to Chain.ETHEREUM, @@ -51,7 +56,10 @@ open class ConfiguredUpstreams( "eth" to Chain.ETHEREUM, "etc" to Chain.ETHEREUM_CLASSIC, "morden" to Chain.TESTNET_MORDEN, - "kovan" to Chain.TESTNET_KOVAN + "kovan" to Chain.TESTNET_KOVAN, + "kovan-testnet" to Chain.TESTNET_KOVAN, + "bitcoin" to Chain.BITCOIN, + "bitcoin-testnet" to Chain.TESTNET_BITCOIN ) @PostConstruct @@ -69,13 +77,20 @@ open class ConfiguredUpstreams( log.error("Chain is unknown: ${up.chain}") return@forEach } - if (BlockchainType.fromBlockchain(chain) != BlockchainType.ETHEREUM) { - log.error("Chain is unsupported: ${up.chain}") - return@forEach - } val options = (up.options ?: UpstreamsConfig.Options()) .merge(defaultOptions[chain] ?: UpstreamsConfig.Options.getDefaults()) - buildEthereumUpstream(up.cast(UpstreamsConfig.EthereumConnection::class.java), chain, options) + when (BlockchainType.fromBlockchain(chain)) { + BlockchainType.ETHEREUM -> { + buildEthereumUpstream(up.cast(UpstreamsConfig.EthereumConnection::class.java), chain, options) + } + BlockchainType.BITCOIN -> { + buildBitcoinUpstream(up.cast(UpstreamsConfig.BitcoinConnection::class.java), chain, options) + } + else -> { + log.error("Chain is unsupported: ${up.chain}") + return@forEach + } + } } } } @@ -101,10 +116,30 @@ open class ConfiguredUpstreams( return defaultOptions } + private fun buildBitcoinUpstream(config: UpstreamsConfig.Upstream, + chain: Chain, + options: UpstreamsConfig.Options) { + + val conn = config.connection!! + var rpcApi: BitcoinApi? = null + + conn.rpc?.let { endpoint -> + val rpcClient = BitcoinRpcClient(endpoint.url.toString(), endpoint.basicAuth!!) + rpcApi = BitcoinApi(rpcClient, objectMapper) + } + rpcApi?.let { api -> + val upstream = BitcoinUpstream(config.id + ?: "bitcoin-${seq.getAndIncrement()}", chain, api, options, objectMapper) + + upstream.start() + currentUpstreams.update(UpstreamChange(chain, upstream, UpstreamChange.ChangeType.ADDED)) + } + + } + private fun buildEthereumUpstream(config: UpstreamsConfig.Upstream, chain: Chain, - options: UpstreamsConfig.Options - ) { + options: UpstreamsConfig.Options) { val conn = config.connection!! var rpcApi: DirectEthereumApi? = null val urls = ArrayList() diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/AbstractHead.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/AbstractHead.kt new file mode 100644 index 00000000..b2eb7ec0 --- /dev/null +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/AbstractHead.kt @@ -0,0 +1,52 @@ +package io.emeraldpay.dshackle.upstream + +import io.emeraldpay.dshackle.data.BlockContainer +import org.slf4j.LoggerFactory +import reactor.core.Disposable +import reactor.core.publisher.Flux +import reactor.core.publisher.Mono +import reactor.core.publisher.TopicProcessor +import java.util.concurrent.atomic.AtomicReference + +abstract class AbstractHead : Head { + + companion object { + private val log = LoggerFactory.getLogger(AbstractHead::class.java) + } + + private val head = AtomicReference(null) + private val stream: TopicProcessor = TopicProcessor.create() + + fun follow(source: Flux): Disposable { + return source.distinctUntilChanged { + it.hash + }.filter { block -> + val curr = head.get() + curr == null || curr.difficulty < block.difficulty + } + .subscribe { block -> + val prev = head.getAndUpdate { curr -> + if (curr == null || curr.difficulty < block.difficulty) { + block + } else { + curr + } + } + if (prev == null || prev.hash != block.hash) { + log.debug("New block ${block.height} ${block.hash}") + stream.onNext(block) + } + } + } + + override fun getFlux(): Flux { + return Flux.merge( + Mono.justOrEmpty(head.get()), + Flux.from(stream) + ).onBackpressureLatest() + } + + fun getCurrent(): BlockContainer? { + return head.get() + } +} \ No newline at end of file diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/AggregatedUpstream.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/AggregatedUpstream.kt index 30d803ac..a461b95d 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/AggregatedUpstream.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/AggregatedUpstream.kt @@ -20,7 +20,6 @@ import io.emeraldpay.dshackle.cache.* import io.emeraldpay.dshackle.config.UpstreamsConfig import io.emeraldpay.dshackle.upstream.calls.AggregatedCallMethods import io.emeraldpay.dshackle.upstream.calls.CallMethods -import io.emeraldpay.dshackle.upstream.ethereum.EthereumHead import org.springframework.context.Lifecycle import reactor.core.Disposable import reactor.core.publisher.Flux @@ -89,7 +88,7 @@ abstract class AggregatedUpstream( cacheSubscription = null } - fun onHeadUpdated(head: EthereumHead) { + fun onHeadUpdated(head: Head) { reconfigLock.withLock { cacheSubscription?.dispose() cacheSubscription = head.getFlux().subscribe { diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/CachingEthereumApi.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/CachingEthereumApi.kt index d60daf40..659088de 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/CachingEthereumApi.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/CachingEthereumApi.kt @@ -18,9 +18,7 @@ package io.emeraldpay.dshackle.upstream import com.fasterxml.jackson.databind.ObjectMapper import io.emeraldpay.dshackle.cache.Caches import io.emeraldpay.dshackle.data.* -import io.emeraldpay.dshackle.upstream.ethereum.EmptyEthereumHead import io.emeraldpay.dshackle.upstream.ethereum.EthereumApi -import io.emeraldpay.dshackle.upstream.ethereum.EthereumHead import io.infinitape.etherjar.hex.HexQuantity import org.slf4j.LoggerFactory import reactor.core.publisher.Mono @@ -30,7 +28,7 @@ import java.util.function.Function open class CachingEthereumApi( private val objectMapper: ObjectMapper, private val caches: Caches, - private val head: EthereumHead + private val head: Head ): EthereumApi(objectMapper) { companion object { @@ -41,7 +39,7 @@ open class CachingEthereumApi( */ @JvmStatic fun empty(objectMapper: ObjectMapper): CachingEthereumApi { - return CachingEthereumApi(objectMapper, Caches.default(objectMapper), EmptyEthereumHead()) + return CachingEthereumApi(objectMapper, Caches.default(objectMapper), EmptyHead()) } } diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ChainUpstreams.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ChainUpstreams.kt index aedfdfc2..b2c5ee1e 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ChainUpstreams.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ChainUpstreams.kt @@ -17,11 +17,14 @@ package io.emeraldpay.dshackle.upstream import com.fasterxml.jackson.databind.ObjectMapper import io.emeraldpay.dshackle.cache.Caches +import io.emeraldpay.dshackle.upstream.ethereum.EthereumChainUpstreams import io.emeraldpay.grpc.Chain import org.slf4j.LoggerFactory import org.springframework.context.Lifecycle import reactor.core.Disposable import reactor.core.publisher.Mono +import java.lang.IllegalStateException +import java.time.Duration /** * General interface to upstream(s) to a single chain @@ -111,5 +114,22 @@ abstract class ChainUpstreams( return 0 } - abstract fun printStatus() + fun printStatus() { + var height: Long? = null + try { + height = getHead().getFlux().next().block(Duration.ofSeconds(1))?.height + } catch (e: IllegalStateException) { + //timout + } catch (e: Exception) { + log.warn("Head processing error: ${e.javaClass} ${e.message}") + } + val statuses = upstreams.map { it.getStatus() } + .groupBy { it } + .map { "${it.key.name}/${it.value.size}" } + .joinToString(",") + val lag = upstreams.map { it.getLag() } + .joinToString(", ") + + log.info("State of ${chain.chainCode}: height=${height ?: '?'}, status=$statuses, lag=[$lag]") + } } \ No newline at end of file diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/CurrentUpstreams.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/CurrentUpstreams.kt index 0ad5cc24..666a1e73 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/CurrentUpstreams.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/CurrentUpstreams.kt @@ -16,17 +16,19 @@ package io.emeraldpay.dshackle.upstream import com.fasterxml.jackson.databind.ObjectMapper +import io.emeraldpay.dshackle.BlockchainType import io.emeraldpay.dshackle.cache.CachesEnabled import io.emeraldpay.dshackle.cache.CachesFactory import io.emeraldpay.dshackle.startup.UpstreamChange +import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinApi +import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinChainUpstreams +import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinUpstream import io.emeraldpay.dshackle.upstream.calls.CallMethods import io.emeraldpay.dshackle.upstream.calls.DefaultEthereumMethods import io.emeraldpay.dshackle.upstream.ethereum.EthereumApi import io.emeraldpay.dshackle.upstream.ethereum.EthereumChainUpstreams import io.emeraldpay.dshackle.upstream.ethereum.EthereumUpstream import io.emeraldpay.grpc.Chain -import io.infinitape.etherjar.rpc.json.BlockJson -import io.infinitape.etherjar.rpc.json.TransactionRefJson import org.slf4j.LoggerFactory import org.springframework.beans.factory.annotation.Autowired import org.springframework.scheduling.annotation.Scheduled @@ -34,6 +36,7 @@ import org.springframework.stereotype.Repository import reactor.core.publisher.Flux import reactor.core.publisher.TopicProcessor import java.util.* +import java.util.concurrent.Callable import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.locks.ReentrantLock import kotlin.concurrent.withLock @@ -54,36 +57,60 @@ class CurrentUpstreams( fun update(change: UpstreamChange) { updateLock.withLock { val chain = change.chain - val up = change.upstream - .cast(EthereumUpstream::class.java, EthereumApi::class.java) as Upstream - val current = chainMapping[chain] as ChainUpstreams? - if (change.type == UpstreamChange.ChangeType.REMOVED) { - current?.removeUpstream(up.getId()) - log.info("Upstream ${change.upstream.getId()} with chain $chain has been removed") - } else { - if (current == null) { - val created = EthereumChainUpstreams(chain, ArrayList(), cachesFactory.getCaches(chain), objectMapper) - if (up is CachesEnabled) { - up.setCaches(created.caches) + when (BlockchainType.fromBlockchain(chain)) { + BlockchainType.ETHEREUM -> { + val up = change.upstream + .cast(EthereumUpstream::class.java, EthereumApi::class.java) as Upstream + val current = chainMapping[chain] as ChainUpstreams? + val factory = Callable { + EthereumChainUpstreams(chain, ArrayList(), cachesFactory.getCaches(chain), objectMapper) as ChainUpstreams } - created.addUpstream(up) - created.start() - chainMapping[chain] = created - chainsBus.onNext(chain) - } else { - if (up is CachesEnabled) { - up.setCaches(current.caches) + processUpdate(change, up, current, factory) + } + BlockchainType.BITCOIN -> { + val up = change.upstream + .cast(BitcoinUpstream::class.java, BitcoinApi::class.java) + val current = chainMapping[chain] as ChainUpstreams? + val factory = Callable { + BitcoinChainUpstreams(chain, ArrayList(), cachesFactory.getCaches(chain), objectMapper) as ChainUpstreams } - current.addUpstream(up) + processUpdate(change, up, current, factory) } - if (!callTargets.containsKey(chain)) { - setupDefaultMethods(chain) + else -> { + log.error("Update for unsupported chain: $chain") } - log.info("Upstream ${change.upstream.getId()} with chain $chain has been added") } } } + fun processUpdate(change: UpstreamChange, up: Upstream, current: ChainUpstreams?, factory: Callable>) { + val chain = change.chain + if (change.type == UpstreamChange.ChangeType.REMOVED) { + current?.removeUpstream(up.getId()) + log.info("Upstream ${change.upstream.getId()} with chain $chain has been removed") + } else { + if (current == null) { + val created = factory.call() + if (up is CachesEnabled) { + up.setCaches(created.caches) + } + created.addUpstream(up) + created.start() + chainMapping[chain] = created + chainsBus.onNext(chain) + } else { + if (up is CachesEnabled) { + up.setCaches(current.caches) + } + current.addUpstream(up) + } + if (!callTargets.containsKey(chain)) { + setupDefaultMethods(chain) + } + log.info("Upstream ${change.upstream.getId()} with chain $chain has been added") + } + } + override fun getUpstream(chain: Chain): AggregatedUpstream<*>? { return chainMapping[chain] } diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/DefaultUpstream.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/DefaultUpstream.kt index 95a196cc..a8be0591 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/DefaultUpstream.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/DefaultUpstream.kt @@ -15,20 +15,29 @@ */ package io.emeraldpay.dshackle.upstream +import io.emeraldpay.dshackle.config.UpstreamsConfig +import io.emeraldpay.dshackle.upstream.calls.CallMethods import reactor.core.publisher.Flux import reactor.core.publisher.TopicProcessor import java.util.concurrent.atomic.AtomicReference abstract class DefaultUpstream( + private val id: String, defaultLag: Long, - defaultAvail: UpstreamAvailability + defaultAvail: UpstreamAvailability, + private val options: UpstreamsConfig.Options, + private val targets: CallMethods? ) : Upstream { - constructor() : this(Long.MAX_VALUE, UpstreamAvailability.UNAVAILABLE) + constructor(id: String, options: UpstreamsConfig.Options, targets: CallMethods?) : this(id, Long.MAX_VALUE, UpstreamAvailability.UNAVAILABLE, options, targets) private val status = AtomicReference(Status(defaultLag, defaultAvail, statusByLag(defaultLag, defaultAvail))) private val statusStream: TopicProcessor = TopicProcessor.create() + override fun isAvailable(): Boolean { + return getStatus() == UpstreamAvailability.OK + } + override fun getStatus(): UpstreamAvailability { return status.get().status } @@ -67,5 +76,17 @@ abstract class DefaultUpstream( return this.status.get().lag } + override fun getId(): String { + return id + } + + override fun getOptions(): UpstreamsConfig.Options { + return options + } + + override fun getMethods(): CallMethods { + return targets ?: throw IllegalStateException("Methods are not set") + } + class Status(val lag: Long, val avail: UpstreamAvailability, val status: UpstreamAvailability) } \ No newline at end of file diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EmptyEthereumHead.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/EmptyHead.kt similarity index 87% rename from src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EmptyEthereumHead.kt rename to src/main/kotlin/io/emeraldpay/dshackle/upstream/EmptyHead.kt index d715abd4..65f18dec 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EmptyEthereumHead.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/EmptyHead.kt @@ -13,12 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.emeraldpay.dshackle.upstream.ethereum +package io.emeraldpay.dshackle.upstream import io.emeraldpay.dshackle.data.BlockContainer +import io.emeraldpay.dshackle.upstream.Head import reactor.core.publisher.Flux -class EmptyEthereumHead : EthereumHead { +class EmptyHead : Head { override fun getFlux(): Flux { return Flux.empty() diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumHeadMerge.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/MergedHead.kt similarity index 87% rename from src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumHeadMerge.kt rename to src/main/kotlin/io/emeraldpay/dshackle/upstream/MergedHead.kt index d2484732..6cef3cdf 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumHeadMerge.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/MergedHead.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.emeraldpay.dshackle.upstream.ethereum +package io.emeraldpay.dshackle.upstream import io.emeraldpay.dshackle.cache.Caches import io.emeraldpay.dshackle.cache.CachesEnabled @@ -21,9 +21,9 @@ import org.springframework.context.Lifecycle import reactor.core.Disposable import reactor.core.publisher.Flux -class EthereumHeadMerge( - private val sources: Iterable -): DefaultEthereumHead(), Lifecycle, CachesEnabled { +class MergedHead( + private val sources: Iterable +) : AbstractHead(), Lifecycle, CachesEnabled { private var subscription: Disposable? = null diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinApi.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinApi.kt new file mode 100644 index 00000000..beb8dcab --- /dev/null +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinApi.kt @@ -0,0 +1,44 @@ +package io.emeraldpay.dshackle.upstream.bitcoin + +import com.fasterxml.jackson.databind.JavaType +import com.fasterxml.jackson.databind.ObjectMapper +import io.emeraldpay.dshackle.upstream.UpstreamApi +import io.infinitape.etherjar.rpc.RpcException +import io.infinitape.etherjar.rpc.json.FullResponseJson +import io.infinitape.etherjar.rpc.json.RequestJson +import io.infinitape.etherjar.rpc.json.ResponseJson +import org.slf4j.LoggerFactory +import reactor.core.publisher.Mono + +open class BitcoinApi( + val bitcoinRpcClient: BitcoinRpcClient, + val objectMapper: ObjectMapper +) : UpstreamApi { + + companion object { + private val log = LoggerFactory.getLogger(BitcoinApi::class.java) + } + + open override fun execute(id: Int, method: String, params: List): Mono { + //TODO optimize extraction + return executeAndResult(id, method, params, Object::class.java).map { + objectMapper.writeValueAsBytes(it) + } + } + + open fun executeAndResult(id: Int, method: String, params: List, resultType: Class): Mono { + val rpc = RequestJson(method, params, id) + return Mono.just(rpc) + .map(objectMapper::writeValueAsBytes) + .flatMap(bitcoinRpcClient::execute) + .flatMap { json -> + val type: JavaType = objectMapper.typeFactory.constructParametricType(FullResponseJson::class.java, resultType, Int::class.java) + val resp = objectMapper.readerFor(type).readValue>(json) + if (resp.hasError()) { + Mono.error(resp.error.asException()) + } else { + Mono.just(resp.result) + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinChainUpstreams.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinChainUpstreams.kt new file mode 100644 index 00000000..d34649b6 --- /dev/null +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinChainUpstreams.kt @@ -0,0 +1,79 @@ +package io.emeraldpay.dshackle.upstream.bitcoin + +import com.fasterxml.jackson.databind.ObjectMapper +import io.emeraldpay.dshackle.cache.Caches +import io.emeraldpay.dshackle.config.UpstreamsConfig +import io.emeraldpay.dshackle.upstream.* +import io.emeraldpay.dshackle.upstream.ethereum.EthereumApi +import io.emeraldpay.dshackle.upstream.ethereum.EthereumHeadLagObserver +import io.emeraldpay.grpc.Chain +import org.slf4j.LoggerFactory +import org.springframework.context.Lifecycle + +class BitcoinChainUpstreams( + chain: Chain, + val upstreams: MutableList, + caches: Caches, + objectMapper: ObjectMapper +) : ChainUpstreams(chain, upstreams as MutableList>, caches, objectMapper) { + + companion object { + private val log = LoggerFactory.getLogger(BitcoinChainUpstreams::class.java) + } + + private var head: Head? = null + + override fun init() { + if (upstreams.size > 0) { + head = updateHead() + } + super.init() + } + + override fun updateHead(): Head { + head?.let { + if (it is Lifecycle) { + it.stop() + } + } + lagObserver?.stop() + lagObserver = null + val head = if (upstreams.size == 1) { + val upstream = upstreams.first() + upstream.setLag(0) + upstream.getHead() + } else { + val newHead = MergedHead(upstreams.map { it.getHead() }).apply { + this.start() + } +// val lagObserver = TODO +// this.lagObserver = lagObserver + newHead + } + onHeadUpdated(head) + return head + } + + override fun setHead(head: Head) { + this.head = head + } + + override fun getHead(): Head { + return head!! + } + + override fun getLabels(): Collection { + return upstreams.flatMap { it.getLabels() } + } + + override fun , TA : UpstreamApi> cast(selfType: Class, upstreamType: Class): T { + if (!selfType.isAssignableFrom(this.javaClass)) { + throw ClassCastException("Cannot cast ${this.javaClass} to $selfType") + } + if (!upstreamType.isAssignableFrom(BitcoinApi::class.java)) { + throw ClassCastException("Cannot cast ${EthereumApi::class.java} to $upstreamType") + } + return this as T + } + +} \ No newline at end of file diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinRpcClient.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinRpcClient.kt new file mode 100644 index 00000000..fa58efb3 --- /dev/null +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinRpcClient.kt @@ -0,0 +1,54 @@ +package io.emeraldpay.dshackle.upstream.bitcoin + +import io.emeraldpay.dshackle.config.AuthConfig +import io.netty.buffer.Unpooled +import io.netty.handler.codec.http.HttpHeaderNames +import io.netty.handler.codec.http.HttpHeaders +import org.slf4j.LoggerFactory +import reactor.core.publisher.Mono +import reactor.netty.http.client.HttpClient +import java.util.* +import java.util.function.Consumer + +class BitcoinRpcClient( + private val target: String, + basicAuth: AuthConfig.ClientBasicAuth? +) { + + companion object { + private val log = LoggerFactory.getLogger(BitcoinRpcClient::class.java) + } + + private val httpClient: HttpClient + + init { + var build = HttpClient.create() + + build = build.headers { h -> + h.add(HttpHeaderNames.CONTENT_TYPE, "application/json") + } + + basicAuth?.let { basicAuth -> + val authString: String = basicAuth.username + ":" + basicAuth.password + val authBase64 = Base64.getEncoder().encodeToString(authString.toByteArray()) + val auth = "Basic $authBase64" + val headers = Consumer { h: HttpHeaders -> h.add(HttpHeaderNames.AUTHORIZATION, auth) } + build = build.headers(headers) + } + + this.httpClient = build + } + + fun execute(request: ByteArray): Mono { + val response = httpClient + .post() + .uri(target) + .send(Mono.just(request).map { Unpooled.wrappedBuffer(it) }) + + return response.responseContent() + .aggregate() + .asByteArray() + } + + +} \ No newline at end of file diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinRpcHead.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinRpcHead.kt new file mode 100644 index 00000000..837adc94 --- /dev/null +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinRpcHead.kt @@ -0,0 +1,63 @@ +package io.emeraldpay.dshackle.upstream.bitcoin + +import io.emeraldpay.dshackle.Defaults +import io.emeraldpay.dshackle.upstream.AbstractHead +import io.emeraldpay.dshackle.upstream.Head +import io.emeraldpay.dshackle.upstream.ethereum.EthereumRpcHead +import org.slf4j.LoggerFactory +import org.springframework.context.Lifecycle +import org.springframework.scheduling.concurrent.CustomizableThreadFactory +import reactor.core.Disposable +import reactor.core.publisher.Flux +import reactor.core.publisher.Mono +import reactor.core.scheduler.Schedulers +import java.time.Duration +import java.util.concurrent.Executors + +class BitcoinRpcHead( + private val api: BitcoinApi, + private val extractBlock: ExtractBlock, + private val interval: Duration = Duration.ofSeconds(15) +) : Head, AbstractHead(), Lifecycle { + + companion object { + private val log = LoggerFactory.getLogger(BitcoinRpcHead::class.java) + val scheduler = Schedulers.fromExecutor(Executors.newCachedThreadPool(CustomizableThreadFactory("bitcoin-rpc-head"))) + } + + private var refreshSubscription: Disposable? = null + + override fun isRunning(): Boolean { + return refreshSubscription != null + } + + override fun start() { + if (refreshSubscription != null) { + log.warn("Called to start when running") + return + } + val base = Flux.interval(interval) + .publishOn(scheduler) + .flatMap { + api.executeAndResult(0, "getbestblockhash", emptyList(), String::class.java) + .timeout(Defaults.timeout, Mono.error(Exception("Best block hash is not received"))) + } + .distinctUntilChanged() + .flatMap { hash -> + api.execute(0, "getblock", listOf(hash)) + .map(extractBlock::extract) + .timeout(Defaults.timeout, Mono.error(Exception("Block data is not received"))) + } + .onErrorContinue { err, _ -> + log.debug("RPC error ${err.message}") + } + refreshSubscription = super.follow(base) + } + + override fun stop() { + val copy = refreshSubscription + refreshSubscription = null + copy?.dispose() + } + +} \ No newline at end of file diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinUpstream.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinUpstream.kt new file mode 100644 index 00000000..b5b0bbfa --- /dev/null +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinUpstream.kt @@ -0,0 +1,91 @@ +package io.emeraldpay.dshackle.upstream.bitcoin + +import com.fasterxml.jackson.databind.ObjectMapper +import io.emeraldpay.dshackle.config.UpstreamsConfig +import io.emeraldpay.dshackle.upstream.* +import io.emeraldpay.dshackle.upstream.ethereum.EthereumUpstreamValidator +import io.emeraldpay.grpc.Chain +import org.slf4j.LoggerFactory +import org.springframework.context.Lifecycle +import reactor.core.Disposable +import reactor.core.publisher.Mono + +class BitcoinUpstream( + id: String, + val chain: Chain, + private val api: BitcoinApi, + options: UpstreamsConfig.Options, + private val objectMapper: ObjectMapper +) : DefaultUpstream(id, options, DefaultBitcoinMethods()), Lifecycle { + + companion object { + private val log = LoggerFactory.getLogger(BitcoinUpstream::class.java) + } + + private val head: Head = createHead() + private var validatorSubscription: Disposable? = null + + private fun createHead(): Head { + return BitcoinRpcHead( + api, + ExtractBlock(objectMapper) + ) + } + + override fun getHead(): Head { + return head + } + + override fun getApi(matcher: Selector.Matcher): Mono { + return Mono.just(api) + } + + override fun getLabels(): Collection { + return listOf(UpstreamsConfig.Labels()) + } + + override fun , TA : UpstreamApi> cast(selfType: Class, upstreamType: Class): T { + if (!selfType.isAssignableFrom(this.javaClass)) { + throw ClassCastException("Cannot cast ${this.javaClass} to $selfType") + } + if (!upstreamType.isAssignableFrom(BitcoinApi::class.java)) { + throw ClassCastException("Cannot cast ${BitcoinApi::class.java} to $upstreamType") + } + return this as T + } + + override fun isRunning(): Boolean { + var runningAny = validatorSubscription != null + if (head is Lifecycle) { + runningAny = runningAny || head.isRunning + } + return runningAny + } + + override fun start() { + log.info("Configured for ${chain.chainName}") + if (head is Lifecycle) { + if (!head.isRunning) { + head.start() + } + } + validatorSubscription?.dispose() + + if (getOptions().disableValidation != null && getOptions().disableValidation!!) { + this.setLag(0) + this.setStatus(UpstreamAvailability.OK) + } else { + val validator = BitcoinUpstreamValidator(api, getOptions()) + validatorSubscription = validator.start() + .subscribe(this::setStatus) + } + } + + override fun stop() { + if (head is Lifecycle) { + head.stop() + } + validatorSubscription?.dispose() + } + +} \ No newline at end of file diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinUpstreamValidator.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinUpstreamValidator.kt new file mode 100644 index 00000000..be7f3ef0 --- /dev/null +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinUpstreamValidator.kt @@ -0,0 +1,47 @@ +package io.emeraldpay.dshackle.upstream.bitcoin + +import io.emeraldpay.dshackle.Defaults +import io.emeraldpay.dshackle.config.UpstreamsConfig +import io.emeraldpay.dshackle.upstream.Selector +import io.emeraldpay.dshackle.upstream.UpstreamAvailability +import io.infinitape.etherjar.rpc.Commands +import org.slf4j.LoggerFactory +import org.springframework.scheduling.concurrent.CustomizableThreadFactory +import reactor.core.publisher.Flux +import reactor.core.publisher.Mono +import reactor.core.scheduler.Schedulers +import java.time.Duration +import java.util.concurrent.Executors + +class BitcoinUpstreamValidator( + private val api: BitcoinApi, + private val options: UpstreamsConfig.Options +) { + + companion object { + private val log = LoggerFactory.getLogger(BitcoinUpstreamValidator::class.java) + val scheduler = Schedulers.fromExecutor(Executors.newCachedThreadPool(CustomizableThreadFactory("bitcoin-validator"))) + } + + fun validate(): Mono { + return api.executeAndResult(0, "getconnectioncount", emptyList(), Int::class.java) + .map { count -> + val minPeers = options.minPeers ?: 1 + if (count < minPeers) { + UpstreamAvailability.IMMATURE + } else { + UpstreamAvailability.OK + } + } + .onErrorReturn(UpstreamAvailability.UNAVAILABLE) + } + + fun start(): Flux { + return Flux.interval(Duration.ofSeconds(15)) + .subscribeOn(scheduler) + .flatMap { + validate() + } + } + +} \ No newline at end of file diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/DefaultBitcoinMethods.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/DefaultBitcoinMethods.kt new file mode 100644 index 00000000..370a464f --- /dev/null +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/DefaultBitcoinMethods.kt @@ -0,0 +1,10 @@ +package io.emeraldpay.dshackle.upstream.bitcoin + +import io.emeraldpay.dshackle.upstream.calls.DirectCallMethods + +class DefaultBitcoinMethods : DirectCallMethods( + listOf("getbestblockhash", "getblock", "gettransaction") +) { + + +} \ No newline at end of file diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/ExtractBlock.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/ExtractBlock.kt new file mode 100644 index 00000000..25e94a01 --- /dev/null +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/ExtractBlock.kt @@ -0,0 +1,40 @@ +package io.emeraldpay.dshackle.upstream.bitcoin + +import com.fasterxml.jackson.databind.ObjectMapper +import io.emeraldpay.dshackle.data.BlockContainer +import io.emeraldpay.dshackle.data.BlockId +import io.emeraldpay.dshackle.data.TxId +import org.apache.commons.codec.binary.Hex +import org.slf4j.LoggerFactory +import java.math.BigInteger +import java.time.Instant + +class ExtractBlock( + private val objectMapper: ObjectMapper +) { + + companion object { + private val log = LoggerFactory.getLogger(ExtractBlock::class.java) + } + + fun extract(json: ByteArray): BlockContainer { + val data = objectMapper.readValue(json, Map::class.java) as Map + + val height = data["height"] as Number? ?: throw IllegalArgumentException("Block JSON has no height") + val time = data["time"] as Number? ?: throw IllegalArgumentException("Block JSON has no time") + val hash = data["hash"] as String? ?: throw IllegalArgumentException("Block JSON has no hash") + val chainwork = data["chainwork"] as String? ?: throw IllegalArgumentException("Block JSON has no chainwork") + val transactions = (data["tx"] as List?)?.map(TxId.Companion::from) ?: emptyList() + + return BlockContainer( + height.toLong(), + BlockId.from(hash), + BigInteger(1, Hex.decodeHex(chainwork)), + Instant.ofEpochMilli(time.toLong() * 1000), + false, + json, + transactions + ) + } + +} \ No newline at end of file diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/calls/DirectCallMethods.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/calls/DirectCallMethods.kt index eea7d106..53d8b622 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/calls/DirectCallMethods.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/calls/DirectCallMethods.kt @@ -22,10 +22,10 @@ import io.emeraldpay.dshackle.quorum.CallQuorum * Configuration that uses [AlwaysQuorum] for all available methods. The methods list itself * is provided in constructor (or empty otherwise) */ -class DirectCallMethods(private val methods: Set) : CallMethods { +open class DirectCallMethods(private val methods: Set) : CallMethods { - constructor(): this(emptySet()) - constructor(methods: Collection): this(methods.toSet()) + constructor() : this(emptySet()) + constructor(methods: Collection) : this(methods.toSet()) override fun getQuorumFor(method: String): CallQuorum { return AlwaysQuorum() diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/DefaultEthereumHead.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/DefaultEthereumHead.kt index 487119d3..ab23c3ad 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/DefaultEthereumHead.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/DefaultEthereumHead.kt @@ -1,49 +1,8 @@ package io.emeraldpay.dshackle.upstream.ethereum -import io.emeraldpay.dshackle.data.BlockContainer -import org.slf4j.LoggerFactory -import reactor.core.Disposable -import reactor.core.publisher.Flux -import reactor.core.publisher.Mono -import reactor.core.publisher.TopicProcessor -import java.util.concurrent.atomic.AtomicReference +import io.emeraldpay.dshackle.upstream.AbstractHead +import io.emeraldpay.dshackle.upstream.Head -open class DefaultEthereumHead: EthereumHead { +open class DefaultEthereumHead : Head, AbstractHead() { - private val log = LoggerFactory.getLogger(DefaultEthereumHead::class.java) - private val head = AtomicReference(null) - private val stream: TopicProcessor = TopicProcessor.create() - - fun follow(source: Flux): Disposable { - return source.distinctUntilChanged { - it.hash - }.filter { block -> - val curr = head.get() - curr == null || curr.difficulty < block.difficulty - } - .subscribe { block -> - val prev = head.getAndUpdate { curr -> - if (curr == null || curr.difficulty < block.difficulty) { - block - } else { - curr - } - } - if (prev == null || prev.hash != block.hash) { - log.debug("New block ${block.height} ${block.hash}") - stream.onNext(block) - } - } - } - - override fun getFlux(): Flux { - return Flux.merge( - Mono.justOrEmpty(head.get()), - Flux.from(stream) - ).onBackpressureLatest() - } - - fun getCurrent(): BlockContainer? { - return head.get() - } } \ No newline at end of file diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumChainUpstreams.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumChainUpstreams.kt index 239b790e..ae389a0e 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumChainUpstreams.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumChainUpstreams.kt @@ -20,13 +20,8 @@ import io.emeraldpay.dshackle.cache.Caches import io.emeraldpay.dshackle.config.UpstreamsConfig import io.emeraldpay.dshackle.upstream.* import io.emeraldpay.grpc.Chain -import io.infinitape.etherjar.rpc.json.BlockJson -import io.infinitape.etherjar.rpc.json.TransactionJson -import io.infinitape.etherjar.rpc.json.TransactionRefJson import org.slf4j.LoggerFactory import org.springframework.context.Lifecycle -import java.lang.IllegalStateException -import java.time.Duration class EthereumChainUpstreams( chain: Chain, @@ -39,7 +34,7 @@ class EthereumChainUpstreams( private val log = LoggerFactory.getLogger(EthereumChainUpstreams::class.java) } - private var head: EthereumHead? = null + private var head: Head? = null init { this.init() @@ -52,15 +47,15 @@ class EthereumChainUpstreams( super.init() } - override fun getHead(): EthereumHead { + override fun getHead(): Head { return head!! } override fun setHead(head: Head) { - this.head = head as EthereumHead + this.head = head } - override fun updateHead(): EthereumHead { + override fun updateHead(): Head { head?.let { if (it is Lifecycle) { it.stop() @@ -73,7 +68,7 @@ class EthereumChainUpstreams( upstream.setLag(0) upstream.getHead() } else { - val newHead = EthereumHeadMerge(upstreams.map { it.getHead() }).apply { + val newHead = MergedHead(upstreams.map { it.getHead() }).apply { this.start() } val lagObserver = EthereumHeadLagObserver(newHead, upstreams as Collection>).apply { @@ -90,25 +85,6 @@ class EthereumChainUpstreams( return upstreams.flatMap { it.getLabels() } } - override fun printStatus() { - var height: Long? = null - try { - height = getHead().getFlux().next().block(Duration.ofSeconds(1))?.height - } catch (e: IllegalStateException) { - //timout - } catch (e: Exception) { - log.warn("Head processing error: ${e.javaClass} ${e.message}") - } - val statuses = upstreams.map { it.getStatus() } - .groupBy { it } - .map { "${it.key.name}/${it.value.size}" } - .joinToString(",") - val lag = upstreams.map { it.getLag() } - .joinToString(", ") - - log.info("State of ${chain.chainCode}: height=${height ?: '?'}, status=$statuses, lag=[$lag]") - } - @SuppressWarnings("unchecked") override fun , TA : UpstreamApi> cast(selfType: Class, upstreamType: Class): T { if (!selfType.isAssignableFrom(this.javaClass)) { diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumHead.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumHead.kt deleted file mode 100644 index 0f5719c7..00000000 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumHead.kt +++ /dev/null @@ -1,24 +0,0 @@ -/** - * Copyright (c) 2019 ETCDEV GmbH - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.emeraldpay.dshackle.upstream.ethereum - -import io.emeraldpay.dshackle.upstream.Head -import io.infinitape.etherjar.domain.TransactionId -import io.infinitape.etherjar.rpc.json.BlockJson -import io.infinitape.etherjar.rpc.json.TransactionRefJson - -interface EthereumHead : Head { -} \ No newline at end of file diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumHeadLagObserver.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumHeadLagObserver.kt index 47c88390..17c6327e 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumHeadLagObserver.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumHeadLagObserver.kt @@ -18,12 +18,13 @@ package io.emeraldpay.dshackle.upstream.ethereum import io.emeraldpay.dshackle.upstream.HeadLagObserver import io.emeraldpay.dshackle.upstream.Upstream import io.emeraldpay.dshackle.data.BlockContainer +import io.emeraldpay.dshackle.upstream.Head import org.slf4j.LoggerFactory import reactor.core.publisher.Flux import java.time.Duration class EthereumHeadLagObserver( - master: EthereumHead, + master: Head, followers: Collection> ) : HeadLagObserver(master, followers) { diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumUpstream.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumUpstream.kt index b405a039..1b4abd00 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumUpstream.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumUpstream.kt @@ -24,8 +24,6 @@ import io.emeraldpay.dshackle.upstream.* import io.emeraldpay.dshackle.upstream.calls.CallMethods import io.emeraldpay.dshackle.upstream.calls.DirectCallMethods import io.emeraldpay.grpc.Chain -import io.infinitape.etherjar.rpc.json.BlockJson -import io.infinitape.etherjar.rpc.json.TransactionRefJson import org.slf4j.LoggerFactory import org.springframework.context.Lifecycle import reactor.core.Disposable @@ -33,15 +31,15 @@ import reactor.core.publisher.Mono import java.time.Duration open class EthereumUpstream( - private val id: String, + id: String, val chain: Chain, private val api: DirectEthereumApi, private val ethereumWs: EthereumWs? = null, - private val options: UpstreamsConfig.Options, + options: UpstreamsConfig.Options, val node: QuorumForLabels.QuorumItem, - private val targets: CallMethods, + targets: CallMethods, private val objectMapper: ObjectMapper -) : DefaultUpstream(), Upstream, CachesEnabled, Lifecycle { +) : DefaultUpstream(id, options, targets), Upstream, CachesEnabled, Lifecycle { constructor(id: String, chain: Chain, api: DirectEthereumApi, objectMapper: ObjectMapper) : this(id, chain, api, null, UpstreamsConfig.Options.getDefaults(), QuorumForLabels.QuorumItem(1, UpstreamsConfig.Labels()), @@ -50,7 +48,7 @@ open class EthereumUpstream( private val log = LoggerFactory.getLogger(EthereumUpstream::class.java) - private val head: EthereumHead = this.createHead() + private val head: Head = this.createHead() private var validatorSubscription: Disposable? = null init { @@ -64,18 +62,14 @@ open class EthereumUpstream( } } - override fun getId(): String { - return id - } - override fun start() { log.info("Configured for ${chain.chainName}") - if (options.disableValidation != null && options.disableValidation!!) { + if (getOptions().disableValidation != null && getOptions().disableValidation!!) { this.setLag(0) this.setStatus(UpstreamAvailability.OK) } else { - val validator = UpstreamValidator(this, options) + val validator = EthereumUpstreamValidator(this, getOptions()) validatorSubscription = validator.start() .subscribe(this::setStatus) } @@ -93,7 +87,7 @@ open class EthereumUpstream( } } - open fun createHead(): EthereumHead { + open fun createHead(): Head { return if (ethereumWs != null) { val ws = EthereumWsHead(ethereumWs).apply { this.start() @@ -102,22 +96,18 @@ open class EthereumUpstream( val rpc = EthereumRpcHead(api, objectMapper, Duration.ofSeconds(30)).apply { this.start() } - EthereumHeadMerge(listOf(rpc, ws)).apply { + MergedHead(listOf(rpc, ws)).apply { this.start() } } else { - log.warn("Setting up upstream $id with RPC-only access, less effective than WS+RPC") + log.warn("Setting up upstream ${this.getId()} with RPC-only access, less effective than WS+RPC") EthereumRpcHead(api, objectMapper).apply { this.start() } } } - override fun isAvailable(): Boolean { - return getStatus() == UpstreamAvailability.OK - } - - override fun getHead(): EthereumHead { + override fun getHead(): Head { return head } @@ -125,18 +115,10 @@ open class EthereumUpstream( return Mono.just(api) } - override fun getOptions(): UpstreamsConfig.Options { - return options - } - override fun getLabels(): Collection { return listOf(node.labels) } - override fun getMethods(): CallMethods { - return targets - } - @Suppress("unchecked") override fun , TA : UpstreamApi> cast(selfType: Class, upstreamType: Class): T { if (!selfType.isAssignableFrom(this.javaClass)) { diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/UpstreamValidator.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumUpstreamValidator.kt similarity index 89% rename from src/main/kotlin/io/emeraldpay/dshackle/upstream/UpstreamValidator.kt rename to src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumUpstreamValidator.kt index c6c5bf12..8adf1840 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/UpstreamValidator.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumUpstreamValidator.kt @@ -13,11 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.emeraldpay.dshackle.upstream +package io.emeraldpay.dshackle.upstream.ethereum import io.emeraldpay.dshackle.Defaults import io.emeraldpay.dshackle.config.UpstreamsConfig -import io.emeraldpay.dshackle.upstream.ethereum.EthereumUpstream +import io.emeraldpay.dshackle.upstream.Selector +import io.emeraldpay.dshackle.upstream.UpstreamAvailability import io.infinitape.etherjar.rpc.* import org.slf4j.LoggerFactory import org.springframework.scheduling.concurrent.CustomizableThreadFactory @@ -27,13 +28,13 @@ import reactor.core.scheduler.Schedulers import java.time.Duration import java.util.concurrent.Executors -class UpstreamValidator( +class EthereumUpstreamValidator( private val ethereumUpstream: EthereumUpstream, private val options: UpstreamsConfig.Options ) { companion object { - private val log = LoggerFactory.getLogger(UpstreamValidator::class.java) - val scheduler = Schedulers.fromExecutor(Executors.newCachedThreadPool(CustomizableThreadFactory("validator"))) + private val log = LoggerFactory.getLogger(EthereumUpstreamValidator::class.java) + val scheduler = Schedulers.fromExecutor(Executors.newCachedThreadPool(CustomizableThreadFactory("ethereum-validator"))) } fun validate(): Mono { diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/grpc/EthereumGrpcUpstream.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/grpc/EthereumGrpcUpstream.kt index 99ef6ee1..1e786ac6 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/grpc/EthereumGrpcUpstream.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/grpc/EthereumGrpcUpstream.kt @@ -33,7 +33,6 @@ import io.emeraldpay.dshackle.upstream.calls.DirectCallMethods import io.emeraldpay.dshackle.upstream.ethereum.DefaultEthereumHead import io.emeraldpay.dshackle.upstream.ethereum.DirectEthereumApi import io.emeraldpay.dshackle.upstream.ethereum.EthereumApi -import io.emeraldpay.dshackle.upstream.ethereum.EthereumHead import io.emeraldpay.grpc.Chain import io.infinitape.etherjar.domain.BlockHash import io.infinitape.etherjar.rpc.* @@ -59,13 +58,16 @@ open class EthereumGrpcUpstream( private val blockchainStub: ReactorBlockchainGrpc.ReactorBlockchainStub, private val objectMapper: ObjectMapper, private val rpcClient: ReactorEmeraldClient -) : DefaultUpstream(), CachesEnabled, Lifecycle { +) : DefaultUpstream( + "$parentId/${chain.chainCode}", + UpstreamsConfig.Options.getDefaults(), + null +), CachesEnabled, Lifecycle { private var allLabels: Collection = ArrayList() private val log = LoggerFactory.getLogger(EthereumGrpcUpstream::class.java) private var caches: Caches? = null - private val options = UpstreamsConfig.Options.getDefaults() private val nodes = AtomicReference(QuorumForLabels()) private val head = DefaultEthereumHead() private var targets: CallMethods? = null @@ -84,10 +86,6 @@ open class EthereumGrpcUpstream( } } - override fun getId(): String { - return "$parentId/${chain.chainCode}" - } - override fun start() { if (this.isRunning) return val chainRef = Common.Chain.newBuilder() @@ -200,12 +198,12 @@ open class EthereumGrpcUpstream( } override fun isAvailable(): Boolean { - return getStatus() == UpstreamAvailability.OK && head.getCurrent() != null && nodes.get().getAll().any { + return super.isAvailable() && head.getCurrent() != null && nodes.get().getAll().any { it.quorum > 0 } } - override fun getHead(): EthereumHead { + override fun getHead(): Head { return head } @@ -213,10 +211,6 @@ open class EthereumGrpcUpstream( return Mono.just(createApi(matcher)) } - override fun getOptions(): UpstreamsConfig.Options { - return options - } - override fun setCaches(caches: Caches) { this.caches = caches } @@ -231,5 +225,4 @@ open class EthereumGrpcUpstream( } return this as T } - } \ No newline at end of file diff --git a/src/test/groovy/io/emeraldpay/dshackle/cache/BlocksMemCacheSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/cache/BlocksMemCacheSpec.groovy index 5bbe6db4..b676248e 100644 --- a/src/test/groovy/io/emeraldpay/dshackle/cache/BlocksMemCacheSpec.groovy +++ b/src/test/groovy/io/emeraldpay/dshackle/cache/BlocksMemCacheSpec.groovy @@ -77,9 +77,9 @@ class BlocksMemCacheSpec extends Specification { def act3 = cache.read(BlockId.from(hash3)).block() def act4 = cache.read(BlockId.from(hash4)).block() then: - act2.hash.toHex() == hash2 - act3.hash.toHex() == hash3 - act4.hash.toHex() == hash4 + act2.hash.toHex() == hash2.substring(2) + act3.hash.toHex() == hash3.substring(2) + act4.hash.toHex() == hash4.substring(2) act1 == null } diff --git a/src/test/groovy/io/emeraldpay/dshackle/cache/HeightCacheSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/cache/HeightCacheSpec.groovy index e711438e..a1cb2abc 100644 --- a/src/test/groovy/io/emeraldpay/dshackle/cache/HeightCacheSpec.groovy +++ b/src/test/groovy/io/emeraldpay/dshackle/cache/HeightCacheSpec.groovy @@ -38,10 +38,10 @@ class HeightCacheSpec extends Specification { def act3 = cache.read(102).block() def act4 = cache.read(103).block() then: - act1.toHex() == hash1 - act2.toHex() == hash2 - act3.toHex() == hash3 - act4.toHex() == hash4 + act1.toHex() == hash1.substring(2) + act2.toHex() == hash2.substring(2) + act3.toHex() == hash3.substring(2) + act4.toHex() == hash4.substring(2) } def "Keeps only configured amount"() { @@ -65,8 +65,8 @@ class HeightCacheSpec extends Specification { def act4 = cache.read(103).block() then: act1 == null - act2.toHex() == hash2 - act3.toHex() == hash3 - act4.toHex() == hash4 + act2.toHex() == hash2.substring(2) + act3.toHex() == hash3.substring(2) + act4.toHex() == hash4.substring(2) } } diff --git a/src/test/groovy/io/emeraldpay/dshackle/cache/TxMemCacheSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/cache/TxMemCacheSpec.groovy index 5b310d7e..c576ae49 100644 --- a/src/test/groovy/io/emeraldpay/dshackle/cache/TxMemCacheSpec.groovy +++ b/src/test/groovy/io/emeraldpay/dshackle/cache/TxMemCacheSpec.groovy @@ -57,9 +57,9 @@ class TxMemCacheSpec extends Specification { def act3 = cache.read(TxId.from(hash3)).block() def act4 = cache.read(TxId.from(hash4)).block() then: - act2.hash.toHex() == hash2 - act3.hash.toHex() == hash3 - act4.hash.toHex() == hash4 + act2.hash.toHex() == hash2.substring(2) + act3.hash.toHex() == hash3.substring(2) + act4.hash.toHex() == hash4.substring(2) act1 == null } @@ -93,8 +93,8 @@ class TxMemCacheSpec extends Specification { then: act1 == null act2 == null - act3.hash.toHex() == hash3 - act4.hash.toHex() == hash4 + act3.hash.toHex() == hash3.substring(2) + act4.hash.toHex() == hash4.substring(2) } def "Evict all by block data"() { @@ -137,7 +137,7 @@ class TxMemCacheSpec extends Specification { then: act1 == null act2 == null - act3.hash.toHex() == hash3 - act4.hash.toHex() == hash4 + act3.hash.toHex() == hash3.substring(2) + act4.hash.toHex() == hash4.substring(2) } } diff --git a/src/test/groovy/io/emeraldpay/dshackle/data/BlockIdSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/data/BlockIdSpec.groovy new file mode 100644 index 00000000..73a16e04 --- /dev/null +++ b/src/test/groovy/io/emeraldpay/dshackle/data/BlockIdSpec.groovy @@ -0,0 +1,30 @@ +package io.emeraldpay.dshackle.data + +import io.infinitape.etherjar.domain.BlockHash +import spock.lang.Specification + +class BlockIdSpec extends Specification { + + def "Create from string"() { + expect: + BlockId.from(source).toHex() == exp + where: + exp | source + "a0e65cbc1b52a8ca60562112c6060552d882f16f34a9dba2ccdc05c0a6a27100" | "a0e65cbc1b52a8ca60562112c6060552d882f16f34a9dba2ccdc05c0a6a27100" + "a0e65cbc1b52a8ca60562112c6060552d882f16f34a9dba2ccdc05c0a6a27100" | "A0E65CBC1B52A8CA60562112C6060552D882F16F34A9DBA2CCDC05C0A6A27100" + "a0e65cbc1b52a8ca60562112c6060552d882f16f34a9dba2ccdc05c0a6a27100" | "0xa0e65cbc1b52a8ca60562112c6060552d882f16f34a9dba2ccdc05c0a6a27100" + "a0e65cbc1b52a8ca60562112c6060552d882f16f34a9dba2ccdc05c0a6a27100" | "0xA0E65CBC1B52A8CA60562112C6060552D882F16F34A9DBA2CCDC05C0A6A27100" + "00e65cbc1b52a8ca60562112c6060552d882f16f34a9dba2ccdc05c0a6a27100" | "00e65cbc1b52a8ca60562112c6060552d882f16f34a9dba2ccdc05c0a6a27100" + "00e65cbc1b52a8ca60562112c6060552d882f16f34a9dba2ccdc05c0a6a27100" | "0x00e65cbc1b52a8ca60562112c6060552d882f16f34a9dba2ccdc05c0a6a27100" + } + + def "Create from block hash"() { + expect: + BlockId.from(BlockHash.from(source)).toHex() == exp + where: + exp | source + "a0e65cbc1b52a8ca60562112c6060552d882f16f34a9dba2ccdc05c0a6a27100" | "0xa0e65cbc1b52a8ca60562112c6060552d882f16f34a9dba2ccdc05c0a6a27100" + "00e65cbc1b52a8ca60562112c6060552d882f16f34a9dba2ccdc05c0a6a27100" | "0x00e65cbc1b52a8ca60562112c6060552d882f16f34a9dba2ccdc05c0a6a27100" + } + +} diff --git a/src/test/groovy/io/emeraldpay/dshackle/data/TxIdSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/data/TxIdSpec.groovy new file mode 100644 index 00000000..d333eb90 --- /dev/null +++ b/src/test/groovy/io/emeraldpay/dshackle/data/TxIdSpec.groovy @@ -0,0 +1,29 @@ +package io.emeraldpay.dshackle.data + +import io.infinitape.etherjar.domain.TransactionId +import spock.lang.Specification + +class TxIdSpec extends Specification { + + def "Create from string"() { + expect: + TxId.from(source).toHex() == exp + where: + exp | source + "a0e65cbc1b52a8ca60562112c6060552d882f16f34a9dba2ccdc05c0a6a27100" | "a0e65cbc1b52a8ca60562112c6060552d882f16f34a9dba2ccdc05c0a6a27100" + "a0e65cbc1b52a8ca60562112c6060552d882f16f34a9dba2ccdc05c0a6a27100" | "A0E65CBC1B52A8CA60562112C6060552D882F16F34A9DBA2CCDC05C0A6A27100" + "a0e65cbc1b52a8ca60562112c6060552d882f16f34a9dba2ccdc05c0a6a27100" | "0xa0e65cbc1b52a8ca60562112c6060552d882f16f34a9dba2ccdc05c0a6a27100" + "a0e65cbc1b52a8ca60562112c6060552d882f16f34a9dba2ccdc05c0a6a27100" | "0xA0E65CBC1B52A8CA60562112C6060552D882F16F34A9DBA2CCDC05C0A6A27100" + "00e65cbc1b52a8ca60562112c6060552d882f16f34a9dba2ccdc05c0a6a27100" | "00e65cbc1b52a8ca60562112c6060552d882f16f34a9dba2ccdc05c0a6a27100" + "00e65cbc1b52a8ca60562112c6060552d882f16f34a9dba2ccdc05c0a6a27100" | "0x00e65cbc1b52a8ca60562112c6060552d882f16f34a9dba2ccdc05c0a6a27100" + } + + def "Create from block hash"() { + expect: + TxId.from(TransactionId.from(source)).toHex() == exp + where: + exp | source + "a0e65cbc1b52a8ca60562112c6060552d882f16f34a9dba2ccdc05c0a6a27100" | "0xa0e65cbc1b52a8ca60562112c6060552d882f16f34a9dba2ccdc05c0a6a27100" + "00e65cbc1b52a8ca60562112c6060552d882f16f34a9dba2ccdc05c0a6a27100" | "0x00e65cbc1b52a8ca60562112c6060552d882f16f34a9dba2ccdc05c0a6a27100" + } +} diff --git a/src/test/groovy/io/emeraldpay/dshackle/test/EthereumHeadMock.groovy b/src/test/groovy/io/emeraldpay/dshackle/test/EthereumHeadMock.groovy index fa737f51..aca59f3c 100644 --- a/src/test/groovy/io/emeraldpay/dshackle/test/EthereumHeadMock.groovy +++ b/src/test/groovy/io/emeraldpay/dshackle/test/EthereumHeadMock.groovy @@ -16,12 +16,12 @@ package io.emeraldpay.dshackle.test import io.emeraldpay.dshackle.data.BlockContainer -import io.emeraldpay.dshackle.upstream.ethereum.EthereumHead +import io.emeraldpay.dshackle.upstream.Head import reactor.core.publisher.Flux import reactor.core.publisher.Mono import reactor.core.publisher.TopicProcessor -class EthereumHeadMock implements EthereumHead { +class EthereumHeadMock implements Head { private TopicProcessor bus = TopicProcessor.create() private BlockContainer latest diff --git a/src/test/groovy/io/emeraldpay/dshackle/test/EthereumUpstreamMock.groovy b/src/test/groovy/io/emeraldpay/dshackle/test/EthereumUpstreamMock.groovy index 22fdd3e5..7c87bbc2 100644 --- a/src/test/groovy/io/emeraldpay/dshackle/test/EthereumUpstreamMock.groovy +++ b/src/test/groovy/io/emeraldpay/dshackle/test/EthereumUpstreamMock.groovy @@ -17,11 +17,11 @@ package io.emeraldpay.dshackle.test import io.emeraldpay.dshackle.config.UpstreamsConfig import io.emeraldpay.dshackle.data.BlockContainer +import io.emeraldpay.dshackle.upstream.Head import io.emeraldpay.dshackle.upstream.calls.CallMethods import io.emeraldpay.dshackle.startup.QuorumForLabels import io.emeraldpay.dshackle.upstream.calls.DefaultEthereumMethods import io.emeraldpay.dshackle.upstream.ethereum.DirectEthereumApi -import io.emeraldpay.dshackle.upstream.ethereum.EthereumHead import io.emeraldpay.dshackle.upstream.ethereum.EthereumUpstream import io.emeraldpay.dshackle.upstream.UpstreamAvailability import io.emeraldpay.grpc.Chain @@ -58,12 +58,12 @@ class EthereumUpstreamMock extends EthereumUpstream { } @Override - EthereumHead createHead() { + Head createHead() { return ethereumHeadMock } @Override - EthereumHead getHead() { + Head getHead() { return ethereumHeadMock } } diff --git a/src/test/groovy/io/emeraldpay/dshackle/test/MockServer.groovy b/src/test/groovy/io/emeraldpay/dshackle/test/MockGrpcServer.groovy similarity index 90% rename from src/test/groovy/io/emeraldpay/dshackle/test/MockServer.groovy rename to src/test/groovy/io/emeraldpay/dshackle/test/MockGrpcServer.groovy index 2940b2ce..35bb1bfe 100644 --- a/src/test/groovy/io/emeraldpay/dshackle/test/MockServer.groovy +++ b/src/test/groovy/io/emeraldpay/dshackle/test/MockGrpcServer.groovy @@ -21,15 +21,15 @@ import io.grpc.inprocess.InProcessChannelBuilder import io.grpc.inprocess.InProcessServerBuilder import io.grpc.testing.GrpcCleanupRule -class MockServer { +class MockGrpcServer { GrpcCleanupRule grpcCleanup = new GrpcCleanupRule() - ReactorBlockchainGrpc.ReactorBlockchainStub clientForServer(ReactorBlockchainGrpc.BlockchainImplBase impl){ + ReactorBlockchainGrpc.ReactorBlockchainStub clientForServer(ReactorBlockchainGrpc.BlockchainImplBase impl) { String serverName = InProcessServerBuilder.generateName() grpcCleanup.register(InProcessServerBuilder .forName(serverName).directExecutor().addService(impl).build().start()); - def channel = grpcCleanup.register(InProcessChannelBuilder.forName(serverName).directExecutor().build()) + def channel = grpcCleanup.register(InProcessChannelBuilder.forName(serverName).directExecutor().build()) return ReactorBlockchainGrpc.newReactorStub(channel) } diff --git a/src/test/groovy/io/emeraldpay/dshackle/upstream/CachingEthereumApiSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/upstream/CachingEthereumApiSpec.groovy index 659d8a85..34212d27 100644 --- a/src/test/groovy/io/emeraldpay/dshackle/upstream/CachingEthereumApiSpec.groovy +++ b/src/test/groovy/io/emeraldpay/dshackle/upstream/CachingEthereumApiSpec.groovy @@ -10,7 +10,6 @@ import io.emeraldpay.dshackle.data.BlockContainer import io.emeraldpay.dshackle.data.BlockId import io.emeraldpay.dshackle.data.TxId import io.emeraldpay.dshackle.test.TestingCommons -import io.emeraldpay.dshackle.upstream.ethereum.EthereumHead import io.infinitape.etherjar.domain.BlockHash import io.infinitape.etherjar.domain.TransactionId import io.infinitape.etherjar.rpc.json.BlockJson @@ -30,7 +29,7 @@ class CachingEthereumApiSpec extends Specification { def "Get blockNumber from head"() { setup: - def head = Mock(EthereumHead.class) + def head = Mock(Head.class) def api = new CachingEthereumApi( objectMapper, Caches.default(objectMapper), @@ -58,7 +57,7 @@ class CachingEthereumApiSpec extends Specification { def "Return empty if block is not cached"() { setup: - def head = Mock(EthereumHead.class) + def head = Mock(Head.class) def api = new CachingEthereumApi( objectMapper, Caches.default(objectMapper), @@ -76,7 +75,7 @@ class CachingEthereumApiSpec extends Specification { def "Return block by hash when cached"() { setup: def cache = new BlocksMemCache(); - def head = Mock(EthereumHead.class) + def head = Mock(Head.class) def api = new CachingEthereumApi( objectMapper, Caches.newBuilder().setObjectMapper(objectMapper).setBlockByHash(cache).build(), @@ -106,7 +105,7 @@ class CachingEthereumApiSpec extends Specification { setup: def blocksCache = new BlocksMemCache() def heightCache = new HeightCache() - def head = Mock(EthereumHead.class) + def head = Mock(Head.class) def api = new CachingEthereumApi( objectMapper, Caches.newBuilder().setObjectMapper(objectMapper).setBlockByHash(blocksCache).setBlockByHeight(heightCache).build(), @@ -135,7 +134,7 @@ class CachingEthereumApiSpec extends Specification { setup: def blocksCache = Mock(BlocksMemCache) def txCache = Mock(TxMemCache) - def head = Mock(EthereumHead.class) + def head = Mock(Head.class) def api = new CachingEthereumApi( objectMapper, Caches.newBuilder().setObjectMapper(objectMapper).setBlockByHash(blocksCache).setTxByHash(txCache).build(), @@ -161,7 +160,7 @@ class CachingEthereumApiSpec extends Specification { setup: def blocksCache = Mock(BlocksMemCache) def txCache = Mock(TxMemCache) - def head = Mock(EthereumHead.class) + def head = Mock(Head.class) def api = new CachingEthereumApi( objectMapper, Caches.newBuilder().setObjectMapper(objectMapper).setBlockByHash(blocksCache).setTxByHash(txCache).build(), @@ -191,7 +190,7 @@ class CachingEthereumApiSpec extends Specification { def blocksCache = Mock(BlocksMemCache) def txCache = Mock(TxMemCache) def heightCache = Mock(HeightCache) - def head = Mock(EthereumHead.class) + def head = Mock(Head.class) def api = new CachingEthereumApi( objectMapper, Caches.newBuilder().setObjectMapper(objectMapper).setBlockByHash(blocksCache).setTxByHash(txCache).setBlockByHeight(heightCache).build(), @@ -219,7 +218,7 @@ class CachingEthereumApiSpec extends Specification { def blocksCache = Mock(BlocksMemCache) def txCache = Mock(TxMemCache) def heightCache = Mock(HeightCache) - def head = Mock(EthereumHead.class) + def head = Mock(Head.class) def api = new CachingEthereumApi( objectMapper, Caches.newBuilder().setObjectMapper(objectMapper).setBlockByHash(blocksCache).setTxByHash(txCache).setBlockByHeight(heightCache).build(), diff --git a/src/test/groovy/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinApiSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinApiSpec.groovy new file mode 100644 index 00000000..78e784bc --- /dev/null +++ b/src/test/groovy/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinApiSpec.groovy @@ -0,0 +1,108 @@ +package io.emeraldpay.dshackle.upstream.bitcoin + +import io.emeraldpay.dshackle.test.TestingCommons +import io.infinitape.etherjar.rpc.RpcException +import org.mockserver.integration.ClientAndServer +import org.mockserver.model.HttpRequest +import org.mockserver.model.HttpResponse +import reactor.test.StepVerifier +import spock.lang.Specification + +import java.time.Duration + +class BitcoinApiSpec extends Specification { + + ClientAndServer mockServer + BitcoinApi api + + def setup() { + mockServer = ClientAndServer.startClientAndServer(18332); + api = new BitcoinApi( + new BitcoinRpcClient("localhost:18332", null), + TestingCommons.objectMapper() + ) + } + + def cleanup() { + mockServer.stop() + } + + def "Request simple"() { + setup: + def resp = '{' + + ' "result": "0000000000000000000889c2e52ca5e1cecac60bce9a3754201a7a9a67791e90",' + + ' "error": null,' + + ' "id": 15' + + '}' + mockServer.when( + HttpRequest.request() + ).respond( + HttpResponse.response(resp) + ) + when: + def act = api.executeAndResult(15, "getbestblockhash", [], String) + then: + StepVerifier.create(act) + .expectNext("0000000000000000000889c2e52ca5e1cecac60bce9a3754201a7a9a67791e90") + .expectComplete() + .verify(Duration.ofSeconds(1)) + mockServer.verify( + HttpRequest.request() + .withMethod("POST") + .withBody('{"jsonrpc":"2.0","method":"getbestblockhash","params":[],"id":15}') + ) + } + + def "Request with params"() { + setup: + def resp = '{' + + ' "result": "something",' + + ' "id": 1' + + '}' + mockServer.when( + HttpRequest.request() + ).respond( + HttpResponse.response(resp) + ) + when: + def act = api.executeAndResult(1, "getsomething", ["something", false], String) + then: + StepVerifier.create(act) + .expectNext("something") + .expectComplete() + .verify(Duration.ofSeconds(1)) + mockServer.verify( + HttpRequest.request() + .withMethod("POST") + .withBody('{"jsonrpc":"2.0","method":"getsomething","params":["something",false],"id":1}') + ) + } + + def "Returns error"() { + setup: + def resp = '{' + + ' "result": null,' + + ' "error": {' + + ' "code": -32601,' + + ' "message": "Method not found"' + + ' },' + + ' "id": 1' + + '}' + mockServer.when( + HttpRequest.request() + ).respond( + HttpResponse.response(resp) + ) + when: + def act = api.executeAndResult(1, "geterror", [], String) + then: + StepVerifier.create(act) + .expectError(RpcException) + .verify(Duration.ofSeconds(1)) + mockServer.verify( + HttpRequest.request() + .withMethod("POST") + .withBody('{"jsonrpc":"2.0","method":"geterror","params":[],"id":1}') + ) + } +} diff --git a/src/test/groovy/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinRpcClientSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinRpcClientSpec.groovy new file mode 100644 index 00000000..a0ea6d1f --- /dev/null +++ b/src/test/groovy/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinRpcClientSpec.groovy @@ -0,0 +1,86 @@ +package io.emeraldpay.dshackle.upstream.bitcoin + +import io.emeraldpay.dshackle.config.AuthConfig +import org.mockserver.integration.ClientAndServer +import org.mockserver.matchers.Times +import org.mockserver.model.HttpRequest +import org.mockserver.model.HttpResponse +import org.mockserver.model.MediaType +import org.mockserver.verify.VerificationTimes +import reactor.test.StepVerifier +import spock.lang.Shared +import spock.lang.Specification + +import java.time.Duration + +class BitcoinRpcClientSpec extends Specification { + + ClientAndServer mockServer + + def setup() { + mockServer = ClientAndServer.startClientAndServer(18332); + } + + def cleanup() { + mockServer.stop() + } + + def "Make request"() { + setup: + def client = new BitcoinRpcClient("localhost:18332", null) + + mockServer.when( + HttpRequest.request() + .withMethod("POST") + .withBody("ping"), + Times.exactly(1) + ).respond( + HttpResponse.response() + .withBody("pong") + ) + when: + def act = client.execute("ping".bytes).map { new String(it) } + then: + StepVerifier.create(act) + .expectNext("pong") + .expectComplete() + .verify(Duration.ofSeconds(1)) + mockServer.verify( + HttpRequest.request() + .withMethod("POST") + .withBody("ping") + .withContentType(MediaType.APPLICATION_JSON) + ) + + } + + def "Make request with basic auth"() { + setup: + def auth = new AuthConfig.ClientBasicAuth("user", "passwd") + def client = new BitcoinRpcClient("localhost:18332", auth) + + mockServer.when( + HttpRequest.request() + .withMethod("POST") + .withBody("ping") + ).respond( + HttpResponse.response() + .withBody("pong") + ) + when: + def act = client.execute("ping".bytes).map { new String(it) } + then: + StepVerifier.create(act) + .expectNext("pong") + .expectComplete() + .verify(Duration.ofSeconds(1)) + mockServer.verify( + HttpRequest.request() + .withMethod("POST") + .withBody("ping") + .withContentType(MediaType.APPLICATION_JSON) + .withHeader("authorization", "Basic dXNlcjpwYXNzd2Q=") + ) + } + +} diff --git a/src/test/groovy/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinRpcHeadSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinRpcHeadSpec.groovy new file mode 100644 index 00000000..a8739cfe --- /dev/null +++ b/src/test/groovy/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinRpcHeadSpec.groovy @@ -0,0 +1,95 @@ +package io.emeraldpay.dshackle.upstream.bitcoin + +import de.jodamob.kotlin.testrunner.OpenedClasses +import de.jodamob.kotlin.testrunner.SpotlinTestRunner +import io.emeraldpay.dshackle.test.TestingCommons +import org.junit.runner.RunWith +import org.mockserver.integration.ClientAndServer +import reactor.core.publisher.Mono +import reactor.test.StepVerifier +import spock.lang.Specification + +import java.time.Duration + +@RunWith(SpotlinTestRunner) +@OpenedClasses(BitcoinApi) +class BitcoinRpcHeadSpec extends Specification { + + ClientAndServer mockServer + + def "Follow 2 blocks created over 3 requests"() { + setup: + String hash1 = "0000000000000000000cf5a5d4dfc4347c0c1a863ec5fdb429b02b2162e50001" + String hash2 = "0000000000000000000cf5a5d4dfc4347c0c1a863ec5fdb429b02b2162e50002" + + def block1 = """ + { + "hash": "${hash1}", + "confirmations": 2, + "strippedsize": 800464, + "size": 1591897, + "weight": 3993289, + "height": 626472, + "version": 549453824, + "versionHex": "20c00000", + "merkleroot": "7835a26b6c9316232f8194c6b64b0a366e26a9a14e6a7e7409c34594d98a83c2", + "tx": [], + "time": 1587171526, + "mediantime": 1587168930, + "nonce": 4111305375, + "bits": "171320bc", + "difficulty": 14715214060656.53, + "chainwork": "00000000000000000000000000000000000000000e9baec5f63190a2b0bbcf6b", + "nTx": 0, + "previousblockhash": "000000000000000000106b8cb739dd3c412613aa5d459a739794ec2572a74c10" + } + """ + def block2 = """ + { + "hash": "${hash2}", + "confirmations": 2, + "strippedsize": 800464, + "size": 1591897, + "weight": 3993289, + "height": 626473, + "version": 549453824, + "versionHex": "20c00000", + "merkleroot": "7835a26b6c9316232f8194c6b64b0a366e26a9a14e6a7e7409c34594d98a83c2", + "tx": [], + "time": 1587172526, + "mediantime": 1587168930, + "nonce": 4111305375, + "bits": "171320bc", + "difficulty": 14715214060656.53, + "chainwork": "00000000000000000000000000000000000000000e9baec5f63190a2b0bbcf6c", + "nTx": 0, + "previousblockhash": "${hash1}" + } + """ + + BitcoinApi api = Mock(BitcoinApi) { + _ * executeAndResult(_, "getbestblockhash", _, String) >>> [ + Mono.just(hash1), Mono.just(hash1), Mono.just(hash2) + ] + _ * execute(_, "getblock", [hash1]) >> Mono.just(block1.bytes) + _ * execute(_, "getblock", [hash2]) >> Mono.just(block2.bytes) + } + BitcoinRpcHead head = new BitcoinRpcHead(api, new ExtractBlock(TestingCommons.objectMapper()), Duration.ofMillis(200)) + + when: + def act = head.flux.take(2) + head.start() + + then: + StepVerifier.create(act) + .expectNextMatches { block -> + block.hash.toHex() == hash1 + }.as("Block 1") + .expectNextMatches { block -> + block.hash.toHex() == hash2 + }.as("Block 2") + .expectComplete() + .verify(Duration.ofSeconds(3)) + + } +} diff --git a/src/test/groovy/io/emeraldpay/dshackle/upstream/bitcoin/ExtractBlockSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/upstream/bitcoin/ExtractBlockSpec.groovy new file mode 100644 index 00000000..7acd5a18 --- /dev/null +++ b/src/test/groovy/io/emeraldpay/dshackle/upstream/bitcoin/ExtractBlockSpec.groovy @@ -0,0 +1,24 @@ +package io.emeraldpay.dshackle.upstream.bitcoin + +import io.emeraldpay.dshackle.test.TestingCommons +import spock.lang.Specification + +class ExtractBlockSpec extends Specification { + + ExtractBlock extractBlock = new ExtractBlock(TestingCommons.objectMapper()) + + def "Extract standard block"() { + setup: + def json = this.class.getClassLoader().getResourceAsStream("bitcoin/block-626472.json").bytes + when: + def act = extractBlock.extract(json) + then: + act.hash.toHex() == "0000000000000000000889c2e52ca5e1cecac60bce9a3754201a7a9a67791e90" + act.height == 626472 + act.timestamp.toString() == "2020-04-18T00:58:46Z" + act.difficulty.toString(16) == "e9baec5f63190a2b0bbcf6b" + !act.full + act.transactions.size() == 1487 + act.json == json + } +} diff --git a/src/test/groovy/io/emeraldpay/dshackle/upstream/ethereum/EthereumHeadLagObserverSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/upstream/ethereum/EthereumHeadLagObserverSpec.groovy index c25b0e2e..d1653a6d 100644 --- a/src/test/groovy/io/emeraldpay/dshackle/upstream/ethereum/EthereumHeadLagObserverSpec.groovy +++ b/src/test/groovy/io/emeraldpay/dshackle/upstream/ethereum/EthereumHeadLagObserverSpec.groovy @@ -18,6 +18,7 @@ package io.emeraldpay.dshackle.upstream.ethereum import com.fasterxml.jackson.databind.ObjectMapper import io.emeraldpay.dshackle.data.BlockContainer import io.emeraldpay.dshackle.test.TestingCommons +import io.emeraldpay.dshackle.upstream.Head import io.emeraldpay.dshackle.upstream.HeadLagObserver import io.emeraldpay.dshackle.upstream.Upstream import io.infinitape.etherjar.domain.BlockHash @@ -37,10 +38,10 @@ class EthereumHeadLagObserverSpec extends Specification { def "Updates lag distance"() { setup: - EthereumHead master = Mock() + Head master = Mock() - EthereumHead head1 = Mock() - EthereumHead head2 = Mock() + Head head1 = Mock() + Head head2 = Mock() Upstream up1 = Mock { _ * getHead() >> head1 @@ -89,7 +90,7 @@ class EthereumHeadLagObserverSpec extends Specification { def "Probes until there is no difference"() { setup: - EthereumHead master = Mock() + Head master = Mock() HeadLagObserver observer = new EthereumHeadLagObserver(master, []) Upstream up = Mock() @@ -118,7 +119,7 @@ class EthereumHeadLagObserverSpec extends Specification { def "Correct distance"() { setup: - EthereumHead master = Mock() + Head master = Mock() HeadLagObserver observer = new EthereumHeadLagObserver(master, []) expect: def top = new BlockJson().with { diff --git a/src/test/groovy/io/emeraldpay/dshackle/upstream/grpc/EthereumGrpcUpstreamSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/upstream/grpc/EthereumGrpcUpstreamSpec.groovy index 0775bdbe..fe3ed37c 100644 --- a/src/test/groovy/io/emeraldpay/dshackle/upstream/grpc/EthereumGrpcUpstreamSpec.groovy +++ b/src/test/groovy/io/emeraldpay/dshackle/upstream/grpc/EthereumGrpcUpstreamSpec.groovy @@ -21,7 +21,7 @@ import io.emeraldpay.api.proto.BlockchainGrpc import io.emeraldpay.api.proto.BlockchainOuterClass import io.emeraldpay.api.proto.Common import io.emeraldpay.dshackle.data.BlockId -import io.emeraldpay.dshackle.test.MockServer +import io.emeraldpay.dshackle.test.MockGrpcServer import io.emeraldpay.dshackle.test.TestingCommons import io.emeraldpay.dshackle.upstream.UpstreamAvailability import io.emeraldpay.grpc.Chain @@ -38,7 +38,7 @@ import java.util.concurrent.CompletableFuture class EthereumGrpcUpstreamSpec extends Specification { - MockServer mockServer = new MockServer() + MockGrpcServer mockServer = new MockGrpcServer() ObjectMapper objectMapper = TestingCommons.objectMapper() def "Subscribe to head"() { diff --git a/src/test/resources/bitcoin/block-626472.json b/src/test/resources/bitcoin/block-626472.json new file mode 100644 index 00000000..1bbf4181 --- /dev/null +++ b/src/test/resources/bitcoin/block-626472.json @@ -0,0 +1,1509 @@ +{ + "hash": "0000000000000000000889c2e52ca5e1cecac60bce9a3754201a7a9a67791e90", + "confirmations": 2, + "strippedsize": 800464, + "size": 1591897, + "weight": 3993289, + "height": 626472, + "version": 549453824, + "versionHex": "20c00000", + "merkleroot": "7835a26b6c9316232f8194c6b64b0a366e26a9a14e6a7e7409c34594d98a83c2", + "tx": [ + "02a920f009f7cf6d029dbb091d43a9a856fb048a0240d3361594129229786432", + "a18629f88032454c86b6271f5863732da2f567b8c61dd78ab638c9432e5f4b1c", + "3413673b45dbb11aff3905fcfd56cdeb06215b93626a026e1822b7b324799a55", + "07832210310dc67ee3c3345ba0ad233ab359191d73bb1b9b0402fccaa6fcfaa8", + "7af0927ff75c10972f68a7a8fdb0055e3a693e16b5499e22cb33417c34c72cc9", + "383cc7febd7187a9257c5aca1033f59137d91fe17b4f4c4dc5c7e05e49ac99fc", + "c25a3ee7946583c1a10fe16826027e65a566292b0441e49e9df848e15ad16687", + "a52986ffdc8a2e6fe387a82196490c14c17264042e4f946a7d4de2e0276bd6b3", + "8bd7dd502a681e318a2fad8cce504c083c441cc8bcd552499f568ef0477a05c3", + "2a8c16889b55dff181c90891b3830120f586df386cbb40446a58b476cb9447d0", + "47a7cf89f1878f9dfb473e83c28748ee8e3d99502109085b4a1ca08db36d70e8", + "8c0be59ee38e0a31df1fdacd91b61e633a92c61669706c2384d5060911ec33f2", + "0c1eb0039a1257762eed60d6ca54138c029176fee1fee8b4de255f7a5865daf7", + "1a0431dddbb0fb202827b825e6eed389acfe2739b58d244abe145862047d1934", + "a56f59ae16ce8edf52dc8532a00f9b5b385aebcee525bb217a6094fb22a72701", + "d4eb304b24c52c41f106dddec72c0e9b224aec1da9da66e17a471a60e8561ef6", + "fc7b4ed728050ee1fad5e3fd99fbe9feb67895177027dce3f82d8348a6c8a80c", + "7a16852effc2bbdc629ecef201f4b4f742c8268afa5f812994fc9e213e481948", + "834c0f500bee28a31a7a9b55969dd4cfb1e8a2c7ee15c0c013b1f606c11e1837", + "4c2142d011a77e303dab6a525e9aee38e33ce3756c34eaeb2a482a4c802b455a", + "2b2d445e951936a91bf3b74b7ebfe3ba36967ca55032a8805e5e7dcf2ed2e490", + "982f171358ff1c8d2c044d5a37368ec23557a4db61a755940c7651646a064755", + "ae93dda12f99c93621b75b1147e1233a7ed1e1caa41a3d565ed07eabbdaa8565", + "4effe153f21f84472a79772cc4d788411b5ec7c09b55a3a4f2abd471e0f4e45b", + "9458e9f4d095dcf76ab12358af95758d4e27ac9cdd2d6597967e1a0f8f9cb7d1", + "ba9aa701ea10d4d766d1b1d1b1cbc7a38e799a6529194c74a7a8b086a5286477", + "d0c1f6f4d4a2c737b8de6925675eede86f3f4e4259f51c3cd32ec980f143c412", + "37f5301eef170ad547a797439bc456d9d3f39054d1b48af8d182365026c1608f", + "8e24a44cfb796c13f3159df8d53243dedfe8abebff44a7119648b4c71bdb18f0", + "4b2aafa57b56a3dff0c261f989be6d7ceb7f969b82cbbf0c9e82f72fb8f60cec", + "4f3f6c72ca4b17849a09f2b56e94b7d0fcd70ca51ae65ef7d9761b3d624b3087", + "3c64109774ae9023dd20b688683583ee59f5d3967f5aaada3ef83b8ffb9d1e45", + "381487155a1cb39688e832b1603f015fec9f7339ac68925a586c17f9285b3542", + "ab22a95ebd1a8cc4569b83f46630451071d8d9121cab6d9c3a712eebc1faf89e", + "783fb6fd58f28b68fb4f40ed9c13ec0c1e6c43c625751f68b18c799e0fc0c854", + "e017a20991d20b1854d006bfd17d6d1f7f8d8790fe4608d528174fac360ed363", + "4f1cea1b0545269298293190d0d6980ea228616f52504986c976385600b61b83", + "051d5877902319ff31a5b8e1d7c12b0d0a0d606392acc2f72a84eaf268e052fb", + "5b07d8bc3982e143441eb4b69004acc750917884e38cae459368d70b1aaafe32", + "fafe5de736267255206bf2eab27a3dc3f6911a94300c63736621c4c18303c6fa", + "23e9de507203a7b40126b88f373043f541fcf56de449c10f395f89d0f31d7a64", + "9fdc988b5a3413070757135bbe9e0d39d44519624248839373d7770c2ab7d86f", + "35fdb0c645970207d023c390bf1bf568cd5336a00f518f29069e7bdd5a3803e1", + "fdea27dab177a54da06c83825e0247d5a9e964c5b78cb65428a12f340c479683", + "7b277ee3c3bc107552b469537f7bcdd9a62bf5e66b8aad9eeb9a86bc6510e194", + "4719c2716a141213884e46cec871cfa9ee149e3a2678eba57dfa3cefbf0ae82e", + "50c6c7fec8a63021f0e115809a3a3c6f8a32191f610bfb8a5ea0f17f6e5d8770", + "514e239c74a60765e299f5fdf9a2499f0b0b8e46dad9f3e92d53e7090c7b463c", + "c5c1ceb288a50024cd7f347be2821eb43ec157a61f9ff286440d8615d77e3e04", + "d11d61276e23161180b600666d1c776607dd24b9f8755042cf6c95b2512dbc69", + "a53fbdb36c9b8ced6ed653fbe950c62658ac7a6e95f59cb21fcf5339b75c9e48", + "f9fb12f196670fdf6cc8d4b75df3e41c126ac96e11e4cbf9136de572b0fa0ab9", + "87e0b25352aba5c588be26c2f8d45344427c0362b011d299ac239111b48eabdd", + "7a3c80eccd7451e70293ef3c61e40ffb5c7bf07b68d4be4d73de5569120bc7bf", + "35ab541484fbe5488c6387687b45f069869f1a568f40b3b68026a7a7e3402b95", + "ff65c703ad43a8a4f7885274d3969308ce6c13ddac7a7316c88b8b176eda191e", + "a92ace1345ef64507ae186349ac3c7d2d6c994e57fe19e0ddab49fbf13a12928", + "96aba1806bbaffdd3d6b742ed34a832543e9e3d386410591ff3d71835c010ada", + "58a82a4d55abb6ea7677e0d2c1718a8df7dd1d4fe540415179ecf313a0cba1fe", + "58d2296063140bf2935c055be7ed0ce809d90be414e00c344bbc5172ddaad852", + "d4ffd634bedca4513b98b7afef827569e09bb772872859f30cceb2acf276c66b", + "366eaa1795fe7fd522ebf649696f835f8c57d4437eca1dabac2e64084fb04bc7", + "cc94ba7c6ab0f1bce010b8b366e5aa22baad176ae087e57a391be2cadaa9f1eb", + "f56f607bd1eff5741c23310af1928f6702ebe30a51a8da377c6b40d174caa836", + "16bf2421c10b41cf73426146f2d7a7c1c5e9a916f281579bca5d44681d45dca8", + "1d65757dafe6245868f76ceb48d14ab9f4d7d252fb974b501d638d537cbf2012", + "0cd8d7ebb323503716f8c72ab35b546b38696f764a38a454c03cbdef0f450964", + "391e4d2b88f9393f01d6eeaa3618f7aca816f8a7c463fcaac8c74bef3e932637", + "89a131044b15f314d3bd5e645ad945edc55319bdf8b13519d3f8724d5e3a4324", + "1ee2dbe775700359a617c5b6380ef256c22d2b80308ac87bac25769e3f6f0af8", + "26e7796190265500182b0c21f5e2f322cf727e30c6abb5603afb80374cdb9f30", + "2b98978c207b68233e02ca0c5f7d0c6e54d4833977203b8a409c05e4428682a1", + "ebece119f96aede36133a88b935df181012ed8c8825f7ef788b1cbb681dbb2e7", + "3341116a1c6e5e6e4851d4397325dd0d3dd50f4cad0a7f7ceb807bef1f190985", + "4f813a4a6ca66af738da7295475aa922d60bc167cdb37dd6424e912f9ed47fe9", + "f096008c127a0565fc17ec51b4717188ac2528343281cd9a438ba3f282ae5ab1", + "0a4b711ee4c7ffe224712d73aa0585af4248eb71addf4a72c72503a2d4463fe7", + "8012c7a95ae65ce9c6ba1250e961f8ef4af314b5d6b8e127c90745e046ae0c26", + "f06a61c4f0552f4dc5edf232f882cb607f5ecae12ed329561f79b446f5a55f87", + "290b1685a53c60e6350682b38035ea8c027e5dff1db3249f2d3dd5c4794697c6", + "e12c183dd5731e0a2f21ce11ce282ef996b1c831a04e5dc883e13055ce3b72fc", + "aa7b20faf2de5d30fca4f38c4124437bd7eba9b6761d7339debb3778c1bd6884", + "c0451d1386c757d326716aef727d963ee29212afaa95ddfe3426b22c07b165fa", + "c086f865233d5eff001e9c1eeec825a960347006e390b53238ebdbbdf1e71aaa", + "9bd927d3d92053f802cd726bf36c6b4f49f4d33c0261ee1bba0be84186a5afee", + "3fa156b2c36aba2ddaa3519f5661d382efb98450f5fbadb9b2f2c46a327ff6e6", + "6604979d7aa0a281bf14776f4d9ee2a55954b165e7c9928a748fc164a46e90a6", + "f756f36ce8d044ed7d5945b644649fd4358a97bc63826c0fc33acbbba86f25bf", + "af64126ae1679509ad9f7b526d0417804b21ccd311fb31a17f00752eb51f0a22", + "6843c93732097af4da88c56988d8e08924a39daa5904e791575d2671305bc205", + "ce7d658b2c6e52a1450e12ad3bf1c7872520f5b5e8a7394b2d48f05cdfd39878", + "31300346eea7e5523aa99c29a2e4c9169d65a120ebf49c663c4ecae447426b2e", + "5549db755cdc2a6cbada2916dcdbd34ad2a3e6156dba5440c6a9fa8f4d01cddf", + "a8b8cd1b90b7289aa96ebe79715f21e8275ddd9c58415f0f1134164ad805fd05", + "339fd5d8842800994abd194f105c59fbd5c7edd35c652735ee40e3999735f80b", + "4c211bc1f5ee923b97aad3587695d3c173644f4ee522ee0f9c2d5569d528bc96", + "ef02d815060d7de45c7ed03bdaa83d2448e0eb1dd1f9ee2758f8b1b2eaacbda9", + "4c2e494c91478d0f83b22cd8244aef5e838ed3e920dc6d33b278f26d715f7597", + "0836aa1c5d8acead15d3830afad798a0fcbba1cd3867d26437fd56edc4e3ad04", + "f13942aefabe6b0ff1aa32881bca303f1144ce77c8c0b18ba94a9e6061944a1d", + "cbf48bc62143e252ce833b261ee457dbb558f6f4c0f95248e98bbe94a4dd04f6", + "cbdea21e1d1c5ead0baba3dd5b48771f362eada2a2abc072174f7dd5a50eef01", + "cb8cbdc6a72cc6ea1abb4f369ef962383f8dfb815225f7ffc3a59140f5a63ded", + "bf855ca1a9018f812fd5cdf150e6227c6d02b5de4e6f7f3479c10f4e95820b50", + "56a40c43410442ec2729674bb47b387fa84a9918e0774f65de5fbcb61428ee16", + "d7413d4ba78bdc80931d1f420466e9a7b5a269966b80fbdf8b02008f752d830a", + "0299ec3ad1bdb997020a4e1e228c837a86c54c64794009f8ee164f961ab79675", + "e7bc6594b33823b6895945c1f2e8d620fcd2f5cee35703931c446bb6d8988c44", + "ab653b60ab81ff4167d1fc9b687f4609975fa3eca4fcb2dfd6590a97841fd32f", + "7836acb4009436c89cf2a908bf3a0c53f40bc453498d01657cba1ba2ffee8a00", + "3079f73a6b19193cf4a4508e749a382851c4467e1ec6016d7cfc55cbf675ec4a", + "f27b68f83145fe2d9367b78f9923a4ec82ac395bc2001e6f8f0867be0a38d17d", + "7882d61899a335b012627ba8b62232d46f088e981553f857a05d600d97bc36a3", + "2c501fba304d9b6c4dc0d884d95bdf22608272373caa666fbc581b13f8a27053", + "4c1c7c27f6bfa6bf59e479973d7dc491ef1addd37085b9d44138f5050105bded", + "4d68fcd6da048a7e88c64186f1fb8f35c49f89d369116580b6daf4c9e7d4491b", + "c1185d9fe8a3ae97890105109e7079719b2f7e09268f6d96df8980f3bf20ef5f", + "0591d0931525d15ad7b2393cb05e853f443cb9be2ff00a7a270228e123df7962", + "3d4a27aa3e67eeedec3a3b5876eb31f704f04404048d55d6c8ba8ae11b309e4f", + "0c91a923a955092e23a8e20a29a895f3c1b6a2a7cb764daae3834da3792c2b89", + "5d45e8a94a3f69801c40379b47157ede5f5cf68959ed109a3dd61318c815b6bc", + "fa45ffc12affd28e1cf493f83c2057e9b3418f9787332dfa1bf447499d00873f", + "7fa88a89228e5eda6798e39d912e92a6aa1eb77e45d568a9a5e729a86cdd581c", + "0332112f87f1cd403d7f16f8e98af5165c472db42ba0e38b68b02bfd469c4b2d", + "d18e366350b60eede7c1873b74f401952146d6de4fc28e07499e3a716b2f076a", + "14366d88ad2be289c0b6a58951b558eb3edccded0c3cad6861c920f7a7d2fc94", + "29f405892878a6c7d9a9d2fb750f12ae5faee61d374897f39618903173f82e13", + "cd094a6ec25a1f823c52435c653b9c5e612dc0dc4a8d5868f31c6cdd333f8f48", + "eb90cb4945827eb9d246e8d905f055f20bf3b30a8fda2c102fe5e73dfcdc99ce", + "fe5a55791a67f67c4022f7dad5c4985b63156336d5c650af36a909ae316c9c80", + "9d2cd0c065f645f15f44a00bb238202f332ae9b67e9438693f78820232abd2f9", + "f84ba45595a56f194e9ee7f12ccc7d7d9c6e612f689ee1876578483f6a8978d5", + "fccaf16f0b14611aa6db828fdd303d6204806d603c9ba315beab8d339dfad3ef", + "4daf74e5cacec1c51f6953a3242622d4af088a06ecafdd83963634bc3d8fe783", + "d3a830fbb717a1d86efee3204722ac7211d69ca357945681b190351be05c5437", + "c0caa3f7f0d4863da02f73661346d08990680b9cf7f606f81006097e7dac5dd6", + "40c177b8bc7710bab88a79d7bc0a99544cf122777a31f167fbe833fc65911953", + "30575c6e48f1f71d1cebbbdd30579ac054e72ea78dca661467dd2f5d19ef206b", + "8b8d2a815f93e5531eb32bbed568b881ab15019c98afa825926d928c54786791", + "840d753f6a69fba08b84ea2bd888d276e73ddd1b7cb382e2220c78167fad01ad", + "ca7965779ea20a987897ebfba08d7205e1b0a8bd93d310b6fc0b4fe94b931bb8", + "a2556c24fd1b853eaad7a62be6ce2bb611af769b35b18b6d8e88afd3350d5220", + "f65873b9b82a1131d10f412c953b23bc0e6b8fb7c105790654e5d48c0855793f", + "60b79b8f53decb12d2369b84828c31c5a9ce149f7a1fb1fbd5db2eb94d6c5af7", + "5fb1dd394ab746544c3ab19a6e2357a6f6bed8fc2d88475d35fce2d02e20dab9", + "9d7a8849abf69c9851eedc3e94f391f5f35b450b16eaeafac990633bacc56405", + "bc6fa1987f441078a4309b46890c49dbc9979513dcd9c2201c4e3462dac73876", + "d182bca4b43eff597d408a53313ae7d4b3c8f98e1df4155a4915c715a597da32", + "a6b61e13e44221f5829c29551f7b185c0c49b937d5cf1d790878d4a3b0a1ed70", + "464fb5c5b2112053280a3024ff319da3823213040ee4a3a4e69010f0b13c7940", + "2df9efc3f00ef02a8d325c7d23cff75966bf2014744fb169ba235ff1b9fe8fda", + "b9623f1dee830ad59c82131c7de9cf9a3a3919e2be72e5f0ff5fb8393b75316d", + "ba69376efe5164d8c301bce6a2aa26bc326e716b243aaefe801b83ead6bcef68", + "996793985f498c44f3d70fe9e4f1aff763722b9bb0b2bc64a361799947e4e835", + "2eb076a9d7616440d1ccbbb61bbcbdfe65c901356ab8070c53cbf05c97c729f8", + "b205fd4409eb142a0b2bd1e227f9c7688e39cff134d6ca29b6bfaa868c4e018c", + "2f2d882835304ecb671aa783f79abc24ee6e90b7bc66248bcb15f15bb5a466ce", + "a7fd7f374157b5137cfefe5245b51cf4a3d25bbb7c7c170d4b2a1efc689fb00b", + "8cf252b39d9dad758e0ce045f13bffaad721833f669a74fa990a25ccd8acac3e", + "e27e34cf94826bf942bab06d21274c2e2c06301cd5d8d930e739a37fec219668", + "d2757efbcf5070f0fff47961ce4b95620213351e5bdfedc127f4d2d694511a66", + "73e4c4515fe3e73c5e1043c03534dab3b13c50e99febd19e63091d8e9cb80309", + "2ac8672ebccf140cd154bd9fffeb538dcd4cea91fe97d8dcdc12e8a0e0788a81", + "05605b6d33069bd93d12d8d7bedf7b28e39fea87ee1effa2b16e28f1ed723858", + "2b95b7f7628a4dbb9cb8dd0175949f4e9d23cc5b66a15b515091aa67bd320b25", + "2b41d370b84c59ab9032652887eac1dfe4ef51e2ab65e3f21282ebb93f3fb45e", + "ad313833200129d9f5b8be0865e93a3f511687fbb8c3a5ce963b63aa68a51145", + "95042151d2cc7957a491172dd5c2bbb551de6ecf99902a58468022dd5dcf521a", + "3a3e69f16dae69a50b0a313fe461431fae8ee4b3ee0435320d19833e941c7cf4", + "d9fe7a72eb37b123f1c9395b88b2482e4066d2bb08add1babf3536bd332491ba", + "8133e68efef108f900c12dd9852468f6a2d6abefa5fa17d4153547837d27645d", + "d434cb33ed489de9b11b2f5a675468483a3358af5938ede7e7429770a922587f", + "cfc4f4aeea513b0af02c943e0c34cbbb180dded09bf6376d8ff5c1b3589e8b54", + "811fe10185589fae68c64139889b697e8462e260a8474ca690657d407eab6b31", + "0ff92445b4ce0a91d2005e2eb82f4080bbad1a128ddc471c828b4d1c757c4653", + "5dcc10509d8c41abc30c592a6d8d8511b142cad05d19cddffebfbfd6493a9aa5", + "86cbcda7cd792297f67b5f442a218d5042d5d1b481189ea0daef414056e4cd42", + "88052aee4c5735819e8256a23ef8fce1b1349f4dfcbae28002feca2b14e22b7a", + "85a1c9e69f087766eea1f3183d50941d6027b1355d8a67369aadcbf809dfc637", + "74c4fe9ab823e498b397509a1da3bd79f8caf2894aaf7e233172ce2f1a09d6d5", + "17b0e1ae7b4a909f0d13275edeaf0150dbd1bf2eea2b4098df38efcfa4094210", + "57efb6434d8d92043ef69c11f1ecb34d1fc97f54201e663c3e4cd8c84e772d4e", + "c1daf39c95b26410b4a7917cd640d4473d2bf18b256a021c41bfc376b875b9ee", + "0304aaaa827ed412d986e4f43f20cf17bfd216ade18ea0b4e494c24a4614594b", + "b5820b2649111a63b25fb161147498691cdac0aec837a55d5d4170c06d8526ea", + "3dcb0c6ea74fb14f42e4137ad3b6e574d92494f2fb3958fd81ac485510096792", + "d9e0f4cccb9251f2179af392f063a74e00a8f5b842b2fba28af4004ee1798b8a", + "68285691f7ef141c68889555a48baf69d0263e55120fe6304663b1e40c868bc6", + "63955acedf5db881867d2f80d88fca3b09568eddcd0f4613cd7542d9333ecbf8", + "b65e4f1fbd1eddd800a47ef471a82baa2f269ee12584844877dab8010b5e5a83", + "2431db053dd0f9fe557d72496b60730726a44936aa09b64b5329e21c52e08c91", + "701a492c7ef65fa32290f9b24e1d1271535e6b14a6a88e7f714819f91f4c56e7", + "cd756d6a40f2555216f197272ff52ca8cb513f0e2e93822828468a47e3467abc", + "f8193d7ef046a67b758b4c95fd377199df9056e839f755099cb052110a1af0b2", + "7aeced62ff9703905e8884bdf618dd9746714d271060faed2f19c1d57f8e8826", + "a9b73d665c532c2c9dc13753335a8ea0782991740fc7687485371532e9c756c4", + "2e95ffe8df9abe9a8366867340962d72d21605413f8b0e893a1b65480c9fc406", + "1e2e6ad2d67e3fa015202234b35a21bdcdd9a0f44af1a800002188f938de0b52", + "e247cb56524dad29cd6e18fa3b7ebc35f1e4511fe8ca1d38f22d455a1efae2a6", + "57bbfb9cce58c86d56e9a85cb63d147361eaa7eea334b71912699f4014e2998d", + "fcedc9412c087616f70bcb1784e15bfcd997239fa2040324f6af1063b23b23db", + "d792cab1aef053a56c7c3d366a7f9160a38b262bf6b24cefad315d1ba19f37c5", + "54d4272502e2ed75cc84cdec3b411e3a6bf9ea05b08842015bd7c6d8c3057691", + "cae638378fc8f86e4e5afebf995dd16f4111789b49f16e76e67d7510be0458db", + "ed13b30dbb444a3ed0adb4cdc196c479f455b66bc64c5f878efcec5111d1dfb3", + "52e7c089d7da5a646816e2f767428cbb5694998c895bff3a33370c4554377ebb", + "05848f1641d466e40f4bd4ebf262b5e8ab829aeeef0fec40a7b866c0257018ee", + "b02b8baf7717a4914318b09bfcdfbca315168c3a167433ab492f0d95d29fd935", + "0040aca7ae8628785dbf4edda585859e92e4b00c0961575a3dcc5bf1eb28d5f6", + "af1c2b8554954de3d8c89da43736a0838267d50a22c06befc48ac60af25f4720", + "0fc5ec85399b8f6b19f4037a75c305b2fb3849ac2581aacae86cae5ae3738a8d", + "4d9635ed8b73678a5deeebe53806b325a0352dea7d1fe7c1a7480537168d3676", + "8883f65c07bd8190d31b7f478126b35b6d1e14be5c86c048c4795b3c463e10fc", + "cd8730666bce497358ad651bdffb340bbe7eea42e0b636a86224d9e47bde1a06", + "238f13723a9e8c3c4a3165d1018f84f96daf7f7310c056f7e0842bc5381eb6a2", + "23b5ef7ff2c8e034823bf0c04a904dad17eb88453ae6514b922956e4d2773c0b", + "a8f6cea35e29c6938c996db910c014e61b76e0203fd348df2be832973412a994", + "0a08e0f42b8bcd074db84bddad9aa24ff5fa9cf9dafeda383502b6fe6d1205f4", + "cb8afa6282f194fe24728e31d211a4b55c4e82af482fb0bbc2c6e101b751bbf3", + "5c64c563190b5fc5d17f2bc55ae9be5be67a9dce1470d778dcb428dc38f9ebfe", + "51d226751c474eade7be87137bdc33dcbb4e0786ecdb5b6ad1cd13c4acfe33bd", + "835f5004783a04f98156a3a3c7e670f4c18b5ad6a2fc967c317f32c01a51e4fe", + "0aebfe196b037e5f2c5387189ef00f3c073ac17d72a4d1b48a7e2cb7096ab0c5", + "453fa4c0d83f8edcd2c9b644fbbb467282977c11e31463f81f3a8e5e0ad16239", + "df6ce91d1af4535446fd949700e9230c28d4d32b6f4fa2a97e15594d56d6acd2", + "790a5f0eb594d9427b1e2a426dd16eb4675f3d54aac4ff40201beea8f9128d77", + "c7a3848b2b56059407d8e260ded1c866440b40aaec8b1355f6d8954a7fef43a1", + "2d7e01a20f4137ebc4ae3c4d0f3794514f443fe260b50491454414907a247585", + "37f63b730765561cd8ccdfae8e9d73059e265e0ab27afebe726aa3bb13b9e6f3", + "2e87608b23b867115a23b0f191dc43d14cfb68752e29e915d579257489422f75", + "b38eeb421ecc1ada2f654b115bbddc7cb14c3b7780379c6be088801ba988d44a", + "0df1e4fc7849f383742215a1fdfc56a23de8e4b28c7186fcd682aadd5ca01005", + "dda75fb6ae50a684e2654be480a74e976d66d9978c3d5c2011bce0afca192455", + "36b0285f7af6d7f91132d7136bc83e1d61bfb7787d7b6623f4b1eaa08a798399", + "f05af72eeb91f6bc389ff4c1d1040c1479705fa0c9ebe9c1abc3130ebc6f1855", + "9c7ccf21086710593608e5f293f7515244a8990587c2a2b08bc6fa29e9637856", + "53b60a07bd2269b05f1468c684089b5b154a4756ca010ab95ffc39e8036fbcca", + "269b89fc74646ac4e425ab96090445e931c9c6c15f52f55ef4dcda116abf96fa", + "a77d6c478629a95dc55e852ee4f856cb9262f49c00666957ecd9faeb7090da2b", + "3feb188c9ce64048d0b6080ae4ec9537b7f66454d699d4ebb7fde2edf6089e67", + "067e674b06a23789985cf45ab6af5001263b1957299059116381d04d778163d0", + "b0da138ad71d5492e0970c883b6b4aea20690e0f0ce4f401cb0857db1ccbdb39", + "bd49bfbb25ef6f2fdc4ebee737f47a54bd89a2fdb72c1f129f17bd9f2f5bc877", + "c2c216cad8bdcf81ec07250f17bfd86272395f5ea4f633bb3433d2dd9e226a23", + "6e81b358469e4df1051298a4ddcbda2ecb8a1c17ceddbdb6bcee4e4f2e20f53b", + "549e52b4167e8732ec33c00688affdeb77a5e3fe33fc29b80af239df39bd041d", + "0a8d0cbd28a010e1c3eeb117047cf9a4f2cb6af0dc6941a8407d646e98227114", + "57550f44bfe80423542e3e9eb2c6dff22577c39014ad9e22ab171a6cc473e7b6", + "0f565b4ef5c2fd82543878ee98cca662f9eb5f28b67a6576a808a5ff08a32a49", + "faaa685bcd5c00659f58a612cdda9f46d27cf48df8e4cb4fb234c75525568d64", + "a24aa7ff73f48fab8cd57c45cbcfeb352ad2fa3f96f7e8c9beafb62081eb0c9d", + "b5df4019d67b2b76cf28464745120da01a8c8a5779099e12e9517fcd95eb12bb", + "7ac40c2a59a5b57ca3517570547c02a9aeb73e6468c4a70360d4746f0dc1a1af", + "d0864a47cb3d2e48d9fc1a9dfce56608048a4120d992c2300baa0776161a090f", + "7b2504823be90fa9963ced33dd7b536f695bb35ba319a9a4df23ce7a38daa2a5", + "c8598f5c5b17d88856cb9ce80518bbaa334bc3b4de29b551c7aa4c49aeddc4eb", + "d201dab4b7a20c5bad7def71405d03e8bd04be94f51fe4890e1572f980ea6938", + "d195c78b8e77b5c4f5a7aba8f110dd49844ff55690334735803efc6de6f8ce0a", + "a628164aff760397c40cf6aed80623135aa62636d522ca5d14da9272ffb6df2e", + "e63711774f86e5f370ef7434867bd4d837fe63abbaf6f6a8d327b96e945baad0", + "0db8eb1fa7f8828287f95533dd354ff284dd75a0bee51cdcae0e4ee98f5bb85d", + "62b8144e51cfc6fe90f8924ea076e410d2f276284fe478f4670fd3ae875e9b1c", + "1952559ea9ed971d00bf402f2cdf5d84a6ad19601c1d56c3015f9334d57b3d58", + "cc307ab0e36dd02a9fc1cf9d4fa61338cf2c7e261140479853aa624b1e748e95", + "c84a153f6c932c56e7425e8a2dae6b11b4f5ad04ae1439c206bb949ac1723e19", + "ad98bf2036acb58b5d9f46aadc6e4ee3d79e77bc2dfa7405934be1505bba5986", + "8f06f5ddfa489545e6a9ed206666b354e5af70a8dd597c41f2549680c774ddf7", + "7e00aa3a41135ff546b3095c09706e9a5f0918b04bb9960d79e8bdd0c92891c0", + "9e05063cf99513e8f73df3d1ad127627751dc7db7879f3d637e9f9e96a3844b0", + "2697f883a70a37b61ef2ac8ae812a45624a2a5b0c3fde3b822fa1642e9dc9b07", + "2145083b9b7b573fc4d3217b9537b830be911ee729372f1aa07db7480967fd23", + "44d36319db44ffa6b33781dba5699ef1c0b85f62519fb27c3fd369ba8bd04230", + "fb941f217ff37a5260b6bedb5c3e562c21c17166b77918e2707cd238b868972f", + "d7ca4d52d50beeb6c7aa13a91dcce183a2e5add1a488ce5c44bb89421dff4183", + "69ae6de59754d04d6401593fc839f88a07e5ebf2eba267fb5edd07dd65479968", + "2e2615b39254393da4ebc7b166166605c0c96afcb289b878be47c82cd91079c5", + "1cbf9bac1ea423c1dd1c2a4ec3b6538f5ea307158e24f40153eef420a18cc4d4", + "734d8d86bce126f06e7fc576cf4c0349c955d3f2ecec0774ff8313eaa9feb3b3", + "adc7d1229d441e80e5a4f2c2621f00906a54e8765b9d0c06756497bd86baf347", + "5de13163a3a682b327310fbe795cd0cecfb535651b794561ba442a9526bac65e", + "b3afb2c95be6c801c566e4f71d409b215f3c5ffcd65e049836e484a67e0a7e9f", + "c6fdad6700bc15cdf468e427b414b12376dc2ba8ea34a724c6a17ed488c364de", + "bc54f167a1934455c643ae70fe42770684bd7508fe637db30e67776d2ef88bfd", + "f2915d9474eeecce6f0f73c6ef38400bdcba35e84fc96a4d63fe25e7b22a6853", + "f69b5a6050bc92fb3b061992ddbeae38b7539d40c8309ec02659420a10630fae", + "7c4562a193834e1489d27cd76d6be09a665335ba289d8eee0af8938509718576", + "02823e644a99b26ac109b62247c55f39106fe7d65bf29ce317e840366298893d", + "91f0ad44478e69d087328642f44225cd140d3586cf620a8cb46a59ce1585df4e", + "d88e148d0e8f23fe5d9ef813bf7e317303f407cf6ee196564e336490e6486ad9", + "e4e77753653cb929b119ffca71295ce9baed62dba679140583f8eb9580bb2bf0", + "454602d231e08ee932787dd49306062bc160cb439d25da51863d28ed48948e74", + "036cf526798eafc31809dc6997c594d1d0792f9cfc967c8017c3fc218f95390a", + "37a930a18148f30403335702f58a4f30059c2163f6128a52cbcb933b879faa81", + "4a32e84bab8c391bd64056af719e26f1f88e21278a319f9f212863cf202e7ed7", + "02fe79248d0704a57b587c6ab33d53a1c0ef30654279842aa986186725584391", + "e30d62e7ca862b3940c9c2e67a2570a13c7497b3f6a5c96b693380634e99b773", + "ed78febf8a5fe584e822efd627f4d8d00c0965da3bb383b5f040051469c4b589", + "cb808d017268879579e62ce056bdd6dd084da69cb94f5bb5b10233636180242a", + "a684c33a6cd5dfa6eb2e07c03fc7a750bec3be4da21bd234ad314abff81dd9f3", + "92802a76f649807b868820c936c57f14951cdacae639d3126931c4461dd19012", + "8ec67be421c00516a1f185b87adf87c04686e5dedab6301065695284e48b9693", + "6a7c5f09a10bf1106c9b48b8f2fced58ea1fd223d091ebf84ef02b6e3e8e17e4", + "6c43281d5c81865182d6921dbfb6e1425a923f17d887897d2e9133b34a8e4377", + "8a722500ae4647aca5dd3bb0e9560bd65fbb94829de2e80a5bc1baff418a9208", + "75a098acae79ebeb61a27fed5d64bf0c81e6d3bdb8d9635ce1d055477d0a725b", + "cdd6576b8ab2ce897649962acb9cd30d927b28cc72f5f93a6314094778c50983", + "6b15f3be1cb4f0feca25db432e3df80889f928c10f3af832b9a30dfe6a5adcc7", + "49417ff392abd0445f08990d83267caaec501f123b0ed354318a07d2cf631def", + "db18056c1ac7c0153df5ee4150fdf7c142ff5250eb578c84f24ee44486cc36e7", + "be8e389fafff8ba816f5f5eaf345f7125828ad32857b4aae311d27055762cbfa", + "7bade65e959e179fba24fb5c30455cd7c68b33f8c4c4b989a5639fafec63a4ce", + "791d49925a24d0bfc4dd14cf91660fc6498c2c409ef7c5ec019bc393dfbda8e2", + "76096c7268ce1f710ab66e6f4f71893a031da93a83225840267be1eb1f5a5c0f", + "aff57a01827ef03dc78b9935909c78c1632bcd68a08098cfacbf82327111863e", + "187f136635ecdd7cc80ccda1be758b2824641f952ff8c27ec900da7463c74f6f", + "bd99b5368f9cc482f5beb51fbbcd330cac27f347d6743d3f02e328639e2aaa8e", + "d5e608a6dd9483e2357792900e1bdfcba8f3f4fa795b0d00bf7595b51ff13d5a", + "5def85825bb966c9d07ca75a0810b2c8298dc8f5ca78381fd96a41fbb7e7439e", + "58f849abd816d39946324d4d69ea6c3700d09193c7edf4cf71b57ce20aa285c9", + "6315de0b506665979841bdd0187eca515135051af80ebd3c28b3157d635962d2", + "f7c7d0c9a51098ef7a88afb91fa26eb9a5372dd9837f088b518f6f2a42d203fe", + "8b7c93b904c75eb43e4fb6a12e28b8361b748d79ee7a53b253ff8da3d6572187", + "353f0a16fc71a75f9f4fa9a5c0c24fbcc804d0c5c67bb3c5cbfcb28baf6b0bdc", + "c7b84aaf53c2ce50d194487dadeed26f3be8a611801622ed2b2b56eea8c2e99d", + "d053ff5440eda0bb2a23ee8ca282ddb4be337c0865f811a727689857b0daa13f", + "6c80455f4682d8feb6da357c1b56c6d2ef01731ccbb8af160a1b2317eba7c0d5", + "fce37804eed334a469c558210390fdc88289d3607a9acababe923694f016aa34", + "c5bf1f9e1458aaf8e0d0d0e12fd9d87713cb0de610662297d84d12038c86c672", + "a3398dbc713a6104fa8a1564f33ff1c64bea743c048b2ced0b9d365a5e5d164d", + "805ebc2ae29da1e690c0212bcce2465930076406e306fecb86e9f56009a16104", + "091287e91b31a55755a0bbd49dccf468612e7b4637c1d3a93c0cbbf6823bc037", + "84e52a8d2a8ac4909955b48345f96c7dbe17cc9223177c1423fc1b4867ac3844", + "c801f345a0599f20bdb0c4f8786761f2beed71752df16b7efc0912fcf1e0f6c1", + "23754236336175907bd2949ff9d4f93f5b1c6d0e691d84378e51fc0295b80bd8", + "657915abf56e6c58be5787de18ea3bf3c5fd0ef6e0b7151ee0ea80dec3cd48de", + "2246aebe47666199fb7871a80f11ed1e60eccbabfa3bba8ff474857c95e7ac8d", + "3d39b74967ed1d5f5337addce3db38eb73fe7fa1dfa8174b0530ecc8066159fa", + "c4b6dc0833b549b7d04d8c9a9e1317c3aa0aab608079a67d2f844cb134886738", + "f3ab654470fa359f5b8e2625a7c4cce6d3af06f67c2d448dd0cf995cefdac0cc", + "c2eedc0411aa7a8a048727a17933a8c9c7bd057d0d4b8453783ccf1302593141", + "9189aa722b367a5e480dfb1d5b6706822466ed504bc7e2c180f1c3e946eaa32c", + "6142cf38e8f1f00684bc7ae70b1ffb5d9c6aa0964c699dbb1798ae548ba0ed86", + "127bd1a635369fd687dcf2fe8655331ca74b1088ab7a80fc42d86fb16d5eb2c4", + "2770927cc8a69d8466aaa8bc4672ffd68835802f2a03c965cce2ba7bfec24560", + "2439ed065b94a40bd61790f09e70a0b1c3d4333f8d80ed06d7031433477c3cb6", + "a4c33e31fc1d2aa3aebf54abb789684758eac98485b7e6a95d1ef6830a6854e8", + "87ece0425563efcd69314b1562c7fe870a4a2a53442311eb1d7536be5e02787d", + "e6668c7427563ebd495e0cd8210946d2243f45516c95003002ee0e17db9f2e0b", + "6305323fa3c528810dcf66288075c1161559994a83945e64f221adc249880823", + "d27353cf0d9bae564f3d7ef30666f1ced760ed204c0024ff89b39782b76aaf06", + "bf3629bad47a6964a36acc7fb7539c5058961954965b580fd0b0d726c5ac09e7", + "2dd321ef167b49c631b8913f3ff84439ff37d44bff0f6e05a58789f41d955557", + "73e029160de8d133f9439b4ec908919708765b2197cc0f0cac18e4fb4a220b9b", + "10133140ff78862ca8403623d3537b169082a04cbfe190ec2deb56ee3c490aba", + "59116d3e8e78700a44bb18db531b42567ac59fb3516e820acb3d5e5e722633ec", + "686509834343dca8918655554e612edf4c048229bae45c21cfc15ad60cbaec27", + "bc7c3e7075db3be4f351daf448649230aae381d4beeb74263e789a2dd695a4cd", + "14d28f3ff5cb82a142831ba41d151208aaade48598382bffef79c9d36f134239", + "e66cae691cc0c1b37d81d196f5f8ccba49a0eefe1c492b7abedcc8e0494f6c30", + "a5a8255f49b633a2de060dca272a60c70ed4dfae9340fe4397a8002fb9ab3482", + "9ef56b8629eaf42061907f733a783f8cfc983e53e45c51dc1d2cb17b5d2af803", + "9684668a6b6785c033f765aa4b10aaaa0c0d76f6dd7a3032497ebc7b26db976b", + "80b7d2b23c2d93dde7b05b90d847a2e092e0ff5cc4a2f1aa99b0129524614794", + "13fb3534d786e19d7490f3e111751871cdebc98fef45801bb11b043349b4a560", + "c54399f02fcd8d508fb8dddedc062f3d834f11f40e64ed7f2eefdec6c9613d1b", + "d4cfc118de13622561179347dba799d74fc9fb18885cd18f8948cc1cb8f9b721", + "b35798d1da5db1e90328e6377f0cc15735be8869990dba663ca0f3fcf2069674", + "4c10e33c50bb9f150e281eeef623a207ec71512ef23075dff7804e70810c86e2", + "876ec83034521e20827cfc83c9a8289cfd457b1f0ddd615d6cfa418b75b839f5", + "0eb84b6bb8fafede5da5ef9863b4ba5b4a366379ea6ef4aa3cd4b729188119d8", + "ac1aae82c8f59287101a6aadfb91f418f53dfb722cbe22674b4c4dfdd309cb25", + "ee866862c8114ffa0f80e885b67de3e4ac0ca550faa0f76735c57f73beb0b240", + "57097c4cfee79f779ef147c5ab0e1b3a357eb99cff3a4d199386a14a5f152d9b", + "f2dbe48d3d43bb0dfd08b916e7cbc399082b667450cac96c886b88ec62bb0361", + "5782b975c017d81f8a73ab4b5aea36f55ce4e68087102b871b7e23139fd66888", + "ce12543ed5d8a45a57f3d85196dd1e9ec8eb866b9c9158dd77251c198eea51f1", + "1f439a4e764a129c31eaccba08a549f336d664a7b0241fb21b7b8f9586663a63", + "dfc6d2feed6e58d0d9acb39ae2420f9a84901d099463e25c06b838c3f8e197f3", + "7a24bc2c3ded3ac4bb743a132876b576c8f39cc04f0fa7fb15c03ab931445a23", + "e5fe0275895b0692cd1da2f144450cb7771621551fd59eb5482b4845729fb261", + "0fb49811035c61e8792cb5e3537d4d162469e896efd5a85767c6bebc838cdb71", + "b947a48dfa3fad6c872e277da83ca2a066a52773f8d2d14ef7c67a1ec797f4af", + "bed4588ca73d49ad52c9e8ffb3a781882b1e8f7c34944f3ac1bee0e530c86aca", + "becff5626b6dba4645429658a950d5912fbf773948136eeda8d08dc3e51b1bfc", + "8f23d90c79c717c63741816898495c800ca3d78f2461a7941153a81ff64209c3", + "593f5a2e29035bc890f89e92908088fa4afca80f6ea4fca53888686cf14dd3fa", + "cbe9c5bc7a07f1f17cb959523ff68715fdbe402b6ffa8e33bfca257adf3e4da2", + "20c47fdfc7e19b6480ffd90a699eb41ef57d6b191f98853e3848c808df6f43f8", + "e16537fa5d6d93478d177171cea42de0df5a7cc88446030089658dcdc0b816c6", + "39ed0b74840e7992ad5335d0c29231491bbf4e568dcb06d1fe1252f1bdfd58e4", + "e34e7357949459317bb27442dc15443f7d67835fe7281d0dcae61a8a07884a0f", + "30908754089453418ab6b8cda218bf26152d7499a873f493ce495bb545e0c866", + "9486cc3c3694d0f67fc5c85b7bc4456e6390f1b55a16343e702436c4afef49f4", + "4351d30b76cffcdad38a6d287e7653b86c24562fb94f9201251f210b37d12409", + "aebbbbbbc0957131c673d232a1999814640185deee7b1d4331721f3cedd34f4a", + "5dc724987ac3c53c2e3aadd1b2938546fe843883cea2395f93517ae4a7bc5806", + "9922ac4d70ad8ed624f7f33fa48bfbc770fdfdb7abf22e385a516e93faf3d571", + "2292d69ba3afaadec3b2038f8b2626370db590192356fa33693c6ba7c324e4d7", + "eaab0f59b9e6e291179b8c557f82190cc363e0b78fa74ea0630abf5ffa8c91bd", + "a71caaf2f865965f79aa9b4f40bc06b3c1418f20e67657279f25a19a22401e11", + "29a437e4bcdac6dcaea8e70263be353ca4adc621c635de0a7aa1690db96829b3", + "ca1b9b3bd03d4f53785fe40dc56be5c6f12cf635a5d205d0c4b5f3d4a271d7eb", + "1e6a9f3e12eb83eb5681731125311d9a7930c78330e6235056055dc61cb6ab6a", + "8ec46cf01198ed5f69f1e05ffbf14f10c965f8891b34d78bc948847ac09187ec", + "a14a5577783f414846b9c97b8a4b0a81b03631bd459429938b2fe5f0951195e5", + "7b5ca20e14ecec5e88ced90f48576b6cacbb471f2ba26e6651031e07b50e8988", + "7da97a08bcbbcfe1cf9b3cb7dff03d57106c37e3f3ac9852c6edcf077f47ef0c", + "2b18467036276a91aeeb3b28c5e664e00041f79008e7aa25fa26c2daa8dd3abb", + "d5255014a49df09db8c14d6bb608c92c0469d64ebd241ef3c33287356ecfcd1a", + "f0f1d0420065ef2bce25c460167b43c03fb49305bf144af75b1e06ffa1099e45", + "ee3b9ff9add060466e8b14603fb0f75c24a8dc6058b2c1c780fff46501d8b4f6", + "2272996ea75a6a8309977443b4bc8c994ce2529c1e7f5524d818c220b0640560", + "29988fc6ea1a521e8eb8b1800ad9c969a794a891e284f8c9e98501ae2607ab4e", + "31e889b7f5582e575f612a090d9e157289fd9a6d4430b862fb89d75a7dcf16f6", + "3081141a8c18a18c39d90a594e786a9853c6349f1ac7643eecff60b480fe5b32", + "ffa4796d714ddceaa31a9af3b277a43bc576174308504a67970a48286c79b349", + "314395debf0f2933dc9133091d0b787533ac96ba3fe02df60fcf466126a46550", + "3d73956679158adeabb2bccc0fe17b636b164fd14505f01ada277a803b59ec49", + "53aa3869adb50d7815d94b65f2841b68012bc3456248ac8bedaff80b6907c64f", + "61e6b45ee7cf1ac4e707f5d14d9523cad161311e57f9f10f583028a04d212bb6", + "67f978024703fffaf9e32179e14be06581bc565645f82da36f68db5bde3f49bc", + "c27774190526971033581ee4ecba95157179beb045d1c29a7dfcb3898a918fcb", + "3e1b2c05972b8cbf24215f3ef5a540936902538b82145127e5b4884f619a2acd", + "9003bb5420d2cc9aabd1a0ab35f2159b777f8ca20ee5df321cba7cddf55e6bdd", + "f72d3faff43c51301e447b978451f570f5836c3d465003906b1ee4626bcb1c1e", + "2cc4f30e52a9ad2a6c8e203b4a97266a5f4a5a800f5f7646fa7d6cf5beec6627", + "063d0a1dca5d4c34ea5b354e53469af5649f247fd83b8b6bd28cd10d5280122e", + "833ec8573177165ad97915049c3c9cb9d40b89bed416c8eff30182ba8a53b7b3", + "df74b9f55e5d8d4c0c1fb7ccddcb3ef4ae93e76745aa4a75dc30d048656d6a23", + "ac205116ed02c34537fcc4f5f918f0eec5c6e5d20e1ddf660ee14d2b67808dbd", + "05becc5ec86643a08b5b49dce8289b794d05f88b5f132555fa61b06485faaa52", + "1b825b1a1924920172afb647525b347b61cd62772963a44f7bfc0cc794276713", + "c43e5bc8f9021545f203d661796d8e26589c3a518e5b6eba86f0942610b79ab3", + "481f557f16b32a914d315411ecf390e42176febe4157e47c75d89566de4e6bd4", + "aaf5181f9c76c18b57355300132975aa693f4cf987511ea2f322920ac0aee2fa", + "5adc836559cd5cadaae4580cd834f630a49d78182d234e9693217db6b70881a6", + "eaeec0ecfb69fd1d07c9dd45bfa8da1a7af42684dfe2ecdebe9b134b3fa521f3", + "9c2d1e16dd54e899d4f4a1815264eb7c1904c23fdd4b24b653df9543bde3ee23", + "35fd9fa796ff76c41b51572c8783fd40ad683de338bb1eaadc26d77f4ebfeb1e", + "d88349084d549c0940e895470a09edac2f143031d087cb9858edf0b84e2d6aef", + "98a97493ca478fa1c0150db95fe6b6b5bad5d4ecc5716a1df2f8a64a19254b78", + "8aac0ea24eb3dffa366e98ec893fe01434e747aab02064e8a9882f053d36caa3", + "5cd8b1b23f3a7029fd694616968093e9ee8fe4cdb33b8597ad13b467c5a14105", + "c03aff7f1f4e0b3eba1ca826c855d7fd54b8fe277309ccb8c944d3325c42ad3c", + "9c99a7c681dc31890559b6582a880c1ddf0d760db321d11ca0763d8aa89e7ec7", + "7551442ac4938464ce1c7e4f5b69f6aa155014de11b5e524ab33e41ba138ba0d", + "2befe4d8321a2f51568a5fbe7622f46bc4b2fd1e37b03a3ca2eba122b8632688", + "30cb34107a06af4797a35e8764061a945f5dd1371d61fbbe86b3d5bfa94c07f7", + "6bf19359977c4835627e55c7a528f45a455c9a1112b1e28684eee44db6ab88e8", + "cb20a31baa04e08d49888582144ad8e1433e5fd18bd9d0087e0f1e9d7fd71e5d", + "5b84d53b5b552fb95a149ef4de05cadcc9584dbe52ef6fc74828e3843441132b", + "7ce61d6772bc85261b3f53353e5d9d388808324ddf7ad9372feeb230f00493ca", + "12bc3cf6e6f244325464ed6081439151b2a0470ae63ee6d674a94c977cbfd85a", + "b3d15f892d7bb9ac463e4d06886a80e980eee45fb345274e92f5d8c6217e3968", + "8c8c748aa5c76d0f8552fbf7900ecb87440d9291081a6caa9a3bcd5a7fcedb51", + "2c46542030dd8ca5258283f335957e884e649bea734276bd42c5466f3e2f4b94", + "f47d853dd3ce2f5eb0d03db6e430816f071c617fda058ae34a163a116125dc6b", + "2f62bcc617709d8a3503b799ae2addd7c596741061b3e94a6bd92d234245bd06", + "a581f9c07f7103b5d8a1b90e0807227f44ac8cb2cb7e560248a5d26b616c0b4d", + "7408ecb8e0e591f4f3bf5a899ba15f07ff662c12fc82971985330bbd1a0283fe", + "30bf53b8e6c626b3c3aa1ace6f4a78f06bf51d91d986d424282d19af60e4b597", + "719043406a29fbfa6382663ab347b1dbabd0545a5cb15ac9a07e7082952bf13e", + "34103b382edd88d82addfdaa5735bad96891a47ef4427305ce08e6cbafe0fca4", + "4a9c1422f75bb98b82ae0f9e04ffdb552b5737904a0bda64ccf6a35a23479041", + "1c075ec18c796253b9a5eb03e48fad37462dee22ad9432f851ad4c6800483317", + "0e0ce3c251a5aed3337a4eeabd56e7730222d6886a6129af505b09c9ca893465", + "02020a29a301e231a091e80ee30d966fe52d72b8014f68b5cd592bd727da0770", + "c3c02f6220ca5fc7baec158a9917f67032992e823a6a20b172f0a4215b427ad3", + "826782ca28dff57ad2f19004cc163052ad139931700cf58008af544745c27f98", + "62a7bb3ab04e526f000d40b8c0a8f09b38f2e057ec439886705f16c2bd5f64ee", + "b793495f8b093985635b19813ea384a52a20f4a3d5f2ab61a5924ca09d108f79", + "27c23712c8439da0f90cc748f5d0c1c7e76c291046ba5896bfea7e3e9d667328", + "6ff954758f3ce214f5d0f9cdbe9dd463e4da0ef0e3f4dd4c675d6b61426da468", + "00bb6268a859406cdbd6b243c988ad477151a265187827f4c6786ae36354d808", + "ee3de952054dca0afd97eff8b54636c6a05e7518dc0d121dd73b2c79ae55279d", + "28bc492de119d140148c6d930d086b1a27e4eec5794c0a71461e7c41443f00d6", + "8ef2d4b72d7ce8bd4eecfb25aca2dfcc7e1e91b86d7eeb0a03b094526e4f4be1", + "ded536228f61495c036d26c499bdf8e58428de595aac479b814d12262137dd20", + "2e4e2e0a21a46e8677cfd189d1a85d6307f9fdf15fe66edf9240df5675e7502f", + "1d23a162536bfdf378c81686c45c5abafb750dc45ae7fda79bfa90cee08ab713", + "61f69d4d477c72a7d834d2c33e29daa4db3577e6f5f139ae65b398daf3cae5ff", + "9bc5e9f4b1ec76ceec39cf7774c4d4cbca512b1ec24392d413ca54ce70d37231", + "7456777909e7c77237c754942fc8718e61e4fe316a568a084680fde215dfd861", + "2b951bc65c7d11fda698f03c478896d681414d695d008470324e9404d8f9503e", + "283871f7c05bbd5dc0629741a49de773df8ccc9d55cf9c005daf23234f63702d", + "dbe9f799bf009ccf56e015e12f34ab91ee79816e5efe7e07365285a3f6b96f05", + "a28ecf6c8cb55975eba95747ad4cba85b12fdbf25f3ff4fb54b35764f4d18f05", + "944fa865dab13fd1f4c58222c06d99683f00162f2a9a2a43f693d126394a035b", + "cf524007a0f6dd39f2f9ea47df23f8eaac63b6458b7186e6864d0821bda2bd6f", + "2923e189762373bba2cc5a8ebf5e4aa1f3b6b26f653705fcd5d3d8175ec6b871", + "2b1744f84c5d6da8fa220cb696cb5fcfdb0269dd7c6cfcc01f89692d63a3ba89", + "c3c5a49819617bb58abec82b57034595f9c01e9256e6d65fd17f4ddc6c70ef9f", + "942a6b03cad1f78bfc6b7622e491885a6a2270a3dc98bc61b1f18de3f67ec8a9", + "e4a6d2c68026d03a1a3295d1a952d63254ff034a42b00fba5271a88e885168df", + "e47fcbf90145cb502f31362ccdb8b51df54131b28b33b20ddfe2be1b60b6eef8", + "3016f5c43efef2803cecbf79f4af5fa8439f0c9fd9d191b773cbd0f47d88c21e", + "5a742d667194e2c3ff8bcb4b232a9f9c4a0e65675558682b4c230832a34df425", + "f48da638098f3522deaf18c842cc1eae33c42da35773187b1576d3bc60203b33", + "31e617f5f159debe1f2ee542762f166f915273868dc1134588e6b3d313dec033", + "10ab8742d4d25b40e47b0ad9eb35c49fb61101ad818d3da5bb33c8c982e88350", + "5106f500f66fc798f2c2caf598218979d8351c4f600deede3669e4a566901654", + "62ab1dfb09d0a4ea2d7e825b71a27cfabe22e8ef9c3d4903ec1c32bcc0debd55", + "0e45d431f867687ac88bf5e7cf02bf565d6a2df773d61124e4f4f0ef20d7435a", + "966db021a254f058acd0e4dc9e9310638315e8fb0e4eb6944bd9581cf0607669", + "2295c762587565fdb3d329c7cafeeea6120be02f761d4579a10c511a265b1474", + "2555348670122d192daa4e43cb807b2c18d395e22a4f22b08b920eea456c1b80", + "eda1b422b995b84b2bfc369a4fe085e6945fe767712902040b1d2d43c1e50b84", + "d09eb86bd5f1e050f392992b28188f22dc148c1af5534d184f6f8b6800645886", + "46772eedc561b26f78014f41f5c07b8a9785f5c82dd4fed064bab8d3e036288c", + "f1797ef505054dd1d63a0216bd3761c84ee2312cec06da79eb5ee818168b0aa1", + "eeb61172f764ca628385625768dce505bc1fbcbef180bba2cd674f9a31e01aa5", + "04c7141898869d9675652d6023ea19c9fbabbb8737d3457d58a59afff6fceeb2", + "f97da0645fa61ce68b0d0ff30eed89346a61c556df0def8d953ffbd6bb8df4cf", + "c94b2f1488d0ca93418e5a9bca9a1171954d1dafc9e65f56c230ec1e9e837cd5", + "9ce1872316bea53828a8bbcbe485a05b2efac6567307015bfe81fe8056fcdce2", + "612ef4b7cd9672bfe8f82bcb099c1dda401986ef86039ffb2c452fae42d4faf1", + "38ff6918010effd989d3bb101048bd7ee0a1ae194e9304745b3067e498b3ed01", + "c20088c8dbc3117c245e20ac53a84ef0b27164fcaf07f9c882e2e790e2d3711f", + "0577e91b44b6ed783bc45a501875852f817df1e9ef98d9a090bee82634454280", + "52a760e09a080956fb689093b776c9695ef0d08a8731ce463b3952dc0e9dd005", + "64bc069783f7484ff9d3d66e1867804f7502e5c1b529a5f043e91664850af263", + "1d426fa6afd66826721829e9c223e580ab62b59ce5937765382dd0f4af6aabcd", + "6515e9d71b71f8c04b798a4c585163d432e727c01502f8581505ffbed0d2559f", + "eb147858fe0a8c33c1bce36b22df683b38c8b7f3df4706780d5e5fa9a5e637b2", + "cc2bbedf47d08eb71ef1c8f40617edeab45e5be9d840d1f08fdae5506951424c", + "fc68a45db02af89dba6aa4235ca84978c421277a5810b84f510fe9a11367c87b", + "a344f017a36c61f398ea8f5e23bd4a3a1a429b76d1415d3161b9e11fdb924d13", + "ea2958dcf28beb5a373cfb65c758af60d4cc8d561c0f42425309e385b6d77414", + "6cc006e28290c53796d7c106e5e9a73d1eef8d191ee25bd3da584103b4407243", + "48ced243921464dfa24d8a29fa267dfa7ffbef8bfd5f63123313bd2d0aa19692", + "bf52e4c1ae9edf78fc61042cbd156bd451733a238b614329e7f7750fd225c611", + "c4b6c48e6d24d587f316959ca18d29f25c7796f59d3ef3271fff07ae07312e97", + "676fbbc33a6cf25ae5c2983f5cb28aadc011559caa92ccdb8a68186250593198", + "5105888b69580180b0aad9169b97acbe23aab5998765e5973c7db65f16e4866b", + "b6f4e8b2e48156e88c0d61ce6c18168a3279cb5426f84a29f0bc34ac73571ede", + "1eb6e0812f1a4c522d1595c5280d70449332f94972d8f7db8d1f1415aa2c9256", + "4eb5d3331f4a6422fa37720969d98b68ce9ac7d86f35c0f65c0310059a893fd6", + "45960b24f33e9e84d9d6680f82ec1976475d8a51ed7bff505b0abf7c73f414fc", + "349a885d187338e71ebde201942acd12b84f15f9bd29fc7a1e8e1e7f48001357", + "1888d7a9ca2f1a1bd598837692c5d8c610315a0def29d839797586bbed5bad8e", + "ee64524c632bdfc9fd0611c09b807bdd33d931f9fbc0f6b484934f1cb88135d4", + "a254a14ceb1631178389588fa1c5fd2a6381cbb025f604ecb4dc7964ada15840", + "76777a9a38584453765ab52efae236b43a92a85ff99421a85c047fb84eaa3da3", + "18d92cb8042c26e8d803d7b61d1024351f87c0538ca54798143bcdc781e68e1f", + "97dec06eea065708e2264d2a6d106d5b70703063a107c100a919ee95699d1efb", + "b40d9220ee8093ec1322f4d5cfed2458886ca16e2af1eececbf64bf02be6c7fc", + "2cae4684e28c9501a6b79f14c1f8f8b1d3a989d4093b1eb767d3244eab9d2ca7", + "f883a6573d5d90676a55129e50f92bd74fc4e998708ccaac90bdb1f84f561896", + "dc5e3597e04e58af0de74e47b70f6414964054fb5df91f5024803708f6989646", + "02a67fe0075656f1836803885319978f1092cbb1d664f34665024e3955959f5d", + "8d745ec8cbc8c6959ad470e59ab32cdb5e6856f0040f3455dd3fa02a40e85dff", + "dc76c6119f14fddbb0c47e0b392adc61e7b1b8974b9ece529bae3f6bd2f08035", + "f0c7bf1f54b24d4ace04adb7f087d2e0773f6fe143082d0dbc3f5582b6b092fa", + "ed605c66129606237fbf01ff509c7f10ad327fb7ad27c33499c8b43412f75272", + "06a3e1ca0a8ec035f87c6152092e6ef6816bb21954a3e76541b93b80658c8d72", + "16e76bd717730602bf03d963e423e6d39303fec03c32d4bc6eb3acf3bb40f458", + "d26412e73b10ae2e6f3450be078d1a0ade6814a2259eab1f9c660c35fefb1feb", + "0b6a9b4a402cdf7c4b56f27103e832345a3f6e4aeb903ef71d24b5d1c3f845cd", + "00b62c8bf3350d339ecab6082965af7b1ddf463bcef6b8370c28d304d854971d", + "e5a7e82416e4adf7f9786661c4eb7fb0ec9bb6a9bde5cb6be82c18e790ee4924", + "e1fe687c11e1f70610fdf1f5510d010570ad6ac04e74105707e485be75164a58", + "e588fd7031d8372a68e7591d1093988bb48fbfeffb45ee7d25ce35c501be55a6", + "7e7bc3bdac44eebe0a4a7f6b353337b52742cb4b9585982fc891683d736602c1", + "1336afdf34bdb14531116f478f69ce5639eeae67148245b4d216e5da3763afe2", + "576b424aed2be1ba43092486b414a57bc187c683d41420e2c0449071463f39fb", + "9d7a13f835788e0d6a737dd75802af9bc6ca8e292005c4dc2f5020652aa09603", + "0be978080cb75ed4e86d4bd9936cddf0ab617430124fff0df750767824281efc", + "535265c74b560ac0b6032b6ccc1899f23260fc23e08220515a245e8e83ef0f18", + "e32f34523627ff97cb2f57db055a4ad51c1b2ef90fb45085a63df62b07914852", + "8d69d9e1e186da9b3713782520bfc46271a42339dc54c5856ae10f124282cd64", + "c241ee188bf771b85c663c26f213b24524b2c6383b59aad602a68ac82830d4fd", + "0b83dc3bee2950e12255517bba63209cc6430ae6549558aada9b302b55c2f838", + "936ec0f72bb9126e6b0e7a9a8ff9170a2276c01f58a3839c3c586493306fd1e9", + "f7858b1cc34f284f37d0d5159b25a6cf6f0c48289f4f53d273ca512e2f6b3cb6", + "2390fd07c1bf1e05671845fd67b4a3e08e0b9163232bb9ccbeffd09f0d9afb4a", + "4673e8ff1598ad56d378c12873067135c01b3debd165c2b6651f9b0c0625cf11", + "c93d604e69c68dade70ff709fe6417ad257be92a4a9835fa454d2806c16ea800", + "4ce5943379dd74340f399b61fc5514c2c15bcc69d181219a83ab6a0c63a448bc", + "7702f2b32b419c9778d8b7a29123c616a075f4e3ba6f8f061cd61706a69cdc2e", + "713b6b640bf7db564250778405104df6132cc21f32e1dda7321bab29e4dc0d2a", + "9b34ec47e1fa1537700e3c739ae3df87eba18dffd164df6bd095fbf26f6f43fe", + "0d238b9784cec1c8d40d99de886807a3d9a636fb27e92176d80acd2b9648ef17", + "e1532ed8113cbba8dc46ccdd529d17ba22a8d125eb5ff7df0c5a1f53141f5f6d", + "752b4541ead7be65cc804833913c1c2450b0045d92cc193cfa67b42e705bf6a4", + "852a4eab49901aee9aa2a3f6553d5ae400675bdc521dba8589f2125cd5dcd947", + "9a48587ee6a08c373ece725cbc207fa9bd5f59bbbde77782269c2bff2e5d825c", + "fdba53953d46d86fecdad6e34db511e3027e383964f6a96f262672965b9c5dda", + "b370f6ab8e67b6480da12b3800fa50f70ce91c22ce8ff82e48cbbfd68503d6e5", + "6e7320dd1201e6e8dcfaff10c61a028b1a642ed2bba555fa40d4bbe5f6355feb", + "6772bcb50862129fc357e3dc76d316a217d16fc10f0e3f76c5d0f8cd20b2e098", + "7d777e36fe2d25003e8fb420e6cee20976c0861d9b287ef845692cf2f3a2e2b5", + "353d7dc50109db8336dbfd7a5a1c5e5c7ca697f92594d4c78d9188bfea2a534a", + "06d933a0938dcab8659c0c28b03b5a981ff4ed7c8b7d7e4dcbe41c8dd56afb44", + "f99bedb4591c6b7a029bec241427eccc1c5c1e77fdb59462815d2121cdb1534b", + "d90a8e096b1909567e8013f6980db3e498faaf9cb3c7129b754f9da0afed3511", + "e4a36cca280c5094eb751b8bbf3668ae0618babb6fdd41aa4d2d023f6ac18e55", + "ccd472cf664d4aca3f9ac779e1d88c3e142552e2752307fda18fdbc8c31a4a5c", + "66f4ec0ac68457fd7052f888297d52efe8b8a4fd07dedfa7f565ff7055e57c49", + "a075450ab7b9e089a391e1f6d5fdbb0b7004b9317033520e96331eb5dd278d69", + "62d1e7124e4198d12f6e4d272282b0a15213bc64c59e241145f27d626dc6d768", + "8c568074c22d556d3ff7b90e64bc54bc5a6fad6e406b5b79c259e28c54f20fce", + "863c55666a32a365d290b92065ed2186cba6e4549fad22f6b70d281c4b088188", + "956b14cff33c6ddad4dbeee554f695d936b04e1cee28f08ec4a186fd126de558", + "eee2deab5bf053f357fce084f3be3713bbfa6ab69612aaa81d782208a6b977ea", + "dd7b23edcdfba83dd3c2b6f965cd9382a92f06003d388ee4630727267e25a1c2", + "9f161fe499182bea38fa484b9e3971b3ac170a140b7146f0789e998bf7a125c8", + "dc7b9edab18d1168b6af53c3fe467c802376c899c7331888ca5b0e4b1def78fb", + "185c24e84b4fa0afe1e5592881436557ad4178337ec5320cd0ab2bc05d347583", + "578b40354302a8042e36af010a707890fd5c06810a971179557bfb7ba851c61d", + "36148d5c39a83bf7ef9d67da239735f59a772d8f863aba96ddeda1b6ccd3c63d", + "d9fc338912027b8dce909e6ed161598671199baad5110d758d62fd5fd4ef1743", + "e10572d32e20a230fd4ed963cf58ef313beff38d376d78c411293433ccf95bd5", + "415a1eaeed6367330d30dfe5727d24beb33311a8c4bf29a766a1b0ca4f795f2e", + "42ffd86cb3f0a1aff21b121d3a83ab019992b13ea7abfd197e56f3b9d4479e31", + "8639f6c362380f5ca9b42594beddfdf416b95425c62f6cbe36f8e5cf3e3187bf", + "ea68708bd8e3dcf28db835e6d48f9ae427585642c387b3880d8d33830f3173ac", + "c68f40dc66eebb9b7dd166725452e8dc0c76009d821dd7b4116c0b8abe6e1874", + "032f39bcc5c1180c83f4558d9a75e26c07ac823357f03c6ab8f3dcea6da820af", + "093ff264e7121eb953ced1ab669f8df1ab7ca77bb56769c8e593523c1c529b05", + "f571808a3782dcc0196e40ddc758a10ba667b8f4435482f79ca273b81e78ed7a", + "321a9a588ea6944f40e345ea25c5ee33e23929108743979e322801425e76ff00", + "5751ec2f3b3c511f417ed8064396ea9bea98fc8a734bf5b9d1eb046cfbac58a8", + "730f7ba7ac0decdb2b1427d5c4e39f05282d6bab84fbaefdd935983cb4b73d69", + "774b9a7a078a2d33b9c59d5ee021ae51f7c3d1981e1f416131b975a8fa73b171", + "0be34e887a503101e291e0a7275b56917d235c74e9deed90f344eaa33e3bde96", + "dd0553ec6836c49534a7193c08626ed70e18633d6d55550e83af4cb80b5d1208", + "086eb9ca8e1561e1cc20dcbcbe4255034bf9e995ac9cd425cd3cd04976698109", + "2cf5ddc2eea07914bd33e61935de7afbf5deeb6f296ed1a92fe24f8a272ec20f", + "d64cb84bed985d8349a855da2dbedac537cbb75316f694ed2569e449c7c93910", + "f1b1e6bdd83c9cd476fb9b371b0b96bd17ed4543f870ffcc5165a72eddd80324", + "138e7e92d38f21424064aa6202963d5ab36013d2cdaab2aab59604b109204d2c", + "83bafa2d6d17aff2b2c673b3a35f627c3f27edcda121af044b5f8357f6d8fd36", + "3242a78b7c76dd5879349ec45c52a944437457afe862e7c37ce963d8ab72293a", + "21aecce39bc8431b8a64a588ee3cff1ba6664fab4995e0de425b9ca1b827a543", + "202a12bdce15e4e4f4537fe6b0b13105a387c1bc9e04245674efb65fddf9e150", + "2ba225f7e0dc4d8ea5c18588f1ce5abab0a306772c3d48e4701b714b5c198353", + "542da92cd8f93c0e018cb0e58c5c0011e843c58d517cbc3502cd8bd30b0f5457", + "c5f75ddb098d45e7bda2bf736a46bda1eccf4dd9129e5ecdd24d98ad66a87059", + "efde7f01b410216607cb43db27224db733c8ba8f924bf4ce9118666715429759", + "36e799814a243bbd145f765cfeb01890e1dbc22722fc35e7c8da81a053bb6c5b", + "ad36786fee03b6c34313736720155c05d3d6a102c06e15d2087540322fcf2f6e", + "c30cb3186e6e6056676007a408bdc31772e782f2476832accd8fff3647f4ce71", + "2cb9ed472b5b67df11adafc7d24510789ac92b0ab94f78f7a4d4ffa5aec44376", + "9ee2b04d4b52d33cdc7caa1fc3822d401fc172e90c0c69b75f74dac6ddbd3478", + "d89f8cd089faadff062dff957e7bd664ce780553571f03dfcf7a4538a0d52179", + "19a8e0a88c914fc9ef953f0e528141d08c900519709e861e7a15092ec51b0183", + "08523965b56a370f0820268bcd46f0e40f5a08efa7ffaf2d0be589f7e06b0c8f", + "32e67e59cc228bdd03aecbfce663ef2cd0beb8590f683f758b7ab1f18096a596", + "e567e08bc47c838da307ccd4f4714769af76d5d3ece06ff4ad9de4b9b43139a0", + "134f57164aa8e71815e96108a500ec2d6e2d5fed1d4362bdd5418dcbe8440aa6", + "f1a289323e8bbf8b47b74fb765d7bc34778e89fbf838b88c0b2e89a0ac061cb1", + "54f8f2e4917aa481c740c76482d2dde80149d66bd5a63ea6c2856c360a05b3b9", + "6926c235956272d374746848703c42ddaaa97d215635400c6e2b09950f8238ba", + "a8b268e3f612955aebfe028c46a53785323e89bb0d50381b6248fc6addac7bc3", + "4260cd297499c52b5d60112cc949b01df1ecf08d0a94a9e8fc225307d13cbcd1", + "473015561a650bdce6c8235764fd5a6d913fa56c8f66f85b9745be11a79e26da", + "fb8d5378c2b46b28f21cf7a11acb3a7f157466a893c6d6bbcc763605dd5529db", + "e5c94bf0236858941283e7cfabd5395882a754aed7cf0a279b1d1e06cc5543de", + "e096b09e819b3235d3d9d02ef9440ffe32e91be897b424bfad28164e2d41ade2", + "357f824d32e0b724b05deb850f616c87a9397c6b54bd71c2e7453c20192b3ee3", + "4e0b5522a384c3b840db7f130e2da193c29ac1cf4d45c7b3ad60bfda5163b4fc", + "cc2e72969f9790fa3e08ca607d5fa967cc92207565ae8a459237aeb737f2f442", + "39b23bb017a77c8eecb86ab1f5b735bd99427884727bd40eba86e2b7422d4fa8", + "034694cacb85977d25e5d48f4d10708135b7fe59e60d255f8358cf1adbbea2f1", + "91218eadb18ff53b46944433c47704be6691f10a649c5cd0ca82a0caff7b0616", + "45b56b405e78b3ab8bb5f780fcc83eb3d81ddf54707e508a4054214a3691169a", + "a970e45e0ab5f38d643b7c26d1ed5ccfc8c4ba8f84752cbaa3bc94a8a0b69df4", + "90fa307b6a12428fc724392f49603ff7d77b5bcdc7ac30a96b2888d3ca1b2c0c", + "13d7a3800898f5de147a8a7847ef9fa2439526b9c0bb505cf8010e77bf5a519f", + "74d4c8184a3be922ef318910d8b2e6d9bd36c56e0001f70af73efc47bf885cf2", + "27a7299a88bee7768ad0ac57dcd758ad7ee47b6dd5f696d4635e389767641ffd", + "30d5ff22255caab9d5284c983ac59d43325da680597a35d382029e4addbe4898", + "f993bd1866e649a213df6eec43f88d865958b77ea9bf5f08ac85c628dd9c767f", + "0a7f7b96ff5b9f0c55e3d94bb4d48cc7c943f66a74d61b1de54276332db4ea58", + "8e5ff2be8bd9d72b1eabe99f6fccdecfbf0e07d37a476f8b8842b863a0ffcea6", + "afac9f324c14fb4684169c85740bc43f79ca664be36f7370ef583ed862f28e96", + "8b5c202932a7030531cbff0d3ebba8e4a97f815cadb76289fbc6fc618676c0cc", + "74a8b8fa2237d295890919be5cddb90a8ecd7b599180c5901aecc81466a6f9ec", + "ee3a4250218fa673acf905db87e6c62ad55eb160b192c18bdea9de4586d91a3e", + "ff17d4450869a11848858675f8a40c1b671f0d3134959fdcbf96a31ebf723d00", + "fd61712e7f3b508a4bebf2b2fb10852bb95144414e976b09faba62d577b4f271", + "0e8ffac9c804368584f055c919e90cca0a6b8ce9517cdcfb99bab642ab30697c", + "692aa1184a8624e6430eb494760de40811473b20dcac8e2127be3828a26f370b", + "07ec98c3b62450ca95b0c37eef23ac7bc864bf67d863789b9991250ba389dde1", + "5def5e14261afa6a3a51860d6c57d24bb1e0b1200b1acd3e52ab71aa742739a9", + "202c1e95dc1d876b2e592d586958e1092a470002dda77771b325fb28ccfa94a9", + "f13fe26c361d430a5c0fe8f07831c1f74b338878a8c9b9c1cf5f781cffb947ac", + "e351e88625b9f552a4db58bfd31df853996e712ab703aecfbd4110b6b8fe9fcb", + "3031dc47f9c75ab382082838ad64703d4ac2d53bad65de5f6871b95f1f51d305", + "7e9f9041bec93b6e4a1a6777bba28e24c9130ca5f5d0ab73b797834675df5603", + "adf50cb92f08127de44d829da97b9939f0cd94d2a17187b1065c5c80387fa7b0", + "6638cd3658e27e7b23462a52c2b77006f30e1493ce7ac20040a586223fa3608f", + "423fc4c8f8b8161cb8750585d0a7ad03c45b29b8934c10dc25a0878ec3e8c3b0", + "fb4b6eec8897f1064b46de91c1539ccfb281df49df49ec2d3b2e70aeca1fe642", + "c035203e267d0ea71322755085a49fabbe4b27a5ed44a807ff09d3443c016d24", + "2f8666bffa567337d67017a69cbd30c4afbf9cba1a5573c031cf38b685ab912e", + "7b72084df6bbaf4674390be1dcdf4dcabe444df6ae3cef6d335a397b00b8445b", + "d8160bfd5ea1c4ba6154f7b5f3ede8943f805dc5c52f90c815c4d402e12ec16b", + "75e2a3615e041093ad641b6bc2f04c47dde26ef4770abadf54de252a6bf86cc2", + "e21c75508acd422718b3bc48b5cac0bf13d300a58fe5a28f6d3b34cd3bf707f6", + "0a5b7258c565444b96daa710933bb0f9c275ceb0a21ac7f5fd7bb67cf431d89f", + "776373972d7b001465784dd2040cd4e6fca577f70bacb6cb1b6889b0aa8bf4bb", + "98b890b602d6f75caab2fa3087d8d8769a22e7df58a7e1ecdeecb5d8a89f28cc", + "110b6a99e45de3d957617e721e9a6e7ec778738acd149cd82e7ca47699ecb07e", + "cc458e336f650942a3f457b3613ebd05e6f23d6c5ed1d93e17b53deb0803168c", + "86ec64abb2db4c4b950322903ad54688c63bcc57d4504d383a79bb9f65510548", + "41fc95209709099f44ec617b64108070a0e5f71ff42fdd066357ccd78f7ac259", + "250dc15c36ac5b0af08fa959382182d2f12fae0f715785d7a7d14c4af72020d2", + "acdd44ab15d385544fed20dcccfc4451073abab85e6a2d0fa9f2fd43bf6a53a0", + "5daea3793af80ca23ece52691af6193516bdfd4d47c6b3b146d65c4f1b321cd7", + "b6636aba359a1105da263a01c4d1130b89ac28e812994235e3d02332954c20a2", + "80dd2dd9d6011f32fd7f535d6402d50e30ffdc22f94a24514514f0cf87586e6c", + "6516a4499b3c6a8c3ae81af5d28d02776b0a0a15ff542a363d0c85fdd1746814", + "8c0a653a30916469606924add5edb161a1c792198b70a40efe185e1d06734def", + "e1ef4329ffbd97cfb0639ad0a9306cec8303512bc573d2e7d1ff3198a3ef876e", + "e6157fceb2ec190502515d179fd3e917b741a5d802a8fb46f7133d89f36e5659", + "1171b867f9475b215e842eca3fb54e6700430b7935d3b87cafcfebc387ee9bd8", + "c551a320e4f36a0c1eba0b5760e5ae8bf75760ae20bab905596f3b3662677441", + "bbb16b75e0005a1b91d7c8da921be9ebca33b1161957608d33d16bc99b3c9c3a", + "fa7db095679214d85dc658d3b782f2a60e26456662b16753cac7a124eb37e7b9", + "fe0df3fd3fdc4e8620d2ee43f02b219ca204c0978e6e8d51dc3ddf894b1225b0", + "17c99eb0164df2d08c18cc9292cbef54db78953e78b10b538728f5f7f5d43077", + "6b92981882e8494d0be50682ae888fe35989913994df6b7676f57f377c9998e4", + "a1af602890909325843da1563e274aca1e37dc3ca519fb271f209d662789d439", + "21a18f13d2399df1d43bd9699124b0c65b96ef8b750752bbbb882ff5b8f5cf10", + "127a24346d0e682ef972b1d85f7f7ad708d6cc5b20dacf4190918ba44127e01c", + "4e2656ade5da9883ee116bf0132416e7d6e426b0f8792886ac66549a47a3d606", + "058997696460aaf11ecc3d9dee04bf7cd256a642343d05503309031764748d32", + "9b4737d5613a715468ef9a99570b7fff4dadcedc950f9743132409f8a2127938", + "7f6b5cc0f62460f66ad46417ff1b5cada436fbfb92c34cec291772eeaf0af1b2", + "fca46619267f63af3b83f75a52d853b68b7437300894f8498851986669a4075e", + "c6dbdc0ebd7ae1f47699c7d443d805cad80aeaa5dedace47d4b1f298b05f40e6", + "5ff009d5f6cb22d9ec9bcec33d5728976a0775042b4dca8c484a6c0c293523bb", + "c542c1640e2aa3d7240c65758268f8b072a030c98cecefd3edd5d250c4018b04", + "5efd80dc275d0e7069d0275e6059c26504360ea423cca6facb1e514ef2f1140d", + "703a2e907e22b184a55b6c0b14deb71e3fcd8274c90fb96eba18eabc3934b813", + "d613ce3f914241d38995d452aa9917cf64821140fb4d4860ebd015abdba54420", + "ab3228eae4609c88c9480b19b5dd45a980afabb662af0db40266cdb9e08b2245", + "b9b4bb4988a3e7c492ec4e976adcb3bac0e0ccf05c17f83f3b906e8db08bc364", + "159186a28b57ca58ec5bae13213635613f4fabfa0d43a3e711585422c749a970", + "5bb73b4e4f4f4eccdf9a0648e2daa45fedc44b1c88f2f70da5cdd9aee55e3872", + "df39d44e474eaf72b4043a17c2e718eafe76f9fb255ac71d53c3f564b1dcef90", + "f94ecf40692ebaca40b9d9c6c5eef07178358d6e3d2b4455b719e26fce5e3ab1", + "34254a7095ffabf24e7f335282dc47d5c708bf0153b3ab51cdc07ad98352c8bc", + "88aac52a1589c4eaaeac86581059d10e083b92f858d935401eeb8dd7d2acc1cd", + "fdc1c7e7ac262b52e1d7bdefd53ccf868d84ff8c7b9d3a7c57083152844adfd9", + "b5389562b6f491272bae1d8de46c9153f0f9d5ecbc855fb425b9e923c6daa4db", + "e4fa2c18279bdae39e8507d60b970d573e72e2afd3ea6e21b733907c4eeca35d", + "361683e4ae90542b6e0726a8da228580dfe7943553d71a962de4ebd1416ca48b", + "3bee3beb0bce09998bdb8577a3cc612e79fc0361337160a0fdbc5c00bb5a6e8b", + "7f76661e6e3bdea469a45fd6b2e0e32404a1d1d7ac9bea9e636dbdeaa58cfc0c", + "8e0d76f97d4e2177648a9a313ba6c44b49f05b41e25ddcc5b30fb7833a169161", + "9795ebed48408d86681fbeb80843e788660421d2caf79e2c8d148146c2f8b920", + "ce2b1cbab760b32cde27eb67e6528c373f4d3d12c9834ce8dc02b11651011d9e", + "0c59afb50ce46875ea859e55c8989a7c93960268bc1812efea4bbb3fdb760202", + "da0bd1de6dddb846992e22146aea0248766ed9291bc38cd1053012d68b9ba3e6", + "c6c5129968f26417d0e3a2ce4213d06fc802536d631d4f8ec7bff76ec9818919", + "b20dc0337dba63d7c042d31ab30c2d3c73cb0b476be03dab0d728d99db0fc2da", + "3269fcdd2fc54878538d434ee0e5f4b34556f244f6fd28732ae870cf3738bd89", + "9c3720ed4f0827c1a86ca82971c409f5544645eabe2f43e6fbaf34e9b3d7368a", + "75d5279dcd54a3ed52832e83eb88328ce34d519e1af03eef72f7b16d67881ec6", + "c7936e1b8c231c635f0e8ba4f36075c0a5c7542a0a88b152af5fd73eb8e38b43", + "fc2ad37687baa7973e4bae13a81a816293cc711e7b431277f9d47b3868b558d4", + "12e80cb945f289604b58ae017fb1c6b8178e15f311dd4924f19a4002ceb49c30", + "494f1bec1c9c4569762f3103f19b6e581d5540da664ba287aefa0bbbb94c2449", + "11517f6e61ee2cb16705f6c6bf34a2451f3e37105e5aadd83c52beecfa17ed8e", + "76df00e357f52119a3e3c62db4b0858c5d9e33377385f8704654b754d407a30b", + "d919b2a126ee39d024aa1621f40602090718d6373e5864806521774ae69ea0b4", + "4360ca292883b9dcb21cb357d35ca7e61506b33aaf843db3f6a1bf10f88c515a", + "c6fbe99e3ccf2b96854ec67e5abd12e924534e6874c1f4e18227df5d5773189f", + "10b83486cc3d2ca0487baa7ee8887aca724f22e9558c38de01003e7ee22f80a0", + "18f5f675de975f81aadeaa049512d0d66aee6649b7865bf50c438ecbdaa3dc2c", + "a0114fd6cdf72544a0f3f730fc45ac25970483d9bc934bb774db0576a80a2e89", + "92c42d3a78e24db27da4bbeaf1030739003ff7754aed25a1719b8580e78d87af", + "87d596f6c9516f7aa6c18b7a8d4402aff0d97a7a7eaecc215ca8e1bd4281b1b6", + "8901ca29aaa8d83931130e740cbf0610967e702a881b09d91cb25acbaa47639a", + "1f67f6fe82ca848b1ce3641a11eb22816d3ff70311ff667cd75ad73ff131c09c", + "a7f925eb02280114bd0e52c1e5596a1621e56d04c99a4e4ea3ed6f1bea98dcc6", + "52ecf62f8614b01ab2501ec6c59f67f048c54cb067f6478a6b23104d02eee8cf", + "f06007497879fb6b3aa7434645349807808248d07ef4d56875c52582681e6d25", + "27e1971acd26637453bc63ed407220e9d2501d402c94a2e860c91d6c4a2f6c14", + "bfd3962e02349397e2b8066058a3a28c6a314375601319a98680331b6bc757c7", + "f71326d7547c7a58518dfdc7772e5765343c1eb9fbd1e9c12fcd3a5445861b16", + "e647ac7c0aff2e7254447e7b7d2c84ec218741f9670c51b07372e8cd13d8820f", + "5224145ff25631da5ad4f9cffa36f717a68bc9fa212e10c95a0b03903024bc4a", + "fa31afa9230ab8a86f13aa417b953ddfe58edf9a97b4d5dd2e2e2dcd0eda68ae", + "67999048949eca1c433283e9fa1aa516fd95ff03952df4e7fa37c904115b9640", + "5928dbc7598516491596eec01b9c106c27410d57fd7cabdd6ac737b5f64ef2c3", + "6a52e29153df7da5855e49db656e90345d713fb63ce28075fae12dc27fd76f96", + "d08aea1e1736d1e71dbccaddd0073e42a31a35208346300ba09d1da65d314383", + "57381fb1a872b8aee2c9a9eebe8e6fa3f11dcf9c58346ed9e45bab312312925a", + "e157067848661402cf4a2677169dc44f15face56058b75a32c6b280493309a0c", + "08c778d125825620e7b53710056efcb4906077be6664b7578879cd77c6b93a02", + "8021cc0bd3f4b70df22a286fcfef87c43c8dbd3adae2e0089da5bf81d8854a82", + "9aab2b6ba0abdb87db89adc0d949712ed4d23f4b29f4be39e6ce93c103ea7b48", + "79a2947d2f3a8f3e8d56a929f04449b386cebce40d96ed7947062a1d0337e963", + "d31e6a1e2b1a6fd346be1ba15a7c3f335fe672fdf18411984f755032ab5ac272", + "5719cc1c3cdce3049224bb62ed7f92a31056e66e0304c129ecccc819697ae4db", + "cc432acaf8074e3799728f1498cd3245e15cbdf3faca35a5799883b246ef3bb7", + "6ed2fa13aa6d363343bd0761376c3f7b94fea577b4e17b1ccbfa218bab87c12e", + "42ba8ba467d9f617604706962d665bbdaae2235a49b0c4593aa284103ec7c066", + "ee16ea7a9697ff688999e9e8a4010afe957e6020c06f01471c26c44fb37ae54e", + "ba0f4a376766855766ea39118626bc03354d6003113a9842e33a626aa8e66705", + "9a66236ad035ddbcdae275541d8946c05ace61ad4d0ab6ed50d32dcd49e4279d", + "0d2d5cc1c0c8ed4be70bcbd7117caa3fbe9eae9785ca1dfd2a0ad00542f99d9d", + "498bb6d790db51609fe4b76002bdc34f186953e846efb34cc3886cf780c34fd8", + "2a3065e9c8130888b8bb64e06c52e81b3d4b97250083a06aade4cbcb5c883248", + "bdf8c4df6cbeca63d3e0960a3396bd2618c714b3805ea6002940a08bdc943f42", + "941eb900445670ae3f31f73a5984689c3353df2de43cef774b08826d70b31e33", + "ef1c54a08b5f49c0043754151acf97e47f67f0e7f65a16ca45d49d4f81365000", + "1849f02dd6f23fe7abab8ca837e57b3dcdf4c7d167adf28de031af612c5b8da5", + "056a65fe9221b44f89777ec89cbcf570dd580b387fc165e26fd58784a7e2aad7", + "620ebeda0e49b2d1cb607d0f67c66c9b3d49f9e422c2a81dc4c640ebbcce6b53", + "6d6e64105a393e8664c7bdcd4f495dc17e2574f804128c3df827be1a40e0e179", + "1caa0115b202efd2e3f2606e8674d812feff73014bc0a86a649aca0a84d52274", + "254b74f7f28fc387a3cc7bed438632ba86ccbde2a9f5fb98632e826728757ce7", + "2cd1c523833c931f0db1b72a334ad6739a08445852fd9cbcc3e71ec821c6d3f7", + "e4a8e8bcbf8baa688ecc8d700e36bab1f250d9d2b0cebd78dd52155462f5b4d1", + "e4c66ecfea654a90a73d314d36b2128978357668d0b2b362d27110ced767cef1", + "a5c15c876d82186aeed499f657ff7b50abe4e7469c332e49861d6a268eedb213", + "3a6c0e5cc800d183d0a9cc7334b95856d74e3ca186b937303c88e7d81a65c324", + "0e0adf94e9a1462e8b4aafe1ac480f2f4a97484bce16961511c27ba4351e8042", + "c2a4709d5d8020ab9c490c784ab528329ce216c2f7515dd9f9970918a50a5b51", + "91b3b47002d295f3d89470139ce210241407269738d2736faa47b4ee20778b82", + "617b635c37ba52139fe16edc8562a2ecdfea8d59a3055b0c5ecd4d2f7bb2c28e", + "5478cc071ea965bff2837b4f8f8577611f839ff029a6547a9c766177b9658691", + "e1cd112dcf5b92c4f56f10cf1b32554f51de55a3c8b886cf54b0f1f7c67ef492", + "c0b7a87125b532af1e60ed4aab4dec881c32ca8d9091e2d160e92059749bfd92", + "49ec7a18b459161c62b749674d5ea9a72f82f7bdeffc80d5545c3c954ba3879d", + "b29c9f86cf46fa1add71368287b3332fb04fac32053cef2124e8a4c5b33bb3a5", + "cb44aa9ecf42342540d32e0b7c0179195a063aecb2a2b3c33e70f1e8c6a9d4ab", + "2c0671d271f30877a15a51c2ba08cabc81f41b55bf8b42dc3760c67f7f4187ca", + "3e96d04c7ce5296fef02f5295967baebf0b8134b1697508ea498d5a702a6c2f1", + "6758f8411b32b58f7048f480078b1272687ebd5bab32d4056b0ed7163cf7d1f5", + "37da3001ba6adaebfca8281050036f81220cf5b76a1b701d893e3a9c7120421c", + "729d6ce70da10258f3be909d16b14113db3575f025ba896b95336793f3f6df9e", + "7e65d6142232066559c0f8aee3f3457849741eb70d6a22eed236ba88fec83702", + "97f1830a6bce50747843421000628fe8299714d4ea2be4c471fb30a9b91b705c", + "4b39d7376553d04ace6021808511be1f55e8c52beb3ffe86c3ac008f2dd48484", + "3a500367e55675b65bfe2a7b4afda8ea3f8d8eebe0ae7e8b84e90f4a3c1b6eb3", + "3d60a2a210cea5214f6e7fc7255a2ad2fa20a8eb20c58131e7a22ea3cf950ec2", + "de12ae7ccdd7c89e9047c51ef6dbcdf122804a53c13ab9294567122c57081181", + "3b7d79c3b0918d5da7d86f189a296bd78b7762ea5fc5bd998c4a60aac486bb36", + "4e33ef08b08ca3e30cc54e9c97396b7509437996a948ff253d746ef4859af392", + "75ecaff41d363dfb7b2db98002f8c39785b5ccb48bb03140c15b845c317da4ba", + "1d6a7894a87b652eb7dc262566ce0504b7b86d02dc20efd1d7d14ed0afbe1c2d", + "676632d7f963285f8cdd18a2c172467016e89cd1aab8fe076986371ea5458c6a", + "a045928ba6807f50cd4f9008f651dca71539d59ceaa5e76bc4f7db0df27fda00", + "c7728cd99c1ed41882e12a521e28deeeb873b86e8c0008066a96d6a906766bde", + "1b00c296c1e6bacdda0a1a0b7d98b017caf370c66e8c12de1e99241820732d8d", + "02a7184ce6e711f919377f3d611ccb027e8c005a5d744a35466c7078ec8037a7", + "965864384ad16749d0b1430874352870ae289a6637519b5561790ed30bcd47cc", + "eefdc9383cc5f9d970adbce6ecf8ab384834343a1dd35b8e10c74cc9e9dc9780", + "07e99bac98692f2d42840d108074800c4530bc32889bf0a5a05f53bc6e54687d", + "df6c69bd073ed11405ff51f1a68191bbdd1f8d0d5a0dc19a9b2934613ec4af41", + "82c5bf31c81c6ec82d4c73223d8f4812681b636c1edf9fb4fcaec5a2839c5d21", + "8e3f6161aecaa495b71b4bce000ca44093c2e6f1d15eb5047bae131a87c94d2e", + "f59ef4d57c8caf82ee3886a8328ce822ffe7c3d14c7c36378c3c96010b6ee14f", + "98a3f58267ea49d05a486e83fa5bdc7e0429a71c10f1e194a937a71e4836b749", + "757193b9561c25895091290e6cbdf4f5d4de68ec1929ad6150929d85bba00dd8", + "20e5a63bc545a29a553cc940c90bc93d720965801b4f81c0f1f2efce2c9486cd", + "1161662085a19d988312b8417641f91e023df6b432f09310f0d51dd5fd37b0d2", + "074a81c334a70a4d9d9205a4a6e227829ddc5d4d5f760c481c61c9681b535185", + "8142bc954748af41bdda0c2b1c9814cbb274fe44b3a90009aa33cf7745114dcb", + "de26f575dd20c66b60e87eef8fb9d7623079837789764e8b7969ac853870f183", + "5a3edb67a352605edb3d8e5794dd6ee39875793b0e08f0b00d7ed03813e580eb", + "a7a7bfd9a878d33f0ce8a2443347bfe4be4c0b2b2ca2b51704fe8e075ec95718", + "0627c9f6b51b9fe8fc9d7b086e64ccec4f971896ec78c7d3a7b3a3f13a178e67", + "6610fc13a7f11b3e99aa200301843fceaaea138e5a3a555477258a7518074d9a", + "38b480cb3c3c458a4957c8bb96de3bf497c18cdb8caec2a4acffafa11f833013", + "97855ae70b649efeadab2e32757433b38547477c3a71502e248fbb8c369a9f9b", + "eb639a98962f612340b9c018f78afca347484bf7baa7832a883b61ff1021612a", + "dfd66810eba7d0ce986d4892abbc803c12f8c4ccec4d686d3418356f2a220c4e", + "52dbf74f07d442714918e72040e42451fd9df5023273c80d94856c2dda5c5568", + "02124bb863d96e540b2f7a11b0ff380b4d921d60e4f39ba92676a49098e7c285", + "15ba047510e01686c84058389f31a5b2abe846b37652dbeb8a9331a24014e1e2", + "91e27d2019f55167d78df2360e8861ab1379780a5d7bee20901f5671cc0b9761", + "fc69ab0fbb160736559363df99bb0d1027ffdcc37407ea6fd80995799c58d531", + "7d2e159662b4b9ef40a67324a05c7d3b98f67938fe101b581257180ea277daa1", + "86eb737153961353bc534f5b9031b624f823712b7000adb9419a5c8c6f3a3a59", + "002b2bdad4e1678e212595fb5c3f5224da5ed6d9fffb37d92b49c4b8e6d01ce1", + "1c2592ab17598a1c281d497499153780c75d649f3538680d40d795a2ad976a09", + "2f7f031d5e0615f2351d5bae3f040742a6179f162c0017ace9eef1570ab77c2a", + "73bebefbcb9ddd0abce6e3a6d28fb94fd79b07e56c4e36f2a0ea6f85bd0d5646", + "e98fe44c7cf1459aef87a721a67689d28ef8fa1bf7fe30d06a45f612b8c7f340", + "0121f00f27497b5976d16a9fc219e9446c5c095c85a10b2ba4e3aeb03e596e19", + "37489e05029e55488ae132ee15b66c2e34783922d661b3ac806cc9c90f36251c", + "407720aa0d27a2fdabd038f96d4dadafcf13a4d9b9886fbb5563e1084a62eb9e", + "9433997c06777619983725ea9db3959909a55d9885a3f848bdf7316a57641da0", + "7a8e9e9e589b814ac67ef6f8db17f1ee8aa4ce44d4d7777a9c87c44edb0e7ccd", + "62f354e78e54aad53dd065ab7742c88043c03aca88850965180d35fd1acf5d3e", + "63c1bfd8aa26304f310e19644349a0e4b8b59bf8e547bb7a65cea5bc4d27fa62", + "526a9d8d0fea4560380a96b06ed4a27d1d0a17402cab827dd7d8feddc4cffc37", + "3299b983304562e7910b30566f0bbcbb2b57386ca2ac873a7a47928fc619ba36", + "6a77226544e9d91d32267cd5b962933e5b88595b3ed49ab907d14a14b20f46f0", + "5b0c315a1a3149796b59fddddf86808d065c536bd4354c1dfdb1163a55a60c19", + "7d2d027d89fdaa289aa5b029934957255aa601bf1598ee162021fd9e9d26cc05", + "bd579a28140d0ccb3e23877b7791a58290a1b28588238a307065627928b0b271", + "ef251e3ac8af728da4c8ab76cf4cff7a12f386c4658ee1ba9b0311b63b3b5b9f", + "6fc5dcb526ca9f70e63512a88a87840c33e05f08bf066ff9084b6e6e78f187d1", + "92751b1d7d1b9917f0474a3aad00bd4f12a2b1a8e41183337a1d5c43e0c0687d", + "66d516afcba118038d6e40989ce77b97e8d2fd695ccc338d28e73066b23a671c", + "bf91636ac35720a43612187ef74ec80f1f1798bf58e67a744ca34c1ef90e7cd2", + "794045405ccf9aaa6606c7662eca8cdebb7bc9198632cd66584e737698350b48", + "6926daffd99b8fd2694ce6c0500152a5ad49a2bb45bddab6fee9bd4d9dc8b37a", + "ada328a0822a782281f46d68c39160f08ee299437ea75af313c8e29a0c91ea68", + "35ad5df5e06fa8357ed9c684a96365306653c875719b9a88120f27f0099b3fca", + "bed76d3b89734c615778b05cc83fac29db93419cba3d22582c170b94c65ba2cf", + "ab887fa562b604bd9d376ca2b42d8cb7f1c2f64d8f94ee80274f3c3e8e650ed7", + "57a73e9f238b5da0308918326b30d23a9b9360cfce6982aae399351941ca1d49", + "693ae769d7d168489c9e414c1d23981568b0fe8cf896b0cada2c290ce52eb2dd", + "c31227bbd0b5739c5442493d792aac08ec4f34f5de1171504368a779e37c1031", + "405e86a81d99f44ffff9ea1d4ab089adc796c35e6be041c83af611b164fa990a", + "b2ac5501e0c077d40249700cfe78239d4e465cc17d8a1fab6c0440e5b4156f4f", + "fb66eeda72a7e6bd5b95b55a1fdf9b4c54dac566a43ee6f4d02caebe77169781", + "225030bf730bf20dc3ef8ff463627183c9ada047ac47ac05a647a1b19a39788b", + "a7da10377fef19bb2e09a4b050388636d41568cf50223c78b6090582fdaa2148", + "fccd78fedf31538ca88da25a3c4e5e993962cccd75922220c138787be5042223", + "815be52131fac962e77b3c820828fef088c352bdf3ef3b03cd623e9373b2b42e", + "ab368fdfef9bed0272bf508c121c8cc9c9336932077b6872f8d950376b70c326", + "bf92b3bc224cc8ba0b979ed9034202d8cd35eb465f399d55b4ff7ebb12d2a008", + "5ff1e602b95d9830234a8fb0956fa81178906af91f56d8854cb3e87616240026", + "93e39499dbc362f318b2e178915a3f9f9178a3c98bda3c6a7ca9acf78a4cdc6b", + "7e6513345ec057a3fc301bfeafe7644c191cbfce6cfb4e737d501c4961ec4a82", + "ff4ad5b7cd5fb87f7f1f02e68b33d8b96fb9910552839d95949db2853e9f1a31", + "0bb29ec4d5a81d3321060cd4fb06445dc05c748a779400afda20c3bc18562850", + "5a6566f6ad6ec41c007c2f2ed08c06e2f3cc6ec3549f7454ff231a15eb7667dc", + "a01504fb4fcb7fa02a2089d7060792b164da4b8f079b78b1d0ecf16b4ddc10bd", + "3f427b41a9000545707e8fe9ef8ed52dc6b3e45a2e29dd994034a83a528e03be", + "d325db1232e7649dcc57b710d214cd84c9eac22a838ad9e4d0449dc727176d2a", + "1382221cac26f8d5cc2e176d0b61f5824a9d42838b842e46f0595d73f1c79964", + "51a8d8b44119b778db02994f0d8881e5db8825ed43f88fb6a670c732e0f46233", + "6844781f30a6df1e1b54189331c10d6c2b0883f01c7548c22a0e6cba3b7cef69", + "f2e46779887c48c64f9c865512c45ae2fcf91ba337b0646921439dc530d03945", + "1862b070ad856fafd24c65c809e1729d4a92ddd9d62bc0a7b69ce442734d8b40", + "647f5ce3356efdf3b40e647460414fe1ab09a7a2622e3e1df1ab701ef668931f", + "936282821a29544b4952bf5c13b4b4a38e850de4b732f6ccb85690c57efd71e2", + "d514125ed5192cccb6d4d79840e2e8be27dae6cb34788dd383d073a1816c890e", + "218dc886128fc66f8e4617fcd977303bd9843efb3788f6b6c3fd7768277eb610", + "040e3099d149e0526e952f0a76998d7fb83deabe50a248bcb2cf65b1ccce8cb4", + "047887aafe5139a73ee42caeb3b4d4caea63609173b87350f4a586c63e1fcdc5", + "8d91dd3150488112948eb867754ca9a4d71944a9fe1b85c80c205914a890cbb6", + "521d0bdc97ad570e8b7b55f9487666ff41eeb004a83b7b75e641fcad24383ab8", + "de2f79530d43bcd76a221efb403ece0f7d8d3d6af61a956f0fb82777392a0a34", + "4d3bc10b295fd36fcd6034f60c9712da872ca57861a34d14722b0f3f552d024d", + "422dc21ea3472e0f454901b3597f3ce28ba0ef6ef61855e719eeb8f837c6925d", + "351bde8b328515c6c4c1987c4c04e72a76dfe38e2c67d96fe86ce1587178286d", + "f03a8f415b3ea2542fec0ab97bfd14587262eb4af72c815798d95f8e8a06037d", + "97b6c7a7a7fa8209a3187f819e67f436221c90c5aaeef60f9b85fb60f8c42fce", + "560a23a22c221327caccdaee0e7e3b54c485fcc972b133bcd1cd2b6c1f2068de", + "be7c017a9d124ca51f1fa662ea43bb2ad60886c97fe476da27f069cd837b5208", + "db46327a012ef6a55f38bd6b4a8f49d68f98b8ce75f0d898a1cfc5552fe0b6c2", + "14f5f6f0e351e978d8354b024ae49833db2634750b02ccf3142048efd6bf6517", + "93592c7e4d4b5237121f25fef8d980e56e0a86469734594ec531d3568a3d8db6", + "ceab796dd4e7dc4222ca1f67bb4ee0c13dd6edfd744ad36e5823e5392ad49f33", + "a1a1aa514f943047cefabe9f3b579b43bad91395cdebc1fdaeb9009d9f5b1865", + "7868be8dd2e28482f12fc34347a9c58bded0c9ba481874a6d14efec9cf8623af", + "9bb08a3a334657a0d760fd8369dfd5325965243fcb570c18cdb4e89998aeebb8", + "590201cfdde55f6626aacde9e6e4da5c0975f3716ae237b2f428471b51a8f0c0", + "160c7d459c1e83d7b27c3194eda1a9018123d8d7581da8bd7fddbfe2a6241ebc", + "0d487a65d96fc45945aaa8ff893a956a0db2b3717c7bf16c3722b44bac47e88d", + "c0303490ba09d9ed9ac4a5dad1e85a30a5c9661b87befab5c4115e9985ffed52", + "f343c09fabfcbb5b40032ab67fc3ed3e215f90d611ea60e50ff4900ad7e2b742", + "8da1c700be5ebe13432edd42d21e6a85c34fd3b3a79c00479aa45f78b0f0fdb3", + "4ad7adb6d99d7093ab244819580123c30a5d0040b60873c64665ed8150938565", + "eda6d8307b625e1fe9752c356e7dde732717cb5b847b1689fd5f8ee251623606", + "cdb406e21af293c0ba9eb9144e1398c567795769ab4f4bb16481dc3d7c97fe35", + "d33b349957417ae326e337276700feda661bb8b56028d7b8796f10b56ee446bd", + "6829c59ffe1b5793cf1b9038fbab728923d5c181dc6818f8ae359b53b497edfd", + "1681a355a2f903b08b999b8b6e68354d14bbb227361b10f8865ec153fbe5cd3d", + "2ee6fc587ffa9ce1648cd78ee4fa237726cd1415bcffd01af0199a601f974ecb", + "c33a4a2034f026425ebfab6bb4a5a8ed9f75f316c463b8c0b936cc3dd3bc4f4a", + "de917a404791f1f236c5f3059541a8b560e1b2a64dbe710b2de0b64a6c655bd3", + "d5624e67b84406fd40c63cd611e47ffcf8c80b1969db2d8e42d92c67f6d179e6", + "079b29b3e107a68447bad01a913e7e3dbfe0c874b7f4e5ab4033f08e90f41720", + "7035d727120e72d0790af41b103d02de47c66820c572ecbdf41f37c3ed310e5d", + "41d5fcddca900bdf7960f5aa4dcdb9f5592845e3a39e7df4e1230f1e76d46677", + "90f1581877e0cf9bfbc775c79e557f074296adf07e4b94edd5738e67c775c0dd", + "896f6a5d3a3e731a825af5c8dd3ad47d74ccc01226b766d835377bfab63702eb", + "157b77664a7f74b4d6c7976469759c42982f619e9f93259240b024de92ad3220", + "fe52aa6b23e8be0ecad9a9bf587eae810e73bc6d6a23245ae4b7e9e2f21a4e28", + "e464d4801300e7115fc3586b63e7c233ef2584ad0be1674bb18d012a0fa9a45f", + "30ae7537e92a46812ade45936662288c7a1d1535347dfcb690da81fbddb3640d", + "fc321561fd802bfddab7517606f1d406cbf2f8e3c6d2ddbcf5ec371df6cb6191", + "d16b9e6920b59e273415ba7f2c10be8b3d0d235d495a1bb2e76a618fc3a71539", + "d54e6e5b5de9e8abb85616289ec5cafc5714e967318a98e6ea599759f9052fb9", + "6046e1066c39a1827d564d2ec02e937fe5caeac4b52e24707915933ef63dbb7f", + "2ce1be1a13f84af9b8a45bd406a7da2b0ed94f70e9775144291c435ae556ccd5", + "15bff067151465223a46c6b90e0839c5199ce3cf09c9679dce52a7fa3b7e8559", + "9ce6fbc21adba8a2925ea7a3851673aaeb6ef17659a5ac08b70e66773bb4c476", + "eb30a7c3fc3bacd1bdd22f552d685650047a2b240d0efb1feebe2b0cae153c64", + "4d101e243fc013a859940680ecfcded8e701012978d9685b311a8b5f888c4130", + "118a26b0590aa9d02f575f2e6c81ba40eed5cf6c2302011251d23504680c9498", + "ae7146d53a547be602bf2b36a8aba5b752f35427b746871b619c52d6f42c0284", + "2c6de6fd3c3bb032c889685f6ff7e375ecd8c3f1c08231b8c4b96d0c03d6fe47", + "dbb574b9255c563a0fb4d6d02998039b63d595902e14344260900771451cd66b", + "309d9b7bca75e9b1f55fe532a9ede769118893ef92c1f5f62b8ca66ef28bd085", + "80b4e9a8e2783743e1c10af1256913ddb0d042752a7db7ed1573973e3cc0813c", + "059ff2503b691dbb821363aabcf03273049faee668708464c4e4d8d5811a488a", + "48ee54f19c93f6a35df39a52e2230131f2bed25dbdac40fa2490a39115188917", + "b70018f986e6b6bfdb7f4f25e5026957dff62b841066f953217ff8cfd2b37e93", + "a9089f562a7228762c6ffacbe4ac1946961dc26c2471cbabf7394844c5de2f9d", + "b90ff985dc566f1cd7cfd3f88a6d6007a3c9b77f8a2ba970008d94c26f8166e2", + "77400180ef5b4d961c05dde48fca7d940a1089eeb97e2fdd16c26073da221a01", + "a5c575cb85904015a46d6b36c49195cdf2b73b7027c587a9266c5fd248238bc7", + "032a82fa3d46bf997748510667725847ec85c1f1e1a02a52bf0967c8c96e855f", + "3296e447a00bd35bfcc685e7d6df81e89823330a76a92412a6a25b079e25ae9d", + "cc29dc00b2b0b8c4aaccfb7baa63996e138a48f9b4036640116f82dacc06a887", + "c04c53e192b88e88dc5bd55e48dbe4aacf278c5174d54ef1f8210e3173a65b0d", + "493d976b12c8b01818c9b6703775a29d7bcc66cc115ff823a0662ef8ffc7a044", + "a92a53acf878166e51b2bff31a17c9994e3408163144d4fd7a5793c609ec3b2e", + "b89034ba8c59d23a1acfd85699fc72c11e619cf739b9fb813a08384243f73acd", + "f341c2da0152d5559d27fdb98209df2cc3bded13200ef6462e6d658cbecc31e8", + "07785830a1fbc5ccd65e3993ea71c8eab96960d97ab163b2ef550c7d9152adfc", + "22ea310af02f78df3820a72cba919b36b3183d3b3443047e51f42aafa14329f8", + "2ab5dc04de2d5fa243856e30b3ca5edfddb93d333616c949ffb6e2c063728dda", + "b37a4cb479c57e545241be9f1f26055675895d8b20e5b4de18cbabf6c222e355", + "27018e22d42c069e3b238565aa9600b3c606030a4c12d5e6e0f9b63f45a10da1", + "49b923dacb757b15afc25b0dd37166637d2d2d5014587eb77c1dedaf148ce44e", + "ed3e7363684269c32c4d3a4bd6f0d45fddbaac8e5a155d735207fb469160d670", + "411bfbe902582736de614f0a8c896904a69373e643386063ed4b3576d2c50fb0", + "6e5e659c3fea6a2aba32bb16751c1a3fa457ec7eb56e5c46210bef3e6c5d0509", + "45b0eeeccdac6a9c88a374a3ddbed2c674aecc379c5dbdb57c826ac81334f636", + "7e482d12c965de4d310e5364ac641c9206abd8cb4ff94023da601c793fabb6e4", + "5f321d8a280a489bcebc32fd87f90db8583d57d752ae9cd06ee69d6a3219cbfb", + "c2a7d6b9329497474a43395ae64be0dc8054e81589fd9b42f3150be88b400935", + "418bc638efeadb29189aa8171537c10746e2dd2b56578fff4ef14f77c2fac39f", + "e0776aea1e1dcc1839cf1e95ccd297b4b5ac004084e922659a9aba619058c737", + "e7b0b37583f3ddc686c67ac7bb294a2bbf5a64d964531527b0bd3fa576b43b00", + "7af4e51b39e75b6f66640c0028c211ff6da2116aa3f82b58a6fa770c96c7dd16", + "466ed8010c951283405c52608abf6a433850c5ffa982a16b448d98df60dc4f1c", + "aed6fcca61ef326e0e1ef845cdcb0971ba3c6a78f29796c90bb17364de91432c", + "fd34ae72f6aa40e7eeeaa5c16d21062ad02e1c4b914eb2ff48cfacc163949337", + "c2cbfb4405612c2db1008a704f2343ee68fb0ce70acd3321b4f3bc813aa3e054", + "8fd68c40cf1577ec4822d94e1d71b637789e66ccda1476e4defaa24126bb685a", + "95877387fd5d23f0bb5f93e5554777cad8a9f00e07f0395b6e912b9e7d0e355d", + "dfbd321d571165bff8a1251d39339e709c45f568ccff51e71f3b0ae23cd8b260", + "f90239d66050e3683db08d5b2ae600fdf2a6bc089b181cc25be205e701b77169", + "35f37ccd37cffe0690cb7c28b3ccea5e362ed91afdf4a0c9f4b9a0da8346a073", + "0146fdcb318694d5fe20bbd36014fcf423d9bfdd9659ab91ead16f56f88d9679", + "817369a4fb36280b476321573b32c11b49cea2c7d70937b1a9b262abbf83029c", + "1fabfe13332ca1f165e0ada58759fd8ddfc9f6a1d014997d79cdc985e0d94fa0", + "6a235ec9a4f5b0b5550fffa3c95145437822ae8bc208f5a301dadd6e957405a1", + "e815142cbc8adac6310b74d3542e822778fc8fb12beb2440afdfc08cda2c16ad", + "c4e7c691ebe3f32ece47fbdd997d205dc1a9b4ed7e386d088c8db1421f8d79f2", + "c1cbc5c4837f93aec69291755562a46717872294b39f1b7f7ec4b2ae28664ea7", + "b7d39ece9a4f43697ab83167cf79630fd504b495e792ae5a6e43e5d2809a82c8", + "86c875b4696f31f8316ffb0b06b7af49084affbd4f73e6b2d4f264ac2b1d757e", + "8a93001be211a141102dfd060a6f1c0e23db204ecb17d1d2c04082d045a59bdf", + "c49aaaae458509ad912c394ceba8d4aed11d506f6eb563a7177b624019d65707", + "a94ea00a3f4d894ab85d2082dffe8384243c9c04cb351592e191f9ad0024c970", + "d34e7cf2569d5742b53dd59c69a3802f5167d8abd0e7c47d2eba89be4610f482", + "e93cc03f3cd29e969f4cee9c36a3ef047c87657683492b7e3a85f905aa4244bf", + "f040263a75c33bb266a6d9027737cf060a62ef15953c228f601e4778baf71547", + "e84fbb431c80da477e4006a12c70784b11a902cf530e34018bae1fbda4414657", + "f49b99c197de2b703b1f12d640da92a7a5b969fe811b147d26352f7159760093", + "47ea5b85c2449d8c4d9ccedd3b7036d0ade6403b7ef99bf7e5e08fb99e6d0f9b", + "b1052d169ce70b7b30f3e765ef2b9e0ba0b7ed03df88a980a57bb940a87fd8b6", + "1a2beae43b0213cd7ffea9528b6bbd541e579321b3ac61563378cef163bf2fe4", + "e95e20b9de5e712969b8d8758abf5856d2e8b56e6b91dbc1c55af9414bc9f8fd", + "387655d771a6a4cc51ea7d161df985a7de25d77f28c7a5a1804a092aead9bf92", + "e93a3c3cd3cb58fe3c948cfd221e7d16fb66cb607dd889a140343a2b4dcc61e4", + "45f00bbb150a02eed1445ef3bf9a6787e291b0ce109468b088244a6f714df12f", + "c08fd6cbeb40310288b741fac607dd37ba029f91c4e27fe9d88d41ea3ec93735", + "c932933700234734f5dc48e3d7c5bf5c9c4d13ea8599a36a4d7be56734cfe14d", + "5de8c427642f319294186fcf877d2d3ea6bb54e564c0304f0ce392a015c8b38f", + "9714e6d37ed90c491175d312f8209094e28c1076f08f18af1364257beae9adab", + "43c42b543c827a6211f71ae4d95eabe94f168f2ddf745e3e2cede27dd115ce7e", + "2bc6942c33d230104420a7d69d246b2657a28366dd9942c8f9fe083f1e6ac07a", + "d7c5be1e4ffa20f660b4153e784b1bb746e3275d357693836713a60b88fcff80", + "58a6e32b1bbf41f088cfa5e470a35727dc9250db64ad4d9deb3bcd38446ce565", + "5af7b7e4ae1b238c126ba0ba9fe0098c6d4e1f2ab410446197d0a2406e7184cd", + "cc5fcab2829d9bd75bed07c8eb4d1095f3df58931d2cddfd3c190a74a887d1e3", + "65c0091c30674946fdbd4d2e12596bc3201f4ab43a0510f5d0af803b42e170c4", + "5447114920c780c03df367e156d774c5595f6b51a0246d440c4f45a488d32215", + "166101727698182691c677e6963f62cd0fc061c75f8ec36ded0134a8ae267ae7", + "fadf493304df01ad74f108f6018bb63d652c76b19634be963b0ac063046ad61a", + "6e79c0317b1b24a44fb19463db8beb6d9a8e1ca270ee8bd9ce6d8fc95a3d746d", + "5b48176f9f33c1de736b0c3984d7291df8d7d3c433207d57eafac5b2eb32fd6d", + "8572c3a8cd67b7c18fddb2f434f1c809538cf9cb4322828b8acf22ead3e74ffa", + "ef094bc32a6731ab59964a8def3602e796d651cfcd0478e949b03da3663c5032", + "89432388d915e7a4e4fe3fc7e638041e410bba76fdf06e4f747dfbe3b9bfbc48", + "474f3853148c0a0ac0fe741e5b02a302aed5bbce5c558eddf9035b754d3f5b66", + "5c33f67fccc4c3ef0218ab73746281a2c7296b4f796d4f65df5f8c3dae307718", + "e9e88725a27e372adfa00f5c5502d67cd73a545a2a222c11f629b237c883ad24", + "86a36fbc0c78612f05eb7ca58b79ad84ca108ebeefb270649147320628f1b638", + "504828fad1e6ca19ebeaa7cf375f44dc21d87ad6f8db4dc0537dc7a94569999b", + "8a8579b28d9a699de04b32b0352b5db917ffa50ab158011d17a7b53a13e509c1", + "3623572b1420ec93827a3186ac87736b8fff420358a1658990b63364a463b9c6", + "95bf16243bc204089eaa59e39d84544c9488545f21577f2f85d26d70a83f59c7", + "d327b6de8ee5d75619db20e355e5ab54afff7906dec7c1d4f39af6fb5dd155d6", + "35c77b65a78cc50ef73a4531dc68689167d892cd35612d5b2b57545522b281f5", + "cfb7ec751469f847aaf70962d365a72abe2bec7f7fe9097eb8ee6f3836aca5fe", + "db2e1cb69103ee4d8337df074e6c84bf6074c8eb790c1147a9a7a6e378c98166", + "9bfe2ab730171f87126ac6bbddcc5514b4efebe984f2c3e64d4b78b80bbd9cfd", + "557b2347d9d191e30cb19cc3fa9b0439ee776f8fb048070bb0796ad5d54b6205", + "c8bb236aa883d025e529cdc69bf35220cf0e1bcc7a5ae7ea3a1c8c90b3e61395", + "c57eb15ec398c615412ca174cc150299faeb8fd99a3019c1d71ce1c6dc604e86", + "46615da20092232a8d7c5ec929f051143b044c5ccca4fd891c6003e99dc9f5e7", + "e02df280a42d6ff2ac96f47000ea5aeaf32087a7ffd91dfe5a71bac7ea53097e", + "9b041c9b7965e6055b77c143beaeff33ac93d8a6f69b4ded8fe7da00586a0608", + "15702a15fcd2847c2653566c2d3263b258403274d3049f02ca893d0f2f086c47", + "9072f289157724c5049afe65c43933e40c1505eb5151b5f57c05ff5251e0b68c", + "5e018963fd77def1a8b93f8ac42bd9bdf48b95d2384e023284577d652b3edf65", + "ef0ddbc62e9c8ac56382141458ea202e898dab8943fd12ddb728f7d14bdb615f", + "edcc26a58fc37fe2d8da6c17c121302325de900344935a931fcdee7e31fcfebb", + "13c9d9ef014a0c674982e5b92adcf5588592763e4906e0a817f720baa8697720", + "2112bb760256b03daa1d13af848414c8fb77ed3cb4074a42a16898de4e144e34", + "231cda7b0e6bd3871a50c2ee7f1d9ba9cff403a519ca48f1220fe6717a6ef24c", + "9d67e240c82748b1ede011a33fd13c0ea9fc66bbb36ee13142fbadcbf56f1169", + "b2afe291b0eda0d15fcf77da7b80c1f0fa7f090439d5efcb63650f66ba92cf56", + "ca422ca452b53a7970f61061db51811349bd7faa748d99f7a177886078f765a3", + "ac3310f54128e969928ca198d135c971a370a8a023d636b7d3afbf8a9f6ae12d", + "67040a751c068ae97d8321a8b5432a76fbbc70b024af17ecf5f29a93ea01a536", + "c01b9c3a2ac48f74b9b5a8b7aeab8021609df541d0e634419dfe86a3fabc8c45", + "68bc58a55fc2b0652faf540279efb974a55bad2a1933c1b87db11358d1213451", + "b6c0adaf0cf27549785d198fcda1276a25d48698a03847daeff21d4ae3b49f89", + "ea4a5b93165ac8cff93c7fcd39df8978b2a08528b059fb6144482d54a4b291c2", + "f5065a179271057614045bd5aeab2502df2500e073591e6973205704d7ded803", + "f3594946a02aeed3df5ad17d71c3fa8d03b32af4b6e199aedb752c43449b9822", + "3929edee1cc42ce42362dd8b307d15e1f5b12539ad7ef39942ab10fdca17eb3b", + "22e284d1bbb777b6269f689edc0c41e52e5ee36f3fdc8e2bfa5180627c53f652", + "072bc285678af08694ae074d4f972bfca18a6dc618cd4067d4a7842d2c9aa288", + "b95f8e8bfef93ae29c16dd0cc358d8dac0b0bb05ca8faf6f18508904fd3c75d3", + "64241219dbef9cd87b4778f3c6ab0ade40edd837cfb15bd2ec9b90dbf57ce8e5", + "581f298970e57ce09bf83bb9d6cf715c8d207a795e28cbc75c2b9b93f956a94e", + "7d1dd08ec30d19ba93898e08313c9eeb7f2675a39964eb6f66c7697c879b0e50", + "89f51ce3b64f1e197877d538c00604eeff53f979f0133e5f14373c40fd803872", + "d561cdcd5965699bf8d2f01861c31c4c3361df24a9b0d3e2d81b8c97f529c0d8", + "a2269089a9f10abc7d3d1f63db86142ed33e4a47a928ee57fe7c0e96467cbde2", + "84d03d09918b84806a64d6b934b9f83ab9ac6c9639c3ba1997af0f0e26f2a7e5", + "a661d3729099fa24d0cf78eef271ad202e7e505f4fd9baf668f25e32f5ee77b5", + "21edbcc42f901bdec0741754560199d4c6c814a84fe45c542a902b3fe48b2462", + "d044026ee4bf865dd51ee7b79a6acbe08e6299e896601c9cc291bc779cd0be42", + "f637c2797f81a7e0d0035bc49abc5e787707dea12dc7c6377b0255c1bbeee797", + "b7a96f7dc7e1d83c2a5958ab50173864c77b459d38c9b7f2b16725900fb8b0b3", + "f716092d7bc1a68e17302b959ed0ad700227ba192b4c14bdec9312ff8bdf8160", + "79119169f5919448fc86be0ee2711946f1e48fae518d7c109cf5e20f969d924f", + "728ed9819e65bb55e9f5e12f8fcd9cc6df10e112b2624f1995ada85c8018089b", + "68f1fd152128649a3dbd24b05e58fee18784bf929a03de468cf6eb9f47ea614a", + "98c9717bd44b7f2798e8c79a1b2c68cac17a31c935702d1119a99bb159abe197", + "725e91eb6b6d52e2e221ab261125c9e8a30d8084760f650bb59c7d5eddcdc55d", + "dc4528b7d110563aed5d88b8ec2920243f845c04f1a2bb04856ba15a916ce9f5", + "12b23af2e8ca66406330672b1a47c905fe7631336e4c0724d64cedf9dd842fe3", + "9bd20faf38dfc3ea4258ac5db9ff47b7396b87be693cacb33695d0148927951a", + "41e763455676936a116904cdb33bd8820a40a1229012e0b416667daa8fdb3b57", + "002f1dcd0c9d9b95b5a1309125e1108fce4304717f3662703ac21118949c6c67", + "7c4517af1846bde66c1caa9c3db260745906e98578f54e2a8332e7089dc70702", + "184d1d9b0b55bd04169b728324707c522f5803a1e5b55e372d358da131c65f37", + "c1438a145763797d9b9653f7ffa923a62325493043f710a03df0148d15f5b246", + "bf5e99d0da0a9e9d6574b3448b5fcc9a65ace79c535121605983c592be0a7f6b", + "ca77fe5edb07920e0b16aefea564b680e083bfaad8b2ccceb5f2b7a1f8e365e7", + "fdbcf8bfacfcd69dcd27366224919aad7d2dbfbe25f7027b1147b68baeea6503", + "7593995498ca6f8c15095dcd20a00415f1d9417b9eb4b6b58cbfc3bab4001782", + "7bf436a1f7e22ea222dbe508603750bdcc259ece4089bd44dcad0904ff6e9dc4", + "e14acfa5ee08106a9032fe70b1db7434a4960b99257d3534a4c96f40f16fbae4", + "52378a2dedc439dd81ce21a28a1fbfe7dfe03d5b3473633289d1ea614b92de42", + "4bee1e47e6e655aa440902216f20458e7fa21ec370b6434c121e641753466b2c", + "c8739248ccbbea6e6be71f3a0de6ca090dac7d5da4194bfc946dc4eacda815b1", + "8086906efa62c08d34ea9d4d3eb01a634d9ff00c98bc61ee2d66629863f84825", + "db78c235c13105c638bd497bbcc364c282cbc1bf28d5f8b189c3ab5b14c0bc57", + "4afef79c314a280297a942cd115bf3c086922fab200d1d8bd6e5b58201d383e3", + "567a2ae0d173f23a985fb393afa38d5d62944528c758fe4acd88d75e8b990aea", + "7ef82e2d74c6b76fbbe19d36a6898362864dd4e905cef34c5cf652979a06a700", + "37947088164ef470b6d3cddf6ea78755794142b509dc31519aad2d7a88258e04", + "95f1d78ee20b35c304975ef4335bedf9b0e99de25fecf2498dbb0bfa2c92900a", + "0eb1ac83480cfbbc35b5ad21231d8fdb1dcedae45adacc830cb1201d5bf03b0e", + "66a1eef7ffb2395def11a81e8086a01a4502855bb12ccf38e0e3cff785353539", + "36bf7af36388c34443d2a2483398bd6c055d8ddbf32c2d87c95c5bbd7157c145", + "ec61f03a5d0a2fcc3ec03044f4962d60c1fc8dd997bed0a86f90b4294240c148", + "943e4f71b64ca562824621af0dd17032065ff6310013580b7c56f89850c2b74b", + "05bb3bdfa02cd14634891c5626ff7ee6528d33c1d188d1c80722b418ff01c55d", + "2648d7d51b729911478afa15d79f654e45c598182e0d2817ec4130ff692d4b61", + "9ae890a5353b8b6a05c8c4592877abf150acff8605dd3565c4ad481927736864", + "cd8fa7f6757787e6abe325a44a508da647661c2c6e9564acc2c33013f4fa8376", + "95a334d87055907d678f459b9545ff93e0734df31b2c4ec45ce3ed4f1a89087e", + "fa50e0b7b25f572cda4e2e4a3472defbe554f7200a376c950c7ecc2c4ee3989a", + "5134a2bdbaf9f256661653e94c0d0a24f3f79490a111703dd13d0e19a67a1cd9", + "3ee6c202d30995837b8b5da364079fe4bba21986419bc6b23a268af919e94eee", + "0fda4917eb28e03e6fae6417dc6ae865c29ecb596793103bac0d85c5236e902f", + "8608ff05a45aa1058f024426bef722d0716948be4e067f2158c66e34e187315e", + "e5bcf1326f0b07dd9c02c4c86fa62208febf97a28cd34eaf1884f3731526a089", + "4d6d99840a22d46619bce7c27439821ac48247d3a49908d702ded9c4c57ffb89", + "23d40bc276317e1a02706d3ebee249b34212babea10fc3c3b2f168a79cb4e1c6", + "51ff4b1f8b56137d294dbb81ff277b5959bcd18a213b78518fe81228dcdbd8ea", + "76e469a27ef211c58037d5a63c3817039845d13aca0b3052bc43845d011d44d3", + "7ef10411df7913cb103371ac71461d66bc34353aa89f2d24ae9b7c27a46594bb", + "12a89a55c1480a9036b3ab9ac862f78444e2b018b9a6e164880cbe9236fa321c", + "e20c61210c9d2ea8ebda093d4ee35359ff194e253a6bc2d68e9fa5b0786db808", + "52e211dae01d67003941bc6ded936225ecf9ec70a14dba6f2a21a84901c84f8b", + "4dd4ee02e06536dcba76bb8b98ec5d99835d5164a02559b904a9905fd72c14df", + "9cbb925c9390c4330c705d250ef32870d0ef070183421867d23b48415227e03d", + "b64def455083e1c6f46d8393fbac8a8d42c5fed736d3499898ed56fba3d79b67", + "d1a4fafce6a22e647ac9fb1ff87eced2b8748c008d6b7f75f44a56a5fd7309af", + "512d23517f3c49084c3c01551c6b83e0740b6f804b5d9020cecad0598f6d0d00", + "f0b1be77c8935b0727d2297fb32f46e7fc5380eb63386ae71dacb5ab16588140", + "0b21879e5bf4ce0b344020f10f23268560a420ca21bf14427a17de18bf80554d", + "cd2b8235c407ebd716e4e0bb311383e99979508dd1a4c111a22f2a8f848c1b57", + "296be9938f8e57103e687ef85f49f2ba254b74969b84d86d593ff9a72f19e460", + "e8c1e16c7182272ade8d1d7413d3aeb45bdc6794464efa1aaeed1ede1889cc61", + "f05825a8dfd50d68926f5287c2682a52f60f8e06705314df6f9fbaaa96e73c63", + "4a7496a09aca807df68278da0b620e20b39f1f298cc642678fc10dff7dea4c8b", + "b2a8ffeb8f2b98b5e2b78b7e31852fd2da92313c87610e887a9dc9f70e294f98", + "6aba914302a05d6896625e7dcca4e372becfb2fcfbc31bb782cafbbdebfd03a8", + "0b18e85a0759b7c29570d8c3f3154bfb258cd9c310015349d86b8c1d03b53fb8", + "2078ca3e701c5a6787cd48b58aeba6a1f64626f8a0971599298ad6c5954072b9", + "1560670d2e29b4ad742a6454adc05afe3fc864348b5c6adbffe92bb19aa561ca", + "294a972af8939046cb7c3bb12264e753a068be6df8a5f43468ebc183e4fcfdd3", + "413c65408d7cf1ca53765855d6066df405c7168b6cd88534d083e33403ac34d8", + "8d5f2484bf0a62213667aebfa143955fd5a9be5b97ef5c037532eae9fad11fe6", + "c4014c1a2d59cbc9c6b78c27677ed7fb71ed387ffe9930634c5b098d5e88ac34", + "9033a457837436d8e1d26a37802e5088c2e8e7409aaf86c359fe36481328dea4", + "c7e608373f61f0abdf7f2f3a2f0e4d5d8732f8621c9e148009cfda18315ceaae", + "d1a598798dd97e608646835a59e853f4c699e7799ec5eacd0c8f1a55ec281ec7", + "d80d1baef1b916554e4a11806fc726e0b27fcaa81f6573074bd119449c535bcd", + "ac6d10865fd1f4eba25b6727176f40ae4973e301a6bc98e8906b47d08643d4e3", + "cf0f8e194eb174a2fea2b4d1869f8eb3dc625e7bb5e7af35320bcc690c231ef2", + "bd7d8f0953401f35a5d68dac4e1bcf59a0cb1d8f96d81da5dda3cf26f29eaaf3", + "e0699e11fcca22ed1b0a3437bb48821f818c3ddf2205cb5626b569c4bd6fb85b", + "974471ac25151711bf096c8e4e6ce83d371324dc06a4e3e75bcccbd8fbd43d8c", + "eae7b5e3c5963f3b2d5ab0e0c0df605c799141380dfb4810cb67cd4ad094d051", + "48c26132ba9113dfe0abca76b9d708cf30dc6562f6cb8b67d8ed15f4bcd5b0f3", + "fe591189d6a958c7646fce16b7e8808d9f0a48f3d0d0f3ced4dce49c2ab1fc2f", + "55efc17f256b5bee261da022cf180e9938b48fdb67443dd585f0d31b3ce00f57", + "2801902506e483fa2d543874696e083d426b87c7a22ac5418d86c1e2cccdad46", + "e519402cc18181c479058922be5a9083cbffa9656053d23dcd28e20b33d4e255", + "dbec693e68c0aa405eb8a96db6225f2f586f4af798cc20363c086685feb09cfa", + "99e489b7add08ef4a4e3f19b5569452650d5df19f414b44734e4fd23dc16bbeb", + "34895cf2f8d26157f651de477b1f1de37b81ba875e911d010a5879928f514245", + "1f1bfbbc60e0921afe83fd1b7180991411a02d79718f4b3b142cee16a7b19e34", + "405a5774ebba5c56f02b260fed2fa3394b4eb287f1700f1d091ce52d9c9b9b0b", + "3aee446615c836bbd514b3eaf8b9ffacd148f541c639153acc2a0334d5c1bee4", + "30150429ca6d6acef8ede8fcd759f56769bd873827328c5135094a2e424d6763", + "acc25daf778add4ceea9bce2e636cc75b158b4a38ac33ddb5dacc181c581b8d7", + "f0a9abe3ad94539f6daa2c1eadf5cd65638939a06c74dede6201e4352cbaf565", + "d5fd794a0534f4b174cc7ebd3e1bbbd6eb9d1a3960ad72aa2f8f652157b5b281", + "28b0982aea21de348d95819089b7b639023b4acb789fc83c7d2f8ab4d9acdf00", + "8401c6b61d7ed591163e74c5a3c1c436f8bb280729dea084ffbce391cacfda28", + "c74d9e5e45a5a5e4438d2528693d98abfebc67c027872b017043feb9da364f29", + "8bc1e09f46430fd358426ae0c6f1d76945c062746d435769502b3eaa69aeb62c", + "00afa97c92c74a6979160421a090dc6421e56ee4daf3bcb7c34c12b309fd082f", + "806e17fdd51a589d25cd7db35d23b09c041b3bd0523b49b429182ca66ad27a30", + "0736d59354ccd473c46fb1f74a4e080dfacaa46bfa286cc880dd73f26b528b46", + "3a040d155e8c97580690c129339fdd5703810a4f5077958376fc6534bccbd64c", + "64148ca19f969360ddbd6288db15c77fad60eaa0eb2fff190f97cc5182cac351", + "02d77bec0e4473618b916df2e20c8e43120fe2e0252c8f481300533bc49ab556", + "3526433110d99834e1f435a748c65a07dbe50fad475ee78e39454d27f8f5a057", + "3aab4c563e788d38731ed56fd5493b44b8711d4ea8ae758e407491ba8ade7262", + "05fefca7e03a2b04e41f8ae26c7228178a7a7185f1455d9f3a3a05ec36b793e2", + "e3a711666bc77f06802c0752eb2a24ffbb23b958f862dbf618d8f373a08005ad", + "beed69c7572b102ba3c1fcadfe0b2550c3643b0feb17bfebdd2f1026913948b2", + "6468d0494ff5bf4c0d59f7f19fdd3f92e0cd1b9742e4c60f22a5b4a6c90292bb", + "8e42034a244affdf6d4b2e7e89b310cf02f71e0cb2252afea1f007a927ec3dbc", + "c49eac0df81e65bbf155c1de3932c66ca9337f989486d176c192efd8ca7511e0", + "1ef4b0f67c19bcee1baae11500c40675e838cbf9976c57a31778479bd73ff1e2", + "6bfbe43e9e183eb771eba0d4ac8677403ffcd336cb7c4eebc268cf5e852c6bf9", + "f55a7cf360d96f12e5b2d7c1c7f0c87e954eb1654123f52bbc1d39fed41ecb0e", + "28afde083d089aebbe09db81d796a2327763a066ffe62e0a3ddb851556a9de33", + "922a1cd85a1419b7c3c2a38a318d04dfe2f0c828d8943b7804680a0cd3438762", + "8a2e7642764343f569efc3de63cfe8a8f759251c572204e3b0cff0d9e4254b6e", + "e1bb38fad133cb9896c37ad4303003c06634588508f808b1963b5d6bd6b7147e", + "829f9846cd07c045f23f8f01983fb652b245f8b4e79cecaabcff1cafc3cdc48a", + "094a0f1c1cb4f0eaca17d3a69888296adfb8d18170514fd0e020065ed0728196", + "c2297975cfacc0d26c53b5fbf7eb1f8b9a8a47718874ed63a80cc20b4b7a9bad", + "192d81fa52782b48194ea1625830610925746771818e68244914b7c8b7cb2fbc", + "f64c8cad742d31bc14ef3d6ea0c89db97e2d10b20bc2d23c71b202d313d679dc", + "3b58ade9e7b84e8bc8e563821c2bd52c5e6d8ac65e080e8c6e981ee1bd8cc5f0", + "deb224a4d27208932ef64523ba6dbdac0ec97188fb837fade5ab508b1eb68c62", + "fc6ccf19e35eb799fdae695d29e1b8b5d973320a0652dde87868a2ca7817e828", + "dd09d65424ecd9ec27c512e46d6a7716d7486ffe60b829679acd3b32109b228e", + "7369537c9a8c711406fbda4be13ca61edca3ad81def0e1447937eeb9815c3a47", + "fe51147adbea48721aff2c63023173f64cc5debe02233bfdc57079aacdaace07", + "748bc74d0c5e0ccaf76691dcae78a4afe7685f8e3b609a08ff8d4e9cb14d420e", + "9edada8e22963650dccb068db2dafe304cb0cbd30aeb6cdbe2e6bd41d9d44d18", + "6e7bc6dc7c59cd81d6249dfd5a1db94a4ab3fd973ff1945ca73b3a979bc1e218", + "bf64fce5754eafe5cb19103e64090caec829523fb5fa26a3afa8bac484208c4b", + "80247598d1b9d6585a71cab39569e0957dd73413411c36862f0b5aa296ae3d4d", + "d9499b84ed0a76425383e7c7263f21b3d6558906f1517761291d70821cef354e", + "fc17afed7b1ecb4509d8d7886ea0af3d65d7aa4c4ed631893221c599b086b656", + "c9105ace9da12c7f5e4fc624226dd67ca9de1c48ae9d75d586918bbf0d0fd65b", + "11b9e2210d290d5d101d4a1c9022a231c53fc1f24997b5eb25c0bcb0e4e9b562", + "8278a30a8148b570fbe4260a67ae04dcac03b38052b9ba721e3fb7c4cc634767", + "35d8f8bb6ce5fb66157d803fe6d078e1a4a3c33c6c8e6fe39b31a8145d0b078c", + "9dee339e6d71ab0ad7da23f52e8b9e59c17c44c00149363999f752466f969191", + "cf3f6854312ce6366f29d6615cbe57a3c21eed4b60b667acddb014f34dba2792", + "c7a3180e04517fd68b89fdc4ae10da534218fb74764ac135888914915e974da1", + "7566f9ff8d4a58c92e9e7b4cfeba03fc37c6389fff7f887cbaee8d993f09aeaa", + "cfc4d5c0c0a434978c7b8ce5f00599b420c03653b3bd72f2e7e9f9e26a4487b2", + "a60c955439e9854a2de94f804b69257110ec3d3dd50b424468ceb312bc61f9b7", + "4da0aadad58310d341b45bb92f7fd62c88bed86a8eaabafc882f4f609606fcd4", + "fbeeecef255f59bb94adcc8d192594b803bcd40250ff6adbf6d718b9fdd1c3d9", + "f65371d7f6c88d0949c0524a48c048cc6ead38456deb991690215b1c39afb0dd", + "00e82f17de82e92a45311c78d0331038c1c362c83020676ae3b6414a75a92ae0", + "673cc40658d773fdbdc0c0c560903e0bed851cd1d3ec2d7f9c2b6b3ffb6b72e2", + "e01db88597fbc09d772c3b926e9f421260eab05e18b48843965438738b78a0e3", + "22c45e3a37a6d5ad9cdfe0d43fe93040481ae72a6ff615137f084244d1b4afe5", + "f8b18af34005e940d1b1948f303891bbaa63e9b9e7e3090f55cc7527b76b27ed", + "af45db83fac5b002909ee256e812cb6e221379ac45a6091b36f895497af415ee", + "737ee79f7dd17c2155303fc2f6c26c14e705c4ca4136c315c902c378c9755ff4", + "e197f9435340641bdd4cf0f7c3baac6216aae307ca20735e4d35053a5bd93c25", + "41c6044bd31ce9f72a603e6c1c68133bb007dd23410d949d5e410acc24d20137", + "a5f6c2185e2b9b240ac1726907216afca40f56737c91d21d0642e208c89cb5b3", + "204a47098478b062982ac41586a7e7762256337171a2f987800cda1e55b2e2c9", + "b94dfc31e60ad1de1e4e38a9b8133d4180d414b006319d01924f6ed0f0c7e3d0", + "ec9011ed62ef47f9e4c07312e286330e0747b8b674fde4c4ed537773e97cb708", + "1145ccec21799fbf42c4d219b476424d812d6201b51a2dbede9f321cf9882826", + "2b6295038ad539ea5720a8d65710d7c17c990cfaaa5d829b0eaa6f64c018182c", + "2003589745a936c42de4c8228b2e246e19f3e7c6089bc2236dbd6eb3eb191415", + "0e5a1fd1ded14dc6d3dec577e220262bdb3a6afa0529ad0e8f119cca0e5edc8a", + "4bb67c218418e1662a0e24f6b475e9842c8f580ef4201232c8d9ce7da792cb2e", + "503d3620497d9a62a8088abb399fa83c34c31c19963ddfbd4aa9629039285dd4", + "e473c828b8e78455ab365b980a77b239b60b203e5fe81653a64f617b4ed92368", + "ab0fb491ac78556ff66ad20b03dbc2efc5c4696c46bd7cfb30ab7e056c7778ef", + "a70faa6d455091542f11ae96840bb578a676ab42130d4e0477e1f91d9aee0332", + "af19977038a44338a99789713d8e9d5141065bc6d72e5974dbafbdbcf1a4ef86", + "c84ca704087ff9f06e6778ea59ddd631acb7d37387dd552341580ce2a9da6362", + "9ef9391fa9006c8842182fbdeb73dd5256514af498c3840c12b61d4c59a416fe", + "5a77ca44ac24115482e8ec6e89780604f79c5bd48d651707274e744010c9b49f", + "7b7e16caf894ff8e976eddd0c10b18d925307dc96b915f8a1447cbb12f06dce2", + "07cadd38ef710cdb27ed978723b508d2d5b7b87b1a02535abe8c56ddf78c35cf", + "2f78fc69a3c2ea7e0da31c8d7cc5099100fc6885aa3a158d6f375364c6b4e6a3", + "68f259bf579fb970109879f77252b1ad0da05ae9ec696f8a915cf2b0b14433fe", + "0d13967574ae23ebf6a618e2855f6b0818960e5163721c8483596673f494a2e5", + "6c6775f923f19a87f59079605f95ebe2afd4eb8cdbf40ccf19b5feda64dd4f0a", + "bbc130d72714796719d965e0f9a15912579ac2e1e13ab60ed19385c9f80e3222", + "383be58623ffa6084cc31b875836424152c54c704c28f62b130aede7f52cddd1", + "d9d2d14decf9833e6d5f2f1545d78f7d45e83b766b90eb572840e4f11a01b798", + "7e4db9c54b3ecfbefd4eb83b5b2000d7cf8b68a7e78a7441e8115c4aa3418899", + "98b76848aeb05b1d2adeff45bb502aa05933c26adceeb15fc88ac23ea145e5db", + "987a66555d461d0448beb9dcdb4a9bb6b1ceaef005a90c3bcadef49222c2c554", + "465ef3c52da280e35ef838d935e2da3b220290bbc4bc9309f53fa52010ff1b5b", + "624b6b74b4838ef4b46a3be11e49f30609973b980b96bf0b517fb1df4a42d5f3", + "6626746f3947571ebb0283cc51fbf736261daacd4bf7fb3dc08344bb5d577374", + "7631ad0eed419d6e823f64cb5f1395d64b6352c25fb211576b034422284c3942", + "ba0aedeb830703f68de1ac12425f885844d92fd4f51ecdcaa72603b3d5285778", + "4d0fdbb0602ac690831393198485d38bfad0a63a009460bd6b56492e518436f6", + "ad5baa9cacc158f266a394da66e58ca6fbf1f3be74ffe2f4a47ece08ee9b79ad", + "c7358ff360c695160a3fc4346993871dadec80578936783c29936558992bc55d", + "055268d5313086de02e8d37b38b494bc6d55a7b02fbcf526bc0dc5e896e2ff5e", + "c1f881c84b53cab36cc3834afbf0f3997e81e8fcf6c8cdf20ed25012674b2ff9", + "921705111671c5e2574187b31b9f937a28d2e124d19e5c0d5cafc00f7155cd64", + "127c6fc84d4d82e62b04b92b08debd909273df038b59d78b76170f0599ad831d", + "839247cb2add83eb3fecd6145ab8df737cc58a3e545d7278d67bf0e2ade46339", + "ab7863d4a2805e7b13a0d7daa84b5cbd6a9fc69bfb2d109d7865c5cd71f32542", + "002c6ee260c8e77c5e4ed79934bb4625eb06363a94b212ef35fc986fd39cff76", + "98d7e3165791bfe2d3ce51a17af05e10f7ccbf9b1729492f72287c137cf2d8aa", + "4ed3199919e61eafe9c1734af34ec7cad5f868963a93d922d930c4ebc69088cd", + "948d35f741d79e0fe1159bdf6f98519b79c82f2afa60f624331ef487c1c16506", + "ed8e6827920031f9c22c034ad764fe7abcb4b09420add34e1d6dc1bb65840a1f", + "6a22c95d7b5afc65cdd5fc1cc872190ba426830b4499599f48f2fc3e10abae48", + "72cda147283c250819748bece46401c4eb43a587e59ac54ff516bb29025b936d", + "b0eb1ae58012532232aa94b178f2236bda7508f525f27b0bd9bf6af7916844c0", + "45b9232b100febf3a520b8d2ed53bc420c3d60acbc6c2fa475f5ef369b4258c0", + "71f2db177be3212a0160975270902db6c11fbd5bd02827f7b4494a4f9cf111fa", + "90c81403765fa5e3435631918f935233693766f659ae1f24d08533dda5758d59", + "1efda6e2f9ab07755164b04b031a63ae035acd75a86c2bca2865b1a5ce8ee410", + "c120447d16b7891facce7d30fa081278a59587e50e6d8994c2af538830474586", + "ef07da70de74384832c354bfb36a32571a6b5b7866848e4831d22a136b7772d8", + "8132c976326028adb0eae8297c6e19b1c4aae44b8473c98bbd01e01f54c21b4b", + "52430aad9fed82d0dbfc6c75bf5c750c239c9ab6055409be9041e85d53ea75bf", + "afa075ce2ed611ae5d4a0e04c2fbb55c1643c64188fe9fcbe4a04c5eb513968f", + "af54b32ebe980b34a04509d9b677d7d28ca65e20a45b6944e3b1ea4e8f94644f", + "c308c9e5ab2f6679dd13a917f66785684c1995039ecf116d11880963a4ff82c2", + "952610f437d9fe01427f5ed171dd52a32f0f78850ef4d0445997fe873ae31595", + "273de9495acc836f54d91376fdea18bc93b0d5fb67b568e58d935094de753715", + "3e39aa9f0817acf05bd3e15cf016c2289f4569bf5c36f21efc69e59c0c2f2d6a", + "4f9e23b30ce1c44e2c68a5949fa68b21290cbb6c6719bb181948e923f5dc670c", + "18eaace26a1aa41317d0dca58aed12ff782dc1441e3f7d41de2f0995461ff328", + "b2b10f9d95789fa1b0c3aa376fdafa0966ca91fca3dadcd30b8d0780506dd757", + "1df31c5b6fa68027eead9177f153d7389a49f7e679a6e4dc505ad33743fa1883", + "fc0fc42fd8e0739df4a98191a4fb4543119f594fbd98b60cc3287442fc0f4cb2", + "d4799ac36e0241a21fb255aca1f4f2482ef3c22340b17bb0c79181aa5e0380cc", + "5036577b6342a037264078b1deefb9405fc759d6741a8d1669a697a01982dccd", + "d8f0652ec362292de21b394118f0e97c295b095fad3911c408889ff58c7d5c3c", + "18d8c9f32bfc5369c9916e62bb67745189172ac8ebda9f057d2483d985b76880", + "8112eb540bbfd0c52f8c91b37965fea9abff32c6a811d900c2333b68e51bfafa", + "41ead8fd0e56c999e1c7941e62baada26ee292299479b4bf0bef6ebd78f484f9", + "c2203a109b1f301348f12e676a0bc7fe4a76246e01c3587b1ba5f50c795e7abf", + "bcd7f57346f6d88bf40ff9a252cc670751cc9abdd7c05794274b1bc07924b241", + "0c394e1ebdfb57a7877c39e299ff0ab9563f878d14ebe9dbbcf189faf0b36a5f", + "d6da8fec56526e5a0302240b66474daa816893a0e5ddd69db367119646f20018", + "ee11047493903e61cee0d9e0ad9b70d3ded9d55973ac7f5566206052be356f1c", + "f8a2cda7e40d5c0e70c18b4028e2d4064707bee57ddd36217135de83bb092e26", + "5e8e12ad700da95a02d283a427a1a412b6b6629cf43be34f9e909d4c70ba9627", + "199c579cc113538ff7a5f8e57f3fc5175d21548c64faf5f05602428227e0a333", + "f820f4a30601c48baf125a6f7267de4252f406ac27b6f5a1ec161786d068eb3c", + "5c338ed7fb877efe70781d788effee56e8a4323a23cf3c44a61b1fe0db90b66f", + "0d6c6e2ef5412dee7ec990a5646039a584f786eecc471a098ba09a85ca1ddbe4", + "8ad775cd9036b739dc54ff38e4c6bbc686c8f810e6051bb9df0f70e733eacee6", + "54fb7b014b9a040561e4856ef367a1eebe5a24fab6a6d4502ffbc48503f5c189", + "a87612f4d9338da64fe25432e9fae44304594f06d6159844b731091167baecce", + "59cf4d5e20c5861b74999e91dc8c6e1c2d92d7f07559d7312a0f84b9ef47fca4", + "118a5f8431aed6f3a3ad93afd2d54e3147a62741a9fc328041d8f231d186f3d4", + "15990e18abd42769d712162fb8cc76be481d275a3ea92d85ae43a04bea61e9b1", + "749e0b74cf98b8a066090abcfde46ce4834f53dca5b6487d782056d622771678", + "59eeb062fec2aae83b00553bd4a26bda7672a39dcf0423cbce50af49403c9809", + "510582485c7f7326521f8afa26f8f234b8b6af5bf67e37997003921d2edb269f", + "43fc6c0d0541eca92370e3b0872c77dc63c5bb375262c775d2792a2bb3e32fc1", + "8fb6a5b8174912256c7ca021f2b0ba79bfdb0463e3ff2040e912d3b11d83bb9e", + "8985ff75c7097565ebb463078f33b5b1f68919bb1b9506edb447c75877e888a2", + "3e2e040047f83b08218dde2ec7c1c644421f0fc422632168de936e21c88a0d93", + "a441701731be9a2360c7eb3d97c39c8cf75e1696939c877f689713fd14fee0c7", + "c04d16210e2cae3c7d46c6c07545b4a1ccb725581238af8e42df1b0326db273a", + "feefea46c98ae8bd891274a8074742f50c93ccc4c8e10a0fa6dc5a2423976c74", + "2e6ec3bbd3ac9537ac06132cb3a7dfb703aba8458ed3040bf8dc9ba435d1274d", + "3a437b4861a0d5fe9143368db5c35297ae4d3a05f20285d24e34b3ec1e7dd7ec", + "6fc78e923c1456e64c93c0f4288425be7d30564af71cb6b3cef360ddd35e7a37", + "4da547aed58e9962657b61f119f63fd0e0950a4fef783ee292881af8bba72c87", + "c93d3fa5cd5c2451443709a29de2ef3f4a602cf0799ec24f54e118e651196294", + "aa1cdbc51eca5ea16aed239c4d4329622079e3d6f76836429db09c304aebe2d1", + "b4367cacd19b420ecb5440a2f03f46622f17b8003b5dbdf27efae5f8c4ae2ac4", + "a28e20ef4f2ecbe3e794240023c747ec994718f575baec5f7ef8cb111779a226", + "215ea5147cbb10b13ba81f6e1727e655289fa8c4685e028e4e027582800ef16a", + "2836ba8184da8cb7a2dd53681ae12af28b57697b875c1f60800ce2bc4ed4521f", + "24088b8dcced5ccedc7b403e5b227e89ea77f01f9399df08dae8c7b77eb329cc", + "8caf687ca5db7f2203a245406242b98338b334fb332197a6ea58814a0d7a8e08", + "7590c56dcc7dac27017381e65239e1206c9c53092d96375ab582383e906ae865", + "00992135e307a49ab718b0032a773975490d53a8b251f03b455c0f9bdd1a1e10", + "65435d8a9c2aea66c2cc642d2931ca2b0e67314597025df1737bceabb2ab2d6f", + "29b0e5180f10fa17a48c317ba4e13ab65d076c8b5e23aa0252acd9a1c5d48b1e", + "5443f385a3c587073e736d6be9b0d6a0d202a5dc3be9f6c9bb866f12b82369ef", + "33fa1f325610c4aae037daa0e9944a71b60a1e83904952df7cff3681c0745c1a", + "3cc503d446afdeb01a5614500845bd6b59516f219f2e2ce5d0e7ff1f5ec1f884", + "1dceb50d74ff488469bd8ea0425e28add8ac66e2d7e472010f90c107df17c976", + "49289512ed481461ba5812ea8ad4c97b071f1d347fc679099bee16ed9b7c1005", + "dee47629ad03b87340b7581f47e5032f3d5d3f1e70b1ffcfa428828486a0253b", + "108e74c8359893e8f32efaa6c7ff1ff766b53d59d9919733380536a4c5959540", + "204f362c44a728c7a20a3e59de7b36122d8849d2e1a20f4c53f1d985031e2542", + "8d2713f2365085d9fc6e3c5e6b45cbc65e223f6bc51c83c56845059468bc7864", + "b76191caa23db17cbe680866c0f7ed0ccc03facaebc52d6053028223fd1e1388", + "4ed1d1eed31fd22ab3512225ab5a0080bc16560aa6a817cb03a6eff71f0036b7", + "b470a3ed6f2f0d630d24e5f3c35563ddbe56d86b63de9c33cc0c599d6d0fd609", + "63f0c8bab72b56dab11fc92348237cdaa20a3b88534b8dd53439a3a31f35e69e", + "e5f28692800022f340524fd5533ece707cf8bfbc6eab19a5d4ec78fb0c9a399d", + "627adc31067bc5f7c58526087d59bc3e265ae2a5ed23f13f8736086578688b79", + "f7ae571c8c0842a2a87fcd834a3030fe73c87ba0120e45a02514818ad05042c4", + "12b35f61b0301bef43a334f12dc6d3b7e0930c75ca5db4fa161c47d66fd668b5", + "0934a9016cfc93f5a54b0ea93c00e3e00cd6b88d41faa0491285abf9868e0253", + "dc9f3af956c3b0e41d101375d4c341e0c7cfc65e85b98687a49f8830f0ed15b2", + "0fd3d9c2abb76b5edf220f96e9c4235bdbe2dfb00b5ece59efced6f7c2c46fb5", + "b7405add7715e2b41bda09f0cfa80199ee2de1d5b07019664409f3a04eeb37c0", + "b2ae7cffc5251f98997bd62fed6d30a760b7d77c11dca2bdad8a91d488f5d53b", + "7bfa7482ced2c9eda6192ce76b5036f64e0adb44c688773a00ad8ca384cb5abd", + "fdb3da878c0b262284cc0a47a15c28f22f4ef06a5cb353e01317fa65a23a99f2", + "33336d070c7a4f6fc91a0423fe0218e197d3741ee0d2e90623fe1877adbc78e6", + "95393c32b2569654b7db774177ba4a581af22ab8b582b2c24a84edf1d74ccfb8", + "dba97b5dd22ad675d3728cb0b22ab02d1a1bbd4050ba5343247736dd250f930c", + "2b810b12e4b080e096f62f7b19a84b614b438551a92c14dc52c529c8111e7b1a", + "81ecfa6b018411cd2a09adb4da2599325f43017a32984cbe7f89780c1df5c14f", + "e910e92e52c6f86e3d1966e6697383953f60a69c92dc0297277324a5eb89b272", + "28e37a1c5e19fe1ebc9e85a1d959a6f7ec2e2bbf2d73b0f35f9b28148d77a341", + "ba097539a30f9f790e6dab1762d90936200d1893f2fbbbc3368dd9b1ffb702b8", + "e198415324f15736e8b760a525a338f9d33cb416698326692245fb0c6455ce08", + "0ce2f13a0a0418bed74eb9cb1d2967a22b9f9b8087596ea7b58f3da609adaaa2", + "f47e90718daccc0c7b5ec8c617be64c605b3a7e13ec633b0227b1fb7fedf0382", + "831156b10e0c3aa9483747bbf92023ce7cd1fd84422eb3fc6dec0ff391d0806c", + "382d9684163d316b9a98ccb8dac747a43ada06870dab1c20eb90c07d0f85f9b1", + "42a2a82cfb969e69f1b3020b2f13544a4a54325b9bd40bc2fc1a576549a92597", + "2f15b390eb000ade274c033c524a52a4ced756289c3d88a30f7ea492f60084fd", + "589aaed54e6d50e53a9b4a0ce5a8c52067f3226e4d165528a24220c6fd0cf92b", + "8789a8c91c2a88bd6068c0fa68d017f0a8d269547127842aaeb0ca23f0b46d30", + "3a38c96bd40123a324c4943379a0d7d457981da414985befb2d5150dc74c1761", + "6dee983efea76c44713ed5e3f00326a83fbb06b69ed799e8aa6e50ea512711df", + "e81eac89bbd1b71d7ad8ad9057c725be8f02040e047fac7fb34e3a3895b337b8", + "f8c0aeb530166c3e9a7c966c0cde1265f1fd4f0c94547584b760d33543c93afc", + "f59e819b06b0898a8328f853f57e0cacda0bd0a06014d5ba113d3065cf514d2b", + "add7a2fc0382fc4e3f20f52fdcf876f2065437cd76465b3415ed4c7cd971b4b1", + "afc41762a7e186250ad16e95c21f4272c93f745d4476bb40cb5099e7d96a9775", + "158bf1b8cb1541262403a220f683d9fe9f92e0a22104b699e35381876b174915", + "7d4f909e42952601a817c218f96d978b8cc96e9c152b82370582c85994510ec4", + "3598053f0925352858cb1f491b964c21806e9c42e278346a9746cc8a5a3f57be", + "6ce979f3f8ad04e5779b93828bb2fe2f6c97791864e0944bca6e602c8fcb5586", + "fe257d720374dc993c025e04b4f904e07b0d9898c7a18e3ce4e812a82e8858e9", + "9d8f03817895404850afbba484bb2bbc743a84d75f0311f11363ad5cd17b6631", + "8e9c08a0bf999699f1f672e8c89d151c52f77cd2b3b68a9319fa0ad8a39eef4a", + "30ec07d52a3fc1dacc203d40f4d73a46e26bb7bad2d3004130cee9aff0f9cd9b", + "f26b153e51c8354531fee29d347d0d2398776c8dc4e6f6d3890146037bc4288d", + "2f26e4e16d9299efe89b7a3c0f2926d60d7e68fa9462e2b83853e391b1a8dc0c", + "431cbf26e799edbe8f431386f163f2625d84410d90b351bd5c547d64525b0438", + "2cfbea26cfca9e51e9b271cf0726019526ddd586f73957389d57f6f83a7f1ff8", + "09b904372320be6ff8d30a5f2d65592ea60a12d75802c33022be9bdcedc11219", + "a7c3b4a9cfc4d0666cb6fdf1aa08105a8e5d68614544dab769c7676d93c83437", + "1fc9749c6d3bd2394a2dba4763135496bdf3586874073eb3d2557bd7256df040", + "e394e331ec1d5b496f882958d27324bfc9d9505824ca4238c2507f4f08f6606c", + "7b652ccf180d21004d88a0209b85048d940572caf4c46b4c4837039fb71d33a0", + "c5742cf938a6985e0b1912d99f530390d6ff1cc76c47e5ccc273a995492e3bad", + "695770629d1e4a7678b4251fc42a5779660b82ae3c3817671f75012f6d47b38a" + ], + "time": 1587171526, + "mediantime": 1587168930, + "nonce": 4111305375, + "bits": "171320bc", + "difficulty": 14715214060656.53, + "chainwork": "00000000000000000000000000000000000000000e9baec5f63190a2b0bbcf6b", + "nTx": 1487, + "previousblockhash": "000000000000000000106b8cb739dd3c412613aa5d459a739794ec2572a74c10", + "nextblockhash": "0000000000000000001263a5a98fb5f732bc5cbb3de807b8f7ab78f0c3ad0bda" +} \ No newline at end of file From 4f0b32f705c80c2cb83041c128d12409e6a5ccee Mon Sep 17 00:00:00 2001 From: Igor Artamonov Date: Sun, 19 Apr 2020 20:47:16 -0400 Subject: [PATCH 02/13] solution: working rpc to get bitcoin address balance --- .../emeraldpay/dshackle/rpc/BlockchainRpc.kt | 19 +- .../emeraldpay/dshackle/rpc/TrackAddress.kt | 17 + .../dshackle/rpc/TrackBitcoinAddress.kt | 120 + .../dshackle/rpc/TrackEthereumAddress.kt | 65 +- .../emeraldpay/dshackle/upstream/Upstream.kt | 3 +- .../upstream/bitcoin/BitcoinChainUpstreams.kt | 15 +- .../upstream/bitcoin/BitcoinUpstream.kt | 14 +- .../ethereum/EthereumChainUpstreams.kt | 12 +- .../upstream/ethereum/EthereumUpstream.kt | 12 +- .../upstream/grpc/EthereumGrpcUpstream.kt | 12 +- .../rpc/TrackBitcoinAddressSpec.groovy | 95 + .../rpc/TrackEthereumAddressSpec.groovy | 4 +- .../resources/bitcoin/unspent-one-addr.json | 434 ++ .../resources/bitcoin/unspent-two-addr.json | 4514 +++++++++++++++++ 14 files changed, 5269 insertions(+), 67 deletions(-) create mode 100644 src/main/kotlin/io/emeraldpay/dshackle/rpc/TrackAddress.kt create mode 100644 src/main/kotlin/io/emeraldpay/dshackle/rpc/TrackBitcoinAddress.kt create mode 100644 src/test/groovy/io/emeraldpay/dshackle/rpc/TrackBitcoinAddressSpec.groovy create mode 100644 src/test/resources/bitcoin/unspent-one-addr.json create mode 100644 src/test/resources/bitcoin/unspent-two-addr.json diff --git a/src/main/kotlin/io/emeraldpay/dshackle/rpc/BlockchainRpc.kt b/src/main/kotlin/io/emeraldpay/dshackle/rpc/BlockchainRpc.kt index 20edca5d..0e34507f 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/rpc/BlockchainRpc.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/rpc/BlockchainRpc.kt @@ -19,6 +19,7 @@ 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.SilentException import io.emeraldpay.grpc.Chain import org.slf4j.LoggerFactory import org.springframework.beans.factory.annotation.Autowired @@ -31,7 +32,7 @@ class BlockchainRpc( @Autowired private val nativeCall: NativeCall, @Autowired private val streamHead: StreamHead, @Autowired private val trackEthereumTx: TrackEthereumTx, - @Autowired private val trackEthereumAddress: TrackEthereumAddress, + @Autowired private val trackAddress: List, @Autowired private val describe: Describe, @Autowired private val subscribeStatus: SubscribeStatus ): ReactorBlockchainGrpc.BlockchainImplBase() { @@ -50,12 +51,20 @@ class BlockchainRpc( return trackEthereumTx.add(request) } - override fun subscribeBalance(request: Mono): Flux { - return trackEthereumAddress.subscribe(request) + override fun subscribeBalance(requestMono: Mono): Flux { + return requestMono.flatMapMany { request -> + val chain = Chain.byId(request.asset.chainValue) + trackAddress.find { it.isSupported(chain) }?.subscribe(request) + ?: Flux.error(SilentException.UnsupportedBlockchain(chain)) + } } - override fun getBalance(request: Mono): Flux { - return trackEthereumAddress.getBalance(request) + override fun getBalance(requestMono: Mono): Flux { + return requestMono.flatMapMany { request -> + val chain = Chain.byId(request.asset.chainValue) + trackAddress.find { it.isSupported(chain) }?.getBalance(request) + ?: Flux.error(SilentException.UnsupportedBlockchain(chain)) + } } override fun describe(request: Mono): Mono { diff --git a/src/main/kotlin/io/emeraldpay/dshackle/rpc/TrackAddress.kt b/src/main/kotlin/io/emeraldpay/dshackle/rpc/TrackAddress.kt new file mode 100644 index 00000000..18c624c5 --- /dev/null +++ b/src/main/kotlin/io/emeraldpay/dshackle/rpc/TrackAddress.kt @@ -0,0 +1,17 @@ +package io.emeraldpay.dshackle.rpc + +import io.emeraldpay.api.proto.BlockchainOuterClass +import io.emeraldpay.grpc.Chain +import reactor.core.publisher.Flux +import reactor.core.publisher.Mono + +/** + * Base interface to tracking balance on a single blockchain + */ +interface TrackAddress { + + fun isSupported(chain: Chain): Boolean + fun getBalance(requestMono: BlockchainOuterClass.BalanceRequest): Flux + fun subscribe(requestMono: BlockchainOuterClass.BalanceRequest): Flux + +} \ No newline at end of file diff --git a/src/main/kotlin/io/emeraldpay/dshackle/rpc/TrackBitcoinAddress.kt b/src/main/kotlin/io/emeraldpay/dshackle/rpc/TrackBitcoinAddress.kt new file mode 100644 index 00000000..0a540114 --- /dev/null +++ b/src/main/kotlin/io/emeraldpay/dshackle/rpc/TrackBitcoinAddress.kt @@ -0,0 +1,120 @@ +package io.emeraldpay.dshackle.rpc + +import io.emeraldpay.api.proto.BlockchainOuterClass +import io.emeraldpay.api.proto.Common +import io.emeraldpay.dshackle.BlockchainType +import io.emeraldpay.dshackle.SilentException +import io.emeraldpay.dshackle.upstream.Selector +import io.emeraldpay.dshackle.upstream.Upstreams +import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinApi +import io.emeraldpay.grpc.Chain +import org.slf4j.LoggerFactory +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.stereotype.Service +import reactor.core.publisher.Flux +import reactor.util.function.Tuples +import java.math.BigDecimal +import java.math.BigInteger +import java.util.* + +@Service +class TrackBitcoinAddress( + @Autowired private val upstreams: Upstreams +) : TrackAddress { + + companion object { + private val log = LoggerFactory.getLogger(TrackBitcoinAddress::class.java) + } + + override fun isSupported(chain: Chain): Boolean { + return BlockchainType.fromBlockchain(chain) == BlockchainType.BITCOIN && upstreams.isAvailable(chain) + } + + override fun getBalance(req: BlockchainOuterClass.BalanceRequest): Flux { + if (!req.hasAddress()) { + return Flux.error(SilentException("Address not provided")) + } + val chain = Chain.byId(req.asset.chainValue) + val upstream = upstreams.getUpstream(chain)?.castApi(BitcoinApi::class.java) + ?: return Flux.error(SilentException.UnsupportedBlockchain(req.asset.chainValue)) + val addressesAll = when { + req.address.hasAddressSingle() -> { + listOf(req.address.addressSingle.address) + } + req.address.hasAddressMulti() -> { + req.address.addressMulti.addressesList + .map { addr -> addr.address } + } + else -> { + return Flux.error(SilentException("Unsupported address")) + } + } + val result = upstream.getApi(Selector.empty).flatMapMany { api -> + val addresses = addressesAll.sorted() + val results = api.executeAndResult(0, "listunspent", emptyList(), List::class.java) + .flatMapMany { unspents -> + val result = getTotal(chain, addresses, unspents) + Flux.fromIterable(result) + } + results.map { addr -> + buildResponse(addr) + } + } + return result + } + + fun getTotal(chain: Chain, addresses: List, unspents: List<*>): List { + return unspents.asSequence() + .filterIsInstance>() + .filter { unspent -> + unspent.containsKey("address") + && Collections.binarySearch(addresses, unspent["address"] as String) >= 0 + && unspent.containsKey("amount") + } + .map { + Tuples.of(it["address"] as String, it["amount"] as Number) + } + .map { + AddressBalance(chain, it.t1, + //use toString because toDecimal makes rounding + BigDecimal(it.t2.toString()).multiply(BigDecimal.TEN.pow(8)).toBigInteger() + ) + } + .plus( + //add default ZERO value + addresses.map { + AddressBalance(chain, it, BigInteger.ZERO) + } + ) + .groupBy { + it.address + } + .map { + it.value.reduceRight { x, acc -> + x.plus(acc) + } + }.toList() + } + + + override fun subscribe(request: BlockchainOuterClass.BalanceRequest): Flux { + return Flux.error(SilentException("Not Implemented")) + } + + private fun buildResponse(address: AddressBalance): BlockchainOuterClass.AddressBalance { + return BlockchainOuterClass.AddressBalance.newBuilder() + .setBalance(address.balance.toString(10)) + .setAsset(Common.Asset.newBuilder() + .setChainValue(address.chain.id) + .setCode("BTC")) + .setAddress(Common.SingleAddress.newBuilder().setAddress(address.address)) + .build() + } + + open class AddressBalance(val chain: Chain, val address: String, var balance: BigInteger = BigInteger.ZERO) { + open fun withBalance(balance: BigInteger) = AddressBalance(chain, address, balance) + + open fun plus(other: AddressBalance) = AddressBalance(chain, address, balance + other.balance) + + } +} \ No newline at end of file diff --git a/src/main/kotlin/io/emeraldpay/dshackle/rpc/TrackEthereumAddress.kt b/src/main/kotlin/io/emeraldpay/dshackle/rpc/TrackEthereumAddress.kt index 11d2a902..baff25ee 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/rpc/TrackEthereumAddress.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/rpc/TrackEthereumAddress.kt @@ -51,7 +51,7 @@ import javax.annotation.PostConstruct class TrackEthereumAddress( @Autowired private val upstreams: Upstreams, @Autowired private val upstreamScheduler: Scheduler -) { +) : TrackAddress { private val log = LoggerFactory.getLogger(TrackEthereumAddress::class.java) private val clients = HashMap>() @@ -83,6 +83,10 @@ class TrackEthereumAddress( } } + override fun isSupported(chain: Chain): Boolean { + return BlockchainType.fromBlockchain(chain) == BlockchainType.ETHEREUM && upstreams.isAvailable(chain) + } + private fun startTracking(client: TrackedAddress) { clients[client.chain]?.add(client) ?: log.warn("Chain ${client.chain} is not available for tracking") } @@ -99,9 +103,6 @@ class TrackEthereumAddress( private fun initializeSimple(request: BlockchainOuterClass.BalanceRequest): Flux { val chain = Chain.byId(request.asset.chainValue) - if (BlockchainType.fromBlockchain(chain) != BlockchainType.ETHEREUM) { - return Flux.error(SilentException.UnsupportedBlockchain(request.asset.chainValue)) - } if (!upstreams.isAvailable(chain)) { return Flux.error(SilentException.UnsupportedBlockchain(request.asset.chainValue)) } @@ -128,43 +129,35 @@ class TrackEthereumAddress( } } - fun subscribe(requestMono: Mono): Flux { - return requestMono.flatMapMany { request -> - val chain = Chain.byId(request.asset.chainValue) - if (BlockchainType.fromBlockchain(chain) != BlockchainType.ETHEREUM) { - return@flatMapMany Flux.error(SilentException.UnsupportedBlockchain(request.asset.chainValue)) - } - val bus = TopicProcessor.create() - initializeSubscription(request, bus) - .flatMap { tracked -> - val current = getBalance(tracked).map { - tracked.withBalance(it) - }.doOnNext { - startTracking(it) - }.map { - buildResponse(it) - } - Flux.merge(current, bus).doFinally { stopTracking(tracked) } + override fun subscribe(request: BlockchainOuterClass.BalanceRequest): Flux { + val bus = TopicProcessor.create() + return initializeSubscription(request, bus) + .flatMap { tracked -> + val current = getBalance(tracked).map { + tracked.withBalance(it) + }.doOnNext { + startTracking(it) + }.map { + buildResponse(it) } - .doOnError { t -> - if (t is SilentException) { - if (t is SilentException.UnsupportedBlockchain) { - log.warn("Unsupported blockchain: ${t.blockchainId}") - } - log.debug("Failed to process subscription", t) - } else { - log.warn("Failed to process subscription", t) + Flux.merge(current, bus).doFinally { stopTracking(tracked) } + } + .doOnError { t -> + if (t is SilentException) { + if (t is SilentException.UnsupportedBlockchain) { + log.warn("Unsupported blockchain: ${t.blockchainId}") } + log.debug("Failed to process subscription", t) + } else { + log.warn("Failed to process subscription", t) } - } + } } - fun getBalance(requestMono: Mono): Flux { - return requestMono.flatMapMany { request -> - initializeSimple(request) - .flatMap { a -> getBalance(a).map { a.withBalance(it) } } - .map { buildResponse(it) } - } + override fun getBalance(request: BlockchainOuterClass.BalanceRequest): Flux { + return initializeSimple(request) + .flatMap { a -> getBalance(a).map { a.withBalance(it) } } + .map { buildResponse(it) } } private fun simpleAddress(address: Common.SingleAddress, chain: Chain): SimpleAddress { diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/Upstream.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/Upstream.kt index 9c20020a..f70a64b4 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/Upstream.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/Upstream.kt @@ -33,5 +33,6 @@ interface Upstream { fun getMethods(): CallMethods fun getId(): String - fun , TA : UpstreamApi> cast(selfType: Class, upstreamType: Class): T + fun castApi(apiType: Class): Upstream + fun , TA : UpstreamApi> cast(selfType: Class, apiType: Class): T } \ No newline at end of file diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinChainUpstreams.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinChainUpstreams.kt index d34649b6..3f1fed75 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinChainUpstreams.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinChainUpstreams.kt @@ -5,7 +5,6 @@ import io.emeraldpay.dshackle.cache.Caches import io.emeraldpay.dshackle.config.UpstreamsConfig import io.emeraldpay.dshackle.upstream.* import io.emeraldpay.dshackle.upstream.ethereum.EthereumApi -import io.emeraldpay.dshackle.upstream.ethereum.EthereumHeadLagObserver import io.emeraldpay.grpc.Chain import org.slf4j.LoggerFactory import org.springframework.context.Lifecycle @@ -66,14 +65,18 @@ class BitcoinChainUpstreams( return upstreams.flatMap { it.getLabels() } } - override fun , TA : UpstreamApi> cast(selfType: Class, upstreamType: Class): T { + override fun castApi(apiType: Class): Upstream { + if (!apiType.isAssignableFrom(BitcoinApi::class.java)) { + throw ClassCastException("Cannot cast ${EthereumApi::class.java} to $apiType") + } + return this as Upstream + } + + override fun , TA : UpstreamApi> cast(selfType: Class, apiType: Class): T { if (!selfType.isAssignableFrom(this.javaClass)) { throw ClassCastException("Cannot cast ${this.javaClass} to $selfType") } - if (!upstreamType.isAssignableFrom(BitcoinApi::class.java)) { - throw ClassCastException("Cannot cast ${EthereumApi::class.java} to $upstreamType") - } - return this as T + return castApi(apiType) as T } } \ No newline at end of file diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinUpstream.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinUpstream.kt index b5b0bbfa..4f59f5df 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinUpstream.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinUpstream.kt @@ -3,7 +3,6 @@ package io.emeraldpay.dshackle.upstream.bitcoin import com.fasterxml.jackson.databind.ObjectMapper import io.emeraldpay.dshackle.config.UpstreamsConfig import io.emeraldpay.dshackle.upstream.* -import io.emeraldpay.dshackle.upstream.ethereum.EthereumUpstreamValidator import io.emeraldpay.grpc.Chain import org.slf4j.LoggerFactory import org.springframework.context.Lifecycle @@ -44,14 +43,18 @@ class BitcoinUpstream( return listOf(UpstreamsConfig.Labels()) } - override fun , TA : UpstreamApi> cast(selfType: Class, upstreamType: Class): T { + override fun , TA : UpstreamApi> cast(selfType: Class, apiType: Class): T { if (!selfType.isAssignableFrom(this.javaClass)) { throw ClassCastException("Cannot cast ${this.javaClass} to $selfType") } - if (!upstreamType.isAssignableFrom(BitcoinApi::class.java)) { - throw ClassCastException("Cannot cast ${BitcoinApi::class.java} to $upstreamType") + return castApi(apiType) as T + } + + override fun castApi(apiType: Class): Upstream { + if (!apiType.isAssignableFrom(BitcoinApi::class.java)) { + throw ClassCastException("Cannot cast ${BitcoinApi::class.java} to $apiType") } - return this as T + return this as Upstream } override fun isRunning(): Boolean { @@ -88,4 +91,5 @@ class BitcoinUpstream( validatorSubscription?.dispose() } + } \ No newline at end of file diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumChainUpstreams.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumChainUpstreams.kt index ae389a0e..cb7ce5f8 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumChainUpstreams.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumChainUpstreams.kt @@ -86,14 +86,18 @@ class EthereumChainUpstreams( } @SuppressWarnings("unchecked") - override fun , TA : UpstreamApi> cast(selfType: Class, upstreamType: Class): T { + override fun , TA : UpstreamApi> cast(selfType: Class, apiType: Class): T { if (!selfType.isAssignableFrom(this.javaClass)) { throw ClassCastException("Cannot cast ${this.javaClass} to $selfType") } - if (!upstreamType.isAssignableFrom(EthereumApi::class.java)) { - throw ClassCastException("Cannot cast ${EthereumApi::class.java} to $upstreamType") + return castApi(apiType) as T + } + + override fun castApi(apiType: Class): Upstream { + if (!apiType.isAssignableFrom(EthereumApi::class.java)) { + throw ClassCastException("Cannot cast ${EthereumApi::class.java} to $apiType") } - return this as T + return this as Upstream } } \ No newline at end of file diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumUpstream.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumUpstream.kt index 1b4abd00..02b7a82b 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumUpstream.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumUpstream.kt @@ -120,14 +120,18 @@ open class EthereumUpstream( } @Suppress("unchecked") - override fun , TA : UpstreamApi> cast(selfType: Class, upstreamType: Class): T { + override fun , TA : UpstreamApi> cast(selfType: Class, apiType: Class): T { if (!selfType.isAssignableFrom(this.javaClass)) { throw ClassCastException("Cannot cast ${this.javaClass} to $selfType") } - if (!upstreamType.isAssignableFrom(EthereumApi::class.java)) { - throw ClassCastException("Cannot cast ${EthereumApi::class.java} to $upstreamType") + return castApi(apiType) as T + } + + override fun castApi(apiType: Class): Upstream { + if (!apiType.isAssignableFrom(EthereumApi::class.java)) { + throw ClassCastException("Cannot cast ${EthereumApi::class.java} to $apiType") } - return this as T + return this as Upstream } } \ No newline at end of file diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/grpc/EthereumGrpcUpstream.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/grpc/EthereumGrpcUpstream.kt index 1e786ac6..bc678d43 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/grpc/EthereumGrpcUpstream.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/grpc/EthereumGrpcUpstream.kt @@ -216,13 +216,17 @@ open class EthereumGrpcUpstream( } @SuppressWarnings("unchecked") - override fun , TA : UpstreamApi> cast(selfType: Class, upstreamType: Class): T { + override fun , TA : UpstreamApi> cast(selfType: Class, apiType: Class): T { if (!selfType.isAssignableFrom(this.javaClass)) { throw ClassCastException("Cannot cast ${this.javaClass} to $selfType") } - if (!upstreamType.isAssignableFrom(EthereumApi::class.java)) { - throw ClassCastException("Cannot cast ${EthereumApi::class.java} to $upstreamType") + return castApi(apiType) as T + } + + override fun castApi(apiType: Class): Upstream { + if (!apiType.isAssignableFrom(EthereumApi::class.java)) { + throw ClassCastException("Cannot cast ${EthereumApi::class.java} to $apiType") } - return this as T + return this as Upstream } } \ No newline at end of file diff --git a/src/test/groovy/io/emeraldpay/dshackle/rpc/TrackBitcoinAddressSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/rpc/TrackBitcoinAddressSpec.groovy new file mode 100644 index 00000000..a317a90d --- /dev/null +++ b/src/test/groovy/io/emeraldpay/dshackle/rpc/TrackBitcoinAddressSpec.groovy @@ -0,0 +1,95 @@ +package io.emeraldpay.dshackle.rpc + +import io.emeraldpay.dshackle.test.TestingCommons +import io.emeraldpay.dshackle.upstream.Upstreams +import io.emeraldpay.grpc.Chain +import spock.lang.Specification + +class TrackBitcoinAddressSpec extends Specification { + + def "Correct sum from multiple"() { + setup: + def json = this.class.getClassLoader().getResourceAsStream("bitcoin/unspent-one-addr.json") + def unspents = TestingCommons.objectMapper().readValue(json, List) + TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(Upstreams)) + when: + def total = track.getTotal(Chain.BITCOIN, ["1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK"], unspents) + + then: + total.size() == 1 + total[0].chain == Chain.BITCOIN + total[0].address == "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK" + total[0].balance.toString() == "32928461" + } + + def "Correct sum when other addresses"() { + setup: + def json = this.class.getClassLoader().getResourceAsStream("bitcoin/unspent-two-addr.json") + def unspents = TestingCommons.objectMapper().readValue(json, List) + TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(Upstreams)) + when: + def total = track.getTotal(Chain.BITCOIN, ["1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK"], unspents) + + then: + total.size() == 1 + total[0].chain == Chain.BITCOIN + total[0].address == "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK" + total[0].balance.toString() == "32928461" + } + + def "Sum for two addresses"() { + setup: + def json = this.class.getClassLoader().getResourceAsStream("bitcoin/unspent-two-addr.json") + def unspents = TestingCommons.objectMapper().readValue(json, List) + TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(Upstreams)) + when: + def total = track.getTotal(Chain.BITCOIN, ["1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP"], unspents).sort { it.address } + + then: + total.size() == 2 + with(total[0]) { + chain == Chain.BITCOIN + address == "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK" + balance.toString() == "32928461" + } + with(total[1]) { + chain == Chain.BITCOIN + address == "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP" + balance.toString() == "25550215615737" + } + } + + def "Zero for empty unspents"() { + setup: + TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(Upstreams)) + when: + def total = track.getTotal(Chain.BITCOIN, ["1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK"], []) + + then: + total.size() == 1 + total[0].chain == Chain.BITCOIN + total[0].address == "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK" + total[0].balance.toString() == "0" + } + + def "Zero for unknown address"() { + setup: + def json = this.class.getClassLoader().getResourceAsStream("bitcoin/unspent-two-addr.json") + def unspents = TestingCommons.objectMapper().readValue(json, List) + TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(Upstreams)) + when: + def total = track.getTotal(Chain.BITCOIN, ["16rCmCmbuWDhPjWTrpQGaU3EPdZF7MTdUk", "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK"], unspents).sort { it.address } + + then: + total.size() == 2 + with(total[0]) { + address == "16rCmCmbuWDhPjWTrpQGaU3EPdZF7MTdUk" + balance.toString() == "0" + } + with(total[1]) { + address == "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK" + balance.toString() == "32928461" + } + } + +} diff --git a/src/test/groovy/io/emeraldpay/dshackle/rpc/TrackEthereumAddressSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/rpc/TrackEthereumAddressSpec.groovy index 764c176f..89a67edf 100644 --- a/src/test/groovy/io/emeraldpay/dshackle/rpc/TrackEthereumAddressSpec.groovy +++ b/src/test/groovy/io/emeraldpay/dshackle/rpc/TrackEthereumAddressSpec.groovy @@ -67,7 +67,7 @@ class TrackEthereumAddressSpec extends Specification { apiMock.answer("eth_getBalance", ["0xe2c8fa8120d813cd0b5e6add120295bf20cfa09f", "latest"], "0x499602D2") when: - def flux = trackAddress.getBalance(Mono.just(req)) + def flux = trackAddress.getBalance(req) then: StepVerifier.create(flux) .expectNext(exp) @@ -111,7 +111,7 @@ class TrackEthereumAddressSpec extends Specification { apiMock.answerOnce("eth_getBalance", ["0xe2c8fa8120d813cd0b5e6add120295bf20cfa09f", "latest"], "0x499602D2") apiMock.answerOnce("eth_getBalance", ["0xe2c8fa8120d813cd0b5e6add120295bf20cfa09f", "latest"], "0xff98") when: - def flux = trackAddress.subscribe(Mono.just(req)) + def flux = trackAddress.subscribe(req) then: StepVerifier.create(flux) .expectNext(exp1) diff --git a/src/test/resources/bitcoin/unspent-one-addr.json b/src/test/resources/bitcoin/unspent-one-addr.json new file mode 100644 index 00000000..a62ad7ee --- /dev/null +++ b/src/test/resources/bitcoin/unspent-one-addr.json @@ -0,0 +1,434 @@ +[ + { + "txid": "e0f946c8f971b25cdffa64eed71d886019e437c0bf6a1b280584c0be5d1b5409", + "vout": 29, + "address": "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", + "label": "", + "scriptPubKey": "76a914c6c34d94969bccf7faaa8e5e5bb739e4d68ee9f888ac", + "amount": 0.01230030, + "confirmations": 2010, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "66e1e4d14ed6f454d2fda036f35cba423274ecdf5d46deb93f172c412a0f650d", + "vout": 83, + "address": "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", + "label": "", + "scriptPubKey": "76a914c6c34d94969bccf7faaa8e5e5bb739e4d68ee9f888ac", + "amount": 0.00756339, + "confirmations": 4963, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "4b30a92f5567dfbbd13b48e51c4ec1d97610daa8a1e008c428beb136e5f5670d", + "vout": 25, + "address": "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", + "label": "", + "scriptPubKey": "76a914c6c34d94969bccf7faaa8e5e5bb739e4d68ee9f888ac", + "amount": 0.00955798, + "confirmations": 111, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "507cb7191a10eaab9e2b4e223c85ef1ddd6b3aafbdfb990fc6e5e2dadf12af0e", + "vout": 10, + "address": "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", + "label": "", + "scriptPubKey": "76a914c6c34d94969bccf7faaa8e5e5bb739e4d68ee9f888ac", + "amount": 0.00289985, + "confirmations": 4840, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "7edc6cf0b680b4e127997c4b5d57320fda314d61b1ed23227cf496610e363410", + "vout": 54, + "address": "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", + "label": "", + "scriptPubKey": "76a914c6c34d94969bccf7faaa8e5e5bb739e4d68ee9f888ac", + "amount": 0.01003612, + "confirmations": 5213, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "0ae8671a15775d8eac990c92b687175a6b154fdcc4f516b0f100f813b2c9d011", + "vout": 93, + "address": "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", + "label": "", + "scriptPubKey": "76a914c6c34d94969bccf7faaa8e5e5bb739e4d68ee9f888ac", + "amount": 0.00973772, + "confirmations": 1213, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "f91b5bfd4a104baafeb0c5a62f1713580ab2d64b2c6d2fdfcd7818747645fc14", + "vout": 19, + "address": "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", + "label": "", + "scriptPubKey": "76a914c6c34d94969bccf7faaa8e5e5bb739e4d68ee9f888ac", + "amount": 0.01218703, + "confirmations": 278, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "7f9ee14424c4b52a2aa5c2b5df199fde2873a56f6ebd07d835722d2fd881b828", + "vout": 94, + "address": "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", + "label": "", + "scriptPubKey": "76a914c6c34d94969bccf7faaa8e5e5bb739e4d68ee9f888ac", + "amount": 0.00635688, + "confirmations": 3214, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "6a278d265ce25c8519ad7af4b115eb8d6d5ca89957d0638f4567c2629c11d82a", + "vout": 48, + "address": "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", + "label": "", + "scriptPubKey": "76a914c6c34d94969bccf7faaa8e5e5bb739e4d68ee9f888ac", + "amount": 0.00529192, + "confirmations": 2472, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "80c673330e0c4a3b981f9acad07b5194b44c36d683cf8e0687f5faf6780a682c", + "vout": 14, + "address": "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", + "label": "", + "scriptPubKey": "76a914c6c34d94969bccf7faaa8e5e5bb739e4d68ee9f888ac", + "amount": 0.00850915, + "confirmations": 3066, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "a032d978409dbd6de66792a78147135903f1a4731781e8e5069a504cc43a8c32", + "vout": 8, + "address": "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", + "label": "", + "scriptPubKey": "76a914c6c34d94969bccf7faaa8e5e5bb739e4d68ee9f888ac", + "amount": 0.01047700, + "confirmations": 4628, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "6dffd0651ae1bc29f43b864c1a097d4ecca0bd16341d13be9e32b121e6f6ca3e", + "vout": 36, + "address": "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", + "label": "", + "scriptPubKey": "76a914c6c34d94969bccf7faaa8e5e5bb739e4d68ee9f888ac", + "amount": 0.00605451, + "confirmations": 2763, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "42e10b12c32f4d6ba91b144b9c5afcccd6bcaf3f8f946d1f81a891c3d47e4346", + "vout": 78, + "address": "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", + "label": "", + "scriptPubKey": "76a914c6c34d94969bccf7faaa8e5e5bb739e4d68ee9f888ac", + "amount": 0.00982508, + "confirmations": 3363, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "70d83eda5f5024a5a7d2de7753ea0a7aa3a7cc5496ebbf9ea044efa2cd35cd47", + "vout": 55, + "address": "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", + "label": "", + "scriptPubKey": "76a914c6c34d94969bccf7faaa8e5e5bb739e4d68ee9f888ac", + "amount": 0.00625446, + "confirmations": 5605, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "253351e033465057d96e54a4fff130853782ae52dddef51486d3d7dc55e07b4c", + "vout": 74, + "address": "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", + "label": "", + "scriptPubKey": "76a914c6c34d94969bccf7faaa8e5e5bb739e4d68ee9f888ac", + "amount": 0.00720758, + "confirmations": 1539, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "39ad1738af322b6049e386a01e05fe8a6479913bd266268634eeadefdfd28c61", + "vout": 14, + "address": "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", + "label": "", + "scriptPubKey": "76a914c6c34d94969bccf7faaa8e5e5bb739e4d68ee9f888ac", + "amount": 0.01023716, + "confirmations": 4158, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "67c9e726a7acc9e7968975b150429e9d593f8e20010c03aeee58b84d2bce2b64", + "vout": 8, + "address": "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", + "label": "", + "scriptPubKey": "76a914c6c34d94969bccf7faaa8e5e5bb739e4d68ee9f888ac", + "amount": 0.01104307, + "confirmations": 889, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "2e731be5a74b5a321055a8b1ebc232c9316b275315ceaa945bad021c6e2a1865", + "vout": 47, + "address": "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", + "label": "", + "scriptPubKey": "76a914c6c34d94969bccf7faaa8e5e5bb739e4d68ee9f888ac", + "amount": 0.00679256, + "confirmations": 5081, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "88d48dae117aa8de4af3c8cde5f4d57d0e7a463d769b74379ea920474c6b527b", + "vout": 14, + "address": "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", + "label": "", + "scriptPubKey": "76a914c6c34d94969bccf7faaa8e5e5bb739e4d68ee9f888ac", + "amount": 0.00876837, + "confirmations": 730, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "c7c47da9165734c7745c13450fd61ec50c6638d1d6e58546f368c0acbe73877e", + "vout": 63, + "address": "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", + "label": "", + "scriptPubKey": "76a914c6c34d94969bccf7faaa8e5e5bb739e4d68ee9f888ac", + "amount": 0.01034200, + "confirmations": 2904, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "fe123697bc5f4ad079e81cdccbbb5834c240072f2874c8a43c68456dd8ef7a84", + "vout": 49, + "address": "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", + "label": "", + "scriptPubKey": "76a914c6c34d94969bccf7faaa8e5e5bb739e4d68ee9f888ac", + "amount": 0.00792699, + "confirmations": 4724, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "6df879ac8f8f2b6fef79b71098fff03cc74085392fb2bbd4b77c14bafdefc497", + "vout": 35, + "address": "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", + "label": "", + "scriptPubKey": "76a914c6c34d94969bccf7faaa8e5e5bb739e4d68ee9f888ac", + "amount": 0.01313314, + "confirmations": 1378, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "05b2a9ae5e2d150ef042af1fe62a67652298b885d0fbe11f1be4ce850fcecfa6", + "vout": 60, + "address": "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", + "label": "", + "scriptPubKey": "76a914c6c34d94969bccf7faaa8e5e5bb739e4d68ee9f888ac", + "amount": 0.00754670, + "confirmations": 4404, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "c9590ed1299e22c2e4e1275a953258c9cb7052f87f5e7de7decb055248a06eba", + "vout": 8, + "address": "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", + "label": "", + "scriptPubKey": "76a914c6c34d94969bccf7faaa8e5e5bb739e4d68ee9f888ac", + "amount": 0.00794840, + "confirmations": 1068, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "6d6dd53d70fa024011ca1a887686d6c0bbf19c739ed5017cad1adb77de7cbdbe", + "vout": 64, + "address": "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", + "label": "", + "scriptPubKey": "76a914c6c34d94969bccf7faaa8e5e5bb739e4d68ee9f888ac", + "amount": 0.01151435, + "confirmations": 3497, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "31ecd41422208033526919fee8a028a1de915da1308728ff8328a53f7f7d59cd", + "vout": 66, + "address": "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", + "label": "", + "scriptPubKey": "76a914c6c34d94969bccf7faaa8e5e5bb739e4d68ee9f888ac", + "amount": 0.00987020, + "confirmations": 2608, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "5963e589736bea2b4a438cf7cede084fbb24f9b545fa7558ad407510260805ce", + "vout": 68, + "address": "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", + "label": "", + "scriptPubKey": "76a914c6c34d94969bccf7faaa8e5e5bb739e4d68ee9f888ac", + "amount": 0.01076674, + "confirmations": 3915, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "df6a74ad3d4af01ad76937b43ac268d9775d447c97f5cc34c2b12c3b405e61cf", + "vout": 50, + "address": "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", + "label": "", + "scriptPubKey": "76a914c6c34d94969bccf7faaa8e5e5bb739e4d68ee9f888ac", + "amount": 0.00493433, + "confirmations": 4518, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "768d3e2f525d8ed24c446463f163fb9b9c7af10e07097efe0eecc8600b7083d6", + "vout": 74, + "address": "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", + "label": "", + "scriptPubKey": "76a914c6c34d94969bccf7faaa8e5e5bb739e4d68ee9f888ac", + "amount": 0.01433360, + "confirmations": 1696, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "f0eb2c2e854ecae7d3bbbdf06ef25de25c78983466be16650c030be3c558ebd6", + "vout": 58, + "address": "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", + "label": "", + "scriptPubKey": "76a914c6c34d94969bccf7faaa8e5e5bb739e4d68ee9f888ac", + "amount": 0.01098392, + "confirmations": 405, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "a73edf99d17868fd458520308febc71609e1fecd686530c525000359866a4ed9", + "vout": 35, + "address": "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", + "label": "", + "scriptPubKey": "76a914c6c34d94969bccf7faaa8e5e5bb739e4d68ee9f888ac", + "amount": 0.00902924, + "confirmations": 4037, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "aea6c1f4b231ab6900332c82fb9d48d24aac2245575e53f5fd7d6e86a7ea02dc", + "vout": 4, + "address": "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", + "label": "", + "scriptPubKey": "76a914c6c34d94969bccf7faaa8e5e5bb739e4d68ee9f888ac", + "amount": 0.00954847, + "confirmations": 563, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "366166e6a1a2d7b99d44834e2d4c2c5988fb8da2e17756f21b8e4d2c0ac972e1", + "vout": 88, + "address": "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", + "label": "", + "scriptPubKey": "76a914c6c34d94969bccf7faaa8e5e5bb739e4d68ee9f888ac", + "amount": 0.01032596, + "confirmations": 4275, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "ac53f2a2052396354333e0e500864f21ac0909c4159f46880e0f9128ac7194e2", + "vout": 19, + "address": "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", + "label": "", + "scriptPubKey": "76a914c6c34d94969bccf7faaa8e5e5bb739e4d68ee9f888ac", + "amount": 0.01480214, + "confirmations": 1848, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "63b840a8abf477305dfad93db69205ff01a58e1527a317dbcc8a360533c0e9e2", + "vout": 60, + "address": "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", + "label": "", + "scriptPubKey": "76a914c6c34d94969bccf7faaa8e5e5bb739e4d68ee9f888ac", + "amount": 0.00412783, + "confirmations": 3822, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "f14b222e652c58d11435fa9172ddea000c6f5e20e6b715eb940fc28d1c4adeef", + "vout": 58, + "address": "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", + "label": "", + "scriptPubKey": "76a914c6c34d94969bccf7faaa8e5e5bb739e4d68ee9f888ac", + "amount": 0.01105047, + "confirmations": 2139, + "spendable": false, + "solvable": false, + "safe": true + } +] \ No newline at end of file diff --git a/src/test/resources/bitcoin/unspent-two-addr.json b/src/test/resources/bitcoin/unspent-two-addr.json new file mode 100644 index 00000000..eaa4e5cc --- /dev/null +++ b/src/test/resources/bitcoin/unspent-two-addr.json @@ -0,0 +1,4514 @@ +[ + { + "txid": "8ad0d954a01eeb4f2c62d58d291699af847f9c8df43b775c27ffe8a5f76eba00", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 0.00216465, + "confirmations": 2583, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "c82feef749e57ce138e21231848060c029a68df522d55913e10cd912a1e58501", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 0.00001367, + "confirmations": 16590, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "df7103c27efd0cbcf286f9ba528b9a7d58feac7919eafe278f0d43f39e8d2302", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 0.00014253, + "confirmations": 496, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "9e27a8132a42a9f52f2966028b97f67745c257f714308bf5ba52133186476402", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 100.00000000, + "confirmations": 22929, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "9e27a8132a42a9f52f2966028b97f67745c257f714308bf5ba52133186476402", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 100.00000000, + "confirmations": 22929, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "a23e1710a6380b3c338f50437384cbd2bd270752c8ee5413a91da9a48c2eed02", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22024, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "a23e1710a6380b3c338f50437384cbd2bd270752c8ee5413a91da9a48c2eed02", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22024, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "ed9f0b91ed1e373c518a8ad583d471310b6186b50ba7b7c971c6e71f01011006", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22157, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "ed9f0b91ed1e373c518a8ad583d471310b6186b50ba7b7c971c6e71f01011006", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22157, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "75f5bcda796cb6128404fa86ba924cfd948b9e735df8c6adebc9394c4a7de907", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 0.00000546, + "confirmations": 20168, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "619d2b359c19058f8527314aec41f3f693a9d0593e0e5e0d7ca0980407131408", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 0.00009000, + "confirmations": 1134, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "e0f946c8f971b25cdffa64eed71d886019e437c0bf6a1b280584c0be5d1b5409", + "vout": 29, + "address": "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", + "label": "", + "scriptPubKey": "76a914c6c34d94969bccf7faaa8e5e5bb739e4d68ee9f888ac", + "amount": 0.01230030, + "confirmations": 2030, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "777671a46b30b068052a73387e036bc8515cd3ba6adf9be4c70dfc0699f67c09", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 0.00307906, + "confirmations": 13705, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "9418126e53d2f31650d83fdfdf508dac2fc074955d16976337108764a531ef0a", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 21190, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "9418126e53d2f31650d83fdfdf508dac2fc074955d16976337108764a531ef0a", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 21190, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "86ab7560506cffdd2c15eab3dc8900692d8653f66b436de0adbfcb5de2b28e0c", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22432, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "86ab7560506cffdd2c15eab3dc8900692d8653f66b436de0adbfcb5de2b28e0c", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22432, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "8befc98ebb5f2b421c17084612a34d65ad48db3db5020ca6865e42b98f021b0d", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22304, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "8befc98ebb5f2b421c17084612a34d65ad48db3db5020ca6865e42b98f021b0d", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22304, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "66e1e4d14ed6f454d2fda036f35cba423274ecdf5d46deb93f172c412a0f650d", + "vout": 83, + "address": "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", + "label": "", + "scriptPubKey": "76a914c6c34d94969bccf7faaa8e5e5bb739e4d68ee9f888ac", + "amount": 0.00756339, + "confirmations": 4983, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "4b30a92f5567dfbbd13b48e51c4ec1d97610daa8a1e008c428beb136e5f5670d", + "vout": 25, + "address": "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", + "label": "", + "scriptPubKey": "76a914c6c34d94969bccf7faaa8e5e5bb739e4d68ee9f888ac", + "amount": 0.00955798, + "confirmations": 131, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "507cb7191a10eaab9e2b4e223c85ef1ddd6b3aafbdfb990fc6e5e2dadf12af0e", + "vout": 10, + "address": "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", + "label": "", + "scriptPubKey": "76a914c6c34d94969bccf7faaa8e5e5bb739e4d68ee9f888ac", + "amount": 0.00289985, + "confirmations": 4860, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "7edc6cf0b680b4e127997c4b5d57320fda314d61b1ed23227cf496610e363410", + "vout": 54, + "address": "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", + "label": "", + "scriptPubKey": "76a914c6c34d94969bccf7faaa8e5e5bb739e4d68ee9f888ac", + "amount": 0.01003612, + "confirmations": 5233, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "efddf6657f6f886b9dfe79bc9eec24d9188c29e35d7f239deb8e2cb443b55710", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 0.00001000, + "confirmations": 6413, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "be37564354453a705c04f62cc68cf985ded3d31b70a80baab0af7416f1a85711", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22311, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "be37564354453a705c04f62cc68cf985ded3d31b70a80baab0af7416f1a85711", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22311, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "0ae8671a15775d8eac990c92b687175a6b154fdcc4f516b0f100f813b2c9d011", + "vout": 93, + "address": "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", + "label": "", + "scriptPubKey": "76a914c6c34d94969bccf7faaa8e5e5bb739e4d68ee9f888ac", + "amount": 0.00973772, + "confirmations": 1233, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "c6ff922d23abe33acf56fa7594288f5eb6990bfb861fd31e0df0b958f22fbf12", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 0.00000600, + "confirmations": 6099, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "a807b24053c6ec279b05cc1ced4e19a7f2df9d72971fc3a16f53c6e34407ef12", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22310, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "a807b24053c6ec279b05cc1ced4e19a7f2df9d72971fc3a16f53c6e34407ef12", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22310, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "f91b5bfd4a104baafeb0c5a62f1713580ab2d64b2c6d2fdfcd7818747645fc14", + "vout": 19, + "address": "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", + "label": "", + "scriptPubKey": "76a914c6c34d94969bccf7faaa8e5e5bb739e4d68ee9f888ac", + "amount": 0.01218703, + "confirmations": 298, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "f47a6d8b49684206e201b39f2369d2d4a69d3e50d55bf1af76bac38c11516f17", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 2061.38172587, + "confirmations": 49293, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "8b4bd36458fa1876026e10d3db7d3c3eb3692ee907cf1021cd3b940b89318719", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 0.00001201, + "confirmations": 12260, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "a547918f667c3b9b6d00dfbaa14d64de43f4fb13f6482232d7461c290691901a", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 21195, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "a547918f667c3b9b6d00dfbaa14d64de43f4fb13f6482232d7461c290691901a", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 21195, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "2833edb48bcaa5e3e62f9550613b8b6206fc55fe8ae55d7edba387cee059ca1a", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22308, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "2833edb48bcaa5e3e62f9550613b8b6206fc55fe8ae55d7edba387cee059ca1a", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22308, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "ad5a27a7b3106202d1af59da5c3de211aa43c49158fe83cb866fba5b2c3c831d", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22024, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "ad5a27a7b3106202d1af59da5c3de211aa43c49158fe83cb866fba5b2c3c831d", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22024, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "7d804f316d289a69b8f17dd6e0a3e5c90a1a6e59335747745bc6d4db2a4a051e", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 0.00450283, + "confirmations": 11696, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "c852e432204607ba053f2708917aad411872a202c79ac20d502737366ea46620", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 21191, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "c852e432204607ba053f2708917aad411872a202c79ac20d502737366ea46620", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 21191, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "fb1223d81a1d519fc07568417ccf548066078b458d9ad5655d60eaeb52314221", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 21191, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "fb1223d81a1d519fc07568417ccf548066078b458d9ad5655d60eaeb52314221", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 21191, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "accd2c8a6162d3d7a66df234b0bbf9c0b29d3184e46cc0e07575c1db3a0d8c23", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 21195, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "accd2c8a6162d3d7a66df234b0bbf9c0b29d3184e46cc0e07575c1db3a0d8c23", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 21195, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "9df2460c06e732d86c2864bc44e717aea1a72da614fd4f583ed3e5fb27a09623", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22155, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "9df2460c06e732d86c2864bc44e717aea1a72da614fd4f583ed3e5fb27a09623", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22155, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "a65f7ed63b8cb4febcf25859b2e54bb32c2ddc2f94cffdb9a319d4835598a623", + "vout": 22, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 0.00015881, + "confirmations": 2648, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "29584c564de770d1eccef22c2a33497bf37842b83b4933def5e03e116f683b26", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 20919, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "29584c564de770d1eccef22c2a33497bf37842b83b4933def5e03e116f683b26", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 20919, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "40b9373605760207906abc2f764adde9312bc7e1bc2c8533cbe77b4ad3866c26", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 0.00000777, + "confirmations": 45053, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "89deb74f1a724fde5724d10ffa30f1403e1bca2304643b64bf397fb3b55b9e26", + "vout": 32, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 0.00028100, + "confirmations": 1904, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "3d20affb0656bb16e24b282c17d9b15743c9bac833d2f3d03db23efee7101928", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 0.00003690, + "confirmations": 40446, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "00d1e2f0cacb0ac36120fe4055b59da6ae4bcfe3763f08cfd71891aa3a709928", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22433, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "7f9ee14424c4b52a2aa5c2b5df199fde2873a56f6ebd07d835722d2fd881b828", + "vout": 94, + "address": "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", + "label": "", + "scriptPubKey": "76a914c6c34d94969bccf7faaa8e5e5bb739e4d68ee9f888ac", + "amount": 0.00635688, + "confirmations": 3234, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "f2c761c08c363de9cb0e379d5f7cfe46e2ac9227b78ea205506bbd8c1ff2e628", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22156, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "f2c761c08c363de9cb0e379d5f7cfe46e2ac9227b78ea205506bbd8c1ff2e628", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22156, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "809d32b841eebddbcad3c155b9778fed72f2e4cdc1be58b6fe0470c59f464629", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 20920, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "b76e7f02d20db859977a179e04d54b018fae8ee79d646eae4f106a796142d62a", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 0.00001366, + "confirmations": 16567, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "6a278d265ce25c8519ad7af4b115eb8d6d5ca89957d0638f4567c2629c11d82a", + "vout": 48, + "address": "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", + "label": "", + "scriptPubKey": "76a914c6c34d94969bccf7faaa8e5e5bb739e4d68ee9f888ac", + "amount": 0.00529192, + "confirmations": 2492, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "74b6855459c7412f5ce46882b5c2a08a511c91a9a1912bf58b8b97dbabe2362c", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 0.00032448, + "confirmations": 7707, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "6b590cb5240d950a338cfc3e58c44db1a49d1b372bd03db08a7be5964199372c", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 0.01000000, + "confirmations": 23069, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "9e745f0769ec458080cd1c1349a0a4078e06fc1d830ee53f6f3ef1d2bfd05c2c", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22306, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "9e745f0769ec458080cd1c1349a0a4078e06fc1d830ee53f6f3ef1d2bfd05c2c", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22306, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "80c673330e0c4a3b981f9acad07b5194b44c36d683cf8e0687f5faf6780a682c", + "vout": 14, + "address": "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", + "label": "", + "scriptPubKey": "76a914c6c34d94969bccf7faaa8e5e5bb739e4d68ee9f888ac", + "amount": 0.00850915, + "confirmations": 3086, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "bbdc03cd25af1274120ecb2a56685854e678276d24fb45eb9fe6560d80bf152d", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 20918, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "bbdc03cd25af1274120ecb2a56685854e678276d24fb45eb9fe6560d80bf152d", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 20918, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "7a8ffefc2d5c9564407f86cf0f91928f6e68efb7e9e318dae03f577a07164e2d", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 20915, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "7a8ffefc2d5c9564407f86cf0f91928f6e68efb7e9e318dae03f577a07164e2d", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 20915, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "21b6f5d47df42337e34b8337090748d0baaeb2b96cbbd9bad0a84749ab012d2e", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 20916, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "21b6f5d47df42337e34b8337090748d0baaeb2b96cbbd9bad0a84749ab012d2e", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 20916, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "c28afd728ae0d6783034ea815af2c80e47d7695c719365cfe5d8fcb390c52931", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 0.00020000, + "confirmations": 46944, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "a032d978409dbd6de66792a78147135903f1a4731781e8e5069a504cc43a8c32", + "vout": 8, + "address": "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", + "label": "", + "scriptPubKey": "76a914c6c34d94969bccf7faaa8e5e5bb739e4d68ee9f888ac", + "amount": 0.01047700, + "confirmations": 4648, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "771d59af3e58889e3555cbbb987cc5d98134cc3995ae1f1a0cad631f0ae70336", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 21898, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "771d59af3e58889e3555cbbb987cc5d98134cc3995ae1f1a0cad631f0ae70336", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 21898, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "d68b5bd589cc20e7e557a724b209327503856c8d4a108ab5987d22ff84e6ea37", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 802.53985111, + "confirmations": 49293, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "63d533df6f629c7ca6f5f68f1d77eb8b61a9e5bb7d2ad614975cf591c1348b38", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22024, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "63d533df6f629c7ca6f5f68f1d77eb8b61a9e5bb7d2ad614975cf591c1348b38", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22024, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "9ffbee23da849580e82ca8849894964fb4888e3a4bf2d6682e25608b2ab4173a", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 0.00012536, + "confirmations": 14618, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "0d162334b3d2f6b924f7be66510ad9cc2e2bbfa29ccc9f193ea4e0aa24f31c3b", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 0.00008808, + "confirmations": 37022, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "4963bc5c7b2cad074f7e46e0140c484a150bd58652bf8b8cbb42bd84fed51e3b", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22155, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "4963bc5c7b2cad074f7e46e0140c484a150bd58652bf8b8cbb42bd84fed51e3b", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22155, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "2be8d89a0cee8da25d240013cceadd7aabca8d25950cfe0e56048ef0404db63c", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 0.00000546, + "confirmations": 51498, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "4f144f1faaa1f3b0fa0ec55f82d16b372285dd293c589b93a7f6d04c9c4bb83c", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22156, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "4f144f1faaa1f3b0fa0ec55f82d16b372285dd293c589b93a7f6d04c9c4bb83c", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22156, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "6dffd0651ae1bc29f43b864c1a097d4ecca0bd16341d13be9e32b121e6f6ca3e", + "vout": 36, + "address": "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", + "label": "", + "scriptPubKey": "76a914c6c34d94969bccf7faaa8e5e5bb739e4d68ee9f888ac", + "amount": 0.00605451, + "confirmations": 2783, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "c9e03ab57e8d87db384705d67ae84e9ddd63c3a2e086362109d569e9c8749942", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22311, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "c9e03ab57e8d87db384705d67ae84e9ddd63c3a2e086362109d569e9c8749942", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22311, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "8aaa8ce840e8fb46d1131fd2375d8e4259631d1cbbcbece398a7d3d41a456143", + "vout": 10, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 0.00000570, + "confirmations": 9895, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "1b4234370ab7cd2789e863cb18ae8573acefaaca30e916aeb1e46b59ee1ea643", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22156, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "1b4234370ab7cd2789e863cb18ae8573acefaaca30e916aeb1e46b59ee1ea643", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22156, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "f0d5cadbd54a21b490031caaa8d074e3c1fe57888d77b519bb7aa71512393f44", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 21191, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "f0d5cadbd54a21b490031caaa8d074e3c1fe57888d77b519bb7aa71512393f44", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 21191, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "d1a076722d11ddfd0fadb96d6d960b8b2ad6c6369570cd00df70a801a6be9e44", + "vout": 2, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 0.99934984, + "confirmations": 25950, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "c730c197cff9694a21bf9812d0cfe90706cad8561e3d5a51a13aa22d224e3b46", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22310, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "c730c197cff9694a21bf9812d0cfe90706cad8561e3d5a51a13aa22d224e3b46", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22310, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "42e10b12c32f4d6ba91b144b9c5afcccd6bcaf3f8f946d1f81a891c3d47e4346", + "vout": 78, + "address": "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", + "label": "", + "scriptPubKey": "76a914c6c34d94969bccf7faaa8e5e5bb739e4d68ee9f888ac", + "amount": 0.00982508, + "confirmations": 3383, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "b1901b04766cfe2d1f743ff20624f90f2040460db2a8b1ad5c8b9b83a8745146", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22158, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "b1901b04766cfe2d1f743ff20624f90f2040460db2a8b1ad5c8b9b83a8745146", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22158, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "6485dc0917d8a03297c4fc46c5fd828c5f3108540ae81237d7f96a3725200147", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 21893, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "6485dc0917d8a03297c4fc46c5fd828c5f3108540ae81237d7f96a3725200147", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 21893, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "0f7e4dac20cfe36be11681c2642830d4f43230369a787303169601a4b31e4447", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 20918, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "0f7e4dac20cfe36be11681c2642830d4f43230369a787303169601a4b31e4447", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 20918, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "70d83eda5f5024a5a7d2de7753ea0a7aa3a7cc5496ebbf9ea044efa2cd35cd47", + "vout": 55, + "address": "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", + "label": "", + "scriptPubKey": "76a914c6c34d94969bccf7faaa8e5e5bb739e4d68ee9f888ac", + "amount": 0.00625446, + "confirmations": 5625, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "860b2b47995d2a68a6fc4cb0055e925bc47d419a33b4ab0a0eebb8b3eac40348", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 0.00008256, + "confirmations": 6447, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "cb6d28499280c4423f48924ee21e788a41d166f37026e8a205822a23ee36ca48", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 0.00002000, + "confirmations": 41928, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "7195c05f3c79f3e097be5f85ff219d17dc83d332d146b1223f6e407cc500d248", + "vout": 11, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 0.00013712, + "confirmations": 1618, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "6cd7ea09ff30a8caeeeb69ff3891c1eca5271bb58f4ea2489cb762ccde7ca64a", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22308, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "6cd7ea09ff30a8caeeeb69ff3891c1eca5271bb58f4ea2489cb762ccde7ca64a", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22308, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "38e331e475dc4372ad776d24cbea3e5e9d3a71166d0c2ce7a23f82b0d4542c4b", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 20915, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "38e331e475dc4372ad776d24cbea3e5e9d3a71166d0c2ce7a23f82b0d4542c4b", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 20915, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "e002a4e6074ef5cb07c913bf809bc744b48e1044bb2281c58a82e942401d5e4c", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 21893, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "e002a4e6074ef5cb07c913bf809bc744b48e1044bb2281c58a82e942401d5e4c", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 21893, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "253351e033465057d96e54a4fff130853782ae52dddef51486d3d7dc55e07b4c", + "vout": 74, + "address": "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", + "label": "", + "scriptPubKey": "76a914c6c34d94969bccf7faaa8e5e5bb739e4d68ee9f888ac", + "amount": 0.00720758, + "confirmations": 1559, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "0cf4f5e87463e3a02364396ac8b8a6f0d17bf612079aef0048dc2ebb2579884c", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 2890.00000000, + "confirmations": 51629, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "c3ca43598669ec87061e04e3718b199a51e0db844ab2dd0c852bff130c3c7d4d", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22026, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "c3ca43598669ec87061e04e3718b199a51e0db844ab2dd0c852bff130c3c7d4d", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22026, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "2bd9532b0f3fc8da5ce4edee1e158d7294714f6f6db0843a5714e207c578e150", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 20920, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "2bd9532b0f3fc8da5ce4edee1e158d7294714f6f6db0843a5714e207c578e150", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 20920, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "c493385850f3c957ab0a36c7b9c9d5a441fbf725fe9beb1dae22723ebf940151", + "vout": 2, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 10.09898528, + "confirmations": 31524, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "898331c5af894301c97659bec72f3691f9454d1144d018a37e6ac0de90a10951", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 0.00010000, + "confirmations": 30653, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "0f0f3229e02862f841f618a26636b21d685aca3392f45cd7142361c2fa556c51", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22155, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "0f0f3229e02862f841f618a26636b21d685aca3392f45cd7142361c2fa556c51", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22155, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "2a2fc66b6ff468f422f531f14b5b6137858c9207bf0eb9e810bf001d2f4ce452", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 0.00001177, + "confirmations": 22713, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "b24ff32cae9e09a4f65c0d595463751bf979247462f25f2b5bfb966c79513754", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22023, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "b24ff32cae9e09a4f65c0d595463751bf979247462f25f2b5bfb966c79513754", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22023, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "68ffd50d808067e09d5a1954bbf3f7d5ab33b8b4894f81d655f170d200cf1856", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 21195, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "68ffd50d808067e09d5a1954bbf3f7d5ab33b8b4894f81d655f170d200cf1856", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 21195, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "afac7c6febf225a386b38b9d23faabdd7675dd9b75d8df27627b2b4f82255357", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22157, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "afac7c6febf225a386b38b9d23faabdd7675dd9b75d8df27627b2b4f82255357", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22157, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "5bbd7a0e0e959cf93cf61918dbe0bea5f58ee5b7c9130fe5cdbcad6ca2687b57", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22312, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "5bbd7a0e0e959cf93cf61918dbe0bea5f58ee5b7c9130fe5cdbcad6ca2687b57", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22312, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "d15a4d5e093c74185de6f1c60bd19bc5e4bfd0d4aebdb15e73649f9489084558", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 0.00001855, + "confirmations": 12758, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "c89fc9869db14dae92fe2e079ca2d34fc23344134c9a879329a9b57ad6ad7c59", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 0.00000600, + "confirmations": 5852, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "bc1d34e2042e9dc7a8cb1e4fd648d11a347a6c261c433686f6a8a254487a9659", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 21892, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "bc1d34e2042e9dc7a8cb1e4fd648d11a347a6c261c433686f6a8a254487a9659", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 21892, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "459c44edebb9e2c5332efd3d9f8d02f2dd7df929894da72ff09eec4af95cdc5a", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22027, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "459c44edebb9e2c5332efd3d9f8d02f2dd7df929894da72ff09eec4af95cdc5a", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22027, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "ab463d1be5995d6320a2876378241ace2ea4d05eeea6e76fe0fb5dadeb1cf35b", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 0.00000546, + "confirmations": 51508, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "cc83aea843913477ecc1403ff4aad1183b541376397672489193c9b8f2794a5d", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 906.36874164, + "confirmations": 47491, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "14619e88f545c66c236ea10a9845428c578aced799e39f97586ed8eba76d4d5e", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 37000.00000000, + "confirmations": 47493, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "56c00a1e8981a76cd868094a78582fd815adb4071f4ae7496ba532e3375f135f", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 0.00003690, + "confirmations": 38902, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "5f67a87255970ad4e6d4ae94afdf9f00520c968ca2fb5abc57070989e83a9f5f", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22431, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "5f67a87255970ad4e6d4ae94afdf9f00520c968ca2fb5abc57070989e83a9f5f", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22431, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "5ea5f4ecb0bad4f2472b6019d8b84e13283760943db2bf53bfe731bc9d0bcf60", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22157, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "5ea5f4ecb0bad4f2472b6019d8b84e13283760943db2bf53bfe731bc9d0bcf60", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22157, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "f57013b1c95d76f9e4a1622847d267eecf6a0df54835596548417a185821e560", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 0.00001000, + "confirmations": 16484, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "e529716028a8fd066c5aac125c05e0c15d6cb4b254ed654397ae441e10fff160", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 0.00000546, + "confirmations": 49290, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "39ad1738af322b6049e386a01e05fe8a6479913bd266268634eeadefdfd28c61", + "vout": 14, + "address": "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", + "label": "", + "scriptPubKey": "76a914c6c34d94969bccf7faaa8e5e5bb739e4d68ee9f888ac", + "amount": 0.01023716, + "confirmations": 4178, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "139298bef62b867eca70cf4b08240ff2ceb98f45b090164551504e2e3261e461", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22306, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "139298bef62b867eca70cf4b08240ff2ceb98f45b090164551504e2e3261e461", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22306, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "67c9e726a7acc9e7968975b150429e9d593f8e20010c03aeee58b84d2bce2b64", + "vout": 8, + "address": "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", + "label": "", + "scriptPubKey": "76a914c6c34d94969bccf7faaa8e5e5bb739e4d68ee9f888ac", + "amount": 0.01104307, + "confirmations": 909, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "585793b1ef8f7fba1a4b1b968036633198fe56ab4776f6d9d3667f665a29ae64", + "vout": 4, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 0.00010000, + "confirmations": 17029, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "2e731be5a74b5a321055a8b1ebc232c9316b275315ceaa945bad021c6e2a1865", + "vout": 47, + "address": "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", + "label": "", + "scriptPubKey": "76a914c6c34d94969bccf7faaa8e5e5bb739e4d68ee9f888ac", + "amount": 0.00679256, + "confirmations": 5101, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "0d15d7b1665531e99a2cc7a13bec5f1cd15df65c55acbde5ab042420284c1266", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22023, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "0d15d7b1665531e99a2cc7a13bec5f1cd15df65c55acbde5ab042420284c1266", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22023, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "02c93f8c842cb285b96ead1b708e9bd8b1ffb70fe31e57ca45e2ff1630fa2f67", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22024, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "02c93f8c842cb285b96ead1b708e9bd8b1ffb70fe31e57ca45e2ff1630fa2f67", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22024, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "9ba087862f5bf13148e2e3f82ce4eb966e9f802ec1c660b7256861f52bbc3067", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22155, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "9ba087862f5bf13148e2e3f82ce4eb966e9f802ec1c660b7256861f52bbc3067", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22155, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "36a64edc8611b27b2b3633d386cad6964bd1ca60d674927d824005707a190868", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 0.00000667, + "confirmations": 19313, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "82c82de6117a869e9ab57b7a663d833a1ff9206e27e2a36b7dab4d4629560069", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 0.00149448, + "confirmations": 37562, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "786cf1ab0510eea36a1c84cd7a32462e1617ba33b8d096ccc01ac3ddd1b5a26b", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 0.00000777, + "confirmations": 45522, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "6a286c761226312fac53ce1cf66fa4e109ba9ecd486a971ab9d851b62719386d", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 620.63285485, + "confirmations": 49276, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "3495762721ea073730a19f706555f048cdd179a718a90fef29098787f16ba06d", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22023, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "3495762721ea073730a19f706555f048cdd179a718a90fef29098787f16ba06d", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22023, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "3895c4a8a35ddaf01d0b73458fbc92d1d6909116dd71b11375712c8b5ba5706e", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 21190, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "3895c4a8a35ddaf01d0b73458fbc92d1d6909116dd71b11375712c8b5ba5706e", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 21190, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "81906c87a24dd8054eb5eb2e480c6cc384a301da61d5e7addcc32d202abeaf6f", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 21195, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "81906c87a24dd8054eb5eb2e480c6cc384a301da61d5e7addcc32d202abeaf6f", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 21195, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "68c554ccb9f8b0693ef65286fda5ce64bd9cdc42775b75741b147a9a6f64c770", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22308, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "68c554ccb9f8b0693ef65286fda5ce64bd9cdc42775b75741b147a9a6f64c770", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22308, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "4b910667243fdce07058ecaf45e8c5cb2f76a29130f15f52418965ddcb8d3c71", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22890, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "4b910667243fdce07058ecaf45e8c5cb2f76a29130f15f52418965ddcb8d3c71", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22890, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "b874c2d315259406ac44ec2f884d1204ef510cc8f68bbc6331c19c11cb4e4871", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 610.85495491, + "confirmations": 49276, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "9730a9b7fefbd4b7c057730a0400e4fde4e22cfab4b2e5bf641f7e352c675072", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 1890.00000000, + "confirmations": 51629, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "365bd910826ea16995ed5ef98b173ee79636843a1462b3369288ef239b468572", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 4807.81367335, + "confirmations": 49293, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "1223605b331ef73c3d41990cfb6f2d923e65c47435abf3e1087b12cef5365874", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 6108.50718403, + "confirmations": 49292, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "cb01a74e23e701744c0df6b83ba30c6cf818cb0b7cdbaaf7c050ebc130902976", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22888, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "cb01a74e23e701744c0df6b83ba30c6cf818cb0b7cdbaaf7c050ebc130902976", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22888, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "d08b0e63c64f166ebc8106b580287dd87a9590e2becc4793d7898d3dc5335676", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 0.00000962, + "confirmations": 24725, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "95d489f0e893d8331a5f02feb322bbad88a5ce90076b844ba0602388cf6b9a76", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 2859.00000000, + "confirmations": 51557, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "becad1e458fbd74f43e6cd6755570fc891f9cc12793a61b081d68e15c77c5778", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 400.00000000, + "confirmations": 20914, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "95a149347c1e8f5bfa07538cbc07006d947804864adc3402227b5d4065f97278", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 21896, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "95a149347c1e8f5bfa07538cbc07006d947804864adc3402227b5d4065f97278", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 21896, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "88d48dae117aa8de4af3c8cde5f4d57d0e7a463d769b74379ea920474c6b527b", + "vout": 14, + "address": "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", + "label": "", + "scriptPubKey": "76a914c6c34d94969bccf7faaa8e5e5bb739e4d68ee9f888ac", + "amount": 0.00876837, + "confirmations": 750, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "846aa9a20bea243f49dc660d44b79308d5ef96b97752e71d163997bdb001af7b", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 1.00000000, + "confirmations": 23066, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "bdbb84c460ae0bdec058f410b208d8d2f00e3c0678607aaa64508539b6266f7d", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22433, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "bdbb84c460ae0bdec058f410b208d8d2f00e3c0678607aaa64508539b6266f7d", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22433, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "4e450dfee8e78d34903f881419980328bf90224c24a35ba5b354302f9d10d97d", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22306, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "4e450dfee8e78d34903f881419980328bf90224c24a35ba5b354302f9d10d97d", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22306, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "c7c47da9165734c7745c13450fd61ec50c6638d1d6e58546f368c0acbe73877e", + "vout": 63, + "address": "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", + "label": "", + "scriptPubKey": "76a914c6c34d94969bccf7faaa8e5e5bb739e4d68ee9f888ac", + "amount": 0.01034200, + "confirmations": 2924, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "75bcd7fc129734aecd5f0c6f42412e77919d8969f499cf534d2c09f1a2a56a7f", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 0.00100000, + "confirmations": 50658, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "75bcd7fc129734aecd5f0c6f42412e77919d8969f499cf534d2c09f1a2a56a7f", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 0.00234267, + "confirmations": 50658, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "6e5a814cc715669e4d8d0b213aa2728fc4ce9ed333462835d75bb1fd9807d580", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 100.00000000, + "confirmations": 22905, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "6e5a814cc715669e4d8d0b213aa2728fc4ce9ed333462835d75bb1fd9807d580", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 100.00000000, + "confirmations": 22905, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "6e5a814cc715669e4d8d0b213aa2728fc4ce9ed333462835d75bb1fd9807d580", + "vout": 2, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 100.00000000, + "confirmations": 22905, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "6e5a814cc715669e4d8d0b213aa2728fc4ce9ed333462835d75bb1fd9807d580", + "vout": 3, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 100.00000000, + "confirmations": 22905, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "6e5a814cc715669e4d8d0b213aa2728fc4ce9ed333462835d75bb1fd9807d580", + "vout": 4, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 100.00000000, + "confirmations": 22905, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "fe123697bc5f4ad079e81cdccbbb5834c240072f2874c8a43c68456dd8ef7a84", + "vout": 49, + "address": "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", + "label": "", + "scriptPubKey": "76a914c6c34d94969bccf7faaa8e5e5bb739e4d68ee9f888ac", + "amount": 0.00792699, + "confirmations": 4744, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "b7fb9b4cc150844e60095f747b68f513214302887c2dd1d70498c107c0803386", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 1034.59775864, + "confirmations": 47493, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "f90fe487d44ba67c394845abe066e87dd0d4324e610ace6e341832d572c8b988", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22023, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "f90fe487d44ba67c394845abe066e87dd0d4324e610ace6e341832d572c8b988", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22023, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "f049e41479e19bf99662ca2d6cbfb476c8490fc1b30b503a20151f5e6b10a789", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22312, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "f049e41479e19bf99662ca2d6cbfb476c8490fc1b30b503a20151f5e6b10a789", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22312, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "c46702d770e419a8da14b9b2d152fd924af4ad12d5b06adbb837857b2ab2f78a", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22431, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "c46702d770e419a8da14b9b2d152fd924af4ad12d5b06adbb837857b2ab2f78a", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22431, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "e7f602dac2e4c2e9eb7fb8e94a4a730f1a2d8e1a9111df4dac26625c691f008b", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 20914, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "e7f602dac2e4c2e9eb7fb8e94a4a730f1a2d8e1a9111df4dac26625c691f008b", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 20914, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "d8a3e312696bb44e0af694027c3e0156eef0d9c4dd0de7257acdf0e2b834038b", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22310, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "d8a3e312696bb44e0af694027c3e0156eef0d9c4dd0de7257acdf0e2b834038b", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22310, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "125debfa9176298af0832a2fa0a1a248d882170de0c6bc6e958340084e5e018f", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 0.00041000, + "confirmations": 1742, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "f9b91059efb2ef140a437fe9895e84857ebe2c691eae716599d1888b5375528f", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22436, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "f9b91059efb2ef140a437fe9895e84857ebe2c691eae716599d1888b5375528f", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22436, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "e003dcdf911f7c1e4b3e0508665067d71b5693bcb6f36eb77f2ec67ed0850c90", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 0.00000546, + "confirmations": 32531, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "7b27646c34be03524296b22972e0893ae4018f213f900dc2fab9d56481910a95", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22436, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "7b27646c34be03524296b22972e0893ae4018f213f900dc2fab9d56481910a95", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22436, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "251dd54b72ecb3f0892983b0271a2cf938b474765af630c78ebe2b660a861c95", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 0.00000900, + "confirmations": 33993, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "3168e5c997f44d8305dee921b768fd34642699e7015f4be506b42e017b4c7296", + "vout": 12, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 0.00243903, + "confirmations": 47160, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "66e44a4b4d45025636465b04d4077e599241f3ad8017c1d22f76d886f6cfd296", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22024, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "66e44a4b4d45025636465b04d4077e599241f3ad8017c1d22f76d886f6cfd296", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22024, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "6df879ac8f8f2b6fef79b71098fff03cc74085392fb2bbd4b77c14bafdefc497", + "vout": 35, + "address": "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", + "label": "", + "scriptPubKey": "76a914c6c34d94969bccf7faaa8e5e5bb739e4d68ee9f888ac", + "amount": 0.01313314, + "confirmations": 1398, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "9d07e03e2860b54a8631a3759a2b9150e47e082cb09fc4783ed5b7a40611779d", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 2138.58020581, + "confirmations": 49275, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "9639154ff134908fafc1c93b2bd3c2cbdbf170bc3f67e55f6a6d328bc155d69d", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 0.00001000, + "confirmations": 6114, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "51d86a3ae7327d196c0c240099ee2586478e688a857498e96a5de807f7e3769f", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22156, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "51d86a3ae7327d196c0c240099ee2586478e688a857498e96a5de807f7e3769f", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22156, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "00e702abddccf05a7da50143c3139436a5c6ef0e613593af01cba8c983faa99f", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 24004.25252255, + "confirmations": 47493, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "4d6c6e9f64bf90c27166e2048f1431286a1d647ea5027d2407fc77a4c80158a1", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 0.00000888, + "confirmations": 11506, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "0fcc2e09886ae15363b974c3624e779bb3d837b55ff76a4968cc8caca9a195a1", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 2190.00000000, + "confirmations": 51628, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "a40df1a4b4142c76cacd64217ebc8112b5427ee5d8dd3b7fea1a3a765e4fbca1", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22158, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "a40df1a4b4142c76cacd64217ebc8112b5427ee5d8dd3b7fea1a3a765e4fbca1", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22158, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "266dcda87eaab342c2804926d99ec2b6a7a09753688125f37de24b2b36bda8a2", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 0.00000546, + "confirmations": 26116, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "05b2a9ae5e2d150ef042af1fe62a67652298b885d0fbe11f1be4ce850fcecfa6", + "vout": 60, + "address": "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", + "label": "", + "scriptPubKey": "76a914c6c34d94969bccf7faaa8e5e5bb739e4d68ee9f888ac", + "amount": 0.00754670, + "confirmations": 4424, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "f20727393b0a586a3062a615fb71f43ec21c24258c3c6ec546fee5cbc1fa2ba7", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 21890, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "f20727393b0a586a3062a615fb71f43ec21c24258c3c6ec546fee5cbc1fa2ba7", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 21890, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "d0b2b44bb25f404539b4b7d2b15f90b82fd63d0e58d84a9e0c6c1829da1051a7", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 0.00003000, + "confirmations": 5099, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "e4f477d4240fbbf56f1ddf7dfd9615c72c71ccefb451ce120340aed0b36298a8", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 0.00000600, + "confirmations": 3661, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "0a6f6ceea85f32cd1c3d6617c77b24b51ec6d80a18c68deaf9e2441ddab309a9", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 21195, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "0a6f6ceea85f32cd1c3d6617c77b24b51ec6d80a18c68deaf9e2441ddab309a9", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 21195, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "28cc1065518b3f9a2829d43e4e722381bcab934f10ec060ef8c6ed4d24ee4da9", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 501.24207676, + "confirmations": 49276, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "1c74b9ca82cc0bb0b993fb1a2148d28a626527686dd879418382f9a7e5fc5aaa", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 100.00000000, + "confirmations": 22891, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "1c74b9ca82cc0bb0b993fb1a2148d28a626527686dd879418382f9a7e5fc5aaa", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 100.00000000, + "confirmations": 22891, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "1c74b9ca82cc0bb0b993fb1a2148d28a626527686dd879418382f9a7e5fc5aaa", + "vout": 2, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 100.00000000, + "confirmations": 22891, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "1c74b9ca82cc0bb0b993fb1a2148d28a626527686dd879418382f9a7e5fc5aaa", + "vout": 3, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 100.00000000, + "confirmations": 22891, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "1c74b9ca82cc0bb0b993fb1a2148d28a626527686dd879418382f9a7e5fc5aaa", + "vout": 4, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 100.00000000, + "confirmations": 22891, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "e017403feec313d8d3070a5f8cba20bce4b1c0fdf464925397f30ccf413aeaac", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 0.00001182, + "confirmations": 22844, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "f8f4c047445d45e0f7b7a2adf40cfc1bf50f877f2413c73418b8d84f843773ad", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 0.00010000, + "confirmations": 21903, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "77c11bae4b6aaa58cf170c06d4fbd18806f67dcce089de9eefe0b15684f4d8ad", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 515.61407310, + "confirmations": 49276, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "b6ae5574b932c760a7f270fb33f57d40f21309ba5d0234375cecb0bd69eab8ae", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 0.00009612, + "confirmations": 32912, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "536ec09ed0c3b4c555b01c23bd7c5b176c7e33b752ab6990e54fcdf0cfccf5ae", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 0.00014609, + "confirmations": 29338, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "c909b52937acbbfabb7f3488cc27265466c456fbfde973d7ec7ee7203e6afcaf", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 0.00002000, + "confirmations": 41783, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "ac37058daaa97ffde292f4f27ea8b040dcdb1ef57ccbf13bed4e19b1ae3112b0", + "vout": 2, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 199.99772171, + "confirmations": 31792, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "186b75e0191b1be3889272d2487b4e9142615d4b01965e9ed3651163cca8c8b2", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22156, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "186b75e0191b1be3889272d2487b4e9142615d4b01965e9ed3651163cca8c8b2", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22156, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "8bd3e8a394ef2c3ebcdc3074835f33187f3ef48442b5b04a75634fc76db4cbb2", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 0.00010604, + "confirmations": 11483, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "5ca9436af300144e1bed6a87c3174123db6d8d6e26c61ee08e9bb179e39e51b4", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 0.00063228, + "confirmations": 12007, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "cdde2bff6d6dccdafe2d7fdb2150a7ed17a9904ff16b20da851acb4304aae1b6", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 2877.78608507, + "confirmations": 51539, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "ea5f605e1292b2f678f9a9f663318fa13fe364bc1d3b607f8a93d52ba13497b9", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 21890, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "ea5f605e1292b2f678f9a9f663318fa13fe364bc1d3b607f8a93d52ba13497b9", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 21890, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "c9590ed1299e22c2e4e1275a953258c9cb7052f87f5e7de7decb055248a06eba", + "vout": 8, + "address": "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", + "label": "", + "scriptPubKey": "76a914c6c34d94969bccf7faaa8e5e5bb739e4d68ee9f888ac", + "amount": 0.00794840, + "confirmations": 1088, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "673157f4e74f8c846851d58b00401ebf9a29884a6963759b92bb470c9d877bba", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22433, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "673157f4e74f8c846851d58b00401ebf9a29884a6963759b92bb470c9d877bba", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22433, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "737a1adaababe975238ecfbc4f45543d377b5a24b3c24089a4eba12611952cbb", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 21191, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "737a1adaababe975238ecfbc4f45543d377b5a24b3c24089a4eba12611952cbb", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 21191, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "bfef18a64c43aeea0567a7d937849751463b6a4f4b9cee3601063316c47734bc", + "vout": 4, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 0.00010272, + "confirmations": 10628, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "aa573d3e4801a27b7012becdb6a2c6dbc62cb7a8d9ac32d19da4c40ab51d69bc", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22155, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "aa573d3e4801a27b7012becdb6a2c6dbc62cb7a8d9ac32d19da4c40ab51d69bc", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22155, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "ea94b05d7a6ffc380fd72fe11adc109f52568631c1c59e0cbcf015f68e7c9bbd", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 324.99800000, + "confirmations": 49276, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "809b4bbdf2d5cf4ff6ab97017054cb5fe2869f05b7b89136d404840aea30aabd", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 20916, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "809b4bbdf2d5cf4ff6ab97017054cb5fe2869f05b7b89136d404840aea30aabd", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 20916, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "d058a01cdd598a9b77b4b7b30e866ddc2963ad3144f96133d2c6c7ef9f399fbe", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 599.60000000, + "confirmations": 51629, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "6d6dd53d70fa024011ca1a887686d6c0bbf19c739ed5017cad1adb77de7cbdbe", + "vout": 64, + "address": "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", + "label": "", + "scriptPubKey": "76a914c6c34d94969bccf7faaa8e5e5bb739e4d68ee9f888ac", + "amount": 0.01151435, + "confirmations": 3517, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "28e0c0f10743a705e775b46c1f8344b9eea9046355c90cacf66f0365e7b3f8be", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22431, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "28e0c0f10743a705e775b46c1f8344b9eea9046355c90cacf66f0365e7b3f8be", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22431, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "2ba72418d6717e8e008cc35861d0c36573cca933b7c9e1cf11d6dc512c8859bf", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 0.00100000, + "confirmations": 6643, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "6a78d61f36e906302eab419db28c1bd2f71f1d2cb21912f22c508938b4e09ebf", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 5000.00000000, + "confirmations": 47494, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "5dc732bf01fdcf328b7ec00db2b44783a0f7f5f8983e538b678751f28945f8bf", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 0.00010000, + "confirmations": 36369, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "48d83a2e85c2fcefe8a50c096186157639d523a56bd009bd254002bbe92febc1", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 21897, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "48d83a2e85c2fcefe8a50c096186157639d523a56bd009bd254002bbe92febc1", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 21897, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "21c5e860574674c6bf26cb5d66197822067a97ca729fa32387d6a9ad50065ec3", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 21896, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "21c5e860574674c6bf26cb5d66197822067a97ca729fa32387d6a9ad50065ec3", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 21896, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "77028e4894e979109f5daeee733097ceb09de1802dc24ac92c56355d39fb38c4", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 21893, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "77028e4894e979109f5daeee733097ceb09de1802dc24ac92c56355d39fb38c4", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 21893, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "54c16263f09435eacfee0bd7d05f7cebf81546dc2bb2b611025916f370745dc4", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 100.00000000, + "confirmations": 22891, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "54c16263f09435eacfee0bd7d05f7cebf81546dc2bb2b611025916f370745dc4", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 100.00000000, + "confirmations": 22891, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "54c16263f09435eacfee0bd7d05f7cebf81546dc2bb2b611025916f370745dc4", + "vout": 2, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 100.00000000, + "confirmations": 22891, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "54c16263f09435eacfee0bd7d05f7cebf81546dc2bb2b611025916f370745dc4", + "vout": 3, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 100.00000000, + "confirmations": 22891, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "54c16263f09435eacfee0bd7d05f7cebf81546dc2bb2b611025916f370745dc4", + "vout": 4, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 100.00000000, + "confirmations": 22891, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "823fb9e82f3aff8842db3d922e8cb5bd25814d0c7838c1cd60a90c879bc13cc6", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 0.00010000, + "confirmations": 21985, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "05c66f65285abe676b4010ff96cae2a246524b42f4085e1552863dc4e0416cc7", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 100.00000000, + "confirmations": 22927, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "05c66f65285abe676b4010ff96cae2a246524b42f4085e1552863dc4e0416cc7", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 100.00000000, + "confirmations": 22927, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "05c66f65285abe676b4010ff96cae2a246524b42f4085e1552863dc4e0416cc7", + "vout": 2, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 100.00000000, + "confirmations": 22927, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "05c66f65285abe676b4010ff96cae2a246524b42f4085e1552863dc4e0416cc7", + "vout": 3, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 100.00000000, + "confirmations": 22927, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "05c66f65285abe676b4010ff96cae2a246524b42f4085e1552863dc4e0416cc7", + "vout": 4, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 100.00000000, + "confirmations": 22927, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "0e3031539462f077f4c400de60fb32cf296819aaa9b2603e44070941047eb4c7", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 21897, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "0e3031539462f077f4c400de60fb32cf296819aaa9b2603e44070941047eb4c7", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 21897, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "b2e9279dcfed523722d60ca62984de5b6f2d12fde985af51cf4178cbf0c452c8", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 0.00000775, + "confirmations": 33736, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "99ea383c330bcb551939d5dc8c53b20f644c32c6fe5f8005fc36caf37ab10eca", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22027, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "99ea383c330bcb551939d5dc8c53b20f644c32c6fe5f8005fc36caf37ab10eca", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22027, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "9b3f2ef5e741df5739c6e02b8b0355c84d6f21ca588f5084bcd73eff86cdeacb", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22436, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "9b3f2ef5e741df5739c6e02b8b0355c84d6f21ca588f5084bcd73eff86cdeacb", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22436, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "4c04c115c959ee2654cdf0250a6151771a965b1bddbb1e95d39ceaee57e41fcc", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 100.00000000, + "confirmations": 22907, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "4c04c115c959ee2654cdf0250a6151771a965b1bddbb1e95d39ceaee57e41fcc", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 100.00000000, + "confirmations": 22907, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "4c04c115c959ee2654cdf0250a6151771a965b1bddbb1e95d39ceaee57e41fcc", + "vout": 2, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 100.00000000, + "confirmations": 22907, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "4c04c115c959ee2654cdf0250a6151771a965b1bddbb1e95d39ceaee57e41fcc", + "vout": 3, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 100.00000000, + "confirmations": 22907, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "4c04c115c959ee2654cdf0250a6151771a965b1bddbb1e95d39ceaee57e41fcc", + "vout": 4, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 100.00000000, + "confirmations": 22907, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "6197d51b6f67cdeaaf33b4639fc4f3d3a7b7e7e69db350f184eba091b7eb29cc", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 5000.00000000, + "confirmations": 47493, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "31ecd41422208033526919fee8a028a1de915da1308728ff8328a53f7f7d59cd", + "vout": 66, + "address": "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", + "label": "", + "scriptPubKey": "76a914c6c34d94969bccf7faaa8e5e5bb739e4d68ee9f888ac", + "amount": 0.00987020, + "confirmations": 2628, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "905b05d30cfc7b6bfb2b3ad644b696b0c03bcdf813ecb85df7140a349f07bacd", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 20919, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "5963e589736bea2b4a438cf7cede084fbb24f9b545fa7558ad407510260805ce", + "vout": 68, + "address": "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", + "label": "", + "scriptPubKey": "76a914c6c34d94969bccf7faaa8e5e5bb739e4d68ee9f888ac", + "amount": 0.01076674, + "confirmations": 3935, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "0fbd0147772e8b25efb7d038c65102572eb7bc1a7003ebaa31bab8ff4afd76ce", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 0.00003690, + "confirmations": 40745, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "df6a74ad3d4af01ad76937b43ac268d9775d447c97f5cc34c2b12c3b405e61cf", + "vout": 50, + "address": "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", + "label": "", + "scriptPubKey": "76a914c6c34d94969bccf7faaa8e5e5bb739e4d68ee9f888ac", + "amount": 0.00493433, + "confirmations": 4538, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "a721c70bf5e18c2320e669177b34075e22aa180165cdb587c358f731c609b8d0", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22309, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "a721c70bf5e18c2320e669177b34075e22aa180165cdb587c358f731c609b8d0", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22309, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "ef0bc16df6a9b89d3c13f34adae820d3cf6aa300970bbe9f06f598d727a4d2d0", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 21192, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "ef0bc16df6a9b89d3c13f34adae820d3cf6aa300970bbe9f06f598d727a4d2d0", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 21192, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "01ea61b951c16dd955c94973dde973a28d4538b741aabb751c7c5840bfb782d1", + "vout": 2079, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 0.00001730, + "confirmations": 9358, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "3a23e4081b37defb0856a1582b6b8e574e271415546340dd68570800b9e74cd2", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 0.00013909, + "confirmations": 2834, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "65ac81f352bc5b3ec65a0deed795c6967c2381f92465602d3281aea70153d8d2", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 0.00018284, + "confirmations": 19763, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "ec020bf10348e2919c5d46df1d8143b69a21f8534955fb1b34abde8cbb727dd6", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 36000.00000000, + "confirmations": 47489, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "768d3e2f525d8ed24c446463f163fb9b9c7af10e07097efe0eecc8600b7083d6", + "vout": 74, + "address": "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", + "label": "", + "scriptPubKey": "76a914c6c34d94969bccf7faaa8e5e5bb739e4d68ee9f888ac", + "amount": 0.01433360, + "confirmations": 1716, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "f0eb2c2e854ecae7d3bbbdf06ef25de25c78983466be16650c030be3c558ebd6", + "vout": 58, + "address": "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", + "label": "", + "scriptPubKey": "76a914c6c34d94969bccf7faaa8e5e5bb739e4d68ee9f888ac", + "amount": 0.01098392, + "confirmations": 425, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "8b7092aaa3357e3e80d10f36cd6d888cb91cb9dd2fa5b8899f509df64841fed6", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 0.00000546, + "confirmations": 51683, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "deb5a4f1534a48946aa7b72b111ba7a244951696c91319105d5f4ae29d528bd7", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22027, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "deb5a4f1534a48946aa7b72b111ba7a244951696c91319105d5f4ae29d528bd7", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22027, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "a73edf99d17868fd458520308febc71609e1fecd686530c525000359866a4ed9", + "vout": 35, + "address": "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", + "label": "", + "scriptPubKey": "76a914c6c34d94969bccf7faaa8e5e5bb739e4d68ee9f888ac", + "amount": 0.00902924, + "confirmations": 4057, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "c00b3813fb51016dd4caff5e26066acc231bfa237b6c51a4698d6faf454badd9", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 100.00000000, + "confirmations": 23064, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "aea6c1f4b231ab6900332c82fb9d48d24aac2245575e53f5fd7d6e86a7ea02dc", + "vout": 4, + "address": "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", + "label": "", + "scriptPubKey": "76a914c6c34d94969bccf7faaa8e5e5bb739e4d68ee9f888ac", + "amount": 0.00954847, + "confirmations": 583, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "8bcbb91847fcf73aad3e5146d4db77a455f373a5090d712fe86f1b88294ac9dc", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 0.00112529, + "confirmations": 8280, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "f0f715210444889ea08629d10bbae05f1c4767387b0baff008829cd571f744de", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 100.00000000, + "confirmations": 22929, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "f0f715210444889ea08629d10bbae05f1c4767387b0baff008829cd571f744de", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 100.00000000, + "confirmations": 22929, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "f0f715210444889ea08629d10bbae05f1c4767387b0baff008829cd571f744de", + "vout": 2, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 100.00000000, + "confirmations": 22929, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "d630ecf2a6a7df9206f0dfa9a3198b4741db3ca54dedde84d639614c5cd0c2df", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 0.00001111, + "confirmations": 32231, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "146951e150e042b26fac8929b9470a9bb4b6bb9abbde1aa3253be074256ad2df", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 20920, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "146951e150e042b26fac8929b9470a9bb4b6bb9abbde1aa3253be074256ad2df", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 20920, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "8e6f9195961da36c22484fc0f0eb83c405cbd341f245801fd60624b61f2e5ee1", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 0.00010000, + "confirmations": 12807, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "366166e6a1a2d7b99d44834e2d4c2c5988fb8da2e17756f21b8e4d2c0ac972e1", + "vout": 88, + "address": "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", + "label": "", + "scriptPubKey": "76a914c6c34d94969bccf7faaa8e5e5bb739e4d68ee9f888ac", + "amount": 0.01032596, + "confirmations": 4295, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "1371d52f0216a7597e192b1f21f25a9bd21323efe345a720a9ba984d8945a2e1", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22433, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "ac53f2a2052396354333e0e500864f21ac0909c4159f46880e0f9128ac7194e2", + "vout": 19, + "address": "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", + "label": "", + "scriptPubKey": "76a914c6c34d94969bccf7faaa8e5e5bb739e4d68ee9f888ac", + "amount": 0.01480214, + "confirmations": 1868, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "63b840a8abf477305dfad93db69205ff01a58e1527a317dbcc8a360533c0e9e2", + "vout": 60, + "address": "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", + "label": "", + "scriptPubKey": "76a914c6c34d94969bccf7faaa8e5e5bb739e4d68ee9f888ac", + "amount": 0.00412783, + "confirmations": 3842, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "8ce946ddf8c5e58aa7ee026693a25e221972e461d3492424bc6ad311d58dbde4", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 1219.00000000, + "confirmations": 51557, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "51441e7cf02d220a2cd8a27009d85be7f2f10a122ff4b98eeed50dd361235be7", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 20918, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "51441e7cf02d220a2cd8a27009d85be7f2f10a122ff4b98eeed50dd361235be7", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 20918, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "71168f5c4da5ce7b8ad5be02ed5b688c197a41f1b10f313dba3ae308a77ae6e8", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 21892, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "71168f5c4da5ce7b8ad5be02ed5b688c197a41f1b10f313dba3ae308a77ae6e8", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 21892, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "a59949f4f5b36ef4946f6c5773da36ff844ca6dfebdf636e3d9b5958e43cf6e9", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 0.00003690, + "confirmations": 41135, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "4aa5af5b601fbf16750cbba0e8b1eb0f254ee7e073d7f30c3b8d724d70d0c8ea", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 0.00000600, + "confirmations": 5852, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "a1b7481a7bd9ca950733d3950e115bf7362c14b52dba930b2d3a4589ca39cfea", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 20918, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "a1b7481a7bd9ca950733d3950e115bf7362c14b52dba930b2d3a4589ca39cfea", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 20918, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "36371ce1c52892825ad9d055753d87b5220b472a9f925ee7d858c0e5079de1ed", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 0.00010000, + "confirmations": 21903, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "f14b222e652c58d11435fa9172ddea000c6f5e20e6b715eb940fc28d1c4adeef", + "vout": 58, + "address": "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", + "label": "", + "scriptPubKey": "76a914c6c34d94969bccf7faaa8e5e5bb739e4d68ee9f888ac", + "amount": 0.01105047, + "confirmations": 2159, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "043315675c765eb4dd419c304796a90be62a8d75ff550616d8b8efe975d5f4ef", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 899.60000000, + "confirmations": 51629, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "6397ddcb286bc03380c376a08a36568d32935f27750e244ff9911c11298530f0", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 0.00000646, + "confirmations": 38407, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "269230f67c30fe6f9af2dac1ef7e45e5548c27482e1784f4e3cc3255bde89df0", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 21894, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "269230f67c30fe6f9af2dac1ef7e45e5548c27482e1784f4e3cc3255bde89df0", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 21894, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "b791724e3b876762bfabedead159ddb0df6d34e87912779c8640b2b82b68f8f1", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 21893, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "b791724e3b876762bfabedead159ddb0df6d34e87912779c8640b2b82b68f8f1", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 21893, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "31e037b468aa76912fe413bde67682823b285dae760959be18d25ab65de38df6", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 20918, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "31e037b468aa76912fe413bde67682823b285dae760959be18d25ab65de38df6", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 20918, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "f03f51abc901c7e29b59ab337afebdb033f81dde68d49f8a495f50f6cf877cf7", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 216.41673403, + "confirmations": 51629, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "3435b8e298717b2136c376d59ce46877008576ffe10a3df20d36e534e2da85f7", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22027, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "3435b8e298717b2136c376d59ce46877008576ffe10a3df20d36e534e2da85f7", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22027, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "b52e041df4f6cbccb11da34673318a0b190560402dc9a666ac99f753347b3af8", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 21896, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "b52e041df4f6cbccb11da34673318a0b190560402dc9a666ac99f753347b3af8", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 21896, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "a6c3328d0c88c9edbc034741c79b0260e4ce6cbf5be63d74d26c8a50102d50f8", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 200.00000000, + "confirmations": 49277, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "a71ddb779a051bcbe34cd5ebf8887ed0369a28022ccdc04382a0882bd83ae3f8", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22023, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "a71ddb779a051bcbe34cd5ebf8887ed0369a28022ccdc04382a0882bd83ae3f8", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 22023, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "61201dcb409d186bd5996d98d8cfd31ef87d31e81c4e0be656a672385442f1f9", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 21193, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "61201dcb409d186bd5996d98d8cfd31ef87d31e81c4e0be656a672385442f1f9", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 21193, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "2d5ce4fe1d1f27a17bc161299834896f4e467f0e7efa3722b7b7408022e4a5fa", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 21191, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "2d5ce4fe1d1f27a17bc161299834896f4e467f0e7efa3722b7b7408022e4a5fa", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 21191, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "4548d64d8f75fcc97ebb9054084161148706d609759d1015dc6e1908c9271afb", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 7511.23986970, + "confirmations": 47493, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "d13fa233bf4c77e8cbae4194e7c1c7550aaec26ccd34c09ba743e81d8d5aa1fd", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 0.00000666, + "confirmations": 43360, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "46653593f5b6c3dbcd4bd56b386194bf7c2459e2a4bfd9adcf86a8025b877eff", + "vout": 0, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 21190, + "spendable": false, + "solvable": false, + "safe": true + }, + { + "txid": "46653593f5b6c3dbcd4bd56b386194bf7c2459e2a4bfd9adcf86a8025b877eff", + "vout": 1, + "address": "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP", + "label": "", + "scriptPubKey": "a9142beec605c9a6512f55fe93ad76753e24fc8579b087", + "amount": 500.00000000, + "confirmations": 21190, + "spendable": false, + "solvable": false, + "safe": true + } +] \ No newline at end of file From 322d9ac480a1dce9d8034272e1cf543e1c679f9c Mon Sep 17 00:00:00 2001 From: Igor Artamonov Date: Mon, 20 Apr 2020 21:42:53 -0400 Subject: [PATCH 03/13] problem: Kotlin class mocking randomly fails solution: upgrade testing libs, and open the class --- build.gradle | 5 ++--- gradle.properties | 2 +- .../dshackle/upstream/bitcoin/BitcoinRpcClient.kt | 2 +- .../dshackle/upstream/bitcoin/BitcoinRpcHeadSpec.groovy | 8 -------- 4 files changed, 4 insertions(+), 13 deletions(-) diff --git a/build.gradle b/build.gradle index a216236a..1e887196 100644 --- a/build.gradle +++ b/build.gradle @@ -107,12 +107,11 @@ dependencies { testCompile "org.codehaus.groovy:groovy:$groovyVersion" - testCompile 'cglib:cglib-nodep:3.2.12' + testCompile 'cglib:cglib-nodep:3.3.0' testCompile "org.spockframework:spock-core:$spockVersion" - testCompile 'de.jodamob.kotlin:kotlin-runner-spock:0.3.1' testCompile "io.grpc:grpc-testing:${grpcVersion}" testCompile "io.projectreactor:reactor-test:$reactorVersion" - testCompile 'org.objenesis:objenesis:3.0.1' + testCompile 'org.objenesis:objenesis:3.1' testCompile 'org.mock-server:mockserver-netty:5.10' } diff --git a/gradle.properties b/gradle.properties index c5ae5013..6f9e6d14 100644 --- a/gradle.properties +++ b/gradle.properties @@ -18,5 +18,5 @@ reactorVersion=3.3.3.RELEASE etherjarVersion=0.9.1 # Testing -spockVersion=1.2-groovy-2.5 +spockVersion=1.3-groovy-2.5 diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinRpcClient.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinRpcClient.kt index fa58efb3..35ce0d9e 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinRpcClient.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinRpcClient.kt @@ -10,7 +10,7 @@ import reactor.netty.http.client.HttpClient import java.util.* import java.util.function.Consumer -class BitcoinRpcClient( +open class BitcoinRpcClient( private val target: String, basicAuth: AuthConfig.ClientBasicAuth? ) { diff --git a/src/test/groovy/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinRpcHeadSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinRpcHeadSpec.groovy index a8739cfe..0aab0d19 100644 --- a/src/test/groovy/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinRpcHeadSpec.groovy +++ b/src/test/groovy/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinRpcHeadSpec.groovy @@ -1,22 +1,14 @@ package io.emeraldpay.dshackle.upstream.bitcoin -import de.jodamob.kotlin.testrunner.OpenedClasses -import de.jodamob.kotlin.testrunner.SpotlinTestRunner import io.emeraldpay.dshackle.test.TestingCommons -import org.junit.runner.RunWith -import org.mockserver.integration.ClientAndServer import reactor.core.publisher.Mono import reactor.test.StepVerifier import spock.lang.Specification import java.time.Duration -@RunWith(SpotlinTestRunner) -@OpenedClasses(BitcoinApi) class BitcoinRpcHeadSpec extends Specification { - ClientAndServer mockServer - def "Follow 2 blocks created over 3 requests"() { setup: String hash1 = "0000000000000000000cf5a5d4dfc4347c0c1a863ec5fdb429b02b2162e50001" From 548154609dd8bb26ffcdd91746c94c756ade1ec8 Mon Sep 17 00:00:00 2001 From: Igor Artamonov Date: Mon, 20 Apr 2020 21:43:49 -0400 Subject: [PATCH 04/13] solution: subscribe to bitcoin address balance --- .../dshackle/rpc/TrackBitcoinAddress.kt | 94 ++++++--- .../rpc/TrackBitcoinAddressSpec.groovy | 184 ++++++++++++++++-- 2 files changed, 234 insertions(+), 44 deletions(-) diff --git a/src/main/kotlin/io/emeraldpay/dshackle/rpc/TrackBitcoinAddress.kt b/src/main/kotlin/io/emeraldpay/dshackle/rpc/TrackBitcoinAddress.kt index 0a540114..3ecde39b 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/rpc/TrackBitcoinAddress.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/rpc/TrackBitcoinAddress.kt @@ -16,6 +16,7 @@ import reactor.util.function.Tuples import java.math.BigDecimal import java.math.BigInteger import java.util.* +import kotlin.collections.HashMap @Service class TrackBitcoinAddress( @@ -30,35 +31,42 @@ class TrackBitcoinAddress( return BlockchainType.fromBlockchain(chain) == BlockchainType.BITCOIN && upstreams.isAvailable(chain) } - override fun getBalance(req: BlockchainOuterClass.BalanceRequest): Flux { - if (!req.hasAddress()) { - return Flux.error(SilentException("Address not provided")) + fun allAddresses(request: BlockchainOuterClass.BalanceRequest): List? { + if (!request.hasAddress()) { + return null } - val chain = Chain.byId(req.asset.chainValue) - val upstream = upstreams.getUpstream(chain)?.castApi(BitcoinApi::class.java) - ?: return Flux.error(SilentException.UnsupportedBlockchain(req.asset.chainValue)) - val addressesAll = when { - req.address.hasAddressSingle() -> { - listOf(req.address.addressSingle.address) + return when { + request.address.hasAddressSingle() -> { + listOf(request.address.addressSingle.address) } - req.address.hasAddressMulti() -> { - req.address.addressMulti.addressesList + request.address.hasAddressMulti() -> { + request.address.addressMulti.addressesList .map { addr -> addr.address } + .sorted() } - else -> { - return Flux.error(SilentException("Unsupported address")) - } + else -> null + } + } + + fun requestBalances(chain: Chain, api: BitcoinApi, addresses: List): Flux { + return api.executeAndResult(0, "listunspent", emptyList(), List::class.java) + .flatMapMany { unspents -> + val result = getTotal(chain, addresses, unspents) + Flux.fromIterable(result) + } + } + + override fun getBalance(request: BlockchainOuterClass.BalanceRequest): Flux { + val chain = Chain.byId(request.asset.chainValue) + val upstream = upstreams.getUpstream(chain)?.castApi(BitcoinApi::class.java) + ?: return Flux.error(SilentException.UnsupportedBlockchain(request.asset.chainValue)) + val addresses = allAddresses(request) ?: return Flux.error(SilentException("Unsupported address")) + if (addresses.isEmpty()) { + return Flux.empty() } val result = upstream.getApi(Selector.empty).flatMapMany { api -> - val addresses = addressesAll.sorted() - val results = api.executeAndResult(0, "listunspent", emptyList(), List::class.java) - .flatMapMany { unspents -> - val result = getTotal(chain, addresses, unspents) - Flux.fromIterable(result) - } - results.map { addr -> - buildResponse(addr) - } + requestBalances(chain, api, addresses) + .map(this@TrackBitcoinAddress::buildResponse) } return result } @@ -98,23 +106,49 @@ class TrackBitcoinAddress( override fun subscribe(request: BlockchainOuterClass.BalanceRequest): Flux { - return Flux.error(SilentException("Not Implemented")) + val chain = Chain.byId(request.asset.chainValue) + val upstream = upstreams.getUpstream(chain)?.castApi(BitcoinApi::class.java) + ?: return Flux.error(SilentException.UnsupportedBlockchain(request.asset.chainValue)) + val addresses = allAddresses(request) ?: return Flux.error(SilentException("Unsupported address")) + if (addresses.isEmpty()) { + return Flux.empty() + } + val initial = upstream.getApi(Selector.empty).flatMapMany { api -> + requestBalances(chain, api, addresses) + } + val following = upstream.getHead().getFlux() + .flatMap { block -> + upstream.getApi(Selector.empty).flatMapMany { api -> + requestBalances(chain, api, addresses) + } + } + val last = HashMap() + val result = Flux.merge(initial, following) + .filter { curr -> + val prev = last[curr.address] + val updated = prev == null || curr.balance != prev + last[curr.address] = curr.balance + updated + } + + return result.map(this@TrackBitcoinAddress::buildResponse) } private fun buildResponse(address: AddressBalance): BlockchainOuterClass.AddressBalance { return BlockchainOuterClass.AddressBalance.newBuilder() .setBalance(address.balance.toString(10)) .setAsset(Common.Asset.newBuilder() - .setChainValue(address.chain.id) + .setChainValue(address.address.chain.id) .setCode("BTC")) - .setAddress(Common.SingleAddress.newBuilder().setAddress(address.address)) + .setAddress(Common.SingleAddress.newBuilder().setAddress(address.address.address)) .build() } - open class AddressBalance(val chain: Chain, val address: String, var balance: BigInteger = BigInteger.ZERO) { - open fun withBalance(balance: BigInteger) = AddressBalance(chain, address, balance) - - open fun plus(other: AddressBalance) = AddressBalance(chain, address, balance + other.balance) + open class AddressBalance(val address: Address, var balance: BigInteger = BigInteger.ZERO) { + constructor(chain: Chain, address: String, balance: BigInteger) : this(Address(chain, address), balance) + fun plus(other: AddressBalance) = AddressBalance(address, balance + other.balance) } + + data class Address(val chain: Chain, val address: String) } \ No newline at end of file diff --git a/src/test/groovy/io/emeraldpay/dshackle/rpc/TrackBitcoinAddressSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/rpc/TrackBitcoinAddressSpec.groovy index a317a90d..ab17ef22 100644 --- a/src/test/groovy/io/emeraldpay/dshackle/rpc/TrackBitcoinAddressSpec.groovy +++ b/src/test/groovy/io/emeraldpay/dshackle/rpc/TrackBitcoinAddressSpec.groovy @@ -1,12 +1,29 @@ package io.emeraldpay.dshackle.rpc +import io.emeraldpay.api.proto.BlockchainOuterClass +import io.emeraldpay.api.proto.Common +import io.emeraldpay.dshackle.data.BlockContainer +import io.emeraldpay.dshackle.data.BlockId import io.emeraldpay.dshackle.test.TestingCommons +import io.emeraldpay.dshackle.upstream.AggregatedUpstream +import io.emeraldpay.dshackle.upstream.Head +import io.emeraldpay.dshackle.upstream.Upstream import io.emeraldpay.dshackle.upstream.Upstreams +import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinApi import io.emeraldpay.grpc.Chain +import reactor.core.publisher.Flux +import reactor.core.publisher.Mono +import reactor.core.publisher.TopicProcessor +import reactor.test.StepVerifier import spock.lang.Specification +import java.time.Duration +import java.time.Instant + class TrackBitcoinAddressSpec extends Specification { + String hash1 = "0xa0e65cbc1b52a8ca60562112c6060552d882f16f34a9dba2ccdc05c0a6a27c22" + def "Correct sum from multiple"() { setup: def json = this.class.getClassLoader().getResourceAsStream("bitcoin/unspent-one-addr.json") @@ -17,8 +34,8 @@ class TrackBitcoinAddressSpec extends Specification { then: total.size() == 1 - total[0].chain == Chain.BITCOIN - total[0].address == "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK" + total[0].address.chain == Chain.BITCOIN + total[0].address.address == "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK" total[0].balance.toString() == "32928461" } @@ -32,8 +49,8 @@ class TrackBitcoinAddressSpec extends Specification { then: total.size() == 1 - total[0].chain == Chain.BITCOIN - total[0].address == "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK" + total[0].address.chain == Chain.BITCOIN + total[0].address.address == "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK" total[0].balance.toString() == "32928461" } @@ -43,18 +60,18 @@ class TrackBitcoinAddressSpec extends Specification { def unspents = TestingCommons.objectMapper().readValue(json, List) TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(Upstreams)) when: - def total = track.getTotal(Chain.BITCOIN, ["1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP"], unspents).sort { it.address } + def total = track.getTotal(Chain.BITCOIN, ["1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP"], unspents).sort { it.address.address } then: total.size() == 2 with(total[0]) { - chain == Chain.BITCOIN - address == "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK" + address.chain == Chain.BITCOIN + address.address == "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK" balance.toString() == "32928461" } with(total[1]) { - chain == Chain.BITCOIN - address == "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP" + address.chain == Chain.BITCOIN + address.address == "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP" balance.toString() == "25550215615737" } } @@ -67,8 +84,8 @@ class TrackBitcoinAddressSpec extends Specification { then: total.size() == 1 - total[0].chain == Chain.BITCOIN - total[0].address == "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK" + total[0].address.chain == Chain.BITCOIN + total[0].address.address == "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK" total[0].balance.toString() == "0" } @@ -78,18 +95,157 @@ class TrackBitcoinAddressSpec extends Specification { def unspents = TestingCommons.objectMapper().readValue(json, List) TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(Upstreams)) when: - def total = track.getTotal(Chain.BITCOIN, ["16rCmCmbuWDhPjWTrpQGaU3EPdZF7MTdUk", "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK"], unspents).sort { it.address } + def total = track.getTotal(Chain.BITCOIN, ["16rCmCmbuWDhPjWTrpQGaU3EPdZF7MTdUk", "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK"], unspents).sort { it.address.address } then: total.size() == 2 with(total[0]) { - address == "16rCmCmbuWDhPjWTrpQGaU3EPdZF7MTdUk" + address.address == "16rCmCmbuWDhPjWTrpQGaU3EPdZF7MTdUk" balance.toString() == "0" } with(total[1]) { - address == "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK" + address.address == "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK" balance.toString() == "32928461" } } + def "One address for single provided"() { + setup: + TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(Upstreams)) + def req = BlockchainOuterClass.BalanceRequest.newBuilder() + .setAddress( + Common.AnyAddress.newBuilder() + .setAddressSingle( + Common.SingleAddress.newBuilder() + .setAddress("16rCmCmbuWDhPjWTrpQGaU3EPdZF7MTdUk") + ) + ) + .build() + when: + def act = track.allAddresses(req) + then: + act == ["16rCmCmbuWDhPjWTrpQGaU3EPdZF7MTdUk"] + } + + def "Sorted addresses for multiple provided"() { + setup: + TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(Upstreams)) + def req = BlockchainOuterClass.BalanceRequest.newBuilder() + .setAddress( + Common.AnyAddress.newBuilder() + .setAddressMulti( + Common.MultiAddress.newBuilder() + .addAddresses(Common.SingleAddress.newBuilder().setAddress("16rCmCmbuWDhPjWTrpQGaU3EPdZF7MTdUk")) + .addAddresses(Common.SingleAddress.newBuilder().setAddress("3BMqADKWoWHPASsUdHvnUL6E1jpZkMnLZz")) + .addAddresses(Common.SingleAddress.newBuilder().setAddress("1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK")) + .addAddresses(Common.SingleAddress.newBuilder().setAddress("bc1qdthqvt6cllzej7uhdddrltdfsmnt7d0gl5ue5n")) + ) + ) + .build() + when: + def act = track.allAddresses(req) + then: + act == ["16rCmCmbuWDhPjWTrpQGaU3EPdZF7MTdUk", "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", "3BMqADKWoWHPASsUdHvnUL6E1jpZkMnLZz", "bc1qdthqvt6cllzej7uhdddrltdfsmnt7d0gl5ue5n"] + } + + def "Null for no address provided"() { + setup: + TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(Upstreams)) + def req = BlockchainOuterClass.BalanceRequest.newBuilder() + .build() + when: + def act = track.allAddresses(req) + then: + act == null + } + + def "Build proto for common balance"() { + setup: + TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(Upstreams)) + def balance = new TrackBitcoinAddress.AddressBalance(Chain.BITCOIN, "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", BigInteger.valueOf(123456)) + when: + def act = track.buildResponse(balance) + then: + act.address.address == "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK" + act.balance == "123456" + act.asset.chain.number == Chain.BITCOIN.id + act.asset.code == "BTC" + } + + def "Build proto for zero balance"() { + setup: + TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(Upstreams)) + def balance = new TrackBitcoinAddress.AddressBalance(Chain.BITCOIN, "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", BigInteger.ZERO) + when: + def act = track.buildResponse(balance) + then: + act.address.address == "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK" + act.balance == "0" + act.asset.chain.number == Chain.BITCOIN.id + act.asset.code == "BTC" + } + + def "Build proto for all bitcoins"() { + setup: + TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(Upstreams)) + def balance = new TrackBitcoinAddress.AddressBalance(Chain.BITCOIN, "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", BigInteger.valueOf(21_000_000).multiply(BigInteger.TEN.pow(8))) + when: + def act = track.buildResponse(balance) + then: + act.address.address == "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK" + act.balance == "2100000000000000" + act.asset.chain.number == Chain.BITCOIN.id + act.asset.code == "BTC" + } + + def "Get update for a balance"() { + setup: + + BitcoinApi api = Mock(BitcoinApi) { + 2 * executeAndResult(0, "listunspent", [], List) >>> [ + Mono.just([]), Mono.just([[address: "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", amount: 0.0123]]) + ] + } + def blocks = TopicProcessor.create() + Head head = Mock(Head) { + 1 * getFlux() >> Flux.from(blocks) + } + Upstream upstream + upstream = Mock(AggregatedUpstream) { + _ * getApi(_) >> Mono.just(api) + _ * getHead() >> head + _ * castApi(_) >> { return upstream } + } + Upstreams upstreams = Mock(Upstreams) { + _ * getUpstream(Chain.BITCOIN) >> upstream + } + TrackBitcoinAddress track = new TrackBitcoinAddress(upstreams) + + when: + def resp = track.subscribe(BlockchainOuterClass.BalanceRequest.newBuilder() + .setAsset(Common.Asset.newBuilder().setChain(Common.ChainRef.CHAIN_BITCOIN)) + .setAddress( + Common.AnyAddress.newBuilder().setAddressSingle( + Common.SingleAddress.newBuilder().setAddress("1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK") + ) + ) + .build() + ).map { + it.balance + } + + then: + StepVerifier.create(resp) + .expectNext("0") + .then { + blocks.onNext(new BlockContainer(1L, BlockId.from(hash1), BigInteger.ONE, Instant.now(), false, null, [])) + } + .expectNext("1230000") + .then { + blocks.onComplete() + } + .expectComplete() + .verify(Duration.ofSeconds(1)) + + } } From a41230a00ab60b952fd89d64abf4f554feba1409 Mon Sep 17 00:00:00 2001 From: Igor Artamonov Date: Wed, 22 Apr 2020 22:54:27 -0400 Subject: [PATCH 05/13] solution: track Bitcoin transactions --- .../emeraldpay/dshackle/rpc/BlockchainRpc.kt | 8 +- .../emeraldpay/dshackle/rpc/TrackBitcoinTx.kt | 158 +++++++++++ .../dshackle/rpc/TrackEthereumTx.kt | 17 +- .../io/emeraldpay/dshackle/rpc/TrackTx.kt | 10 + .../dshackle/upstream/AbstractHead.kt | 1 + .../io/emeraldpay/dshackle/upstream/Head.kt | 1 + .../dshackle/upstream/bitcoin/BitcoinApi.kt | 12 + .../dshackle/upstream/bitcoin/ExtractBlock.kt | 28 +- .../dshackle/rpc/TrackBitcoinTxSpec.groovy | 257 ++++++++++++++++++ .../dshackle/rpc/TrackEthereumTxSpec.groovy | 8 +- .../upstream/bitcoin/ExtractBlockSpec.groovy | 66 +++++ 11 files changed, 546 insertions(+), 20 deletions(-) create mode 100644 src/main/kotlin/io/emeraldpay/dshackle/rpc/TrackBitcoinTx.kt create mode 100644 src/main/kotlin/io/emeraldpay/dshackle/rpc/TrackTx.kt create mode 100644 src/test/groovy/io/emeraldpay/dshackle/rpc/TrackBitcoinTxSpec.groovy diff --git a/src/main/kotlin/io/emeraldpay/dshackle/rpc/BlockchainRpc.kt b/src/main/kotlin/io/emeraldpay/dshackle/rpc/BlockchainRpc.kt index 0e34507f..acc48916 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/rpc/BlockchainRpc.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/rpc/BlockchainRpc.kt @@ -31,7 +31,7 @@ import reactor.core.publisher.Mono class BlockchainRpc( @Autowired private val nativeCall: NativeCall, @Autowired private val streamHead: StreamHead, - @Autowired private val trackEthereumTx: TrackEthereumTx, + @Autowired private val trackTx: List, @Autowired private val trackAddress: List, @Autowired private val describe: Describe, @Autowired private val subscribeStatus: SubscribeStatus @@ -48,7 +48,11 @@ class BlockchainRpc( } override fun subscribeTxStatus(request: Mono): Flux { - return trackEthereumTx.add(request) + return request.flatMapMany { request -> + val chain = Chain.byId(request.chainValue) + trackTx.find { it.isSupported(chain) }?.subscribe(request) + ?: Flux.error(SilentException.UnsupportedBlockchain(chain)) + } } override fun subscribeBalance(requestMono: Mono): Flux { diff --git a/src/main/kotlin/io/emeraldpay/dshackle/rpc/TrackBitcoinTx.kt b/src/main/kotlin/io/emeraldpay/dshackle/rpc/TrackBitcoinTx.kt new file mode 100644 index 00000000..4fb191df --- /dev/null +++ b/src/main/kotlin/io/emeraldpay/dshackle/rpc/TrackBitcoinTx.kt @@ -0,0 +1,158 @@ +package io.emeraldpay.dshackle.rpc + +import com.google.protobuf.ByteString +import io.emeraldpay.api.proto.BlockchainOuterClass +import io.emeraldpay.api.proto.Common +import io.emeraldpay.dshackle.BlockchainType +import io.emeraldpay.dshackle.SilentException +import io.emeraldpay.dshackle.upstream.Selector +import io.emeraldpay.dshackle.upstream.Upstream +import io.emeraldpay.dshackle.upstream.Upstreams +import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinApi +import io.emeraldpay.dshackle.upstream.bitcoin.ExtractBlock +import io.emeraldpay.grpc.Chain +import org.slf4j.LoggerFactory +import org.springframework.beans.factory.annotation.Autowired +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.time.Instant +import kotlin.math.max +import kotlin.math.min + +@Service +class TrackBitcoinTx( + @Autowired private val upstreams: Upstreams +) : TrackTx { + + companion object { + private val log = LoggerFactory.getLogger(TrackBitcoinTx::class.java) + } + + override fun isSupported(chain: Chain): Boolean { + return BlockchainType.fromBlockchain(chain) == BlockchainType.BITCOIN && upstreams.isAvailable(chain) + } + + override fun subscribe(request: BlockchainOuterClass.TxStatusRequest): Flux { + val chain = Chain.byId(request.chainValue) + val upstream = upstreams.getUpstream(chain)?.castApi(BitcoinApi::class.java) + ?: return Flux.error(SilentException.UnsupportedBlockchain(chain)) + val txid = request.txId + val confirmations = max(min(1, request.confirmationLimit), 12) + return upstream.getApi(Selector.empty).flatMapMany { api -> + subscribe(chain, api, upstream, txid) + }.takeUntil { tx -> + tx.confirmations >= confirmations + }.map(this::asProto) + } + + fun subscribe(chain: Chain, api: BitcoinApi, upstream: Upstream, txid: String): Flux { + return loadExisting(api, txid) + .flatMapMany { status -> + if (status.mined) { + //Head almost always knows the current height, so it can continue with calculating confirmations + //without publishing an empty TxStatus first + continueWithMined(api, upstream, status) + } else { + loadMempool(api, txid) + .flatMapMany { tx -> + val next = if (tx.found) { + untilMined(upstream, tx) + } else { + untilFound(chain, api, upstream, txid) + } + //fist provide the current status, then updates + Flux.concat(Mono.just(tx), next) + } + } + } + } + + fun continueWithMined(api: BitcoinApi, upstream: Upstream, status: TxStatus): Flux { + return api.getBlock(status.blockHash!!) + .map { block -> + TxStatus(status.txid, true, ExtractBlock.getHeight(block), true, status.blockHash, ExtractBlock.getTime(block), ExtractBlock.getDifficulty(block)) + }.flatMapMany { tx -> + withConfirmations(upstream, tx) + } + } + + fun untilFound(chain: Chain, api: BitcoinApi, upstream: Upstream, txid: String): Flux { + return Flux.interval(Duration.ofSeconds(1)) + .take(Duration.ofMinutes(10)) + .flatMap { loadMempool(api, txid) } + .skipUntil { it.found } + .flatMap { subscribe(chain, api, upstream, txid) } + .doOnError { t -> + log.error("Failed to wait until found", t) + } + } + + fun untilMined(upstream: Upstream, tx: TxStatus): Mono { + return upstream.getHead().getFlux().flatMap { + upstream.getApi(Selector.empty).flatMap { api -> + loadExisting(api, tx.txid) + }.filter { it.mined } + }.single() + } + + fun withConfirmations(upstream: Upstream, tx: TxStatus): Flux { + return upstream.getHead().getFlux().map { + tx.withHead(it.height) + } + } + + fun loadExisting(api: BitcoinApi, txid: String): Mono { + val mined = api.getTx(txid) + return mined.map { + val block = it["blockhash"] as String? + TxStatus(txid, found = true, mined = block != null, blockHash = block, height = ExtractBlock.getHeight(it)) + } + } + + fun loadMempool(api: BitcoinApi, txid: String): Mono { + val mempool = api.getMempool() + return mempool.map { + if (it.contains(txid)) { + TxStatus(txid, found = true, mined = false) + } else { + TxStatus(txid, found = false, mined = false) + } + } + } + + private fun asProto(tx: TxStatus): BlockchainOuterClass.TxStatus { + val data = BlockchainOuterClass.TxStatus.newBuilder() + .setTxId(tx.txid) + .setConfirmations(tx.confirmations.toInt()) + + data.broadcasted = tx.found + val isMined = tx.mined + data.mined = isMined + if (isMined) { + data.setBlock( + Common.BlockInfo.newBuilder() + .setBlockId(tx.blockHash!!.substring(2)) + .setTimestamp(tx.blockTime!!.toEpochMilli()) + .setWeight(ByteString.copyFrom(tx.blockTotalDifficulty!!.toByteArray())) + .setHeight(tx.height!!) + ) + } + return data.build() + } + + class TxStatus( + val txid: String, + val found: Boolean = false, + val height: Long? = null, + val mined: Boolean = false, + val blockHash: String? = null, + val blockTime: Instant? = null, + val blockTotalDifficulty: BigInteger? = null, + val confirmations: Long = 0) { + + fun withHead(headHeight: Long) = TxStatus(txid, found, height, mined, blockHash, blockTime, blockTotalDifficulty, headHeight - height!! + 1) + } +} \ No newline at end of file diff --git a/src/main/kotlin/io/emeraldpay/dshackle/rpc/TrackEthereumTx.kt b/src/main/kotlin/io/emeraldpay/dshackle/rpc/TrackEthereumTx.kt index c15f0048..651b0e9c 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/rpc/TrackEthereumTx.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/rpc/TrackEthereumTx.kt @@ -58,7 +58,7 @@ import kotlin.math.min class TrackEthereumTx( @Autowired private val upstreams: Upstreams, @Autowired private val upstreamScheduler: Scheduler -) { +) : TrackTx { companion object { private val ZERO_BLOCK = BlockHash.from("0x0000000000000000000000000000000000000000000000000000000000000000") @@ -75,6 +75,10 @@ class TrackEthereumTx( val notFound = ConcurrentLinkedQueue() + override fun isSupported(chain: Chain): Boolean { + return BlockchainType.fromBlockchain(chain) == BlockchainType.ETHEREUM && upstreams.isAvailable(chain) + } + @PostConstruct fun init() { upstreams.observeChains().subscribe { chain -> @@ -165,7 +169,7 @@ class TrackEthereumTx( fun streamAllUpdates(tx: TxDetails): Flux { val current = checkForUpdate(tx) - .doOnNext (this::onFirstUpdate) + .doOnNext(this::onFirstUpdate) .map { Notification(it, asProto(it)) } val updates = Flux.from(tx.bus) @@ -175,12 +179,9 @@ class TrackEthereumTx( .map { it.proto } } - fun add(requestMono: Mono): Flux { - return requestMono.map { request -> - prepareTracking(request) - }.flatMapMany { tx -> - streamAllUpdates(tx) - } + override fun subscribe(request: BlockchainOuterClass.TxStatusRequest): Flux { + val tx = prepareTracking(request) + return streamAllUpdates(tx) } private fun verifyAll(chain: Chain) { diff --git a/src/main/kotlin/io/emeraldpay/dshackle/rpc/TrackTx.kt b/src/main/kotlin/io/emeraldpay/dshackle/rpc/TrackTx.kt new file mode 100644 index 00000000..40bcb0fc --- /dev/null +++ b/src/main/kotlin/io/emeraldpay/dshackle/rpc/TrackTx.kt @@ -0,0 +1,10 @@ +package io.emeraldpay.dshackle.rpc + +import io.emeraldpay.api.proto.BlockchainOuterClass +import io.emeraldpay.grpc.Chain +import reactor.core.publisher.Flux + +interface TrackTx { + fun isSupported(chain: Chain): Boolean + fun subscribe(request: BlockchainOuterClass.TxStatusRequest): Flux +} \ No newline at end of file diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/AbstractHead.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/AbstractHead.kt index b2eb7ec0..e1795162 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/AbstractHead.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/AbstractHead.kt @@ -49,4 +49,5 @@ abstract class AbstractHead : Head { fun getCurrent(): BlockContainer? { return head.get() } + } \ No newline at end of file diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/Head.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/Head.kt index 142d0d6b..f5546b18 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/Head.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/Head.kt @@ -17,6 +17,7 @@ package io.emeraldpay.dshackle.upstream import io.emeraldpay.dshackle.data.BlockContainer import reactor.core.publisher.Flux +import reactor.core.publisher.Mono interface Head { fun getFlux(): Flux diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinApi.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinApi.kt index beb8dcab..ae4d22f2 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinApi.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinApi.kt @@ -41,4 +41,16 @@ open class BitcoinApi( } } } + + open fun getBlock(hash: String): Mono> { + return executeAndResult(0, "getblock", listOf(hash), Map::class.java) as Mono> + } + + open fun getTx(txid: String): Mono> { + return executeAndResult(0, "getrawtransaction", listOf(txid, true), Map::class.java) as Mono> + } + + open fun getMempool(): Mono> { + return executeAndResult(0, "getrawmempool", emptyList(), List::class.java) as Mono> + } } \ No newline at end of file diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/ExtractBlock.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/ExtractBlock.kt index 25e94a01..330aa9ec 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/ExtractBlock.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/ExtractBlock.kt @@ -15,26 +15,42 @@ class ExtractBlock( companion object { private val log = LoggerFactory.getLogger(ExtractBlock::class.java) + + @JvmStatic + fun getHeight(data: Map): Long? { + val height = data["height"] as Number? ?: return null + return height.toLong() + } + + @JvmStatic + fun getTime(data: Map): Instant? { + val time = data["time"] as Number? ?: return null + return Instant.ofEpochMilli(time.toLong() * 1000) + } + + @JvmStatic + fun getDifficulty(data: Map): BigInteger? { + val chainwork = data["chainwork"] as String? ?: return null + return BigInteger(1, Hex.decodeHex(chainwork)) + } } fun extract(json: ByteArray): BlockContainer { val data = objectMapper.readValue(json, Map::class.java) as Map - val height = data["height"] as Number? ?: throw IllegalArgumentException("Block JSON has no height") - val time = data["time"] as Number? ?: throw IllegalArgumentException("Block JSON has no time") val hash = data["hash"] as String? ?: throw IllegalArgumentException("Block JSON has no hash") - val chainwork = data["chainwork"] as String? ?: throw IllegalArgumentException("Block JSON has no chainwork") val transactions = (data["tx"] as List?)?.map(TxId.Companion::from) ?: emptyList() return BlockContainer( - height.toLong(), + getHeight(data) ?: throw IllegalArgumentException("Block JSON has no height"), BlockId.from(hash), - BigInteger(1, Hex.decodeHex(chainwork)), - Instant.ofEpochMilli(time.toLong() * 1000), + getDifficulty(data) ?: throw IllegalArgumentException("Block JSON has no chainwork"), + getTime(data) ?: throw IllegalArgumentException("Block JSON has no time"), false, json, transactions ) } + } \ No newline at end of file diff --git a/src/test/groovy/io/emeraldpay/dshackle/rpc/TrackBitcoinTxSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/rpc/TrackBitcoinTxSpec.groovy new file mode 100644 index 00000000..77a21f6b --- /dev/null +++ b/src/test/groovy/io/emeraldpay/dshackle/rpc/TrackBitcoinTxSpec.groovy @@ -0,0 +1,257 @@ +package io.emeraldpay.dshackle.rpc + +import io.emeraldpay.dshackle.data.BlockContainer +import io.emeraldpay.dshackle.data.BlockId +import io.emeraldpay.dshackle.upstream.Head +import io.emeraldpay.dshackle.upstream.Upstream +import io.emeraldpay.dshackle.upstream.Upstreams +import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinApi +import io.emeraldpay.grpc.Chain +import reactor.core.publisher.Flux +import reactor.core.publisher.Mono +import reactor.test.StepVerifier +import spock.lang.Specification + +import java.time.Duration +import java.time.Instant + +class TrackBitcoinTxSpec extends Specification { + + def "loadMempool() returns not found when not found"() { + setup: + TrackBitcoinTx track = new TrackBitcoinTx(Stub(Upstreams)) + BitcoinApi api = Mock(BitcoinApi) { + 1 * getMempool() >> Mono.just([ + "69cd44d7c641db82e69824523c7ac0c5c1e5628f025474529cf5ffe64527efc9", + "d296c6d47335a7f283574b06f1d6303b30ac75631e081ab128346a549ad93350" + ]) + } + when: + def act = track.loadMempool(api, "65ce58db064bd105b14dc76a0bce0df14653cf5263d22d17e78864cf272ee367") + + then: + StepVerifier.create(act) + .expectNextMatches { + it.found == false && it.mined == false && it.blockHash == null + } + .expectComplete() + .verify(Duration.ofSeconds(1)) + } + + def "loadMempool() returns ok when found"() { + setup: + TrackBitcoinTx track = new TrackBitcoinTx(Stub(Upstreams)) + BitcoinApi api = Mock(BitcoinApi) { + 1 * getMempool() >> Mono.just([ + "69cd44d7c641db82e69824523c7ac0c5c1e5628f025474529cf5ffe64527efc9", + "d296c6d47335a7f283574b06f1d6303b30ac75631e081ab128346a549ad93350" + ]) + } + when: + def act = track.loadMempool(api, "69cd44d7c641db82e69824523c7ac0c5c1e5628f025474529cf5ffe64527efc9") + + then: + StepVerifier.create(act) + .expectNextMatches { + it.found == true && it.mined == false && it.blockHash == null + } + .expectComplete() + .verify(Duration.ofSeconds(1)) + } + + def "loadExiting() returns not found if not mined"() { + setup: + TrackBitcoinTx track = new TrackBitcoinTx(Stub(Upstreams)) + def txid = "69cd44d7c641db82e69824523c7ac0c5c1e5628f025474529cf5ffe64527efc9" + BitcoinApi api = Mock(BitcoinApi) { + 1 * getTx(txid) >> Mono.just([ + txid: txid + ]) + } + when: + def act = track.loadExisting(api, txid) + + then: + StepVerifier.create(act) + .expectNextMatches { + it.found == true && it.mined == false && it.blockHash == null + } + .expectComplete() + .verify(Duration.ofSeconds(1)) + } + + def "loadExiting() returns block if mined"() { + setup: + TrackBitcoinTx track = new TrackBitcoinTx(Stub(Upstreams)) + def txid = "69cd44d7c641db82e69824523c7ac0c5c1e5628f025474529cf5ffe64527efc9" + BitcoinApi api = Mock(BitcoinApi) { + 1 * getTx(txid) >> Mono.just([ + txid : txid, + blockhash: "0000000000000000000895d1b9d3898700e1deecc3b0e69f439aa77875e6042f", + height : 100 + ]) + } + when: + def act = track.loadExisting(api, txid) + + then: + StepVerifier.create(act) + .expectNextMatches { + it.found == true && it.mined == true && + it.blockHash == "0000000000000000000895d1b9d3898700e1deecc3b0e69f439aa77875e6042f" && + it.height == 100 + } + .expectComplete() + .verify(Duration.ofSeconds(1)) + } + + def "Goes with confirmations"() { + setup: + TrackBitcoinTx track = new TrackBitcoinTx(Stub(Upstreams)) + def txid = "69cd44d7c641db82e69824523c7ac0c5c1e5628f025474529cf5ffe64527efc9" + // start with the current block + def next = Flux.fromIterable([10, 12, 13, 14, 15]).map { h -> + new BlockContainer(h.longValue(), BlockId.from("0000000000000000000895d1b9d3898700e1deecc3b0e69f439aa77875e6042f"), BigInteger.ONE, Instant.now(), false, null, []) + } + Head head = Mock(Head) { + 1 * getFlux() >> next + } + Upstream upstream = Mock(Upstream) { + 1 * getHead() >> head + } + def status = new TrackBitcoinTx.TxStatus( + txid, true, 10, true, "0000000000000000000895d1b9d3898700e1deecc3b0e69f439aa77875e6042f", Instant.now(), BigInteger.ONE, 0 + ) + when: + def act = track.withConfirmations(upstream, status) + + then: + StepVerifier.create(act) + .expectNextMatches { it.confirmations == 1 } + .expectNextMatches { it.confirmations == 3 } + .expectNextMatches { it.confirmations == 4 } + .expectNextMatches { it.confirmations == 5 } + .expectNextMatches { it.confirmations == 6 } + .expectComplete() + .verify(Duration.ofSeconds(1)) + } + + def "Wait until mined"() { + setup: + TrackBitcoinTx track = new TrackBitcoinTx(Stub(Upstreams)) + def txid = "69cd44d7c641db82e69824523c7ac0c5c1e5628f025474529cf5ffe64527efc9" + // start with the current block + def next = Flux.fromIterable([10, 12, 13]).map { h -> + new BlockContainer(h.longValue(), BlockId.from("0000000000000000000895d1b9d3898700e1deecc3b0e69f439aa77875e6042f"), BigInteger.ONE, Instant.now(), false, null, []) + } + Head head = Mock(Head) { + 1 * getFlux() >> next + } + BitcoinApi api = Mock(BitcoinApi) { + 3 * getTx(txid) >>> [ + Mono.just([ + txid: txid + ]), + Mono.just([ + txid: txid + ]), + Mono.just([ + txid : txid, + blockhash: "0000000000000000000895d1b9d3898700e1deecc3b0e69f439aa77875e6042f", + height : 100 + ]) + ] + } + Upstream upstream = Mock(Upstream) { + 1 * getHead() >> head + _ * getApi(_) >> Mono.just(api) + } + def status = new TrackBitcoinTx.TxStatus( + txid, false, null, false, null, null, null, 0 + ) + when: + def act = track.untilMined(upstream, status) + + then: + StepVerifier.create(act) + .expectNextMatches { it.mined && it.height == 100 && it.blockHash == "0000000000000000000895d1b9d3898700e1deecc3b0e69f439aa77875e6042f" } + .expectComplete() + .verify(Duration.ofSeconds(1)) + } + + def "Check mempool until found"() { + setup: + TrackBitcoinTx track = new TrackBitcoinTx(Stub(Upstreams)) + def txid = "69cd44d7c641db82e69824523c7ac0c5c1e5628f025474529cf5ffe64527efc9" + BitcoinApi api = Mock(BitcoinApi) { + 4 * getMempool() >>> [ + Mono.just([]), + Mono.just(["4523c7ac0c5c1e5628f025474529c69cd44d7c641db82e6982f5ffe64527efc9"]), + Mono.just(["4523c7ac0c5c1e5628f025474529c69cd44d7c641db82e6982f5ffe64527efc9", txid]), + Mono.just(["4523c7ac0c5c1e5628f025474529c69cd44d7c641db82e6982f5ffe64527efc9", txid]) //second call when started over + ] + 1 * getTx(txid) >> Mono.just([ + txid: txid + ]) + } + Head head = Mock(Head) { + _ * getFlux() >> Flux.empty() + } + Upstream upstream = Mock(Upstream) { + _ * getApi(_) >> Mono.just(api) + _ * getHead() >> head + } + + when: + def steps = StepVerifier.withVirtualTime { + track.untilFound(Chain.BITCOIN, api, upstream, txid).take(1) + } + + then: + steps + .expectSubscription() + .expectNoEvent(Duration.ofSeconds(3)) + .expectNextMatches { it.found && !it.mined } + .expectComplete() + .verify(Duration.ofSeconds(1)) + } + + def "Subscribe to an existing tx"() { + setup: + TrackBitcoinTx track = new TrackBitcoinTx(Stub(Upstreams)) + def txid = "69cd44d7c641db82e69824523c7ac0c5c1e5628f025474529cf5ffe64527efc9" + BitcoinApi api = Mock(BitcoinApi) { + _ * getTx(txid) >> Mono.just([ + txid : txid, + blockhash: "0000000000000000000895d1b9d3898700e1deecc3b0e69f439aa77875e6042f", + height : 1 + ]) + _ * getBlock("0000000000000000000895d1b9d3898700e1deecc3b0e69f439aa77875e6042f") >> Mono.just([ + height : 1, + chainwork: "01", + time : 10000 + ]) + } + def next = Flux.fromIterable([10, 11, 12]).map { h -> + new BlockContainer(h.longValue(), BlockId.from("0000000000000000000895d1b9d3898700e1deecc3b0e69f439aa77875e6042f"), BigInteger.ONE, Instant.now(), false, null, []) + } + Head head = Mock(Head) { + _ * getFlux() >> next + } + Upstream upstream = Mock(Upstream) { + _ * getApi(_) >> Mono.just(api) + _ * getHead() >> head + } + + when: + def act = track.subscribe(Chain.BITCOIN, api, upstream, txid) + + then: + StepVerifier.create(act) + .expectNextMatches { it.found && it.mined && it.confirmations == 10 && it.blockHash == "0000000000000000000895d1b9d3898700e1deecc3b0e69f439aa77875e6042f" } + .expectNextMatches { it.found && it.mined && it.confirmations == 11 } + .expectNextMatches { it.found && it.mined && it.confirmations == 12 } + .expectComplete() + .verify(Duration.ofSeconds(1)) + } +} diff --git a/src/test/groovy/io/emeraldpay/dshackle/rpc/TrackEthereumTxSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/rpc/TrackEthereumTxSpec.groovy index c12b618c..414ce232 100644 --- a/src/test/groovy/io/emeraldpay/dshackle/rpc/TrackEthereumTxSpec.groovy +++ b/src/test/groovy/io/emeraldpay/dshackle/rpc/TrackEthereumTxSpec.groovy @@ -103,7 +103,7 @@ class TrackEthereumTxSpec extends Specification { upstreamMock.nextBlock(BlockContainer.from(blockHeadJson, TestingCommons.objectMapper())) when: - def flux = trackTx.add(Mono.just(req)) + def flux = trackTx.subscribe(req) then: StepVerifier.create(flux) .expectNext(exp1) @@ -134,7 +134,7 @@ class TrackEthereumTxSpec extends Specification { when: def act = StepVerifier.withVirtualTime { - return trackTx.add(Mono.just(req)) + return trackTx.subscribe(req) } then: act @@ -192,7 +192,7 @@ class TrackEthereumTxSpec extends Specification { when: def act = StepVerifier.withVirtualTime { - return trackTx.add(Mono.just(req)) + return trackTx.subscribe(req) } then: act @@ -301,7 +301,7 @@ class TrackEthereumTxSpec extends Specification { } when: - def flux = trackTx.add(Mono.just(req)) + def flux = trackTx.subscribe(req) then: StepVerifier.create(flux) .expectNext(exp1.build()) diff --git a/src/test/groovy/io/emeraldpay/dshackle/upstream/bitcoin/ExtractBlockSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/upstream/bitcoin/ExtractBlockSpec.groovy index 7acd5a18..2e5b0dd5 100644 --- a/src/test/groovy/io/emeraldpay/dshackle/upstream/bitcoin/ExtractBlockSpec.groovy +++ b/src/test/groovy/io/emeraldpay/dshackle/upstream/bitcoin/ExtractBlockSpec.groovy @@ -21,4 +21,70 @@ class ExtractBlockSpec extends Specification { act.transactions.size() == 1487 act.json == json } + + def "Shouldn't extract time from empty"() { + when: + def act = ExtractBlock.getTime([:]) + then: + act == null + } + + def "Shouldn't extract time from null"() { + when: + def act = ExtractBlock.getTime([time: null]) + then: + act == null + } + + def "Extract correct time"() { + expect: + ExtractBlock.getTime(block).toString() == time + + where: + time | block + "2020-04-18T00:58:46Z" | [time: 1587171526] + "1970-01-01T00:00:00Z" | [time: 0] + } + + def "Shouldn't extract height from empty"() { + when: + def act = ExtractBlock.getHeight([:]) + then: + act == null + } + + def "Shouldn't extract height from null"() { + when: + def act = ExtractBlock.getHeight([height: null]) + then: + act == null + } + + def "Should extract height"() { + when: + def act = ExtractBlock.getHeight([height: 123456]) + then: + act == 123456L + } + + def "Shouldn't extract difficulty from empty"() { + when: + def act = ExtractBlock.getDifficulty([:]) + then: + act == null + } + + def "Shouldn't extract difficulty from null"() { + when: + def act = ExtractBlock.getDifficulty([chainwork: null]) + then: + act == null + } + + def "Should extract difficulty"() { + when: + def act = ExtractBlock.getDifficulty([chainwork: "00000000000000000000000000000000000000000ea25fe034fa07aed9e338eb"]) + then: + act.toString(16) == "ea25fe034fa07aed9e338eb" + } } From 7ab21f92814e7e9091b68a4af121fdfe817c954e Mon Sep 17 00:00:00 2001 From: Igor Artamonov Date: Thu, 23 Apr 2020 23:30:32 -0400 Subject: [PATCH 06/13] problem: no quorum details for bitcoin describe --- src/main/kotlin/io/emeraldpay/dshackle/rpc/Describe.kt | 3 +++ .../io/emeraldpay/dshackle/startup/ConfiguredUpstreams.kt | 4 +++- .../emeraldpay/dshackle/upstream/bitcoin/BitcoinUpstream.kt | 2 ++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/io/emeraldpay/dshackle/rpc/Describe.kt b/src/main/kotlin/io/emeraldpay/dshackle/rpc/Describe.kt index e0f20a6b..7b0c27d3 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/rpc/Describe.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/rpc/Describe.kt @@ -19,6 +19,7 @@ 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 io.emeraldpay.dshackle.upstream.ethereum.EthereumUpstream import io.emeraldpay.dshackle.upstream.grpc.EthereumGrpcUpstream import org.springframework.beans.factory.annotation.Autowired @@ -47,6 +48,8 @@ class Describe( val nodes = QuorumForLabels() if (up is EthereumUpstream) { nodes.add(up.node) + } else if (up is BitcoinUpstream) { + nodes.add(up.node) } else if (up is EthereumGrpcUpstream) { nodes.add(up.getNodes()) } diff --git a/src/main/kotlin/io/emeraldpay/dshackle/startup/ConfiguredUpstreams.kt b/src/main/kotlin/io/emeraldpay/dshackle/startup/ConfiguredUpstreams.kt index 5d1ac9fb..02eb54bb 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/startup/ConfiguredUpstreams.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/startup/ConfiguredUpstreams.kt @@ -129,7 +129,9 @@ open class ConfiguredUpstreams( } rpcApi?.let { api -> val upstream = BitcoinUpstream(config.id - ?: "bitcoin-${seq.getAndIncrement()}", chain, api, options, objectMapper) + ?: "bitcoin-${seq.getAndIncrement()}", chain, api, + options, QuorumForLabels.QuorumItem(1, config.labels), + objectMapper) upstream.start() currentUpstreams.update(UpstreamChange(chain, upstream, UpstreamChange.ChangeType.ADDED)) diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinUpstream.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinUpstream.kt index 4f59f5df..52753baf 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinUpstream.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinUpstream.kt @@ -2,6 +2,7 @@ package io.emeraldpay.dshackle.upstream.bitcoin import com.fasterxml.jackson.databind.ObjectMapper import io.emeraldpay.dshackle.config.UpstreamsConfig +import io.emeraldpay.dshackle.startup.QuorumForLabels import io.emeraldpay.dshackle.upstream.* import io.emeraldpay.grpc.Chain import org.slf4j.LoggerFactory @@ -14,6 +15,7 @@ class BitcoinUpstream( val chain: Chain, private val api: BitcoinApi, options: UpstreamsConfig.Options, + val node: QuorumForLabels.QuorumItem, private val objectMapper: ObjectMapper ) : DefaultUpstream(id, options, DefaultBitcoinMethods()), Lifecycle { From f827f5a97ab86aa9ea185feb75a5cfe665a8b762 Mon Sep 17 00:00:00 2001 From: Igor Artamonov Date: Fri, 24 Apr 2020 00:04:10 -0400 Subject: [PATCH 07/13] problem: critical bitcoin methods are not enabled by default --- .../dshackle/upstream/bitcoin/DefaultBitcoinMethods.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/DefaultBitcoinMethods.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/DefaultBitcoinMethods.kt index 370a464f..c6e94d3b 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/DefaultBitcoinMethods.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/DefaultBitcoinMethods.kt @@ -3,7 +3,12 @@ package io.emeraldpay.dshackle.upstream.bitcoin import io.emeraldpay.dshackle.upstream.calls.DirectCallMethods class DefaultBitcoinMethods : DirectCallMethods( - listOf("getbestblockhash", "getblock", "gettransaction") + listOf( + "getbestblockhash", "getblock", "getblocknumber", "getblockcount", + "gettransaction", "getrawtransaction", "gettxout", + "getreceivedbyaddress", "listunspent", + "getmemorypool" + ) ) { From 2de5d8d8e47ea93c26fa9cb7f4eb530eb55277d2 Mon Sep 17 00:00:00 2001 From: Igor Artamonov Date: Fri, 24 Apr 2020 00:04:53 -0400 Subject: [PATCH 08/13] problem: native-call doesn't accept bitcoin calls --- .../io/emeraldpay/dshackle/rpc/NativeCall.kt | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/main/kotlin/io/emeraldpay/dshackle/rpc/NativeCall.kt b/src/main/kotlin/io/emeraldpay/dshackle/rpc/NativeCall.kt index 6cc41c17..b1cbb0f5 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/rpc/NativeCall.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/rpc/NativeCall.kt @@ -23,11 +23,8 @@ import io.emeraldpay.dshackle.SilentException import io.emeraldpay.dshackle.upstream.* import io.emeraldpay.dshackle.quorum.AlwaysQuorum import io.emeraldpay.dshackle.quorum.CallQuorum -import io.emeraldpay.dshackle.upstream.ethereum.EthereumApi import io.emeraldpay.grpc.Chain import io.infinitape.etherjar.rpc.RpcException -import io.infinitape.etherjar.rpc.json.BlockJson -import io.infinitape.etherjar.rpc.json.TransactionRefJson import org.apache.commons.lang3.StringUtils import org.slf4j.LoggerFactory import org.springframework.beans.factory.annotation.Autowired @@ -88,17 +85,18 @@ open class NativeCall( if (chain == Chain.UNSPECIFIED) { return Flux.error(CallFailure(0, SilentException.UnsupportedBlockchain(request.chain.number))) } - if (BlockchainType.fromBlockchain(chain) != BlockchainType.ETHEREUM) { + + if (!upstreams.isAvailable(chain)) { return Flux.error(CallFailure(0, SilentException.UnsupportedBlockchain(request.chain.number))) } val upstream = upstreams.getUpstream(chain) ?: return Flux.error(CallFailure(0, SilentException.UnsupportedBlockchain(chain))) - return prepareCall(request, upstream as AggregatedUpstream) + return prepareCall(request, upstream) } - fun prepareCall(request: BlockchainOuterClass.NativeCallRequest, upstream: AggregatedUpstream): Flux> { + fun prepareCall(request: BlockchainOuterClass.NativeCallRequest, upstream: AggregatedUpstream<*>): Flux> { return request.itemsList.toFlux().map { val method = it.method val params = it.payload.toStringUtf8() @@ -137,7 +135,7 @@ open class NativeCall( var failures = 0 return Flux.from(apis) .flatMap { api -> - val upstream = api.upstream!! + val upstream = ctx.upstream api.execute(ctx.id, ctx.payload.method, ctx.payload.params) // on error notify quorum, it may use error message or other details .doOnError { err -> @@ -205,7 +203,7 @@ open class NativeCall( } open class CallContext(val id: Int, - val upstream: AggregatedUpstream, + val upstream: AggregatedUpstream<*>, val matcher: Selector.Matcher, val callQuorum: CallQuorum, val payload: T) { @@ -213,7 +211,7 @@ open class NativeCall( return CallContext(id, upstream, matcher, callQuorum, payload) } - fun getApis(): ApiSource { + fun getApis(): ApiSource<*> { return upstream.getApis(matcher) } } From 63e5fb8824cfd6a6eee42bc55e3063dacc09c260 Mon Sep 17 00:00:00 2001 From: Igor Artamonov Date: Sun, 26 Apr 2020 19:58:42 -0400 Subject: [PATCH 09/13] problem: unit test failure --- .../groovy/io/emeraldpay/dshackle/rpc/NativeCallSpec.groovy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/groovy/io/emeraldpay/dshackle/rpc/NativeCallSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/rpc/NativeCallSpec.groovy index 95dc0924..cd295014 100644 --- a/src/test/groovy/io/emeraldpay/dshackle/rpc/NativeCallSpec.groovy +++ b/src/test/groovy/io/emeraldpay/dshackle/rpc/NativeCallSpec.groovy @@ -259,7 +259,7 @@ class NativeCallSpec extends Specification { .build() }) .build() - 1 * upstreams.getUpstream(Chain.TESTNET_MORDEN) >> null + 1 * upstreams.isAvailable(Chain.TESTNET_MORDEN) >> false when: def resp = nativeCall.prepareCall(req) then: From 101578aa5f819fa29e5169f3559b8e63ab84612f Mon Sep 17 00:00:00 2001 From: Igor Artamonov Date: Sun, 26 Apr 2020 20:35:19 -0400 Subject: [PATCH 10/13] solution: methods configuration for bitcoin --- .../dshackle/startup/ConfiguredUpstreams.kt | 28 ++++--- .../dshackle/upstream/CurrentUpstreams.kt | 7 +- .../dshackle/upstream/bitcoin/BitcoinApi.kt | 62 +++++++++++++++- .../upstream/bitcoin/BitcoinUpstream.kt | 7 +- .../upstream/bitcoin/DefaultBitcoinMethods.kt | 74 ++++++++++++++++--- .../upstream/bitcoin/BitcoinApiSpec.groovy | 2 +- 6 files changed, 152 insertions(+), 28 deletions(-) diff --git a/src/main/kotlin/io/emeraldpay/dshackle/startup/ConfiguredUpstreams.kt b/src/main/kotlin/io/emeraldpay/dshackle/startup/ConfiguredUpstreams.kt index 02eb54bb..4f2735b4 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/startup/ConfiguredUpstreams.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/startup/ConfiguredUpstreams.kt @@ -23,6 +23,8 @@ import io.emeraldpay.dshackle.upstream.CurrentUpstreams import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinApi import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinRpcClient import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinUpstream +import io.emeraldpay.dshackle.upstream.bitcoin.DefaultBitcoinMethods +import io.emeraldpay.dshackle.upstream.calls.CallMethods import io.emeraldpay.dshackle.upstream.calls.ManagedCallMethods import io.emeraldpay.dshackle.upstream.ethereum.DirectEthereumApi import io.emeraldpay.dshackle.upstream.ethereum.EthereumUpstream @@ -116,22 +118,33 @@ open class ConfiguredUpstreams( return defaultOptions } + private fun buildMethods(config: UpstreamsConfig.Upstream<*>, chain: Chain): CallMethods { + return if (config.methods != null) { + ManagedCallMethods(currentUpstreams.getDefaultMethods(chain), + config.methods!!.enabled.map { it.name }.toSet(), + config.methods!!.disabled.map { it.name }.toSet() + ) + } else { + currentUpstreams.getDefaultMethods(chain) + } + } + private fun buildBitcoinUpstream(config: UpstreamsConfig.Upstream, chain: Chain, options: UpstreamsConfig.Options) { val conn = config.connection!! var rpcApi: BitcoinApi? = null - + val methods = buildMethods(config, chain) conn.rpc?.let { endpoint -> val rpcClient = BitcoinRpcClient(endpoint.url.toString(), endpoint.basicAuth!!) - rpcApi = BitcoinApi(rpcClient, objectMapper) + rpcApi = BitcoinApi(rpcClient, objectMapper, methods) } rpcApi?.let { api -> val upstream = BitcoinUpstream(config.id ?: "bitcoin-${seq.getAndIncrement()}", chain, api, options, QuorumForLabels.QuorumItem(1, config.labels), - objectMapper) + objectMapper, methods) upstream.start() currentUpstreams.update(UpstreamChange(chain, upstream, UpstreamChange.ChangeType.ADDED)) @@ -145,14 +158,7 @@ open class ConfiguredUpstreams( val conn = config.connection!! var rpcApi: DirectEthereumApi? = null val urls = ArrayList() - val methods = if (config.methods != null) { - ManagedCallMethods(currentUpstreams.getDefaultMethods(chain), - config.methods!!.enabled.map { it.name }.toSet(), - config.methods!!.disabled.map { it.name }.toSet() - ) - } else { - currentUpstreams.getDefaultMethods(chain) - } + val methods = buildMethods(config, chain) conn.rpc?.let { endpoint -> val rpcClient = ReactorHttpRpcClient.newBuilder() .connectTo(endpoint.url) diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/CurrentUpstreams.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/CurrentUpstreams.kt index 666a1e73..8d0733cf 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/CurrentUpstreams.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/CurrentUpstreams.kt @@ -23,6 +23,7 @@ import io.emeraldpay.dshackle.startup.UpstreamChange import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinApi import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinChainUpstreams import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinUpstream +import io.emeraldpay.dshackle.upstream.bitcoin.DefaultBitcoinMethods import io.emeraldpay.dshackle.upstream.calls.CallMethods import io.emeraldpay.dshackle.upstream.calls.DefaultEthereumMethods import io.emeraldpay.dshackle.upstream.ethereum.EthereumApi @@ -136,7 +137,11 @@ class CurrentUpstreams( } fun setupDefaultMethods(chain: Chain): CallMethods { - val created = DefaultEthereumMethods(objectMapper, chain) + val created = when (BlockchainType.fromBlockchain(chain)) { + BlockchainType.ETHEREUM -> DefaultEthereumMethods(objectMapper, chain) + BlockchainType.BITCOIN -> DefaultBitcoinMethods(objectMapper) + else -> throw IllegalStateException("Unsupported chain: $chain") + } callTargets[chain] = created return created } diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinApi.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinApi.kt index ae4d22f2..195d7880 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinApi.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinApi.kt @@ -3,7 +3,11 @@ package io.emeraldpay.dshackle.upstream.bitcoin import com.fasterxml.jackson.databind.JavaType import com.fasterxml.jackson.databind.ObjectMapper import io.emeraldpay.dshackle.upstream.UpstreamApi +import io.emeraldpay.dshackle.upstream.calls.CallMethods +import io.grpc.Status +import io.grpc.StatusRuntimeException import io.infinitape.etherjar.rpc.RpcException +import io.infinitape.etherjar.rpc.RpcResponseError import io.infinitape.etherjar.rpc.json.FullResponseJson import io.infinitape.etherjar.rpc.json.RequestJson import io.infinitape.etherjar.rpc.json.ResponseJson @@ -12,7 +16,8 @@ import reactor.core.publisher.Mono open class BitcoinApi( val bitcoinRpcClient: BitcoinRpcClient, - val objectMapper: ObjectMapper + val objectMapper: ObjectMapper, + val targets: CallMethods ) : UpstreamApi { companion object { @@ -20,10 +25,59 @@ open class BitcoinApi( } open override fun execute(id: Int, method: String, params: List): Mono { - //TODO optimize extraction - return executeAndResult(id, method, params, Object::class.java).map { - objectMapper.writeValueAsBytes(it) + //TODO it's almost the same code as for DirectEthereumApi; refactor + val result: Mono = when { + targets.isHardcoded(method) -> Mono.just(method).map { targets.executeHardcoded(it) } + targets.isAllowed(method) -> executeAndResult(id, method, params, Object::class.java) + else -> Mono.error(RpcException(-32601, "Method not allowed or not found")) } + return processResult(id, method, result) + } + + public fun processResult(id: Int, method: String, result: Mono): Mono { + //TODO it's the same code as for DirectEthereumApi; refactor + return result + .doOnError { t -> + log.warn("Upstream error: [${t.message}] for $method") + } + .map { + val resp = ResponseJson() + resp.id = id + resp.result = it + resp + } + .switchIfEmpty( + Mono.fromCallable { + val resp = ResponseJson() + resp.id = id + resp.result = null + resp + } + ) + .map { + objectMapper.writer().writeValueAsBytes(it) + } + .onErrorResume(StatusRuntimeException::class.java) { t -> + if (t.status.code == Status.Code.CANCELLED) { + Mono.empty() + } else { + Mono.error(RpcException(RpcResponseError.CODE_UPSTREAM_CONNECTION_ERROR, "gRPC error ${t.status}")) + } + } + .onErrorMap { t -> + if (RpcException::class.java.isAssignableFrom(t.javaClass)) { + t + } else { + log.warn("Convert to RPC error. Exception ${t.javaClass}:${t.message}", t) + RpcException(-32020, "Error reading from upstream", null, t) + } + } + .onErrorResume(RpcException::class.java) { t -> + val resp = ResponseJson() + resp.id = id + resp.error = t.error + Mono.just(objectMapper.writer().writeValueAsBytes(resp)) + } } open fun executeAndResult(id: Int, method: String, params: List, resultType: Class): Mono { diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinUpstream.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinUpstream.kt index 52753baf..f801cd21 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinUpstream.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinUpstream.kt @@ -4,7 +4,9 @@ import com.fasterxml.jackson.databind.ObjectMapper import io.emeraldpay.dshackle.config.UpstreamsConfig import io.emeraldpay.dshackle.startup.QuorumForLabels import io.emeraldpay.dshackle.upstream.* +import io.emeraldpay.dshackle.upstream.calls.CallMethods import io.emeraldpay.grpc.Chain +import io.infinitape.etherjar.rpc.JacksonRpcConverter import org.slf4j.LoggerFactory import org.springframework.context.Lifecycle import reactor.core.Disposable @@ -16,8 +18,9 @@ class BitcoinUpstream( private val api: BitcoinApi, options: UpstreamsConfig.Options, val node: QuorumForLabels.QuorumItem, - private val objectMapper: ObjectMapper -) : DefaultUpstream(id, options, DefaultBitcoinMethods()), Lifecycle { + private val objectMapper: ObjectMapper, + callMethods: CallMethods +) : DefaultUpstream(id, options, callMethods), Lifecycle { companion object { private val log = LoggerFactory.getLogger(BitcoinUpstream::class.java) diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/DefaultBitcoinMethods.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/DefaultBitcoinMethods.kt index c6e94d3b..7ec44584 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/DefaultBitcoinMethods.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/DefaultBitcoinMethods.kt @@ -1,15 +1,71 @@ package io.emeraldpay.dshackle.upstream.bitcoin -import io.emeraldpay.dshackle.upstream.calls.DirectCallMethods +import com.fasterxml.jackson.databind.ObjectMapper +import io.emeraldpay.dshackle.quorum.* +import io.emeraldpay.dshackle.upstream.calls.CallMethods +import io.infinitape.etherjar.rpc.JacksonRpcConverter +import io.infinitape.etherjar.rpc.RpcException +import java.util.* -class DefaultBitcoinMethods : DirectCallMethods( - listOf( - "getbestblockhash", "getblock", "getblocknumber", "getblockcount", - "gettransaction", "getrawtransaction", "gettxout", - "getreceivedbyaddress", "listunspent", - "getmemorypool" - ) -) { +class DefaultBitcoinMethods( + private val objectMapper: ObjectMapper +) : CallMethods { + //TODO maybe Ethereum RPC parser should not be really used for Bitcoin + private val jacksonRpcConverter = JacksonRpcConverter(objectMapper) + + private val anyResponseMethods = listOf( + "getblock", + "gettransaction", "getrawtransaction", "gettxout", + "getmemorypool" + ).sorted() + + private val headVerifiedMethods = listOf( + "getbestblockhash", "getblocknumber", "getblockcount", + "listunspent", "getreceivedbyaddress" + ).sorted() + + private val hardcodedMethods = listOf( + "getconnectioncount", "getnetworkinfo" + ).sorted() + + private val broadcastMethods = listOf( + "sendrawtransaction" + ).sorted() + + private val allowedMethods = (anyResponseMethods + hardcodedMethods + headVerifiedMethods).sorted() + + override fun getQuorumFor(method: String): CallQuorum { + return when { + Collections.binarySearch(hardcodedMethods, method) >= 0 -> AlwaysQuorum() + Collections.binarySearch(anyResponseMethods, method) >= 0 -> NotLaggingQuorum(2) + Collections.binarySearch(headVerifiedMethods, method) >= 0 -> NotLaggingQuorum(0) + Collections.binarySearch(broadcastMethods, method) >= 0 -> BroadcastQuorum(jacksonRpcConverter) + else -> AlwaysQuorum() + } + } + + override fun isAllowed(method: String): Boolean { + return Collections.binarySearch(allowedMethods, method) >= 0 + } + + override fun getSupportedMethods(): Set { + return allowedMethods.toSortedSet() + } + + override fun isHardcoded(method: String): Boolean { + return Collections.binarySearch(hardcodedMethods, method) >= 0; + } + + override fun executeHardcoded(method: String): Any { + return when (method) { + "getconnectioncount" -> 42 + "getnetworkinfo" -> mapOf( + "version" to 700000, + "subversion" to "/EmeraldDshackle:v0.7/" + ) + else -> throw RpcException(-32601, "Method not found") + } + } } \ No newline at end of file diff --git a/src/test/groovy/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinApiSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinApiSpec.groovy index 78e784bc..d63a23cb 100644 --- a/src/test/groovy/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinApiSpec.groovy +++ b/src/test/groovy/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinApiSpec.groovy @@ -19,7 +19,7 @@ class BitcoinApiSpec extends Specification { mockServer = ClientAndServer.startClientAndServer(18332); api = new BitcoinApi( new BitcoinRpcClient("localhost:18332", null), - TestingCommons.objectMapper() + TestingCommons.objectMapper(), new DefaultBitcoinMethods(TestingCommons.objectMapper()) ) } From c9082b3cd0ad4d7ac9c81a5d157e81d5cdfd82d8 Mon Sep 17 00:00:00 2001 From: Igor Artamonov Date: Sun, 26 Apr 2020 20:43:46 -0400 Subject: [PATCH 11/13] problem: unit test failure --- .../emeraldpay/dshackle/cache/BlocksRedisCacheSpec.groovy | 6 +++--- .../io/emeraldpay/dshackle/cache/TxRedisCacheSpec.groovy | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/test/groovy/io/emeraldpay/dshackle/cache/BlocksRedisCacheSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/cache/BlocksRedisCacheSpec.groovy index a6de791f..cbd64196 100644 --- a/src/test/groovy/io/emeraldpay/dshackle/cache/BlocksRedisCacheSpec.groovy +++ b/src/test/groovy/io/emeraldpay/dshackle/cache/BlocksRedisCacheSpec.groovy @@ -57,13 +57,13 @@ class BlocksRedisCacheSpec extends Specification { then: dec.height == 100 - dec.hash.toHex() == hash3 + dec.hash.toHex() == hash3.substring(2) dec.difficulty.toString() == "10515" dec.timestamp == Instant.ofEpochSecond(10501050) dec.json == "test".bytes dec.transactions.size() == 2 - dec.transactions[0].toHex() == hash2 - dec.transactions[1].toHex() == hash1 + dec.transactions[0].toHex() == hash2.substring(2) + dec.transactions[1].toHex() == hash1.substring(2) dec == cont } diff --git a/src/test/groovy/io/emeraldpay/dshackle/cache/TxRedisCacheSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/cache/TxRedisCacheSpec.groovy index cadcdda8..04a028af 100644 --- a/src/test/groovy/io/emeraldpay/dshackle/cache/TxRedisCacheSpec.groovy +++ b/src/test/groovy/io/emeraldpay/dshackle/cache/TxRedisCacheSpec.groovy @@ -56,8 +56,8 @@ class TxRedisCacheSpec extends Specification { then: dec.height == 2000 - dec.hash.toHex() == hash1 - dec.blockId.toHex() == hash2 + dec.hash.toHex() == hash1.substring(2) + dec.blockId.toHex() == hash2.substring(2) dec.json == "test".bytes dec == cont } @@ -170,8 +170,8 @@ class TxRedisCacheSpec extends Specification { act1 == null act2 == null act3 != null - act3.hash.toHex() == hash3 + act3.hash.toHex() == hash3.substring(2) act4 != null - act4.hash.toHex() == hash4 + act4.hash.toHex() == hash4.substring(2) } } From bd58ba977440947b2f959488a0a9ae72d56ea8ff Mon Sep 17 00:00:00 2001 From: Igor Artamonov Date: Mon, 27 Apr 2020 00:05:02 -0400 Subject: [PATCH 12/13] problem: bitcoin mempool can be queries too often solution: introduce data access wrappers, in this case caching mempool state in memory --- .../dshackle/rpc/TrackBitcoinAddress.kt | 8 +-- .../emeraldpay/dshackle/rpc/TrackBitcoinTx.kt | 27 +++---- .../dshackle/startup/ConfiguredUpstreams.kt | 7 +- .../dshackle/upstream/CurrentUpstreams.kt | 8 +-- .../upstream/bitcoin/BitcoinChainUpstreams.kt | 4 +- .../dshackle/upstream/bitcoin/BitcoinData.kt | 33 +++++++++ .../upstream/bitcoin/BitcoinRpcHead.kt | 3 +- .../upstream/bitcoin/BitcoinUpstream.kt | 22 ++++-- .../bitcoin/BitcoinUpstreamValidator.kt | 5 +- .../upstream/bitcoin/CachingMempoolData.kt | 71 +++++++++++++++++++ .../{BitcoinApi.kt => DirectBitcoinApi.kt} | 7 +- .../rpc/TrackBitcoinAddressSpec.groovy | 4 +- .../dshackle/rpc/TrackBitcoinTxSpec.groovy | 63 ++++++++++------ .../bitcoin/BitcoinRpcHeadSpec.groovy | 2 +- ...pec.groovy => DirectBitcoinApiSpec.groovy} | 6 +- 15 files changed, 197 insertions(+), 73 deletions(-) create mode 100644 src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinData.kt create mode 100644 src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/CachingMempoolData.kt rename src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/{BitcoinApi.kt => DirectBitcoinApi.kt} (94%) rename src/test/groovy/io/emeraldpay/dshackle/upstream/bitcoin/{BitcoinApiSpec.groovy => DirectBitcoinApiSpec.groovy} (96%) diff --git a/src/main/kotlin/io/emeraldpay/dshackle/rpc/TrackBitcoinAddress.kt b/src/main/kotlin/io/emeraldpay/dshackle/rpc/TrackBitcoinAddress.kt index 3ecde39b..0ce071b1 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/rpc/TrackBitcoinAddress.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/rpc/TrackBitcoinAddress.kt @@ -6,7 +6,7 @@ import io.emeraldpay.dshackle.BlockchainType import io.emeraldpay.dshackle.SilentException import io.emeraldpay.dshackle.upstream.Selector import io.emeraldpay.dshackle.upstream.Upstreams -import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinApi +import io.emeraldpay.dshackle.upstream.bitcoin.DirectBitcoinApi import io.emeraldpay.grpc.Chain import org.slf4j.LoggerFactory import org.springframework.beans.factory.annotation.Autowired @@ -48,7 +48,7 @@ class TrackBitcoinAddress( } } - fun requestBalances(chain: Chain, api: BitcoinApi, addresses: List): Flux { + fun requestBalances(chain: Chain, api: DirectBitcoinApi, addresses: List): Flux { return api.executeAndResult(0, "listunspent", emptyList(), List::class.java) .flatMapMany { unspents -> val result = getTotal(chain, addresses, unspents) @@ -58,7 +58,7 @@ class TrackBitcoinAddress( override fun getBalance(request: BlockchainOuterClass.BalanceRequest): Flux { val chain = Chain.byId(request.asset.chainValue) - val upstream = upstreams.getUpstream(chain)?.castApi(BitcoinApi::class.java) + val upstream = upstreams.getUpstream(chain)?.castApi(DirectBitcoinApi::class.java) ?: return Flux.error(SilentException.UnsupportedBlockchain(request.asset.chainValue)) val addresses = allAddresses(request) ?: return Flux.error(SilentException("Unsupported address")) if (addresses.isEmpty()) { @@ -107,7 +107,7 @@ class TrackBitcoinAddress( override fun subscribe(request: BlockchainOuterClass.BalanceRequest): Flux { val chain = Chain.byId(request.asset.chainValue) - val upstream = upstreams.getUpstream(chain)?.castApi(BitcoinApi::class.java) + val upstream = upstreams.getUpstream(chain)?.castApi(DirectBitcoinApi::class.java) ?: return Flux.error(SilentException.UnsupportedBlockchain(request.asset.chainValue)) val addresses = allAddresses(request) ?: return Flux.error(SilentException("Unsupported address")) if (addresses.isEmpty()) { diff --git a/src/main/kotlin/io/emeraldpay/dshackle/rpc/TrackBitcoinTx.kt b/src/main/kotlin/io/emeraldpay/dshackle/rpc/TrackBitcoinTx.kt index 4fb191df..1a0411d3 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/rpc/TrackBitcoinTx.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/rpc/TrackBitcoinTx.kt @@ -6,9 +6,9 @@ import io.emeraldpay.api.proto.Common import io.emeraldpay.dshackle.BlockchainType import io.emeraldpay.dshackle.SilentException import io.emeraldpay.dshackle.upstream.Selector -import io.emeraldpay.dshackle.upstream.Upstream import io.emeraldpay.dshackle.upstream.Upstreams -import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinApi +import io.emeraldpay.dshackle.upstream.bitcoin.DirectBitcoinApi +import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinUpstream import io.emeraldpay.dshackle.upstream.bitcoin.ExtractBlock import io.emeraldpay.grpc.Chain import org.slf4j.LoggerFactory @@ -37,7 +37,7 @@ class TrackBitcoinTx( override fun subscribe(request: BlockchainOuterClass.TxStatusRequest): Flux { val chain = Chain.byId(request.chainValue) - val upstream = upstreams.getUpstream(chain)?.castApi(BitcoinApi::class.java) + val upstream = upstreams.getUpstream(chain)?.cast(BitcoinUpstream::class.java, DirectBitcoinApi::class.java) ?: return Flux.error(SilentException.UnsupportedBlockchain(chain)) val txid = request.txId val confirmations = max(min(1, request.confirmationLimit), 12) @@ -48,7 +48,7 @@ class TrackBitcoinTx( }.map(this::asProto) } - fun subscribe(chain: Chain, api: BitcoinApi, upstream: Upstream, txid: String): Flux { + fun subscribe(chain: Chain, api: DirectBitcoinApi, upstream: BitcoinUpstream, txid: String): Flux { return loadExisting(api, txid) .flatMapMany { status -> if (status.mined) { @@ -56,7 +56,7 @@ class TrackBitcoinTx( //without publishing an empty TxStatus first continueWithMined(api, upstream, status) } else { - loadMempool(api, txid) + loadMempool(upstream, txid) .flatMapMany { tx -> val next = if (tx.found) { untilMined(upstream, tx) @@ -70,7 +70,7 @@ class TrackBitcoinTx( } } - fun continueWithMined(api: BitcoinApi, upstream: Upstream, status: TxStatus): Flux { + fun continueWithMined(api: DirectBitcoinApi, upstream: BitcoinUpstream, status: TxStatus): Flux { return api.getBlock(status.blockHash!!) .map { block -> TxStatus(status.txid, true, ExtractBlock.getHeight(block), true, status.blockHash, ExtractBlock.getTime(block), ExtractBlock.getDifficulty(block)) @@ -79,10 +79,10 @@ class TrackBitcoinTx( } } - fun untilFound(chain: Chain, api: BitcoinApi, upstream: Upstream, txid: String): Flux { + fun untilFound(chain: Chain, api: DirectBitcoinApi, upstream: BitcoinUpstream, txid: String): Flux { return Flux.interval(Duration.ofSeconds(1)) .take(Duration.ofMinutes(10)) - .flatMap { loadMempool(api, txid) } + .flatMap { loadMempool(upstream, txid) } .skipUntil { it.found } .flatMap { subscribe(chain, api, upstream, txid) } .doOnError { t -> @@ -90,7 +90,7 @@ class TrackBitcoinTx( } } - fun untilMined(upstream: Upstream, tx: TxStatus): Mono { + fun untilMined(upstream: BitcoinUpstream, tx: TxStatus): Mono { return upstream.getHead().getFlux().flatMap { upstream.getApi(Selector.empty).flatMap { api -> loadExisting(api, tx.txid) @@ -98,13 +98,13 @@ class TrackBitcoinTx( }.single() } - fun withConfirmations(upstream: Upstream, tx: TxStatus): Flux { + fun withConfirmations(upstream: BitcoinUpstream, tx: TxStatus): Flux { return upstream.getHead().getFlux().map { tx.withHead(it.height) } } - fun loadExisting(api: BitcoinApi, txid: String): Mono { + fun loadExisting(api: DirectBitcoinApi, txid: String): Mono { val mined = api.getTx(txid) return mined.map { val block = it["blockhash"] as String? @@ -112,8 +112,9 @@ class TrackBitcoinTx( } } - fun loadMempool(api: BitcoinApi, txid: String): Mono { - val mempool = api.getMempool() + fun loadMempool(upstream: BitcoinUpstream, txid: String): Mono { + println("access: ${upstream.getData()}") + val mempool = upstream.getData().getMempool().get() return mempool.map { if (it.contains(txid)) { TxStatus(txid, found = true, mined = false) diff --git a/src/main/kotlin/io/emeraldpay/dshackle/startup/ConfiguredUpstreams.kt b/src/main/kotlin/io/emeraldpay/dshackle/startup/ConfiguredUpstreams.kt index 4f2735b4..2e09a20f 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/startup/ConfiguredUpstreams.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/startup/ConfiguredUpstreams.kt @@ -20,10 +20,9 @@ import io.emeraldpay.dshackle.BlockchainType import io.emeraldpay.dshackle.FileResolver import io.emeraldpay.dshackle.config.UpstreamsConfig import io.emeraldpay.dshackle.upstream.CurrentUpstreams -import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinApi +import io.emeraldpay.dshackle.upstream.bitcoin.DirectBitcoinApi import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinRpcClient import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinUpstream -import io.emeraldpay.dshackle.upstream.bitcoin.DefaultBitcoinMethods import io.emeraldpay.dshackle.upstream.calls.CallMethods import io.emeraldpay.dshackle.upstream.calls.ManagedCallMethods import io.emeraldpay.dshackle.upstream.ethereum.DirectEthereumApi @@ -134,11 +133,11 @@ open class ConfiguredUpstreams( options: UpstreamsConfig.Options) { val conn = config.connection!! - var rpcApi: BitcoinApi? = null + var rpcApi: DirectBitcoinApi? = null val methods = buildMethods(config, chain) conn.rpc?.let { endpoint -> val rpcClient = BitcoinRpcClient(endpoint.url.toString(), endpoint.basicAuth!!) - rpcApi = BitcoinApi(rpcClient, objectMapper, methods) + rpcApi = DirectBitcoinApi(rpcClient, objectMapper, methods) } rpcApi?.let { api -> val upstream = BitcoinUpstream(config.id diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/CurrentUpstreams.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/CurrentUpstreams.kt index 8d0733cf..32eaaa7a 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/CurrentUpstreams.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/CurrentUpstreams.kt @@ -20,7 +20,7 @@ import io.emeraldpay.dshackle.BlockchainType import io.emeraldpay.dshackle.cache.CachesEnabled import io.emeraldpay.dshackle.cache.CachesFactory import io.emeraldpay.dshackle.startup.UpstreamChange -import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinApi +import io.emeraldpay.dshackle.upstream.bitcoin.DirectBitcoinApi import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinChainUpstreams import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinUpstream import io.emeraldpay.dshackle.upstream.bitcoin.DefaultBitcoinMethods @@ -70,10 +70,10 @@ class CurrentUpstreams( } BlockchainType.BITCOIN -> { val up = change.upstream - .cast(BitcoinUpstream::class.java, BitcoinApi::class.java) - val current = chainMapping[chain] as ChainUpstreams? + .cast(BitcoinUpstream::class.java, DirectBitcoinApi::class.java) + val current = chainMapping[chain] as ChainUpstreams? val factory = Callable { - BitcoinChainUpstreams(chain, ArrayList(), cachesFactory.getCaches(chain), objectMapper) as ChainUpstreams + BitcoinChainUpstreams(chain, ArrayList(), cachesFactory.getCaches(chain), objectMapper) as ChainUpstreams } processUpdate(change, up, current, factory) } diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinChainUpstreams.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinChainUpstreams.kt index 3f1fed75..4f882450 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinChainUpstreams.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinChainUpstreams.kt @@ -14,7 +14,7 @@ class BitcoinChainUpstreams( val upstreams: MutableList, caches: Caches, objectMapper: ObjectMapper -) : ChainUpstreams(chain, upstreams as MutableList>, caches, objectMapper) { +) : ChainUpstreams(chain, upstreams as MutableList>, caches, objectMapper) { companion object { private val log = LoggerFactory.getLogger(BitcoinChainUpstreams::class.java) @@ -66,7 +66,7 @@ class BitcoinChainUpstreams( } override fun castApi(apiType: Class): Upstream { - if (!apiType.isAssignableFrom(BitcoinApi::class.java)) { + if (!apiType.isAssignableFrom(DirectBitcoinApi::class.java)) { throw ClassCastException("Cannot cast ${EthereumApi::class.java} to $apiType") } return this as Upstream diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinData.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinData.kt new file mode 100644 index 00000000..41be7173 --- /dev/null +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinData.kt @@ -0,0 +1,33 @@ +package io.emeraldpay.dshackle.upstream.bitcoin + +import io.emeraldpay.dshackle.upstream.Head +import org.slf4j.LoggerFactory +import org.springframework.context.Lifecycle + +open class BitcoinData( + api: DirectBitcoinApi, + head: Head +) : Lifecycle { + + companion object { + private val log = LoggerFactory.getLogger(BitcoinData::class.java) + } + + private val mempool = CachingMempoolData(api, head) + + open fun getMempool(): CachingMempoolData { + return mempool + } + + override fun isRunning(): Boolean { + return mempool.isRunning + } + + override fun start() { + mempool.start() + } + + override fun stop() { + mempool.stop() + } +} \ No newline at end of file diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinRpcHead.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinRpcHead.kt index 837adc94..a6d6f192 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinRpcHead.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinRpcHead.kt @@ -3,7 +3,6 @@ package io.emeraldpay.dshackle.upstream.bitcoin import io.emeraldpay.dshackle.Defaults import io.emeraldpay.dshackle.upstream.AbstractHead import io.emeraldpay.dshackle.upstream.Head -import io.emeraldpay.dshackle.upstream.ethereum.EthereumRpcHead import org.slf4j.LoggerFactory import org.springframework.context.Lifecycle import org.springframework.scheduling.concurrent.CustomizableThreadFactory @@ -15,7 +14,7 @@ import java.time.Duration import java.util.concurrent.Executors class BitcoinRpcHead( - private val api: BitcoinApi, + private val api: DirectBitcoinApi, private val extractBlock: ExtractBlock, private val interval: Duration = Duration.ofSeconds(15) ) : Head, AbstractHead(), Lifecycle { diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinUpstream.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinUpstream.kt index f801cd21..4c30c799 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinUpstream.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinUpstream.kt @@ -6,21 +6,20 @@ import io.emeraldpay.dshackle.startup.QuorumForLabels import io.emeraldpay.dshackle.upstream.* import io.emeraldpay.dshackle.upstream.calls.CallMethods import io.emeraldpay.grpc.Chain -import io.infinitape.etherjar.rpc.JacksonRpcConverter import org.slf4j.LoggerFactory import org.springframework.context.Lifecycle import reactor.core.Disposable import reactor.core.publisher.Mono -class BitcoinUpstream( +open class BitcoinUpstream( id: String, val chain: Chain, - private val api: BitcoinApi, + private val api: DirectBitcoinApi, options: UpstreamsConfig.Options, val node: QuorumForLabels.QuorumItem, private val objectMapper: ObjectMapper, callMethods: CallMethods -) : DefaultUpstream(id, options, callMethods), Lifecycle { +) : DefaultUpstream(id, options, callMethods), Lifecycle { companion object { private val log = LoggerFactory.getLogger(BitcoinUpstream::class.java) @@ -28,6 +27,7 @@ class BitcoinUpstream( private val head: Head = createHead() private var validatorSubscription: Disposable? = null + private val data = BitcoinData(api, head) private fun createHead(): Head { return BitcoinRpcHead( @@ -36,11 +36,15 @@ class BitcoinUpstream( ) } + open fun getData(): BitcoinData { + return data + } + override fun getHead(): Head { return head } - override fun getApi(matcher: Selector.Matcher): Mono { + override fun getApi(matcher: Selector.Matcher): Mono { return Mono.just(api) } @@ -56,8 +60,8 @@ class BitcoinUpstream( } override fun castApi(apiType: Class): Upstream { - if (!apiType.isAssignableFrom(BitcoinApi::class.java)) { - throw ClassCastException("Cannot cast ${BitcoinApi::class.java} to $apiType") + if (!apiType.isAssignableFrom(DirectBitcoinApi::class.java)) { + throw ClassCastException("Cannot cast ${DirectBitcoinApi::class.java} to $apiType") } return this as Upstream } @@ -67,6 +71,7 @@ class BitcoinUpstream( if (head is Lifecycle) { runningAny = runningAny || head.isRunning } + runningAny = runningAny || data.isRunning return runningAny } @@ -77,6 +82,8 @@ class BitcoinUpstream( head.start() } } + data.start() + validatorSubscription?.dispose() if (getOptions().disableValidation != null && getOptions().disableValidation!!) { @@ -93,6 +100,7 @@ class BitcoinUpstream( if (head is Lifecycle) { head.stop() } + data.stop() validatorSubscription?.dispose() } diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinUpstreamValidator.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinUpstreamValidator.kt index be7f3ef0..6d134a60 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinUpstreamValidator.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinUpstreamValidator.kt @@ -1,10 +1,7 @@ package io.emeraldpay.dshackle.upstream.bitcoin -import io.emeraldpay.dshackle.Defaults import io.emeraldpay.dshackle.config.UpstreamsConfig -import io.emeraldpay.dshackle.upstream.Selector import io.emeraldpay.dshackle.upstream.UpstreamAvailability -import io.infinitape.etherjar.rpc.Commands import org.slf4j.LoggerFactory import org.springframework.scheduling.concurrent.CustomizableThreadFactory import reactor.core.publisher.Flux @@ -14,7 +11,7 @@ import java.time.Duration import java.util.concurrent.Executors class BitcoinUpstreamValidator( - private val api: BitcoinApi, + private val api: DirectBitcoinApi, private val options: UpstreamsConfig.Options ) { diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/CachingMempoolData.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/CachingMempoolData.kt new file mode 100644 index 00000000..65614c92 --- /dev/null +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/CachingMempoolData.kt @@ -0,0 +1,71 @@ +package io.emeraldpay.dshackle.upstream.bitcoin + +import io.emeraldpay.dshackle.upstream.Head +import org.slf4j.LoggerFactory +import org.springframework.context.Lifecycle +import reactor.core.Disposable +import reactor.core.publisher.Mono +import java.time.Duration +import java.time.Instant +import java.util.concurrent.atomic.AtomicReference +import java.util.concurrent.locks.ReentrantLock + +open class CachingMempoolData( + private val api: DirectBitcoinApi, + private val head: Head +) : Lifecycle { + + companion object { + private val log = LoggerFactory.getLogger(CachingMempoolData::class.java) + private val TTL = Duration.ofSeconds(15) + } + + private val current = AtomicReference(Container.empty()) + private val updateLock = ReentrantLock() + private var headListener: Disposable? = null + + open fun get(): Mono> { + val value = current.get() + return if (value.since < Instant.now().minus(TTL)) { + updateLock.lock() + fetchFromUpstream() + .timeout(Duration.ofSeconds(3), Mono.empty()) + .doOnNext { + current.set(Container(Instant.now(), it)) + }.doFinally { + updateLock.unlock() + } + } else { + Mono.just(value.value) + } + } + + fun fetchFromUpstream(): Mono> { + return api.executeAndResult(0, "getrawmempool", emptyList(), List::class.java) as Mono> + } + + class Container(val since: Instant, val value: List) { + companion object { + fun empty(): Container { + return Container(Instant.MIN, emptyList()) + } + } + } + + override fun isRunning(): Boolean { + return headListener != null + } + + override fun start() { + headListener?.dispose() + headListener = head.getFlux().doOnNext { + current.set(Container.empty()) + }.subscribe() + } + + override fun stop() { + headListener?.dispose() + headListener = null + } + +} \ No newline at end of file diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinApi.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/DirectBitcoinApi.kt similarity index 94% rename from src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinApi.kt rename to src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/DirectBitcoinApi.kt index 195d7880..c128bd6c 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinApi.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/DirectBitcoinApi.kt @@ -14,14 +14,14 @@ import io.infinitape.etherjar.rpc.json.ResponseJson import org.slf4j.LoggerFactory import reactor.core.publisher.Mono -open class BitcoinApi( +open class DirectBitcoinApi( val bitcoinRpcClient: BitcoinRpcClient, val objectMapper: ObjectMapper, val targets: CallMethods ) : UpstreamApi { companion object { - private val log = LoggerFactory.getLogger(BitcoinApi::class.java) + private val log = LoggerFactory.getLogger(DirectBitcoinApi::class.java) } open override fun execute(id: Int, method: String, params: List): Mono { @@ -104,7 +104,4 @@ open class BitcoinApi( return executeAndResult(0, "getrawtransaction", listOf(txid, true), Map::class.java) as Mono> } - open fun getMempool(): Mono> { - return executeAndResult(0, "getrawmempool", emptyList(), List::class.java) as Mono> - } } \ No newline at end of file diff --git a/src/test/groovy/io/emeraldpay/dshackle/rpc/TrackBitcoinAddressSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/rpc/TrackBitcoinAddressSpec.groovy index ab17ef22..78689d28 100644 --- a/src/test/groovy/io/emeraldpay/dshackle/rpc/TrackBitcoinAddressSpec.groovy +++ b/src/test/groovy/io/emeraldpay/dshackle/rpc/TrackBitcoinAddressSpec.groovy @@ -9,7 +9,7 @@ import io.emeraldpay.dshackle.upstream.AggregatedUpstream import io.emeraldpay.dshackle.upstream.Head import io.emeraldpay.dshackle.upstream.Upstream import io.emeraldpay.dshackle.upstream.Upstreams -import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinApi +import io.emeraldpay.dshackle.upstream.bitcoin.DirectBitcoinApi import io.emeraldpay.grpc.Chain import reactor.core.publisher.Flux import reactor.core.publisher.Mono @@ -201,7 +201,7 @@ class TrackBitcoinAddressSpec extends Specification { def "Get update for a balance"() { setup: - BitcoinApi api = Mock(BitcoinApi) { + DirectBitcoinApi api = Mock(DirectBitcoinApi) { 2 * executeAndResult(0, "listunspent", [], List) >>> [ Mono.just([]), Mono.just([[address: "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", amount: 0.0123]]) ] diff --git a/src/test/groovy/io/emeraldpay/dshackle/rpc/TrackBitcoinTxSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/rpc/TrackBitcoinTxSpec.groovy index 77a21f6b..05e4eafa 100644 --- a/src/test/groovy/io/emeraldpay/dshackle/rpc/TrackBitcoinTxSpec.groovy +++ b/src/test/groovy/io/emeraldpay/dshackle/rpc/TrackBitcoinTxSpec.groovy @@ -5,7 +5,10 @@ import io.emeraldpay.dshackle.data.BlockId import io.emeraldpay.dshackle.upstream.Head import io.emeraldpay.dshackle.upstream.Upstream import io.emeraldpay.dshackle.upstream.Upstreams -import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinApi +import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinData +import io.emeraldpay.dshackle.upstream.bitcoin.DirectBitcoinApi +import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinUpstream +import io.emeraldpay.dshackle.upstream.bitcoin.CachingMempoolData import io.emeraldpay.grpc.Chain import reactor.core.publisher.Flux import reactor.core.publisher.Mono @@ -20,14 +23,20 @@ class TrackBitcoinTxSpec extends Specification { def "loadMempool() returns not found when not found"() { setup: TrackBitcoinTx track = new TrackBitcoinTx(Stub(Upstreams)) - BitcoinApi api = Mock(BitcoinApi) { - 1 * getMempool() >> Mono.just([ + + CachingMempoolData mempoolAccess = Mock(CachingMempoolData) { + 1 * get() >> Mono.just([ "69cd44d7c641db82e69824523c7ac0c5c1e5628f025474529cf5ffe64527efc9", "d296c6d47335a7f283574b06f1d6303b30ac75631e081ab128346a549ad93350" ]) } + BitcoinUpstream upstream = Mock(BitcoinUpstream) { + _ * getData() >> Mock(BitcoinData) { + _ * getMempool() >> mempoolAccess + } + } when: - def act = track.loadMempool(api, "65ce58db064bd105b14dc76a0bce0df14653cf5263d22d17e78864cf272ee367") + def act = track.loadMempool(upstream, "65ce58db064bd105b14dc76a0bce0df14653cf5263d22d17e78864cf272ee367") then: StepVerifier.create(act) @@ -41,14 +50,19 @@ class TrackBitcoinTxSpec extends Specification { def "loadMempool() returns ok when found"() { setup: TrackBitcoinTx track = new TrackBitcoinTx(Stub(Upstreams)) - BitcoinApi api = Mock(BitcoinApi) { - 1 * getMempool() >> Mono.just([ + CachingMempoolData mempoolAccess = Mock(CachingMempoolData) { + 1 * get() >> Mono.just([ "69cd44d7c641db82e69824523c7ac0c5c1e5628f025474529cf5ffe64527efc9", "d296c6d47335a7f283574b06f1d6303b30ac75631e081ab128346a549ad93350" ]) } + BitcoinUpstream upstream = Mock(BitcoinUpstream) { + _ * getData() >> Mock(BitcoinData) { + _ * getMempool() >> mempoolAccess + } + } when: - def act = track.loadMempool(api, "69cd44d7c641db82e69824523c7ac0c5c1e5628f025474529cf5ffe64527efc9") + def act = track.loadMempool(upstream, "69cd44d7c641db82e69824523c7ac0c5c1e5628f025474529cf5ffe64527efc9") then: StepVerifier.create(act) @@ -63,7 +77,7 @@ class TrackBitcoinTxSpec extends Specification { setup: TrackBitcoinTx track = new TrackBitcoinTx(Stub(Upstreams)) def txid = "69cd44d7c641db82e69824523c7ac0c5c1e5628f025474529cf5ffe64527efc9" - BitcoinApi api = Mock(BitcoinApi) { + DirectBitcoinApi api = Mock(DirectBitcoinApi) { 1 * getTx(txid) >> Mono.just([ txid: txid ]) @@ -84,7 +98,7 @@ class TrackBitcoinTxSpec extends Specification { setup: TrackBitcoinTx track = new TrackBitcoinTx(Stub(Upstreams)) def txid = "69cd44d7c641db82e69824523c7ac0c5c1e5628f025474529cf5ffe64527efc9" - BitcoinApi api = Mock(BitcoinApi) { + DirectBitcoinApi api = Mock(DirectBitcoinApi) { 1 * getTx(txid) >> Mono.just([ txid : txid, blockhash: "0000000000000000000895d1b9d3898700e1deecc3b0e69f439aa77875e6042f", @@ -116,7 +130,7 @@ class TrackBitcoinTxSpec extends Specification { Head head = Mock(Head) { 1 * getFlux() >> next } - Upstream upstream = Mock(Upstream) { + Upstream upstream = Mock(BitcoinUpstream) { 1 * getHead() >> head } def status = new TrackBitcoinTx.TxStatus( @@ -147,7 +161,7 @@ class TrackBitcoinTxSpec extends Specification { Head head = Mock(Head) { 1 * getFlux() >> next } - BitcoinApi api = Mock(BitcoinApi) { + DirectBitcoinApi api = Mock(DirectBitcoinApi) { 3 * getTx(txid) >>> [ Mono.just([ txid: txid @@ -162,7 +176,7 @@ class TrackBitcoinTxSpec extends Specification { ]) ] } - Upstream upstream = Mock(Upstream) { + Upstream upstream = Mock(BitcoinUpstream) { 1 * getHead() >> head _ * getApi(_) >> Mono.just(api) } @@ -183,13 +197,7 @@ class TrackBitcoinTxSpec extends Specification { setup: TrackBitcoinTx track = new TrackBitcoinTx(Stub(Upstreams)) def txid = "69cd44d7c641db82e69824523c7ac0c5c1e5628f025474529cf5ffe64527efc9" - BitcoinApi api = Mock(BitcoinApi) { - 4 * getMempool() >>> [ - Mono.just([]), - Mono.just(["4523c7ac0c5c1e5628f025474529c69cd44d7c641db82e6982f5ffe64527efc9"]), - Mono.just(["4523c7ac0c5c1e5628f025474529c69cd44d7c641db82e6982f5ffe64527efc9", txid]), - Mono.just(["4523c7ac0c5c1e5628f025474529c69cd44d7c641db82e6982f5ffe64527efc9", txid]) //second call when started over - ] + DirectBitcoinApi api = Mock(DirectBitcoinApi) { 1 * getTx(txid) >> Mono.just([ txid: txid ]) @@ -197,9 +205,20 @@ class TrackBitcoinTxSpec extends Specification { Head head = Mock(Head) { _ * getFlux() >> Flux.empty() } - Upstream upstream = Mock(Upstream) { + CachingMempoolData mempoolAccess = Mock(CachingMempoolData) { + 4 * get() >>> [ + Mono.just([]), + Mono.just(["4523c7ac0c5c1e5628f025474529c69cd44d7c641db82e6982f5ffe64527efc9"]), + Mono.just(["4523c7ac0c5c1e5628f025474529c69cd44d7c641db82e6982f5ffe64527efc9", txid]), + Mono.just(["4523c7ac0c5c1e5628f025474529c69cd44d7c641db82e6982f5ffe64527efc9", txid]) //second call when started over + ] + } + BitcoinUpstream upstream = Mock(BitcoinUpstream) { _ * getApi(_) >> Mono.just(api) _ * getHead() >> head + _ * getData() >> Mock(BitcoinData) { + _ * getMempool() >> mempoolAccess + } } when: @@ -220,7 +239,7 @@ class TrackBitcoinTxSpec extends Specification { setup: TrackBitcoinTx track = new TrackBitcoinTx(Stub(Upstreams)) def txid = "69cd44d7c641db82e69824523c7ac0c5c1e5628f025474529cf5ffe64527efc9" - BitcoinApi api = Mock(BitcoinApi) { + DirectBitcoinApi api = Mock(DirectBitcoinApi) { _ * getTx(txid) >> Mono.just([ txid : txid, blockhash: "0000000000000000000895d1b9d3898700e1deecc3b0e69f439aa77875e6042f", @@ -238,7 +257,7 @@ class TrackBitcoinTxSpec extends Specification { Head head = Mock(Head) { _ * getFlux() >> next } - Upstream upstream = Mock(Upstream) { + BitcoinUpstream upstream = Mock(BitcoinUpstream) { _ * getApi(_) >> Mono.just(api) _ * getHead() >> head } diff --git a/src/test/groovy/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinRpcHeadSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinRpcHeadSpec.groovy index 0aab0d19..687386dd 100644 --- a/src/test/groovy/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinRpcHeadSpec.groovy +++ b/src/test/groovy/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinRpcHeadSpec.groovy @@ -59,7 +59,7 @@ class BitcoinRpcHeadSpec extends Specification { } """ - BitcoinApi api = Mock(BitcoinApi) { + DirectBitcoinApi api = Mock(DirectBitcoinApi) { _ * executeAndResult(_, "getbestblockhash", _, String) >>> [ Mono.just(hash1), Mono.just(hash1), Mono.just(hash2) ] diff --git a/src/test/groovy/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinApiSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/upstream/bitcoin/DirectBitcoinApiSpec.groovy similarity index 96% rename from src/test/groovy/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinApiSpec.groovy rename to src/test/groovy/io/emeraldpay/dshackle/upstream/bitcoin/DirectBitcoinApiSpec.groovy index d63a23cb..146b2443 100644 --- a/src/test/groovy/io/emeraldpay/dshackle/upstream/bitcoin/BitcoinApiSpec.groovy +++ b/src/test/groovy/io/emeraldpay/dshackle/upstream/bitcoin/DirectBitcoinApiSpec.groovy @@ -10,14 +10,14 @@ import spock.lang.Specification import java.time.Duration -class BitcoinApiSpec extends Specification { +class DirectBitcoinApiSpec extends Specification { ClientAndServer mockServer - BitcoinApi api + DirectBitcoinApi api def setup() { mockServer = ClientAndServer.startClientAndServer(18332); - api = new BitcoinApi( + api = new DirectBitcoinApi( new BitcoinRpcClient("localhost:18332", null), TestingCommons.objectMapper(), new DefaultBitcoinMethods(TestingCommons.objectMapper()) ) From cfcb0bfffff58231bc271e5d723deae9b973a144 Mon Sep 17 00:00:00 2001 From: Igor Artamonov Date: Mon, 27 Apr 2020 20:56:06 -0400 Subject: [PATCH 13/13] solution: update docs with info about Bitcoin support --- README.adoc | 85 ++++++++++++++++++++++++------- docs/00-intro.adoc | 9 ++-- docs/02-quick-start.adoc | 64 ++++++++++++++++++----- docs/04-upstream-config.adoc | 40 +++++++++++++-- docs/06-methods.adoc | 32 +++++++----- docs/reference-configuration.adoc | 17 ++++++- 6 files changed, 194 insertions(+), 53 deletions(-) diff --git a/README.adoc b/README.adoc index ef14b8f2..f25ab517 100644 --- a/README.adoc +++ b/README.adoc @@ -9,10 +9,11 @@ image:https://img.shields.io/docker/pulls/emeraldpay/dshackle?style=flat-square[ image:https://img.shields.io/github/license/emeraldpay/dshackle.svg?style=flat-square&maxAge=2592000["License",link="https://github.com/emeraldpay/dshackle/blob/master/LICENSE"] image:https://badges.gitter.im/emeraldpay/community.svg[link="https://gitter.im/emeraldpay/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge] -_Dshackle is an L7 Blockchain API Load Balancer._ It provided a high level aggregated API on top of several underlying upstreams, i.e., blockchain nodes or providers, such as Geth, Parity, Infura, etc. -It automatically verifies their availability and the current status of the network, executes commands making sure that the response is consistent and/or sent data successfully broadcasted to the network. +_Dshackle is an L7 Blockchain API Load Balancer._ It provides a high level aggregated API on top of several underlying upstreams. +I.e. on top of blockchain nodes such as Bitcoind, Geth, Parity, or providers like Infura, and so on. +It automatically verifies their availability and the current status of the network, executes commands making sure that the response is consistent and/or data successfully broadcasted to the network. -- Standard Ethereum JSON RPC API, plus advanced gRPC-based API +- Standard Bitcoin and Ethereum JSON RPC API, plus enhanced gRPC-based API - **Secure** TLS with optional client authentication - Blockchain-aware **caching** in memory and in Redis - Routing based on **data availability** (peers, height, sync status) @@ -22,6 +23,13 @@ It automatically verifies their availability and the current status of the netwo Dshackle allows to build a mesh network of interconnected Dshackle servers for building blockchain based services that needs to have fast, secure, stable and fail-proof access to blockchain APIs. +Blockchain support: + +- Ethereum, Ethereum Classic and Kovan testnet +- Bitcoin and Bitcoin Testnet + +WARNING: The project is still under development, please use with caution. + Dshackle connects to several upstreams via JSON RPC, Websockets, or gRPC protocol. The server verifies if a node ("upstream") is fully synchronized (not in initial sync mode), has enough peers, and its height is not behind other nodes. If upstream lags behind others, lost peers below required, started to resync or went down, then Dshackle temporarily excludes it from requests and returns it when the upstream problem is fixed. @@ -30,7 +38,6 @@ image::dshackle-intro.png[alt="",width=80%,align="center"] == Roadmap -- [ ] Support Bitcoin RPC - [ ] External logging - [ ] Access to ERC-20 tokens on asset level - [ ] Subscription to bitcoind notification over gRPC (instead of ZeroMQ) @@ -58,8 +65,8 @@ proxy: routes: - id: eth blockchain: ethereum - - id: kovan - blockchain: kovan + - id: btc + blockchain: bitcoin cluster: upstreams: @@ -71,12 +78,15 @@ cluster: url: "https://mainnet.infura.io/v3/${INFURA_USER}" ws: url: "wss://mainnet.infura.io/ws/v3/${INFURA_USER}" - - id: infura-kovan - chain: kovan + - id: bitcoin-main + chain: bitcoin connection: - ethereum: + bitcoin: rpc: - url: "https://kovan.infura.io/v3/${INFURA_USER}" + url: "http://localhost:8332" + basic-auth: + username: bitcoin + password: mypassword ---- Which sets the following: @@ -85,11 +95,11 @@ Which sets the following: ** TLS security is disabled (_please don't use in production!_) - JSON RPC access through 0.0.0.0:8545 ** proxy requests to Ethereum and Kovan upstreams -** request path for Ethereum Mainnet is `/eth`, for Kovan is `/kovan` -** i.e. call Mainnet by `POST http://127.0.0.0:8545/eth` with JSON RPC payload +** request path for Ethereum Mainnet is `/eth`, for bitcoin is `/btc` +** i.e. call Ethereum Mainnet by `POST http://127.0.0.0:8545/eth` with JSON RPC payload - two upstreams, one for Ethereum Mainnet and another for Kovan Testnet (both upstreams are configured to use Infura endpoint) -- for Ethereum Mainnet it connects using JSON RPC and Websockets connections, for Kovan only JSON RPC is used -- Infura authentication config is omitted for this demo +- for Ethereum Mainnet it connects using JSON RPC and Websockets connections, +- for Bitcoin Mainet only JSON RPC is used - `${INFURA_USER}` will be provided through environment variable @@ -137,15 +147,20 @@ curl --request POST \ ==== Access using gRPC +NOTE: It's not necessary to use gRPC, as Dshackle can provide standard JSON RPC proxy, but Dshackle gRPC interface improves performance and provides additional features. + Dshackle provides a custom gRPC based API, which provides additional methods and other features such as streaming responses. Please refer to the documentation: link:docs/06-methods.adoc[gRPC Methods] .Connect and listen for new blocks on Ethereum Mainnet [source,bash] ---- -grpcurl -import-path ./proto/ -proto blockchain.proto -d "{\"type\": 100}" -plaintext 127.0.0.1:2449 io.emeraldpay.api.Blockchain/SubscribeHead +grpcurl -import-path ./proto/ -proto blockchain.proto -d "{\"type\": 100}" -plaintext 127.0.0.1:2449 emerald.Blockchain/SubscribeHead ---- +`type: 100` specifies the blockchain id, and 100 means Ethereum Mainnet. `1` is for Bitcoin Mainnet. +There we use Ethereum because it creates new blocks every 14 seconds, which works better for demo purposes, but the same request applied to Bitcoin as well. + .Output would be like ---- { @@ -166,8 +181,42 @@ grpcurl -import-path ./proto/ -proto blockchain.proto -d "{\"type\": 100}" -plai ... ---- -The output above is for a _streaming subscription_ to all new blocks on Ethereum Mainnet. -It's one of services provided by Dshackle, in additional to standard methods provided by RPC JSON of underlying nodes. +The output above is for a _streaming subscription_ to all new blocks on the Ethereum Mainnet. + +It's one of the services provided by Dshackle, in addition to standard methods provided by RPC JSON of underlying nodes. + +.You can also subscribe to balances changes of the balance on an address: +[source,bash] +---- +grpcurl -import-path ./proto/ -proto blockchain.proto -d '{"asset": {"chain": "100", "code": "ether"}, "address": {"address_single": {"address": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"}}}' -plaintext 127.0.0.1:2449 emerald.Blockchain/SubscribeBalance +---- + +.and see how balance of the contract responsible for Wrapped Ether is changing: +---- +{ + "asset": { + "chain": "CHAIN_ETHEREUM", + "code": "ETHER" + }, + "address": { + "address": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2" + }, + "balance": "2410941696896999943701015" +} +{ + "asset": { + "chain": "CHAIN_ETHEREUM", + "code": "ETHER" + }, + "address": { + "address": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2" + }, + "balance": "2410930748488073834320430" +} +... +---- + +See other enhanced methods in the link:docs/06-methods.adoc[Documentation for Enhanced Methods] == Documentation @@ -214,6 +263,8 @@ See more in the documentation for link:docs/10-client-libraries.adoc[Client Libr == Development +WARNING: The code in `master` branch is considered a development version, which may lack proper testing and should not be used in production. + === Setting up environment Dshackle is JVM based project written in Kotlin. diff --git a/docs/00-intro.adoc b/docs/00-intro.adoc index 5530ad48..501be5ac 100644 --- a/docs/00-intro.adoc +++ b/docs/00-intro.adoc @@ -3,14 +3,13 @@ Dshackle is an L7 Blockchain API Load Balancer with automatic discovery and health checking, authentication, and TLS termination. It is designed to work as an edge proxy, middle proxy, or API gateway. -Dshackle provided a high level aggregated API on top of several underlying upstreams (blockchain nodes or providers, such -as Geth, Parity, Infura, etc), it automatically verifies their availability and the current status of the network, executes -commands making sure that the response is consistent and/or data successfully broadcasted to the network. +Dshackle provides a high level aggregated API on top of several underlying upstreams. +I.e. to blockchain nodes such as Bitcoind, Geth, Parity, or providers like Infura, and so on. +It automatically verifies their availability and the current status of the network, executes commands making sure that the response is consistent and/or data successfully broadcasted to the network. Example use cases: -- Read a _transaction_ - Dshackle tries to find it on different nodes and retries until it's found or there is a - consistent answer from upstreams that the transaction doesn't exist +- Read a _transaction_ - Dshackle tries to find it on different nodes and retries until it's found or there is a consistent answer from upstreams that the transaction doesn't exist - Get _nonce_ to send a transaction - Dshackle makes sure it finds the highest value across several nodes - Send _transaction_ - Dshackle distributes it to several nodes in parallel diff --git a/docs/02-quick-start.adoc b/docs/02-quick-start.adoc index 1d55a72d..a022f1bf 100644 --- a/docs/02-quick-start.adoc +++ b/docs/02-quick-start.adoc @@ -36,15 +36,18 @@ cluster: connection: ethereum: rpc: # <6> - url: "https://mainnet.infura.io/v3/${INFURA_USER}" - ws: # <7> + url: "https://mainnet.infura.io/v3/${INFURA_USER}" <7> + ws: # <8> url: "wss://mainnet.infura.io/ws/v3/${INFURA_USER}" - - id: infura-kovan - chain: kovan # <8> + - id: bitcoin-main + chain: bitcoin # <9> connection: - ethereum: + bitcoin: rpc: - url: "https://kovan.infura.io/v3/${INFURA_USER}" # <9> + url: "http://localhost:8332" + basic-auth: # <10> + username: bitcoin + password: test ---- <1> application listen for gRPC connections on 0.0.0.0:2449 <2> with TLS security for gRPC disabled (_never use in production!_) @@ -52,12 +55,12 @@ cluster: <4> sets up 2 upstreams <5> one for Ethereum Mainnet, using <6> HTTP and -<7> Websocket conection -<8> and another for Kovan Testnet -<9> `${INFURA_USER}` value is provided through environment variable +<7> `${INFURA_USER}` value is provided through environment variable +<8> Websocket conection +<9> and another for Bitcoin <10> with Basic Auth for bitcoin node connection -Both upstreams are configured for Infura for demo purposes, but you can use other compatible endpoints. -Infura authentication is omitted for this demo + +See detailed link:reference-configuration.adoc[Configuration Reference] ==== Run as docker @@ -99,9 +102,11 @@ curl --request POST \ .Connect and listen for new blocks on Ethereum Mainnet [source,bash] ---- -grpcurl -import-path ./proto/ -proto blockchain.proto -d "{\"type\": 100}" -plaintext 127.0.0.1:2449 io.emeraldpay.api.Blockchain/SubscribeHead +grpcurl -import-path ./proto/ -proto blockchain.proto -d "{\"type\": 100}" -plaintext 127.0.0.1:2449 emerald.Blockchain/SubscribeHead ---- +`type: 100` specifies the blockchain id, and 100 means Ethereum Mainnet. + .Output would be like ---- { @@ -123,4 +128,37 @@ grpcurl -import-path ./proto/ -proto blockchain.proto -d "{\"type\": 100}" -plai ---- The output above is for a _streaming subscription_ to all new blocks on Ethereum Mainnet. -It's a method provided by Dshackle, available in additional to methods provided by RPC JSON of underlying nodes. +It's one of the services provided by Dshackle, in addition to standard methods provided by RPC JSON of underlying nodes. + +.You can also subscribe to balances changes of the balance on an address: +[source,bash] +---- +grpcurl -import-path ./proto/ -proto blockchain.proto -d '{"asset": {"chain": "100", "code": "ether"}, "address": {"address_single": {"address": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"}}}' -plaintext 127.0.0.1:2449 emerald.Blockchain/SubscribeBalance +---- + +.and see how balance of the contract responsible for Wrapped Ether is changing: +---- +{ + "asset": { + "chain": "CHAIN_ETHEREUM", + "code": "ETHER" + }, + "address": { + "address": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2" + }, + "balance": "2410941696896999943701015" +} +{ + "asset": { + "chain": "CHAIN_ETHEREUM", + "code": "ETHER" + }, + "address": { + "address": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2" + }, + "balance": "2410930748488073834320430" +} +... +---- + +See other enhanced methods in the link:06-methods.adoc[Documentation for Enhanced Methods] \ No newline at end of file diff --git a/docs/04-upstream-config.adoc b/docs/04-upstream-config.adoc index b68a387e..91ecb2e7 100644 --- a/docs/04-upstream-config.adoc +++ b/docs/04-upstream-config.adoc @@ -10,7 +10,22 @@ Supported upstream protocols: Those protocols can be configures with additional security, TLS and authentication. -=== Example +=== Notes on upstream configuration + +==== Ethereum + +- Most of Ethereum nodes support WebSocket connection, in addition to the JSON RPC. +If it's available on your node, it's suggested to configure both JSON RPC and WebSocket connection + +==== Bitcoin + +- Bitcoind needs to be configured to index/track addresses that you're going to request. +Make sure you configure it to index your addresses with `importaddress`. +If you request balance for an address that is not indexed then it returns 0 balance. +- To track all transactions you need to setup index for transactions, which is disabled by default. +Run it with `-reindex` option, or set `txindex=1` in the config. + +=== Example Configuration .upstreams.yaml [source,yaml] @@ -104,9 +119,28 @@ Dshackle currently supports - `ws` websocket connection (supposed to be used in addition to `rpc` connection) - `grpc` connects to another Dshackle instance -=== Methods +=== Bitcoin Methods -.By default an ethereum upstream supports following JSON RPC methods: +.By default an ethereum upstream allows call to the following JSON RPC methods: +- `getbestblockhash` +- `getblock` +- `getblocknumber` +- `getblockcount` +- `gettransaction` +- `getrawtransaction` +- `gettxout` +- `getreceivedbyaddress` +- `listunspent` +- `sendrawtransaction` + +.Plus following methods are answered directly by Dshackle +- `getmemorypool` +- `getconnectioncount` +- `getnetworkinfo` + +=== Ethereum Methods + +.By default an ethereum upstream allows calls to the following JSON RPC methods: - `eth_gasPrice` - `eth_call` - `eth_estimateGas` diff --git a/docs/06-methods.adoc b/docs/06-methods.adoc index b3e49187..61736092 100644 --- a/docs/06-methods.adoc +++ b/docs/06-methods.adoc @@ -1,9 +1,12 @@ -== Methods +== Enhanced Methods -IMPORTANT: Dshackle provides an unified API based on gRPC and Protobuf. +IMPORTANT: Dshackle provides an enhanced API based on gRPC and Protobuf, in addition to JSON RPC proxy -Dshackle provides an unified API based on gRPC and Protobuf. I.e. it's not standard JSON RPC but client libraries could -be generated for all major libraries, and there're official libraries for Java and Javascript. +Dshackle provides an enhanced and unified API based on HTTP2, gRPC and Protobuf. +It works in addition to JSON RPC proxy and can be used separately. +Clients can be generated for all major programming languages, and there're official libraries for Java and Javascript. + +NOTE: It's not necessary to use gRPC, as Dshackle can provide standard JSON RPC proxy, but Dshackle gRPC interface improves performance and provides additional features. === gRPC definition @@ -25,8 +28,7 @@ service Blockchain { === Wrapped JSON RPC methods -Standard JSON RPC methods provided by Ethereum nodes are available wrapped into gRPC/Protobuf with additional features. -To call standard methods you use `NativeCall` method. +To call standard JSON RPC methods provided by Ethereum/Bitcoin you use `NativeCall` wrapping method, which provides additional flexibility and configuration for the calls. .NativeCallRequest [source,proto] @@ -72,13 +74,13 @@ Where: - `payload` is JSON response for a particular call, encoded into a string (when `succeed` is true) - or `error` if request failed (`succeed` is false) -NOTE: Reply Items comes right after their execution on an upstream, therefore streaming response. It allows to build - non-blocking queries +NOTE: Reply Items comes right after their execution on an upstream, therefore streaming response. +It allows to build non-blocking queries === SubscribeHead -Subscribes to new blocks on the specified chain. Returns stream of blocks right after it was accepted (and verified by -Dshackle) by any of the upstreams. +This methods provides subscription to the new blocks on the specified chain. +Returns stream of blocks right after it was accepted (and verified by Dshackle) by any of the upstreams. .ChainHead [source,proto] @@ -104,7 +106,7 @@ Where: === SubscribeBalance or GetBalance -Subscribes to changes of the balance for a single address or a set of addresses +Subscribes to changes (`SubscribeBalance`) or get current (`GetBalance`) balance for a single address or a set of addresses. .Request [source,proto] @@ -136,8 +138,9 @@ message AddressBalance { ==== SubscribeTxStatus -Subscribes to transaction confirmations. Allows to send a transactions and then listen to all changes until it -gets enough confirmations. Changes is `NOTFOUND -> BROADCASTED <- -> MINED <- -> CONFIRMED` +Subscribes to transaction confirmations. +Allows to send a transactions and then listen to all changes until it gets enough confirmations. +Changes are `NOTFOUND -> BROADCASTED <- -> MINED <- -> CONFIRMED` .Request [source,proto] @@ -161,3 +164,6 @@ message TxStatus { } ---- +=== gRPC Client Libraries + +See link:10-client-libraries.adoc[Client Libraries] documentation. diff --git a/docs/reference-configuration.adoc b/docs/reference-configuration.adoc index b76a5d98..1e9d485f 100644 --- a/docs/reference-configuration.adoc +++ b/docs/reference-configuration.adoc @@ -89,6 +89,15 @@ cluster: basic-auth: username: 4fc258fe41a68149c199ad8f281f2015 password: 1a68f20154fc258fe4149c199ad8f281 + - id: bitcoin + chain: bitcoin + connection: + bitcoin: + rpc: + url: "http://localhost:8332" + basic-auth: + username: bitcoin + password: e984af45bb888428207c290 - id: remote connection: grpc: @@ -357,7 +366,7 @@ configuration, and may be omitted for most of the situations. | `chain` | yes -| Blockchain which is the provided by the upstream. `ethereum`, `ethereum_classic`, `kovan` +| Blockchain which is the provided by the upstream. `bitcoin`, `bitcoin-testnet`, `ethereum`, `ethereum-classic`, or `kovan` | `labels` | no @@ -369,7 +378,11 @@ configuration, and may be omitted for most of the situations. | `connection.ethereum` | yes -| Connection configuration for Ethereum JSON RPC API +| Connection configuration for Ethereum API + +| `connection.bitcoin` +| yes +| Connection configuration for Bitcoin API |===