solution: working rpc to get bitcoin address balance

This commit is contained in:
Igor Artamonov
2020-04-19 20:47:16 -04:00
parent ba996a8d2b
commit 4f0b32f705
14 changed files with 5269 additions and 67 deletions

View File

@@ -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<TrackAddress>,
@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<BlockchainOuterClass.BalanceRequest>): Flux<BlockchainOuterClass.AddressBalance> {
return trackEthereumAddress.subscribe(request)
override fun subscribeBalance(requestMono: Mono<BlockchainOuterClass.BalanceRequest>): Flux<BlockchainOuterClass.AddressBalance> {
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<BlockchainOuterClass.BalanceRequest>): Flux<BlockchainOuterClass.AddressBalance> {
return trackEthereumAddress.getBalance(request)
override fun getBalance(requestMono: Mono<BlockchainOuterClass.BalanceRequest>): Flux<BlockchainOuterClass.AddressBalance> {
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<BlockchainOuterClass.DescribeRequest>): Mono<BlockchainOuterClass.DescribeResponse> {

View File

@@ -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<BlockchainOuterClass.AddressBalance>
fun subscribe(requestMono: BlockchainOuterClass.BalanceRequest): Flux<BlockchainOuterClass.AddressBalance>
}

View File

@@ -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<BlockchainOuterClass.AddressBalance> {
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<String>, unspents: List<*>): List<AddressBalance> {
return unspents.asSequence()
.filterIsInstance<Map<String, Any>>()
.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<BlockchainOuterClass.AddressBalance> {
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)
}
}

View File

@@ -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<Chain, ConcurrentLinkedQueue<TrackedAddress>>()
@@ -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<SimpleAddress> {
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<BlockchainOuterClass.BalanceRequest>): Flux<BlockchainOuterClass.AddressBalance> {
return requestMono.flatMapMany { request ->
val chain = Chain.byId(request.asset.chainValue)
if (BlockchainType.fromBlockchain(chain) != BlockchainType.ETHEREUM) {
return@flatMapMany Flux.error<BlockchainOuterClass.AddressBalance>(SilentException.UnsupportedBlockchain(request.asset.chainValue))
}
val bus = TopicProcessor.create<BlockchainOuterClass.AddressBalance>()
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<BlockchainOuterClass.AddressBalance> {
val bus = TopicProcessor.create<BlockchainOuterClass.AddressBalance>()
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<BlockchainOuterClass.BalanceRequest>): Flux<BlockchainOuterClass.AddressBalance> {
return requestMono.flatMapMany { request ->
initializeSimple(request)
.flatMap { a -> getBalance(a).map { a.withBalance(it) } }
.map { buildResponse(it) }
}
override fun getBalance(request: BlockchainOuterClass.BalanceRequest): Flux<BlockchainOuterClass.AddressBalance> {
return initializeSimple(request)
.flatMap { a -> getBalance(a).map { a.withBalance(it) } }
.map { buildResponse(it) }
}
private fun simpleAddress(address: Common.SingleAddress, chain: Chain): SimpleAddress {

View File

@@ -33,5 +33,6 @@ interface Upstream<out T : UpstreamApi> {
fun getMethods(): CallMethods
fun getId(): String
fun <T : Upstream<TA>, TA : UpstreamApi> cast(selfType: Class<T>, upstreamType: Class<TA>): T
fun <A : UpstreamApi> castApi(apiType: Class<A>): Upstream<A>
fun <T : Upstream<TA>, TA : UpstreamApi> cast(selfType: Class<T>, apiType: Class<TA>): T
}

View File

@@ -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 <T : Upstream<TA>, TA : UpstreamApi> cast(selfType: Class<T>, upstreamType: Class<TA>): T {
override fun <A : UpstreamApi> castApi(apiType: Class<A>): Upstream<A> {
if (!apiType.isAssignableFrom(BitcoinApi::class.java)) {
throw ClassCastException("Cannot cast ${EthereumApi::class.java} to $apiType")
}
return this as Upstream<A>
}
override fun <T : Upstream<TA>, TA : UpstreamApi> cast(selfType: Class<T>, apiType: Class<TA>): 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
}
}

View File

@@ -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 <T : Upstream<TA>, TA : UpstreamApi> cast(selfType: Class<T>, upstreamType: Class<TA>): T {
override fun <T : Upstream<TA>, TA : UpstreamApi> cast(selfType: Class<T>, apiType: Class<TA>): 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 <A : UpstreamApi> castApi(apiType: Class<A>): Upstream<A> {
if (!apiType.isAssignableFrom(BitcoinApi::class.java)) {
throw ClassCastException("Cannot cast ${BitcoinApi::class.java} to $apiType")
}
return this as T
return this as Upstream<A>
}
override fun isRunning(): Boolean {
@@ -88,4 +91,5 @@ class BitcoinUpstream(
validatorSubscription?.dispose()
}
}

View File

@@ -86,14 +86,18 @@ class EthereumChainUpstreams(
}
@SuppressWarnings("unchecked")
override fun <T : Upstream<TA>, TA : UpstreamApi> cast(selfType: Class<T>, upstreamType: Class<TA>): T {
override fun <T : Upstream<TA>, TA : UpstreamApi> cast(selfType: Class<T>, apiType: Class<TA>): 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 <A : UpstreamApi> castApi(apiType: Class<A>): Upstream<A> {
if (!apiType.isAssignableFrom(EthereumApi::class.java)) {
throw ClassCastException("Cannot cast ${EthereumApi::class.java} to $apiType")
}
return this as T
return this as Upstream<A>
}
}

View File

@@ -120,14 +120,18 @@ open class EthereumUpstream(
}
@Suppress("unchecked")
override fun <T : Upstream<TA>, TA : UpstreamApi> cast(selfType: Class<T>, upstreamType: Class<TA>): T {
override fun <T : Upstream<TA>, TA : UpstreamApi> cast(selfType: Class<T>, apiType: Class<TA>): 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 <A : UpstreamApi> castApi(apiType: Class<A>): Upstream<A> {
if (!apiType.isAssignableFrom(EthereumApi::class.java)) {
throw ClassCastException("Cannot cast ${EthereumApi::class.java} to $apiType")
}
return this as T
return this as Upstream<A>
}
}

View File

@@ -216,13 +216,17 @@ open class EthereumGrpcUpstream(
}
@SuppressWarnings("unchecked")
override fun <T : Upstream<TA>, TA : UpstreamApi> cast(selfType: Class<T>, upstreamType: Class<TA>): T {
override fun <T : Upstream<TA>, TA : UpstreamApi> cast(selfType: Class<T>, apiType: Class<TA>): 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 <A : UpstreamApi> castApi(apiType: Class<A>): Upstream<A> {
if (!apiType.isAssignableFrom(EthereumApi::class.java)) {
throw ClassCastException("Cannot cast ${EthereumApi::class.java} to $apiType")
}
return this as T
return this as Upstream<A>
}
}

View File

@@ -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"
}
}
}

View File

@@ -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)

View File

@@ -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
}
]

File diff suppressed because it is too large Load Diff