solution: support xpub addresses for bitcoin

This commit is contained in:
Igor Artamonov
2020-09-15 22:55:58 -04:00
parent 77d7ec1101
commit 96a6f22a5b
10 changed files with 764 additions and 34 deletions

View File

@@ -45,7 +45,6 @@ image::dshackle-intro.png[alt="",width=80%,align="center"]
- [ ] External logging
- [ ] Prometheus monitoring
- [ ] Subscription to bitcoind notification over gRPC (instead of ZeroMQ)
- [ ] BIP-32 Pubkey
- [ ] Lightweight sidecar node connector
- [ ] Configurable upstream roles

View File

@@ -47,25 +47,43 @@ class TrackBitcoinAddress(
&& BlockchainType.fromBlockchain(chain) == BlockchainType.BITCOIN && multistreamHolder.isAvailable(chain)
}
fun allAddresses(request: BlockchainOuterClass.BalanceRequest): List<String>? {
fun allAddresses(api: BitcoinMultistream, request: BlockchainOuterClass.BalanceRequest): Flux<String> {
if (!request.hasAddress()) {
return null
return Flux.empty()
}
return when {
request.address.hasAddressXpub() -> {
val xpubAddresses = api.getXpubAddresses()
?: return Flux.error(IllegalStateException("Xpub verification is not available"))
val addressXpub = request.address.addressXpub
if (!addressXpub.xpub.isValidUtf8) {
return Flux.error(IllegalArgumentException("Invalid xpub string"))
}
val xpub = addressXpub.xpub.toStringUtf8()
val start = Math.max(0, addressXpub.start).toInt()
val limit = Math.min(100, Math.max(1, addressXpub.limit)).toInt()
xpubAddresses.activeAddresses(xpub, start, limit)
.map { it.toString() }
.doOnError { t -> log.error("Failed to process xpub. ${t.javaClass}:${t.message}") }
}
request.address.hasAddressSingle() -> {
listOf(request.address.addressSingle.address)
Flux.just(request.address.addressSingle.address)
}
request.address.hasAddressMulti() -> {
Flux.fromIterable(
request.address.addressMulti.addressesList
.map { addr -> addr.address }
//TODO why sorted?
.sorted()
)
}
else -> null
else -> Flux.error(IllegalArgumentException("Unsupported address type"))
}
}
fun requestBalances(chain: Chain, api: BitcoinMultistream, addresses: List<String>, includeUtxo: Boolean): Flux<AddressBalance> {
return Flux.fromIterable(addresses)
fun requestBalances(chain: Chain, api: BitcoinMultistream, addresses: Flux<String>, includeUtxo: Boolean): Flux<AddressBalance> {
return addresses
.map { Address(chain, it) }
.flatMap { address ->
balanceForAddress(api, address, includeUtxo)
@@ -103,10 +121,7 @@ class TrackBitcoinAddress(
val chain = Chain.byId(request.asset.chainValue)
val upstream = multistreamHolder.getUpstream(chain)?.cast(BitcoinMultistream::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 addresses = allAddresses(upstream, request) ?: return Flux.error(SilentException("Unsupported address"))
return requestBalances(chain, upstream, addresses, request.includeUtxo)
.map(this@TrackBitcoinAddress::buildResponse)
}
@@ -116,10 +131,7 @@ class TrackBitcoinAddress(
val chain = Chain.byId(request.asset.chainValue)
val upstream = multistreamHolder.getUpstream(chain)?.cast(BitcoinMultistream::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 addresses = allAddresses(upstream, request) ?: return Flux.error(SilentException("Unsupported address"))
val initial = requestBalances(chain, upstream, addresses, request.includeUtxo)
val following = upstream.getHead().getFlux()
.flatMap { block ->
@@ -164,6 +176,7 @@ class TrackBitcoinAddress(
open class BalanceUtxo(val txid: String, val vout: Int, val value: Long)
//TODO use bitcoin class for address
class Address(val chain: Chain, val address: String) {
val network = if (chain == Chain.BITCOIN) {
MainNetParams()

View File

@@ -0,0 +1,35 @@
/**
* Copyright (c) 2020 EmeraldPay, Inc
*
* 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.bitcoin
import org.bitcoinj.core.Address
import org.slf4j.LoggerFactory
import reactor.core.publisher.Mono
open class AddressActiveCheck(
private val esploraClient: EsploraClient
) {
companion object {
private val log = LoggerFactory.getLogger(AddressActiveCheck::class.java)
}
open fun isActive(address: Address): Mono<Boolean> {
//TODO cache with bloom filter
return esploraClient.getTransactions(address).map { it.isNotEmpty() }
}
}

View File

@@ -40,6 +40,8 @@ open class BitcoinMultistream(
private var head: Head = EmptyHead()
private var esplora = upstreams.find { it.esploraClient != null }?.esploraClient
private var reader = BitcoinReader(this, head, esplora)
private var addressActiveCheck: AddressActiveCheck? = null
private var xpubAddresses: XpubAddresses? = null
override fun init() {
if (upstreams.size > 0) {
@@ -48,6 +50,10 @@ open class BitcoinMultistream(
super.init()
}
open fun getXpubAddresses(): XpubAddresses? {
return xpubAddresses
}
override fun updateHead(): Head {
head.let {
if (it is Lifecycle) {
@@ -85,6 +91,8 @@ open class BitcoinMultistream(
super.onUpstreamsUpdated()
esplora = upstreams.find { it.esploraClient != null }?.esploraClient
reader = BitcoinReader(this, this.head, esplora)
addressActiveCheck = esplora?.let { AddressActiveCheck(it) }
xpubAddresses = addressActiveCheck?.let { XpubAddresses(it) }
}
override fun setHead(head: Head) {

View File

@@ -80,17 +80,7 @@ class EsploraClient(
val response = httpClient
.get()
.uri("$url/address/$address/utxo")
val json = response
.response { header, bytes ->
if (header.status().code() != 200) {
Mono.error(EsploraException("HTTP Code: ${header.status().code()}"))
} else {
bytes.aggregate().asByteArray()
}
}
.single()
.map { bytes -> String(bytes) }
val json = parseResponse(response)
return json.map {
val parsed = Global.objectMapper.readerFor(EsploraUnspent::class.java)
@@ -99,6 +89,32 @@ class EsploraClient(
}
}
fun getTransactions(address: Address): Mono<List<Map<String, Any>>> {
val response = httpClient
.get()
.uri("$url/address/$address/txs")
val json = parseResponse(response)
return json.map {
val parsed = Global.objectMapper.readerFor(Map::class.java)
.readValues<Map<String, Any>>(it);
parsed.readAll()
}
}
private fun parseResponse(response: HttpClient.ResponseReceiver<*>): Mono<String> {
return response
.response { header, bytes ->
if (header.status().code() != 200) {
Mono.error(EsploraException("HTTP Code: ${header.status().code()} for ${header.fullPath()}"))
} else {
bytes.aggregate().asByteArray()
}
}
.single()
.map { bytes -> String(bytes) }
}
class EsploraException(msg: String) : Exception(msg)
}

View File

@@ -0,0 +1,103 @@
/**
* Copyright (c) 2020 EmeraldPay, Inc
*
* 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.bitcoin
import org.bitcoinj.core.Address
import org.bitcoinj.core.ECKey
import org.bitcoinj.core.NetworkParameters
import org.bitcoinj.crypto.ChildNumber
import org.bitcoinj.crypto.DeterministicKey
import org.bitcoinj.crypto.HDKeyDerivation
import org.bitcoinj.params.MainNetParams
import org.bitcoinj.params.TestNet3Params
import org.bitcoinj.script.Script
import org.slf4j.LoggerFactory
import reactor.core.publisher.Flux
import reactor.util.function.Tuples
import java.util.concurrent.atomic.AtomicInteger
open class XpubAddresses(
private val addressActiveCheck: AddressActiveCheck
) {
companion object {
private val log = LoggerFactory.getLogger(XpubAddresses::class.java)
private val MAINNET = MainNetParams()
private val TESTNET = TestNet3Params()
private val INACTIVE_LIMIT = 20
}
open fun allAddresses(xpub: String, start: Int, limit: Int): Flux<Address> {
// versions:
// https://electrum.readthedocs.io/en/latest/xpub_version_bytes.html
// TODO doesn't support SH keys right now. should?
val prefix = xpub.substring(0, 4)
val type: Script.ScriptType
val network: NetworkParameters
when (prefix) {
"xpub" -> {
type = Script.ScriptType.P2PKH
network = MAINNET
}
"zpub" -> {
type = Script.ScriptType.P2WPKH
network = MAINNET
}
"tpub" -> {
type = Script.ScriptType.P2PKH
network = TESTNET
}
"vpub" -> {
type = Script.ScriptType.P2WPKH
network = TESTNET
}
else -> return Flux.error(IllegalArgumentException("Unsupported type: $prefix"))
}
val key: DeterministicKey
try {
key = DeterministicKey.deserializeB58(xpub, network)
} catch (t: Throwable) {
return Flux.error(t)
}
return Flux.range(start, limit)
.map { HDKeyDerivation.deriveChildKey(key, ChildNumber(it, false)) }
.map { Address.fromKey(network, ECKey.fromPublicOnly(it.pubKeyPoint), type) }
}
open fun activeAddresses(xpub: String, start: Int, limit: Int): Flux<Address> {
val lastActive = AtomicInteger(0)
return this.allAddresses(xpub, start, limit)
.zipWith(Flux.range(0, limit))
.takeUntil {
it.t2 - lastActive.get() >= INACTIVE_LIMIT
}
.concatMap { toCheck ->
addressActiveCheck.isActive(toCheck.t1)
.doOnNext { active -> if (active) lastActive.set(toCheck.t2) }
.map { Tuples.of(toCheck.t1, it) }
}
.filter {
it.t2
}
.map {
it.t1
}
}
}

View File

@@ -16,6 +16,7 @@
package io.emeraldpay.dshackle.rpc
import com.fasterxml.jackson.databind.ObjectMapper
import com.google.protobuf.ByteString
import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.api.proto.Common
import io.emeraldpay.dshackle.Global
@@ -27,8 +28,12 @@ import io.emeraldpay.dshackle.upstream.Head
import io.emeraldpay.dshackle.upstream.MultistreamHolder
import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinMultistream
import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinReader
import io.emeraldpay.dshackle.upstream.bitcoin.XpubAddresses
import io.emeraldpay.dshackle.upstream.bitcoin.data.SimpleUnspent
import io.emeraldpay.grpc.Chain
import org.bitcoinj.core.Address
import org.bitcoinj.params.MainNetParams
import org.bitcoinj.params.TestNet3Params
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
import reactor.core.publisher.TopicProcessor
@@ -111,7 +116,7 @@ class TrackBitcoinAddressSpec extends Specification {
)
.build()
when:
def act = track.allAddresses(req)
def act = track.allAddresses(Stub(BitcoinMultistream), req).collectList().block()
then:
act == ["16rCmCmbuWDhPjWTrpQGaU3EPdZF7MTdUk"]
}
@@ -132,20 +137,58 @@ class TrackBitcoinAddressSpec extends Specification {
)
.build()
when:
def act = track.allAddresses(req)
def act = track.allAddresses(Stub(BitcoinMultistream), req).collectList().block()
then:
act == ["16rCmCmbuWDhPjWTrpQGaU3EPdZF7MTdUk", "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", "3BMqADKWoWHPASsUdHvnUL6E1jpZkMnLZz", "bc1qdthqvt6cllzej7uhdddrltdfsmnt7d0gl5ue5n"]
}
def "Null for no address provided"() {
def "Empty for no address provided"() {
setup:
TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(MultistreamHolder))
def req = BlockchainOuterClass.BalanceRequest.newBuilder()
.build()
when:
def act = track.allAddresses(req)
def act = track.allAddresses(Stub(BitcoinMultistream), req).collectList().block()
then:
act == null
act == []
}
def "Use active for xpub"() {
setup:
TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(MultistreamHolder))
def req = BlockchainOuterClass.BalanceRequest.newBuilder()
.setAddress(
Common.AnyAddress.newBuilder()
.setAddressXpub(
// seed: chimney battle code relief era plug finish video patch dream pumpkin govern destroy fresh color
Common.XpubAddress.newBuilder()
.setXpub(ByteString.copyFromUtf8("zpub6tz4F49K5B4m7r7EyBKYM9K44eGECaQ2AfrCybq1w7ALFatz9856vrXxAPSrteDA4d5sjUPW3ACNq8wB2V3ugXVJxvAPAYPAYHsVm3VAncL"))
.setLimit(25)
)
)
.build()
def xpubAddresses = Mock(XpubAddresses) {
1 * activeAddresses(
"zpub6tz4F49K5B4m7r7EyBKYM9K44eGECaQ2AfrCybq1w7ALFatz9856vrXxAPSrteDA4d5sjUPW3ACNq8wB2V3ugXVJxvAPAYPAYHsVm3VAncL",
0,
25
) >> Flux.fromIterable([
"bc1q25590fu8djhw9lvxxqz8ufjyfwup9h54u8fl6t",
"bc1q3k6e6vawd5l5syu9nlxn2xsch9afgunl8dnz94",
"bc1qu7hd6wycy686kakfps9c093szufjpwnh6rjs9s"
]).map { Address.fromString(MainNetParams.get(), it) }
}
def multistream = Mock(BitcoinMultistream) {
1 * getXpubAddresses() >> xpubAddresses
}
when:
def act = track.allAddresses(multistream, req).collectList().block()
then:
act == [
"bc1q25590fu8djhw9lvxxqz8ufjyfwup9h54u8fl6t",
"bc1q3k6e6vawd5l5syu9nlxn2xsch9afgunl8dnz94",
"bc1qu7hd6wycy686kakfps9c093szufjpwnh6rjs9s"
]
}
def "Build proto for common balance"() {

View File

@@ -18,6 +18,7 @@ package io.emeraldpay.dshackle.upstream.bitcoin
import io.emeraldpay.dshackle.upstream.bitcoin.data.EsploraUnspent
import org.bitcoinj.core.Address
import org.bitcoinj.params.MainNetParams
import org.bitcoinj.params.TestNet3Params
import org.mockserver.integration.ClientAndServer
import org.mockserver.model.HttpRequest
import org.mockserver.model.HttpResponse
@@ -80,4 +81,54 @@ class EsploraClientSpec extends Specification {
actTotal == 309841L
}
def "get txs"() {
setup:
def responseJson = this.class.getClassLoader().getResourceAsStream("bitcoin/esplora-txs-1.json").text
mockServer.when(
HttpRequest.request()
.withMethod("GET")
.withPath("/address/tb1qyatuwvkfx8thy2ntmtuea6v42vp3zefqvll8kx/txs")
).respond(
HttpResponse.response(responseJson)
)
def client = new EsploraClient(new URI("http://localhost:23001"), null, null)
when:
def act = client.getTransactions(Address.fromString(TestNet3Params.get(), "tb1qyatuwvkfx8thy2ntmtuea6v42vp3zefqvll8kx"))
then:
StepVerifier.create(act)
.expectNextMatches { list ->
def ok = list.size() == 6
if (!ok) println("invalid size")
ok = ok && list[0].txid == "a738f4bd63f785d58acd2e83e8c8c5e84e68dabacac3a1142ee47cb188103aab"
ok = ok && list[1].txid == "4a7066adb2cd8e37f1578d6991f14c12e677e8bc0556992a73a91a26d263f89a"
ok = ok && list[2].txid == "16aa2d98e37e50c4c007a815a3cb8c20026a3df467781a7e97206a730cf4ef01"
ok = ok && list[3].txid == "caaea0ca92343a2d7115c44a0f58cb6574b9349905799854e1b5a6c3b3f33587"
ok = ok && list[4].txid == "a386377a406465423275f51d6dc71a2c245acc55c356a7e851a03b508827bc1e"
ok = ok && list[5].txid == "7944cfcd3d04c58a81aaa7067616f8ac368581a847e03447d5d2917cc75b67d2"
ok
}
.expectComplete()
.verify(Duration.ofSeconds(3))
}
def "get txs when empty"() {
setup:
mockServer.when(
HttpRequest.request()
.withMethod("GET")
.withPath("/address/tb1qyatuwvkfx8thy2ntmtuea6v42vp3zefqvll8kx/txs")
).respond(
HttpResponse.response("[]")
)
def client = new EsploraClient(new URI("http://localhost:23001"), null, null)
when:
def act = client.getTransactions(Address.fromString(TestNet3Params.get(), "tb1qyatuwvkfx8thy2ntmtuea6v42vp3zefqvll8kx"))
then:
StepVerifier.create(act)
.expectNext([])
.expectComplete()
.verify(Duration.ofSeconds(3))
}
}

View File

@@ -0,0 +1,175 @@
/**
* Copyright (c) 2020 EmeraldPay, Inc
*
* 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.bitcoin
import org.bitcoinj.core.Address
import org.bitcoinj.params.MainNetParams
import reactor.core.publisher.Mono
import reactor.test.StepVerifier
import spock.lang.Specification
import java.time.Duration
class XpubAddressesSpec extends Specification {
def "list all for mainnet"() {
setup:
XpubAddresses addresses = new XpubAddresses(Stub(AddressActiveCheck))
when:
// seed sustain pyramid victory drum primary silver safe wrestle section leg caught praise spring prepare input
// account 0
def act = addresses.allAddresses(
"zpub6tsHyQGj1UmDcw9UavB31HSChH9rqMi8Qje5KPbnczzhFSZykmm3afivpVytxWubaabDih6AbGGhioiTCg5PssYXuVGiZ1vTVaMYvvQvvmz",
0,
4
).collectList().block()
then:
act.size() == 4
act[0].toString() == "bc1qd6p79j20w2zy5zagptf5ksjkdmhx4d5sykrz0e"
act[1].toString() == "bc1q54um5vzdjuwt6qvzvk2y5em72jedpktrvr24a3"
act[2].toString() == "bc1q3yl03ugy6l5k3te5x340m70hyeqjzmmvksnuc8"
act[3].toString() == "bc1qp0cgpkxl52r82ev9k2wpdzzk4m4nflwurj5yr8"
}
def "list all for testnet"() {
setup:
XpubAddresses addresses = new XpubAddresses(Stub(AddressActiveCheck))
when:
// seed sustain pyramid victory drum primary silver safe wrestle section leg caught praise spring prepare input
// account 1
def act = addresses.allAddresses(
"vpub5aEsY5bNGvjkHPjdMiz8FbdoiT81kmF3JwuJ1LRdoFb7DxNh1qAHQCmMeDnr6RG2EcCwzGohem3oESxZGa6YWLPW79ryCyMrdYj54uUzNNq",
0,
4
).collectList().block()
then:
act.size() == 4
act[0].toString() == "tb1q2phmcxl4tflgcrnm00jvrkh4a8n876vkjcv97m"
act[1].toString() == "tb1qnhn0psv0uqtmg64tm258kqu0thurjvutcc80xs"
act[2].toString() == "tb1qg62jwv77ul329pe4wcchr83zz02egwfh6syqsr"
act[3].toString() == "tb1q8yg5qknrl5wmc98lw8dyfgrzamzaq8c9l0g44d"
}
def "list starting from some"() {
setup:
XpubAddresses addresses = new XpubAddresses(Stub(AddressActiveCheck))
when:
// seed sustain pyramid victory drum primary silver safe wrestle section leg caught praise spring prepare input
// account 2
def act = addresses.allAddresses(
"zpub6tyKtxsQL16XpzxwFJSrVTxpKF4z8GL2WTqvdErC2iQBSEAzLqJNc8h6hg4EHMSPEavFDneR3JQjWjbZL7vtQrXMwN9bD4c9K9s1Z55aZfB",
10,
4
).collectList().block()
then:
act.size() == 4
act[0].toString() == "bc1q3dzm92mnqvkyadwl3x5cqdvxaqa5h9c37qdlgs"
act[1].toString() == "bc1q3kqug4cx95a02yhwn6geelftmw3zklrgmhjll8"
act[2].toString() == "bc1q6znmcw8zzjv3asdylgkmt0k0xvhzl9q5awh0sq"
act[3].toString() == "bc1q3fzlxvw803r8j44culrt7jz7q5y7zakushujmq"
}
def "list when only fist is active"() {
setup:
AddressActiveCheck check = Mock(AddressActiveCheck) {
1 * isActive(Address.fromString(MainNetParams.get(), "bc1qaexx257l7sgm62szw2ulj6n2v99t5ph9ekkul3")) >> Mono.just(true)
20 * isActive(_) >> Mono.just(false)
}
XpubAddresses addresses = new XpubAddresses(check)
// seed sustain pyramid victory drum primary silver safe wrestle section leg caught praise spring prepare input
// account 2
when:
def act = addresses.activeAddresses(
"zpub6tyKtxsQL16XpzxwFJSrVTxpKF4z8GL2WTqvdErC2iQBSEAzLqJNc8h6hg4EHMSPEavFDneR3JQjWjbZL7vtQrXMwN9bD4c9K9s1Z55aZfB",
0,
50
).map { it.toString() }
then:
StepVerifier.create(act)
.expectNext("bc1qaexx257l7sgm62szw2ulj6n2v99t5ph9ekkul3")
.expectComplete()
.verify(Duration.ofSeconds(1))
}
def "list when only 3rd is active"() {
setup:
AddressActiveCheck check = Mock(AddressActiveCheck) {
1 * isActive(Address.fromString(MainNetParams.get(), "bc1qah84zz7aavf3f5eyx5f29809y6xugqyphq0wtz")) >> Mono.just(true)
// 2 times before, 20 times after
22 * isActive(_) >> Mono.just(false)
}
XpubAddresses addresses = new XpubAddresses(check)
// seed sustain pyramid victory drum primary silver safe wrestle section leg caught praise spring prepare input
// account 2
when:
def act = addresses.activeAddresses(
"zpub6tyKtxsQL16XpzxwFJSrVTxpKF4z8GL2WTqvdErC2iQBSEAzLqJNc8h6hg4EHMSPEavFDneR3JQjWjbZL7vtQrXMwN9bD4c9K9s1Z55aZfB",
0,
50
).map { it.toString() }
then:
StepVerifier.create(act)
.expectNext("bc1qah84zz7aavf3f5eyx5f29809y6xugqyphq0wtz")
.expectComplete()
.verify(Duration.ofSeconds(1))
}
def "list when only 22 is active"() {
setup:
AddressActiveCheck check = Mock(AddressActiveCheck) {
// 2
1 * isActive(Address.fromString(MainNetParams.get(), "bc1qah84zz7aavf3f5eyx5f29809y6xugqyphq0wtz")) >> Mono.just(true)
// 11
1 * isActive(Address.fromString(MainNetParams.get(), "bc1q3kqug4cx95a02yhwn6geelftmw3zklrgmhjll8")) >> Mono.just(true)
// 22
1 * isActive(Address.fromString(MainNetParams.get(), "bc1qf7m2rrmrksj34vhgxmm04y43dlj7c5f58f8ku6")) >> Mono.just(true)
// 0..1 = 2
// + 3..10 = 8
// + 12..21 = 10
// + 20
40 * isActive(_) >> Mono.just(false)
}
XpubAddresses addresses = new XpubAddresses(check)
// seed sustain pyramid victory drum primary silver safe wrestle section leg caught praise spring prepare input
// account 2
when:
def act = addresses.activeAddresses(
"zpub6tyKtxsQL16XpzxwFJSrVTxpKF4z8GL2WTqvdErC2iQBSEAzLqJNc8h6hg4EHMSPEavFDneR3JQjWjbZL7vtQrXMwN9bD4c9K9s1Z55aZfB",
0,
100
).map { it.toString() }
then:
StepVerifier.create(act)
.expectNext("bc1qah84zz7aavf3f5eyx5f29809y6xugqyphq0wtz")
.expectNext("bc1q3kqug4cx95a02yhwn6geelftmw3zklrgmhjll8")
.expectNext("bc1qf7m2rrmrksj34vhgxmm04y43dlj7c5f58f8ku6")
.expectComplete()
.verify(Duration.ofSeconds(1))
}
}

View File

@@ -0,0 +1,287 @@
[
{
"txid": "a738f4bd63f785d58acd2e83e8c8c5e84e68dabacac3a1142ee47cb188103aab",
"version": 1,
"locktime": 0,
"vin": [
{
"txid": "16aa2d98e37e50c4c007a815a3cb8c20026a3df467781a7e97206a730cf4ef01",
"vout": 1,
"prevout": {
"scriptpubkey": "00142757c732c931d7722a6bdaf99ee9955303116520",
"scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 2757c732c931d7722a6bdaf99ee9955303116520",
"scriptpubkey_type": "v0_p2wpkh",
"scriptpubkey_address": "tb1qyatuwvkfx8thy2ntmtuea6v42vp3zefqvll8kx",
"value": 120000
},
"scriptsig": "",
"scriptsig_asm": "",
"witness": [
"304502210084f12093ae826635c6bce4915e927dd7a2285b37d0b51148c6d6177ce72e459d02201bf0b3d7cde090d2dbc4766b7a8b3d947e1bdb02dcf03ce894ff1a105278eb8901",
"03a3aa29d96671671b065b35de511c03ea5592eafb5de7a07542633af2d42f49ea"
],
"is_coinbase": false,
"sequence": 4294967293
}
],
"vout": [
{
"scriptpubkey": "00142757c732c931d7722a6bdaf99ee9955303116520",
"scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 2757c732c931d7722a6bdaf99ee9955303116520",
"scriptpubkey_type": "v0_p2wpkh",
"scriptpubkey_address": "tb1qyatuwvkfx8thy2ntmtuea6v42vp3zefqvll8kx",
"value": 119568
}
],
"size": 192,
"weight": 438,
"fee": 432,
"status": {
"confirmed": true,
"block_height": 1831865,
"block_hash": "000000000000014e643687acc0a975f9aa7319f1e9bb428e0f1d618ed6aba370",
"block_time": 1599447368
}
},
{
"txid": "4a7066adb2cd8e37f1578d6991f14c12e677e8bc0556992a73a91a26d263f89a",
"version": 1,
"locktime": 0,
"vin": [
{
"txid": "a386377a406465423275f51d6dc71a2c245acc55c356a7e851a03b508827bc1e",
"vout": 1,
"prevout": {
"scriptpubkey": "00142757c732c931d7722a6bdaf99ee9955303116520",
"scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 2757c732c931d7722a6bdaf99ee9955303116520",
"scriptpubkey_type": "v0_p2wpkh",
"scriptpubkey_address": "tb1qyatuwvkfx8thy2ntmtuea6v42vp3zefqvll8kx",
"value": 120000
},
"scriptsig": "",
"scriptsig_asm": "",
"witness": [
"3045022100c5af6d33efc9a564c0e955160e0b8def089cc6c933ddf08967eac25d4ddfc09a02205ed59dcc068251dee669ca19425c0c83208d1b1c1f5b7147f4f0b7cb2d15a87201",
"03a3aa29d96671671b065b35de511c03ea5592eafb5de7a07542633af2d42f49ea"
],
"is_coinbase": false,
"sequence": 4294967295
}
],
"vout": [
{
"scriptpubkey": "00142757c732c931d7722a6bdaf99ee9955303116520",
"scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 2757c732c931d7722a6bdaf99ee9955303116520",
"scriptpubkey_type": "v0_p2wpkh",
"scriptpubkey_address": "tb1qyatuwvkfx8thy2ntmtuea6v42vp3zefqvll8kx",
"value": 119568
}
],
"size": 192,
"weight": 438,
"fee": 432,
"status": {
"confirmed": true,
"block_height": 1831846,
"block_hash": "0000000000000104a5b1241385c2cfb728128ce692ab8bd8970a0372761f8887",
"block_time": 1599439188
}
},
{
"txid": "16aa2d98e37e50c4c007a815a3cb8c20026a3df467781a7e97206a730cf4ef01",
"version": 2,
"locktime": 1831845,
"vin": [
{
"txid": "a386377a406465423275f51d6dc71a2c245acc55c356a7e851a03b508827bc1e",
"vout": 0,
"prevout": {
"scriptpubkey": "00140fc773ca476039acb48162e26190b3b93cda6762",
"scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 0fc773ca476039acb48162e26190b3b93cda6762",
"scriptpubkey_type": "v0_p2wpkh",
"scriptpubkey_address": "tb1qplrh8jj8vqu6edypvt3xry9nhy7d5emzyj3a3h",
"value": 1619316
},
"scriptsig": "",
"scriptsig_asm": "",
"witness": [
"3044022067011a2a7b6823146ff8291eed673e2e52703e07b8b99cf78b0fbb968e5ca980022018244b0584f4851ec9d09296007898c00f05afdcc6c2d8eafb48e95923be609801",
"031117740bfada16dbe932219d5d5ddbb0a829d83ccbfe5eb7734d8646f9a63479"
],
"is_coinbase": false,
"sequence": 4294967293
}
],
"vout": [
{
"scriptpubkey": "0014a871aa972c4ef9255a610b3b849598991ba9df1c",
"scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 a871aa972c4ef9255a610b3b849598991ba9df1c",
"scriptpubkey_type": "v0_p2wpkh",
"scriptpubkey_address": "tb1q4pc649evfmuj2knppvacf9vcnyd6nhculm5lsk",
"value": 1499175
},
{
"scriptpubkey": "00142757c732c931d7722a6bdaf99ee9955303116520",
"scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 2757c732c931d7722a6bdaf99ee9955303116520",
"scriptpubkey_type": "v0_p2wpkh",
"scriptpubkey_address": "tb1qyatuwvkfx8thy2ntmtuea6v42vp3zefqvll8kx",
"value": 120000
}
],
"size": 222,
"weight": 561,
"fee": 141,
"status": {
"confirmed": true,
"block_height": 1831846,
"block_hash": "0000000000000104a5b1241385c2cfb728128ce692ab8bd8970a0372761f8887",
"block_time": 1599439188
}
},
{
"txid": "caaea0ca92343a2d7115c44a0f58cb6574b9349905799854e1b5a6c3b3f33587",
"version": 1,
"locktime": 0,
"vin": [
{
"txid": "7944cfcd3d04c58a81aaa7067616f8ac368581a847e03447d5d2917cc75b67d2",
"vout": 1,
"prevout": {
"scriptpubkey": "00142757c732c931d7722a6bdaf99ee9955303116520",
"scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 2757c732c931d7722a6bdaf99ee9955303116520",
"scriptpubkey_type": "v0_p2wpkh",
"scriptpubkey_address": "tb1qyatuwvkfx8thy2ntmtuea6v42vp3zefqvll8kx",
"value": 120000
},
"scriptsig": "",
"scriptsig_asm": "",
"witness": [
"304402200fe43b74f1326e51d5c718b5c0a9319c0241de9329e7d39e613218f66bf72f1202205918d5b29b7d6887a4f61fe3f7c330beb306e195b49e7149f5de1554f8aaa10501",
"03a3aa29d96671671b065b35de511c03ea5592eafb5de7a07542633af2d42f49ea"
],
"is_coinbase": false,
"sequence": 4294967294
}
],
"vout": [
{
"scriptpubkey": "00146d2112c30c7ef1bf66a4ff2e1bfa0304c3b8f118",
"scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 6d2112c30c7ef1bf66a4ff2e1bfa0304c3b8f118",
"scriptpubkey_type": "v0_p2wpkh",
"scriptpubkey_address": "tb1qd5s39scv0mcm7e4yluhph7srqnpm3ugcx2mc5d",
"value": 119568
}
],
"size": 191,
"weight": 437,
"fee": 432,
"status": {
"confirmed": true,
"block_height": 1831820,
"block_hash": "0000000009f11ea4cd3a3393d1cc355a63ea37a5a2cbf983e34cbb7f37590eb2",
"block_time": 1599426488
}
},
{
"txid": "a386377a406465423275f51d6dc71a2c245acc55c356a7e851a03b508827bc1e",
"version": 2,
"locktime": 1831819,
"vin": [
{
"txid": "7944cfcd3d04c58a81aaa7067616f8ac368581a847e03447d5d2917cc75b67d2",
"vout": 0,
"prevout": {
"scriptpubkey": "0014da239eb3f9727ccbfe73823e427fb7289e7c192a",
"scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 da239eb3f9727ccbfe73823e427fb7289e7c192a",
"scriptpubkey_type": "v0_p2wpkh",
"scriptpubkey_address": "tb1qmg3eavlewf7vhlnnsglyylah9z08cxf2a0g2nv",
"value": 1739457
},
"scriptsig": "",
"scriptsig_asm": "",
"witness": [
"3044022039020c3e01db872751e8c55f584aea2820b4e7c04c5a859ac785983aa2aaf612022012e8b0f6b052cecbcbe452dc0c8b6093f0520b355d206476c934ee879e892cfe01",
"023a49d61f3162fbba038809868a3c943de0549bae2f197d78ea8670452595cece"
],
"is_coinbase": false,
"sequence": 4294967293
}
],
"vout": [
{
"scriptpubkey": "00140fc773ca476039acb48162e26190b3b93cda6762",
"scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 0fc773ca476039acb48162e26190b3b93cda6762",
"scriptpubkey_type": "v0_p2wpkh",
"scriptpubkey_address": "tb1qplrh8jj8vqu6edypvt3xry9nhy7d5emzyj3a3h",
"value": 1619316
},
{
"scriptpubkey": "00142757c732c931d7722a6bdaf99ee9955303116520",
"scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 2757c732c931d7722a6bdaf99ee9955303116520",
"scriptpubkey_type": "v0_p2wpkh",
"scriptpubkey_address": "tb1qyatuwvkfx8thy2ntmtuea6v42vp3zefqvll8kx",
"value": 120000
}
],
"size": 222,
"weight": 561,
"fee": 141,
"status": {
"confirmed": true,
"block_height": 1831820,
"block_hash": "0000000009f11ea4cd3a3393d1cc355a63ea37a5a2cbf983e34cbb7f37590eb2",
"block_time": 1599426488
}
},
{
"txid": "7944cfcd3d04c58a81aaa7067616f8ac368581a847e03447d5d2917cc75b67d2",
"version": 2,
"locktime": 1831428,
"vin": [
{
"txid": "86f3a772c109695ae67fe15e30df8929d2b71a2beef86ac51df406150d991a4b",
"vout": 0,
"prevout": {
"scriptpubkey": "00141d7aa33e35e539b16b11167627e818fd436ea907",
"scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 1d7aa33e35e539b16b11167627e818fd436ea907",
"scriptpubkey_type": "v0_p2wpkh",
"scriptpubkey_address": "tb1qr4a2x034u5umz6c3zemz06qcl4pka2g8cc2qaf",
"value": 1859598
},
"scriptsig": "",
"scriptsig_asm": "",
"witness": [
"3044022020fa758e75fa4f67665b14cc50f871883a80c1c1f4f45c403e7f8c5631b8734002200ddde3f72dbe88975365777c15da85c44eaa6ba53384194b351709f015f4bf2001",
"02f0db65b4318ee91f58d1f7da51fb16c46e3a01f6f3bd07af1b8be63e525e3e45"
],
"is_coinbase": false,
"sequence": 4294967293
}
],
"vout": [
{
"scriptpubkey": "0014da239eb3f9727ccbfe73823e427fb7289e7c192a",
"scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 da239eb3f9727ccbfe73823e427fb7289e7c192a",
"scriptpubkey_type": "v0_p2wpkh",
"scriptpubkey_address": "tb1qmg3eavlewf7vhlnnsglyylah9z08cxf2a0g2nv",
"value": 1739457
},
{
"scriptpubkey": "00142757c732c931d7722a6bdaf99ee9955303116520",
"scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 2757c732c931d7722a6bdaf99ee9955303116520",
"scriptpubkey_type": "v0_p2wpkh",
"scriptpubkey_address": "tb1qyatuwvkfx8thy2ntmtuea6v42vp3zefqvll8kx",
"value": 120000
}
],
"size": 222,
"weight": 561,
"fee": 141,
"status": {
"confirmed": true,
"block_height": 1831429,
"block_hash": "00000000000000235978c8a27a20881376bb94b831d2072d80500d2350115be1",
"block_time": 1599254339
}
}
]