solution: initial version
This commit is contained in:
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
.gradle/
|
||||||
|
build/
|
||||||
|
out/
|
||||||
|
*.iml
|
||||||
108
build.gradle
Normal file
108
build.gradle
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
buildscript {
|
||||||
|
repositories {
|
||||||
|
mavenLocal()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
dependencies {
|
||||||
|
classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.11'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id 'org.jetbrains.kotlin.jvm' version '1.3.11'
|
||||||
|
}
|
||||||
|
|
||||||
|
apply plugin: 'war'
|
||||||
|
apply plugin: 'java'
|
||||||
|
apply plugin: 'groovy'
|
||||||
|
apply plugin: "kotlin"
|
||||||
|
apply plugin: 'idea'
|
||||||
|
apply plugin: 'maven'
|
||||||
|
apply plugin: 'application'
|
||||||
|
|
||||||
|
group = 'io.emeraldpay.dshackle'
|
||||||
|
version = '0.1-SNAPSHOT'
|
||||||
|
|
||||||
|
targetCompatibility = '1.8'
|
||||||
|
sourceCompatibility = '1.8'
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenLocal()
|
||||||
|
mavenCentral()
|
||||||
|
maven { url "http://repo.spring.io/snapshot" }
|
||||||
|
maven { url "http://repo.spring.io/milestone" }
|
||||||
|
maven { url "https://dl.bintray.com/infinitape/etherjar" }
|
||||||
|
}
|
||||||
|
|
||||||
|
configurations {
|
||||||
|
compile.exclude group: "commons-logging"
|
||||||
|
compile.exclude group: "ch.qos.logback"
|
||||||
|
compile.exclude group: "org.slf4j", module: "slf4j-jdk14"
|
||||||
|
compile.exclude group: "org.slf4j", module: "log4j-over-slf4j"
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
compile "io.emeraldpay:emerald-grpc:0.1-SNAPSHOT"
|
||||||
|
|
||||||
|
compile "io.grpc:grpc-protobuf:${grpcVersion}"
|
||||||
|
compile "io.grpc:grpc-stub:${grpcVersion}"
|
||||||
|
compile "io.grpc:grpc-netty:${grpcVersion}"
|
||||||
|
compile "io.netty:netty-tcnative-boringssl-static:2.0.22.Final"
|
||||||
|
|
||||||
|
compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
|
||||||
|
compile "org.jetbrains.kotlin:kotlin-reflect"
|
||||||
|
|
||||||
|
compile "org.springframework:spring-core:$springVersion"
|
||||||
|
compile "org.springframework:spring-context:$springVersion"
|
||||||
|
compile "org.springframework.security:spring-security-core:$springVersion"
|
||||||
|
compile "org.springframework.security:spring-security-web:$springVersion"
|
||||||
|
compile "org.springframework.security:spring-security-config:$springVersion"
|
||||||
|
compile 'io.projectreactor:reactor-core:3.2.9.RELEASE'
|
||||||
|
compile 'io.projectreactor.addons:reactor-extra:3.2.3.RELEASE'
|
||||||
|
compile 'io.projectreactor.kotlin:reactor-kotlin-extensions:1.0.0.M1'
|
||||||
|
|
||||||
|
|
||||||
|
compile "io.infinitape:etherjar-domain:$etherjarVersion"
|
||||||
|
compile "io.infinitape:etherjar-hex:$etherjarVersion"
|
||||||
|
compile "io.infinitape:etherjar-rpc-http:$etherjarVersion"
|
||||||
|
compile "io.infinitape:etherjar-tx:$etherjarVersion"
|
||||||
|
|
||||||
|
compile 'org.apache.httpcomponents:httpmime:4.5.8'
|
||||||
|
compile 'org.apache.httpcomponents:httpclient:4.5.8'
|
||||||
|
compile 'com.fasterxml.jackson.core:jackson-core:2.9.8'
|
||||||
|
compile 'com.fasterxml.jackson.core:jackson-databind:2.9.8'
|
||||||
|
compile 'commons-io:commons-io:2.6'
|
||||||
|
compile 'org.apache.commons:commons-lang3:3.9'
|
||||||
|
compile 'org.apache.commons:commons-collections4:4.3'
|
||||||
|
compile 'javax.annotation:javax.annotation-api:1.3.2'
|
||||||
|
compile 'org.bouncycastle:bcprov-jdk15on:1.61'
|
||||||
|
|
||||||
|
compile("org.springframework.boot:spring-boot-starter:$springBootVersion") {
|
||||||
|
exclude module: 'spring-boot-starter-logging'
|
||||||
|
}
|
||||||
|
|
||||||
|
compile "org.slf4j:slf4j-api:$slf4jVersion"
|
||||||
|
compile "org.apache.logging.log4j:log4j-slf4j-impl:2.11.1"
|
||||||
|
compile "org.slf4j:jul-to-slf4j:$slf4jVersion"
|
||||||
|
compile "org.slf4j:jcl-over-slf4j:$slf4jVersion"
|
||||||
|
|
||||||
|
|
||||||
|
testCompile "org.codehaus.groovy:groovy:$groovyVersion"
|
||||||
|
testCompile "org.spockframework:spock-core:$spockVersion"
|
||||||
|
}
|
||||||
|
|
||||||
|
compileKotlin {
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = "1.8"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
compileTestKotlin {
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = "1.8"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
application {
|
||||||
|
mainClassName = 'io.emeraldpay.dshackle.StarterKt'
|
||||||
|
}
|
||||||
|
|
||||||
21
gradle.properties
Normal file
21
gradle.properties
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# Languages
|
||||||
|
groovyVersion=2.5.5
|
||||||
|
kotlinVersion=1.3.11
|
||||||
|
|
||||||
|
# Main Libs
|
||||||
|
slf4jVersion=1.7.25
|
||||||
|
jacksonVersion=2.9.8
|
||||||
|
grpcVersion=1.20.0
|
||||||
|
protocVersion=3.7.1
|
||||||
|
protobufVersion=3.7.1
|
||||||
|
|
||||||
|
# Core
|
||||||
|
springBootVersion=2.1.4.RELEASE
|
||||||
|
springVersion=5.1.4.RELEASE
|
||||||
|
|
||||||
|
# Our Libs
|
||||||
|
etherjarVersion=0.6.0
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
spockVersion=1.2-groovy-2.5
|
||||||
|
|
||||||
37
src/main/kotlin/io/emeraldpay/dshackle/Config.kt
Normal file
37
src/main/kotlin/io/emeraldpay/dshackle/Config.kt
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
package io.emeraldpay.dshackle
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.Version
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
|
import com.fasterxml.jackson.databind.module.SimpleModule
|
||||||
|
import org.springframework.context.annotation.Bean
|
||||||
|
import org.springframework.context.annotation.Configuration
|
||||||
|
import org.springframework.scheduling.annotation.EnableAsync
|
||||||
|
import org.springframework.scheduling.annotation.EnableScheduling
|
||||||
|
import org.springframework.scheduling.annotation.Scheduled
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@EnableScheduling
|
||||||
|
@EnableAsync
|
||||||
|
open class Config {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
open fun objectMapper(): ObjectMapper {
|
||||||
|
val module = SimpleModule("EmeraldDShackle", Version(1, 0, 0, null, null, null))
|
||||||
|
|
||||||
|
val objectMapper = ObjectMapper()
|
||||||
|
objectMapper.registerModule(module)
|
||||||
|
objectMapper
|
||||||
|
.setDateFormat(SimpleDateFormat("yyyy-MM-dd\'T\'HH:mm:ss.SSS"))
|
||||||
|
.setTimeZone(TimeZone.getTimeZone("UTC"))
|
||||||
|
|
||||||
|
return objectMapper
|
||||||
|
}
|
||||||
|
|
||||||
|
// Temporally hack to let Spring Boot know it has something active (i.e shouldn't shutdown, as non-web server)
|
||||||
|
@Scheduled(fixedRate = 60000)
|
||||||
|
fun readCurrentTime() {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
49
src/main/kotlin/io/emeraldpay/dshackle/GrpcServer.kt
Normal file
49
src/main/kotlin/io/emeraldpay/dshackle/GrpcServer.kt
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
package io.emeraldpay.dshackle
|
||||||
|
|
||||||
|
import io.grpc.Server
|
||||||
|
import io.grpc.ServerBuilder
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired
|
||||||
|
import org.springframework.core.io.ResourceLoader
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
|
import javax.annotation.PostConstruct
|
||||||
|
import javax.annotation.PreDestroy
|
||||||
|
|
||||||
|
@Service
|
||||||
|
open class GrpcServer(
|
||||||
|
@Autowired val rpcs: List<io.grpc.BindableService>,
|
||||||
|
@Autowired val resourceLoader: ResourceLoader
|
||||||
|
) {
|
||||||
|
|
||||||
|
private val log = LoggerFactory.getLogger(GrpcServer::class.java)
|
||||||
|
|
||||||
|
private var server: Server? = null;
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
|
fun start() {
|
||||||
|
log.info("Starting GRPC Server...")
|
||||||
|
val serverBuilder = ServerBuilder.forPort(8090)
|
||||||
|
rpcs.forEach {
|
||||||
|
serverBuilder.addService(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
// serverBuilder
|
||||||
|
// .useTransportSecurity(
|
||||||
|
// resourceLoader.getResource("127.0.0.1.crt").inputStream,
|
||||||
|
// resourceLoader.getResource("127.0.0.1.p8.key").inputStream
|
||||||
|
// )
|
||||||
|
|
||||||
|
val server = serverBuilder.build()
|
||||||
|
this.server = server
|
||||||
|
|
||||||
|
Thread { server.start() }.run()
|
||||||
|
log.info("GRPC Server started")
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreDestroy
|
||||||
|
fun stop() {
|
||||||
|
log.info("Shutting down GRPC Server...")
|
||||||
|
server?.shutdownNow()
|
||||||
|
log.info("GRPC Server shot down")
|
||||||
|
}
|
||||||
|
}
|
||||||
14
src/main/kotlin/io/emeraldpay/dshackle/Starter.kt
Normal file
14
src/main/kotlin/io/emeraldpay/dshackle/Starter.kt
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
package io.emeraldpay.dshackle
|
||||||
|
|
||||||
|
import org.springframework.boot.SpringApplication
|
||||||
|
import org.springframework.boot.autoconfigure.SpringBootApplication
|
||||||
|
import org.springframework.context.annotation.Import
|
||||||
|
|
||||||
|
@SpringBootApplication(scanBasePackages = [ "io.emeraldpay.dshackle" ])
|
||||||
|
@Import(Config::class)
|
||||||
|
open class Starter
|
||||||
|
|
||||||
|
fun main(args: Array<String>) {
|
||||||
|
val app = SpringApplication(Starter::class.java)
|
||||||
|
app.run()
|
||||||
|
}
|
||||||
17
src/main/kotlin/io/emeraldpay/dshackle/rpc/BlockchainRpc.kt
Normal file
17
src/main/kotlin/io/emeraldpay/dshackle/rpc/BlockchainRpc.kt
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
package io.emeraldpay.dshackle.rpc
|
||||||
|
|
||||||
|
import io.emeraldpay.api.proto.BlockchainGrpc
|
||||||
|
import io.emeraldpay.api.proto.BlockchainOuterClass
|
||||||
|
import io.grpc.stub.StreamObserver
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
|
|
||||||
|
@Service
|
||||||
|
class BlockchainRpc(
|
||||||
|
@Autowired private val nativeCall: NativeCall
|
||||||
|
): BlockchainGrpc.BlockchainImplBase() {
|
||||||
|
|
||||||
|
override fun nativeCall(request: BlockchainOuterClass.CallBlockchainRequest, responseObserver: StreamObserver<BlockchainOuterClass.CallBlockchainReplyItem>) {
|
||||||
|
nativeCall.nativeCall(request, responseObserver)
|
||||||
|
}
|
||||||
|
}
|
||||||
113
src/main/kotlin/io/emeraldpay/dshackle/rpc/NativeCall.kt
Normal file
113
src/main/kotlin/io/emeraldpay/dshackle/rpc/NativeCall.kt
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
package io.emeraldpay.dshackle.rpc
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
|
import com.google.protobuf.ByteString
|
||||||
|
import io.emeraldpay.api.proto.BlockchainOuterClass
|
||||||
|
import io.emeraldpay.dshackle.upstream.Upstreams
|
||||||
|
import io.emeraldpay.grpc.Chain
|
||||||
|
import io.grpc.stub.StreamObserver
|
||||||
|
import io.infinitape.etherjar.rpc.json.ResponseJson
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
|
import reactor.core.publisher.toFlux
|
||||||
|
import reactor.core.publisher.toMono
|
||||||
|
import reactor.util.function.Tuples
|
||||||
|
import java.lang.Exception
|
||||||
|
|
||||||
|
@Service
|
||||||
|
class NativeCall(
|
||||||
|
@Autowired private val upstreams: Upstreams,
|
||||||
|
@Autowired private val objectMapper: ObjectMapper
|
||||||
|
) {
|
||||||
|
|
||||||
|
private val log = LoggerFactory.getLogger(NativeCall::class.java)
|
||||||
|
|
||||||
|
private val allowedMethods = listOf(
|
||||||
|
"eth_gasPrice",
|
||||||
|
"eth_blockNumber",
|
||||||
|
"eth_getBalance",
|
||||||
|
"eth_getStorageAt",
|
||||||
|
"eth_getTransactionCount",
|
||||||
|
"eth_getBlockTransactionCountByHash",
|
||||||
|
"eth_getBlockTransactionCountByNumber",
|
||||||
|
"eth_getUncleCountByBlockHash",
|
||||||
|
"eth_getUncleCountByBlockNumber",
|
||||||
|
"eth_getCode",
|
||||||
|
"eth_sendRawTransaction",
|
||||||
|
"eth_call",
|
||||||
|
"eth_estimateGas",
|
||||||
|
"eth_getBlockByHash",
|
||||||
|
"eth_getBlockByNumber",
|
||||||
|
"eth_getTransactionByHash",
|
||||||
|
"eth_getTransactionByBlockHashAndIndex",
|
||||||
|
"eth_getTransactionByBlockNumberAndIndex",
|
||||||
|
"eth_getTransactionReceipt",
|
||||||
|
"eth_getUncleByBlockHashAndIndex",
|
||||||
|
"eth_getUncleByBlockNumberAndIndex"
|
||||||
|
)
|
||||||
|
|
||||||
|
open fun nativeCall(request: BlockchainOuterClass.CallBlockchainRequest, responseObserver: StreamObserver<BlockchainOuterClass.CallBlockchainReplyItem>) {
|
||||||
|
val chain= Chain.byId(request.chain.number)
|
||||||
|
if (chain == Chain.UNSPECIFIED) {
|
||||||
|
throw Exception("Invalid chain id: ${request.chain.number}")
|
||||||
|
}
|
||||||
|
val upstream = upstreams.ethereumUpstream(chain) ?: throw Exception("Chain ${chain.id} is unavailable")
|
||||||
|
request.itemsList.toFlux()
|
||||||
|
.map {
|
||||||
|
val method = it.target
|
||||||
|
val params = it.payload.toStringUtf8()
|
||||||
|
return@map CallContext(it.id, Tuples.of(method, params))
|
||||||
|
}
|
||||||
|
.map {
|
||||||
|
val params = extractParams(it.payload.t2)
|
||||||
|
return@map it.withPayload(Tuples.of(it.payload.t1, params))
|
||||||
|
}
|
||||||
|
.flatMap { ctx ->
|
||||||
|
upstream.execute(ctx.id, ctx.payload.t1, ctx.payload.t2).map { resp ->
|
||||||
|
ctx.withPayload(resp)
|
||||||
|
}.onErrorMap {
|
||||||
|
CallFailure(ctx.id, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.map {
|
||||||
|
BlockchainOuterClass.CallBlockchainReplyItem.newBuilder()
|
||||||
|
.setSucceed(true)
|
||||||
|
.setId(it.id)
|
||||||
|
.setPayload(ByteString.copyFrom(it.payload))
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
.onErrorResume() {
|
||||||
|
val id: Int = if (it != null && CallFailure::class.isInstance(it)) {
|
||||||
|
(it as CallFailure).id
|
||||||
|
} else {
|
||||||
|
log.error("Lost context for a native call", it)
|
||||||
|
0
|
||||||
|
}
|
||||||
|
return@onErrorResume BlockchainOuterClass.CallBlockchainReplyItem.newBuilder()
|
||||||
|
.setSucceed(false)
|
||||||
|
.setId(id)
|
||||||
|
.build()
|
||||||
|
.toMono()
|
||||||
|
}
|
||||||
|
.doOnComplete {
|
||||||
|
responseObserver.onCompleted()
|
||||||
|
}
|
||||||
|
.subscribe {
|
||||||
|
responseObserver.onNext(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun extractParams(jsonParams: String): List<Any> {
|
||||||
|
val req = objectMapper.readValue(jsonParams, List::class.java)
|
||||||
|
return req as List<Any>
|
||||||
|
}
|
||||||
|
|
||||||
|
private class CallContext<T>(val id: Int, val payload: T) {
|
||||||
|
fun <X> withPayload(payload: X): CallContext<X> {
|
||||||
|
return CallContext(id, payload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CallFailure(val id: Int, val reason: Throwable): Exception("Failed to call $id: ${reason.message}")
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package io.emeraldpay.dshackle.upstream
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
|
import io.infinitape.etherjar.rpc.json.ResponseJson
|
||||||
|
import io.infinitape.etherjar.rpc.transport.RpcTransport
|
||||||
|
import reactor.core.publisher.Mono
|
||||||
|
|
||||||
|
class EthereumUpstream(
|
||||||
|
private val rpcTransport: RpcTransport,
|
||||||
|
private val objectMapper: ObjectMapper
|
||||||
|
) {
|
||||||
|
|
||||||
|
fun execute(id: Int, method: String, params: List<Any>): Mono<ByteArray> {
|
||||||
|
return Mono
|
||||||
|
.fromCompletionStage(rpcTransport.execute(method, params, Any::class.java))
|
||||||
|
.map {
|
||||||
|
val resp = ResponseJson<Any, Int>()
|
||||||
|
resp.id = id
|
||||||
|
resp.result = it
|
||||||
|
objectMapper.writer().writeValueAsBytes(resp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
49
src/main/kotlin/io/emeraldpay/dshackle/upstream/Upstreams.kt
Normal file
49
src/main/kotlin/io/emeraldpay/dshackle/upstream/Upstreams.kt
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
package io.emeraldpay.dshackle.upstream
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
|
import io.emeraldpay.grpc.Chain
|
||||||
|
import io.infinitape.etherjar.rpc.transport.DefaultRpcTransport
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired
|
||||||
|
import org.springframework.core.env.Environment
|
||||||
|
import org.springframework.stereotype.Repository
|
||||||
|
import java.net.URI
|
||||||
|
import javax.annotation.PostConstruct
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
class Upstreams(
|
||||||
|
@Autowired val env: Environment,
|
||||||
|
@Autowired private val objectMapper: ObjectMapper
|
||||||
|
) {
|
||||||
|
|
||||||
|
private var seq = 0
|
||||||
|
private val chainMapping = HashMap<Chain, List<EthereumUpstream>>()
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
|
fun start() {
|
||||||
|
env.getProperty("upstream.ethereum")?.let {
|
||||||
|
chainMapping[Chain.ETHEREUM] = listOf(buildClient(it))
|
||||||
|
}
|
||||||
|
env.getProperty("upstream.ethereumclassic")?.let {
|
||||||
|
chainMapping[Chain.ETHEREUM_CLASSIC] = listOf(buildClient(it))
|
||||||
|
}
|
||||||
|
env.getProperty("upstream.morden")?.let {
|
||||||
|
chainMapping[Chain.MORDEN] = listOf(buildClient(it))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildClient(url: String): EthereumUpstream {
|
||||||
|
return EthereumUpstream(
|
||||||
|
DefaultRpcTransport(URI(url)),
|
||||||
|
objectMapper
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun validateUpstream(upstream: EthereumUpstream): Boolean {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun ethereumUpstream(chain: Chain): EthereumUpstream? {
|
||||||
|
val all = chainMapping[chain] ?: return null
|
||||||
|
return all[seq++ % all.size]
|
||||||
|
}
|
||||||
|
}
|
||||||
2
src/main/resources/application.properties
Normal file
2
src/main/resources/application.properties
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
upstream.ethereumclassic=http://localhost:8545
|
||||||
|
upstream.ethereum=http://localhost:8546
|
||||||
Reference in New Issue
Block a user