solution: initial version
This commit is contained in:
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