ws subscription refactoring
added support of pending transactions subscription
This commit is contained in:
@@ -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))
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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"))
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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]
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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 ->
|
||||
|
||||
@@ -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",
|
||||
@@ -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
|
||||
])
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
@@ -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)
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user