ws subscription refactoring

added support of pending transactions subscription
This commit is contained in:
a10zn8
2022-12-21 17:44:56 +04:00
parent 7bc4acb376
commit e0fdfa4420
64 changed files with 2051 additions and 310 deletions

View File

@@ -0,0 +1,135 @@
/**
* Copyright (c) 2022 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.commons
import org.springframework.util.backoff.ExponentialBackOff
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
import reactor.test.StepVerifier
import spock.lang.Retry
import spock.lang.Specification
import java.time.Duration
class DurableFluxSpec extends Specification {
private static CLOCK_ALLOW_ERROR_MS = 10
def "Normal subscribe works"() {
when:
def flux = DurableFlux.newBuilder()
.using({
Flux.fromIterable([1, 2, 3])
})
.build()
def values = flux.connect().collectList().block(Duration.ofSeconds(1))
then:
values == [1, 2, 3]
}
def "Reconnects when broken"() {
when:
def connects = 0
def flux = DurableFlux.newBuilder()
.using({
connects++
def id = connects
Flux.fromIterable([100+id, 200+id, 300+id])
.concatWith(Mono.error(new RuntimeException("[TEST Reached the end of $id]")))
})
.backoffOnError(Duration.ofMillis(50))
.build()
def values = flux.connect()
.take(5)
.collectList().block(Duration.ofSeconds(1))
then:
values == [101, 201, 301, 102, 202]
}
@Retry // sometimes it goes too fast or too slow
def "No continuous backoff if restored"() {
when:
def connects = 0
def flux = DurableFlux.newBuilder()
.using({
connects++
def id = connects
Flux.fromIterable([100+id, 200+id])
.concatWith(Mono.error(new RuntimeException("[TEST ERROR $id]")))
})
.backoffOnError(new ExponentialBackOff(100, 2))
.build()
def values = flux.connect()
.take(7)
def verifier = StepVerifier.create(values)
.expectNext(101, 201)
.expectNoEvent(Duration.ofMillis(100 - CLOCK_ALLOW_ERROR_MS))
.expectNext(102, 202)
.expectNoEvent(Duration.ofMillis(100 - CLOCK_ALLOW_ERROR_MS))
.expectNext(103, 203)
.expectNoEvent(Duration.ofMillis(100 - CLOCK_ALLOW_ERROR_MS))
.expectNext(104)
.expectComplete()
.verifyLater()
then:
verifier.verify(Duration.ofSeconds(3))
}
@Retry // sometimes it goes too fast or too slow
def "Continue backoff if not restored immediately"() {
when:
def connects = 0
def flux = DurableFlux.newBuilder()
.using({
connects++
def id = connects
if (id in [2, 4,5,6 ]) {
Flux.error(new RuntimeException("[TEST ERROR $id]"))
} else {
Flux.fromIterable([100+id, 200+id])
.concatWith(Mono.error(new RuntimeException("[TEST ERROR $id]")))
}
})
.backoffOnError(new ExponentialBackOff(100, 2))
.build()
def values = flux.connect()
.take(7)
def verifier = StepVerifier.create(values)
.expectNext(101, 201)
.as("first batch")
.expectNoEvent(Duration.ofMillis(100 + 200 - CLOCK_ALLOW_ERROR_MS))
.as("immediate fail on #2")
.expectNext(103, 203)
.as("second batch")
.expectNoEvent(Duration.ofMillis(100 + 200 + 400 + 800 - CLOCK_ALLOW_ERROR_MS))
.as("three fails in row (after the original) as #4, #5, #6")
.expectNext(107, 207)
.as("third batch")
.expectNoEvent(Duration.ofMillis(100 - CLOCK_ALLOW_ERROR_MS))
.expectNext(108)
.expectComplete()
.verifyLater()
then:
verifier.verify(Duration.ofSeconds(3))
}
}

View File

@@ -0,0 +1,104 @@
/**
* Copyright (c) 2022 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.commons
import io.emeraldpay.etherjar.domain.TransactionId
import io.emeraldpay.etherjar.hex.HexDataComparator
import io.emeraldpay.etherjar.tx.Transaction
import spock.lang.Specification
import java.time.Duration
class ExpiringSetSpec extends Specification {
def "Add and check item"() {
setup:
def set = new ExpiringSet(Duration.ofSeconds(60), new HexDataComparator() as Comparator<TransactionId>, 100)
when:
def firstAdded = set.add(TransactionId.from("0xc008b506367e1f96fcfdf6b683e84601434f8b655334bc61dae7970f0fb7d02c"))
def firstExists = set.contains(TransactionId.from("0xc008b506367e1f96fcfdf6b683e84601434f8b655334bc61dae7970f0fb7d02c"))
def secondAdded = set.add(TransactionId.from("0x424e36776777ddd2877df0f9d278c37077d1e00afa84defcfb6b367880d8eb6d"))
def secondExists = set.contains(TransactionId.from("0x424e36776777ddd2877df0f9d278c37077d1e00afa84defcfb6b367880d8eb6d"))
def thirdExists = set.contains(TransactionId.from("0xe218f09c3060099cb7302304e2a29ed7a8693bd5dfe3a55cce96fabce27012e0"))
then:
firstAdded
secondAdded
firstExists
secondExists
!thirdExists
set.size == 2
}
def "Doesn't grow after limit"() {
setup:
def set = new ExpiringSet(Duration.ofSeconds(60), new HexDataComparator() as Comparator<TransactionId>, 3)
when:
set.add(TransactionId.from("0xc008b506367e1f96fcfdf6b683e84601434f8b655334bc61dae7970f0fb7d02c"))
set.add(TransactionId.from("0x424e36776777ddd2877df0f9d278c37077d1e00afa84defcfb6b367880d8eb6d"))
set.add(TransactionId.from("0xe218f09c3060099cb7302304e2a29ed7a8693bd5dfe3a55cce96fabce27012e0"))
set.add(TransactionId.from("0xd3082daa344a64369c8aace137f22beb5085351bf111859202bac66b70b28bdd"))
then:
set.size == 3
when:
def firstExists = set.contains(TransactionId.from("0xc008b506367e1f96fcfdf6b683e84601434f8b655334bc61dae7970f0fb7d02c"))
def secondExists = set.contains(TransactionId.from("0x424e36776777ddd2877df0f9d278c37077d1e00afa84defcfb6b367880d8eb6d"))
def thirdExists = set.contains(TransactionId.from("0xe218f09c3060099cb7302304e2a29ed7a8693bd5dfe3a55cce96fabce27012e0"))
then:
!firstExists
secondExists
thirdExists
}
def "Remove expired"() {
setup:
def set = new ExpiringSet(Duration.ofMillis(100), new HexDataComparator() as Comparator<TransactionId>, 100)
when:
set.add(TransactionId.from("0x1118b506367e1f96fcfdf6b683e84601434f8b655334bc61dae7970f0fb7d02c"))
set.add(TransactionId.from("0x222e36776777ddd2877df0f9d278c37077d1e00afa84defcfb6b367880d8eb6d"))
then:
set.size == 2
when:
Thread.sleep(60)
set.add(TransactionId.from("0x3338f09c3060099cb7302304e2a29ed7a8693bd5dfe3a55cce96fabce27012e0"))
set.add(TransactionId.from("0x44482daa344a64369c8aace137f22beb5085351bf111859202bac66b70b28bdd"))
then:
set.size == 4
when:
Thread.sleep(60)
set.add(TransactionId.from("0x55531d466acf4ef72f7e0fbc60a5c2c9d2845b90a3d27b6d7581575cb119cac9"))
set.add(TransactionId.from("0x6665e1f32cb21aee5f27d804cfc65781d5c140b002776bc073f9405479e8b1e5"))
then:
set.size == 4
set.contains(TransactionId.from("0x3338f09c3060099cb7302304e2a29ed7a8693bd5dfe3a55cce96fabce27012e0"))
set.contains(TransactionId.from("0x44482daa344a64369c8aace137f22beb5085351bf111859202bac66b70b28bdd"))
set.contains(TransactionId.from("0x55531d466acf4ef72f7e0fbc60a5c2c9d2845b90a3d27b6d7581575cb119cac9"))
set.contains(TransactionId.from("0x6665e1f32cb21aee5f27d804cfc65781d5c140b002776bc073f9405479e8b1e5"))
}
}

View File

@@ -0,0 +1,79 @@
/**
* Copyright (c) 2022 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.commons
import reactor.core.publisher.Flux
import spock.lang.Specification
import java.time.Duration
class SharedFluxHolderSpec extends Specification {
def "Keeps one flux"() {
when:
def called = 0
def shared = new SharedFluxHolder<Integer>({
Flux.fromIterable([100+called, 200+called, 300+called, 400+called, 500+called])
.delayElements(Duration.ofMillis(100))
.doOnSubscribe {called++ }
})
List<Integer> values1 = []
List<Integer> values2 = []
new Thread({
values1 = shared.get().collectList().block(Duration.ofSeconds(1))
}).start()
new Thread({
values2 = shared.get().collectList().block(Duration.ofSeconds(1))
}).start()
Thread.sleep(1000)
then:
values1 == [100, 200, 300, 400, 500]
values2 == [100, 200, 300, 400, 500]
called == 1
}
def "Create a new flux if existing completes"() {
when:
def called = 0
def shared = new SharedFluxHolder<Integer>({
Flux.fromIterable([100+called, 200+called, 300+called, 400+called, 500+called])
.delayElements(Duration.ofMillis(100))
.doOnSubscribe {called++ }
})
List<Integer> values1 = []
List<Integer> values2 = []
List<Integer> values3 = []
new Thread({
values1 = shared.get().collectList().block(Duration.ofSeconds(1))
}).start()
new Thread({
values2 = shared.get().collectList().block(Duration.ofSeconds(1))
}).start()
Thread.sleep(1000)
new Thread({
values3 = shared.get().collectList().block(Duration.ofSeconds(1))
}).start()
Thread.sleep(1000)
then:
values1 == [100, 200, 300, 400, 500]
values2 == [100, 200, 300, 400, 500]
values3 == [101, 201, 301, 401, 501]
}
}

View File

@@ -20,7 +20,7 @@ import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.dshackle.test.MultistreamHolderMock
import io.emeraldpay.dshackle.upstream.Selector
import io.emeraldpay.dshackle.upstream.ethereum.EthereumPosMultiStream
import io.emeraldpay.dshackle.upstream.ethereum.EthereumSubscribe
import io.emeraldpay.dshackle.upstream.ethereum.EthereumSubscriptionApi
import io.emeraldpay.dshackle.upstream.signature.NoSigner
import io.emeraldpay.dshackle.Chain
import reactor.core.publisher.Flux
@@ -39,12 +39,12 @@ class NativeSubscribeSpec extends Specification {
.setMethod("newHeads")
.build()
def subscribe = Mock(EthereumSubscribe) {
def subscribe = Mock(EthereumSubscriptionApi) {
1 * it.subscribe("newHeads", null, _ as Selector.AnyLabelMatcher) >> Flux.just("{}")
}
def up = Mock(EthereumPosMultiStream) {
1 * it.tryProxy(_ as Selector.AnyLabelMatcher, call) >> null
1 * it.getSubscribe() >> subscribe
1 * it.getSubscriptionApi() >> subscribe
}
def nativeSubscribe = new NativeSubscribe(new MultistreamHolderMock(Chain.ETHEREUM, up), signer)
@@ -70,7 +70,7 @@ class NativeSubscribeSpec extends Specification {
))
.build()
def subscribe = Mock(EthereumSubscribe) {
def subscribe = Mock(EthereumSubscriptionApi) {
1 * it.subscribe("logs", { params ->
println("params: $params")
def ok = params instanceof Map &&
@@ -83,7 +83,7 @@ class NativeSubscribeSpec extends Specification {
}
def up = Mock(EthereumPosMultiStream) {
1 * it.tryProxy(_ as Selector.AnyLabelMatcher, call) >> null
1 * it.getSubscribe() >> subscribe
1 * it.getSubscriptionApi() >> subscribe
}
def nativeSubscribe = new NativeSubscribe(new MultistreamHolderMock(Chain.ETHEREUM, up), signer)
@@ -106,7 +106,7 @@ class NativeSubscribeSpec extends Specification {
.build()
def up = Mock(EthereumPosMultiStream) {
1 * it.tryProxy(_ as Selector.AnyLabelMatcher, call) >> Flux.just("{}")
0 * it.getSubscribe()
0 * it.getSubscriptionApi()
}
def nativeSubscribe = new NativeSubscribe(new MultistreamHolderMock(Chain.ETHEREUM, up), signer)

View File

@@ -5,10 +5,10 @@ import io.emeraldpay.api.proto.Common
import io.emeraldpay.dshackle.config.TokensConfig
import io.emeraldpay.dshackle.upstream.MultistreamHolder
import io.emeraldpay.dshackle.upstream.Selector
import io.emeraldpay.dshackle.upstream.SubscriptionConnect
import io.emeraldpay.dshackle.upstream.ethereum.ERC20Balance
import io.emeraldpay.dshackle.upstream.ethereum.EthereumMultistream
import io.emeraldpay.dshackle.upstream.ethereum.EthereumPosMultiStream
import io.emeraldpay.dshackle.upstream.ethereum.EthereumSubscribe
import io.emeraldpay.dshackle.upstream.ethereum.EthereumSubscriptionApi
import io.emeraldpay.dshackle.upstream.ethereum.subscribe.ConnectLogs
import io.emeraldpay.dshackle.upstream.ethereum.subscribe.json.LogMessage
import io.emeraldpay.etherjar.domain.BlockHash
@@ -169,21 +169,26 @@ class TrackERC20AddressSpec extends Specification {
"unknown"
)
]
def logs = Mock(ConnectLogs) {
1 * start(
[Address.from("0x54EedeAC495271d0F6B175474E89094C44Da98b9")],
[Hex32.from("0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef")],
Selector.empty
) >> { args ->
println("ConnectLogs.start $args")
def connect = Mock(SubscriptionConnect) {
1 * connect(Selector.empty) >> {
Flux.fromIterable(events)
}
}
def sub = Mock(EthereumSubscribe) {
def logs = Mock(ConnectLogs) {
1 * create(
[Address.from("0x54EedeAC495271d0F6B175474E89094C44Da98b9")],
[Hex32.from("0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef")],
) >> { args ->
println("ConnectLogs.start $args")
connect
}
}
def sub = Mock(EthereumSubscriptionApi) {
1 * getLogs() >> logs
}
def up = Mock(EthereumPosMultiStream) {
1 * getSubscribe() >> sub
1 * getSubscriptionApi() >> sub
_ * cast(EthereumPosMultiStream) >> { args ->
it
}

View File

@@ -2,6 +2,8 @@ package io.emeraldpay.dshackle.test
import io.emeraldpay.dshackle.reader.Reader
import io.emeraldpay.dshackle.upstream.Head
import io.emeraldpay.dshackle.upstream.ethereum.EthereumUpstreamSubscriptions
import io.emeraldpay.dshackle.upstream.ethereum.NoEthereumUpstreamSubscriptions
import io.emeraldpay.dshackle.upstream.ethereum.connectors.EthereumConnector
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
@@ -34,4 +36,9 @@ class EthereumConnectorMock implements EthereumConnector {
boolean isRunning() {
return true
}
@Override
EthereumUpstreamSubscriptions getUpstreamSubscriptions() {
return NoEthereumUpstreamSubscriptions.DEFAULT
}
}

View File

@@ -48,7 +48,7 @@ class FilteredApisSpec extends Specification {
def httpFactory = Mock(HttpFactory) {
create(_, _) >> TestingCommons.api().tap { it.id = "${i++}" }
}
def connectorFactory = new EthereumConnectorFactory(false, null, httpFactory, new MostWorkForkChoice(), BlockValidator.@Companion.ALWAYS_VALID)
def connectorFactory = new EthereumConnectorFactory(false, null, httpFactory, new MostWorkForkChoice(), BlockValidator.ALWAYS_VALID)
new EthereumRpcUpstream(
"test",
(byte)123,

View File

@@ -32,7 +32,7 @@ import java.time.Instant
class DefaultEthereumHeadSpec extends Specification {
DefaultEthereumHead head = new DefaultEthereumHead("upstream", new MostWorkForkChoice(), BlockValidator.@Companion.ALWAYS_VALID)
DefaultEthereumHead head = new DefaultEthereumHead("upstream", new MostWorkForkChoice(), BlockValidator.ALWAYS_VALID)
ObjectMapper objectMapper = Global.objectMapper
def blocks = (10L..20L).collect { i ->

View File

@@ -16,15 +16,16 @@
package io.emeraldpay.dshackle.upstream.ethereum
import io.emeraldpay.dshackle.test.TestingCommons
import io.emeraldpay.dshackle.upstream.ethereum.subscribe.PendingTxesSource
import io.emeraldpay.etherjar.domain.Address
import io.emeraldpay.etherjar.hex.Hex32
import spock.lang.Specification
class EthereumSubscribeSpec extends Specification {
class EthereumSubscriptionApiSpec extends Specification {
def "read empty logs request"() {
setup:
def ethereumSubscribe = new EthereumSubscribe(TestingCommons.emptyMultistream() as EthereumPosMultiStream)
def ethereumSubscribe = new EthereumSubscriptionApi(TestingCommons.emptyMultistream() as EthereumPosMultiStream, Stub(PendingTxesSource))
when:
def act = ethereumSubscribe.readLogsRequest([:])
@@ -35,7 +36,7 @@ class EthereumSubscribeSpec extends Specification {
def "read single address logs request"() {
setup:
def ethereumSubscribe = new EthereumSubscribe(TestingCommons.emptyMultistream() as EthereumPosMultiStream)
def ethereumSubscribe = new EthereumSubscriptionApi(TestingCommons.emptyMultistream() as EthereumPosMultiStream, Stub(PendingTxesSource))
when:
def act = ethereumSubscribe.readLogsRequest([
address: "0x829bd824b016326a401d083b33d092293333a830"
@@ -60,7 +61,7 @@ class EthereumSubscribeSpec extends Specification {
def "ignores invalid address for logs request"() {
setup:
def ethereumSubscribe = new EthereumSubscribe(TestingCommons.emptyMultistream() as EthereumPosMultiStream)
def ethereumSubscribe = new EthereumSubscriptionApi(TestingCommons.emptyMultistream() as EthereumPosMultiStream, Stub(PendingTxesSource))
when:
def act = ethereumSubscribe.readLogsRequest([
address: "829bd824b016326a401d083b33d092293333a830"
@@ -73,7 +74,7 @@ class EthereumSubscribeSpec extends Specification {
def "read multi address logs request"() {
setup:
def ethereumSubscribe = new EthereumSubscribe(TestingCommons.emptyMultistream() as EthereumPosMultiStream)
def ethereumSubscribe = new EthereumSubscriptionApi(TestingCommons.emptyMultistream() as EthereumPosMultiStream, Stub(PendingTxesSource))
when:
def act = ethereumSubscribe.readLogsRequest([
address: ["0x829bd824b016326a401d083b33d092293333a830", "0x401d083b33d092293333a83829bd824b016326a0"]
@@ -89,7 +90,7 @@ class EthereumSubscribeSpec extends Specification {
def "read single topic logs request"() {
setup:
def ethereumSubscribe = new EthereumSubscribe(TestingCommons.emptyMultistream() as EthereumPosMultiStream)
def ethereumSubscribe = new EthereumSubscriptionApi(TestingCommons.emptyMultistream() as EthereumPosMultiStream, Stub(PendingTxesSource))
when:
def act = ethereumSubscribe.readLogsRequest([
topics: "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"
@@ -114,7 +115,7 @@ class EthereumSubscribeSpec extends Specification {
def "read invalid topic for request"() {
setup:
def ethereumSubscribe = new EthereumSubscribe(TestingCommons.emptyMultistream() as EthereumPosMultiStream)
def ethereumSubscribe = new EthereumSubscriptionApi(TestingCommons.emptyMultistream() as EthereumPosMultiStream, Stub(PendingTxesSource))
when:
def act = ethereumSubscribe.readLogsRequest([
topics: [
@@ -132,7 +133,7 @@ class EthereumSubscribeSpec extends Specification {
def "read multi topic logs request"() {
setup:
def ethereumSubscribe = new EthereumSubscribe(TestingCommons.emptyMultistream() as EthereumPosMultiStream)
def ethereumSubscribe = new EthereumSubscriptionApi(TestingCommons.emptyMultistream() as EthereumPosMultiStream, Stub(PendingTxesSource))
when:
def act = ethereumSubscribe.readLogsRequest([
topics: [
@@ -151,7 +152,7 @@ class EthereumSubscribeSpec extends Specification {
def "read full logs request"() {
setup:
def ethereumSubscribe = new EthereumSubscribe(TestingCommons.emptyMultistream() as EthereumPosMultiStream)
def ethereumSubscribe = new EthereumSubscriptionApi(TestingCommons.emptyMultistream() as EthereumPosMultiStream, Stub(PendingTxesSource))
when:
def act = ethereumSubscribe.readLogsRequest([
address: "0x298d492e8c1d909d3f63bc4a36c66c64acb3d695",

View File

@@ -0,0 +1,74 @@
/**
* Copyright (c) 2022 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.ethereum
import io.emeraldpay.dshackle.Global
import io.emeraldpay.dshackle.data.BlockContainer
import io.emeraldpay.dshackle.test.TestingCommons
import io.emeraldpay.dshackle.upstream.BlockValidator
import io.emeraldpay.dshackle.upstream.forkchoice.AlwaysForkChoice
import io.emeraldpay.etherjar.domain.BlockHash
import io.emeraldpay.etherjar.domain.TransactionId
import io.emeraldpay.etherjar.rpc.json.BlockJson
import io.emeraldpay.etherjar.rpc.json.TransactionRefJson
import reactor.core.publisher.Flux
import spock.lang.Specification
import java.time.Instant
import java.time.temporal.ChronoUnit
class EthereumWsHeadSpec extends Specification {
def "Fetch block"() {
setup:
def block = new BlockJson<TransactionRefJson>()
block.number = 100
block.hash = BlockHash.from("0x3ec2ebf5d0ec474d0ac6bc50d2770d8409ad76e119968e7919f85d5ec8915200")
block.timestamp = Instant.now().truncatedTo(ChronoUnit.SECONDS)
block.transactions = [
new TransactionRefJson(TransactionId.from("0x29229361dc5aa1ec66c323dc7a299e2b61a8c8dd2a3522d41255ec10eca25dd8")),
new TransactionRefJson(TransactionId.from("0xebe8f22a55a9e26892a8545b93cbb2bfa4fd81c3184e50e5cf6276025bb42b93"))
]
block.uncles = []
block.totalDifficulty = BigInteger.ONE
def headBlock = block.copy().tap {
it.transactions = null
}.with {
Global.objectMapper.writeValueAsBytes(it)
}
def apiMock = TestingCommons.api()
apiMock.answerOnce("eth_getBlockByHash", ["0x3ec2ebf5d0ec474d0ac6bc50d2770d8409ad76e119968e7919f85d5ec8915200", false], block)
def ws = Mock(WsSubscriptions)
def head = new EthereumWsHead("fake", new AlwaysForkChoice(), BlockValidator.ALWAYS_VALID, apiMock, ws)
when:
def act = head.listenNewHeads().blockFirst()
then:
act == BlockContainer.from(block)
act.transactions.size() == 2
act.transactions[0].toHexWithPrefix() == "0x29229361dc5aa1ec66c323dc7a299e2b61a8c8dd2a3522d41255ec10eca25dd8"
act.transactions[1].toHexWithPrefix() == "0xebe8f22a55a9e26892a8545b93cbb2bfa4fd81c3184e50e5cf6276025bb42b93"
1 * ws.subscribe("newHeads") >> Flux.fromIterable([
headBlock
])
}
}

View File

@@ -11,7 +11,7 @@ import spock.lang.Specification
import java.time.Duration
class WsConnectionRealSpec extends Specification {
class WsConnectionImplRealSpec extends Specification {
static SLEEP = 500
@@ -19,7 +19,7 @@ class WsConnectionRealSpec extends Specification {
@Shared
MockWSServer server
@Shared
WsConnection conn
WsConnectionImpl conn
def setup() {
if (System.getenv("CI") == "true") {
@@ -31,7 +31,7 @@ class WsConnectionRealSpec extends Specification {
server = new MockWSServer(port)
server.start()
Thread.sleep(SLEEP)
conn = new EthereumWsFactory("test", Chain.ETHEREUM, "ws://localhost:${port}".toURI(), "http://localhost:${port}".toURI()).create(null, null)
conn = new EthereumWsFactory("test", Chain.ETHEREUM, "ws://localhost:${port}".toURI(), "http://localhost:${port}".toURI()).create(null)
}
def cleanup() {
@@ -39,22 +39,10 @@ class WsConnectionRealSpec extends Specification {
server.stop()
}
def "Connects to server"() {
def "Can make a RPC request"() {
when:
conn.connect()
Thread.sleep(SLEEP)
println("verify....")
def act = server.received
then:
act.size() > 0
act[0].value.contains("\"method\":\"eth_subscribe\"")
act[0].value.contains("\"params\":[\"newHeads\"]")
}
def "Makes RPC request"() {
when:
conn.connect()
def resp = conn.call(new JsonRpcRequest("foo_bar", []))
def resp = conn.callRpc(new JsonRpcRequest("foo_bar", []))
then:
StepVerifier.create(resp)
.then {
@@ -70,8 +58,8 @@ class WsConnectionRealSpec extends Specification {
Thread.sleep(SLEEP)
def act = server.received
then:
act.size() == 2
act[1].value.contains("\"method\":\"foo_bar\"")
act.size() == 1
act[0].value.contains("\"method\":\"foo_bar\"")
}
def "Reconnects after server disconnect"() {
@@ -83,15 +71,15 @@ class WsConnectionRealSpec extends Specification {
Thread.sleep(SLEEP)
server = new MockWSServer(port)
server.start()
def resp = conn.call(new JsonRpcRequest("foo_bar", []))
server.onNextReply('{"jsonrpc":"2.0","id":100,"result":1}')
// reconnects in 2 seconds, give 1 extra
Thread.sleep(3_000)
def resp = conn.callRpc(new JsonRpcRequest("foo_bar", [])).block(Duration.ofSeconds(1))
def act = server.received
then:
act.size() > 0
act[0].value.contains("\"method\":\"eth_subscribe\"")
act[0].value.contains("\"params\":[\"newHeads\"]")
act.size() == 1
act[0].value.contains("\"method\":\"foo_bar\"")
}
def "Error on request when server disconnects"() {
@@ -99,7 +87,7 @@ class WsConnectionRealSpec extends Specification {
conn.connect()
conn.reconnectIntervalSeconds = 2
def resp = conn.call(new JsonRpcRequest("foo_bar", []))
def resp = conn.callRpc(new JsonRpcRequest("foo_bar", []))
then:
StepVerifier.create(resp)
@@ -113,7 +101,7 @@ class WsConnectionRealSpec extends Specification {
def up = Mock(DefaultUpstream) {
_ * getId() >> "test"
}
conn = new EthereumWsFactory("test", Chain.ETHEREUM, "ws://localhost:${port}".toURI(), "http://localhost:${port}".toURI()).create(up, null)
conn = new EthereumWsFactory("test", Chain.ETHEREUM, "ws://localhost:${port}".toURI(), "http://localhost:${port}".toURI()).create(up)
when:
conn.connect()
conn.reconnectIntervalSeconds = 10
@@ -125,18 +113,6 @@ class WsConnectionRealSpec extends Specification {
1 * up.setStatus(UpstreamAvailability.UNAVAILABLE)
}
def "Validates after connect"() {
setup:
def validator = Mock(EthereumUpstreamValidator)
conn = new EthereumWsFactory("test", Chain.ETHEREUM, "ws://localhost:${port}".toURI(), "http://localhost:${port}".toURI()).create(null, validator)
when:
conn.connect()
Thread.sleep(100)
then:
1 * validator.validate()
}
def "Try to connects to server until it's available"() {
when:
server.stop()
@@ -146,12 +122,14 @@ class WsConnectionRealSpec extends Specification {
Thread.sleep(3_000)
server = new MockWSServer(port)
server.start()
Thread.sleep(2_000)
server.onNextReply('{"jsonrpc":"2.0","id":100,"result":1}')
Thread.sleep(3_000)
def resp = conn.callRpc(new JsonRpcRequest("foo_bar", [])).block(Duration.ofSeconds(1))
def act = server.received
then:
act.size() > 0
act[0].value.contains("\"method\":\"eth_subscribe\"")
act[0].value.contains("\"params\":[\"newHeads\"]")
act.size() == 1
act[0].value.contains("\"method\":\"foo_bar\"")
}
def "Call after reconnect"() {
@@ -166,7 +144,7 @@ class WsConnectionRealSpec extends Specification {
// reconnects in 2 seconds, give 1 extra
Thread.sleep(3_000)
def resp = conn.call(new JsonRpcRequest("foo_bar", []))
def resp = conn.callRpc(new JsonRpcRequest("foo_bar", []))
then:
StepVerifier.create(resp)
.then {

View File

@@ -34,48 +34,14 @@ import java.time.Duration
import java.time.Instant
import java.time.temporal.ChronoUnit
class WsConnectionSpec extends Specification {
def "Fetch block"() {
setup:
def wsf = new EthereumWsFactory("test", Chain.ETHEREUM, new URI("http://localhost"), new URI("http://localhost"))
def block = new BlockJson<TransactionRefJson>()
block.number = 100
block.hash = BlockHash.from("0x3ec2ebf5d0ec474d0ac6bc50d2770d8409ad76e119968e7919f85d5ec8915200")
block.timestamp = Instant.now().truncatedTo(ChronoUnit.SECONDS)
block.transactions = []
block.uncles = []
block.totalDifficulty = BigInteger.ONE
def headBlock = block.copy().tap {
it.transactions = null
}
def apiMock = TestingCommons.api()
def wsApiMock = apiMock.asWebsocket()
def ws = wsf.create(null, null)
apiMock.answerOnce("eth_getBlockByHash", ["0x3ec2ebf5d0ec474d0ac6bc50d2770d8409ad76e119968e7919f85d5ec8915200", false], block)
when:
Flux.from(ws.handle(wsApiMock.inbound, wsApiMock.outbound)).subscribe()
def act = Flux.from(ws.getBlocksFlux())
then:
StepVerifier.create(act)
.then { ws.onNewHeads(headBlock).subscribe() }
.expectNext(BlockContainer.from(block))
.thenCancel()
.verify(Duration.ofSeconds(5))
}
class WsConnectionImplSpec extends Specification {
def "Makes a RPC call"() {
setup:
def wsf = new EthereumWsFactory("test", Chain.ETHEREUM, new URI("http://localhost"), new URI("http://localhost"))
def apiMock = TestingCommons.api()
def wsApiMock = apiMock.asWebsocket()
def ws = wsf.create(null, null)
def ws = wsf.create(null)
def tx = new TransactionJson().tap {
hash = TransactionId.from("0x3ec2ebf5d0ec474d0ac6bc50d2770d8409ad76e119968e7919f85d5ec8915200")
@@ -84,7 +50,7 @@ class WsConnectionSpec extends Specification {
when:
Flux.from(ws.handle(wsApiMock.inbound, wsApiMock.outbound)).subscribe()
def act = ws.call(new JsonRpcRequest("eth_getTransactionByHash", ["0x3ec2ebf5d0ec474d0ac6bc50d2770d8409ad76e119968e7919f85d5ec8915200"], 15, null, null))
def act = ws.callRpc(new JsonRpcRequest("eth_getTransactionByHash", ["0x3ec2ebf5d0ec474d0ac6bc50d2770d8409ad76e119968e7919f85d5ec8915200"], 15, null, null))
then:
StepVerifier.create(act)
@@ -100,13 +66,13 @@ class WsConnectionSpec extends Specification {
def wsf = new EthereumWsFactory("test", Chain.ETHEREUM, new URI("http://localhost"), new URI("http://localhost"))
def apiMock = TestingCommons.api()
def wsApiMock = apiMock.asWebsocket()
def ws = wsf.create(null, null)
def ws = wsf.create(null)
apiMock.answerOnce("eth_getTransactionByHash", ["0x3ec2ebf5d0ec474d0ac6bc50d2770d8409ad76e119968e7919f85d5ec8915200"], null)
when:
Flux.from(ws.handle(wsApiMock.inbound, wsApiMock.outbound)).subscribe()
def act = ws.call(new JsonRpcRequest("eth_getTransactionByHash", ["0x3ec2ebf5d0ec474d0ac6bc50d2770d8409ad76e119968e7919f85d5ec8915200"], 15, null, null))
def act = ws.callRpc(new JsonRpcRequest("eth_getTransactionByHash", ["0x3ec2ebf5d0ec474d0ac6bc50d2770d8409ad76e119968e7919f85d5ec8915200"], 15, null, null))
then:
StepVerifier.create(act)
@@ -123,14 +89,14 @@ class WsConnectionSpec extends Specification {
def wsf = new EthereumWsFactory("test", Chain.ETHEREUM, new URI("http://localhost"), new URI("http://localhost"))
def apiMock = TestingCommons.api()
def wsApiMock = apiMock.asWebsocket()
def ws = wsf.create(null, null)
def ws = wsf.create(null)
apiMock.answerOnce("eth_getTransactionByHash", ["0x3ec2ebf5d0ec474d0ac6bc50d2770d8409ad76e119968e7919f85d5ec8915200"],
new RpcResponseError(RpcResponseError.CODE_METHOD_NOT_EXIST, "test"))
when:
Flux.from(ws.handle(wsApiMock.inbound, wsApiMock.outbound)).subscribe()
def act = ws.call(new JsonRpcRequest("eth_getTransactionByHash", ["0x3ec2ebf5d0ec474d0ac6bc50d2770d8409ad76e119968e7919f85d5ec8915200"], 15, null, null))
def act = ws.callRpc(new JsonRpcRequest("eth_getTransactionByHash", ["0x3ec2ebf5d0ec474d0ac6bc50d2770d8409ad76e119968e7919f85d5ec8915200"], 15, null, null))
then:
StepVerifier.create(act)

View File

@@ -0,0 +1,90 @@
/**
* Copyright (c) 2022 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.ethereum
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcWsMessage
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
import reactor.core.publisher.Sinks
import reactor.test.StepVerifier
import spock.lang.Specification
import java.time.Duration
class WsSubscriptionsImplSpec extends Specification {
def "Makes a subscription"() {
setup:
def answers = Flux.fromIterable(
[
new JsonRpcWsMessage("100".bytes, null, "0xcff45d00e7"),
new JsonRpcWsMessage("101".bytes, null, "0xcff45d00e7"),
new JsonRpcWsMessage("102".bytes, null, "0xcff45d00e7"),
]
)
def conn = Mock(WsConnectionImpl)
def ws = new WsSubscriptionsImpl(conn)
when:
def act = ws.subscribe("foo_bar")
.map { new String(it) }
.take(3)
.collectList().block(Duration.ofSeconds(1))
then:
act == ["100", "101", "102"]
1 * conn.callRpc({ JsonRpcRequest req ->
req.method == "eth_subscribe" && req.params == ["foo_bar"]
}) >> Mono.just(new JsonRpcResponse('"0xcff45d00e7"'.bytes, null))
1 * conn.getSubscribeResponses() >> answers
}
def "Produces only messages to the actual subscription"() {
setup:
def answers = Flux.fromIterable(
[
new JsonRpcWsMessage("100".bytes, null, "0xcff45d00e7"),
new JsonRpcWsMessage("AAA".bytes, null, "0x000001a0e7"),
new JsonRpcWsMessage("101".bytes, null, "0xcff45d00e7"),
new JsonRpcWsMessage("BBB".bytes, null, "0x000001a0e7"),
new JsonRpcWsMessage("CCC".bytes, null, "0x000001a0e7"),
new JsonRpcWsMessage("102".bytes, null, "0xcff45d00e7"),
new JsonRpcWsMessage("DDD".bytes, null, "0x000001a0e7"),
]
)
def conn = Mock(WsConnectionImpl)
def ws = new WsSubscriptionsImpl(conn)
when:
def act = ws.subscribe("foo_bar")
.map { new String(it) }
.take(3)
.collectList().block(Duration.ofSeconds(1))
then:
act == ["100", "101", "102"]
1 * conn.callRpc({ JsonRpcRequest req ->
req.method == "eth_subscribe" && req.params == ["foo_bar"]
}) >> Mono.just(new JsonRpcResponse('"0xcff45d00e7"'.bytes, null))
1 * conn.getSubscribeResponses() >> answers
}
}

View File

@@ -0,0 +1,92 @@
/**
* Copyright (c) 2022 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.ethereum.subscribe
import io.emeraldpay.dshackle.upstream.Selector
import io.emeraldpay.etherjar.domain.TransactionId
import reactor.core.publisher.Flux
import spock.lang.Specification
import java.time.Duration
class AggregatedPendingTxesSpec extends Specification {
def "Produces values from two sources"() {
setup:
def source1 = Mock(PendingTxesSource)
def source2 = Mock(PendingTxesSource)
when:
def aggregate = new AggregatedPendingTxes([source1, source2])
def values = aggregate.connect(Selector.empty)
.collectList().block(Duration.ofSeconds(1))
then:
1 * source1.connect(Selector.empty) >> Flux.fromIterable(
[
"0xa61bab14fc9720ea8725622688c2f964666d7c2afdae38af7dad53f12f242d5c",
"0x911548eb0f3bf353a54e03a3506c7c3e747470d6c201f03babbc07ff6e14cd6e"
].collect { TransactionId.from(it) }
)
1 * source2.connect(Selector.empty) >> Flux.fromIterable(
[
"0x9a9d4618b12d36d17a63d48c5b5efc05b461feead124ddd86803d8bca4015248",
"0xa38173981f8eab96ee70cefe42735af0f574b7ef354565f2fea32a28e5ed9bd2"
].collect { TransactionId.from(it) }
)
values.collect { it.toHex() }.toSorted() == [
"0xa61bab14fc9720ea8725622688c2f964666d7c2afdae38af7dad53f12f242d5c",
"0x911548eb0f3bf353a54e03a3506c7c3e747470d6c201f03babbc07ff6e14cd6e",
"0x9a9d4618b12d36d17a63d48c5b5efc05b461feead124ddd86803d8bca4015248",
"0xa38173981f8eab96ee70cefe42735af0f574b7ef354565f2fea32a28e5ed9bd2"
].toSorted()
}
def "Skip duplicates"() {
setup:
def source1 = Mock(PendingTxesSource)
def source2 = Mock(PendingTxesSource)
when:
def aggregate = new AggregatedPendingTxes([source1, source2])
def values = aggregate.connect(Selector.empty)
.collectList().block(Duration.ofSeconds(1))
then:
1 * source1.connect(Selector.empty) >> Flux.fromIterable(
[
"0xa61bab14fc9720ea8725622688c2f964666d7c2afdae38af7dad53f12f242d5c",
"0x9a9d4618b12d36d17a63d48c5b5efc05b461feead124ddd86803d8bca4015248",
"0x911548eb0f3bf353a54e03a3506c7c3e747470d6c201f03babbc07ff6e14cd6e"
].collect { TransactionId.from(it) }
)
1 * source2.connect(Selector.empty) >> Flux.fromIterable(
[
"0x9a9d4618b12d36d17a63d48c5b5efc05b461feead124ddd86803d8bca4015248",
"0xa61bab14fc9720ea8725622688c2f964666d7c2afdae38af7dad53f12f242d5c",
"0xa38173981f8eab96ee70cefe42735af0f574b7ef354565f2fea32a28e5ed9bd2"
].collect { TransactionId.from(it) }
)
values.collect { it.toHex() }.toSorted() == [
"0xa61bab14fc9720ea8725622688c2f964666d7c2afdae38af7dad53f12f242d5c",
"0x911548eb0f3bf353a54e03a3506c7c3e747470d6c201f03babbc07ff6e14cd6e",
"0x9a9d4618b12d36d17a63d48c5b5efc05b461feead124ddd86803d8bca4015248",
"0xa38173981f8eab96ee70cefe42735af0f574b7ef354565f2fea32a28e5ed9bd2"
].toSorted()
}
}

View File

@@ -0,0 +1,125 @@
/**
* Copyright (c) 2022 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.ethereum.subscribe
import com.google.protobuf.ByteString
import io.emeraldpay.api.proto.BlockchainGrpc
import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.api.proto.ReactorBlockchainGrpc
import io.emeraldpay.dshackle.Chain
import io.emeraldpay.dshackle.upstream.Selector
import io.grpc.Channel
import io.grpc.ManagedChannel
import io.grpc.Server
import io.grpc.inprocess.InProcessChannelBuilder
import io.grpc.inprocess.InProcessServerBuilder
import io.grpc.stub.StreamObserver
import spock.lang.Specification
import java.time.Duration
class DshacklePendingTxesSourceSpec extends Specification {
def "Produces values"() {
setup:
BlockchainOuterClass.NativeSubscribeRequest receivedRequest
String uniqueName = InProcessServerBuilder.generateName();
Server server = InProcessServerBuilder.forName(uniqueName)
.directExecutor()
.addService(new BlockchainGrpc.BlockchainImplBase() {
@Override
void nativeSubscribe(BlockchainOuterClass.NativeSubscribeRequest request, StreamObserver<BlockchainOuterClass.NativeSubscribeReplyItem> responseObserver) {
receivedRequest = request
[
'"0xa61bab14fc9720ea8725622688c2f964666d7c2afdae38af7dad53f12f242d5c"',
'"0x911548eb0f3bf353a54e03a3506c7c3e747470d6c201f03babbc07ff6e14cd6e"',
'"0x9a9d4618b12d36d17a63d48c5b5efc05b461feead124ddd86803d8bca4015248"',
'"0xa38173981f8eab96ee70cefe42735af0f574b7ef354565f2fea32a28e5ed9bd2"'
]
.collect { it.bytes }
.collect {
BlockchainOuterClass.NativeSubscribeReplyItem.newBuilder()
.setPayload(ByteString.copyFrom(it))
.build()
}.forEach {
responseObserver.onNext(it)
}
responseObserver.onCompleted()
}
})
.build().start()
ManagedChannel channel = InProcessChannelBuilder.forName(uniqueName)
.directExecutor()
.build()
def remote = ReactorBlockchainGrpc.newReactorStub(channel)
def pending = new DshacklePendingTxesSource(Chain.ETHEREUM, remote)
when:
pending.available = true
def txes = pending.connect(Selector.empty).take(3)
.collectList().block(Duration.ofSeconds(1))
then:
receivedRequest != null
receivedRequest.chainValue == Chain.ETHEREUM.id
receivedRequest.method == "newPendingTransactions"
txes.collect {it.toHex() } == [
"0xa61bab14fc9720ea8725622688c2f964666d7c2afdae38af7dad53f12f242d5c",
"0x911548eb0f3bf353a54e03a3506c7c3e747470d6c201f03babbc07ff6e14cd6e",
"0x9a9d4618b12d36d17a63d48c5b5efc05b461feead124ddd86803d8bca4015248",
]
}
def "available when method is enabled on remote"() {
setup:
def pending = new DshacklePendingTxesSource(Chain.ETHEREUM, ReactorBlockchainGrpc.newReactorStub(Stub(Channel)))
pending.available = false
when:
pending.update(
BlockchainOuterClass.DescribeChain.newBuilder()
.addAllSupportedMethods(["newPendingTransactions"])
.build()
)
then:
pending.available
}
def "unavailable when not method is enabled on remote"() {
setup:
def pending = new DshacklePendingTxesSource(Chain.ETHEREUM, ReactorBlockchainGrpc.newReactorStub(Stub(Channel)))
pending.available = false
when:
pending.update(
BlockchainOuterClass.DescribeChain.newBuilder()
.addAllSupportedMethods(["other_method"])
.build()
)
then:
!pending.available
when: "It was enabled before getting an update"
pending.available = true
pending.update(
BlockchainOuterClass.DescribeChain.newBuilder()
.addAllSupportedMethods(["other_method"])
.build()
)
then:
!pending.available
}
}

View File

@@ -0,0 +1,50 @@
/**
* Copyright (c) 2022 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.ethereum.subscribe
import io.emeraldpay.dshackle.upstream.Selector
import io.emeraldpay.dshackle.upstream.ethereum.WsSubscriptions
import reactor.core.publisher.Flux
import spock.lang.Specification
import java.time.Duration
class WebsocketPendingTxesSpec extends Specification {
def "Produces values"() {
setup:
def responses = [
'"0xa61bab14fc9720ea8725622688c2f964666d7c2afdae38af7dad53f12f242d5c"',
'"0x911548eb0f3bf353a54e03a3506c7c3e747470d6c201f03babbc07ff6e14cd6e"',
'"0x67f22a3b441ea312306f97694ca8159f8d6faaccf0f5ce6442c84b13991f1d23"',
'"0xa38173981f8eab96ee70cefe42735af0f574b7ef354565f2fea32a28e5ed9bd2"',
].collect { it.bytes }
def ws = Mock(WsSubscriptions)
def pending = new WebsocketPendingTxes(ws)
when:
def txes = pending.connect(Selector.empty).take(3)
.collectList().block(Duration.ofSeconds(1))
then:
1 * ws.subscribe("newPendingTransactions") >> Flux.fromIterable(responses)
txes.collect {it.toHex() } == [
"0xa61bab14fc9720ea8725622688c2f964666d7c2afdae38af7dad53f12f242d5c",
"0x911548eb0f3bf353a54e03a3506c7c3e747470d6c201f03babbc07ff6e14cd6e",
"0x67f22a3b441ea312306f97694ca8159f8d6faaccf0f5ce6442c84b13991f1d23",
]
}
}

View File

@@ -1,6 +1,6 @@
package io.emeraldpay.dshackle.upstream.rpcclient
import io.emeraldpay.dshackle.upstream.ethereum.WsConnection
import io.emeraldpay.dshackle.upstream.ethereum.WsConnectionImpl
import reactor.core.Exceptions
import spock.lang.Specification
@@ -10,7 +10,7 @@ class JsonRpcWsClientSpec extends Specification {
def "Produce error if WS is not connected"() {
setup:
def ws = Mock(WsConnection)
def ws = Mock(WsConnectionImpl)
def client = new JsonRpcWsClient(ws)
when:
client.read(new JsonRpcRequest("foo_bar", [], 1))