diff --git a/build.gradle b/build.gradle index 7ec58e6e..d811fe76 100644 --- a/build.gradle +++ b/build.gradle @@ -57,7 +57,7 @@ configurations { } dependencies { - implementation "io.emeraldpay:emerald-api:0.7.1" + implementation "io.emeraldpay:emerald-api:0.8.0" implementation "io.grpc:grpc-protobuf:${grpcVersion}" implementation "io.grpc:grpc-stub:${grpcVersion}" diff --git a/src/main/kotlin/io/emeraldpay/dshackle/rpc/TrackBitcoinAddress.kt b/src/main/kotlin/io/emeraldpay/dshackle/rpc/TrackBitcoinAddress.kt index 268f5fe5..717cc081 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/rpc/TrackBitcoinAddress.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/rpc/TrackBitcoinAddress.kt @@ -64,22 +64,41 @@ class TrackBitcoinAddress( } } - fun requestBalances(chain: Chain, api: BitcoinMultistream, addresses: List): Flux { + fun requestBalances(chain: Chain, api: BitcoinMultistream, addresses: List, includeUtxo: Boolean): Flux { return Flux.fromIterable(addresses) .map { Address(chain, it) } .flatMap { address -> - api.getReader() - .listUnspent(address.bitcoinAddress) - .map { unspents -> - getTotal(address, unspents) - } - .onErrorResume { t -> - log.error("Failed to get unspent", t) - Mono.empty() - } + balanceForAddress(api, address, includeUtxo) } } + fun balanceForAddress(api: BitcoinMultistream, address: Address, includeUtxo: Boolean): Mono { + return api.getReader() + .listUnspent(address.bitcoinAddress) + .map { unspent -> + totalUnspent(address, includeUtxo, unspent) + } + .onErrorResume { t -> + log.error("Failed to get unspent", t) + Mono.empty() + } + } + + fun totalUnspent(address: Address, includeUtxo: Boolean, unspent: List): AddressBalance { + return if (unspent.isEmpty()) { + AddressBalance(address, BigInteger.ZERO) + } else { + unspent.map { + AddressBalance( + address, + BigInteger.valueOf(it.value), + if (includeUtxo) listOf(BalanceUtxo(it.txid, it.vout, it.value)) + else emptyList() + ) + }.reduce { a, b -> a.plus(b) } + } + } + override fun getBalance(request: BlockchainOuterClass.BalanceRequest): Flux { val chain = Chain.byId(request.asset.chainValue) val upstream = multistreamHolder.getUpstream(chain)?.cast(BitcoinMultistream::class.java) @@ -88,19 +107,10 @@ class TrackBitcoinAddress( if (addresses.isEmpty()) { return Flux.empty() } - return requestBalances(chain, upstream, addresses) + return requestBalances(chain, upstream, addresses, request.includeUtxo) .map(this@TrackBitcoinAddress::buildResponse) } - fun getTotal(address: Address, unspents: List): AddressBalance { - val total = if (unspents.isEmpty()) { - 0L - } else { - unspents.map { it.value }.reduce(Long::plus) - } - return AddressBalance(address, BigInteger.valueOf(total)) - } - override fun subscribe(request: BlockchainOuterClass.BalanceRequest): Flux { val chain = Chain.byId(request.asset.chainValue) @@ -110,10 +120,10 @@ class TrackBitcoinAddress( if (addresses.isEmpty()) { return Flux.empty() } - val initial = requestBalances(chain, upstream, addresses) + val initial = requestBalances(chain, upstream, addresses, request.includeUtxo) val following = upstream.getHead().getFlux() .flatMap { block -> - requestBalances(chain, upstream, addresses) + requestBalances(chain, upstream, addresses, request.includeUtxo) } val last = HashMap() val result = Flux.merge(initial, following) @@ -134,15 +144,26 @@ class TrackBitcoinAddress( .setChainValue(address.address.chain.id) .setCode("BTC")) .setAddress(Common.SingleAddress.newBuilder().setAddress(address.address.address)) + .addAllUtxo( + address.utxo.map { utxo -> + BlockchainOuterClass.Utxo.newBuilder() + .setBalance(utxo.value.toString()) + .setIndex(utxo.vout.toLong()) + .setTxId(utxo.txid) + .build() + } + ) .build() } - open class AddressBalance(val address: Address, var balance: BigInteger = BigInteger.ZERO) { + open class AddressBalance(val address: Address, var balance: BigInteger = BigInteger.ZERO, var utxo: List = emptyList()) { constructor(chain: Chain, address: String, balance: BigInteger) : this(Address(chain, address), balance) - fun plus(other: AddressBalance) = AddressBalance(address, balance + other.balance) + fun plus(other: AddressBalance) = AddressBalance(address, balance + other.balance, utxo.plus(other.utxo)) } + open class BalanceUtxo(val txid: String, val vout: Int, val value: Long) + class Address(val chain: Chain, val address: String) { val network = if (chain == Chain.BITCOIN) { MainNetParams() diff --git a/src/test/groovy/io/emeraldpay/dshackle/rpc/TrackBitcoinAddressSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/rpc/TrackBitcoinAddressSpec.groovy index 14513dd9..8fe856a9 100644 --- a/src/test/groovy/io/emeraldpay/dshackle/rpc/TrackBitcoinAddressSpec.groovy +++ b/src/test/groovy/io/emeraldpay/dshackle/rpc/TrackBitcoinAddressSpec.groovy @@ -53,27 +53,49 @@ class TrackBitcoinAddressSpec extends Specification { Chain.BITCOIN, "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK" ) when: - def total = track.getTotal(address, unspents) + def total = track.totalUnspent(address, false, unspents) then: total.balance == 100 + total.utxo.isEmpty() } def "Correct sum for few"() { setup: def unspents = [ new SimpleUnspent("f14b222e652c58d11435fa9172ddea000c6f5e20e6b715eb940fc28d1c4adeef", 0, 100L, 123L), - new SimpleUnspent("f14b222e652c58d11435fa9172ddea000c6f5e20e6b715eb940fc28d1c4adeef", 0, 123L, 123L), + new SimpleUnspent("17d1c4adf14b222e652c58d11435fa9ee2ddea000c6f5e20e6b715eb940fc28f", 0, 123L, 123L), ] TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(MultistreamHolder)) def address = new TrackBitcoinAddress.Address( Chain.BITCOIN, "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK" ) when: - def total = track.getTotal(address, unspents) + def total = track.totalUnspent(address, false, unspents) then: total.balance == 223 + total.utxo.isEmpty() + } + + def "Correct sum for few with utxo"() { + setup: + def unspents = [ + new SimpleUnspent("f14b222e652c58d11435fa9172ddea000c6f5e20e6b715eb940fc28d1c4adeef", 0, 100L, 123L), + new SimpleUnspent("17d1c4adf14b222e652c58d11435fa9ee2ddea000c6f5e20e6b715eb940fc28f", 0, 123L, 123L), + ] + TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(MultistreamHolder)) + def address = new TrackBitcoinAddress.Address( + Chain.BITCOIN, "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK" + ) + when: + def total = track.totalUnspent(address, true, unspents) + + then: + total.balance == 223 + total.utxo.size() == 2 + total.utxo[0].txid == "f14b222e652c58d11435fa9172ddea000c6f5e20e6b715eb940fc28d1c4adeef" + total.utxo[1].txid == "17d1c4adf14b222e652c58d11435fa9ee2ddea000c6f5e20e6b715eb940fc28f" } def "One address for single provided"() {