solution: esplora upstream connection for bitcoin
This commit is contained in:
@@ -74,6 +74,61 @@ class UpstreamsConfigReaderSpec extends Specification {
|
||||
}
|
||||
}
|
||||
|
||||
def "Parse bitcoin upstreams"() {
|
||||
setup:
|
||||
def config = this.class.getClassLoader().getResourceAsStream("upstreams-bitcoin.yaml")
|
||||
when:
|
||||
def act = reader.read(config)
|
||||
then:
|
||||
act != null
|
||||
with(act.defaultOptions) {
|
||||
size() == 1
|
||||
with(get(0)) {
|
||||
chains == ["bitcoin"]
|
||||
options.minPeers == 3
|
||||
}
|
||||
}
|
||||
act.upstreams.size() == 1
|
||||
with(act.upstreams.get(0)) {
|
||||
id == "local"
|
||||
chain == "bitcoin"
|
||||
connection instanceof UpstreamsConfig.BitcoinConnection
|
||||
with((UpstreamsConfig.BitcoinConnection) connection) {
|
||||
rpc != null
|
||||
rpc.url == new URI("http://localhost:8545")
|
||||
esplora == null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def "Parse bitcoin upstreams with esplora"() {
|
||||
setup:
|
||||
def config = this.class.getClassLoader().getResourceAsStream("upstreams-bitcoin-esplora.yaml")
|
||||
when:
|
||||
def act = reader.read(config)
|
||||
then:
|
||||
act != null
|
||||
with(act.defaultOptions) {
|
||||
size() == 1
|
||||
with(get(0)) {
|
||||
chains == ["bitcoin"]
|
||||
options.minPeers == 3
|
||||
}
|
||||
}
|
||||
act.upstreams.size() == 1
|
||||
with(act.upstreams.get(0)) {
|
||||
id == "local"
|
||||
chain == "bitcoin"
|
||||
connection instanceof UpstreamsConfig.BitcoinConnection
|
||||
with((UpstreamsConfig.BitcoinConnection) connection) {
|
||||
rpc != null
|
||||
rpc.url == new URI("http://localhost:8545")
|
||||
esplora != null
|
||||
esplora.url == new URI("http://localhost:3001")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def "Parse ds config"() {
|
||||
setup:
|
||||
def config = this.class.getClassLoader().getResourceAsStream("upstreams-ds.yaml")
|
||||
|
||||
@@ -27,6 +27,7 @@ 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.data.SimpleUnspent
|
||||
import io.emeraldpay.grpc.Chain
|
||||
import reactor.core.publisher.Flux
|
||||
import reactor.core.publisher.Mono
|
||||
@@ -42,89 +43,37 @@ class TrackBitcoinAddressSpec extends Specification {
|
||||
String hash1 = "0xa0e65cbc1b52a8ca60562112c6060552d882f16f34a9dba2ccdc05c0a6a27c22"
|
||||
ObjectMapper objectMapper = Global.objectMapper
|
||||
|
||||
def "Correct sum from multiple"() {
|
||||
def "Correct sum for single"() {
|
||||
setup:
|
||||
def json = this.class.getClassLoader().getResourceAsStream("bitcoin/unspent-one-addr.json")
|
||||
def unspents = objectMapper.readValue(json, List)
|
||||
def unspents = [
|
||||
new SimpleUnspent("f14b222e652c58d11435fa9172ddea000c6f5e20e6b715eb940fc28d1c4adeef", 0, 100L, 123L)
|
||||
]
|
||||
TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(MultistreamHolder))
|
||||
def address = new TrackBitcoinAddress.Address(
|
||||
Chain.BITCOIN, "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK"
|
||||
)
|
||||
when:
|
||||
def total = track.getTotal(Chain.BITCOIN, ["1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK"], unspents)
|
||||
def total = track.getTotal(address, unspents)
|
||||
|
||||
then:
|
||||
total.size() == 1
|
||||
total[0].address.chain == Chain.BITCOIN
|
||||
total[0].address.address == "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK"
|
||||
total[0].balance.toString() == "32928461"
|
||||
total.balance == 100
|
||||
}
|
||||
|
||||
def "Correct sum when other addresses"() {
|
||||
def "Correct sum for few"() {
|
||||
setup:
|
||||
def json = this.class.getClassLoader().getResourceAsStream("bitcoin/unspent-two-addr.json")
|
||||
def unspents = objectMapper.readValue(json, List)
|
||||
def unspents = [
|
||||
new SimpleUnspent("f14b222e652c58d11435fa9172ddea000c6f5e20e6b715eb940fc28d1c4adeef", 0, 100L, 123L),
|
||||
new SimpleUnspent("f14b222e652c58d11435fa9172ddea000c6f5e20e6b715eb940fc28d1c4adeef", 0, 123L, 123L),
|
||||
]
|
||||
TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(MultistreamHolder))
|
||||
def address = new TrackBitcoinAddress.Address(
|
||||
Chain.BITCOIN, "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK"
|
||||
)
|
||||
when:
|
||||
def total = track.getTotal(Chain.BITCOIN, ["1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK"], unspents)
|
||||
def total = track.getTotal(address, unspents)
|
||||
|
||||
then:
|
||||
total.size() == 1
|
||||
total[0].address.chain == Chain.BITCOIN
|
||||
total[0].address.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 = objectMapper.readValue(json, List)
|
||||
TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(MultistreamHolder))
|
||||
when:
|
||||
def total = track.getTotal(Chain.BITCOIN, ["1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP"], unspents).sort { it.address.address }
|
||||
|
||||
then:
|
||||
total.size() == 2
|
||||
with(total[0]) {
|
||||
address.chain == Chain.BITCOIN
|
||||
address.address == "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK"
|
||||
balance.toString() == "32928461"
|
||||
}
|
||||
with(total[1]) {
|
||||
address.chain == Chain.BITCOIN
|
||||
address.address == "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP"
|
||||
balance.toString() == "25550215615737"
|
||||
}
|
||||
}
|
||||
|
||||
def "Zero for empty unspents"() {
|
||||
setup:
|
||||
TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(MultistreamHolder))
|
||||
when:
|
||||
def total = track.getTotal(Chain.BITCOIN, ["1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK"], [])
|
||||
|
||||
then:
|
||||
total.size() == 1
|
||||
total[0].address.chain == Chain.BITCOIN
|
||||
total[0].address.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 = objectMapper.readValue(json, List)
|
||||
TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(MultistreamHolder))
|
||||
when:
|
||||
def total = track.getTotal(Chain.BITCOIN, ["16rCmCmbuWDhPjWTrpQGaU3EPdZF7MTdUk", "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK"], unspents).sort { it.address.address }
|
||||
|
||||
then:
|
||||
total.size() == 2
|
||||
with(total[0]) {
|
||||
address.address == "16rCmCmbuWDhPjWTrpQGaU3EPdZF7MTdUk"
|
||||
balance.toString() == "0"
|
||||
}
|
||||
with(total[1]) {
|
||||
address.address == "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK"
|
||||
balance.toString() == "32928461"
|
||||
}
|
||||
total.balance == 223
|
||||
}
|
||||
|
||||
def "One address for single provided"() {
|
||||
@@ -226,9 +175,11 @@ class TrackBitcoinAddressSpec extends Specification {
|
||||
def upstream = null
|
||||
upstream = Mock(BitcoinMultistream) {
|
||||
_ * getReader() >> Mock(BitcoinReader) {
|
||||
2 * listUnspent() >>> [
|
||||
2 * listUnspent(_) >>> [
|
||||
Mono.just([]),
|
||||
Mono.just([[address: "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", amount: 0.0123]])
|
||||
Mono.just([
|
||||
new SimpleUnspent("f14b222e652c58d11435fa9172ddea000c6f5e20e6b715eb940fc28d1c4adeef", 0, 1230000L, 123L)
|
||||
])
|
||||
]
|
||||
}
|
||||
_ * getHead() >> head
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
package io.emeraldpay.dshackle.upstream.bitcoin
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
import io.emeraldpay.dshackle.upstream.bitcoin.data.EsploraUnspent
|
||||
import org.bitcoinj.core.Address
|
||||
import org.bitcoinj.params.MainNetParams
|
||||
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
|
||||
import java.time.Instant
|
||||
|
||||
class EsploraClientSpec extends Specification {
|
||||
|
||||
ClientAndServer mockServer
|
||||
|
||||
def setup() {
|
||||
mockServer = ClientAndServer.startClientAndServer(23001);
|
||||
}
|
||||
|
||||
def cleanup() {
|
||||
mockServer.stop()
|
||||
}
|
||||
|
||||
def "get utxo"() {
|
||||
setup:
|
||||
def responseJson = this.class.getClassLoader().getResourceAsStream("bitcoin/esplora-utxo-1.json").text
|
||||
mockServer.when(
|
||||
HttpRequest.request()
|
||||
.withMethod("GET")
|
||||
.withPath("/address/35vktkPo4wdK8Twu4VMiuPLdCx23XEykGY/utxo")
|
||||
).respond(
|
||||
HttpResponse.response(responseJson)
|
||||
)
|
||||
def client = new EsploraClient(new URI("http://localhost:23001"), null, null)
|
||||
when:
|
||||
def act = client.getUtxo(Address.fromString(new MainNetParams(), "35vktkPo4wdK8Twu4VMiuPLdCx23XEykGY"))
|
||||
|
||||
then:
|
||||
StepVerifier.create(act)
|
||||
.expectNextMatches { list ->
|
||||
def ok = list.size() == 14
|
||||
if (!ok) println("invalid size")
|
||||
ok = ok && list[0] == new EsploraUnspent("002eba7d9e8081afc687e1c3fa7b6a6451713b75bc91432d441bb1e7e1511c5c", 0, 105524, Instant.ofEpochSecond(1599808442), 647721)
|
||||
if (!ok) println("invalid 0")
|
||||
ok = ok && list[1] == new EsploraUnspent("1ee3cb3833b1e499e7f8babb1100c4aa0b4273f655036561d84dd72a5b197258", 0, 5248, Instant.ofEpochSecond(1600114632), 648312)
|
||||
if (!ok) println("invalid 1")
|
||||
ok = ok && list[12] == new EsploraUnspent("311d0d1d4eea6ee37d57954cd1002898bef64885d8e438afbbd7fe4fdc6e08de", 2250, 22978, Instant.ofEpochSecond(1599018027), 646382)
|
||||
if (!ok) println("invalid 12")
|
||||
ok = ok && list[13] == new EsploraUnspent("230317b8fdf1ae85f5fbdc49ca90851b1728c2d9432b2738d3fe1c6f68f046e4", 11, 8642, Instant.ofEpochSecond(1599273442), 646777)
|
||||
if (!ok) println("invalid 13")
|
||||
ok
|
||||
}
|
||||
.expectComplete()
|
||||
.verify(Duration.ofSeconds(3))
|
||||
|
||||
when:
|
||||
def actTotal = client.getUtxo(Address.fromString(new MainNetParams(), "35vktkPo4wdK8Twu4VMiuPLdCx23XEykGY"))
|
||||
.block()
|
||||
.sum { it.value }
|
||||
|
||||
then:
|
||||
actTotal == 309841L
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
/**
|
||||
* 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 io.emeraldpay.dshackle.reader.Reader
|
||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
|
||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
|
||||
import org.bitcoinj.core.Address
|
||||
import org.bitcoinj.params.MainNetParams
|
||||
import reactor.core.publisher.Mono
|
||||
import spock.lang.Specification
|
||||
|
||||
class RpcUnspentReaderSpec extends Specification {
|
||||
|
||||
def "all if single address"() {
|
||||
setup:
|
||||
def json = this.class.getClassLoader().getResourceAsStream("bitcoin/unspent-one-addr.json").bytes
|
||||
def rpcReader = Mock(Reader) {
|
||||
1 * read(new JsonRpcRequest("listunspent", [])) >> Mono.just(JsonRpcResponse.ok(json))
|
||||
}
|
||||
def upstreams = Mock(BitcoinMultistream) {
|
||||
1 * getDirectApi(_) >> Mono.just(rpcReader)
|
||||
}
|
||||
def reader = new RpcUnspentReader(upstreams)
|
||||
|
||||
when:
|
||||
def act = reader.read(Address.fromString(new MainNetParams(), "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK")).block()
|
||||
|
||||
then:
|
||||
// cat src/test/resources/bitcoin/unspent-one-addr.json | jq '. | length'
|
||||
act.size() == 36
|
||||
with(act[0]) {
|
||||
txid == "e0f946c8f971b25cdffa64eed71d886019e437c0bf6a1b280584c0be5d1b5409"
|
||||
vout == 29
|
||||
confirmations == 2010
|
||||
value == 1230030
|
||||
}
|
||||
with(act[1]) {
|
||||
txid == "66e1e4d14ed6f454d2fda036f35cba423274ecdf5d46deb93f172c412a0f650d"
|
||||
vout == 83
|
||||
confirmations == 4963
|
||||
value == 756339
|
||||
}
|
||||
with(act[35]) {
|
||||
txid == "f14b222e652c58d11435fa9172ddea000c6f5e20e6b715eb940fc28d1c4adeef"
|
||||
vout == 58
|
||||
confirmations == 2139
|
||||
value == 1105047
|
||||
}
|
||||
}
|
||||
|
||||
def "select if two addresses - 35hK"() {
|
||||
setup:
|
||||
def json = this.class.getClassLoader().getResourceAsStream("bitcoin/unspent-two-addr.json").bytes
|
||||
def rpcReader = Mock(Reader) {
|
||||
1 * read(new JsonRpcRequest("listunspent", [])) >> Mono.just(JsonRpcResponse.ok(json))
|
||||
}
|
||||
def upstreams = Mock(BitcoinMultistream) {
|
||||
1 * getDirectApi(_) >> Mono.just(rpcReader)
|
||||
}
|
||||
def reader = new RpcUnspentReader(upstreams)
|
||||
|
||||
when:
|
||||
def act = reader.read(Address.fromString(new MainNetParams(), "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP")).block()
|
||||
|
||||
then:
|
||||
// cat src/test/resources/bitcoin/unspent-two-addr.json | jq '[.[] | select(.address == "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP")] | length'
|
||||
act.size() == 340
|
||||
with(act[0]) {
|
||||
txid == "8ad0d954a01eeb4f2c62d58d291699af847f9c8df43b775c27ffe8a5f76eba00"
|
||||
vout == 1
|
||||
value == 216465
|
||||
confirmations == 2583
|
||||
}
|
||||
with(act[11]) {
|
||||
txid == "777671a46b30b068052a73387e036bc8515cd3ba6adf9be4c70dfc0699f67c09"
|
||||
vout == 0
|
||||
confirmations == 13705
|
||||
value == 307906
|
||||
}
|
||||
// cat src/test/resources/bitcoin/unspent-two-addr.json | jq '[.[] | select(.address == "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP")] | .[211]'
|
||||
with(act[211]) {
|
||||
txid == "f20727393b0a586a3062a615fb71f43ec21c24258c3c6ec546fee5cbc1fa2ba7"
|
||||
vout == 0
|
||||
confirmations == 21890
|
||||
value == 50000000000
|
||||
}
|
||||
}
|
||||
|
||||
def "select if two addresses - 1K7x"() {
|
||||
setup:
|
||||
def json = this.class.getClassLoader().getResourceAsStream("bitcoin/unspent-two-addr.json").bytes
|
||||
def rpcReader = Mock(Reader) {
|
||||
1 * read(new JsonRpcRequest("listunspent", [])) >> Mono.just(JsonRpcResponse.ok(json))
|
||||
}
|
||||
def upstreams = Mock(BitcoinMultistream) {
|
||||
1 * getDirectApi(_) >> Mono.just(rpcReader)
|
||||
}
|
||||
def reader = new RpcUnspentReader(upstreams)
|
||||
|
||||
when:
|
||||
def act = reader.read(Address.fromString(new MainNetParams(), "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK")).block()
|
||||
|
||||
then:
|
||||
// cat src/test/resources/bitcoin/unspent-two-addr.json | jq '[.[] | select(.address == "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK")] | length'
|
||||
act.size() == 36
|
||||
// cat src/test/resources/bitcoin/unspent-two-addr.json | jq '[.[] | select(.address == "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK")] | .[0]'
|
||||
with(act[0]) {
|
||||
txid == "e0f946c8f971b25cdffa64eed71d886019e437c0bf6a1b280584c0be5d1b5409"
|
||||
vout == 29
|
||||
value == 1230030
|
||||
confirmations == 2030
|
||||
}
|
||||
// cat src/test/resources/bitcoin/unspent-two-addr.json | jq '[.[] | select(.address == "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK")] | .[35]'
|
||||
with(act[35]) {
|
||||
txid == "f14b222e652c58d11435fa9172ddea000c6f5e20e6b715eb940fc28d1c4adeef"
|
||||
vout == 58
|
||||
confirmations == 2159
|
||||
value == 1105047
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,9 +31,10 @@ import java.time.Duration
|
||||
class JsonRpcHttpClientSpec extends Specification {
|
||||
|
||||
ClientAndServer mockServer
|
||||
int port = 19332
|
||||
|
||||
def setup() {
|
||||
mockServer = ClientAndServer.startClientAndServer(18332);
|
||||
mockServer = ClientAndServer.startClientAndServer(port);
|
||||
}
|
||||
|
||||
def cleanup() {
|
||||
@@ -42,7 +43,7 @@ class JsonRpcHttpClientSpec extends Specification {
|
||||
|
||||
def "Make a request"() {
|
||||
setup:
|
||||
JsonRpcHttpClient client = new JsonRpcHttpClient("localhost:18332", null, null)
|
||||
JsonRpcHttpClient client = new JsonRpcHttpClient("localhost:${port}", null, null)
|
||||
def resp = '{' +
|
||||
' "jsonrpc": "2.0",' +
|
||||
' "result": "0x98de45",' +
|
||||
@@ -64,7 +65,7 @@ class JsonRpcHttpClientSpec extends Specification {
|
||||
def "Make request with basic auth"() {
|
||||
setup:
|
||||
def auth = new AuthConfig.ClientBasicAuth("user", "passwd")
|
||||
def client = new JsonRpcHttpClient("localhost:18332", auth, null)
|
||||
def client = new JsonRpcHttpClient("localhost:${port}", auth, null)
|
||||
|
||||
mockServer.when(
|
||||
HttpRequest.request()
|
||||
@@ -92,7 +93,7 @@ class JsonRpcHttpClientSpec extends Specification {
|
||||
|
||||
def "Produces RPC Exception on error status code"() {
|
||||
setup:
|
||||
def client = new JsonRpcHttpClient("localhost:18332", null, null)
|
||||
def client = new JsonRpcHttpClient("localhost:${port}", null, null)
|
||||
|
||||
mockServer.when(
|
||||
HttpRequest.request()
|
||||
|
||||
Reference in New Issue
Block a user