solution: connects to remote dshackle
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -3,3 +3,4 @@ build/
|
|||||||
out/
|
out/
|
||||||
*.iml
|
*.iml
|
||||||
*.yaml
|
*.yaml
|
||||||
|
testsetup/
|
||||||
@@ -44,7 +44,7 @@ configurations {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
compile "io.emeraldpay:emerald-grpc:0.4"
|
compile "io.emeraldpay:emerald-grpc:0.6-SNAPSHOT"
|
||||||
|
|
||||||
compile "io.grpc:grpc-protobuf:${grpcVersion}"
|
compile "io.grpc:grpc-protobuf:${grpcVersion}"
|
||||||
compile "io.grpc:grpc-stub:${grpcVersion}"
|
compile "io.grpc:grpc-stub:${grpcVersion}"
|
||||||
@@ -63,6 +63,7 @@ dependencies {
|
|||||||
compile 'io.projectreactor:reactor-core:3.2.9.RELEASE'
|
compile 'io.projectreactor:reactor-core:3.2.9.RELEASE'
|
||||||
compile 'io.projectreactor.addons:reactor-extra:3.2.3.RELEASE'
|
compile 'io.projectreactor.addons:reactor-extra:3.2.3.RELEASE'
|
||||||
compile 'io.projectreactor.kotlin:reactor-kotlin-extensions:1.0.0.M1'
|
compile 'io.projectreactor.kotlin:reactor-kotlin-extensions:1.0.0.M1'
|
||||||
|
compile 'com.salesforce.servicelibs:reactor-grpc:0.10.0'
|
||||||
|
|
||||||
compile 'org.yaml:snakeyaml:1.24'
|
compile 'org.yaml:snakeyaml:1.24'
|
||||||
|
|
||||||
@@ -93,7 +94,9 @@ dependencies {
|
|||||||
|
|
||||||
|
|
||||||
testCompile "org.codehaus.groovy:groovy:$groovyVersion"
|
testCompile "org.codehaus.groovy:groovy:$groovyVersion"
|
||||||
|
testCompile 'cglib:cglib-nodep:3.2.12'
|
||||||
testCompile "org.spockframework:spock-core:$spockVersion"
|
testCompile "org.spockframework:spock-core:$spockVersion"
|
||||||
|
testCompile "io.grpc:grpc-testing:${grpcVersion}"
|
||||||
}
|
}
|
||||||
|
|
||||||
compileKotlin {
|
compileKotlin {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package io.emeraldpay.dshackle.config;
|
package io.emeraldpay.dshackle.config;
|
||||||
|
|
||||||
import org.yaml.snakeyaml.TypeDescription;
|
import org.yaml.snakeyaml.TypeDescription;
|
||||||
import org.yaml.snakeyaml.Yaml;
|
import org.yaml.snakeyaml.introspector.GenericProperty;
|
||||||
import org.yaml.snakeyaml.introspector.MethodProperty;
|
import org.yaml.snakeyaml.introspector.MethodProperty;
|
||||||
import org.yaml.snakeyaml.introspector.Property;
|
import org.yaml.snakeyaml.introspector.Property;
|
||||||
import org.yaml.snakeyaml.introspector.PropertySubstitute;
|
import org.yaml.snakeyaml.introspector.PropertySubstitute;
|
||||||
@@ -12,6 +12,10 @@ import org.yaml.snakeyaml.nodes.ScalarNode;
|
|||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
import java.beans.IntrospectionException;
|
import java.beans.IntrospectionException;
|
||||||
import java.beans.PropertyDescriptor;
|
import java.beans.PropertyDescriptor;
|
||||||
|
import java.lang.annotation.Annotation;
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.lang.reflect.InvocationTargetException;
|
||||||
|
import java.lang.reflect.Type;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
@@ -201,6 +205,8 @@ public class UpstreamsConfig {
|
|||||||
public static class Endpoint {
|
public static class Endpoint {
|
||||||
private EndpointType type;
|
private EndpointType type;
|
||||||
private URI url;
|
private URI url;
|
||||||
|
private String host;
|
||||||
|
private int port;
|
||||||
@Nullable
|
@Nullable
|
||||||
private Auth auth;
|
private Auth auth;
|
||||||
private Boolean enabled = true;
|
private Boolean enabled = true;
|
||||||
@@ -248,6 +254,22 @@ public class UpstreamsConfig {
|
|||||||
public void setOrigin(@Nullable URI origin) {
|
public void setOrigin(@Nullable URI origin) {
|
||||||
this.origin = origin;
|
this.origin = origin;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getHost() {
|
||||||
|
return host;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setHost(String host) {
|
||||||
|
this.host = host;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getPort() {
|
||||||
|
return port;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPort(int port) {
|
||||||
|
this.port = port;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class Auth {
|
public static class Auth {
|
||||||
@@ -262,7 +284,12 @@ public class UpstreamsConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class BasicAuth extends Auth {
|
public static interface WithKey {
|
||||||
|
public String getKey();
|
||||||
|
public void setKey(String key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class BasicAuth extends Auth implements WithKey {
|
||||||
private String key;
|
private String key;
|
||||||
|
|
||||||
public String getKey() {
|
public String getKey() {
|
||||||
@@ -274,18 +301,72 @@ public class UpstreamsConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static class TlsAuth extends Auth implements WithKey {
|
||||||
|
private String ca;
|
||||||
|
private String certificate;
|
||||||
|
private String key;
|
||||||
|
|
||||||
|
public String getCa() {
|
||||||
|
return ca;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCa(String ca) {
|
||||||
|
this.ca = ca;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCertificate() {
|
||||||
|
return certificate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCertificate(String certificate) {
|
||||||
|
this.certificate = certificate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getKey() {
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setKey(String key) {
|
||||||
|
this.key = key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static class AuthYaml extends TypeDescription {
|
public static class AuthYaml extends TypeDescription {
|
||||||
|
|
||||||
public AuthYaml() {
|
public AuthYaml() {
|
||||||
super(Auth.class);
|
super(Auth.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Class getImpl() {
|
||||||
|
try {
|
||||||
|
Field f = TypeDescription.class.getDeclaredField("impl");
|
||||||
|
f.setAccessible(true);
|
||||||
|
return (Class) f.get(this);
|
||||||
|
} catch (NoSuchFieldException | IllegalAccessException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Property getProperty(String name) {
|
public Property getProperty(String name) {
|
||||||
if ("key".equals(name)) {
|
if ("key".equals(name)) {
|
||||||
try {
|
try {
|
||||||
return new MethodProperty(new PropertyDescriptor("key", BasicAuth.class, "getKey", "setKey"));
|
return new MethodProperty(new PropertyDescriptor("key", WithKey.class, "getKey", "setKey"));
|
||||||
|
} catch (IntrospectionException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ("ca".equals(name)) {
|
||||||
|
try {
|
||||||
|
return new MethodProperty(new PropertyDescriptor("ca", TlsAuth.class, "getCa", "setCa"));
|
||||||
|
} catch (IntrospectionException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ("certificate".equals(name)) {
|
||||||
|
try {
|
||||||
|
return new MethodProperty(new PropertyDescriptor("certificate", TlsAuth.class, "getCertificate", "setCertificate"));
|
||||||
} catch (IntrospectionException e) {
|
} catch (IntrospectionException e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
@@ -293,22 +374,28 @@ public class UpstreamsConfig {
|
|||||||
return super.getProperty(name);
|
return super.getProperty(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Optional<ScalarNode> getValue(MappingNode mappingNode, String key) {
|
||||||
|
return mappingNode.getValue()
|
||||||
|
.stream()
|
||||||
|
.filter((n) -> n.getKeyNode() instanceof ScalarNode && n.getValueNode() instanceof ScalarNode)
|
||||||
|
.filter((n) -> {
|
||||||
|
ScalarNode sn = (ScalarNode)n.getKeyNode();
|
||||||
|
return "type".equals(sn.getValue());
|
||||||
|
})
|
||||||
|
.map((n) -> (ScalarNode)n.getValueNode())
|
||||||
|
.findFirst();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Object newInstance(Node node) {
|
public Object newInstance(Node node) {
|
||||||
if (node instanceof MappingNode) {
|
if (node instanceof MappingNode) {
|
||||||
MappingNode mappingNode = (MappingNode)node;
|
MappingNode mappingNode = (MappingNode)node;
|
||||||
Optional<ScalarNode> type = mappingNode.getValue()
|
Optional<ScalarNode> type = getValue(mappingNode, "type");
|
||||||
.stream()
|
|
||||||
.filter((n) -> n.getKeyNode() instanceof ScalarNode && n.getValueNode() instanceof ScalarNode)
|
|
||||||
.filter((n) -> {
|
|
||||||
ScalarNode sn = (ScalarNode)n.getKeyNode();
|
|
||||||
return "type".equals(sn.getValue());
|
|
||||||
})
|
|
||||||
.map((n) -> (ScalarNode)n.getValueNode())
|
|
||||||
.findFirst();
|
|
||||||
if (type.isPresent()) {
|
if (type.isPresent()) {
|
||||||
if ("basic".equals(type.get().getValue())) {
|
if ("basic".equals(type.get().getValue())) {
|
||||||
return new BasicAuth();
|
return new BasicAuth();
|
||||||
|
} else if ("tls".equals(type.get().getValue())) {
|
||||||
|
return new TlsAuth();
|
||||||
} else {
|
} else {
|
||||||
throw new IllegalArgumentException("Unsupported auth type: " + type.get().getValue());
|
throw new IllegalArgumentException("Unsupported auth type: " + type.get().getValue());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,9 @@ class BlockchainRpc(
|
|||||||
@Autowired private val nativeCall: NativeCall,
|
@Autowired private val nativeCall: NativeCall,
|
||||||
@Autowired private val streamHead: StreamHead,
|
@Autowired private val streamHead: StreamHead,
|
||||||
@Autowired private val trackTx: TrackTx,
|
@Autowired private val trackTx: TrackTx,
|
||||||
@Autowired private val trackAddress: TrackAddress
|
@Autowired private val trackAddress: TrackAddress,
|
||||||
|
@Autowired private val describe: Describe,
|
||||||
|
@Autowired private val subscribeStatus: SubscribeStatus
|
||||||
): BlockchainGrpc.BlockchainImplBase() {
|
): BlockchainGrpc.BlockchainImplBase() {
|
||||||
|
|
||||||
private val log = LoggerFactory.getLogger(BlockchainRpc::class.java)
|
private val log = LoggerFactory.getLogger(BlockchainRpc::class.java)
|
||||||
@@ -56,4 +58,12 @@ class BlockchainRpc(
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun describe(request: BlockchainOuterClass.DescribeRequest, responseObserver: StreamObserver<BlockchainOuterClass.DescribeResponse>) {
|
||||||
|
describe.describe(request, responseObserver)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun subscribeStatus(request: BlockchainOuterClass.StatusRequest, responseObserver: StreamObserver<BlockchainOuterClass.ChainStatus>) {
|
||||||
|
subscribeStatus.subscribeStatus(request, responseObserver)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
43
src/main/kotlin/io/emeraldpay/dshackle/rpc/Describe.kt
Normal file
43
src/main/kotlin/io/emeraldpay/dshackle/rpc/Describe.kt
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
package io.emeraldpay.dshackle.rpc
|
||||||
|
|
||||||
|
import io.emeraldpay.api.proto.BlockchainOuterClass
|
||||||
|
import io.emeraldpay.api.proto.Common
|
||||||
|
import io.emeraldpay.dshackle.upstream.UpstreamAvailability
|
||||||
|
import io.emeraldpay.dshackle.upstream.ConfiguredUpstreams
|
||||||
|
import io.emeraldpay.dshackle.upstream.Upstreams
|
||||||
|
import io.grpc.stub.StreamObserver
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
|
|
||||||
|
@Service
|
||||||
|
class Describe(
|
||||||
|
@Autowired private val upstreams: Upstreams
|
||||||
|
) {
|
||||||
|
|
||||||
|
fun describe(request: BlockchainOuterClass.DescribeRequest, responseObserver: StreamObserver<BlockchainOuterClass.DescribeResponse>) {
|
||||||
|
val resp = BlockchainOuterClass.DescribeResponse.newBuilder()
|
||||||
|
upstreams.getAvailable().forEach { chain ->
|
||||||
|
upstreams.ethereumUpstream(chain).let { chainUpstreams ->
|
||||||
|
val quorum = chainUpstreams.getAll().map { u ->
|
||||||
|
if (u.getStatus() == UpstreamAvailability.OK) {
|
||||||
|
u.getOptions().quorum
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
}.sum()
|
||||||
|
val available = chainUpstreams.getAll().any { u ->
|
||||||
|
u.getStatus() == UpstreamAvailability.OK
|
||||||
|
}
|
||||||
|
resp.addChains(
|
||||||
|
BlockchainOuterClass.DescribeChain.newBuilder()
|
||||||
|
.setChain(Common.ChainRef.forNumber(chain.id))
|
||||||
|
.setQuorum(quorum)
|
||||||
|
.setAvailable(available)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
responseObserver.onNext(resp.build())
|
||||||
|
responseObserver.onCompleted()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ package io.emeraldpay.dshackle.rpc
|
|||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
import com.google.protobuf.ByteString
|
import com.google.protobuf.ByteString
|
||||||
import io.emeraldpay.api.proto.BlockchainOuterClass
|
import io.emeraldpay.api.proto.BlockchainOuterClass
|
||||||
|
import io.emeraldpay.dshackle.upstream.ConfiguredUpstreams
|
||||||
import io.emeraldpay.dshackle.upstream.Upstreams
|
import io.emeraldpay.dshackle.upstream.Upstreams
|
||||||
import io.emeraldpay.grpc.Chain
|
import io.emeraldpay.grpc.Chain
|
||||||
import io.grpc.stub.StreamObserver
|
import io.grpc.stub.StreamObserver
|
||||||
@@ -27,7 +28,7 @@ class NativeCall(
|
|||||||
if (chain == Chain.UNSPECIFIED) {
|
if (chain == Chain.UNSPECIFIED) {
|
||||||
throw Exception("Invalid chain id: ${request.chain.number}")
|
throw Exception("Invalid chain id: ${request.chain.number}")
|
||||||
}
|
}
|
||||||
val upstream = upstreams.ethereumUpstream(chain)?.api ?: throw Exception("Chain ${chain.id} is unavailable")
|
val upstream = upstreams.ethereumUpstream(chain)?.getApi() ?: throw Exception("Chain ${chain.id} is unavailable")
|
||||||
request.itemsList.toFlux()
|
request.itemsList.toFlux()
|
||||||
.map {
|
.map {
|
||||||
val method = it.target
|
val method = it.target
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package io.emeraldpay.dshackle.rpc
|
|||||||
|
|
||||||
import com.google.protobuf.ByteString
|
import com.google.protobuf.ByteString
|
||||||
import io.emeraldpay.api.proto.BlockchainOuterClass
|
import io.emeraldpay.api.proto.BlockchainOuterClass
|
||||||
|
import io.emeraldpay.dshackle.upstream.ConfiguredUpstreams
|
||||||
import io.emeraldpay.dshackle.upstream.Upstreams
|
import io.emeraldpay.dshackle.upstream.Upstreams
|
||||||
import io.emeraldpay.grpc.Chain
|
import io.emeraldpay.grpc.Chain
|
||||||
import io.grpc.stub.StreamObserver
|
import io.grpc.stub.StreamObserver
|
||||||
@@ -12,7 +13,6 @@ import org.springframework.beans.factory.annotation.Autowired
|
|||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
import reactor.core.publisher.toFlux
|
import reactor.core.publisher.toFlux
|
||||||
import java.lang.Exception
|
import java.lang.Exception
|
||||||
import java.util.*
|
|
||||||
import java.util.concurrent.ConcurrentLinkedQueue
|
import java.util.concurrent.ConcurrentLinkedQueue
|
||||||
import javax.annotation.PostConstruct
|
import javax.annotation.PostConstruct
|
||||||
import kotlin.collections.HashMap
|
import kotlin.collections.HashMap
|
||||||
@@ -28,7 +28,7 @@ class StreamHead(
|
|||||||
@PostConstruct
|
@PostConstruct
|
||||||
fun init() {
|
fun init() {
|
||||||
listOf(Chain.ETHEREUM, Chain.ETHEREUM_CLASSIC, Chain.TESTNET_MORDEN, Chain.TESTNET_KOVAN).forEach { chain ->
|
listOf(Chain.ETHEREUM, Chain.ETHEREUM_CLASSIC, Chain.TESTNET_MORDEN, Chain.TESTNET_KOVAN).forEach { chain ->
|
||||||
if (upstreams.ethereumUpstream(chain)?.head != null) {
|
if (upstreams.ethereumUpstream(chain)?.getHead() != null) {
|
||||||
clients[chain] = ConcurrentLinkedQueue()
|
clients[chain] = ConcurrentLinkedQueue()
|
||||||
subscribe(chain)
|
subscribe(chain)
|
||||||
}
|
}
|
||||||
@@ -36,7 +36,7 @@ class StreamHead(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun subscribe(chain: Chain) {
|
private fun subscribe(chain: Chain) {
|
||||||
upstreams.ethereumUpstream(chain)!!.head.getFlux()
|
upstreams.ethereumUpstream(chain)!!.getHead().getFlux()
|
||||||
.doOnComplete {
|
.doOnComplete {
|
||||||
log.info("Closing streams for ${chain.chainCode}")
|
log.info("Closing streams for ${chain.chainCode}")
|
||||||
clients.replace(chain, ConcurrentLinkedQueue())!!.forEach { client ->
|
clients.replace(chain, ConcurrentLinkedQueue())!!.forEach { client ->
|
||||||
@@ -68,7 +68,7 @@ class StreamHead(
|
|||||||
|
|
||||||
fun process(chain: Chain, client: StreamSender<BlockchainOuterClass.ChainHead>): Boolean {
|
fun process(chain: Chain, client: StreamSender<BlockchainOuterClass.ChainHead>): Boolean {
|
||||||
val upstream = upstreams.ethereumUpstream(chain) ?: return false
|
val upstream = upstreams.ethereumUpstream(chain) ?: return false
|
||||||
val head = upstream.head.getHead()
|
val head = upstream.getHead().getHead()
|
||||||
return head.map {
|
return head.map {
|
||||||
notify(chain, it, client)
|
notify(chain, it, client)
|
||||||
}.defaultIfEmpty(false).block()!!
|
}.defaultIfEmpty(false).block()!!
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package io.emeraldpay.dshackle.rpc
|
||||||
|
|
||||||
|
import io.emeraldpay.api.proto.BlockchainOuterClass
|
||||||
|
import io.emeraldpay.dshackle.upstream.Upstreams
|
||||||
|
import io.grpc.stub.StreamObserver
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
|
|
||||||
|
@Service
|
||||||
|
class SubscribeStatus(
|
||||||
|
@Autowired private val upstreams: Upstreams
|
||||||
|
) {
|
||||||
|
|
||||||
|
fun subscribeStatus(request: BlockchainOuterClass.StatusRequest, responseObserver: StreamObserver<BlockchainOuterClass.ChainStatus>) {
|
||||||
|
upstreams.getAvailable().forEach { chain ->
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -2,12 +2,12 @@ package io.emeraldpay.dshackle.rpc
|
|||||||
|
|
||||||
import io.emeraldpay.api.proto.BlockchainOuterClass
|
import io.emeraldpay.api.proto.BlockchainOuterClass
|
||||||
import io.emeraldpay.api.proto.Common
|
import io.emeraldpay.api.proto.Common
|
||||||
|
import io.emeraldpay.dshackle.upstream.ConfiguredUpstreams
|
||||||
import io.emeraldpay.dshackle.upstream.Upstreams
|
import io.emeraldpay.dshackle.upstream.Upstreams
|
||||||
import io.emeraldpay.grpc.Chain
|
import io.emeraldpay.grpc.Chain
|
||||||
import io.grpc.stub.StreamObserver
|
import io.grpc.stub.StreamObserver
|
||||||
import io.infinitape.etherjar.domain.Address
|
import io.infinitape.etherjar.domain.Address
|
||||||
import io.infinitape.etherjar.domain.Wei
|
import io.infinitape.etherjar.domain.Wei
|
||||||
import io.infinitape.etherjar.rpc.Batch
|
|
||||||
import io.infinitape.etherjar.rpc.Commands
|
import io.infinitape.etherjar.rpc.Commands
|
||||||
import io.infinitape.etherjar.rpc.json.BlockTag
|
import io.infinitape.etherjar.rpc.json.BlockTag
|
||||||
import org.springframework.beans.factory.annotation.Autowired
|
import org.springframework.beans.factory.annotation.Autowired
|
||||||
@@ -17,14 +17,11 @@ import reactor.core.publisher.Flux
|
|||||||
import reactor.core.publisher.Mono
|
import reactor.core.publisher.Mono
|
||||||
import reactor.core.publisher.toFlux
|
import reactor.core.publisher.toFlux
|
||||||
import reactor.math.sum
|
import reactor.math.sum
|
||||||
import reactor.util.function.Tuple2
|
|
||||||
import reactor.util.function.Tuples
|
|
||||||
import java.lang.Exception
|
import java.lang.Exception
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.ConcurrentLinkedQueue
|
import java.util.concurrent.ConcurrentLinkedQueue
|
||||||
import java.util.concurrent.Future
|
|
||||||
import javax.annotation.PostConstruct
|
import javax.annotation.PostConstruct
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@@ -40,7 +37,7 @@ class TrackAddress(
|
|||||||
fun init() {
|
fun init() {
|
||||||
allChains.forEach { chain ->
|
allChains.forEach { chain ->
|
||||||
clients[chain] = ConcurrentLinkedQueue()
|
clients[chain] = ConcurrentLinkedQueue()
|
||||||
upstreams.ethereumUpstream(chain)?.head?.let { head ->
|
upstreams.ethereumUpstream(chain)?.getHead()?.let { head ->
|
||||||
head.getFlux().subscribe { verifyAll(chain) }
|
head.getFlux().subscribe { verifyAll(chain) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -120,25 +117,18 @@ class TrackAddress(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun verify(chain: Chain, group: List<TrackedAddress>): Flux<TrackedAddress> {
|
private fun verify(chain: Chain, group: List<TrackedAddress>): Flux<TrackedAddress> {
|
||||||
val up = upstreams.ethereumUpstream(chain)!!
|
val up = upstreams.ethereumUpstream(chain)
|
||||||
return group.toFlux()
|
return group.toFlux()
|
||||||
.reduce<Tuple2<Batch, ArrayList<Update>>>(Tuples.of(Batch(), ArrayList())) { batch, a ->
|
.flatMap { a ->
|
||||||
val f = batch.t1.add(Commands.eth().getBalance(a.address, BlockTag.LATEST));
|
up.getApi()
|
||||||
batch.t2.add(Update(a, f))
|
.executeAndConvert(Commands.eth().getBalance(a.address, BlockTag.LATEST))
|
||||||
batch
|
.map { Update(a, it) }
|
||||||
}
|
|
||||||
.flatMap {
|
|
||||||
Mono.fromCompletionStage(up.api.execute(it.t1))
|
|
||||||
.thenReturn(it.t2)
|
|
||||||
}
|
|
||||||
.flatMapMany {
|
|
||||||
it.toFlux()
|
|
||||||
}
|
}
|
||||||
.filter {
|
.filter {
|
||||||
it.addr.balance == null || it.addr.balance != it.value.get()
|
it.addr.balance == null || it.addr.balance != it.value
|
||||||
}
|
}
|
||||||
.doOnNext {
|
.doOnNext {
|
||||||
it.addr.balance = it.value.get()
|
it.addr.balance = it.value
|
||||||
}
|
}
|
||||||
.map {
|
.map {
|
||||||
it.addr
|
it.addr
|
||||||
@@ -163,7 +153,7 @@ class TrackAddress(
|
|||||||
return sent
|
return sent
|
||||||
}
|
}
|
||||||
|
|
||||||
class Update(val addr: TrackedAddress, val value: Future<Wei>)
|
class Update(val addr: TrackedAddress, val value: Wei)
|
||||||
|
|
||||||
class TrackedAddress(val chain: Chain,
|
class TrackedAddress(val chain: Chain,
|
||||||
val stream: StreamSender<BlockchainOuterClass.AddressBalance>,
|
val stream: StreamSender<BlockchainOuterClass.AddressBalance>,
|
||||||
|
|||||||
@@ -3,19 +3,17 @@ package io.emeraldpay.dshackle.rpc
|
|||||||
import com.google.protobuf.ByteString
|
import com.google.protobuf.ByteString
|
||||||
import io.emeraldpay.api.proto.BlockchainOuterClass
|
import io.emeraldpay.api.proto.BlockchainOuterClass
|
||||||
import io.emeraldpay.api.proto.Common
|
import io.emeraldpay.api.proto.Common
|
||||||
|
import io.emeraldpay.dshackle.upstream.ConfiguredUpstreams
|
||||||
import io.emeraldpay.dshackle.upstream.Upstreams
|
import io.emeraldpay.dshackle.upstream.Upstreams
|
||||||
import io.emeraldpay.grpc.Chain
|
import io.emeraldpay.grpc.Chain
|
||||||
import io.grpc.stub.StreamObserver
|
|
||||||
import io.infinitape.etherjar.domain.BlockHash
|
import io.infinitape.etherjar.domain.BlockHash
|
||||||
import io.infinitape.etherjar.domain.TransactionId
|
import io.infinitape.etherjar.domain.TransactionId
|
||||||
import io.infinitape.etherjar.rpc.Batch
|
|
||||||
import io.infinitape.etherjar.rpc.Commands
|
import io.infinitape.etherjar.rpc.Commands
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import org.springframework.beans.factory.annotation.Autowired
|
import org.springframework.beans.factory.annotation.Autowired
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
import reactor.core.publisher.Mono
|
import reactor.core.publisher.Mono
|
||||||
import reactor.core.publisher.toFlux
|
import reactor.core.publisher.toFlux
|
||||||
import reactor.kotlin.core.publisher.switchIfEmpty
|
|
||||||
import java.lang.Exception
|
import java.lang.Exception
|
||||||
import java.math.BigInteger
|
import java.math.BigInteger
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
@@ -37,7 +35,7 @@ class TrackTx(
|
|||||||
fun init() {
|
fun init() {
|
||||||
listOf(Chain.TESTNET_MORDEN, Chain.ETHEREUM_CLASSIC, Chain.ETHEREUM, Chain.TESTNET_KOVAN).forEach { chain ->
|
listOf(Chain.TESTNET_MORDEN, Chain.ETHEREUM_CLASSIC, Chain.ETHEREUM, Chain.TESTNET_KOVAN).forEach { chain ->
|
||||||
clients[chain] = ConcurrentLinkedQueue()
|
clients[chain] = ConcurrentLinkedQueue()
|
||||||
upstreams.ethereumUpstream(chain)?.head?.let { head ->
|
upstreams.ethereumUpstream(chain)?.getHead()?.let { head ->
|
||||||
head.getFlux().subscribe { verifyAll(chain) }
|
head.getFlux().subscribe { verifyAll(chain) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -63,9 +61,9 @@ class TrackTx(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun loadWeight(tx: TrackedTx): Mono<TrackedTx> {
|
private fun loadWeight(tx: TrackedTx): Mono<TrackedTx> {
|
||||||
val batch = Batch()
|
val upstream = upstreams.ethereumUpstream(tx.chain)
|
||||||
val execution = Mono
|
return upstream.getApi()
|
||||||
.fromCompletionStage(batch.add(Commands.eth().getBlock(tx.status.blockHash)))
|
.executeAndConvert(Commands.eth().getBlock(tx.status.blockHash))
|
||||||
.map { block ->
|
.map { block ->
|
||||||
if (block != null && block.number != null && block.totalDifficulty != null) {
|
if (block != null && block.number != null && block.totalDifficulty != null) {
|
||||||
tx.withStatus(
|
tx.withStatus(
|
||||||
@@ -78,18 +76,14 @@ class TrackTx(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val upstream = upstreams.ethereumUpstream(tx.chain)!!
|
|
||||||
upstream.api.execute(batch)
|
|
||||||
return execution
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun verify(tx: TrackedTx): Boolean {
|
private fun verify(tx: TrackedTx): Boolean {
|
||||||
val found = tx.status.found
|
val found = tx.status.found
|
||||||
val mined = tx.status.mined
|
val mined = tx.status.mined
|
||||||
val batch = Batch()
|
val upstream = upstreams.ethereumUpstream(tx.chain)
|
||||||
val execution = Mono.fromCompletionStage(batch.add(Commands.eth().getTransaction(tx.txid)))
|
val execution = upstream.getApi()
|
||||||
val upstream = upstreams.ethereumUpstream(tx.chain)!!
|
.executeAndConvert(Commands.eth().getTransaction(tx.txid))
|
||||||
upstream.api.execute(batch)
|
|
||||||
val update = execution.flatMap {
|
val update = execution.flatMap {
|
||||||
if (it.blockNumber != null
|
if (it.blockNumber != null
|
||||||
&& it.blockHash != null && it.blockHash != ZERO_BLOCK) {
|
&& it.blockHash != null && it.blockHash != ZERO_BLOCK) {
|
||||||
@@ -100,7 +94,7 @@ class TrackTx(
|
|||||||
mined = true,
|
mined = true,
|
||||||
confirmation = 1
|
confirmation = 1
|
||||||
)
|
)
|
||||||
return@flatMap upstream.head.getHead().map { head ->
|
return@flatMap upstream.getHead().getHead().map { head ->
|
||||||
tx.withStatus(
|
tx.withStatus(
|
||||||
confirmation = head.number - tx.status.height!! + 1
|
confirmation = head.number - tx.status.height!! + 1
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package io.emeraldpay.dshackle.upstream
|
||||||
|
|
||||||
|
abstract class AggregatedUpstreams {
|
||||||
|
|
||||||
|
abstract fun getAll(): List<Upstream>
|
||||||
|
abstract fun addUpstream(upstream: Upstream)
|
||||||
|
abstract fun getApis(quorum: Int): Iterator<EthereumApi>
|
||||||
|
abstract fun getApi(): EthereumApi
|
||||||
|
abstract fun getHead(): EthereumHead
|
||||||
|
|
||||||
|
class SingleApi(
|
||||||
|
private val quorumApi: QuorumApi
|
||||||
|
): Iterator<EthereumApi> {
|
||||||
|
|
||||||
|
private var consumed = false
|
||||||
|
|
||||||
|
override fun hasNext(): Boolean {
|
||||||
|
return !consumed && quorumApi.hasNext()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun next(): EthereumApi {
|
||||||
|
consumed = true
|
||||||
|
return quorumApi.next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class QuorumApi(
|
||||||
|
private val apis: List<Upstream>,
|
||||||
|
private val quorum: Int,
|
||||||
|
private var pos: Int
|
||||||
|
): Iterator<EthereumApi> {
|
||||||
|
|
||||||
|
private var consumed = 0
|
||||||
|
|
||||||
|
override fun hasNext(): Boolean {
|
||||||
|
return consumed < quorum
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun next(): EthereumApi {
|
||||||
|
val start = pos
|
||||||
|
while (pos < start + apis.size) {
|
||||||
|
val api = apis[pos++ % apis.size]
|
||||||
|
if (api.isAvailable()) {
|
||||||
|
consumed++
|
||||||
|
return api.getApi()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw IllegalStateException("No upstream API available")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
package io.emeraldpay.dshackle.upstream
|
|
||||||
|
|
||||||
import io.emeraldpay.grpc.Chain
|
|
||||||
import io.infinitape.etherjar.domain.TransactionId
|
|
||||||
import io.infinitape.etherjar.rpc.json.BlockJson
|
|
||||||
import org.slf4j.LoggerFactory
|
|
||||||
import java.lang.IllegalStateException
|
|
||||||
import java.time.Duration
|
|
||||||
|
|
||||||
class ChainConnect(
|
|
||||||
val chain: Chain,
|
|
||||||
val upstreams: List<Upstream>
|
|
||||||
) {
|
|
||||||
|
|
||||||
private val log = LoggerFactory.getLogger(ChainConnect::class.java)
|
|
||||||
private var seq = 0
|
|
||||||
|
|
||||||
val head: EthereumHead = if (upstreams.size == 1) {
|
|
||||||
upstreams.first().head
|
|
||||||
} else {
|
|
||||||
EthereumHeadMerge(upstreams.map { it.head })
|
|
||||||
}
|
|
||||||
|
|
||||||
val api: EthereumApi
|
|
||||||
get() {
|
|
||||||
return getApis(1).next()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getApis(quorum: Int): Iterator<EthereumApi> {
|
|
||||||
val i = seq++
|
|
||||||
if (seq >= Int.MAX_VALUE / 2) {
|
|
||||||
seq = 0
|
|
||||||
}
|
|
||||||
return QuorumApi(upstreams, 1, seq)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun printStatus() {
|
|
||||||
var height: Long = -1
|
|
||||||
try {
|
|
||||||
height = head.getHead().block(Duration.ofSeconds(1))?.number ?: -1
|
|
||||||
} catch (e: Exception) { }
|
|
||||||
val statuses = upstreams.map { it.getStatus() }
|
|
||||||
.groupBy { it }
|
|
||||||
.map { "${it.key.name}/${it.value.size}" }
|
|
||||||
.joinToString(",")
|
|
||||||
|
|
||||||
log.info("State of ${chain.chainCode}: height=$height, status=$statuses")
|
|
||||||
}
|
|
||||||
|
|
||||||
class SingleApi(
|
|
||||||
private val quorumApi: QuorumApi
|
|
||||||
): Iterator<EthereumApi> {
|
|
||||||
|
|
||||||
private var consumed = false
|
|
||||||
|
|
||||||
override fun hasNext(): Boolean {
|
|
||||||
return !consumed && quorumApi.hasNext()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun next(): EthereumApi {
|
|
||||||
consumed = true
|
|
||||||
return quorumApi.next()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class QuorumApi(
|
|
||||||
private val apis: List<Upstream>,
|
|
||||||
private val quorum: Int,
|
|
||||||
private var pos: Int
|
|
||||||
): Iterator<EthereumApi> {
|
|
||||||
|
|
||||||
private var consumed = 0
|
|
||||||
|
|
||||||
override fun hasNext(): Boolean {
|
|
||||||
return consumed < quorum
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun next(): EthereumApi {
|
|
||||||
val start = pos
|
|
||||||
while (pos < start + apis.size) {
|
|
||||||
val api = apis[pos++ % apis.size]
|
|
||||||
if (api.isAvailable()) {
|
|
||||||
consumed++
|
|
||||||
return api.api
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw IllegalStateException("No upstream API available")
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
package io.emeraldpay.dshackle.upstream
|
||||||
|
|
||||||
|
import io.emeraldpay.grpc.Chain
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import java.lang.IllegalStateException
|
||||||
|
import java.time.Duration
|
||||||
|
|
||||||
|
class ChainUpstreams (
|
||||||
|
val chain: Chain,
|
||||||
|
private val upstreams: MutableList<Upstream>
|
||||||
|
) : AggregatedUpstreams() {
|
||||||
|
|
||||||
|
private val log = LoggerFactory.getLogger(ChainUpstreams::class.java)
|
||||||
|
private var seq = 0
|
||||||
|
private var head: EthereumHead
|
||||||
|
|
||||||
|
init {
|
||||||
|
head = updateHead()
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun updateHead(): EthereumHead {
|
||||||
|
return if (upstreams.size == 1) {
|
||||||
|
upstreams.first().getHead()
|
||||||
|
} else {
|
||||||
|
EthereumHeadMerge(upstreams.map { it.getHead() })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getAll(): List<Upstream> {
|
||||||
|
return upstreams
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun addUpstream(upstream: Upstream) {
|
||||||
|
upstreams.add(upstream)
|
||||||
|
head = updateHead()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getApis(quorum: Int): Iterator<EthereumApi> {
|
||||||
|
val i = seq++
|
||||||
|
if (seq >= Int.MAX_VALUE / 2) {
|
||||||
|
seq = 0
|
||||||
|
}
|
||||||
|
return QuorumApi(upstreams, 1, seq)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getApi(): EthereumApi {
|
||||||
|
return getApis(1).next()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getHead(): EthereumHead {
|
||||||
|
return head
|
||||||
|
}
|
||||||
|
|
||||||
|
fun printStatus() {
|
||||||
|
var height: Long = -1
|
||||||
|
try {
|
||||||
|
height = head.getHead().block(Duration.ofSeconds(1))?.number ?: -1
|
||||||
|
} catch (e: IllegalStateException) {
|
||||||
|
//timout
|
||||||
|
} catch (e: Exception) {
|
||||||
|
log.warn("Head processing error: ${e.javaClass} ${e.message}")
|
||||||
|
}
|
||||||
|
val statuses = upstreams.map { it.getStatus() }
|
||||||
|
.groupBy { it }
|
||||||
|
.map { "${it.key.name}/${it.value.size}" }
|
||||||
|
.joinToString(",")
|
||||||
|
|
||||||
|
log.info("State of ${chain.chainCode}: height=$height, status=$statuses")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
package io.emeraldpay.dshackle.upstream
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
|
import io.emeraldpay.dshackle.config.UpstreamsConfig
|
||||||
|
import io.emeraldpay.dshackle.config.UpstreamsConfigReader
|
||||||
|
import io.emeraldpay.grpc.Chain
|
||||||
|
import io.infinitape.etherjar.rpc.DefaultRpcClient
|
||||||
|
import io.infinitape.etherjar.rpc.transport.DefaultRpcTransport
|
||||||
|
import org.apache.commons.lang3.StringUtils
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired
|
||||||
|
import org.springframework.core.env.Environment
|
||||||
|
import org.springframework.scheduling.annotation.Scheduled
|
||||||
|
import org.springframework.stereotype.Repository
|
||||||
|
import reactor.core.publisher.toFlux
|
||||||
|
import java.io.File
|
||||||
|
import java.net.URI
|
||||||
|
import java.util.*
|
||||||
|
import javax.annotation.PostConstruct
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
open class ConfiguredUpstreams(
|
||||||
|
@Autowired val env: Environment,
|
||||||
|
@Autowired private val objectMapper: ObjectMapper
|
||||||
|
) : Upstreams {
|
||||||
|
|
||||||
|
private val log = LoggerFactory.getLogger(ConfiguredUpstreams::class.java)
|
||||||
|
private val chainMapping = HashMap<Chain, ChainUpstreams>()
|
||||||
|
|
||||||
|
private val chainNames = mapOf(
|
||||||
|
"ethereum" to Chain.ETHEREUM,
|
||||||
|
"ethereum-classic" to Chain.ETHEREUM_CLASSIC,
|
||||||
|
"eth" to Chain.ETHEREUM,
|
||||||
|
"etc" to Chain.ETHEREUM_CLASSIC,
|
||||||
|
"morden" to Chain.TESTNET_MORDEN,
|
||||||
|
"kovan" to Chain.TESTNET_KOVAN
|
||||||
|
)
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
|
fun start() {
|
||||||
|
val config = readConfig()
|
||||||
|
val defaultOptions = buildDefaultOptions(config)
|
||||||
|
val groups = HashMap<Chain, ArrayList<Upstream>>()
|
||||||
|
config.upstreams.forEach { up ->
|
||||||
|
if (up.provider == "dshackle") {
|
||||||
|
buildGrpcUpstream(up)
|
||||||
|
} else {
|
||||||
|
buildEthereumUpstream(up, defaultOptions, groups)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
groups.forEach { chain, group ->
|
||||||
|
chainMapping[chain] = ChainUpstreams(chain, group)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun readConfig(): UpstreamsConfig {
|
||||||
|
val path = env.getProperty("upstreams.config")
|
||||||
|
if (StringUtils.isEmpty(path)) {
|
||||||
|
log.error("Path to upstreams is not set (upstreams.config)")
|
||||||
|
System.exit(1)
|
||||||
|
}
|
||||||
|
val upstreamConfig = File(path!!)
|
||||||
|
val ok = upstreamConfig.exists() && upstreamConfig.isFile
|
||||||
|
if (!ok) {
|
||||||
|
log.error("Unable to setup upstreams from ${upstreamConfig.path}")
|
||||||
|
System.exit(1)
|
||||||
|
}
|
||||||
|
log.info("Read upstream configuration from ${upstreamConfig.path}")
|
||||||
|
val reader = UpstreamsConfigReader()
|
||||||
|
return reader.read(upstreamConfig.inputStream())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildDefaultOptions(config: UpstreamsConfig): HashMap<Chain, UpstreamsConfig.Options> {
|
||||||
|
val defaultOptions = HashMap<Chain, UpstreamsConfig.Options>()
|
||||||
|
config.defaultOptions.forEach { df ->
|
||||||
|
df.chains.forEach { chainName ->
|
||||||
|
chainNames[chainName]?.let { chain ->
|
||||||
|
var current = defaultOptions[chain]
|
||||||
|
if (current == null) {
|
||||||
|
current = df.options
|
||||||
|
} else {
|
||||||
|
current = current.merge(df.options)
|
||||||
|
}
|
||||||
|
defaultOptions[chain] = current
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return defaultOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildEthereumUpstream(up: UpstreamsConfig.Upstream,
|
||||||
|
defaultOptions: HashMap<Chain, UpstreamsConfig.Options>,
|
||||||
|
groups: HashMap<Chain, ArrayList<Upstream>>) {
|
||||||
|
val chain = chainNames[up.chain] ?: return
|
||||||
|
var rpcApi: EthereumApi? = null
|
||||||
|
var wsApi: EthereumWs? = null
|
||||||
|
val urls = ArrayList<URI>()
|
||||||
|
up.endpoints.forEach { endpoint ->
|
||||||
|
if (endpoint.type == UpstreamsConfig.EndpointType.JSON_RPC) {
|
||||||
|
rpcApi = EthereumApi(
|
||||||
|
DefaultRpcClient(DefaultRpcTransport(endpoint.url)),
|
||||||
|
objectMapper,
|
||||||
|
chain
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (endpoint.type == UpstreamsConfig.EndpointType.WEBSOCKET) {
|
||||||
|
wsApi = EthereumWs(
|
||||||
|
endpoint.url,
|
||||||
|
endpoint.origin ?: URI("http://localhost")
|
||||||
|
)
|
||||||
|
wsApi!!.connect()
|
||||||
|
}
|
||||||
|
urls.add(endpoint.url)
|
||||||
|
}
|
||||||
|
val options = (up.options ?: UpstreamsConfig.Options())
|
||||||
|
.merge(defaultOptions[chain])
|
||||||
|
.merge(UpstreamsConfig.Options.getDefaults())
|
||||||
|
if (rpcApi != null) {
|
||||||
|
log.info("Using ${chain.chainName} upstream, at ${urls.joinToString()}")
|
||||||
|
val current = groups[chain] ?: ArrayList()
|
||||||
|
current.add(EthereumUpstream(chain, rpcApi!!, wsApi, options))
|
||||||
|
groups[chain] = current
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildGrpcUpstream(up: UpstreamsConfig.Upstream) {
|
||||||
|
if (up.endpoints.size == 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val options = (up.options ?: UpstreamsConfig.Options())
|
||||||
|
.merge(UpstreamsConfig.Options.getDefaults())
|
||||||
|
|
||||||
|
val endpoint = up.endpoints.first()
|
||||||
|
if (endpoint.type == UpstreamsConfig.EndpointType.DSHACKLE) {
|
||||||
|
val ds = GrpcUpstreams(
|
||||||
|
endpoint.host,
|
||||||
|
endpoint.port ?: 443,
|
||||||
|
objectMapper,
|
||||||
|
options
|
||||||
|
)
|
||||||
|
log.info("Using ALL CHAINS (gRPC) upstream, at ${endpoint.host}:${endpoint.port}")
|
||||||
|
ds.start()
|
||||||
|
.flatMapMany {
|
||||||
|
it.toFlux()
|
||||||
|
}
|
||||||
|
.subscribe {
|
||||||
|
log.info("Subscribed to $it through gRPC at ${endpoint.host}:${endpoint.port}")
|
||||||
|
ethereumUpstream(it).addUpstream(ds.getOrCreate(it))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun ethereumUpstream(chain: Chain): ChainUpstreams {
|
||||||
|
val current = chainMapping[chain]
|
||||||
|
if (current == null) {
|
||||||
|
val created = ChainUpstreams(chain, ArrayList<Upstream>())
|
||||||
|
chainMapping[chain] = created
|
||||||
|
return created
|
||||||
|
}
|
||||||
|
return current
|
||||||
|
}
|
||||||
|
|
||||||
|
@Scheduled(fixedRate = 15000)
|
||||||
|
fun printStatuses() {
|
||||||
|
chainMapping.forEach { it.value.printStatus() }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getAvailable(): List<Chain> {
|
||||||
|
return Collections.unmodifiableList(chainMapping.keys.toList())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,24 +3,25 @@ package io.emeraldpay.dshackle.upstream
|
|||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
import io.emeraldpay.grpc.Chain
|
import io.emeraldpay.grpc.Chain
|
||||||
import io.infinitape.etherjar.hex.HexQuantity
|
import io.infinitape.etherjar.hex.HexQuantity
|
||||||
import io.infinitape.etherjar.rpc.Batch
|
import io.infinitape.etherjar.rpc.*
|
||||||
import io.infinitape.etherjar.rpc.RpcCall
|
|
||||||
import io.infinitape.etherjar.rpc.RpcClient
|
|
||||||
import io.infinitape.etherjar.rpc.RpcException
|
|
||||||
import io.infinitape.etherjar.rpc.json.ResponseJson
|
import io.infinitape.etherjar.rpc.json.ResponseJson
|
||||||
import io.infinitape.etherjar.rpc.transport.BatchStatus
|
import io.infinitape.etherjar.rpc.transport.BatchStatus
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
|
import reactor.core.publisher.Flux
|
||||||
import reactor.core.publisher.Mono
|
import reactor.core.publisher.Mono
|
||||||
|
import reactor.core.publisher.toFlux
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.CompletableFuture
|
import java.util.concurrent.CompletableFuture
|
||||||
|
|
||||||
class EthereumApi(
|
open class EthereumApi(
|
||||||
private val rpcClient: RpcClient,
|
val rpcClient: RpcClient,
|
||||||
private val objectMapper: ObjectMapper,
|
private val objectMapper: ObjectMapper,
|
||||||
private val chain: Chain
|
private val chain: Chain
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
private val jacksonRpcConverter = JacksonRpcConverter(objectMapper)
|
||||||
|
|
||||||
private val timeout = Duration.ofSeconds(5)
|
private val timeout = Duration.ofSeconds(5)
|
||||||
private val log = LoggerFactory.getLogger(EthereumApi::class.java)
|
private val log = LoggerFactory.getLogger(EthereumApi::class.java)
|
||||||
var ws: EthereumWs? = null
|
var ws: EthereumWs? = null
|
||||||
@@ -65,13 +66,19 @@ class EthereumApi(
|
|||||||
"eth_accounts"
|
"eth_accounts"
|
||||||
)
|
)
|
||||||
|
|
||||||
fun execute(batch: Batch): CompletableFuture<BatchStatus> {
|
open fun <JS, RS> executeAndConvert(rpcCall: RpcCall<JS, RS>): Mono<RS> {
|
||||||
return rpcClient.execute(batch)
|
return execute(0, rpcCall.method, rpcCall.params as List<Any>)
|
||||||
|
.map {
|
||||||
|
jacksonRpcConverter.fromJson(it.inputStream(), rpcCall.jsonType, Int::class.java)
|
||||||
|
}.map {
|
||||||
|
rpcCall.converter.apply(it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun execute(id: Int, method: String, params: List<Any>): Mono<ByteArray> {
|
open fun execute(id: Int, method: String, params: List<Any>): Mono<ByteArray> {
|
||||||
val result: Mono<Any> = if (hardcodedMethods.contains(method)) {
|
val result: Mono<Any> = if (hardcodedMethods.contains(method)) {
|
||||||
Mono.just(method).map { hardcoded(it) }
|
Mono.just(method)
|
||||||
|
.map { hardcoded(it) }
|
||||||
} else if (allowedMethods.contains(method)) {
|
} else if (allowedMethods.contains(method)) {
|
||||||
callUpstream(method, params)
|
callUpstream(method, params)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
package io.emeraldpay.dshackle.upstream
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
|
import com.google.protobuf.ByteString
|
||||||
|
import io.emeraldpay.api.proto.BlockchainOuterClass
|
||||||
|
import io.emeraldpay.api.proto.Common
|
||||||
|
import io.emeraldpay.api.proto.ReactorBlockchainGrpc
|
||||||
|
import io.emeraldpay.grpc.Chain
|
||||||
|
import io.infinitape.etherjar.rpc.Batch
|
||||||
|
import io.infinitape.etherjar.rpc.JacksonRpcConverter
|
||||||
|
import io.infinitape.etherjar.rpc.RpcException
|
||||||
|
import io.infinitape.etherjar.rpc.transport.BatchStatus
|
||||||
|
import io.infinitape.etherjar.rpc.transport.RpcTransport
|
||||||
|
import reactor.core.publisher.Flux
|
||||||
|
import reactor.core.publisher.Mono
|
||||||
|
import reactor.core.publisher.toFlux
|
||||||
|
import reactor.util.function.Tuple3
|
||||||
|
import reactor.util.function.Tuples
|
||||||
|
import java.util.concurrent.CompletableFuture
|
||||||
|
import java.util.function.Function
|
||||||
|
|
||||||
|
class EthereumGrpcTransport(
|
||||||
|
private val chain: Chain,
|
||||||
|
private val client: ReactorBlockchainGrpc.ReactorBlockchainStub,
|
||||||
|
private val objectMapper: ObjectMapper
|
||||||
|
): RpcTransport {
|
||||||
|
|
||||||
|
private val chainRef = Common.ChainRef.forNumber(chain.id)
|
||||||
|
private val jacksonRpcConverter = JacksonRpcConverter(objectMapper)
|
||||||
|
|
||||||
|
override fun close() {
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun replyProcessor(mapping: HashMap<Int, Batch.BatchItem<Any, Any>>): Function<BlockchainOuterClass.NativeCallReplyItem, Boolean> {
|
||||||
|
return Function { resp ->
|
||||||
|
val id = resp.id
|
||||||
|
val bi = mapping.remove(id)
|
||||||
|
if (bi != null) {
|
||||||
|
if (resp.succeed) {
|
||||||
|
try {
|
||||||
|
val rpcResp = jacksonRpcConverter.fromJson(resp.payload.toByteArray().inputStream(), bi.call.jsonType, Int::class.java)
|
||||||
|
bi.onComplete(rpcResp)
|
||||||
|
return@Function true
|
||||||
|
} catch (e: RpcException) {
|
||||||
|
bi.onError(e)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
bi.onError(RpcException(-32603, resp.error.toString()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val sumStatus = { t: Tuple3<Int, Int, Int>, ok: Boolean ->
|
||||||
|
if (ok) Tuples.of(t.t1 + 1, t.t2, t.t3 + 1)
|
||||||
|
else Tuples.of(t.t1, t.t2 + 1, t.t3 + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val asStatus = Function<Tuple3<Int, Int, Int>, BatchStatus> {
|
||||||
|
BatchStatus.newBuilder()
|
||||||
|
.withSucceed(it.t1)
|
||||||
|
.withFailed(it.t2)
|
||||||
|
.withTotal(it.t3)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun prepareMapping(items: List<Batch.BatchItem<out Any, out Any>>, req: BlockchainOuterClass.NativeCallRequest.Builder): HashMap<Int, Batch.BatchItem<Any, Any>> {
|
||||||
|
val mapping = HashMap<Int, Batch.BatchItem<Any, Any>>()
|
||||||
|
var seq: Int = 0
|
||||||
|
items.forEach { bi ->
|
||||||
|
val id = seq++
|
||||||
|
mapping[id] = bi as Batch.BatchItem<Any, Any>
|
||||||
|
val call = bi.call
|
||||||
|
val params = objectMapper.writeValueAsBytes(call.params)
|
||||||
|
val nativeCallItem = BlockchainOuterClass.NativeCallItem.newBuilder()
|
||||||
|
.setId(id)
|
||||||
|
.setMethod("POST")
|
||||||
|
.setTarget(call.method)
|
||||||
|
.setPayload(ByteString.copyFrom(params))
|
||||||
|
.build()
|
||||||
|
req.addItems(nativeCallItem)
|
||||||
|
}
|
||||||
|
return mapping
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun execute(items: List<Batch.BatchItem<out Any, out Any>>): CompletableFuture<BatchStatus> {
|
||||||
|
val req = BlockchainOuterClass.NativeCallRequest.newBuilder()
|
||||||
|
.setChain(chainRef);
|
||||||
|
val mapping = prepareMapping(items, req)
|
||||||
|
return client.nativeCall(req.build())
|
||||||
|
.map(replyProcessor(mapping))
|
||||||
|
.reduce(Tuples.of(0, 0, 0), sumStatus)
|
||||||
|
.map(asStatus)
|
||||||
|
.doFinally {
|
||||||
|
mapping.values.forEach { bi ->
|
||||||
|
bi.onError(RpcException(-32603, "RPC response not received"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.toFuture()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,13 +25,13 @@ class EthereumRpcHead(
|
|||||||
.flatMap {
|
.flatMap {
|
||||||
val batch = Batch()
|
val batch = Batch()
|
||||||
val f = batch.add(Commands.eth().blockNumber)
|
val f = batch.add(Commands.eth().blockNumber)
|
||||||
api.execute(batch)
|
api.rpcClient.execute(batch)
|
||||||
Mono.fromCompletionStage(f).timeout(Duration.ofSeconds(5))
|
Mono.fromCompletionStage(f).timeout(Duration.ofSeconds(5))
|
||||||
}
|
}
|
||||||
.flatMap {
|
.flatMap {
|
||||||
val batch = Batch()
|
val batch = Batch()
|
||||||
val f = batch.add(Commands.eth().getBlock(it))
|
val f = batch.add(Commands.eth().getBlock(it))
|
||||||
api.execute(batch)
|
api.rpcClient.execute(batch)
|
||||||
Mono.fromCompletionStage(f).timeout(Duration.ofSeconds(5))
|
Mono.fromCompletionStage(f).timeout(Duration.ofSeconds(5))
|
||||||
}
|
}
|
||||||
.onErrorContinue { err, _ ->
|
.onErrorContinue { err, _ ->
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package io.emeraldpay.dshackle.upstream
|
||||||
|
|
||||||
|
import io.emeraldpay.dshackle.config.UpstreamsConfig
|
||||||
|
import io.emeraldpay.grpc.Chain
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import java.util.concurrent.atomic.AtomicReference
|
||||||
|
|
||||||
|
class EthereumUpstream(
|
||||||
|
val chain: Chain,
|
||||||
|
private val api: EthereumApi,
|
||||||
|
private val ethereumWs: EthereumWs? = null,
|
||||||
|
private val options: UpstreamsConfig.Options
|
||||||
|
): Upstream {
|
||||||
|
|
||||||
|
private val log = LoggerFactory.getLogger(EthereumUpstream::class.java)
|
||||||
|
|
||||||
|
private val head: EthereumHead = if (ethereumWs != null) {
|
||||||
|
EthereumWsHead(ethereumWs)
|
||||||
|
} else {
|
||||||
|
EthereumRpcHead(api).apply {
|
||||||
|
this.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val validator = UpstreamValidator(this, options)
|
||||||
|
private val status = AtomicReference(UpstreamAvailability.UNAVAILABLE)
|
||||||
|
|
||||||
|
init {
|
||||||
|
log.info("Configured for ${chain.chainName}")
|
||||||
|
|
||||||
|
validator.start()
|
||||||
|
.subscribe {
|
||||||
|
status.set(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isAvailable(): Boolean {
|
||||||
|
return status.get() == UpstreamAvailability.OK
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getStatus(): UpstreamAvailability {
|
||||||
|
return status.get()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getHead(): EthereumHead {
|
||||||
|
return head
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getApi(): EthereumApi {
|
||||||
|
return api
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getOptions(): UpstreamsConfig.Options {
|
||||||
|
return options
|
||||||
|
}
|
||||||
|
}
|
||||||
143
src/main/kotlin/io/emeraldpay/dshackle/upstream/GrpcUpstream.kt
Normal file
143
src/main/kotlin/io/emeraldpay/dshackle/upstream/GrpcUpstream.kt
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
package io.emeraldpay.dshackle.upstream
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
|
import com.salesforce.reactorgrpc.GrpcRetry
|
||||||
|
import io.emeraldpay.api.proto.BlockchainOuterClass
|
||||||
|
import io.emeraldpay.api.proto.Common
|
||||||
|
import io.emeraldpay.api.proto.ReactorBlockchainGrpc
|
||||||
|
import io.emeraldpay.dshackle.config.UpstreamsConfig
|
||||||
|
import io.emeraldpay.grpc.Chain
|
||||||
|
import io.infinitape.etherjar.domain.BlockHash
|
||||||
|
import io.infinitape.etherjar.domain.TransactionId
|
||||||
|
import io.infinitape.etherjar.rpc.*
|
||||||
|
import io.infinitape.etherjar.rpc.json.BlockJson
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import reactor.core.publisher.Flux
|
||||||
|
import reactor.core.publisher.Mono
|
||||||
|
import reactor.core.publisher.TopicProcessor
|
||||||
|
import reactor.core.publisher.toMono
|
||||||
|
import java.math.BigInteger
|
||||||
|
import java.time.Duration
|
||||||
|
import java.util.concurrent.atomic.AtomicReference
|
||||||
|
import java.util.function.Function
|
||||||
|
|
||||||
|
open class GrpcUpstream(
|
||||||
|
private val chain: Chain,
|
||||||
|
private val client: ReactorBlockchainGrpc.ReactorBlockchainStub,
|
||||||
|
private val objectMapper: ObjectMapper,
|
||||||
|
private val options: UpstreamsConfig.Options
|
||||||
|
): Upstream {
|
||||||
|
|
||||||
|
constructor(chain: Chain, client: ReactorBlockchainGrpc.ReactorBlockchainStub, objectMapper: ObjectMapper)
|
||||||
|
: this(chain, client, objectMapper, UpstreamsConfig.Options.getDefaults())
|
||||||
|
|
||||||
|
private val log = LoggerFactory.getLogger(GrpcUpstream::class.java)
|
||||||
|
|
||||||
|
private val headBlock = AtomicReference<BlockJson<TransactionId>>(null)
|
||||||
|
private val streamBlocks: TopicProcessor<BlockJson<TransactionId>> = TopicProcessor.create()
|
||||||
|
private var status = AtomicReference<UpstreamAvailability>(UpstreamAvailability.UNAVAILABLE)
|
||||||
|
private val head = Head(this)
|
||||||
|
private val api: EthereumApi
|
||||||
|
|
||||||
|
init {
|
||||||
|
val grpcTransport = EthereumGrpcTransport(chain, client, objectMapper)
|
||||||
|
val rpcClient = DefaultRpcClient(grpcTransport)
|
||||||
|
api = EthereumApi(rpcClient, objectMapper, chain)
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun connect() {
|
||||||
|
val chainRef = Common.Chain.newBuilder()
|
||||||
|
.setTypeValue(chain.id)
|
||||||
|
.build()
|
||||||
|
.toMono()
|
||||||
|
|
||||||
|
val retry: Function<Flux<BlockchainOuterClass.ChainHead>, Flux<BlockchainOuterClass.ChainHead>> = Function {
|
||||||
|
status.set(UpstreamAvailability.UNAVAILABLE)
|
||||||
|
client.subscribeHead(chainRef)
|
||||||
|
}
|
||||||
|
|
||||||
|
val flux = client.subscribeHead(chainRef)
|
||||||
|
.compose(GrpcRetry.ManyToMany.retryAfter(retry, Duration.ofSeconds(5)))
|
||||||
|
subscribe(flux)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun subscribe(flux: Flux<BlockchainOuterClass.ChainHead>) {
|
||||||
|
flux.map { value ->
|
||||||
|
val block = BlockJson<TransactionId>()
|
||||||
|
block.number = value.height
|
||||||
|
block.totalDifficulty = BigInteger(1, value.weight.toByteArray())
|
||||||
|
block.hash = BlockHash.from("0x"+value.blockId)
|
||||||
|
block
|
||||||
|
}
|
||||||
|
.filter { block ->
|
||||||
|
val curr = headBlock.get()
|
||||||
|
curr == null || curr.totalDifficulty < block.totalDifficulty
|
||||||
|
}
|
||||||
|
.doOnError { err ->
|
||||||
|
log.error("Head subscription error", err)
|
||||||
|
}
|
||||||
|
.subscribe { block ->
|
||||||
|
log.debug("New block ${block.number} on ${chain}")
|
||||||
|
headBlock.set(block)
|
||||||
|
streamBlocks.onNext(block)
|
||||||
|
status.set(UpstreamAvailability.OK)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun init(conf: BlockchainOuterClass.DescribeChain) {
|
||||||
|
val available = conf.available
|
||||||
|
val quorum = conf.quorum
|
||||||
|
status.set(
|
||||||
|
if (available && quorum > 0) UpstreamAvailability.OK else UpstreamAvailability.UNAVAILABLE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onStatus(value: BlockchainOuterClass.ChainStatus) {
|
||||||
|
val available = value.available
|
||||||
|
val quorum = value.quorum
|
||||||
|
status.set(
|
||||||
|
if (available && quorum > 0) UpstreamAvailability.OK else UpstreamAvailability.UNAVAILABLE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
override fun isAvailable(): Boolean {
|
||||||
|
return headBlock.get() != null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getStatus(): UpstreamAvailability {
|
||||||
|
return status.get()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getHead(): EthereumHead {
|
||||||
|
return head
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getApi(): EthereumApi {
|
||||||
|
return api
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getOptions(): UpstreamsConfig.Options {
|
||||||
|
return options
|
||||||
|
}
|
||||||
|
|
||||||
|
class Head(
|
||||||
|
val upstream: GrpcUpstream
|
||||||
|
): EthereumHead {
|
||||||
|
|
||||||
|
override fun getHead(): Mono<BlockJson<TransactionId>> {
|
||||||
|
val current = upstream.headBlock.get()
|
||||||
|
if (current != null) {
|
||||||
|
return Mono.just(current)
|
||||||
|
}
|
||||||
|
return Mono.from(upstream.streamBlocks)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getFlux(): Flux<BlockJson<TransactionId>> {
|
||||||
|
return Flux.from(upstream.streamBlocks)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
package io.emeraldpay.dshackle.upstream
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
|
import io.emeraldpay.api.proto.BlockchainOuterClass
|
||||||
|
import io.emeraldpay.api.proto.ReactorBlockchainGrpc
|
||||||
|
import io.emeraldpay.dshackle.config.UpstreamsConfig
|
||||||
|
import io.emeraldpay.grpc.Chain
|
||||||
|
import io.grpc.ManagedChannelBuilder
|
||||||
|
import reactor.core.publisher.Mono
|
||||||
|
import java.util.*
|
||||||
|
import java.util.concurrent.locks.ReentrantLock
|
||||||
|
import kotlin.concurrent.withLock
|
||||||
|
|
||||||
|
class GrpcUpstreams(
|
||||||
|
private val host: String,
|
||||||
|
private val port: Int,
|
||||||
|
private val objectMapper: ObjectMapper,
|
||||||
|
private val options: UpstreamsConfig.Options
|
||||||
|
) {
|
||||||
|
|
||||||
|
private var client: ReactorBlockchainGrpc.ReactorBlockchainStub? = null
|
||||||
|
private var known = HashMap<Chain, GrpcUpstream>()
|
||||||
|
private val lock = ReentrantLock()
|
||||||
|
|
||||||
|
fun start(): Mono<List<Chain>> {
|
||||||
|
val channel = ManagedChannelBuilder.forAddress(host, port)
|
||||||
|
.enableRetry()
|
||||||
|
channel.usePlaintext()
|
||||||
|
val client = ReactorBlockchainGrpc.newReactorStub(channel.build())
|
||||||
|
this.client = client
|
||||||
|
val loaded = client.describe(BlockchainOuterClass.DescribeRequest.newBuilder().build())
|
||||||
|
.map { value ->
|
||||||
|
val chains = ArrayList<Chain>()
|
||||||
|
value.chainsList.forEach { chainDetails ->
|
||||||
|
val chain = Chain.byId(chainDetails.chain.number)
|
||||||
|
if (chain != Chain.UNSPECIFIED) {
|
||||||
|
getOrCreate(chain)
|
||||||
|
.init(chainDetails)
|
||||||
|
chains.add(chain)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
chains as List<Chain>
|
||||||
|
}
|
||||||
|
//TODO subscribe only after receiving details
|
||||||
|
client.subscribeStatus(BlockchainOuterClass.StatusRequest.newBuilder().build())
|
||||||
|
.subscribe { value ->
|
||||||
|
val chain = Chain.byId(value.chain.number)
|
||||||
|
if (chain != Chain.UNSPECIFIED) {
|
||||||
|
getOrCreate(chain).onStatus(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return loaded
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getOrCreate(chain: Chain): GrpcUpstream {
|
||||||
|
lock.withLock {
|
||||||
|
val current = known[chain]
|
||||||
|
return if (current == null) {
|
||||||
|
val created = GrpcUpstream(chain, client!!, objectMapper, options)
|
||||||
|
known[chain] = created
|
||||||
|
created.connect()
|
||||||
|
created
|
||||||
|
} else {
|
||||||
|
current
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,44 +1,11 @@
|
|||||||
package io.emeraldpay.dshackle.upstream
|
package io.emeraldpay.dshackle.upstream
|
||||||
|
|
||||||
import io.emeraldpay.dshackle.config.UpstreamsConfig
|
import io.emeraldpay.dshackle.config.UpstreamsConfig
|
||||||
import io.emeraldpay.grpc.Chain
|
|
||||||
import org.slf4j.LoggerFactory
|
|
||||||
import java.util.concurrent.atomic.AtomicReference
|
|
||||||
|
|
||||||
class Upstream(
|
interface Upstream {
|
||||||
val chain: Chain,
|
fun isAvailable(): Boolean
|
||||||
val api: EthereumApi,
|
fun getStatus(): UpstreamAvailability
|
||||||
private val ethereumWs: EthereumWs? = null,
|
fun getHead(): EthereumHead
|
||||||
private val options: UpstreamsConfig.Options
|
fun getApi(): EthereumApi
|
||||||
) {
|
fun getOptions(): UpstreamsConfig.Options
|
||||||
|
|
||||||
private val log = LoggerFactory.getLogger(Upstream::class.java)
|
|
||||||
|
|
||||||
val head: EthereumHead = if (ethereumWs != null) {
|
|
||||||
EthereumWsHead(ethereumWs)
|
|
||||||
} else {
|
|
||||||
EthereumRpcHead(api).apply {
|
|
||||||
this.start()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val validator = UpstreamValidator(this, options)
|
|
||||||
private val status = AtomicReference(UpstreamAvailability.UNAVAILABLE)
|
|
||||||
|
|
||||||
init {
|
|
||||||
log.info("Configured for ${chain.chainName}")
|
|
||||||
|
|
||||||
validator.start()
|
|
||||||
.subscribe {
|
|
||||||
status.set(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isAvailable(): Boolean {
|
|
||||||
return status.get() == UpstreamAvailability.OK
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getStatus(): UpstreamAvailability {
|
|
||||||
return status.get()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -8,7 +8,7 @@ import java.time.Duration
|
|||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
class UpstreamValidator(
|
class UpstreamValidator(
|
||||||
private val upstream: Upstream,
|
private val ethereumUpstream: EthereumUpstream,
|
||||||
private val options: UpstreamsConfig.Options
|
private val options: UpstreamsConfig.Options
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@ class UpstreamValidator(
|
|||||||
val peerCount = batch.add(Commands.net().peerCount())
|
val peerCount = batch.add(Commands.net().peerCount())
|
||||||
val syncing = batch.add(Commands.eth().syncing())
|
val syncing = batch.add(Commands.eth().syncing())
|
||||||
try {
|
try {
|
||||||
upstream.api.execute(batch).get(5, TimeUnit.SECONDS)
|
ethereumUpstream.getApi().rpcClient.execute(batch).get(5, TimeUnit.SECONDS)
|
||||||
if (syncing.get().isSyncing) {
|
if (syncing.get().isSyncing) {
|
||||||
return UpstreamAvailability.SYNCING
|
return UpstreamAvailability.SYNCING
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,116 +1,8 @@
|
|||||||
package io.emeraldpay.dshackle.upstream
|
package io.emeraldpay.dshackle.upstream
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
|
||||||
import io.emeraldpay.dshackle.config.UpstreamsConfig
|
|
||||||
import io.emeraldpay.dshackle.config.UpstreamsConfigReader
|
|
||||||
import io.emeraldpay.grpc.Chain
|
import io.emeraldpay.grpc.Chain
|
||||||
import io.infinitape.etherjar.rpc.DefaultRpcClient
|
|
||||||
import io.infinitape.etherjar.rpc.transport.DefaultRpcTransport
|
|
||||||
import org.apache.commons.lang3.StringUtils
|
|
||||||
import org.slf4j.LoggerFactory
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired
|
|
||||||
import org.springframework.core.env.Environment
|
|
||||||
import org.springframework.scheduling.annotation.Scheduled
|
|
||||||
import org.springframework.stereotype.Repository
|
|
||||||
import java.io.File
|
|
||||||
import java.net.URI
|
|
||||||
import javax.annotation.PostConstruct
|
|
||||||
|
|
||||||
@Repository
|
interface Upstreams {
|
||||||
class Upstreams(
|
fun ethereumUpstream(chain: Chain): AggregatedUpstreams
|
||||||
@Autowired val env: Environment,
|
fun getAvailable(): List<Chain>
|
||||||
@Autowired private val objectMapper: ObjectMapper
|
|
||||||
) {
|
|
||||||
|
|
||||||
private val log = LoggerFactory.getLogger(Upstreams::class.java)
|
|
||||||
private val chainMapping = HashMap<Chain, ChainConnect>()
|
|
||||||
|
|
||||||
private val chainNames = mapOf(
|
|
||||||
"ethereum" to Chain.ETHEREUM,
|
|
||||||
"ethereum-classic" to Chain.ETHEREUM_CLASSIC,
|
|
||||||
"eth" to Chain.ETHEREUM,
|
|
||||||
"etc" to Chain.ETHEREUM_CLASSIC,
|
|
||||||
"morden" to Chain.TESTNET_MORDEN,
|
|
||||||
"kovan" to Chain.TESTNET_KOVAN
|
|
||||||
)
|
|
||||||
|
|
||||||
@PostConstruct
|
|
||||||
fun start() {
|
|
||||||
val path = env.getProperty("upstreams.config")
|
|
||||||
if (StringUtils.isEmpty(path)) {
|
|
||||||
log.error("Path to upstreams is not set (upstreams.config)")
|
|
||||||
System.exit(1)
|
|
||||||
}
|
|
||||||
val upstreamConfig = File(path)
|
|
||||||
val ok = upstreamConfig.exists() && upstreamConfig.isFile
|
|
||||||
if (!ok) {
|
|
||||||
log.error("Unable to setup upstreams from ${upstreamConfig.path}")
|
|
||||||
System.exit(1)
|
|
||||||
}
|
|
||||||
log.info("Read upstream configuration from ${upstreamConfig.path}")
|
|
||||||
val reader = UpstreamsConfigReader()
|
|
||||||
val config = reader.read(upstreamConfig.inputStream())
|
|
||||||
|
|
||||||
val groups = HashMap<Chain, ArrayList<Upstream>>()
|
|
||||||
|
|
||||||
val defaultOptions = HashMap<Chain, UpstreamsConfig.Options>()
|
|
||||||
config.defaultOptions.forEach { df ->
|
|
||||||
df.chains.forEach { chainName ->
|
|
||||||
chainNames[chainName]?.let { chain ->
|
|
||||||
var current = defaultOptions[chain]
|
|
||||||
if (current == null) {
|
|
||||||
current = df.options
|
|
||||||
} else {
|
|
||||||
current = current.merge(df.options)
|
|
||||||
}
|
|
||||||
defaultOptions[chain] = current
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
config.upstreams.forEach { up ->
|
|
||||||
val chain = chainNames[up.chain] ?: return@forEach
|
|
||||||
var rpcApi: EthereumApi? = null
|
|
||||||
var wsApi: EthereumWs? = null
|
|
||||||
val urls = ArrayList<URI>()
|
|
||||||
up.endpoints.forEach { endpoint ->
|
|
||||||
if (endpoint.type == UpstreamsConfig.EndpointType.JSON_RPC) {
|
|
||||||
rpcApi = EthereumApi(
|
|
||||||
DefaultRpcClient(DefaultRpcTransport(endpoint.url)),
|
|
||||||
objectMapper,
|
|
||||||
chain
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (endpoint.type == UpstreamsConfig.EndpointType.WEBSOCKET) {
|
|
||||||
wsApi = EthereumWs(
|
|
||||||
endpoint.url,
|
|
||||||
endpoint.origin ?: URI("http://localhost")
|
|
||||||
)
|
|
||||||
wsApi!!.connect()
|
|
||||||
}
|
|
||||||
urls.add(endpoint.url)
|
|
||||||
}
|
|
||||||
val options = (up.options ?: UpstreamsConfig.Options())
|
|
||||||
.merge(defaultOptions[chain])
|
|
||||||
.merge(UpstreamsConfig.Options.getDefaults())
|
|
||||||
if (rpcApi != null) {
|
|
||||||
log.info("Info using ${chain.chainName} upstream, at ${urls.joinToString()}")
|
|
||||||
val current = groups[chain] ?: ArrayList()
|
|
||||||
current.add(Upstream(chain, rpcApi!!, wsApi, options))
|
|
||||||
groups[chain] = current
|
|
||||||
}
|
|
||||||
}
|
|
||||||
groups.forEach { chain, group ->
|
|
||||||
chainMapping[chain] = ChainConnect(chain, group)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun ethereumUpstream(chain: Chain): ChainConnect? {
|
|
||||||
return chainMapping[chain]
|
|
||||||
}
|
|
||||||
|
|
||||||
@Scheduled(fixedRate = 15000)
|
|
||||||
fun printStatuses() {
|
|
||||||
chainMapping.forEach { it.value.printStatus() }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
32
src/main/resources/log4j2.xml
Normal file
32
src/main/resources/log4j2.xml
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Configuration status="WARN">
|
||||||
|
|
||||||
|
<Properties>
|
||||||
|
<Property name="dfltPattern">%d{HH:mm:ss.SSS} [%-20t] %-5level %24.24c{1} | %msg%n</Property>
|
||||||
|
</Properties>
|
||||||
|
|
||||||
|
<Appenders>
|
||||||
|
<Console name="STDOUT" target="SYSTEM_OUT">
|
||||||
|
<PatternLayout pattern="${dfltPattern}"/>
|
||||||
|
<Filters>
|
||||||
|
<ThresholdFilter level="WARN" onMatch="DENY" onMismatch="ACCEPT" />
|
||||||
|
</Filters>
|
||||||
|
</Console>
|
||||||
|
<Console name="STDERR" target="SYSTEM_ERR">
|
||||||
|
<PatternLayout pattern="${dfltPattern}" />
|
||||||
|
</Console>
|
||||||
|
</Appenders>
|
||||||
|
|
||||||
|
<Loggers>
|
||||||
|
<Logger name="io.emeraldpay" level="debug" additivity="false">
|
||||||
|
<AppenderRef ref="STDOUT"/>
|
||||||
|
<AppenderRef ref="STDERR" level="warn"/>
|
||||||
|
</Logger>
|
||||||
|
|
||||||
|
<Root level="warn" additivity="false">
|
||||||
|
<AppenderRef ref="STDOUT"/>
|
||||||
|
<AppenderRef ref="STDERR" level="warn"/>
|
||||||
|
</Root>
|
||||||
|
</Loggers>
|
||||||
|
|
||||||
|
</Configuration>
|
||||||
@@ -54,4 +54,31 @@ class UpstreamsConfigReaderSpec extends Specification {
|
|||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def "Parse ds config"() {
|
||||||
|
setup:
|
||||||
|
def config = this.class.getClassLoader().getResourceAsStream("upstreams-ds.yaml")
|
||||||
|
when:
|
||||||
|
def act = reader.read(config)
|
||||||
|
then:
|
||||||
|
act != null
|
||||||
|
act.version == "v1"
|
||||||
|
act.upstreams.size() == 1
|
||||||
|
with(act.upstreams.get(0)) {
|
||||||
|
id == "remote"
|
||||||
|
chain == "auto"
|
||||||
|
provider == "dshackle"
|
||||||
|
endpoints.size() == 1
|
||||||
|
with(endpoints.get(0)) {
|
||||||
|
type == UpstreamsConfig.EndpointType.DSHACKLE
|
||||||
|
host == "10.2.0.15"
|
||||||
|
auth instanceof UpstreamsConfig.TlsAuth
|
||||||
|
with((UpstreamsConfig.TlsAuth)auth) {
|
||||||
|
ca == "/etc/ca.myservice.com.crt"
|
||||||
|
certificate == "/etc/client1.myservice.com.crt"
|
||||||
|
key == "/etc/client1.myservice.com.key"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package io.emeraldpay.dshackle.test
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
|
import io.emeraldpay.dshackle.upstream.EthereumApi
|
||||||
|
import io.emeraldpay.grpc.Chain
|
||||||
|
import io.infinitape.etherjar.rpc.RpcClient
|
||||||
|
import io.infinitape.etherjar.rpc.RpcResponseError
|
||||||
|
import io.infinitape.etherjar.rpc.json.ResponseJson
|
||||||
|
import org.jetbrains.annotations.NotNull
|
||||||
|
import reactor.core.publisher.Mono
|
||||||
|
|
||||||
|
class EthereumApiMock extends EthereumApi {
|
||||||
|
|
||||||
|
List<PredefinedResponse> predefined = []
|
||||||
|
private ObjectMapper objectMapper
|
||||||
|
|
||||||
|
EthereumApiMock(@NotNull RpcClient rpcClient, @NotNull ObjectMapper objectMapper, @NotNull Chain chain) {
|
||||||
|
super(rpcClient, objectMapper, chain)
|
||||||
|
this.objectMapper = objectMapper
|
||||||
|
}
|
||||||
|
|
||||||
|
EthereumApiMock answer(@NotNull String method, List<Object> params, Object result) {
|
||||||
|
predefined << new PredefinedResponse(method: method, params: params, result: result)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
Mono<byte[]> execute(int id, @NotNull String method, @NotNull List<?> params) {
|
||||||
|
def predefined = predefined.find { it.isSame(id, method, params) }
|
||||||
|
ResponseJson json = new ResponseJson<Object, Integer>(id: id)
|
||||||
|
if (predefined != null) {
|
||||||
|
json.result = predefined.result
|
||||||
|
} else {
|
||||||
|
json.error = new RpcResponseError(-32601, "Method ${method} with ${params} is not mocked")
|
||||||
|
}
|
||||||
|
return Mono.just(objectMapper.writeValueAsBytes(json))
|
||||||
|
}
|
||||||
|
|
||||||
|
class PredefinedResponse {
|
||||||
|
String method
|
||||||
|
List params
|
||||||
|
Object result
|
||||||
|
|
||||||
|
boolean isSame(int id, String method, List<?> params) {
|
||||||
|
if (method != this.method) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (this.params == null) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return this.params == params
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package io.emeraldpay.dshackle.test
|
||||||
|
|
||||||
|
import io.emeraldpay.api.proto.BlockchainGrpc
|
||||||
|
import io.emeraldpay.api.proto.ReactorBlockchainGrpc
|
||||||
|
import io.grpc.inprocess.InProcessChannelBuilder
|
||||||
|
import io.grpc.inprocess.InProcessServerBuilder
|
||||||
|
import io.grpc.testing.GrpcCleanupRule
|
||||||
|
|
||||||
|
class MockServer {
|
||||||
|
|
||||||
|
GrpcCleanupRule grpcCleanup = new GrpcCleanupRule()
|
||||||
|
|
||||||
|
ReactorBlockchainGrpc.ReactorBlockchainStub runServer(BlockchainGrpc.BlockchainImplBase impl){
|
||||||
|
String serverName = InProcessServerBuilder.generateName()
|
||||||
|
grpcCleanup.register(InProcessServerBuilder
|
||||||
|
.forName(serverName).directExecutor().addService(impl).build().start());
|
||||||
|
def channel = grpcCleanup.register(InProcessChannelBuilder.forName(serverName).directExecutor().build())
|
||||||
|
return ReactorBlockchainGrpc.newReactorStub(channel)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package io.emeraldpay.dshackle.test
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.Version
|
||||||
|
import com.fasterxml.jackson.databind.DeserializationFeature
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
|
import com.fasterxml.jackson.databind.module.SimpleModule
|
||||||
|
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
|
||||||
|
class TestingCommons {
|
||||||
|
|
||||||
|
static ObjectMapper objectMapper() {
|
||||||
|
def module = new SimpleModule("EmeraldDShackle", new Version(1, 0, 0, null, null, null))
|
||||||
|
|
||||||
|
def objectMapper = new ObjectMapper()
|
||||||
|
objectMapper.registerModule(module)
|
||||||
|
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
|
||||||
|
objectMapper
|
||||||
|
.setDateFormat(new SimpleDateFormat("yyyy-MM-dd\'T\'HH:mm:ss.SSS"))
|
||||||
|
.setTimeZone(TimeZone.getTimeZone("UTC"))
|
||||||
|
|
||||||
|
return objectMapper
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
package io.emeraldpay.dshackle.upstream
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
|
import io.emeraldpay.api.proto.BlockchainGrpc
|
||||||
|
import io.emeraldpay.api.proto.BlockchainOuterClass
|
||||||
|
import io.emeraldpay.dshackle.rpc.NativeCall
|
||||||
|
import io.emeraldpay.dshackle.test.EthereumApiMock
|
||||||
|
import io.emeraldpay.dshackle.test.MockServer
|
||||||
|
import io.emeraldpay.dshackle.test.TestingCommons
|
||||||
|
import io.emeraldpay.grpc.Chain
|
||||||
|
import io.grpc.stub.StreamObserver
|
||||||
|
import io.infinitape.etherjar.rpc.Batch
|
||||||
|
import io.infinitape.etherjar.rpc.RpcCall
|
||||||
|
import io.infinitape.etherjar.rpc.RpcClient
|
||||||
|
import spock.lang.Specification
|
||||||
|
|
||||||
|
class EthereumGrpcTransportSpec extends Specification {
|
||||||
|
|
||||||
|
MockServer mockServer = new MockServer()
|
||||||
|
ObjectMapper objectMapper = TestingCommons.objectMapper()
|
||||||
|
|
||||||
|
def "Make simple call"() {
|
||||||
|
setup:
|
||||||
|
def callData = [:]
|
||||||
|
def otherSideUpstreams = Mock(Upstreams)
|
||||||
|
def otherSideAggr = Mock(AggregatedUpstreams)
|
||||||
|
def otherSideNativeCall = new NativeCall(otherSideUpstreams, objectMapper)
|
||||||
|
def otherSideApi = new EthereumApiMock(Mock(RpcClient), objectMapper, Chain.ETHEREUM)
|
||||||
|
|
||||||
|
def client = mockServer.runServer(new BlockchainGrpc.BlockchainImplBase() {
|
||||||
|
@Override
|
||||||
|
void nativeCall(BlockchainOuterClass.NativeCallRequest request, StreamObserver<BlockchainOuterClass.NativeCallReplyItem> responseObserver) {
|
||||||
|
callData["request"] = request
|
||||||
|
otherSideNativeCall.nativeCall(request, responseObserver)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
EthereumGrpcTransport transport = new EthereumGrpcTransport(Chain.ETHEREUM, client, objectMapper)
|
||||||
|
when:
|
||||||
|
otherSideApi.answer("eth_test", [1], "bar")
|
||||||
|
def batch = new Batch()
|
||||||
|
def f = batch.add(RpcCall.create("eth_test", [1]))
|
||||||
|
def status = transport.execute(batch.items).get()
|
||||||
|
|
||||||
|
then:
|
||||||
|
1 * otherSideUpstreams.ethereumUpstream(Chain.ETHEREUM) >> otherSideAggr
|
||||||
|
1 * otherSideAggr.api >> otherSideApi
|
||||||
|
status.failed == 0
|
||||||
|
status.succeed == 1
|
||||||
|
status.total == 1
|
||||||
|
callData.request != null
|
||||||
|
with((BlockchainOuterClass.NativeCallRequest)callData.request) {
|
||||||
|
chain.number == Chain.ETHEREUM.id
|
||||||
|
itemsCount == 1
|
||||||
|
with(getItems(0)) {
|
||||||
|
target == "eth_test"
|
||||||
|
payload.toStringUtf8() == "[1]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
f.get() == "bar"
|
||||||
|
}
|
||||||
|
|
||||||
|
def "Make few calls"() {
|
||||||
|
setup:
|
||||||
|
def callData = [:]
|
||||||
|
def otherSideUpstreams = Mock(Upstreams)
|
||||||
|
def otherSideAggr = Mock(AggregatedUpstreams)
|
||||||
|
def otherSideNativeCall = new NativeCall(otherSideUpstreams, objectMapper)
|
||||||
|
def otherSideApi = new EthereumApiMock(Mock(RpcClient), objectMapper, Chain.ETHEREUM)
|
||||||
|
|
||||||
|
def client = mockServer.runServer(new BlockchainGrpc.BlockchainImplBase() {
|
||||||
|
@Override
|
||||||
|
void nativeCall(BlockchainOuterClass.NativeCallRequest request, StreamObserver<BlockchainOuterClass.NativeCallReplyItem> responseObserver) {
|
||||||
|
callData["request"] = request
|
||||||
|
otherSideNativeCall.nativeCall(request, responseObserver)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
EthereumGrpcTransport transport = new EthereumGrpcTransport(Chain.ETHEREUM, client, objectMapper)
|
||||||
|
when:
|
||||||
|
otherSideApi.answer("eth_test", [1], "bar")
|
||||||
|
otherSideApi.answer("eth_test2", [2, "3"], "baz")
|
||||||
|
|
||||||
|
def batch = new Batch()
|
||||||
|
def f1 = batch.add(RpcCall.create("eth_test", [1]))
|
||||||
|
def f2 = batch.add(RpcCall.create("eth_test2", [2, "3"]))
|
||||||
|
def status = transport.execute(batch.items).get()
|
||||||
|
|
||||||
|
then:
|
||||||
|
1 * otherSideUpstreams.ethereumUpstream(Chain.ETHEREUM) >> otherSideAggr
|
||||||
|
1 * otherSideAggr.api >> otherSideApi
|
||||||
|
status.failed == 0
|
||||||
|
status.succeed == 2
|
||||||
|
status.total == 2
|
||||||
|
callData.request != null
|
||||||
|
with((BlockchainOuterClass.NativeCallRequest)callData.request) {
|
||||||
|
chain.number == Chain.ETHEREUM.id
|
||||||
|
itemsCount == 2
|
||||||
|
with(getItems(0)) {
|
||||||
|
target == "eth_test"
|
||||||
|
payload.toStringUtf8() == "[1]"
|
||||||
|
}
|
||||||
|
with(getItems(1)) {
|
||||||
|
target == "eth_test2"
|
||||||
|
payload.toStringUtf8() == "[2,\"3\"]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
f1.get() == "bar"
|
||||||
|
f2.get() == "baz"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
package io.emeraldpay.dshackle.upstream
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
|
import com.google.protobuf.ByteString
|
||||||
|
import io.emeraldpay.api.proto.BlockchainGrpc
|
||||||
|
import io.emeraldpay.api.proto.BlockchainOuterClass
|
||||||
|
import io.emeraldpay.api.proto.Common
|
||||||
|
import io.emeraldpay.dshackle.test.MockServer
|
||||||
|
import io.emeraldpay.dshackle.test.TestingCommons
|
||||||
|
import io.emeraldpay.grpc.Chain
|
||||||
|
import io.grpc.stub.StreamObserver
|
||||||
|
import io.infinitape.etherjar.domain.BlockHash
|
||||||
|
import org.apache.commons.codec.binary.Hex
|
||||||
|
import spock.lang.Specification
|
||||||
|
|
||||||
|
import java.util.concurrent.CompletableFuture
|
||||||
|
|
||||||
|
class GrpcUpstreamSpec extends Specification {
|
||||||
|
|
||||||
|
MockServer mockServer = new MockServer()
|
||||||
|
ObjectMapper objectMapper = TestingCommons.objectMapper()
|
||||||
|
|
||||||
|
def "Subscribe to head"() {
|
||||||
|
setup:
|
||||||
|
def callData = [:]
|
||||||
|
def client = mockServer.runServer(new BlockchainGrpc.BlockchainImplBase() {
|
||||||
|
@Override
|
||||||
|
void subscribeHead(Common.Chain request, StreamObserver<BlockchainOuterClass.ChainHead> responseObserver) {
|
||||||
|
callData.chain = request.getTypeValue()
|
||||||
|
responseObserver.onNext(
|
||||||
|
BlockchainOuterClass.ChainHead.newBuilder()
|
||||||
|
.setBlockId("50d26e119968e791970d84a7bf5d0ec474d3ec2ef85d5ec8915210ac6bc09ad7")
|
||||||
|
.setHeight(650246)
|
||||||
|
.setWeight(ByteString.copyFrom(Hex.decodeHex("35bbde5595de6456")))
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
def chain = Chain.ETHEREUM
|
||||||
|
def upstream = new GrpcUpstream(chain, client, objectMapper)
|
||||||
|
when:
|
||||||
|
upstream.connect()
|
||||||
|
def h = upstream.head.head.block()
|
||||||
|
then:
|
||||||
|
callData.chain == Chain.ETHEREUM.id
|
||||||
|
upstream.status == UpstreamAvailability.OK
|
||||||
|
h.hash == BlockHash.from("0x50d26e119968e791970d84a7bf5d0ec474d3ec2ef85d5ec8915210ac6bc09ad7")
|
||||||
|
}
|
||||||
|
|
||||||
|
def "Follows difficulty, ignores less difficult"() {
|
||||||
|
setup:
|
||||||
|
def callData = [:]
|
||||||
|
def finished = new CompletableFuture<Boolean>()
|
||||||
|
def client = mockServer.runServer(new BlockchainGrpc.BlockchainImplBase() {
|
||||||
|
@Override
|
||||||
|
void subscribeHead(Common.Chain request, StreamObserver<BlockchainOuterClass.ChainHead> responseObserver) {
|
||||||
|
responseObserver.onNext(
|
||||||
|
BlockchainOuterClass.ChainHead.newBuilder()
|
||||||
|
.setBlockId("50d26e119968e791970d84a7bf5d0ec474d3ec2ef85d5ec8915210ac6bc09ad7")
|
||||||
|
.setHeight(650246)
|
||||||
|
.setWeight(ByteString.copyFrom(Hex.decodeHex("35bbde5595de6456")))
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
responseObserver.onNext(
|
||||||
|
BlockchainOuterClass.ChainHead.newBuilder()
|
||||||
|
.setBlockId("3ec2ebf5d0ec474d0ac6bca770d8409ad750d26e119968e7919f85d5ec891521")
|
||||||
|
.setHeight(650247)
|
||||||
|
.setWeight(ByteString.copyFrom(Hex.decodeHex("35bbde5595de6455")))
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
finished.complete(true)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
def chain = Chain.ETHEREUM
|
||||||
|
def upstream = new GrpcUpstream(chain, client, objectMapper)
|
||||||
|
when:
|
||||||
|
upstream.connect()
|
||||||
|
finished.get()
|
||||||
|
def h = upstream.head.head.block()
|
||||||
|
then:
|
||||||
|
upstream.status == UpstreamAvailability.OK
|
||||||
|
h.hash == BlockHash.from("0x50d26e119968e791970d84a7bf5d0ec474d3ec2ef85d5ec8915210ac6bc09ad7")
|
||||||
|
h.number == 650246
|
||||||
|
}
|
||||||
|
|
||||||
|
def "Follows difficulty"() {
|
||||||
|
setup:
|
||||||
|
def callData = [:]
|
||||||
|
def finished = new CompletableFuture<Boolean>()
|
||||||
|
def client = mockServer.runServer(new BlockchainGrpc.BlockchainImplBase() {
|
||||||
|
@Override
|
||||||
|
void subscribeHead(Common.Chain request, StreamObserver<BlockchainOuterClass.ChainHead> responseObserver) {
|
||||||
|
responseObserver.onNext(
|
||||||
|
BlockchainOuterClass.ChainHead.newBuilder()
|
||||||
|
.setBlockId("50d26e119968e791970d84a7bf5d0ec474d3ec2ef85d5ec8915210ac6bc09ad7")
|
||||||
|
.setHeight(650246)
|
||||||
|
.setWeight(ByteString.copyFrom(Hex.decodeHex("35bbde5595de6456")))
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
responseObserver.onNext(
|
||||||
|
BlockchainOuterClass.ChainHead.newBuilder()
|
||||||
|
.setBlockId("3ec2ebf5d0ec474d0ac6bc50d2770d8409ad76e119968e7919f85d5ec891521a")
|
||||||
|
.setHeight(650247)
|
||||||
|
.setWeight(ByteString.copyFrom(Hex.decodeHex("35bbde5595de6457")))
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
finished.complete(true)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
def chain = Chain.ETHEREUM
|
||||||
|
def upstream = new GrpcUpstream(chain, client, objectMapper)
|
||||||
|
when:
|
||||||
|
upstream.connect()
|
||||||
|
finished.get()
|
||||||
|
def h = upstream.head.head.block()
|
||||||
|
then:
|
||||||
|
upstream.status == UpstreamAvailability.OK
|
||||||
|
h.hash == BlockHash.from("0x3ec2ebf5d0ec474d0ac6bc50d2770d8409ad76e119968e7919f85d5ec891521a")
|
||||||
|
h.number == 650247
|
||||||
|
}
|
||||||
|
}
|
||||||
32
src/test/resources/log4j2.xml
Normal file
32
src/test/resources/log4j2.xml
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Configuration status="WARN">
|
||||||
|
|
||||||
|
<Properties>
|
||||||
|
<Property name="dfltPattern">%d{HH:mm:ss.SSS} [%-20t] %-5level %24.24c{1} | %msg%n</Property>
|
||||||
|
</Properties>
|
||||||
|
|
||||||
|
<Appenders>
|
||||||
|
<Console name="STDOUT" target="SYSTEM_OUT">
|
||||||
|
<PatternLayout pattern="${dfltPattern}"/>
|
||||||
|
<Filters>
|
||||||
|
<ThresholdFilter level="WARN" onMatch="DENY" onMismatch="ACCEPT" />
|
||||||
|
</Filters>
|
||||||
|
</Console>
|
||||||
|
<Console name="STDERR" target="SYSTEM_ERR">
|
||||||
|
<PatternLayout pattern="${dfltPattern}" />
|
||||||
|
</Console>
|
||||||
|
</Appenders>
|
||||||
|
|
||||||
|
<Loggers>
|
||||||
|
<Logger name="io.emeraldpay" level="debug" additivity="false">
|
||||||
|
<AppenderRef ref="STDOUT"/>
|
||||||
|
<AppenderRef ref="STDERR" level="warn"/>
|
||||||
|
</Logger>
|
||||||
|
|
||||||
|
<Root level="warn" additivity="false">
|
||||||
|
<AppenderRef ref="STDOUT"/>
|
||||||
|
<AppenderRef ref="STDERR" level="warn"/>
|
||||||
|
</Root>
|
||||||
|
</Loggers>
|
||||||
|
|
||||||
|
</Configuration>
|
||||||
Reference in New Issue
Block a user