solution: subscribe to bitcoin address balance

This commit is contained in:
Igor Artamonov
2020-04-20 21:43:49 -04:00
parent 322d9ac480
commit 548154609d
2 changed files with 234 additions and 44 deletions

View File

@@ -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<BlockchainOuterClass.AddressBalance> {
if (!req.hasAddress()) {
return Flux.error(SilentException("Address not provided"))
fun allAddresses(request: BlockchainOuterClass.BalanceRequest): List<String>? {
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<String>): Flux<AddressBalance> {
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<BlockchainOuterClass.AddressBalance> {
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<BlockchainOuterClass.AddressBalance> {
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<Address, BigInteger>()
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)
}

View File

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