Merge pull request #6 from emeraldpay/feat/normal-request-flow
refactor request flow
This commit is contained in:
37
build.gradle
37
build.gradle
@@ -81,7 +81,6 @@ dependencies {
|
|||||||
implementation "io.infinitape:etherjar-hex:$etherjarVersion"
|
implementation "io.infinitape:etherjar-hex:$etherjarVersion"
|
||||||
implementation "io.infinitape:etherjar-rpc-http:$etherjarVersion"
|
implementation "io.infinitape:etherjar-rpc-http:$etherjarVersion"
|
||||||
implementation "io.infinitape:etherjar-rpc-ws:$etherjarVersion"
|
implementation "io.infinitape:etherjar-rpc-ws:$etherjarVersion"
|
||||||
implementation "io.infinitape:etherjar-rpc-emerald:$etherjarVersion"
|
|
||||||
implementation "io.infinitape:etherjar-tx:$etherjarVersion"
|
implementation "io.infinitape:etherjar-tx:$etherjarVersion"
|
||||||
implementation 'org.bitcoinj:bitcoinj-core:0.15.8'
|
implementation 'org.bitcoinj:bitcoinj-core:0.15.8'
|
||||||
|
|
||||||
@@ -125,7 +124,7 @@ compileTestKotlin {
|
|||||||
|
|
||||||
test {
|
test {
|
||||||
jvmArgs '-ea'
|
jvmArgs '-ea'
|
||||||
testLogging.showStandardStreams = true
|
testLogging.showStandardStreams = false
|
||||||
testLogging.exceptionFormat = 'full'
|
testLogging.exceptionFormat = 'full'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,3 +199,37 @@ task generateVersion() {
|
|||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Show the list of failed tests and output only for them, helpful for CI
|
||||||
|
ext.failedTests = []
|
||||||
|
tasks.withType(Test) {
|
||||||
|
def stdout = new LinkedList<String>()
|
||||||
|
beforeTest { TestDescriptor td ->
|
||||||
|
stdout.clear()
|
||||||
|
}
|
||||||
|
onOutput { TestDescriptor td, TestOutputEvent toe ->
|
||||||
|
stdout.addAll(toe.getMessage().split('(?m)$'))
|
||||||
|
while (stdout.size() > 100) {
|
||||||
|
stdout.remove()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
afterTest { TestDescriptor descriptor, TestResult result ->
|
||||||
|
if(result.resultType == org.gradle.api.tasks.testing.TestResult.ResultType.FAILURE){
|
||||||
|
failedTests << "${descriptor.className} > ${descriptor.name}"
|
||||||
|
if (!stdout.isEmpty()) {
|
||||||
|
println("-------- ${descriptor.className} > ${descriptor.name} OUTPUT ".padRight(120, "-"))
|
||||||
|
stdout.each { print(it) }
|
||||||
|
println("================".padRight(120, "="))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
gradle.buildFinished {
|
||||||
|
if(!failedTests.empty){
|
||||||
|
println "Failed tests for ${project.name}:"
|
||||||
|
failedTests.each { failedTest ->
|
||||||
|
println failedTest
|
||||||
|
}
|
||||||
|
println ""
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -93,6 +93,7 @@ class BlocksRedisCache(
|
|||||||
Instant.ofEpochMilli(meta.timestamp),
|
Instant.ofEpochMilli(meta.timestamp),
|
||||||
false,
|
false,
|
||||||
value.value.toByteArray(),
|
value.value.toByteArray(),
|
||||||
|
null,
|
||||||
meta.txHashesList.map {
|
meta.txHashesList.map {
|
||||||
TxId(it.toByteArray())
|
TxId(it.toByteArray())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import io.emeraldpay.dshackle.data.TxContainer
|
|||||||
import io.emeraldpay.dshackle.data.TxId
|
import io.emeraldpay.dshackle.data.TxId
|
||||||
import io.emeraldpay.dshackle.reader.CompoundReader
|
import io.emeraldpay.dshackle.reader.CompoundReader
|
||||||
import io.emeraldpay.dshackle.reader.Reader
|
import io.emeraldpay.dshackle.reader.Reader
|
||||||
|
import io.emeraldpay.dshackle.upstream.ethereum.EthereumFullBlocksReader
|
||||||
import io.infinitape.etherjar.rpc.json.BlockJson
|
import io.infinitape.etherjar.rpc.json.BlockJson
|
||||||
import io.infinitape.etherjar.rpc.json.TransactionJson
|
import io.infinitape.etherjar.rpc.json.TransactionJson
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
@@ -158,11 +159,11 @@ open class Caches(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getFullBlocks(): Reader<BlockId, BlockContainer> {
|
fun getFullBlocks(): Reader<BlockId, BlockContainer> {
|
||||||
return EthereumBlocksWithTxCache(objectMapper, blocksByHash, txsByHash)
|
return EthereumFullBlocksReader(objectMapper, blocksByHash, txsByHash)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getFullBlocksByHeight(): Reader<Long, BlockContainer> {
|
fun getFullBlocksByHeight(): Reader<Long, BlockContainer> {
|
||||||
return BlockByHeight(blocksByHeight, EthereumBlocksWithTxCache(objectMapper, blocksByHash, txsByHash))
|
return BlockByHeight(blocksByHeight, EthereumFullBlocksReader(objectMapper, blocksByHash, txsByHash))
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class Tag {
|
enum class Tag {
|
||||||
|
|||||||
@@ -59,7 +59,10 @@ class TxRedisCache(
|
|||||||
fun toProto(value: TxContainer): ByteArray {
|
fun toProto(value: TxContainer): ByteArray {
|
||||||
val meta = CachesProto.TxMeta.newBuilder()
|
val meta = CachesProto.TxMeta.newBuilder()
|
||||||
.setHash(ByteString.copyFrom(value.hash.value))
|
.setHash(ByteString.copyFrom(value.hash.value))
|
||||||
.setHeight(value.height)
|
|
||||||
|
value.height?.let {
|
||||||
|
meta.setHeight(it)
|
||||||
|
}
|
||||||
|
|
||||||
value.blockId?.value?.let {
|
value.blockId?.value?.let {
|
||||||
meta.setBlockHash(ByteString.copyFrom(it))
|
meta.setBlockHash(ByteString.copyFrom(it))
|
||||||
|
|||||||
@@ -86,19 +86,21 @@ class UpstreamsConfig {
|
|||||||
|
|
||||||
open class UpstreamConnection
|
open class UpstreamConnection
|
||||||
|
|
||||||
|
open class RpcConnection : UpstreamConnection() {
|
||||||
|
var rpc: HttpEndpoint? = null
|
||||||
|
}
|
||||||
|
|
||||||
class GrpcConnection : UpstreamConnection() {
|
class GrpcConnection : UpstreamConnection() {
|
||||||
var host: String? = null
|
var host: String? = null
|
||||||
var port: Int = 0
|
var port: Int = 0
|
||||||
var auth: AuthConfig.ClientTlsAuth? = null
|
var auth: AuthConfig.ClientTlsAuth? = null
|
||||||
}
|
}
|
||||||
|
|
||||||
class EthereumConnection : UpstreamConnection() {
|
class EthereumConnection : RpcConnection() {
|
||||||
var rpc: HttpEndpoint? = null
|
|
||||||
var ws: WsEndpoint? = null
|
var ws: WsEndpoint? = null
|
||||||
}
|
}
|
||||||
|
|
||||||
class BitcoinConnection : UpstreamConnection() {
|
class BitcoinConnection : RpcConnection() {
|
||||||
var rpc: HttpEndpoint? = null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class HttpEndpoint(val url: URI) {
|
class HttpEndpoint(val url: URI) {
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import com.fasterxml.jackson.databind.ObjectMapper
|
|||||||
import io.infinitape.etherjar.rpc.json.BlockJson
|
import io.infinitape.etherjar.rpc.json.BlockJson
|
||||||
import io.infinitape.etherjar.rpc.json.TransactionJson
|
import io.infinitape.etherjar.rpc.json.TransactionJson
|
||||||
import io.infinitape.etherjar.rpc.json.TransactionRefJson
|
import io.infinitape.etherjar.rpc.json.TransactionRefJson
|
||||||
|
import org.apache.commons.codec.binary.Hex
|
||||||
import java.math.BigInteger
|
import java.math.BigInteger
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
|
|
||||||
@@ -30,12 +31,13 @@ class BlockContainer(
|
|||||||
val timestamp: Instant,
|
val timestamp: Instant,
|
||||||
val full: Boolean,
|
val full: Boolean,
|
||||||
json: ByteArray?,
|
json: ByteArray?,
|
||||||
|
val parsed: Any?,
|
||||||
val transactions: List<TxId> = emptyList()
|
val transactions: List<TxId> = emptyList()
|
||||||
) : SourceContainer(json) {
|
) : SourceContainer(json, parsed) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun from(block: BlockJson<*>, objectMapper: ObjectMapper): BlockContainer {
|
fun from(block: BlockJson<*>, raw: ByteArray): BlockContainer {
|
||||||
val hasTransactions = block.transactions?.filterIsInstance<TransactionJson>()?.count() ?: 0 > 0
|
val hasTransactions = block.transactions?.filterIsInstance<TransactionJson>()?.count() ?: 0 > 0
|
||||||
return BlockContainer(
|
return BlockContainer(
|
||||||
block.number,
|
block.number,
|
||||||
@@ -43,10 +45,26 @@ class BlockContainer(
|
|||||||
block.totalDifficulty,
|
block.totalDifficulty,
|
||||||
block.timestamp,
|
block.timestamp,
|
||||||
hasTransactions,
|
hasTransactions,
|
||||||
objectMapper.writeValueAsBytes(block),
|
raw,
|
||||||
|
block,
|
||||||
block.transactions?.map { TxId.from(it.hash) } ?: emptyList()
|
block.transactions?.map { TxId.from(it.hash) } ?: emptyList()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun from(block: BlockJson<*>, objectMapper: ObjectMapper): BlockContainer {
|
||||||
|
return from(block, objectMapper.writeValueAsBytes(block))
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun from(raw: ByteArray, objectMapper: ObjectMapper): BlockContainer {
|
||||||
|
val block = objectMapper.readValue(raw, BlockJson::class.java)
|
||||||
|
return from(block, raw)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return "Block $height = $hash"
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
|
|||||||
@@ -25,6 +25,11 @@ class BlockId(
|
|||||||
) : HashId(value) {
|
) : HashId(value) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
@JvmStatic
|
||||||
|
fun from(hash: ByteArray): BlockId {
|
||||||
|
return BlockId(hash)
|
||||||
|
}
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun from(hash: BlockHash): BlockId {
|
fun from(hash: BlockHash): BlockId {
|
||||||
return BlockId(hash.bytes)
|
return BlockId(hash.bytes)
|
||||||
|
|||||||
@@ -40,6 +40,10 @@ open class HashId(
|
|||||||
return String(hex)
|
return String(hex)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun toHexWithPrefix(): String {
|
||||||
|
return "0x" + toHex()
|
||||||
|
}
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
if (this === other) return true
|
if (this === other) return true
|
||||||
if (other !is HashId) return false
|
if (other !is HashId) return false
|
||||||
|
|||||||
@@ -16,10 +16,24 @@
|
|||||||
*/
|
*/
|
||||||
package io.emeraldpay.dshackle.data
|
package io.emeraldpay.dshackle.data
|
||||||
|
|
||||||
|
import java.lang.ClassCastException
|
||||||
|
|
||||||
abstract class SourceContainer(
|
abstract class SourceContainer(
|
||||||
val json: ByteArray?
|
val json: ByteArray?,
|
||||||
|
private val parsed: Any?
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
fun <T> getParsed(clazz: Class<T>): T? {
|
||||||
|
if (parsed == null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
if (clazz.isAssignableFrom(parsed.javaClass)) {
|
||||||
|
return parsed as T
|
||||||
|
}
|
||||||
|
throw ClassCastException("Cannot cast ${parsed.javaClass} to $clazz")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
if (this === other) return true
|
if (this === other) return true
|
||||||
if (other !is SourceContainer) return false
|
if (other !is SourceContainer) return false
|
||||||
|
|||||||
@@ -20,20 +20,26 @@ import com.fasterxml.jackson.databind.ObjectMapper
|
|||||||
import io.infinitape.etherjar.rpc.json.TransactionJson
|
import io.infinitape.etherjar.rpc.json.TransactionJson
|
||||||
|
|
||||||
class TxContainer(
|
class TxContainer(
|
||||||
val height: Long,
|
val height: Long?,
|
||||||
val hash: TxId,
|
val hash: TxId,
|
||||||
val blockId: BlockId?,
|
val blockId: BlockId?,
|
||||||
json: ByteArray?
|
json: ByteArray?,
|
||||||
) : SourceContainer(json) {
|
parsed: Any? = null
|
||||||
|
) : SourceContainer(json, parsed) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun from(tx: TransactionJson, objectMapper: ObjectMapper): TxContainer {
|
fun from(tx: TransactionJson, objectMapper: ObjectMapper): TxContainer {
|
||||||
|
return from(tx, objectMapper.writeValueAsBytes(tx))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun from(tx: TransactionJson, raw: ByteArray): TxContainer {
|
||||||
return TxContainer(
|
return TxContainer(
|
||||||
tx.blockNumber,
|
tx.blockNumber,
|
||||||
TxId.from(tx.hash),
|
TxId.from(tx.hash),
|
||||||
BlockId.from(tx.blockHash),
|
tx.blockHash?.let { BlockId.from(it) },
|
||||||
objectMapper.writeValueAsBytes(tx)
|
raw,
|
||||||
|
tx
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,13 +32,17 @@ open class AlwaysQuorum: CallQuorum {
|
|||||||
return resolved
|
return resolved
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun record(response: ByteArray, upstream: Upstream<*>): Boolean {
|
override fun isFailed(): Boolean {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun record(response: ByteArray, upstream: Upstream): Boolean {
|
||||||
result = response
|
result = response
|
||||||
resolved = true
|
resolved = true
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun record(error: RpcException, upstream: Upstream<*>) {
|
override fun record(error: RpcException, upstream: Upstream) {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getResult(): ByteArray? {
|
override fun getResult(): ByteArray? {
|
||||||
|
|||||||
@@ -16,14 +16,15 @@
|
|||||||
*/
|
*/
|
||||||
package io.emeraldpay.dshackle.quorum
|
package io.emeraldpay.dshackle.quorum
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
import io.emeraldpay.dshackle.upstream.Head
|
import io.emeraldpay.dshackle.upstream.Head
|
||||||
import io.emeraldpay.dshackle.upstream.Upstream
|
import io.emeraldpay.dshackle.upstream.Upstream
|
||||||
import io.infinitape.etherjar.rpc.JacksonRpcConverter
|
import io.infinitape.etherjar.rpc.JacksonRpcConverter
|
||||||
|
|
||||||
open class BroadcastQuorum(
|
open class BroadcastQuorum(
|
||||||
jacksonRpcConverter: JacksonRpcConverter,
|
objectMapper: ObjectMapper,
|
||||||
val quorum: Int = 3
|
val quorum: Int = 3
|
||||||
): CallQuorum, ValueAwareQuorum<String>(jacksonRpcConverter, String::class.java) {
|
) : CallQuorum, ValueAwareQuorum<String>(objectMapper, String::class.java) {
|
||||||
|
|
||||||
private var result: ByteArray? = null
|
private var result: ByteArray? = null
|
||||||
private var txid: String? = null
|
private var txid: String? = null
|
||||||
@@ -36,11 +37,15 @@ open class BroadcastQuorum(
|
|||||||
return calls >= quorum
|
return calls >= quorum
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun isFailed(): Boolean {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
override fun getResult(): ByteArray? {
|
override fun getResult(): ByteArray? {
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun recordValue(response: ByteArray, responseValue: String?, upstream: Upstream<*>) {
|
override fun recordValue(response: ByteArray, responseValue: String?, upstream: Upstream) {
|
||||||
calls++
|
calls++
|
||||||
if (txid == null && responseValue != null) {
|
if (txid == null && responseValue != null) {
|
||||||
txid = responseValue
|
txid = responseValue
|
||||||
@@ -48,7 +53,7 @@ open class BroadcastQuorum(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun recordError(response: ByteArray?, errorMessage: String?, upstream: Upstream<*>) {
|
override fun recordError(response: ByteArray?, errorMessage: String?, upstream: Upstream) {
|
||||||
// can be "message: known transaction: TXID", "Transaction with the same hash was already imported" or "message: Nonce too low"
|
// can be "message: known transaction: TXID", "Transaction with the same hash was already imported" or "message: Nonce too low"
|
||||||
calls++
|
calls++
|
||||||
if (result == null) {
|
if (result == null) {
|
||||||
|
|||||||
@@ -31,8 +31,10 @@ interface CallQuorum {
|
|||||||
fun init(head: Head)
|
fun init(head: Head)
|
||||||
|
|
||||||
fun isResolved(): Boolean
|
fun isResolved(): Boolean
|
||||||
fun record(response: ByteArray, upstream: Upstream<*>): Boolean
|
fun isFailed(): Boolean
|
||||||
fun record(error: RpcException, upstream: Upstream<*>)
|
|
||||||
|
fun record(response: ByteArray, upstream: Upstream): Boolean
|
||||||
|
fun record(error: RpcException, upstream: Upstream)
|
||||||
fun getResult(): ByteArray?
|
fun getResult(): ByteArray?
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -42,8 +44,8 @@ interface CallQuorum {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun asReducer(): BiFunction<CallQuorum, Tuple2<ByteArray, Upstream<*>>, CallQuorum> {
|
fun asReducer(): BiFunction<CallQuorum, Tuple2<ByteArray, Upstream>, CallQuorum> {
|
||||||
return BiFunction<CallQuorum, Tuple2<ByteArray, Upstream<*>>, CallQuorum> { a, b ->
|
return BiFunction<CallQuorum, Tuple2<ByteArray, Upstream>, CallQuorum> { a, b ->
|
||||||
a.record(b.t1, b.t2)
|
a.record(b.t1, b.t2)
|
||||||
return@BiFunction a
|
return@BiFunction a
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,15 +16,16 @@
|
|||||||
*/
|
*/
|
||||||
package io.emeraldpay.dshackle.quorum
|
package io.emeraldpay.dshackle.quorum
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
import io.emeraldpay.dshackle.upstream.Head
|
import io.emeraldpay.dshackle.upstream.Head
|
||||||
import io.emeraldpay.dshackle.upstream.Upstream
|
import io.emeraldpay.dshackle.upstream.Upstream
|
||||||
import io.infinitape.etherjar.rpc.JacksonRpcConverter
|
import io.infinitape.etherjar.rpc.JacksonRpcConverter
|
||||||
import io.infinitape.etherjar.rpc.RpcException
|
import io.infinitape.etherjar.rpc.RpcException
|
||||||
|
|
||||||
open class NonEmptyQuorum(
|
open class NonEmptyQuorum(
|
||||||
jacksonRpcConverter: JacksonRpcConverter,
|
objectMapper: ObjectMapper,
|
||||||
val maxTries: Int = 3
|
val maxTries: Int = 3
|
||||||
): CallQuorum, ValueAwareQuorum<Any>(jacksonRpcConverter, Any::class.java) {
|
) : CallQuorum, ValueAwareQuorum<Any>(objectMapper, Any::class.java) {
|
||||||
|
|
||||||
private var result: ByteArray? = null
|
private var result: ByteArray? = null
|
||||||
private var tries: Int = 0
|
private var tries: Int = 0
|
||||||
@@ -33,10 +34,14 @@ open class NonEmptyQuorum(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun isResolved(): Boolean {
|
override fun isResolved(): Boolean {
|
||||||
return result != null || tries >= maxTries
|
return result != null
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun recordValue(response: ByteArray, responseValue: Any?, upstream: Upstream<*>) {
|
override fun isFailed(): Boolean {
|
||||||
|
return tries >= maxTries
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun recordValue(response: ByteArray, responseValue: Any?, upstream: Upstream) {
|
||||||
tries++
|
tries++
|
||||||
if (responseValue != null) {
|
if (responseValue != null) {
|
||||||
result = response
|
result = response
|
||||||
@@ -47,10 +52,12 @@ open class NonEmptyQuorum(
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun recordError(response: ByteArray?, errorMessage: String?, upstream: Upstream<*>) {
|
override fun recordError(response: ByteArray?, errorMessage: String?, upstream: Upstream) {
|
||||||
|
tries++
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun record(error: RpcException, upstream: Upstream<*>) {
|
override fun record(error: RpcException, upstream: Upstream) {
|
||||||
|
tries++
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -16,6 +16,7 @@
|
|||||||
*/
|
*/
|
||||||
package io.emeraldpay.dshackle.quorum
|
package io.emeraldpay.dshackle.quorum
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
import io.emeraldpay.dshackle.upstream.Head
|
import io.emeraldpay.dshackle.upstream.Head
|
||||||
import io.emeraldpay.dshackle.upstream.Upstream
|
import io.emeraldpay.dshackle.upstream.Upstream
|
||||||
import io.infinitape.etherjar.hex.HexQuantity
|
import io.infinitape.etherjar.hex.HexQuantity
|
||||||
@@ -25,9 +26,9 @@ import java.util.concurrent.locks.ReentrantLock
|
|||||||
import kotlin.concurrent.withLock
|
import kotlin.concurrent.withLock
|
||||||
|
|
||||||
open class NonceQuorum(
|
open class NonceQuorum(
|
||||||
jacksonRpcConverter: JacksonRpcConverter,
|
objectMapper: ObjectMapper,
|
||||||
val tries: Int = 3
|
val tries: Int = 3
|
||||||
): CallQuorum, ValueAwareQuorum<String>(jacksonRpcConverter, String::class.java) {
|
) : CallQuorum, ValueAwareQuorum<String>(objectMapper, String::class.java) {
|
||||||
|
|
||||||
private val lock = ReentrantLock()
|
private val lock = ReentrantLock()
|
||||||
private var resultValue = 0L
|
private var resultValue = 0L
|
||||||
@@ -40,11 +41,15 @@ open class NonceQuorum(
|
|||||||
|
|
||||||
override fun isResolved(): Boolean {
|
override fun isResolved(): Boolean {
|
||||||
lock.withLock {
|
lock.withLock {
|
||||||
return receivedTimes >= tries || errors >= tries
|
return receivedTimes >= tries && !isFailed()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun recordValue(response: ByteArray, responseValue: String?, upstream: Upstream<*>) {
|
override fun isFailed(): Boolean {
|
||||||
|
return errors >= tries
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun recordValue(response: ByteArray, responseValue: String?, upstream: Upstream) {
|
||||||
val value = responseValue?.let { str ->
|
val value = responseValue?.let { str ->
|
||||||
HexQuantity.from(str).value.toLong()
|
HexQuantity.from(str).value.toLong()
|
||||||
}
|
}
|
||||||
@@ -63,11 +68,7 @@ open class NonceQuorum(
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun recordError(response: ByteArray?, errorMessage: String?, upstream: Upstream<*>) {
|
override fun recordError(response: ByteArray?, errorMessage: String?, upstream: Upstream) {
|
||||||
errors++
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun record(error: RpcException, upstream: Upstream<*>) {
|
|
||||||
errors++
|
errors++
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,11 @@ class NotLaggingQuorum(val maxLag: Long = 0): CallQuorum {
|
|||||||
return result.get() != null
|
return result.get() != null
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun record(response: ByteArray, upstream: Upstream<*>): Boolean {
|
override fun isFailed(): Boolean {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun record(response: ByteArray, upstream: Upstream): Boolean {
|
||||||
val lagging = upstream.getLag() > maxLag
|
val lagging = upstream.getLag() > maxLag
|
||||||
if (!lagging) {
|
if (!lagging) {
|
||||||
result.set(response)
|
result.set(response)
|
||||||
@@ -41,10 +45,9 @@ class NotLaggingQuorum(val maxLag: Long = 0): CallQuorum {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun record(error: RpcException, upstream: Upstream<*>) {
|
override fun record(error: RpcException, upstream: Upstream) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
override fun getResult(): ByteArray {
|
override fun getResult(): ByteArray {
|
||||||
return result.get()
|
return result.get()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2020 EmeraldPay, Inc
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package io.emeraldpay.dshackle.quorum
|
||||||
|
|
||||||
|
import io.emeraldpay.dshackle.reader.Reader
|
||||||
|
import io.emeraldpay.dshackle.upstream.ApiSource
|
||||||
|
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
|
||||||
|
|
||||||
|
// creates instance of a Quorum based reader
|
||||||
|
interface QuorumReaderFactory {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun default(): QuorumReaderFactory {
|
||||||
|
return Default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun create(apis: ApiSource, quorum: CallQuorum): Reader<JsonRpcRequest, QuorumRpcReader.Result>
|
||||||
|
|
||||||
|
class Default : QuorumReaderFactory {
|
||||||
|
override fun create(apis: ApiSource, quorum: CallQuorum): Reader<JsonRpcRequest, QuorumRpcReader.Result> {
|
||||||
|
return QuorumRpcReader(apis, quorum)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
105
src/main/kotlin/io/emeraldpay/dshackle/quorum/QuorumRpcReader.kt
Normal file
105
src/main/kotlin/io/emeraldpay/dshackle/quorum/QuorumRpcReader.kt
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2020 EmeraldPay, Inc
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package io.emeraldpay.dshackle.quorum
|
||||||
|
|
||||||
|
import io.emeraldpay.dshackle.reader.Reader
|
||||||
|
import io.emeraldpay.dshackle.upstream.ApiSource
|
||||||
|
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
|
||||||
|
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
|
||||||
|
import io.infinitape.etherjar.rpc.RpcException
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import reactor.core.publisher.Flux
|
||||||
|
import reactor.core.publisher.Mono
|
||||||
|
import reactor.util.function.Tuples
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes request with applying Quorum
|
||||||
|
*/
|
||||||
|
class QuorumRpcReader(
|
||||||
|
private val apis: ApiSource,
|
||||||
|
private val quorum: CallQuorum
|
||||||
|
) : Reader<JsonRpcRequest, QuorumRpcReader.Result> {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val log = LoggerFactory.getLogger(QuorumRpcReader::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun read(key: JsonRpcRequest): Mono<QuorumRpcReader.Result> {
|
||||||
|
apis.request(1)
|
||||||
|
|
||||||
|
// uses a mix of retry strategy and managed Publisher for calls.
|
||||||
|
// retry is used when an error happened
|
||||||
|
// but if no error received, we check quorum and if not enough data received we request more
|
||||||
|
// eventually source of upstreams is Completed (or something Errored) and if finalizes the result
|
||||||
|
|
||||||
|
val retrySpec = reactor.util.retry.Retry.from { signal ->
|
||||||
|
signal.takeUntil {
|
||||||
|
it.totalRetries() >= 3 || quorum.isResolved() || quorum.isFailed()
|
||||||
|
}.doOnNext {
|
||||||
|
// need one more API source if retried
|
||||||
|
apis.request(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Flux.from(apis)
|
||||||
|
.flatMap { api ->
|
||||||
|
api.getApi().read(key)
|
||||||
|
.flatMap(JsonRpcResponse::requireResult)
|
||||||
|
// on error notify quorum, it may use error message or other details
|
||||||
|
.doOnError { err ->
|
||||||
|
if (err is RpcException) {
|
||||||
|
quorum.record(err, api)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.map { Tuples.of(it, api) }
|
||||||
|
}
|
||||||
|
.retryWhen(retrySpec)
|
||||||
|
// record all correct responses until quorum reached
|
||||||
|
.reduce(quorum, { res, a ->
|
||||||
|
if (res.record(a.t1, a.t2)) {
|
||||||
|
apis.resolve()
|
||||||
|
} else {
|
||||||
|
apis.request(1)
|
||||||
|
}
|
||||||
|
res
|
||||||
|
})
|
||||||
|
// if last call resulted in error it's still possible that request was resolved correctly. i.e. for BroadcastQuorum
|
||||||
|
.onErrorResume { err ->
|
||||||
|
if (quorum.isResolved()) {
|
||||||
|
Mono.just(quorum)
|
||||||
|
} else {
|
||||||
|
Mono.error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.doOnNext {
|
||||||
|
if (!it.isResolved()) {
|
||||||
|
log.debug("No quorum for ${key.method} as ${quorum}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// return nothing if not resolved
|
||||||
|
.filter { it.isResolved() }
|
||||||
|
.map {
|
||||||
|
// TODO find actual quorum number
|
||||||
|
QuorumRpcReader.Result(it.getResult()!!, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class Result(
|
||||||
|
val value: ByteArray,
|
||||||
|
val quorum: Int
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -16,23 +16,24 @@
|
|||||||
*/
|
*/
|
||||||
package io.emeraldpay.dshackle.quorum
|
package io.emeraldpay.dshackle.quorum
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
import io.emeraldpay.dshackle.upstream.Upstream
|
import io.emeraldpay.dshackle.upstream.Upstream
|
||||||
import io.infinitape.etherjar.rpc.JacksonRpcConverter
|
import io.infinitape.etherjar.rpc.JacksonRpcConverter
|
||||||
import io.infinitape.etherjar.rpc.RpcException
|
import io.infinitape.etherjar.rpc.RpcException
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
|
|
||||||
abstract class ValueAwareQuorum<T>(
|
abstract class ValueAwareQuorum<T>(
|
||||||
val jacksonRpcConverter: JacksonRpcConverter,
|
val objectMapper: ObjectMapper,
|
||||||
val clazz: Class<T>
|
val clazz: Class<T>
|
||||||
): CallQuorum {
|
): CallQuorum {
|
||||||
|
|
||||||
private val log = LoggerFactory.getLogger(ValueAwareQuorum::class.java)
|
private val log = LoggerFactory.getLogger(ValueAwareQuorum::class.java)
|
||||||
|
|
||||||
fun extractValue(response: ByteArray, clazz: Class<T>): T? {
|
fun extractValue(response: ByteArray, clazz: Class<T>): T? {
|
||||||
return jacksonRpcConverter.fromJson(response.inputStream(), clazz)
|
return objectMapper.readValue(response.inputStream(), clazz)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun record(response: ByteArray, upstream: Upstream<*>): Boolean {
|
override fun record(response: ByteArray, upstream: Upstream): Boolean {
|
||||||
try {
|
try {
|
||||||
val value = extractValue(response, clazz)
|
val value = extractValue(response, clazz)
|
||||||
recordValue(response, value, upstream)
|
recordValue(response, value, upstream)
|
||||||
@@ -44,12 +45,12 @@ abstract class ValueAwareQuorum<T>(
|
|||||||
return isResolved();
|
return isResolved();
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun record(error: RpcException, upstream: Upstream<*>) {
|
override fun record(error: RpcException, upstream: Upstream) {
|
||||||
recordError(null, error.rpcMessage, upstream)
|
recordError(null, error.rpcMessage, upstream)
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract fun recordValue(response: ByteArray, responseValue: T?, upstream: Upstream<*>)
|
abstract fun recordValue(response: ByteArray, responseValue: T?, upstream: Upstream)
|
||||||
|
|
||||||
abstract fun recordError(response: ByteArray?, errorMessage: String?, upstream: Upstream<*>)
|
abstract fun recordError(response: ByteArray?, errorMessage: String?, upstream: Upstream)
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -18,7 +18,7 @@ package io.emeraldpay.dshackle.reader
|
|||||||
|
|
||||||
import reactor.core.publisher.Mono
|
import reactor.core.publisher.Mono
|
||||||
|
|
||||||
interface Reader<K, D> {
|
interface Reader<in K, D> {
|
||||||
|
|
||||||
fun read(key: K): Mono<D>
|
fun read(key: K): Mono<D>
|
||||||
|
|
||||||
|
|||||||
@@ -29,15 +29,15 @@ import reactor.core.publisher.Mono
|
|||||||
|
|
||||||
@Service
|
@Service
|
||||||
class Describe(
|
class Describe(
|
||||||
@Autowired private val upstreams: Upstreams,
|
@Autowired private val multistreamHolder: MultistreamHolder,
|
||||||
@Autowired private val subscribeStatus: SubscribeStatus
|
@Autowired private val subscribeStatus: SubscribeStatus
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun describe(requestMono: Mono<BlockchainOuterClass.DescribeRequest>): Mono<BlockchainOuterClass.DescribeResponse> {
|
fun describe(requestMono: Mono<BlockchainOuterClass.DescribeRequest>): Mono<BlockchainOuterClass.DescribeResponse> {
|
||||||
return requestMono.map { _ ->
|
return requestMono.map { _ ->
|
||||||
val resp = BlockchainOuterClass.DescribeResponse.newBuilder()
|
val resp = BlockchainOuterClass.DescribeResponse.newBuilder()
|
||||||
upstreams.getAvailable().forEach { chain ->
|
multistreamHolder.getAvailable().forEach { chain ->
|
||||||
upstreams.getUpstream(chain)?.let { chainUpstreams ->
|
multistreamHolder.getUpstream(chain)?.let { chainUpstreams ->
|
||||||
val status = subscribeStatus.chainStatus(chain, chainUpstreams.getAll())
|
val status = subscribeStatus.chainStatus(chain, chainUpstreams.getAll())
|
||||||
val targets = chainUpstreams.getMethods().getSupportedMethods()
|
val targets = chainUpstreams.getMethods().getSupportedMethods()
|
||||||
val chainDescription = BlockchainOuterClass.DescribeChain.newBuilder()
|
val chainDescription = BlockchainOuterClass.DescribeChain.newBuilder()
|
||||||
|
|||||||
@@ -24,8 +24,14 @@ import io.emeraldpay.dshackle.SilentException
|
|||||||
import io.emeraldpay.dshackle.upstream.*
|
import io.emeraldpay.dshackle.upstream.*
|
||||||
import io.emeraldpay.dshackle.quorum.AlwaysQuorum
|
import io.emeraldpay.dshackle.quorum.AlwaysQuorum
|
||||||
import io.emeraldpay.dshackle.quorum.CallQuorum
|
import io.emeraldpay.dshackle.quorum.CallQuorum
|
||||||
|
import io.emeraldpay.dshackle.quorum.QuorumReaderFactory
|
||||||
|
import io.emeraldpay.dshackle.quorum.QuorumRpcReader
|
||||||
|
import io.emeraldpay.dshackle.upstream.ethereum.EthereumMultistream
|
||||||
|
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
|
||||||
|
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
|
||||||
import io.emeraldpay.grpc.Chain
|
import io.emeraldpay.grpc.Chain
|
||||||
import io.infinitape.etherjar.rpc.RpcException
|
import io.infinitape.etherjar.rpc.RpcException
|
||||||
|
import io.infinitape.etherjar.rpc.RpcResponseError
|
||||||
import org.apache.commons.lang3.StringUtils
|
import org.apache.commons.lang3.StringUtils
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import org.springframework.beans.factory.annotation.Autowired
|
import org.springframework.beans.factory.annotation.Autowired
|
||||||
@@ -36,21 +42,23 @@ import java.lang.Exception
|
|||||||
|
|
||||||
@Service
|
@Service
|
||||||
open class NativeCall(
|
open class NativeCall(
|
||||||
@Autowired private val upstreams: Upstreams,
|
@Autowired private val multistreamHolder: MultistreamHolder,
|
||||||
@Autowired private val objectMapper: ObjectMapper
|
@Autowired private val objectMapper: ObjectMapper
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private val log = LoggerFactory.getLogger(NativeCall::class.java)
|
private val log = LoggerFactory.getLogger(NativeCall::class.java)
|
||||||
|
|
||||||
|
var quorumReaderFactory: QuorumReaderFactory = QuorumReaderFactory.default()
|
||||||
|
|
||||||
open fun nativeCall(requestMono: Mono<BlockchainOuterClass.NativeCallRequest>): Flux<BlockchainOuterClass.NativeCallReplyItem> {
|
open fun nativeCall(requestMono: Mono<BlockchainOuterClass.NativeCallRequest>): Flux<BlockchainOuterClass.NativeCallReplyItem> {
|
||||||
return requestMono.flatMapMany(this::prepareCall)
|
return requestMono.flatMapMany(this::prepareCall)
|
||||||
.map(this::setupCallParams)
|
.map(this::setupCallParams)
|
||||||
.parallel()
|
.parallel()
|
||||||
.flatMap(this::fetch)
|
.flatMap(this::fetch)
|
||||||
.sequential()
|
.sequential()
|
||||||
.map(this::buildResponse)
|
.map(this::buildResponse)
|
||||||
.doOnError { e -> log.warn("Error during native call: ${e.message}") }
|
.doOnError { e -> log.warn("Error during native call: ${e.message}") }
|
||||||
.onErrorResume(this::processException)
|
.onErrorResume(this::processException)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setupCallParams(it: CallContext<RawCallDetails>): CallContext<ParsedCallDetails> {
|
fun setupCallParams(it: CallContext<RawCallDetails>): CallContext<ParsedCallDetails> {
|
||||||
@@ -87,17 +95,17 @@ open class NativeCall(
|
|||||||
return Flux.error(CallFailure(0, SilentException.UnsupportedBlockchain(request.chain.number)))
|
return Flux.error(CallFailure(0, SilentException.UnsupportedBlockchain(request.chain.number)))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!upstreams.isAvailable(chain)) {
|
if (!multistreamHolder.isAvailable(chain)) {
|
||||||
return Flux.error(CallFailure(0, SilentException.UnsupportedBlockchain(request.chain.number)))
|
return Flux.error(CallFailure(0, SilentException.UnsupportedBlockchain(request.chain.number)))
|
||||||
}
|
}
|
||||||
|
|
||||||
val upstream = upstreams.getUpstream(chain)
|
val upstream = multistreamHolder.getUpstream(chain)
|
||||||
?: return Flux.error(CallFailure(0, SilentException.UnsupportedBlockchain(chain)))
|
?: return Flux.error(CallFailure(0, SilentException.UnsupportedBlockchain(chain)))
|
||||||
|
|
||||||
return prepareCall(request, upstream)
|
return prepareCall(request, upstream)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun prepareCall(request: BlockchainOuterClass.NativeCallRequest, upstream: AggregatedUpstream<*>): Flux<CallContext<RawCallDetails>> {
|
fun prepareCall(request: BlockchainOuterClass.NativeCallRequest, upstream: Multistream): Flux<CallContext<RawCallDetails>> {
|
||||||
return request.itemsList.toFlux().map {
|
return request.itemsList.toFlux().map {
|
||||||
val method = it.method
|
val method = it.method
|
||||||
val params = it.payload.toStringUtf8()
|
val params = it.payload.toStringUtf8()
|
||||||
@@ -115,75 +123,29 @@ open class NativeCall(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun fetch(ctx: CallContext<ParsedCallDetails>): Mono<CallContext<ByteArray>> {
|
fun fetch(ctx: CallContext<ParsedCallDetails>): Mono<CallContext<ByteArray>> {
|
||||||
return fetchFromCache(ctx)
|
return ctx.upstream.getRoutedApi(ctx.matcher)
|
||||||
.onErrorResume { t ->
|
.flatMap { api ->
|
||||||
log.warn("Failed to read from cache", t);
|
api.read(JsonRpcRequest(ctx.payload.method, ctx.payload.params))
|
||||||
Mono.empty()
|
.flatMap(JsonRpcResponse::requireResult)
|
||||||
}
|
.map {
|
||||||
.switchIfEmpty(
|
ctx.withPayload(it)
|
||||||
|
}
|
||||||
|
}.switchIfEmpty(
|
||||||
Mono.just(ctx).flatMap(this::executeOnRemote)
|
Mono.just(ctx).flatMap(this::executeOnRemote)
|
||||||
)
|
)
|
||||||
}
|
.onErrorMap {
|
||||||
|
CallFailure(ctx.id, it)
|
||||||
fun fetchFromCache(ctx: CallContext<ParsedCallDetails>): Mono<CallContext<ByteArray>> {
|
}
|
||||||
val cachingApi = ctx.upstream.cache
|
|
||||||
return cachingApi.execute(ctx.id, ctx.payload.method, ctx.payload.params).map { ctx.withPayload(it) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun executeOnRemote(ctx: CallContext<ParsedCallDetails>): Mono<CallContext<ByteArray>> {
|
fun executeOnRemote(ctx: CallContext<ParsedCallDetails>): Mono<CallContext<ByteArray>> {
|
||||||
val apis = ctx.getApis()
|
if (!ctx.upstream.getMethods().isAllowed(ctx.payload.method)) {
|
||||||
apis.request(1)
|
return Mono.error(RpcException(RpcResponseError.CODE_METHOD_NOT_EXIST, "Unsupported method"))
|
||||||
var failures = 0
|
}
|
||||||
return Flux.from(apis)
|
val reader = quorumReaderFactory.create(ctx.getApis(), ctx.callQuorum)
|
||||||
.flatMap { api ->
|
return reader.read(JsonRpcRequest(ctx.payload.method, ctx.payload.params))
|
||||||
val upstream = ctx.upstream
|
|
||||||
api.execute(ctx.id, ctx.payload.method, ctx.payload.params)
|
|
||||||
// on error notify quorum, it may use error message or other details
|
|
||||||
.doOnError { err ->
|
|
||||||
if (err is RpcException) {
|
|
||||||
ctx.callQuorum.record(err, upstream)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.map { Tuples.of(it, upstream) }
|
|
||||||
}
|
|
||||||
.retry {
|
|
||||||
failures++
|
|
||||||
if (ctx.callQuorum.isResolved()) {
|
|
||||||
false
|
|
||||||
} else if (failures < 3) {
|
|
||||||
apis.request(1)
|
|
||||||
true
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// record all correct responses until quorum reached
|
|
||||||
.reduce(ctx.callQuorum, {res, a ->
|
|
||||||
if (res.record(a.t1, a.t2)) {
|
|
||||||
apis.resolve()
|
|
||||||
} else {
|
|
||||||
apis.request(1)
|
|
||||||
}
|
|
||||||
res
|
|
||||||
})
|
|
||||||
// if last call resulted in error it's still possible that request was resolved correctly. i.e. for BroadcastQuorum
|
|
||||||
.onErrorResume { err ->
|
|
||||||
if (ctx.callQuorum.isResolved()) {
|
|
||||||
Mono.just(ctx.callQuorum)
|
|
||||||
} else {
|
|
||||||
Mono.error(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.doOnNext {
|
|
||||||
if (!it.isResolved()) {
|
|
||||||
log.debug("No quorum for ${ctx.payload.method} as ${ctx.callQuorum}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.filter { it.isResolved() }
|
|
||||||
.map {
|
.map {
|
||||||
val result = it.getResult()
|
ctx.withPayload(it.value)
|
||||||
?: throw CallFailure(ctx.id, Exception("No response from upstream for ${ctx.payload.method}"))
|
|
||||||
ctx.withPayload(result)
|
|
||||||
}
|
}
|
||||||
.onErrorMap {
|
.onErrorMap {
|
||||||
log.error("Failed to make a call", it)
|
log.error("Failed to make a call", it)
|
||||||
@@ -204,7 +166,7 @@ open class NativeCall(
|
|||||||
}
|
}
|
||||||
|
|
||||||
open class CallContext<T>(val id: Int,
|
open class CallContext<T>(val id: Int,
|
||||||
val upstream: AggregatedUpstream<*>,
|
val upstream: Multistream,
|
||||||
val matcher: Selector.Matcher,
|
val matcher: Selector.Matcher,
|
||||||
val callQuorum: CallQuorum,
|
val callQuorum: CallQuorum,
|
||||||
val payload: T) {
|
val payload: T) {
|
||||||
@@ -212,8 +174,8 @@ open class NativeCall(
|
|||||||
return CallContext(id, upstream, matcher, callQuorum, payload)
|
return CallContext(id, upstream, matcher, callQuorum, payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getApis(): ApiSource<*> {
|
fun getApis(): ApiSource {
|
||||||
return upstream.getApis(matcher)
|
return upstream.getApiSource(matcher)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,10 +21,8 @@ import io.emeraldpay.api.proto.BlockchainOuterClass
|
|||||||
import io.emeraldpay.api.proto.Common
|
import io.emeraldpay.api.proto.Common
|
||||||
import io.emeraldpay.dshackle.BlockchainType
|
import io.emeraldpay.dshackle.BlockchainType
|
||||||
import io.emeraldpay.dshackle.data.BlockContainer
|
import io.emeraldpay.dshackle.data.BlockContainer
|
||||||
import io.emeraldpay.dshackle.upstream.Upstreams
|
import io.emeraldpay.dshackle.upstream.MultistreamHolder
|
||||||
import io.emeraldpay.grpc.Chain
|
import io.emeraldpay.grpc.Chain
|
||||||
import io.infinitape.etherjar.rpc.json.BlockJson
|
|
||||||
import io.infinitape.etherjar.rpc.json.TransactionRefJson
|
|
||||||
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
|
||||||
@@ -33,7 +31,7 @@ import reactor.core.publisher.Mono
|
|||||||
|
|
||||||
@Service
|
@Service
|
||||||
class StreamHead(
|
class StreamHead(
|
||||||
@Autowired private val upstreams: Upstreams
|
@Autowired private val multistreamHolder: MultistreamHolder
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private val log = LoggerFactory.getLogger(StreamHead::class.java)
|
private val log = LoggerFactory.getLogger(StreamHead::class.java)
|
||||||
@@ -42,7 +40,7 @@ class StreamHead(
|
|||||||
return requestMono.map { request ->
|
return requestMono.map { request ->
|
||||||
Chain.byId(request.type.number)
|
Chain.byId(request.type.number)
|
||||||
}.flatMapMany { chain ->
|
}.flatMapMany { chain ->
|
||||||
val up = upstreams.getUpstream(chain)
|
val up = multistreamHolder.getUpstream(chain)
|
||||||
?: return@flatMapMany Flux.error<BlockchainOuterClass.ChainHead>(Exception("Unavailable chain: $chain"))
|
?: return@flatMapMany Flux.error<BlockchainOuterClass.ChainHead>(Exception("Unavailable chain: $chain"))
|
||||||
up.getHead()
|
up.getHead()
|
||||||
.getFlux()
|
.getFlux()
|
||||||
@@ -64,7 +62,7 @@ class StreamHead(
|
|||||||
return BlockchainOuterClass.ChainHead.newBuilder()
|
return BlockchainOuterClass.ChainHead.newBuilder()
|
||||||
.setChainValue(chain.id)
|
.setChainValue(chain.id)
|
||||||
.setHeight(block.height)
|
.setHeight(block.height)
|
||||||
.setTimestamp(block.timestamp!!.toEpochMilli())
|
.setTimestamp(block.timestamp.toEpochMilli())
|
||||||
.setWeight(ByteString.copyFrom(block.difficulty.toByteArray()))
|
.setWeight(ByteString.copyFrom(block.difficulty.toByteArray()))
|
||||||
.setBlockId(block.hash.toHex())
|
.setBlockId(block.hash.toHex())
|
||||||
.build()
|
.build()
|
||||||
|
|||||||
@@ -27,13 +27,13 @@ import reactor.core.publisher.Mono
|
|||||||
|
|
||||||
@Service
|
@Service
|
||||||
class SubscribeStatus(
|
class SubscribeStatus(
|
||||||
@Autowired private val upstreams: Upstreams
|
@Autowired private val multistreamHolder: MultistreamHolder
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun subscribeStatus(requestMono: Mono<BlockchainOuterClass.StatusRequest>): Flux<BlockchainOuterClass.ChainStatus> {
|
fun subscribeStatus(requestMono: Mono<BlockchainOuterClass.StatusRequest>): Flux<BlockchainOuterClass.ChainStatus> {
|
||||||
return requestMono.flatMapMany {
|
return requestMono.flatMapMany {
|
||||||
val ups = upstreams.getAvailable().mapNotNull { chain ->
|
val ups = multistreamHolder.getAvailable().mapNotNull { chain ->
|
||||||
val chainUpstream = upstreams.getUpstream(chain)
|
val chainUpstream = multistreamHolder.getUpstream(chain)
|
||||||
chainUpstream?.observeStatus()?.map { avail ->
|
chainUpstream?.observeStatus()?.map { avail ->
|
||||||
ChainSubscription(chain, chainUpstream, avail)
|
ChainSubscription(chain, chainUpstream, avail)
|
||||||
}
|
}
|
||||||
@@ -46,7 +46,7 @@ class SubscribeStatus(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun chainStatus(chain: Chain, ups: List<Upstream<*>>): BlockchainOuterClass.ChainStatus {
|
fun chainStatus(chain: Chain, ups: List<Upstream>): BlockchainOuterClass.ChainStatus {
|
||||||
val available = ups.map { u ->
|
val available = ups.map { u ->
|
||||||
u.getStatus()
|
u.getStatus()
|
||||||
}.min() ?: UpstreamAvailability.UNAVAILABLE
|
}.min() ?: UpstreamAvailability.UNAVAILABLE
|
||||||
@@ -60,6 +60,6 @@ class SubscribeStatus(
|
|||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
class ChainSubscription(val chain: Chain, val up: AggregatedUpstream<*>, val avail: UpstreamAvailability)
|
class ChainSubscription(val chain: Chain, val up: Multistream, val avail: UpstreamAvailability)
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -19,9 +19,8 @@ import io.emeraldpay.api.proto.BlockchainOuterClass
|
|||||||
import io.emeraldpay.api.proto.Common
|
import io.emeraldpay.api.proto.Common
|
||||||
import io.emeraldpay.dshackle.BlockchainType
|
import io.emeraldpay.dshackle.BlockchainType
|
||||||
import io.emeraldpay.dshackle.SilentException
|
import io.emeraldpay.dshackle.SilentException
|
||||||
import io.emeraldpay.dshackle.upstream.Selector
|
import io.emeraldpay.dshackle.upstream.MultistreamHolder
|
||||||
import io.emeraldpay.dshackle.upstream.Upstreams
|
import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinMultistream
|
||||||
import io.emeraldpay.dshackle.upstream.bitcoin.DirectBitcoinApi
|
|
||||||
import io.emeraldpay.grpc.Chain
|
import io.emeraldpay.grpc.Chain
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import org.springframework.beans.factory.annotation.Autowired
|
import org.springframework.beans.factory.annotation.Autowired
|
||||||
@@ -35,7 +34,7 @@ import kotlin.collections.HashMap
|
|||||||
|
|
||||||
@Service
|
@Service
|
||||||
class TrackBitcoinAddress(
|
class TrackBitcoinAddress(
|
||||||
@Autowired private val upstreams: Upstreams
|
@Autowired private val multistreamHolder: MultistreamHolder
|
||||||
) : TrackAddress {
|
) : TrackAddress {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -43,7 +42,7 @@ class TrackBitcoinAddress(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun isSupported(chain: Chain): Boolean {
|
override fun isSupported(chain: Chain): Boolean {
|
||||||
return BlockchainType.fromBlockchain(chain) == BlockchainType.BITCOIN && upstreams.isAvailable(chain)
|
return BlockchainType.fromBlockchain(chain) == BlockchainType.BITCOIN && multistreamHolder.isAvailable(chain)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun allAddresses(request: BlockchainOuterClass.BalanceRequest): List<String>? {
|
fun allAddresses(request: BlockchainOuterClass.BalanceRequest): List<String>? {
|
||||||
@@ -63,8 +62,8 @@ class TrackBitcoinAddress(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun requestBalances(chain: Chain, api: DirectBitcoinApi, addresses: List<String>): Flux<AddressBalance> {
|
fun requestBalances(chain: Chain, api: BitcoinMultistream, addresses: List<String>): Flux<AddressBalance> {
|
||||||
return api.executeAndResult(0, "listunspent", emptyList(), List::class.java)
|
return api.getReader().listUnspent()
|
||||||
.flatMapMany { unspents ->
|
.flatMapMany { unspents ->
|
||||||
val result = getTotal(chain, addresses, unspents)
|
val result = getTotal(chain, addresses, unspents)
|
||||||
Flux.fromIterable(result)
|
Flux.fromIterable(result)
|
||||||
@@ -73,17 +72,14 @@ class TrackBitcoinAddress(
|
|||||||
|
|
||||||
override fun getBalance(request: BlockchainOuterClass.BalanceRequest): Flux<BlockchainOuterClass.AddressBalance> {
|
override fun getBalance(request: BlockchainOuterClass.BalanceRequest): Flux<BlockchainOuterClass.AddressBalance> {
|
||||||
val chain = Chain.byId(request.asset.chainValue)
|
val chain = Chain.byId(request.asset.chainValue)
|
||||||
val upstream = upstreams.getUpstream(chain)?.castApi(DirectBitcoinApi::class.java)
|
val upstream = multistreamHolder.getUpstream(chain)?.cast(BitcoinMultistream::class.java)
|
||||||
?: return Flux.error(SilentException.UnsupportedBlockchain(request.asset.chainValue))
|
?: return Flux.error(SilentException.UnsupportedBlockchain(request.asset.chainValue))
|
||||||
val addresses = allAddresses(request) ?: return Flux.error(SilentException("Unsupported address"))
|
val addresses = allAddresses(request) ?: return Flux.error(SilentException("Unsupported address"))
|
||||||
if (addresses.isEmpty()) {
|
if (addresses.isEmpty()) {
|
||||||
return Flux.empty()
|
return Flux.empty()
|
||||||
}
|
}
|
||||||
val result = upstream.getApi(Selector.empty).flatMapMany { api ->
|
return requestBalances(chain, upstream, addresses)
|
||||||
requestBalances(chain, api, addresses)
|
.map(this@TrackBitcoinAddress::buildResponse)
|
||||||
.map(this@TrackBitcoinAddress::buildResponse)
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getTotal(chain: Chain, addresses: List<String>, unspents: List<*>): List<AddressBalance> {
|
fun getTotal(chain: Chain, addresses: List<String>, unspents: List<*>): List<AddressBalance> {
|
||||||
@@ -122,20 +118,16 @@ class TrackBitcoinAddress(
|
|||||||
|
|
||||||
override fun subscribe(request: BlockchainOuterClass.BalanceRequest): Flux<BlockchainOuterClass.AddressBalance> {
|
override fun subscribe(request: BlockchainOuterClass.BalanceRequest): Flux<BlockchainOuterClass.AddressBalance> {
|
||||||
val chain = Chain.byId(request.asset.chainValue)
|
val chain = Chain.byId(request.asset.chainValue)
|
||||||
val upstream = upstreams.getUpstream(chain)?.castApi(DirectBitcoinApi::class.java)
|
val upstream = multistreamHolder.getUpstream(chain)?.cast(BitcoinMultistream::class.java)
|
||||||
?: return Flux.error(SilentException.UnsupportedBlockchain(request.asset.chainValue))
|
?: return Flux.error(SilentException.UnsupportedBlockchain(request.asset.chainValue))
|
||||||
val addresses = allAddresses(request) ?: return Flux.error(SilentException("Unsupported address"))
|
val addresses = allAddresses(request) ?: return Flux.error(SilentException("Unsupported address"))
|
||||||
if (addresses.isEmpty()) {
|
if (addresses.isEmpty()) {
|
||||||
return Flux.empty()
|
return Flux.empty()
|
||||||
}
|
}
|
||||||
val initial = upstream.getApi(Selector.empty).flatMapMany { api ->
|
val initial = requestBalances(chain, upstream, addresses)
|
||||||
requestBalances(chain, api, addresses)
|
|
||||||
}
|
|
||||||
val following = upstream.getHead().getFlux()
|
val following = upstream.getHead().getFlux()
|
||||||
.flatMap { block ->
|
.flatMap { block ->
|
||||||
upstream.getApi(Selector.empty).flatMapMany { api ->
|
requestBalances(chain, upstream, addresses)
|
||||||
requestBalances(chain, api, addresses)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
val last = HashMap<Address, BigInteger>()
|
val last = HashMap<Address, BigInteger>()
|
||||||
val result = Flux.merge(initial, following)
|
val result = Flux.merge(initial, following)
|
||||||
|
|||||||
@@ -20,10 +20,8 @@ import io.emeraldpay.api.proto.BlockchainOuterClass
|
|||||||
import io.emeraldpay.api.proto.Common
|
import io.emeraldpay.api.proto.Common
|
||||||
import io.emeraldpay.dshackle.BlockchainType
|
import io.emeraldpay.dshackle.BlockchainType
|
||||||
import io.emeraldpay.dshackle.SilentException
|
import io.emeraldpay.dshackle.SilentException
|
||||||
import io.emeraldpay.dshackle.upstream.Selector
|
import io.emeraldpay.dshackle.upstream.MultistreamHolder
|
||||||
import io.emeraldpay.dshackle.upstream.Upstreams
|
import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinMultistream
|
||||||
import io.emeraldpay.dshackle.upstream.bitcoin.DirectBitcoinApi
|
|
||||||
import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinUpstream
|
|
||||||
import io.emeraldpay.dshackle.upstream.bitcoin.ExtractBlock
|
import io.emeraldpay.dshackle.upstream.bitcoin.ExtractBlock
|
||||||
import io.emeraldpay.grpc.Chain
|
import io.emeraldpay.grpc.Chain
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
@@ -39,7 +37,7 @@ import kotlin.math.min
|
|||||||
|
|
||||||
@Service
|
@Service
|
||||||
class TrackBitcoinTx(
|
class TrackBitcoinTx(
|
||||||
@Autowired private val upstreams: Upstreams
|
@Autowired private val multistreamHolder: MultistreamHolder
|
||||||
) : TrackTx {
|
) : TrackTx {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -47,36 +45,35 @@ class TrackBitcoinTx(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun isSupported(chain: Chain): Boolean {
|
override fun isSupported(chain: Chain): Boolean {
|
||||||
return BlockchainType.fromBlockchain(chain) == BlockchainType.BITCOIN && upstreams.isAvailable(chain)
|
return BlockchainType.fromBlockchain(chain) == BlockchainType.BITCOIN && multistreamHolder.isAvailable(chain)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun subscribe(request: BlockchainOuterClass.TxStatusRequest): Flux<BlockchainOuterClass.TxStatus> {
|
override fun subscribe(request: BlockchainOuterClass.TxStatusRequest): Flux<BlockchainOuterClass.TxStatus> {
|
||||||
val chain = Chain.byId(request.chainValue)
|
val chain = Chain.byId(request.chainValue)
|
||||||
val upstream = upstreams.getUpstream(chain)?.cast(BitcoinUpstream::class.java, DirectBitcoinApi::class.java)
|
val upstream = multistreamHolder.getUpstream(chain)?.cast(BitcoinMultistream::class.java)
|
||||||
?: return Flux.error(SilentException.UnsupportedBlockchain(chain))
|
?: return Flux.error(SilentException.UnsupportedBlockchain(chain))
|
||||||
val txid = request.txId
|
val txid = request.txId
|
||||||
val confirmations = max(min(1, request.confirmationLimit), 12)
|
val confirmations = max(min(1, request.confirmationLimit), 12)
|
||||||
return upstream.getApi(Selector.empty).flatMapMany { api ->
|
return subscribe(chain, upstream, txid)
|
||||||
subscribe(chain, api, upstream, txid)
|
.takeUntil { tx ->
|
||||||
}.takeUntil { tx ->
|
tx.confirmations >= confirmations
|
||||||
tx.confirmations >= confirmations
|
}.map(this::asProto)
|
||||||
}.map(this::asProto)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun subscribe(chain: Chain, api: DirectBitcoinApi, upstream: BitcoinUpstream, txid: String): Flux<TxStatus> {
|
fun subscribe(chain: Chain, upstream: BitcoinMultistream, txid: String): Flux<TxStatus> {
|
||||||
return loadExisting(api, txid)
|
return loadExisting(upstream, txid)
|
||||||
.flatMapMany { status ->
|
.flatMapMany { status ->
|
||||||
if (status.mined) {
|
if (status.mined) {
|
||||||
//Head almost always knows the current height, so it can continue with calculating confirmations
|
//Head almost always knows the current height, so it can continue with calculating confirmations
|
||||||
//without publishing an empty TxStatus first
|
//without publishing an empty TxStatus first
|
||||||
continueWithMined(api, upstream, status)
|
continueWithMined(upstream, status)
|
||||||
} else {
|
} else {
|
||||||
loadMempool(upstream, txid)
|
loadMempool(upstream, txid)
|
||||||
.flatMapMany { tx ->
|
.flatMapMany { tx ->
|
||||||
val next = if (tx.found) {
|
val next = if (tx.found) {
|
||||||
untilMined(upstream, tx)
|
untilMined(upstream, tx)
|
||||||
} else {
|
} else {
|
||||||
untilFound(chain, api, upstream, txid)
|
untilFound(chain, upstream, txid)
|
||||||
}
|
}
|
||||||
//fist provide the current status, then updates
|
//fist provide the current status, then updates
|
||||||
Flux.concat(Mono.just(tx), next)
|
Flux.concat(Mono.just(tx), next)
|
||||||
@@ -85,8 +82,8 @@ class TrackBitcoinTx(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun continueWithMined(api: DirectBitcoinApi, upstream: BitcoinUpstream, status: TxStatus): Flux<TxStatus> {
|
fun continueWithMined(upstream: BitcoinMultistream, status: TxStatus): Flux<TxStatus> {
|
||||||
return api.getBlock(status.blockHash!!)
|
return upstream.getReader().getBlock(status.blockHash!!)
|
||||||
.map { block ->
|
.map { block ->
|
||||||
TxStatus(status.txid, true, ExtractBlock.getHeight(block), true, status.blockHash, ExtractBlock.getTime(block), ExtractBlock.getDifficulty(block))
|
TxStatus(status.txid, true, ExtractBlock.getHeight(block), true, status.blockHash, ExtractBlock.getTime(block), ExtractBlock.getDifficulty(block))
|
||||||
}.flatMapMany { tx ->
|
}.flatMapMany { tx ->
|
||||||
@@ -94,41 +91,40 @@ class TrackBitcoinTx(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun untilFound(chain: Chain, api: DirectBitcoinApi, upstream: BitcoinUpstream, txid: String): Flux<TxStatus> {
|
fun untilFound(chain: Chain, upstream: BitcoinMultistream, txid: String): Flux<TxStatus> {
|
||||||
return Flux.interval(Duration.ofSeconds(1))
|
return Flux.interval(Duration.ofSeconds(1))
|
||||||
.take(Duration.ofMinutes(10))
|
.take(Duration.ofMinutes(10))
|
||||||
.flatMap { loadMempool(upstream, txid) }
|
.flatMap { loadMempool(upstream, txid) }
|
||||||
.skipUntil { it.found }
|
.skipUntil { it.found }
|
||||||
.flatMap { subscribe(chain, api, upstream, txid) }
|
.flatMap { subscribe(chain, upstream, txid) }
|
||||||
.doOnError { t ->
|
.doOnError { t ->
|
||||||
log.error("Failed to wait until found", t)
|
log.error("Failed to wait until found", t)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun untilMined(upstream: BitcoinUpstream, tx: TxStatus): Mono<TxStatus> {
|
fun untilMined(upstream: BitcoinMultistream, tx: TxStatus): Mono<TxStatus> {
|
||||||
return upstream.getHead().getFlux().flatMap {
|
return upstream.getHead().getFlux().flatMap {
|
||||||
upstream.getApi(Selector.empty).flatMap { api ->
|
loadExisting(upstream, tx.txid)
|
||||||
loadExisting(api, tx.txid)
|
.filter { it.mined }
|
||||||
}.filter { it.mined }
|
|
||||||
}.single()
|
}.single()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun withConfirmations(upstream: BitcoinUpstream, tx: TxStatus): Flux<TxStatus> {
|
fun withConfirmations(upstream: BitcoinMultistream, tx: TxStatus): Flux<TxStatus> {
|
||||||
return upstream.getHead().getFlux().map {
|
return upstream.getHead().getFlux().map {
|
||||||
tx.withHead(it.height)
|
tx.withHead(it.height)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadExisting(api: DirectBitcoinApi, txid: String): Mono<TxStatus> {
|
fun loadExisting(api: BitcoinMultistream, txid: String): Mono<TxStatus> {
|
||||||
val mined = api.getTx(txid)
|
val mined = api.getReader().getTx(txid)
|
||||||
return mined.map {
|
return mined.map {
|
||||||
val block = it["blockhash"] as String?
|
val block = it["blockhash"] as String?
|
||||||
TxStatus(txid, found = true, mined = block != null, blockHash = block, height = ExtractBlock.getHeight(it))
|
TxStatus(txid, found = true, mined = block != null, blockHash = block, height = ExtractBlock.getHeight(it))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadMempool(upstream: BitcoinUpstream, txid: String): Mono<TxStatus> {
|
fun loadMempool(upstream: BitcoinMultistream, txid: String): Mono<TxStatus> {
|
||||||
val mempool = upstream.getData().getMempool().get()
|
val mempool = upstream.getReader().getMempool().get()
|
||||||
return mempool.map {
|
return mempool.map {
|
||||||
if (it.contains(txid)) {
|
if (it.contains(txid)) {
|
||||||
TxStatus(txid, found = true, mined = false)
|
TxStatus(txid, found = true, mined = false)
|
||||||
|
|||||||
@@ -21,9 +21,8 @@ import io.emeraldpay.api.proto.Common
|
|||||||
import io.emeraldpay.dshackle.BlockchainType
|
import io.emeraldpay.dshackle.BlockchainType
|
||||||
import io.emeraldpay.dshackle.Defaults
|
import io.emeraldpay.dshackle.Defaults
|
||||||
import io.emeraldpay.dshackle.SilentException
|
import io.emeraldpay.dshackle.SilentException
|
||||||
import io.emeraldpay.dshackle.upstream.Upstreams
|
import io.emeraldpay.dshackle.upstream.MultistreamHolder
|
||||||
import io.emeraldpay.dshackle.upstream.ethereum.EthereumApi
|
import io.emeraldpay.dshackle.upstream.ethereum.EthereumMultistream
|
||||||
import io.emeraldpay.dshackle.upstream.ethereum.AggregatedEthereumUpstreams
|
|
||||||
import io.emeraldpay.grpc.Chain
|
import io.emeraldpay.grpc.Chain
|
||||||
import io.infinitape.etherjar.domain.Address
|
import io.infinitape.etherjar.domain.Address
|
||||||
import io.infinitape.etherjar.domain.Wei
|
import io.infinitape.etherjar.domain.Wei
|
||||||
@@ -35,13 +34,13 @@ import reactor.core.publisher.Mono
|
|||||||
|
|
||||||
@Service
|
@Service
|
||||||
class TrackEthereumAddress(
|
class TrackEthereumAddress(
|
||||||
@Autowired private val upstreams: Upstreams
|
@Autowired private val multistreamHolder: MultistreamHolder
|
||||||
) : TrackAddress {
|
) : TrackAddress {
|
||||||
|
|
||||||
private val log = LoggerFactory.getLogger(TrackEthereumAddress::class.java)
|
private val log = LoggerFactory.getLogger(TrackEthereumAddress::class.java)
|
||||||
|
|
||||||
override fun isSupported(chain: Chain): Boolean {
|
override fun isSupported(chain: Chain): Boolean {
|
||||||
return BlockchainType.fromBlockchain(chain) == BlockchainType.ETHEREUM && upstreams.isAvailable(chain)
|
return BlockchainType.fromBlockchain(chain) == BlockchainType.ETHEREUM && multistreamHolder.isAvailable(chain)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getBalance(request: BlockchainOuterClass.BalanceRequest): Flux<BlockchainOuterClass.AddressBalance> {
|
override fun getBalance(request: BlockchainOuterClass.BalanceRequest): Flux<BlockchainOuterClass.AddressBalance> {
|
||||||
@@ -52,7 +51,7 @@ class TrackEthereumAddress(
|
|||||||
|
|
||||||
override fun subscribe(request: BlockchainOuterClass.BalanceRequest): Flux<BlockchainOuterClass.AddressBalance> {
|
override fun subscribe(request: BlockchainOuterClass.BalanceRequest): Flux<BlockchainOuterClass.AddressBalance> {
|
||||||
val chain = Chain.byId(request.asset.chainValue)
|
val chain = Chain.byId(request.asset.chainValue)
|
||||||
val head = upstreams.getUpstream(chain)?.getHead()?.getFlux() ?: Flux.empty()
|
val head = multistreamHolder.getUpstream(chain)?.getHead()?.getFlux() ?: Flux.empty()
|
||||||
val balances = initAddress(request)
|
val balances = initAddress(request)
|
||||||
.flatMap { tracked ->
|
.flatMap { tracked ->
|
||||||
val current = getBalance(tracked)
|
val current = getBalance(tracked)
|
||||||
@@ -87,14 +86,14 @@ class TrackEthereumAddress(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getUpstream(chain: Chain): AggregatedEthereumUpstreams {
|
fun getUpstream(chain: Chain): EthereumMultistream {
|
||||||
return upstreams.getUpstream(chain)?.cast(AggregatedEthereumUpstreams::class.java, EthereumApi::class.java)
|
return multistreamHolder.getUpstream(chain)?.cast(EthereumMultistream::class.java)
|
||||||
?: throw SilentException.UnsupportedBlockchain(chain)
|
?: throw SilentException.UnsupportedBlockchain(chain)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initAddress(request: BlockchainOuterClass.BalanceRequest): Flux<TrackedAddress> {
|
private fun initAddress(request: BlockchainOuterClass.BalanceRequest): Flux<TrackedAddress> {
|
||||||
val chain = Chain.byId(request.asset.chainValue)
|
val chain = Chain.byId(request.asset.chainValue)
|
||||||
if (!upstreams.isAvailable(chain)) {
|
if (!multistreamHolder.isAvailable(chain)) {
|
||||||
return Flux.error(SilentException.UnsupportedBlockchain(request.asset.chainValue))
|
return Flux.error(SilentException.UnsupportedBlockchain(request.asset.chainValue))
|
||||||
}
|
}
|
||||||
if (request.asset.code?.toLowerCase() != "ether") {
|
if (request.asset.code?.toLowerCase() != "ether") {
|
||||||
|
|||||||
@@ -23,10 +23,8 @@ import io.emeraldpay.dshackle.BlockchainType
|
|||||||
import io.emeraldpay.dshackle.SilentException
|
import io.emeraldpay.dshackle.SilentException
|
||||||
import io.emeraldpay.dshackle.data.BlockContainer
|
import io.emeraldpay.dshackle.data.BlockContainer
|
||||||
import io.emeraldpay.dshackle.data.TxId
|
import io.emeraldpay.dshackle.data.TxId
|
||||||
import io.emeraldpay.dshackle.upstream.Upstream
|
import io.emeraldpay.dshackle.upstream.MultistreamHolder
|
||||||
import io.emeraldpay.dshackle.upstream.Upstreams
|
import io.emeraldpay.dshackle.upstream.ethereum.EthereumMultistream
|
||||||
import io.emeraldpay.dshackle.upstream.ethereum.EthereumApi
|
|
||||||
import io.emeraldpay.dshackle.upstream.ethereum.AggregatedEthereumUpstreams
|
|
||||||
import io.emeraldpay.grpc.Chain
|
import io.emeraldpay.grpc.Chain
|
||||||
import io.infinitape.etherjar.domain.BlockHash
|
import io.infinitape.etherjar.domain.BlockHash
|
||||||
import io.infinitape.etherjar.domain.TransactionId
|
import io.infinitape.etherjar.domain.TransactionId
|
||||||
@@ -50,7 +48,7 @@ import kotlin.math.min
|
|||||||
|
|
||||||
@Service
|
@Service
|
||||||
class TrackEthereumTx(
|
class TrackEthereumTx(
|
||||||
@Autowired private val upstreams: Upstreams
|
@Autowired private val multistreamHolder: MultistreamHolder
|
||||||
) : TrackTx {
|
) : TrackTx {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -65,7 +63,7 @@ class TrackEthereumTx(
|
|||||||
private val log = LoggerFactory.getLogger(TrackEthereumTx::class.java)
|
private val log = LoggerFactory.getLogger(TrackEthereumTx::class.java)
|
||||||
|
|
||||||
override fun isSupported(chain: Chain): Boolean {
|
override fun isSupported(chain: Chain): Boolean {
|
||||||
return BlockchainType.fromBlockchain(chain) == BlockchainType.ETHEREUM && upstreams.isAvailable(chain)
|
return BlockchainType.fromBlockchain(chain) == BlockchainType.ETHEREUM && multistreamHolder.isAvailable(chain)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun subscribe(request: BlockchainOuterClass.TxStatusRequest): Flux<BlockchainOuterClass.TxStatus> {
|
override fun subscribe(request: BlockchainOuterClass.TxStatusRequest): Flux<BlockchainOuterClass.TxStatus> {
|
||||||
@@ -85,12 +83,12 @@ class TrackEthereumTx(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun getUpstream(chain: Chain): AggregatedEthereumUpstreams {
|
fun getUpstream(chain: Chain): EthereumMultistream {
|
||||||
return upstreams.getUpstream(chain)?.cast(AggregatedEthereumUpstreams::class.java, EthereumApi::class.java)
|
return multistreamHolder.getUpstream(chain)?.cast(EthereumMultistream::class.java)
|
||||||
?: throw SilentException.UnsupportedBlockchain(chain)
|
?: throw SilentException.UnsupportedBlockchain(chain)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun subscribe(base: TxDetails, up: AggregatedEthereumUpstreams): Flux<TxDetails> {
|
fun subscribe(base: TxDetails, up: EthereumMultistream): Flux<TxDetails> {
|
||||||
var latestTx = base
|
var latestTx = base
|
||||||
|
|
||||||
val untilFound = Mono.just(latestTx)
|
val untilFound = Mono.just(latestTx)
|
||||||
@@ -215,7 +213,7 @@ class TrackEthereumTx(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateFromBlock(upstream: AggregatedEthereumUpstreams, tx: TxDetails, blockTx: TransactionJson): Mono<TxDetails> {
|
fun updateFromBlock(upstream: EthereumMultistream, tx: TxDetails, blockTx: TransactionJson): Mono<TxDetails> {
|
||||||
return if (blockTx.blockNumber != null && blockTx.blockHash != null && blockTx.blockHash != ZERO_BLOCK) {
|
return if (blockTx.blockNumber != null && blockTx.blockHash != null && blockTx.blockHash != ZERO_BLOCK) {
|
||||||
val updated = tx.withStatus(
|
val updated = tx.withStatus(
|
||||||
blockHash = blockTx.blockHash,
|
blockHash = blockTx.blockHash,
|
||||||
|
|||||||
@@ -19,19 +19,20 @@ package io.emeraldpay.dshackle.startup
|
|||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
import io.emeraldpay.dshackle.BlockchainType
|
import io.emeraldpay.dshackle.BlockchainType
|
||||||
import io.emeraldpay.dshackle.FileResolver
|
import io.emeraldpay.dshackle.FileResolver
|
||||||
|
import io.emeraldpay.dshackle.cache.CachesFactory
|
||||||
import io.emeraldpay.dshackle.config.UpstreamsConfig
|
import io.emeraldpay.dshackle.config.UpstreamsConfig
|
||||||
import io.emeraldpay.dshackle.upstream.CurrentUpstreams
|
import io.emeraldpay.dshackle.reader.Reader
|
||||||
import io.emeraldpay.dshackle.upstream.bitcoin.DirectBitcoinApi
|
import io.emeraldpay.dshackle.upstream.CurrentMultistreamHolder
|
||||||
import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinRpcClient
|
|
||||||
import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinUpstream
|
import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinUpstream
|
||||||
import io.emeraldpay.dshackle.upstream.calls.CallMethods
|
import io.emeraldpay.dshackle.upstream.calls.CallMethods
|
||||||
import io.emeraldpay.dshackle.upstream.calls.ManagedCallMethods
|
import io.emeraldpay.dshackle.upstream.calls.ManagedCallMethods
|
||||||
import io.emeraldpay.dshackle.upstream.ethereum.DirectEthereumApi
|
|
||||||
import io.emeraldpay.dshackle.upstream.ethereum.EthereumUpstream
|
import io.emeraldpay.dshackle.upstream.ethereum.EthereumUpstream
|
||||||
import io.emeraldpay.dshackle.upstream.ethereum.EthereumWs
|
import io.emeraldpay.dshackle.upstream.ethereum.EthereumWsFactory
|
||||||
import io.emeraldpay.dshackle.upstream.grpc.GrpcUpstreams
|
import io.emeraldpay.dshackle.upstream.grpc.GrpcUpstreams
|
||||||
|
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcHttpClient
|
||||||
|
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
|
||||||
|
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
|
||||||
import io.emeraldpay.grpc.Chain
|
import io.emeraldpay.grpc.Chain
|
||||||
import io.infinitape.etherjar.rpc.http.ReactorHttpRpcClient
|
|
||||||
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.Repository
|
import org.springframework.stereotype.Repository
|
||||||
@@ -44,9 +45,10 @@ import kotlin.collections.HashMap
|
|||||||
@Repository
|
@Repository
|
||||||
open class ConfiguredUpstreams(
|
open class ConfiguredUpstreams(
|
||||||
@Autowired private val objectMapper: ObjectMapper,
|
@Autowired private val objectMapper: ObjectMapper,
|
||||||
@Autowired private val currentUpstreams: CurrentUpstreams,
|
@Autowired private val currentUpstreams: CurrentMultistreamHolder,
|
||||||
@Autowired private val fileResolver: FileResolver,
|
@Autowired private val fileResolver: FileResolver,
|
||||||
@Autowired private val config: UpstreamsConfig
|
@Autowired private val config: UpstreamsConfig,
|
||||||
|
@Autowired private val cachesFactory: CachesFactory
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private val log = LoggerFactory.getLogger(ConfiguredUpstreams::class.java)
|
private val log = LoggerFactory.getLogger(ConfiguredUpstreams::class.java)
|
||||||
@@ -134,80 +136,61 @@ open class ConfiguredUpstreams(
|
|||||||
options: UpstreamsConfig.Options) {
|
options: UpstreamsConfig.Options) {
|
||||||
|
|
||||||
val conn = config.connection!!
|
val conn = config.connection!!
|
||||||
var rpcApi: DirectBitcoinApi? = null
|
val directApi: Reader<JsonRpcRequest, JsonRpcResponse>? = buildHttpClient(config)
|
||||||
|
if (directApi == null) {
|
||||||
|
log.warn("Upstream doesn't have API configuration")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
val methods = buildMethods(config, chain)
|
val methods = buildMethods(config, chain)
|
||||||
conn.rpc?.let { endpoint ->
|
val upstream = BitcoinUpstream(config.id
|
||||||
val rpcClient = BitcoinRpcClient(endpoint.url.toString(), endpoint.basicAuth!!)
|
?: "bitcoin-${seq.getAndIncrement()}", chain, directApi,
|
||||||
rpcApi = DirectBitcoinApi(rpcClient, objectMapper, methods)
|
options, QuorumForLabels.QuorumItem(1, config.labels),
|
||||||
}
|
objectMapper, methods)
|
||||||
rpcApi?.let { api ->
|
|
||||||
val upstream = BitcoinUpstream(config.id
|
|
||||||
?: "bitcoin-${seq.getAndIncrement()}", chain, api,
|
|
||||||
options, QuorumForLabels.QuorumItem(1, config.labels),
|
|
||||||
objectMapper, methods)
|
|
||||||
|
|
||||||
upstream.start()
|
|
||||||
currentUpstreams.update(UpstreamChange(chain, upstream, UpstreamChange.ChangeType.ADDED))
|
|
||||||
}
|
|
||||||
|
|
||||||
|
upstream.start()
|
||||||
|
currentUpstreams.update(UpstreamChange(chain, upstream, UpstreamChange.ChangeType.ADDED))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildEthereumUpstream(config: UpstreamsConfig.Upstream<UpstreamsConfig.EthereumConnection>,
|
private fun buildEthereumUpstream(config: UpstreamsConfig.Upstream<UpstreamsConfig.EthereumConnection>,
|
||||||
chain: Chain,
|
chain: Chain,
|
||||||
options: UpstreamsConfig.Options) {
|
options: UpstreamsConfig.Options) {
|
||||||
val conn = config.connection!!
|
val conn = config.connection!!
|
||||||
var rpcApi: DirectEthereumApi? = null
|
val directApi: Reader<JsonRpcRequest, JsonRpcResponse>? = buildHttpClient(config)
|
||||||
|
if (directApi == null) {
|
||||||
|
log.warn("Upstream doesn't have API configuration")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
val urls = ArrayList<URI>()
|
val urls = ArrayList<URI>()
|
||||||
val methods = buildMethods(config, chain)
|
val methods = buildMethods(config, chain)
|
||||||
conn.rpc?.let { endpoint ->
|
conn.rpc?.let { endpoint ->
|
||||||
val rpcClient = ReactorHttpRpcClient.newBuilder()
|
|
||||||
.connectTo(endpoint.url)
|
|
||||||
.alwaysSeparate()
|
|
||||||
conn.rpc?.basicAuth?.let { auth ->
|
|
||||||
rpcClient.basicAuth(auth.username, auth.password)
|
|
||||||
}
|
|
||||||
conn.rpc?.tls?.let { tls ->
|
|
||||||
tls.ca?.let { ca ->
|
|
||||||
fileResolver.resolve(ca).inputStream().use { cert -> rpcClient.trustedCertificate(cert) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
rpcApi = DirectEthereumApi(
|
|
||||||
rpcClient.build(),
|
|
||||||
null,
|
|
||||||
objectMapper,
|
|
||||||
methods
|
|
||||||
).apply {
|
|
||||||
timeout = options.timeout
|
|
||||||
}
|
|
||||||
|
|
||||||
urls.add(endpoint.url)
|
urls.add(endpoint.url)
|
||||||
}
|
}
|
||||||
if (rpcApi != null) {
|
|
||||||
val wsApi: EthereumWs? = conn.ws?.let { endpoint ->
|
|
||||||
val wsApi = EthereumWs(
|
|
||||||
endpoint.url,
|
|
||||||
endpoint.origin ?: URI("http://localhost"),
|
|
||||||
rpcApi!!,
|
|
||||||
objectMapper
|
|
||||||
)
|
|
||||||
endpoint.basicAuth?.let { auth ->
|
|
||||||
wsApi.basicAuth = auth
|
|
||||||
}
|
|
||||||
wsApi.connect()
|
|
||||||
urls.add(endpoint.url)
|
|
||||||
wsApi
|
|
||||||
}
|
|
||||||
|
|
||||||
log.info("Using ${chain.chainName} upstream, at ${urls.joinToString()}")
|
val wsFactoryApi: EthereumWsFactory? = conn.ws?.let { endpoint ->
|
||||||
val ethereumUpstream = EthereumUpstream(
|
val wsApi = EthereumWsFactory(
|
||||||
config.id!!,
|
endpoint.url,
|
||||||
chain, rpcApi!!, wsApi, options,
|
endpoint.origin ?: URI("http://localhost"),
|
||||||
QuorumForLabels.QuorumItem(1, config.labels),
|
objectMapper
|
||||||
methods,
|
)
|
||||||
objectMapper)
|
endpoint.basicAuth?.let { auth ->
|
||||||
ethereumUpstream.start()
|
wsApi.basicAuth = auth
|
||||||
currentUpstreams.update(UpstreamChange(chain, ethereumUpstream, UpstreamChange.ChangeType.ADDED))
|
}
|
||||||
|
urls.add(endpoint.url)
|
||||||
|
wsApi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.info("Using ${chain.chainName} upstream, at ${urls.joinToString()}")
|
||||||
|
val ethereumUpstream = EthereumUpstream(
|
||||||
|
config.id!!,
|
||||||
|
chain, directApi, wsFactoryApi, options,
|
||||||
|
QuorumForLabels.QuorumItem(1, config.labels),
|
||||||
|
methods,
|
||||||
|
objectMapper
|
||||||
|
)
|
||||||
|
ethereumUpstream.start()
|
||||||
|
currentUpstreams.update(UpstreamChange(chain, ethereumUpstream, UpstreamChange.ChangeType.ADDED))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildGrpcUpstream(config: UpstreamsConfig.Upstream<UpstreamsConfig.GrpcConnection>, options: UpstreamsConfig.Options) {
|
private fun buildGrpcUpstream(config: UpstreamsConfig.Upstream<UpstreamsConfig.GrpcConnection>, options: UpstreamsConfig.Options) {
|
||||||
@@ -230,5 +213,22 @@ open class ConfiguredUpstreams(
|
|||||||
.subscribe(currentUpstreams::update)
|
.subscribe(currentUpstreams::update)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun buildHttpClient(config: UpstreamsConfig.Upstream<out UpstreamsConfig.RpcConnection>): JsonRpcHttpClient? {
|
||||||
|
val conn = config.connection!!
|
||||||
|
val urls = ArrayList<URI>()
|
||||||
|
return conn.rpc?.let { endpoint ->
|
||||||
|
val tls = conn.rpc?.tls?.let { tls ->
|
||||||
|
tls.ca?.let { ca ->
|
||||||
|
fileResolver.resolve(ca).readBytes()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
urls.add(endpoint.url)
|
||||||
|
JsonRpcHttpClient(
|
||||||
|
endpoint.url.toString(),
|
||||||
|
objectMapper,
|
||||||
|
conn.rpc?.basicAuth,
|
||||||
|
tls
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -32,7 +32,7 @@ class UpstreamChange(
|
|||||||
/**
|
/**
|
||||||
* Corresponding upstream
|
* Corresponding upstream
|
||||||
*/
|
*/
|
||||||
val upstream: Upstream<*>,
|
val upstream: Upstream,
|
||||||
/**
|
/**
|
||||||
* Type of the change
|
* Type of the change
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -31,15 +31,24 @@ abstract class AbstractHead : Head {
|
|||||||
|
|
||||||
private val head = AtomicReference<BlockContainer>(null)
|
private val head = AtomicReference<BlockContainer>(null)
|
||||||
private val stream: TopicProcessor<BlockContainer> = TopicProcessor.create()
|
private val stream: TopicProcessor<BlockContainer> = TopicProcessor.create()
|
||||||
|
private val beforeBlockHandlers = ArrayList<Runnable>()
|
||||||
|
|
||||||
fun follow(source: Flux<BlockContainer>): Disposable {
|
fun follow(source: Flux<BlockContainer>): Disposable {
|
||||||
return source.distinctUntilChanged {
|
return source
|
||||||
it.hash
|
.distinctUntilChanged {
|
||||||
}.filter { block ->
|
it.hash
|
||||||
val curr = head.get()
|
}.filter { block ->
|
||||||
curr == null || curr.difficulty < block.difficulty
|
val curr = head.get()
|
||||||
}
|
curr == null || curr.difficulty < block.difficulty
|
||||||
|
}
|
||||||
|
.doFinally {
|
||||||
|
// close internal stream if upstream is finished, otherwise it gets stuck
|
||||||
|
// but technically is should never happen during normal work, only when the Head
|
||||||
|
// is stopping
|
||||||
|
stream.onComplete()
|
||||||
|
}
|
||||||
.subscribe { block ->
|
.subscribe { block ->
|
||||||
|
notifyBeforeBlock()
|
||||||
val prev = head.getAndUpdate { curr ->
|
val prev = head.getAndUpdate { curr ->
|
||||||
if (curr == null || curr.difficulty < block.difficulty) {
|
if (curr == null || curr.difficulty < block.difficulty) {
|
||||||
block
|
block
|
||||||
@@ -54,6 +63,20 @@ abstract class AbstractHead : Head {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun notifyBeforeBlock() {
|
||||||
|
beforeBlockHandlers.forEach { handler ->
|
||||||
|
try {
|
||||||
|
handler.run()
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
log.warn("Before Block handler error", t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBeforeBlock(handler: Runnable) {
|
||||||
|
beforeBlockHandlers.add(handler)
|
||||||
|
}
|
||||||
|
|
||||||
override fun getFlux(): Flux<BlockContainer> {
|
override fun getFlux(): Flux<BlockContainer> {
|
||||||
return Flux.merge(
|
return Flux.merge(
|
||||||
Mono.justOrEmpty(head.get()),
|
Mono.justOrEmpty(head.get()),
|
||||||
|
|||||||
@@ -1,122 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2020 EmeraldPay, Inc
|
|
||||||
* Copyright (c) 2019 ETCDEV GmbH
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
package io.emeraldpay.dshackle.upstream
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
|
||||||
import io.emeraldpay.dshackle.cache.*
|
|
||||||
import io.emeraldpay.dshackle.config.UpstreamsConfig
|
|
||||||
import io.emeraldpay.dshackle.upstream.calls.AggregatedCallMethods
|
|
||||||
import io.emeraldpay.dshackle.upstream.calls.CallMethods
|
|
||||||
import org.springframework.context.Lifecycle
|
|
||||||
import reactor.core.Disposable
|
|
||||||
import reactor.core.publisher.Flux
|
|
||||||
import java.time.Duration
|
|
||||||
import java.time.Instant
|
|
||||||
import java.util.concurrent.atomic.AtomicReference
|
|
||||||
import java.util.concurrent.locks.ReentrantLock
|
|
||||||
import java.util.function.Predicate
|
|
||||||
import kotlin.concurrent.withLock
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Aggregation of multiple upstreams responding to a single blockchain
|
|
||||||
*/
|
|
||||||
abstract class AggregatedUpstream<U : UpstreamApi>(
|
|
||||||
private val objectMapper: ObjectMapper,
|
|
||||||
val caches: Caches
|
|
||||||
) : Upstream<U>, Lifecycle {
|
|
||||||
|
|
||||||
private var cacheSubscription: Disposable? = null
|
|
||||||
var cache: CachingEthereumApi = CachingEthereumApi.empty(objectMapper)
|
|
||||||
private val reconfigLock = ReentrantLock()
|
|
||||||
private var callMethods: CallMethods? = null
|
|
||||||
|
|
||||||
abstract fun getAll(): List<Upstream<U>>
|
|
||||||
abstract fun addUpstream(upstream: Upstream<U>)
|
|
||||||
abstract fun getApis(matcher: Selector.Matcher): ApiSource<U>
|
|
||||||
|
|
||||||
fun onUpstreamsUpdated() {
|
|
||||||
reconfigLock.withLock {
|
|
||||||
getAll().map { it.getMethods() }.let {
|
|
||||||
callMethods = AggregatedCallMethods(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun observeStatus(): Flux<UpstreamAvailability> {
|
|
||||||
val upstreamsFluxes = getAll().map { up -> up.observeStatus().map { UpstreamStatus(up, it) } }
|
|
||||||
return Flux.merge(upstreamsFluxes)
|
|
||||||
.filter(FilterBestAvailability())
|
|
||||||
.map { it.status }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun isAvailable(): Boolean {
|
|
||||||
return getAll().any { it.isAvailable() }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getStatus(): UpstreamAvailability {
|
|
||||||
val upstreams = getAll()
|
|
||||||
return if (upstreams.isEmpty()) UpstreamAvailability.UNAVAILABLE
|
|
||||||
else upstreams.map { it.getStatus() }.min()!!
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getOptions(): UpstreamsConfig.Options {
|
|
||||||
return UpstreamsConfig.Options()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getMethods(): CallMethods {
|
|
||||||
return callMethods ?: throw IllegalStateException("Methods are not initialized yet")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun start() {
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun stop() {
|
|
||||||
cacheSubscription?.dispose()
|
|
||||||
cacheSubscription = null
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onHeadUpdated(head: Head) {
|
|
||||||
reconfigLock.withLock {
|
|
||||||
cacheSubscription?.dispose()
|
|
||||||
cacheSubscription = head.getFlux().subscribe {
|
|
||||||
caches.cache(Caches.Tag.LATEST, it)
|
|
||||||
}
|
|
||||||
cache = CachingEthereumApi(objectMapper, caches, head)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --------------------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
class UpstreamStatus(val upstream: Upstream<UpstreamApi>, val status: UpstreamAvailability, val ts: Instant = Instant.now())
|
|
||||||
|
|
||||||
class FilterBestAvailability() : Predicate<UpstreamStatus> {
|
|
||||||
private val lastRef = AtomicReference<UpstreamStatus>()
|
|
||||||
|
|
||||||
override fun test(t: UpstreamStatus): Boolean {
|
|
||||||
val last = lastRef.get()
|
|
||||||
val changed = last == null
|
|
||||||
|| t.status > last.status
|
|
||||||
|| (last.upstream == t.upstream && t.status != last.status)
|
|
||||||
|| last.ts.isBefore(Instant.now() - Duration.ofSeconds(60))
|
|
||||||
if (changed) {
|
|
||||||
lastRef.set(t)
|
|
||||||
}
|
|
||||||
return changed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -16,10 +16,12 @@
|
|||||||
*/
|
*/
|
||||||
package io.emeraldpay.dshackle.upstream
|
package io.emeraldpay.dshackle.upstream
|
||||||
|
|
||||||
import io.emeraldpay.dshackle.upstream.ethereum.DirectEthereumApi
|
import io.emeraldpay.dshackle.reader.Reader
|
||||||
|
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
|
||||||
|
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
|
||||||
import org.reactivestreams.Publisher
|
import org.reactivestreams.Publisher
|
||||||
|
|
||||||
interface ApiSource<U : UpstreamApi> : Publisher<U> {
|
interface ApiSource : Publisher<Upstream> {
|
||||||
|
|
||||||
fun resolve()
|
fun resolve()
|
||||||
fun request(tries: Int)
|
fun request(tries: Int)
|
||||||
|
|||||||
@@ -1,145 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2020 EmeraldPay, Inc
|
|
||||||
* Copyright (c) 2019 ETCDEV GmbH
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
package io.emeraldpay.dshackle.upstream
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
|
||||||
import io.emeraldpay.dshackle.cache.Caches
|
|
||||||
import io.emeraldpay.dshackle.data.*
|
|
||||||
import io.emeraldpay.dshackle.upstream.ethereum.EthereumApi
|
|
||||||
import io.infinitape.etherjar.hex.HexQuantity
|
|
||||||
import org.slf4j.LoggerFactory
|
|
||||||
import reactor.core.publisher.Mono
|
|
||||||
import java.math.BigInteger
|
|
||||||
import java.util.function.Function
|
|
||||||
|
|
||||||
open class CachingEthereumApi(
|
|
||||||
private val objectMapper: ObjectMapper,
|
|
||||||
private val caches: Caches,
|
|
||||||
private val head: Head
|
|
||||||
): EthereumApi(objectMapper) {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private val log = LoggerFactory.getLogger(CachingEthereumApi::class.java)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create caching API with empty memory-only cache
|
|
||||||
*/
|
|
||||||
@JvmStatic
|
|
||||||
fun empty(objectMapper: ObjectMapper): CachingEthereumApi {
|
|
||||||
return CachingEthereumApi(objectMapper, Caches.default(objectMapper), EmptyHead())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val rawJsonBuilder = RawJsonBuilder()
|
|
||||||
|
|
||||||
private val cacheBlocks = caches.getBlocksByHash()
|
|
||||||
private val cacheBlocksByHeight = caches.getBlocksByHeight()
|
|
||||||
private val cacheTx = caches.getTxByHash()
|
|
||||||
private val cacheFullBlocks = caches.getFullBlocks()
|
|
||||||
private val cacheFullBlocksByHeight = caches.getFullBlocksByHeight()
|
|
||||||
|
|
||||||
fun readBlockByHash(id: Int, method: String, params: List<Any>): Mono<ByteArray> {
|
|
||||||
return if (params.size == 2) {
|
|
||||||
val includeTransactions = params[1].toString().toBoolean()
|
|
||||||
val cache = if (includeTransactions) {
|
|
||||||
cacheFullBlocks
|
|
||||||
} else {
|
|
||||||
cacheBlocks
|
|
||||||
}
|
|
||||||
Mono.just(params[0])
|
|
||||||
.map { BlockId.from(it as String) }
|
|
||||||
.flatMap(cache::read)
|
|
||||||
.transform(converter(id))
|
|
||||||
.transform(finalizer())
|
|
||||||
}
|
|
||||||
else Mono.empty()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun readBlockByNumber(id: Int, method: String, params: List<Any>): Mono<ByteArray> {
|
|
||||||
return if (params.size == 2) {
|
|
||||||
val includeTransactions = params[1].toString().toBoolean()
|
|
||||||
val cache = if (includeTransactions) {
|
|
||||||
cacheFullBlocksByHeight
|
|
||||||
} else {
|
|
||||||
cacheBlocksByHeight
|
|
||||||
}
|
|
||||||
Mono.just(params[0])
|
|
||||||
.map { HexQuantity.from(it as String) }
|
|
||||||
.filter { it.value < BigInteger.valueOf(Long.MAX_VALUE) }
|
|
||||||
.map { it.value.toLong() }
|
|
||||||
.flatMap(cache::read)
|
|
||||||
.transform(converter(id))
|
|
||||||
.transform(finalizer())
|
|
||||||
}
|
|
||||||
else Mono.empty()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun execute(id: Int, method: String, params: List<Any>): Mono<ByteArray> {
|
|
||||||
return when (method) {
|
|
||||||
"eth_blockNumber" ->
|
|
||||||
head.getFlux().next()
|
|
||||||
.map { HexQuantity.from(it.height).toHex() }
|
|
||||||
.map { objectMapper.writeValueAsBytes(it) }
|
|
||||||
.map(bytesToJson(id))
|
|
||||||
"eth_getBlockByHash" -> readBlockByHash(id, method, params)
|
|
||||||
"eth_getBlockByNumber" -> readBlockByNumber(id, method, params)
|
|
||||||
"eth_getTransactionByHash" ->
|
|
||||||
if (params.size == 1)
|
|
||||||
Mono.just(params[0])
|
|
||||||
.map { TxId.from(it as String) }
|
|
||||||
.flatMap(cacheTx::read)
|
|
||||||
.transform(converter(id))
|
|
||||||
.transform(finalizer())
|
|
||||||
else Mono.empty()
|
|
||||||
else ->
|
|
||||||
Mono.empty()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert to JSON RPC response
|
|
||||||
*/
|
|
||||||
fun converter(id: Int): Function<in Mono<out SourceContainer>, out Mono<ByteArray>> {
|
|
||||||
return Function { mono ->
|
|
||||||
mono.map(containerToJson(id))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle errors and other stuff
|
|
||||||
*/
|
|
||||||
fun finalizer(): Function<Mono<ByteArray>, Mono<ByteArray>> {
|
|
||||||
return Function { mono ->
|
|
||||||
mono.onErrorResume { t ->
|
|
||||||
log.warn("Error during read from cache", t)
|
|
||||||
Mono.empty()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun bytesToJson(id: Int): Function<ByteArray, ByteArray> {
|
|
||||||
return Function { data ->
|
|
||||||
rawJsonBuilder.write(id, data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun containerToJson(id: Int): Function<SourceContainer, ByteArray> {
|
|
||||||
return Function { data ->
|
|
||||||
rawJsonBuilder.write(id, data.json!!)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,135 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2020 EmeraldPay, Inc
|
|
||||||
* Copyright (c) 2019 ETCDEV GmbH
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
package io.emeraldpay.dshackle.upstream
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
|
||||||
import io.emeraldpay.dshackle.cache.Caches
|
|
||||||
import io.emeraldpay.grpc.Chain
|
|
||||||
import org.slf4j.LoggerFactory
|
|
||||||
import org.springframework.context.Lifecycle
|
|
||||||
import reactor.core.Disposable
|
|
||||||
import reactor.core.publisher.Mono
|
|
||||||
import java.lang.IllegalStateException
|
|
||||||
import java.time.Duration
|
|
||||||
|
|
||||||
/**
|
|
||||||
* General interface to upstream(s) to a single chain
|
|
||||||
*/
|
|
||||||
abstract class ChainUpstreams<U : UpstreamApi>(
|
|
||||||
val chain: Chain,
|
|
||||||
private val upstreams: MutableList<Upstream<U>>,
|
|
||||||
caches: Caches,
|
|
||||||
objectMapper: ObjectMapper
|
|
||||||
) : AggregatedUpstream<U>(objectMapper, caches), Lifecycle {
|
|
||||||
|
|
||||||
private val log = LoggerFactory.getLogger(ChainUpstreams::class.java)
|
|
||||||
private var seq = 0
|
|
||||||
protected var lagObserver: HeadLagObserver<U>? = null
|
|
||||||
private var subscription: Disposable? = null
|
|
||||||
|
|
||||||
open fun init() {
|
|
||||||
onUpstreamsUpdated()
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract fun updateHead(): Head
|
|
||||||
abstract fun setHead(head: Head)
|
|
||||||
|
|
||||||
override fun getId(): String {
|
|
||||||
return "!all:${chain.chainCode}"
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun isRunning(): Boolean {
|
|
||||||
return subscription != null
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun start() {
|
|
||||||
super.start()
|
|
||||||
subscription = observeStatus()
|
|
||||||
.distinctUntilChanged()
|
|
||||||
.subscribe { printStatus() }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun stop() {
|
|
||||||
super.stop()
|
|
||||||
subscription?.dispose()
|
|
||||||
subscription = null
|
|
||||||
getHead().let {
|
|
||||||
if (it is Lifecycle) {
|
|
||||||
it.stop()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
lagObserver?.stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getAll(): List<Upstream<U>> {
|
|
||||||
return upstreams
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun addUpstream(upstream: Upstream<U>) {
|
|
||||||
upstreams.add(upstream)
|
|
||||||
setHead(updateHead())
|
|
||||||
onUpstreamsUpdated()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun removeUpstream(id: String) {
|
|
||||||
if (upstreams.removeIf { it.getId() == id }) {
|
|
||||||
setHead(updateHead())
|
|
||||||
onUpstreamsUpdated()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getApis(matcher: Selector.Matcher): ApiSource<U> {
|
|
||||||
val i = seq++
|
|
||||||
if (seq >= Int.MAX_VALUE / 2) {
|
|
||||||
seq = 0
|
|
||||||
}
|
|
||||||
return FilteredApis(upstreams, matcher, i)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getApi(matcher: Selector.Matcher): Mono<U> {
|
|
||||||
val apis = getApis(matcher)
|
|
||||||
apis.request(1)
|
|
||||||
return Mono.from(apis)
|
|
||||||
.switchIfEmpty(Mono.error<U>(Exception("No API available")))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun setLag(lag: Long) {
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getLag(): Long {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
fun printStatus() {
|
|
||||||
var height: Long? = null
|
|
||||||
try {
|
|
||||||
height = getHead().getFlux().next().block(Duration.ofSeconds(1))?.height
|
|
||||||
} 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(",")
|
|
||||||
val lag = upstreams.map { it.getLag() }
|
|
||||||
.joinToString(", ")
|
|
||||||
|
|
||||||
log.info("State of ${chain.chainCode}: height=${height ?: '?'}, status=$statuses, lag=[$lag]")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -21,14 +21,12 @@ import io.emeraldpay.dshackle.BlockchainType
|
|||||||
import io.emeraldpay.dshackle.cache.CachesEnabled
|
import io.emeraldpay.dshackle.cache.CachesEnabled
|
||||||
import io.emeraldpay.dshackle.cache.CachesFactory
|
import io.emeraldpay.dshackle.cache.CachesFactory
|
||||||
import io.emeraldpay.dshackle.startup.UpstreamChange
|
import io.emeraldpay.dshackle.startup.UpstreamChange
|
||||||
import io.emeraldpay.dshackle.upstream.bitcoin.DirectBitcoinApi
|
import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinMultistream
|
||||||
import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinChainUpstreams
|
|
||||||
import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinUpstream
|
import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinUpstream
|
||||||
import io.emeraldpay.dshackle.upstream.bitcoin.DefaultBitcoinMethods
|
import io.emeraldpay.dshackle.upstream.calls.DefaultBitcoinMethods
|
||||||
import io.emeraldpay.dshackle.upstream.calls.CallMethods
|
import io.emeraldpay.dshackle.upstream.calls.CallMethods
|
||||||
import io.emeraldpay.dshackle.upstream.calls.DefaultEthereumMethods
|
import io.emeraldpay.dshackle.upstream.calls.DefaultEthereumMethods
|
||||||
import io.emeraldpay.dshackle.upstream.ethereum.EthereumApi
|
import io.emeraldpay.dshackle.upstream.ethereum.EthereumMultistream
|
||||||
import io.emeraldpay.dshackle.upstream.ethereum.AggregatedEthereumUpstreams
|
|
||||||
import io.emeraldpay.dshackle.upstream.ethereum.EthereumUpstream
|
import io.emeraldpay.dshackle.upstream.ethereum.EthereumUpstream
|
||||||
import io.emeraldpay.grpc.Chain
|
import io.emeraldpay.grpc.Chain
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
@@ -44,14 +42,14 @@ import java.util.concurrent.locks.ReentrantLock
|
|||||||
import kotlin.concurrent.withLock
|
import kotlin.concurrent.withLock
|
||||||
|
|
||||||
@Repository
|
@Repository
|
||||||
class CurrentUpstreams(
|
class CurrentMultistreamHolder(
|
||||||
@Autowired private val objectMapper: ObjectMapper,
|
@Autowired private val objectMapper: ObjectMapper,
|
||||||
@Autowired private val cachesFactory: CachesFactory
|
@Autowired private val cachesFactory: CachesFactory
|
||||||
): Upstreams {
|
) : MultistreamHolder {
|
||||||
|
|
||||||
private val log = LoggerFactory.getLogger(CurrentUpstreams::class.java)
|
private val log = LoggerFactory.getLogger(CurrentMultistreamHolder::class.java)
|
||||||
|
|
||||||
private val chainMapping = ConcurrentHashMap<Chain, ChainUpstreams<*>>()
|
private val chainMapping = ConcurrentHashMap<Chain, Multistream>()
|
||||||
private val chainsBus = TopicProcessor.create<Chain>()
|
private val chainsBus = TopicProcessor.create<Chain>()
|
||||||
private val callTargets = HashMap<Chain, CallMethods>()
|
private val callTargets = HashMap<Chain, CallMethods>()
|
||||||
private val updateLock = ReentrantLock()
|
private val updateLock = ReentrantLock()
|
||||||
@@ -61,20 +59,18 @@ class CurrentUpstreams(
|
|||||||
val chain = change.chain
|
val chain = change.chain
|
||||||
when (BlockchainType.fromBlockchain(chain)) {
|
when (BlockchainType.fromBlockchain(chain)) {
|
||||||
BlockchainType.ETHEREUM -> {
|
BlockchainType.ETHEREUM -> {
|
||||||
val up = change.upstream
|
val up = change.upstream.cast(EthereumUpstream::class.java)
|
||||||
.cast(EthereumUpstream::class.java, EthereumApi::class.java) as Upstream<EthereumApi>
|
val current = chainMapping[chain] as Multistream?
|
||||||
val current = chainMapping[chain] as ChainUpstreams<EthereumApi>?
|
|
||||||
val factory = Callable {
|
val factory = Callable {
|
||||||
AggregatedEthereumUpstreams(chain, ArrayList(), cachesFactory.getCaches(chain), objectMapper) as ChainUpstreams<EthereumApi>
|
EthereumMultistream(chain, ArrayList(), cachesFactory.getCaches(chain), objectMapper) as Multistream
|
||||||
}
|
}
|
||||||
processUpdate(change, up, current, factory)
|
processUpdate(change, up, current, factory)
|
||||||
}
|
}
|
||||||
BlockchainType.BITCOIN -> {
|
BlockchainType.BITCOIN -> {
|
||||||
val up = change.upstream
|
val up = change.upstream.cast(BitcoinUpstream::class.java)
|
||||||
.cast(BitcoinUpstream::class.java, DirectBitcoinApi::class.java)
|
val current = chainMapping[chain] as Multistream?
|
||||||
val current = chainMapping[chain] as ChainUpstreams<DirectBitcoinApi>?
|
|
||||||
val factory = Callable {
|
val factory = Callable {
|
||||||
BitcoinChainUpstreams(chain, ArrayList(), cachesFactory.getCaches(chain), objectMapper) as ChainUpstreams<DirectBitcoinApi>
|
BitcoinMultistream(chain, ArrayList(), cachesFactory.getCaches(chain), objectMapper) as Multistream
|
||||||
}
|
}
|
||||||
processUpdate(change, up, current, factory)
|
processUpdate(change, up, current, factory)
|
||||||
}
|
}
|
||||||
@@ -85,7 +81,7 @@ class CurrentUpstreams(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun <A : UpstreamApi> processUpdate(change: UpstreamChange, up: Upstream<A>, current: ChainUpstreams<A>?, factory: Callable<ChainUpstreams<A>>) {
|
fun processUpdate(change: UpstreamChange, up: Upstream, current: Multistream?, factory: Callable<Multistream>) {
|
||||||
val chain = change.chain
|
val chain = change.chain
|
||||||
if (change.type == UpstreamChange.ChangeType.REMOVED) {
|
if (change.type == UpstreamChange.ChangeType.REMOVED) {
|
||||||
current?.removeUpstream(up.getId())
|
current?.removeUpstream(up.getId())
|
||||||
@@ -113,7 +109,7 @@ class CurrentUpstreams(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getUpstream(chain: Chain): AggregatedUpstream<*>? {
|
override fun getUpstream(chain: Chain): Multistream? {
|
||||||
return chainMapping[chain]
|
return chainMapping[chain]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22,13 +22,13 @@ import reactor.core.publisher.Flux
|
|||||||
import reactor.core.publisher.TopicProcessor
|
import reactor.core.publisher.TopicProcessor
|
||||||
import java.util.concurrent.atomic.AtomicReference
|
import java.util.concurrent.atomic.AtomicReference
|
||||||
|
|
||||||
abstract class DefaultUpstream<U : UpstreamApi>(
|
abstract class DefaultUpstream(
|
||||||
private val id: String,
|
private val id: String,
|
||||||
defaultLag: Long,
|
defaultLag: Long,
|
||||||
defaultAvail: UpstreamAvailability,
|
defaultAvail: UpstreamAvailability,
|
||||||
private val options: UpstreamsConfig.Options,
|
private val options: UpstreamsConfig.Options,
|
||||||
private val targets: CallMethods?
|
private val targets: CallMethods?
|
||||||
) : Upstream<U> {
|
) : Upstream {
|
||||||
|
|
||||||
constructor(id: String, options: UpstreamsConfig.Options, targets: CallMethods?) : this(id, Long.MAX_VALUE, UpstreamAvailability.UNAVAILABLE, options, targets)
|
constructor(id: String, options: UpstreamsConfig.Options, targets: CallMethods?) : this(id, Long.MAX_VALUE, UpstreamAvailability.UNAVAILABLE, options, targets)
|
||||||
|
|
||||||
|
|||||||
@@ -25,4 +25,7 @@ class EmptyHead : Head {
|
|||||||
override fun getFlux(): Flux<BlockContainer> {
|
override fun getFlux(): Flux<BlockContainer> {
|
||||||
return Flux.empty()
|
return Flux.empty()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onBeforeBlock(handler: Runnable) {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -16,6 +16,9 @@
|
|||||||
*/
|
*/
|
||||||
package io.emeraldpay.dshackle.upstream
|
package io.emeraldpay.dshackle.upstream
|
||||||
|
|
||||||
|
import io.emeraldpay.dshackle.reader.Reader
|
||||||
|
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
|
||||||
|
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
|
||||||
import org.reactivestreams.Subscriber
|
import org.reactivestreams.Subscriber
|
||||||
import reactor.core.publisher.EmitterProcessor
|
import reactor.core.publisher.EmitterProcessor
|
||||||
import reactor.core.publisher.Flux
|
import reactor.core.publisher.Flux
|
||||||
@@ -26,28 +29,28 @@ import kotlin.math.pow
|
|||||||
import kotlin.math.roundToLong
|
import kotlin.math.roundToLong
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
|
|
||||||
class FilteredApis<U : UpstreamApi>(
|
class FilteredApis(
|
||||||
allUpstreams: List<Upstream<U>>,
|
allUpstreams: List<Upstream>,
|
||||||
private val matcher: Selector.Matcher,
|
private val matcher: Selector.Matcher,
|
||||||
pos: Int,
|
pos: Int,
|
||||||
private val repeatLimit: Long,
|
private val repeatLimit: Long,
|
||||||
jitter: Int
|
jitter: Int
|
||||||
) : ApiSource<U> {
|
) : ApiSource {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val DEFAULT_DELAY_STEP = 100
|
private const val DEFAULT_DELAY_STEP = 100
|
||||||
private const val MAX_WAIT_MILLIS = 5000L
|
private const val MAX_WAIT_MILLIS = 5000L
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(allUpstreams: List<Upstream<U>>,
|
constructor(allUpstreams: List<Upstream>,
|
||||||
matcher: Selector.Matcher,
|
matcher: Selector.Matcher,
|
||||||
pos: Int) : this(allUpstreams, matcher, pos, 10, 7)
|
pos: Int) : this(allUpstreams, matcher, pos, 10, 7)
|
||||||
|
|
||||||
constructor(allUpstreams: List<Upstream<U>>,
|
constructor(allUpstreams: List<Upstream>,
|
||||||
matcher: Selector.Matcher) : this(allUpstreams, matcher, 0, 10, 10)
|
matcher: Selector.Matcher) : this(allUpstreams, matcher, 0, 10, 10)
|
||||||
|
|
||||||
private val delay: Int
|
private val delay: Int
|
||||||
private val upstreams: List<Upstream<UpstreamApi>>
|
private val upstreams: List<Upstream>
|
||||||
|
|
||||||
private val control = EmitterProcessor.create<Boolean>(32, false)
|
private val control = EmitterProcessor.create<Boolean>(32, false)
|
||||||
|
|
||||||
@@ -75,18 +78,18 @@ class FilteredApis<U : UpstreamApi>(
|
|||||||
return Duration.ofMillis(time)
|
return Duration.ofMillis(time)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun subscribe(subscriber: Subscriber<in U>) {
|
override fun subscribe(subscriber: Subscriber<in Upstream>) {
|
||||||
val first = Flux.fromIterable(upstreams)
|
val first = Flux.fromIterable(upstreams)
|
||||||
val retries = (1 until repeatLimit).map { r ->
|
val retries = (1 until repeatLimit).map { r ->
|
||||||
Flux.fromIterable(upstreams).delaySubscription(waitDuration(r))
|
Flux.fromIterable(upstreams).delaySubscription(waitDuration(r))
|
||||||
}.let { Flux.concat(it) }
|
}.let { Flux.concat(it) }
|
||||||
|
|
||||||
Flux.concat(first, retries)
|
Flux.concat(first, retries)
|
||||||
.filter(Upstream<UpstreamApi>::isAvailable)
|
.filter(Upstream::isAvailable)
|
||||||
.filter(matcher::matches)
|
.filter(matcher::matches)
|
||||||
.flatMap { it.getApi(matcher) }
|
.zipWith(control)
|
||||||
.zipWith(control).map { it.t1 }
|
.map { it.t1 }
|
||||||
.subscribe(subscriber as Subscriber<in UpstreamApi>)
|
.subscribe(subscriber)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun resolve() {
|
override fun resolve() {
|
||||||
|
|||||||
@@ -20,6 +20,20 @@ import io.emeraldpay.dshackle.data.BlockContainer
|
|||||||
import reactor.core.publisher.Flux
|
import reactor.core.publisher.Flux
|
||||||
import reactor.core.publisher.Mono
|
import reactor.core.publisher.Mono
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscription to listen to updates to the head of a blockchain.
|
||||||
|
*/
|
||||||
interface Head {
|
interface Head {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return stream of all new blocks, starts from the current block (i.e., first item should be available immediately).
|
||||||
|
*/
|
||||||
fun getFlux(): Flux<BlockContainer>
|
fun getFlux(): Flux<BlockContainer>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add handler that is going to be called each time _before_ a new block is submitted to stream of new blocks.
|
||||||
|
* Supposed to be used for cleanup/preparation before actual block data will come, to avoid race condition.
|
||||||
|
* @see getFlux
|
||||||
|
*/
|
||||||
|
fun onBeforeBlock(handler: Runnable)
|
||||||
}
|
}
|
||||||
@@ -28,9 +28,9 @@ import reactor.util.function.Tuples
|
|||||||
* Observer group of upstreams and defined a distance in blocks (lag) between a leader (best height/difficulty) and
|
* Observer group of upstreams and defined a distance in blocks (lag) between a leader (best height/difficulty) and
|
||||||
* other upstreams.
|
* other upstreams.
|
||||||
*/
|
*/
|
||||||
abstract class HeadLagObserver<A : UpstreamApi>(
|
abstract class HeadLagObserver(
|
||||||
private val master: Head,
|
private val master: Head,
|
||||||
private val followers: Collection<Upstream<A>>
|
private val followers: Collection<Upstream>
|
||||||
) : Lifecycle {
|
) : Lifecycle {
|
||||||
|
|
||||||
private val log = LoggerFactory.getLogger(HeadLagObserver::class.java)
|
private val log = LoggerFactory.getLogger(HeadLagObserver::class.java)
|
||||||
@@ -58,7 +58,7 @@ abstract class HeadLagObserver<A : UpstreamApi>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun probeFollowers(top: BlockContainer): Flux<Tuple2<Long, Upstream<A>>> {
|
fun probeFollowers(top: BlockContainer): Flux<Tuple2<Long, Upstream>> {
|
||||||
return Flux.fromIterable(followers)
|
return Flux.fromIterable(followers)
|
||||||
.parallel(followers.size)
|
.parallel(followers.size)
|
||||||
.flatMap { mapLagging(top, it, getCurrentBlocks(it)) }
|
.flatMap { mapLagging(top, it, getCurrentBlocks(it)) }
|
||||||
@@ -66,9 +66,9 @@ abstract class HeadLagObserver<A : UpstreamApi>(
|
|||||||
.onErrorContinue { t, _ -> log.warn("Failed to update lagging distance", t) }
|
.onErrorContinue { t, _ -> log.warn("Failed to update lagging distance", t) }
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract fun getCurrentBlocks(up: Upstream<A>): Flux<BlockContainer>
|
abstract fun getCurrentBlocks(up: Upstream): Flux<BlockContainer>
|
||||||
|
|
||||||
fun mapLagging(top: BlockContainer, up: Upstream<A>, blocks: Flux<BlockContainer>): Flux<Tuple2<Long, Upstream<A>>> {
|
fun mapLagging(top: BlockContainer, up: Upstream, blocks: Flux<BlockContainer>): Flux<Tuple2<Long, Upstream>> {
|
||||||
return blocks
|
return blocks
|
||||||
.map { extractDistance(top, it) }
|
.map { extractDistance(top, it) }
|
||||||
.takeUntil { lag -> lag <= 0L }
|
.takeUntil { lag -> lag <= 0L }
|
||||||
|
|||||||
235
src/main/kotlin/io/emeraldpay/dshackle/upstream/Multistream.kt
Normal file
235
src/main/kotlin/io/emeraldpay/dshackle/upstream/Multistream.kt
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2020 EmeraldPay, Inc
|
||||||
|
* Copyright (c) 2019 ETCDEV GmbH
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package io.emeraldpay.dshackle.upstream
|
||||||
|
|
||||||
|
import io.emeraldpay.dshackle.cache.*
|
||||||
|
import io.emeraldpay.dshackle.config.UpstreamsConfig
|
||||||
|
import io.emeraldpay.dshackle.reader.Reader
|
||||||
|
import io.emeraldpay.dshackle.upstream.calls.AggregatedCallMethods
|
||||||
|
import io.emeraldpay.dshackle.upstream.calls.CallMethods
|
||||||
|
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
|
||||||
|
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
|
||||||
|
import io.emeraldpay.grpc.Chain
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import org.springframework.context.Lifecycle
|
||||||
|
import reactor.core.Disposable
|
||||||
|
import reactor.core.publisher.Flux
|
||||||
|
import reactor.core.publisher.Mono
|
||||||
|
import java.time.Duration
|
||||||
|
import java.time.Instant
|
||||||
|
import java.util.concurrent.atomic.AtomicReference
|
||||||
|
import java.util.concurrent.locks.ReentrantLock
|
||||||
|
import java.util.function.Predicate
|
||||||
|
import kotlin.concurrent.withLock
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Aggregation of multiple upstreams responding to a single blockchain
|
||||||
|
*/
|
||||||
|
abstract class Multistream(
|
||||||
|
val chain: Chain,
|
||||||
|
private val upstreams: MutableList<Upstream>,
|
||||||
|
val caches: Caches
|
||||||
|
) : Upstream, Lifecycle {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val log = LoggerFactory.getLogger(Multistream::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var cacheSubscription: Disposable? = null
|
||||||
|
private val reconfigLock = ReentrantLock()
|
||||||
|
private var callMethods: CallMethods? = null
|
||||||
|
private var seq = 0
|
||||||
|
protected var lagObserver: HeadLagObserver? = null
|
||||||
|
private var subscription: Disposable? = null
|
||||||
|
|
||||||
|
open fun init() {
|
||||||
|
onUpstreamsUpdated()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get list of all underlying upstreams
|
||||||
|
*/
|
||||||
|
fun getAll(): List<Upstream> {
|
||||||
|
return upstreams
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add an upstream
|
||||||
|
*/
|
||||||
|
fun addUpstream(upstream: Upstream) {
|
||||||
|
upstreams.add(upstream)
|
||||||
|
setHead(updateHead())
|
||||||
|
onUpstreamsUpdated()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeUpstream(id: String) {
|
||||||
|
if (upstreams.removeIf { it.getId() == id }) {
|
||||||
|
setHead(updateHead())
|
||||||
|
onUpstreamsUpdated()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a source for direct APIs
|
||||||
|
*/
|
||||||
|
fun getApiSource(matcher: Selector.Matcher): ApiSource {
|
||||||
|
val i = seq++
|
||||||
|
if (seq >= Int.MAX_VALUE / 2) {
|
||||||
|
seq = 0
|
||||||
|
}
|
||||||
|
return FilteredApis(upstreams, matcher, i)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds an API that executed directly on a remote.
|
||||||
|
*/
|
||||||
|
fun getDirectApi(matcher: Selector.Matcher): Mono<Reader<JsonRpcRequest, JsonRpcResponse>> {
|
||||||
|
val apis = getApiSource(matcher)
|
||||||
|
apis.request(1)
|
||||||
|
return Mono.from(apis)
|
||||||
|
.map(Upstream::getApi)
|
||||||
|
.switchIfEmpty(Mono.error(Exception("No API available for $chain")))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds an API that leverages caches and other optimizations/transformations of the request.
|
||||||
|
*/
|
||||||
|
abstract fun getRoutedApi(matcher: Selector.Matcher): Mono<Reader<JsonRpcRequest, JsonRpcResponse>>
|
||||||
|
|
||||||
|
override fun getApi(): Reader<JsonRpcRequest, JsonRpcResponse> {
|
||||||
|
throw NotImplementedError("Immediate direct API is not implemented for Aggregated Upstream")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onUpstreamsUpdated() {
|
||||||
|
reconfigLock.withLock {
|
||||||
|
getAll().map { it.getMethods() }.let {
|
||||||
|
callMethods = AggregatedCallMethods(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun observeStatus(): Flux<UpstreamAvailability> {
|
||||||
|
val upstreamsFluxes = getAll().map { up -> up.observeStatus().map { UpstreamStatus(up, it) } }
|
||||||
|
return Flux.merge(upstreamsFluxes)
|
||||||
|
.filter(FilterBestAvailability())
|
||||||
|
.map { it.status }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isAvailable(): Boolean {
|
||||||
|
return getAll().any { it.isAvailable() }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getStatus(): UpstreamAvailability {
|
||||||
|
val upstreams = getAll()
|
||||||
|
return if (upstreams.isEmpty()) UpstreamAvailability.UNAVAILABLE
|
||||||
|
else upstreams.map { it.getStatus() }.min()!!
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getOptions(): UpstreamsConfig.Options {
|
||||||
|
return UpstreamsConfig.Options()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getMethods(): CallMethods {
|
||||||
|
return callMethods ?: throw IllegalStateException("Methods are not initialized yet")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun start() {
|
||||||
|
subscription = observeStatus()
|
||||||
|
.distinctUntilChanged()
|
||||||
|
.subscribe { printStatus() }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun stop() {
|
||||||
|
cacheSubscription?.dispose()
|
||||||
|
cacheSubscription = null
|
||||||
|
subscription?.dispose()
|
||||||
|
subscription = null
|
||||||
|
getHead().let {
|
||||||
|
if (it is Lifecycle) {
|
||||||
|
it.stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lagObserver?.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onHeadUpdated(head: Head) {
|
||||||
|
reconfigLock.withLock {
|
||||||
|
cacheSubscription?.dispose()
|
||||||
|
cacheSubscription = head.getFlux().subscribe {
|
||||||
|
caches.cache(Caches.Tag.LATEST, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract fun updateHead(): Head
|
||||||
|
abstract fun setHead(head: Head)
|
||||||
|
|
||||||
|
override fun getId(): String {
|
||||||
|
return "!all:${chain.chainCode}"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isRunning(): Boolean {
|
||||||
|
return subscription != null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setLag(lag: Long) {
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getLag(): Long {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
fun printStatus() {
|
||||||
|
var height: Long? = null
|
||||||
|
try {
|
||||||
|
height = getHead().getFlux().next().block(Duration.ofSeconds(1))?.height
|
||||||
|
} catch (e: java.lang.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(",")
|
||||||
|
val lag = upstreams.map { it.getLag() }
|
||||||
|
.joinToString(", ")
|
||||||
|
|
||||||
|
log.info("State of ${chain.chainCode}: height=${height ?: '?'}, status=$statuses, lag=[$lag]")
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class UpstreamStatus(val upstream: Upstream, val status: UpstreamAvailability, val ts: Instant = Instant.now())
|
||||||
|
|
||||||
|
class FilterBestAvailability() : Predicate<UpstreamStatus> {
|
||||||
|
private val lastRef = AtomicReference<UpstreamStatus>()
|
||||||
|
|
||||||
|
override fun test(t: UpstreamStatus): Boolean {
|
||||||
|
val last = lastRef.get()
|
||||||
|
val changed = last == null
|
||||||
|
|| t.status > last.status
|
||||||
|
|| (last.upstream == t.upstream && t.status != last.status)
|
||||||
|
|| last.ts.isBefore(Instant.now() - Duration.ofSeconds(60))
|
||||||
|
if (changed) {
|
||||||
|
lastRef.set(t)
|
||||||
|
}
|
||||||
|
return changed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -20,8 +20,11 @@ import io.emeraldpay.dshackle.upstream.calls.CallMethods
|
|||||||
import io.emeraldpay.grpc.Chain
|
import io.emeraldpay.grpc.Chain
|
||||||
import reactor.core.publisher.Flux
|
import reactor.core.publisher.Flux
|
||||||
|
|
||||||
interface Upstreams {
|
/**
|
||||||
fun getUpstream(chain: Chain): AggregatedUpstream<*>?
|
* Holds Multistreams configured for a chain.
|
||||||
|
*/
|
||||||
|
interface MultistreamHolder {
|
||||||
|
fun getUpstream(chain: Chain): Multistream?
|
||||||
fun getAvailable(): List<Chain>
|
fun getAvailable(): List<Chain>
|
||||||
fun observeChains(): Flux<Chain>
|
fun observeChains(): Flux<Chain>
|
||||||
fun getDefaultMethods(chain: Chain): CallMethods
|
fun getDefaultMethods(chain: Chain): CallMethods
|
||||||
@@ -96,13 +96,13 @@ class Selector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface Matcher {
|
interface Matcher {
|
||||||
fun matches(up: Upstream<UpstreamApi>): Boolean
|
fun matches(up: Upstream): Boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
class MultiMatcher(
|
class MultiMatcher(
|
||||||
private val matchers: Collection<Matcher>
|
private val matchers: Collection<Matcher>
|
||||||
): Matcher {
|
): Matcher {
|
||||||
override fun matches(up: Upstream<UpstreamApi>): Boolean {
|
override fun matches(up: Upstream): Boolean {
|
||||||
return matchers.all { it.matches(up) }
|
return matchers.all { it.matches(up) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,13 +114,13 @@ class Selector {
|
|||||||
class MethodMatcher(
|
class MethodMatcher(
|
||||||
val method: String
|
val method: String
|
||||||
): Matcher {
|
): Matcher {
|
||||||
override fun matches(up: Upstream<UpstreamApi>): Boolean {
|
override fun matches(up: Upstream): Boolean {
|
||||||
return up.getMethods().isAllowed(method)
|
return up.getMethods().isAllowed(method)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class LabelSelectorMatcher: Matcher {
|
abstract class LabelSelectorMatcher: Matcher {
|
||||||
override fun matches(up: Upstream<UpstreamApi>): Boolean {
|
override fun matches(up: Upstream): Boolean {
|
||||||
return up.getLabels().any(this::matches)
|
return up.getLabels().any(this::matches)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,7 +129,7 @@ class Selector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class EmptyMatcher: Matcher {
|
class EmptyMatcher: Matcher {
|
||||||
override fun matches(up: Upstream<UpstreamApi>): Boolean {
|
override fun matches(up: Upstream): Boolean {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -144,7 +144,7 @@ class Selector {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun matches(up: Upstream<UpstreamApi>): Boolean {
|
override fun matches(up: Upstream): Boolean {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,16 +17,19 @@
|
|||||||
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.dshackle.reader.Reader
|
||||||
import io.emeraldpay.dshackle.upstream.calls.CallMethods
|
import io.emeraldpay.dshackle.upstream.calls.CallMethods
|
||||||
|
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
|
||||||
|
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
|
||||||
import reactor.core.publisher.Flux
|
import reactor.core.publisher.Flux
|
||||||
import reactor.core.publisher.Mono
|
import reactor.core.publisher.Mono
|
||||||
|
|
||||||
interface Upstream<out A : UpstreamApi> {
|
interface Upstream {
|
||||||
fun isAvailable(): Boolean
|
fun isAvailable(): Boolean
|
||||||
fun getStatus(): UpstreamAvailability
|
fun getStatus(): UpstreamAvailability
|
||||||
fun observeStatus(): Flux<UpstreamAvailability>
|
fun observeStatus(): Flux<UpstreamAvailability>
|
||||||
fun getHead(): Head
|
fun getHead(): Head
|
||||||
fun getApi(matcher: Selector.Matcher): Mono<out A>
|
fun getApi(): Reader<JsonRpcRequest, JsonRpcResponse>
|
||||||
fun getOptions(): UpstreamsConfig.Options
|
fun getOptions(): UpstreamsConfig.Options
|
||||||
fun setLag(lag: Long)
|
fun setLag(lag: Long)
|
||||||
fun getLag(): Long
|
fun getLag(): Long
|
||||||
@@ -34,6 +37,5 @@ interface Upstream<out A : UpstreamApi> {
|
|||||||
fun getMethods(): CallMethods
|
fun getMethods(): CallMethods
|
||||||
fun getId(): String
|
fun getId(): String
|
||||||
|
|
||||||
fun <TA : UpstreamApi> castApi(apiType: Class<TA>): Upstream<TA>
|
fun <T : Upstream> cast(selfType: Class<T>): T
|
||||||
fun <T : Upstream<TA>, TA : UpstreamApi> cast(selfType: Class<T>, apiType: Class<TA>): T
|
|
||||||
}
|
}
|
||||||
@@ -18,24 +18,29 @@ package io.emeraldpay.dshackle.upstream.bitcoin
|
|||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
import io.emeraldpay.dshackle.cache.Caches
|
import io.emeraldpay.dshackle.cache.Caches
|
||||||
import io.emeraldpay.dshackle.config.UpstreamsConfig
|
import io.emeraldpay.dshackle.config.UpstreamsConfig
|
||||||
|
import io.emeraldpay.dshackle.reader.EmptyReader
|
||||||
|
import io.emeraldpay.dshackle.reader.Reader
|
||||||
import io.emeraldpay.dshackle.upstream.*
|
import io.emeraldpay.dshackle.upstream.*
|
||||||
import io.emeraldpay.dshackle.upstream.ethereum.EthereumApi
|
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
|
||||||
|
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
|
||||||
import io.emeraldpay.grpc.Chain
|
import io.emeraldpay.grpc.Chain
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import org.springframework.context.Lifecycle
|
import org.springframework.context.Lifecycle
|
||||||
|
import reactor.core.publisher.Mono
|
||||||
|
|
||||||
class BitcoinChainUpstreams(
|
open class BitcoinMultistream(
|
||||||
chain: Chain,
|
chain: Chain,
|
||||||
val upstreams: MutableList<BitcoinUpstream>,
|
val upstreams: MutableList<BitcoinUpstream>,
|
||||||
caches: Caches,
|
caches: Caches,
|
||||||
objectMapper: ObjectMapper
|
private val objectMapper: ObjectMapper
|
||||||
) : ChainUpstreams<DirectBitcoinApi>(chain, upstreams as MutableList<Upstream<DirectBitcoinApi>>, caches, objectMapper) {
|
) : Multistream(chain, upstreams as MutableList<Upstream>, caches), Lifecycle {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val log = LoggerFactory.getLogger(BitcoinChainUpstreams::class.java)
|
private val log = LoggerFactory.getLogger(BitcoinMultistream::class.java)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var head: Head? = null
|
private var head: Head? = null
|
||||||
|
private var reader = BitcoinReader(this, EmptyHead(), objectMapper)
|
||||||
|
|
||||||
override fun init() {
|
override fun init() {
|
||||||
if (upstreams.size > 0) {
|
if (upstreams.size > 0) {
|
||||||
@@ -68,8 +73,18 @@ class BitcoinChainUpstreams(
|
|||||||
return head
|
return head
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getRoutedApi(matcher: Selector.Matcher): Mono<Reader<JsonRpcRequest, JsonRpcResponse>> {
|
||||||
|
//TODO
|
||||||
|
return Mono.just(EmptyReader())
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun getReader(): BitcoinReader {
|
||||||
|
return reader
|
||||||
|
}
|
||||||
|
|
||||||
override fun setHead(head: Head) {
|
override fun setHead(head: Head) {
|
||||||
this.head = head
|
this.head = head
|
||||||
|
reader = BitcoinReader(this, head, objectMapper)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getHead(): Head {
|
override fun getHead(): Head {
|
||||||
@@ -80,18 +95,24 @@ class BitcoinChainUpstreams(
|
|||||||
return upstreams.flatMap { it.getLabels() }
|
return upstreams.flatMap { it.getLabels() }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun <A : UpstreamApi> castApi(apiType: Class<A>): Upstream<A> {
|
override fun <T : Upstream> cast(selfType: Class<T>): T {
|
||||||
if (!apiType.isAssignableFrom(DirectBitcoinApi::class.java)) {
|
|
||||||
throw ClassCastException("Cannot cast ${EthereumApi::class.java} to $apiType")
|
|
||||||
}
|
|
||||||
return this as Upstream<A>
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun <T : Upstream<TA>, TA : UpstreamApi> cast(selfType: Class<T>, apiType: Class<TA>): T {
|
|
||||||
if (!selfType.isAssignableFrom(this.javaClass)) {
|
if (!selfType.isAssignableFrom(this.javaClass)) {
|
||||||
throw ClassCastException("Cannot cast ${this.javaClass} to $selfType")
|
throw ClassCastException("Cannot cast ${this.javaClass} to $selfType")
|
||||||
}
|
}
|
||||||
return castApi(apiType) as T
|
return this as T
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun isRunning(): Boolean {
|
||||||
|
return super.isRunning() || reader.isRunning
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun start() {
|
||||||
|
super.start()
|
||||||
|
reader.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun stop() {
|
||||||
|
super.stop()
|
||||||
|
reader.stop()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -15,25 +15,44 @@
|
|||||||
*/
|
*/
|
||||||
package io.emeraldpay.dshackle.upstream.bitcoin
|
package io.emeraldpay.dshackle.upstream.bitcoin
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
import io.emeraldpay.dshackle.upstream.Head
|
import io.emeraldpay.dshackle.upstream.Head
|
||||||
|
import io.emeraldpay.dshackle.upstream.Selector
|
||||||
|
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
|
||||||
|
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import org.springframework.context.Lifecycle
|
import org.springframework.context.Lifecycle
|
||||||
|
import reactor.core.publisher.Mono
|
||||||
|
import reactor.kotlin.core.publisher.cast
|
||||||
|
|
||||||
open class BitcoinReader(
|
open class BitcoinReader(
|
||||||
api: DirectBitcoinApi,
|
private val upstreams: BitcoinMultistream,
|
||||||
head: Head
|
head: Head,
|
||||||
|
private val objectMapper: ObjectMapper
|
||||||
) : Lifecycle {
|
) : Lifecycle {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val log = LoggerFactory.getLogger(BitcoinReader::class.java)
|
private val log = LoggerFactory.getLogger(BitcoinReader::class.java)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val mempool = CachingMempoolData(api, head)
|
private val mempool = CachingMempoolData(upstreams, head, objectMapper)
|
||||||
|
|
||||||
open fun getMempool(): CachingMempoolData {
|
open fun getMempool(): CachingMempoolData {
|
||||||
return mempool
|
return mempool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
open fun getBlock(hash: String): Mono<Map<String, Any>> {
|
||||||
|
return castedRead(JsonRpcRequest("getblock", listOf(hash)), Map::class.java).cast()
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun getTx(txid: String): Mono<Map<String, Any>> {
|
||||||
|
return castedRead(JsonRpcRequest("getrawtransaction", listOf(txid, true)), Map::class.java).cast()
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun listUnspent(): Mono<List<String>> {
|
||||||
|
return castedRead(JsonRpcRequest("listunspent", emptyList()), List::class.java).cast()
|
||||||
|
}
|
||||||
|
|
||||||
override fun isRunning(): Boolean {
|
override fun isRunning(): Boolean {
|
||||||
return mempool.isRunning
|
return mempool.isRunning
|
||||||
}
|
}
|
||||||
@@ -45,4 +64,14 @@ open class BitcoinReader(
|
|||||||
override fun stop() {
|
override fun stop() {
|
||||||
mempool.stop()
|
mempool.stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun <T> castedRead(req: JsonRpcRequest, clazz: Class<T>): Mono<T> {
|
||||||
|
return upstreams.getDirectApi(Selector.empty).flatMap { api ->
|
||||||
|
api.read(req)
|
||||||
|
.flatMap(JsonRpcResponse::requireResult)
|
||||||
|
.map {
|
||||||
|
objectMapper.readValue(it, clazz) as T
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -16,8 +16,11 @@
|
|||||||
package io.emeraldpay.dshackle.upstream.bitcoin
|
package io.emeraldpay.dshackle.upstream.bitcoin
|
||||||
|
|
||||||
import io.emeraldpay.dshackle.Defaults
|
import io.emeraldpay.dshackle.Defaults
|
||||||
|
import io.emeraldpay.dshackle.reader.Reader
|
||||||
import io.emeraldpay.dshackle.upstream.AbstractHead
|
import io.emeraldpay.dshackle.upstream.AbstractHead
|
||||||
import io.emeraldpay.dshackle.upstream.Head
|
import io.emeraldpay.dshackle.upstream.Head
|
||||||
|
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
|
||||||
|
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import org.springframework.context.Lifecycle
|
import org.springframework.context.Lifecycle
|
||||||
import org.springframework.scheduling.concurrent.CustomizableThreadFactory
|
import org.springframework.scheduling.concurrent.CustomizableThreadFactory
|
||||||
@@ -29,7 +32,7 @@ import java.time.Duration
|
|||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
|
|
||||||
class BitcoinRpcHead(
|
class BitcoinRpcHead(
|
||||||
private val api: DirectBitcoinApi,
|
private val api: Reader<JsonRpcRequest, JsonRpcResponse>,
|
||||||
private val extractBlock: ExtractBlock,
|
private val extractBlock: ExtractBlock,
|
||||||
private val interval: Duration = Duration.ofSeconds(15)
|
private val interval: Duration = Duration.ofSeconds(15)
|
||||||
) : Head, AbstractHead(), Lifecycle {
|
) : Head, AbstractHead(), Lifecycle {
|
||||||
@@ -53,12 +56,14 @@ class BitcoinRpcHead(
|
|||||||
val base = Flux.interval(interval)
|
val base = Flux.interval(interval)
|
||||||
.publishOn(scheduler)
|
.publishOn(scheduler)
|
||||||
.flatMap {
|
.flatMap {
|
||||||
api.executeAndResult(0, "getbestblockhash", emptyList(), String::class.java)
|
api.read(JsonRpcRequest("getbestblockhash", emptyList()))
|
||||||
|
.flatMap(JsonRpcResponse::requireStringResult)
|
||||||
.timeout(Defaults.timeout, Mono.error(Exception("Best block hash is not received")))
|
.timeout(Defaults.timeout, Mono.error(Exception("Best block hash is not received")))
|
||||||
}
|
}
|
||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
.flatMap { hash ->
|
.flatMap { hash ->
|
||||||
api.execute(0, "getblock", listOf(hash))
|
api.read(JsonRpcRequest("getblock", listOf(hash)))
|
||||||
|
.flatMap(JsonRpcResponse::requireResult)
|
||||||
.map(extractBlock::extract)
|
.map(extractBlock::extract)
|
||||||
.timeout(Defaults.timeout, Mono.error(Exception("Block data is not received")))
|
.timeout(Defaults.timeout, Mono.error(Exception("Block data is not received")))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,9 +17,12 @@ package io.emeraldpay.dshackle.upstream.bitcoin
|
|||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
import io.emeraldpay.dshackle.config.UpstreamsConfig
|
import io.emeraldpay.dshackle.config.UpstreamsConfig
|
||||||
|
import io.emeraldpay.dshackle.reader.Reader
|
||||||
import io.emeraldpay.dshackle.startup.QuorumForLabels
|
import io.emeraldpay.dshackle.startup.QuorumForLabels
|
||||||
import io.emeraldpay.dshackle.upstream.*
|
import io.emeraldpay.dshackle.upstream.*
|
||||||
import io.emeraldpay.dshackle.upstream.calls.CallMethods
|
import io.emeraldpay.dshackle.upstream.calls.CallMethods
|
||||||
|
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
|
||||||
|
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
|
||||||
import io.emeraldpay.grpc.Chain
|
import io.emeraldpay.grpc.Chain
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import org.springframework.context.Lifecycle
|
import org.springframework.context.Lifecycle
|
||||||
@@ -29,12 +32,12 @@ import reactor.core.publisher.Mono
|
|||||||
open class BitcoinUpstream(
|
open class BitcoinUpstream(
|
||||||
id: String,
|
id: String,
|
||||||
val chain: Chain,
|
val chain: Chain,
|
||||||
private val api: DirectBitcoinApi,
|
private val directApi: Reader<JsonRpcRequest, JsonRpcResponse>,
|
||||||
options: UpstreamsConfig.Options,
|
options: UpstreamsConfig.Options,
|
||||||
val node: QuorumForLabels.QuorumItem,
|
val node: QuorumForLabels.QuorumItem,
|
||||||
private val objectMapper: ObjectMapper,
|
private val objectMapper: ObjectMapper,
|
||||||
callMethods: CallMethods
|
callMethods: CallMethods
|
||||||
) : DefaultUpstream<DirectBitcoinApi>(id, options, callMethods), Lifecycle {
|
) : DefaultUpstream(id, options, callMethods), Lifecycle {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val log = LoggerFactory.getLogger(BitcoinUpstream::class.java)
|
private val log = LoggerFactory.getLogger(BitcoinUpstream::class.java)
|
||||||
@@ -42,43 +45,31 @@ open class BitcoinUpstream(
|
|||||||
|
|
||||||
private val head: Head = createHead()
|
private val head: Head = createHead()
|
||||||
private var validatorSubscription: Disposable? = null
|
private var validatorSubscription: Disposable? = null
|
||||||
private val data = BitcoinReader(api, head)
|
|
||||||
|
|
||||||
private fun createHead(): Head {
|
private fun createHead(): Head {
|
||||||
return BitcoinRpcHead(
|
return BitcoinRpcHead(
|
||||||
api,
|
directApi,
|
||||||
ExtractBlock(objectMapper)
|
ExtractBlock(objectMapper)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
open fun getData(): BitcoinReader {
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getHead(): Head {
|
override fun getHead(): Head {
|
||||||
return head
|
return head
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getApi(matcher: Selector.Matcher): Mono<out DirectBitcoinApi> {
|
override fun getApi(): Reader<JsonRpcRequest, JsonRpcResponse> {
|
||||||
return Mono.just(api)
|
return directApi
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getLabels(): Collection<UpstreamsConfig.Labels> {
|
override fun getLabels(): Collection<UpstreamsConfig.Labels> {
|
||||||
return listOf(UpstreamsConfig.Labels())
|
return listOf(UpstreamsConfig.Labels())
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun <T : Upstream<TA>, TA : UpstreamApi> cast(selfType: Class<T>, apiType: Class<TA>): T {
|
override fun <T : Upstream> cast(selfType: Class<T>): T {
|
||||||
if (!selfType.isAssignableFrom(this.javaClass)) {
|
if (!selfType.isAssignableFrom(this.javaClass)) {
|
||||||
throw ClassCastException("Cannot cast ${this.javaClass} to $selfType")
|
throw ClassCastException("Cannot cast ${this.javaClass} to $selfType")
|
||||||
}
|
}
|
||||||
return castApi(apiType) as T
|
return this as T
|
||||||
}
|
|
||||||
|
|
||||||
override fun <A : UpstreamApi> castApi(apiType: Class<A>): Upstream<A> {
|
|
||||||
if (!apiType.isAssignableFrom(DirectBitcoinApi::class.java)) {
|
|
||||||
throw ClassCastException("Cannot cast ${DirectBitcoinApi::class.java} to $apiType")
|
|
||||||
}
|
|
||||||
return this as Upstream<A>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun isRunning(): Boolean {
|
override fun isRunning(): Boolean {
|
||||||
@@ -86,7 +77,7 @@ open class BitcoinUpstream(
|
|||||||
if (head is Lifecycle) {
|
if (head is Lifecycle) {
|
||||||
runningAny = runningAny || head.isRunning
|
runningAny = runningAny || head.isRunning
|
||||||
}
|
}
|
||||||
runningAny = runningAny || data.isRunning
|
runningAny = runningAny
|
||||||
return runningAny
|
return runningAny
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,7 +88,6 @@ open class BitcoinUpstream(
|
|||||||
head.start()
|
head.start()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
data.start()
|
|
||||||
|
|
||||||
validatorSubscription?.dispose()
|
validatorSubscription?.dispose()
|
||||||
|
|
||||||
@@ -105,7 +95,7 @@ open class BitcoinUpstream(
|
|||||||
this.setLag(0)
|
this.setLag(0)
|
||||||
this.setStatus(UpstreamAvailability.OK)
|
this.setStatus(UpstreamAvailability.OK)
|
||||||
} else {
|
} else {
|
||||||
val validator = BitcoinUpstreamValidator(api, getOptions())
|
val validator = BitcoinUpstreamValidator(directApi, getOptions())
|
||||||
validatorSubscription = validator.start()
|
validatorSubscription = validator.start()
|
||||||
.subscribe(this::setStatus)
|
.subscribe(this::setStatus)
|
||||||
}
|
}
|
||||||
@@ -115,9 +105,7 @@ open class BitcoinUpstream(
|
|||||||
if (head is Lifecycle) {
|
if (head is Lifecycle) {
|
||||||
head.stop()
|
head.stop()
|
||||||
}
|
}
|
||||||
data.stop()
|
|
||||||
validatorSubscription?.dispose()
|
validatorSubscription?.dispose()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -16,7 +16,10 @@
|
|||||||
package io.emeraldpay.dshackle.upstream.bitcoin
|
package io.emeraldpay.dshackle.upstream.bitcoin
|
||||||
|
|
||||||
import io.emeraldpay.dshackle.config.UpstreamsConfig
|
import io.emeraldpay.dshackle.config.UpstreamsConfig
|
||||||
|
import io.emeraldpay.dshackle.reader.Reader
|
||||||
import io.emeraldpay.dshackle.upstream.UpstreamAvailability
|
import io.emeraldpay.dshackle.upstream.UpstreamAvailability
|
||||||
|
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
|
||||||
|
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import org.springframework.scheduling.concurrent.CustomizableThreadFactory
|
import org.springframework.scheduling.concurrent.CustomizableThreadFactory
|
||||||
import reactor.core.publisher.Flux
|
import reactor.core.publisher.Flux
|
||||||
@@ -26,7 +29,7 @@ import java.time.Duration
|
|||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
|
|
||||||
class BitcoinUpstreamValidator(
|
class BitcoinUpstreamValidator(
|
||||||
private val api: DirectBitcoinApi,
|
private val api: Reader<JsonRpcRequest, JsonRpcResponse>,
|
||||||
private val options: UpstreamsConfig.Options
|
private val options: UpstreamsConfig.Options
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@@ -36,7 +39,9 @@ class BitcoinUpstreamValidator(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun validate(): Mono<UpstreamAvailability> {
|
fun validate(): Mono<UpstreamAvailability> {
|
||||||
return api.executeAndResult(0, "getconnectioncount", emptyList(), Int::class.java)
|
return api.read(JsonRpcRequest("getconnectioncount", emptyList()))
|
||||||
|
.flatMap(JsonRpcResponse::requireResult)
|
||||||
|
.map { Integer.parseInt(String(it)) }
|
||||||
.map { count ->
|
.map { count ->
|
||||||
val minPeers = options.minPeers ?: 1
|
val minPeers = options.minPeers ?: 1
|
||||||
if (count < minPeers) {
|
if (count < minPeers) {
|
||||||
|
|||||||
@@ -15,7 +15,11 @@
|
|||||||
*/
|
*/
|
||||||
package io.emeraldpay.dshackle.upstream.bitcoin
|
package io.emeraldpay.dshackle.upstream.bitcoin
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
import io.emeraldpay.dshackle.upstream.Head
|
import io.emeraldpay.dshackle.upstream.Head
|
||||||
|
import io.emeraldpay.dshackle.upstream.Selector
|
||||||
|
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
|
||||||
|
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import org.springframework.context.Lifecycle
|
import org.springframework.context.Lifecycle
|
||||||
import reactor.core.Disposable
|
import reactor.core.Disposable
|
||||||
@@ -26,8 +30,9 @@ import java.util.concurrent.atomic.AtomicReference
|
|||||||
import java.util.concurrent.locks.ReentrantLock
|
import java.util.concurrent.locks.ReentrantLock
|
||||||
|
|
||||||
open class CachingMempoolData(
|
open class CachingMempoolData(
|
||||||
private val api: DirectBitcoinApi,
|
private val upstreams: BitcoinMultistream,
|
||||||
private val head: Head
|
private val head: Head,
|
||||||
|
private val objectMapper: ObjectMapper
|
||||||
) : Lifecycle {
|
) : Lifecycle {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -56,7 +61,11 @@ open class CachingMempoolData(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun fetchFromUpstream(): Mono<List<String>> {
|
fun fetchFromUpstream(): Mono<List<String>> {
|
||||||
return api.executeAndResult(0, "getrawmempool", emptyList(), List::class.java) as Mono<List<String>>
|
return upstreams.getDirectApi(Selector.empty).flatMap { api ->
|
||||||
|
api.read(JsonRpcRequest("getrawmempool", emptyList()))
|
||||||
|
.flatMap(JsonRpcResponse::requireResult)
|
||||||
|
.map { objectMapper.readValue(it, List::class.java) as List<String> }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class Container(val since: Instant, val value: List<String>) {
|
class Container(val since: Instant, val value: List<String>) {
|
||||||
|
|||||||
@@ -1,122 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2020 EmeraldPay, Inc
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
package io.emeraldpay.dshackle.upstream.bitcoin
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.JavaType
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
|
||||||
import io.emeraldpay.dshackle.upstream.UpstreamApi
|
|
||||||
import io.emeraldpay.dshackle.upstream.calls.CallMethods
|
|
||||||
import io.grpc.Status
|
|
||||||
import io.grpc.StatusRuntimeException
|
|
||||||
import io.infinitape.etherjar.rpc.RpcException
|
|
||||||
import io.infinitape.etherjar.rpc.RpcResponseError
|
|
||||||
import io.infinitape.etherjar.rpc.json.FullResponseJson
|
|
||||||
import io.infinitape.etherjar.rpc.json.RequestJson
|
|
||||||
import io.infinitape.etherjar.rpc.json.ResponseJson
|
|
||||||
import org.slf4j.LoggerFactory
|
|
||||||
import reactor.core.publisher.Mono
|
|
||||||
|
|
||||||
open class DirectBitcoinApi(
|
|
||||||
val bitcoinRpcClient: BitcoinRpcClient,
|
|
||||||
val objectMapper: ObjectMapper,
|
|
||||||
val targets: CallMethods
|
|
||||||
) : UpstreamApi {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private val log = LoggerFactory.getLogger(DirectBitcoinApi::class.java)
|
|
||||||
}
|
|
||||||
|
|
||||||
open override fun execute(id: Int, method: String, params: List<Any>): Mono<ByteArray> {
|
|
||||||
//TODO it's almost the same code as for DirectEthereumApi; refactor
|
|
||||||
val result: Mono<out Any> = when {
|
|
||||||
targets.isHardcoded(method) -> Mono.just(method).map { targets.executeHardcoded(it) }
|
|
||||||
targets.isAllowed(method) -> executeAndResult(id, method, params, Object::class.java)
|
|
||||||
else -> Mono.error(RpcException(-32601, "Method not allowed or not found"))
|
|
||||||
}
|
|
||||||
return processResult(id, method, result)
|
|
||||||
}
|
|
||||||
|
|
||||||
public fun processResult(id: Int, method: String, result: Mono<out Any>): Mono<ByteArray> {
|
|
||||||
//TODO it's the same code as for DirectEthereumApi; refactor
|
|
||||||
return result
|
|
||||||
.doOnError { t ->
|
|
||||||
log.warn("Upstream error: [${t.message}] for $method")
|
|
||||||
}
|
|
||||||
.map {
|
|
||||||
val resp = ResponseJson<Any, Int>()
|
|
||||||
resp.id = id
|
|
||||||
resp.result = it
|
|
||||||
resp
|
|
||||||
}
|
|
||||||
.switchIfEmpty(
|
|
||||||
Mono.fromCallable {
|
|
||||||
val resp = ResponseJson<Any, Int>()
|
|
||||||
resp.id = id
|
|
||||||
resp.result = null
|
|
||||||
resp
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.map {
|
|
||||||
objectMapper.writer().writeValueAsBytes(it)
|
|
||||||
}
|
|
||||||
.onErrorResume(StatusRuntimeException::class.java) { t ->
|
|
||||||
if (t.status.code == Status.Code.CANCELLED) {
|
|
||||||
Mono.empty<ByteArray>()
|
|
||||||
} else {
|
|
||||||
Mono.error(RpcException(RpcResponseError.CODE_UPSTREAM_CONNECTION_ERROR, "gRPC error ${t.status}"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.onErrorMap { t ->
|
|
||||||
if (RpcException::class.java.isAssignableFrom(t.javaClass)) {
|
|
||||||
t
|
|
||||||
} else {
|
|
||||||
log.warn("Convert to RPC error. Exception ${t.javaClass}:${t.message}", t)
|
|
||||||
RpcException(-32020, "Error reading from upstream", null, t)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.onErrorResume(RpcException::class.java) { t ->
|
|
||||||
val resp = ResponseJson<Any, Int>()
|
|
||||||
resp.id = id
|
|
||||||
resp.error = t.error
|
|
||||||
Mono.just(objectMapper.writer().writeValueAsBytes(resp))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
open fun <T> executeAndResult(id: Int, method: String, params: List<Any>, resultType: Class<T>): Mono<T> {
|
|
||||||
val rpc = RequestJson<Int>(method, params, id)
|
|
||||||
return Mono.just(rpc)
|
|
||||||
.map(objectMapper::writeValueAsBytes)
|
|
||||||
.flatMap(bitcoinRpcClient::execute)
|
|
||||||
.flatMap { json ->
|
|
||||||
val type: JavaType = objectMapper.typeFactory.constructParametricType(FullResponseJson::class.java, resultType, Int::class.java)
|
|
||||||
val resp = objectMapper.readerFor(type).readValue<FullResponseJson<T, Int>>(json)
|
|
||||||
if (resp.hasError()) {
|
|
||||||
Mono.error(resp.error.asException())
|
|
||||||
} else {
|
|
||||||
Mono.just(resp.result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
open fun getBlock(hash: String): Mono<Map<String, Any>> {
|
|
||||||
return executeAndResult(0, "getblock", listOf(hash), Map::class.java) as Mono<Map<String, Any>>
|
|
||||||
}
|
|
||||||
|
|
||||||
open fun getTx(txid: String): Mono<Map<String, Any>> {
|
|
||||||
return executeAndResult(0, "getrawtransaction", listOf(txid, true), Map::class.java) as Mono<Map<String, Any>>
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -63,6 +63,7 @@ class ExtractBlock(
|
|||||||
getTime(data) ?: throw IllegalArgumentException("Block JSON has no time"),
|
getTime(data) ?: throw IllegalArgumentException("Block JSON has no time"),
|
||||||
false,
|
false,
|
||||||
json,
|
json,
|
||||||
|
data,
|
||||||
transactions
|
transactions
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ class AggregatedCallMethods(
|
|||||||
*/
|
*/
|
||||||
override fun getQuorumFor(method: String): CallQuorum {
|
override fun getQuorumFor(method: String): CallQuorum {
|
||||||
return delegates.find {
|
return delegates.find {
|
||||||
it.isAllowed(method)
|
it.isAllowed(method) || it.isHardcoded(method)
|
||||||
}?.getQuorumFor(method) ?: throw IllegalStateException("No quorum for $method")
|
}?.getQuorumFor(method) ?: throw IllegalStateException("No quorum for $method")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,15 +62,15 @@ class AggregatedCallMethods(
|
|||||||
* @return true if there is at least one delegate that allows the method and it's hardcoded on that delegate
|
* @return true if there is at least one delegate that allows the method and it's hardcoded on that delegate
|
||||||
*/
|
*/
|
||||||
override fun isHardcoded(method: String): Boolean {
|
override fun isHardcoded(method: String): Boolean {
|
||||||
return delegates.any { it.isAllowed(method) && it.isHardcoded(method) }
|
return delegates.any { it.isHardcoded(method) }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Executed the method on the first delegate that supports it as a hardcoded method
|
* Executed the method on the first delegate that supports it as a hardcoded method
|
||||||
*/
|
*/
|
||||||
override fun executeHardcoded(method: String): Any {
|
override fun executeHardcoded(method: String): ByteArray {
|
||||||
return delegates.find {
|
return delegates.find {
|
||||||
it.isAllowed(method) && it.isHardcoded(method)
|
it.isHardcoded(method)
|
||||||
}?.executeHardcoded(method) ?: throw IllegalStateException("No hardcoded for $method")
|
}?.executeHardcoded(method) ?: throw IllegalStateException("No hardcoded for $method")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -46,5 +46,5 @@ interface CallMethods {
|
|||||||
/**
|
/**
|
||||||
* Read [supposed to be predefined] method from this config
|
* Read [supposed to be predefined] method from this config
|
||||||
*/
|
*/
|
||||||
fun executeHardcoded(method: String): Any
|
fun executeHardcoded(method: String): ByteArray
|
||||||
}
|
}
|
||||||
@@ -13,12 +13,10 @@
|
|||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package io.emeraldpay.dshackle.upstream.bitcoin
|
package io.emeraldpay.dshackle.upstream.calls
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
import io.emeraldpay.dshackle.quorum.*
|
import io.emeraldpay.dshackle.quorum.*
|
||||||
import io.emeraldpay.dshackle.upstream.calls.CallMethods
|
|
||||||
import io.infinitape.etherjar.rpc.JacksonRpcConverter
|
|
||||||
import io.infinitape.etherjar.rpc.RpcException
|
import io.infinitape.etherjar.rpc.RpcException
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
@@ -26,9 +24,6 @@ class DefaultBitcoinMethods(
|
|||||||
private val objectMapper: ObjectMapper
|
private val objectMapper: ObjectMapper
|
||||||
) : CallMethods {
|
) : CallMethods {
|
||||||
|
|
||||||
//TODO maybe Ethereum RPC parser should not be really used for Bitcoin
|
|
||||||
private val jacksonRpcConverter = JacksonRpcConverter(objectMapper)
|
|
||||||
|
|
||||||
private val anyResponseMethods = listOf(
|
private val anyResponseMethods = listOf(
|
||||||
"getblock",
|
"getblock",
|
||||||
"gettransaction", "getrawtransaction", "gettxout",
|
"gettransaction", "getrawtransaction", "gettxout",
|
||||||
@@ -55,7 +50,7 @@ class DefaultBitcoinMethods(
|
|||||||
Collections.binarySearch(hardcodedMethods, method) >= 0 -> AlwaysQuorum()
|
Collections.binarySearch(hardcodedMethods, method) >= 0 -> AlwaysQuorum()
|
||||||
Collections.binarySearch(anyResponseMethods, method) >= 0 -> NotLaggingQuorum(2)
|
Collections.binarySearch(anyResponseMethods, method) >= 0 -> NotLaggingQuorum(2)
|
||||||
Collections.binarySearch(headVerifiedMethods, method) >= 0 -> NotLaggingQuorum(0)
|
Collections.binarySearch(headVerifiedMethods, method) >= 0 -> NotLaggingQuorum(0)
|
||||||
Collections.binarySearch(broadcastMethods, method) >= 0 -> BroadcastQuorum(jacksonRpcConverter)
|
Collections.binarySearch(broadcastMethods, method) >= 0 -> BroadcastQuorum(objectMapper)
|
||||||
else -> AlwaysQuorum()
|
else -> AlwaysQuorum()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -72,13 +67,10 @@ class DefaultBitcoinMethods(
|
|||||||
return Collections.binarySearch(hardcodedMethods, method) >= 0;
|
return Collections.binarySearch(hardcodedMethods, method) >= 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun executeHardcoded(method: String): Any {
|
override fun executeHardcoded(method: String): ByteArray {
|
||||||
return when (method) {
|
return when (method) {
|
||||||
"getconnectioncount" -> 42
|
"getconnectioncount" -> "42".toByteArray()
|
||||||
"getnetworkinfo" -> mapOf(
|
"getnetworkinfo" -> "{\"version\": 700000, \"subversion\": \"/EmeraldDshackle:v0.7/\"}".toByteArray()
|
||||||
"version" to 700000,
|
|
||||||
"subversion" to "/EmeraldDshackle:v0.7/"
|
|
||||||
)
|
|
||||||
else -> throw RpcException(-32601, "Method not found")
|
else -> throw RpcException(-32601, "Method not found")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -32,8 +32,6 @@ class DefaultEthereumMethods(
|
|||||||
private val chain: Chain
|
private val chain: Chain
|
||||||
) : CallMethods {
|
) : CallMethods {
|
||||||
|
|
||||||
private val jacksonRpcConverter = JacksonRpcConverter(objectMapper)
|
|
||||||
|
|
||||||
private val anyResponseMethods = listOf(
|
private val anyResponseMethods = listOf(
|
||||||
"eth_gasPrice",
|
"eth_gasPrice",
|
||||||
"eth_call",
|
"eth_call",
|
||||||
@@ -90,9 +88,9 @@ class DefaultEthereumMethods(
|
|||||||
headVerifiedMethods.contains(method) -> NotLaggingQuorum(1)
|
headVerifiedMethods.contains(method) -> NotLaggingQuorum(1)
|
||||||
specialMethods.contains(method) -> {
|
specialMethods.contains(method) -> {
|
||||||
when (method) {
|
when (method) {
|
||||||
"eth_getTransactionCount" -> NonceQuorum(jacksonRpcConverter)
|
"eth_getTransactionCount" -> NonceQuorum(objectMapper)
|
||||||
"eth_getBalance" -> NotLaggingQuorum(1)
|
"eth_getBalance" -> NotLaggingQuorum(1)
|
||||||
"eth_sendRawTransaction" -> BroadcastQuorum(jacksonRpcConverter)
|
"eth_sendRawTransaction" -> BroadcastQuorum(objectMapper)
|
||||||
else -> AlwaysQuorum()
|
else -> AlwaysQuorum()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -108,50 +106,55 @@ class DefaultEthereumMethods(
|
|||||||
return hardcodedMethods.contains(method)
|
return hardcodedMethods.contains(method)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun executeHardcoded(method: String): Any {
|
override fun executeHardcoded(method: String): ByteArray {
|
||||||
if ("net_version" == method) {
|
val json = when (method) {
|
||||||
if (Chain.ETHEREUM == chain) {
|
"net_version" -> {
|
||||||
return "1"
|
when {
|
||||||
|
Chain.ETHEREUM == chain -> {
|
||||||
|
"1"
|
||||||
|
}
|
||||||
|
Chain.ETHEREUM_CLASSIC == chain -> {
|
||||||
|
"1"
|
||||||
|
}
|
||||||
|
Chain.TESTNET_MORDEN == chain -> {
|
||||||
|
"2"
|
||||||
|
}
|
||||||
|
Chain.TESTNET_KOVAN == chain -> {
|
||||||
|
"42"
|
||||||
|
}
|
||||||
|
else -> throw RpcException(-32602, "Invalid chain")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (Chain.ETHEREUM_CLASSIC == chain) {
|
"net_peerCount" -> {
|
||||||
return "1"
|
"\"0x2a\""
|
||||||
}
|
}
|
||||||
if (Chain.TESTNET_MORDEN == chain) {
|
"net_listening" -> {
|
||||||
return "2"
|
"true"
|
||||||
}
|
}
|
||||||
if (Chain.TESTNET_KOVAN == chain) {
|
"web3_clientVersion" -> {
|
||||||
return "42"
|
"\"EmeraldDshackle/v0.2\""
|
||||||
}
|
}
|
||||||
throw RpcException(-32602, "Invalid chain")
|
"eth_protocolVersion" -> {
|
||||||
|
"\"0x3f\""
|
||||||
|
}
|
||||||
|
"eth_syncing" -> {
|
||||||
|
"false"
|
||||||
|
}
|
||||||
|
"eth_coinbase" -> {
|
||||||
|
"\"0x0000000000000000000000000000000000000000\""
|
||||||
|
}
|
||||||
|
"eth_mining" -> {
|
||||||
|
"false"
|
||||||
|
}
|
||||||
|
"eth_hashrate" -> {
|
||||||
|
"\"0x0\""
|
||||||
|
}
|
||||||
|
"eth_accounts" -> {
|
||||||
|
"[]"
|
||||||
|
}
|
||||||
|
else -> throw RpcException(-32601, "Method not found")
|
||||||
}
|
}
|
||||||
if ("net_peerCount" == method) {
|
return json.toByteArray()
|
||||||
return "0x2a"
|
|
||||||
}
|
|
||||||
if ("net_listening" == method) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if ("web3_clientVersion" == method) {
|
|
||||||
return "EmeraldDshackle/v0.2"
|
|
||||||
}
|
|
||||||
if ("eth_protocolVersion" == method) {
|
|
||||||
return "0x3f"
|
|
||||||
}
|
|
||||||
if ("eth_syncing" == method) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if ("eth_coinbase" == method) {
|
|
||||||
return "0x0000000000000000000000000000000000000000"
|
|
||||||
}
|
|
||||||
if ("eth_mining" == method) {
|
|
||||||
return "false"
|
|
||||||
}
|
|
||||||
if ("eth_hashrate" == method) {
|
|
||||||
return "0x0"
|
|
||||||
}
|
|
||||||
if ("eth_accounts" == method) {
|
|
||||||
return Collections.emptyList<String>()
|
|
||||||
}
|
|
||||||
throw RpcException(-32601, "Method not found")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getSupportedMethods(): Set<String> {
|
override fun getSupportedMethods(): Set<String> {
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ open class DirectCallMethods(private val methods: Set<String>) : CallMethods {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun executeHardcoded(method: String): Any {
|
override fun executeHardcoded(method: String): ByteArray {
|
||||||
return "unsupported"
|
return "unsupported".toByteArray()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -55,7 +55,7 @@ class ManagedCallMethods(
|
|||||||
return delegate.isHardcoded(method)
|
return delegate.isHardcoded(method)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun executeHardcoded(method: String): Any {
|
override fun executeHardcoded(method: String): ByteArray {
|
||||||
return delegate.executeHardcoded(method)
|
return delegate.executeHardcoded(method)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,177 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2020 EmeraldPay, Inc
|
|
||||||
* Copyright (c) 2019 ETCDEV GmbH
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
package io.emeraldpay.dshackle.upstream.ethereum
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
|
||||||
import io.emeraldpay.dshackle.Defaults
|
|
||||||
import io.emeraldpay.dshackle.cache.Caches
|
|
||||||
import io.emeraldpay.dshackle.upstream.calls.CallMethods
|
|
||||||
import io.grpc.Status
|
|
||||||
import io.grpc.StatusRuntimeException
|
|
||||||
import io.infinitape.etherjar.domain.BlockHash
|
|
||||||
import io.infinitape.etherjar.domain.TransactionId
|
|
||||||
import io.infinitape.etherjar.hex.HexQuantity
|
|
||||||
import io.infinitape.etherjar.rpc.*
|
|
||||||
import io.infinitape.etherjar.rpc.json.ResponseJson
|
|
||||||
import org.slf4j.LoggerFactory
|
|
||||||
import reactor.core.publisher.Mono
|
|
||||||
import java.math.BigInteger
|
|
||||||
|
|
||||||
open class DirectEthereumApi(
|
|
||||||
val rpcClient: ReactorRpcClient,
|
|
||||||
var caches: Caches?,
|
|
||||||
private val objectMapper: ObjectMapper,
|
|
||||||
val targets: CallMethods
|
|
||||||
): EthereumApi(objectMapper) {
|
|
||||||
|
|
||||||
var timeout = Defaults.timeout
|
|
||||||
private val log = LoggerFactory.getLogger(EthereumApi::class.java)
|
|
||||||
|
|
||||||
override fun execute(id: Int, method: String, params: List<Any>): Mono<ByteArray> {
|
|
||||||
val result: Mono<out Any> = when {
|
|
||||||
targets.isHardcoded(method) -> Mono.just(method).map { targets.executeHardcoded(it) }
|
|
||||||
targets.isAllowed(method) -> callUpstream(method, params)
|
|
||||||
else -> Mono.error(RpcException(-32601, "Method not allowed or not found"))
|
|
||||||
}
|
|
||||||
return processResult(id, method, result)
|
|
||||||
}
|
|
||||||
|
|
||||||
public fun processResult(id: Int, method: String, result: Mono<out Any>): Mono<ByteArray> {
|
|
||||||
return result
|
|
||||||
.doOnError { t ->
|
|
||||||
log.warn("Upstream error: [${t.message}] for $method")
|
|
||||||
}
|
|
||||||
.map {
|
|
||||||
val resp = ResponseJson<Any, Int>()
|
|
||||||
resp.id = id
|
|
||||||
resp.result = it
|
|
||||||
resp
|
|
||||||
}
|
|
||||||
.switchIfEmpty(
|
|
||||||
Mono.fromCallable {
|
|
||||||
val resp = ResponseJson<Any, Int>()
|
|
||||||
resp.id = id
|
|
||||||
resp.result = null
|
|
||||||
resp
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.map {
|
|
||||||
objectMapper.writer().writeValueAsBytes(it)
|
|
||||||
}
|
|
||||||
.onErrorResume(StatusRuntimeException::class.java) { t ->
|
|
||||||
if (t.status.code == Status.Code.CANCELLED) {
|
|
||||||
Mono.empty<ByteArray>()
|
|
||||||
} else {
|
|
||||||
Mono.error(RpcException(RpcResponseError.CODE_UPSTREAM_CONNECTION_ERROR, "gRPC error ${t.status}"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.onErrorMap { t ->
|
|
||||||
if (RpcException::class.java.isAssignableFrom(t.javaClass)) {
|
|
||||||
t
|
|
||||||
} else {
|
|
||||||
log.warn("Convert to RPC error. Exception ${t.javaClass}:${t.message}", t)
|
|
||||||
RpcException(-32020, "Error reading from upstream", null, t)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.onErrorResume(RpcException::class.java) { t ->
|
|
||||||
val resp = ResponseJson<Any, Int>()
|
|
||||||
resp.id = id
|
|
||||||
resp.error = t.error
|
|
||||||
Mono.just(objectMapper.writer().writeValueAsBytes(resp))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Actual request to the remote endpoint
|
|
||||||
*/
|
|
||||||
private fun callUpstream(method: String, params: List<Any>): Mono<out Any> {
|
|
||||||
return rpcClient.execute(callMapping(method, params))
|
|
||||||
.timeout(timeout, Mono.error(RpcException(-32603, "Upstream timeout")))
|
|
||||||
.doOnNext { value ->
|
|
||||||
try {
|
|
||||||
caches?.cacheRequested(value)
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
//ignore all caching errors, client shouldn't have problems because of them
|
|
||||||
log.warn("Uncaught caching exception", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Prepare RpcCall with data types specific for that particular requests. In general it may return a call that just
|
|
||||||
* parses JSON into Map. But the purpose of further processing and caching for some of the requests we want
|
|
||||||
* to have actual data types.
|
|
||||||
*/
|
|
||||||
fun callMapping(method: String, params: List<Any>): RpcCall<out Any, out Any> {
|
|
||||||
return when {
|
|
||||||
method == "eth_getTransactionByHash" -> {
|
|
||||||
if (params.size != 1) {
|
|
||||||
throw RpcException(RpcResponseError.CODE_INVALID_METHOD_PARAMS, "Must provide 1 parameter")
|
|
||||||
}
|
|
||||||
val hash: TransactionId
|
|
||||||
try {
|
|
||||||
hash = TransactionId.from(params[0].toString())
|
|
||||||
} catch (e: IllegalArgumentException) {
|
|
||||||
throw RpcException(RpcResponseError.CODE_INVALID_METHOD_PARAMS, "[0] must be transaction id")
|
|
||||||
}
|
|
||||||
Commands.eth().getTransaction(hash)
|
|
||||||
}
|
|
||||||
method == "eth_getBlockByHash" -> {
|
|
||||||
if (params.size != 2) {
|
|
||||||
throw RpcException(RpcResponseError.CODE_INVALID_METHOD_PARAMS, "Must provide 2 parameters")
|
|
||||||
}
|
|
||||||
val hash: BlockHash
|
|
||||||
try {
|
|
||||||
hash = BlockHash.from(params[0].toString())
|
|
||||||
} catch (e: IllegalArgumentException) {
|
|
||||||
throw RpcException(RpcResponseError.CODE_INVALID_METHOD_PARAMS, "[0] must be block hash")
|
|
||||||
}
|
|
||||||
val withTx = params[1].toString().toBoolean()
|
|
||||||
if (withTx) {
|
|
||||||
Commands.eth().getBlockWithTransactions(hash)
|
|
||||||
} else {
|
|
||||||
Commands.eth().getBlock(hash)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
method == "eth_getBlockByNumber" -> {
|
|
||||||
if (params.size != 2) {
|
|
||||||
throw RpcException(RpcResponseError.CODE_INVALID_METHOD_PARAMS, "Must provide 2 parameters")
|
|
||||||
}
|
|
||||||
val number: Long
|
|
||||||
try {
|
|
||||||
val quantity = HexQuantity.from(params[0].toString()) ?: throw IllegalArgumentException()
|
|
||||||
number = quantity.value.let {
|
|
||||||
if (it < BigInteger.valueOf(Long.MAX_VALUE) && it >= BigInteger.ZERO) {
|
|
||||||
it.toLong()
|
|
||||||
} else {
|
|
||||||
throw IllegalArgumentException()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: IllegalArgumentException) {
|
|
||||||
throw RpcException(RpcResponseError.CODE_INVALID_METHOD_PARAMS, "[0] must be block number")
|
|
||||||
}
|
|
||||||
val withTx = params[1].toString().toBoolean()
|
|
||||||
if (withTx) {
|
|
||||||
Commands.eth().getBlockWithTransactions(number)
|
|
||||||
} else {
|
|
||||||
Commands.eth().getBlock(number)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> RpcCall.create(method, Any::class.java, params)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2020 EmeraldPay, Inc
|
|
||||||
* Copyright (c) 2019 ETCDEV GmbH
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
package io.emeraldpay.dshackle.upstream.ethereum
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
|
||||||
import io.emeraldpay.dshackle.reader.Reader
|
|
||||||
import io.emeraldpay.dshackle.upstream.Upstream
|
|
||||||
import io.emeraldpay.dshackle.upstream.UpstreamApi
|
|
||||||
import io.infinitape.etherjar.rpc.*
|
|
||||||
import io.infinitape.etherjar.rpc.json.BlockJson
|
|
||||||
import io.infinitape.etherjar.rpc.json.TransactionRefJson
|
|
||||||
import org.slf4j.LoggerFactory
|
|
||||||
import reactor.core.publisher.Mono
|
|
||||||
import java.io.InputStream
|
|
||||||
|
|
||||||
abstract class EthereumApi(
|
|
||||||
objectMapper: ObjectMapper
|
|
||||||
) : UpstreamApi {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private val log = LoggerFactory.getLogger(EthereumApi::class.java)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val jacksonRpcConverter = JacksonRpcConverter(objectMapper)
|
|
||||||
var upstream: Upstream<EthereumApi>? = null
|
|
||||||
|
|
||||||
fun <JS, RS> reader(): Reader<RpcCall<JS, RS>, RS> {
|
|
||||||
return object : Reader<RpcCall<JS, RS>, RS> {
|
|
||||||
override fun read(key: RpcCall<JS, RS>): Mono<RS> {
|
|
||||||
return this@EthereumApi.executeAndConvert(key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun <JS, RS> execute(rpcCall: RpcCall<JS, RS>): Mono<ByteArray> {
|
|
||||||
return execute(0, rpcCall.method, rpcCall.params as List<Any>)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun <JS, RS> executeAndConvert(rpcCall: RpcCall<JS, RS>): Mono<RS> {
|
|
||||||
val convertToJS = java.util.function.Function<ByteArray, Mono<JS>> { resp ->
|
|
||||||
val inputStream: InputStream = resp.inputStream()
|
|
||||||
val jsonValue: JS? = jacksonRpcConverter.fromJson(inputStream, rpcCall.jsonType, Int::class.java)
|
|
||||||
if (jsonValue == null) Mono.empty<JS>()
|
|
||||||
else Mono.just(jsonValue)
|
|
||||||
}
|
|
||||||
return execute(rpcCall)
|
|
||||||
.flatMap(convertToJS)
|
|
||||||
.map(rpcCall.converter::apply)
|
|
||||||
.doOnError { err -> log.debug("Failed to read from upstream", err) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package io.emeraldpay.dshackle.cache
|
package io.emeraldpay.dshackle.upstream.ethereum
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
import io.emeraldpay.dshackle.data.BlockContainer
|
import io.emeraldpay.dshackle.data.BlockContainer
|
||||||
@@ -30,20 +30,20 @@ import reactor.core.publisher.Flux
|
|||||||
import reactor.core.publisher.Mono
|
import reactor.core.publisher.Mono
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reads blocks with full transactions details. Based on data contained in cashes for blocks
|
* Reads blocks with full transactions details. Based on data contained in readers for blocks
|
||||||
* and transactions, i.e. two separate caches that must be provided.
|
* and transactions, i.e. two separate readers that must be provided.
|
||||||
*
|
*
|
||||||
* If source block, with just transaction hashes is not available, it returns empty
|
* If source block, with just transaction hashes is not available, it returns empty
|
||||||
* If any of the expected block transactions is not available it returns empty
|
* If any of the expected block transactions is not available it returns empty
|
||||||
*/
|
*/
|
||||||
class EthereumBlocksWithTxCache(
|
class EthereumFullBlocksReader(
|
||||||
private val objectMapper: ObjectMapper,
|
private val objectMapper: ObjectMapper,
|
||||||
private val blocks: Reader<BlockId, BlockContainer>,
|
private val blocks: Reader<BlockId, BlockContainer>,
|
||||||
private val txes: Reader<TxId, TxContainer>
|
private val txes: Reader<TxId, TxContainer>
|
||||||
) : Reader<BlockId, BlockContainer> {
|
) : Reader<BlockId, BlockContainer> {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val log = LoggerFactory.getLogger(EthereumBlocksWithTxCache::class.java)
|
private val log = LoggerFactory.getLogger(EthereumFullBlocksReader::class.java)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun read(key: BlockId): Mono<BlockContainer> {
|
override fun read(key: BlockId): Mono<BlockContainer> {
|
||||||
@@ -26,14 +26,14 @@ import java.time.Duration
|
|||||||
|
|
||||||
class EthereumHeadLagObserver(
|
class EthereumHeadLagObserver(
|
||||||
master: Head,
|
master: Head,
|
||||||
followers: Collection<Upstream<EthereumApi>>
|
followers: Collection<Upstream>
|
||||||
) : HeadLagObserver<EthereumApi>(master, followers) {
|
) : HeadLagObserver(master, followers) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val log = LoggerFactory.getLogger(EthereumHeadLagObserver::class.java)
|
private val log = LoggerFactory.getLogger(EthereumHeadLagObserver::class.java)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getCurrentBlocks(up: Upstream<EthereumApi>): Flux<BlockContainer> {
|
override fun getCurrentBlocks(up: Upstream): Flux<BlockContainer> {
|
||||||
val head = up.getHead()
|
val head = up.getHead()
|
||||||
return head.getFlux().take(Duration.ofSeconds(1))
|
return head.getFlux().take(Duration.ofSeconds(1))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,20 +19,24 @@ package io.emeraldpay.dshackle.upstream.ethereum
|
|||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
import io.emeraldpay.dshackle.cache.Caches
|
import io.emeraldpay.dshackle.cache.Caches
|
||||||
import io.emeraldpay.dshackle.config.UpstreamsConfig
|
import io.emeraldpay.dshackle.config.UpstreamsConfig
|
||||||
|
import io.emeraldpay.dshackle.reader.Reader
|
||||||
import io.emeraldpay.dshackle.upstream.*
|
import io.emeraldpay.dshackle.upstream.*
|
||||||
|
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
|
||||||
|
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
|
||||||
import io.emeraldpay.grpc.Chain
|
import io.emeraldpay.grpc.Chain
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import org.springframework.context.Lifecycle
|
import org.springframework.context.Lifecycle
|
||||||
|
import reactor.core.publisher.Mono
|
||||||
|
|
||||||
open class AggregatedEthereumUpstreams(
|
open class EthereumMultistream(
|
||||||
chain: Chain,
|
chain: Chain,
|
||||||
val upstreams: MutableList<EthereumUpstream>,
|
val upstreams: MutableList<EthereumUpstream>,
|
||||||
caches: Caches,
|
caches: Caches,
|
||||||
objectMapper: ObjectMapper
|
private val objectMapper: ObjectMapper
|
||||||
) : ChainUpstreams<EthereumApi>(chain, upstreams as MutableList<Upstream<EthereumApi>>, caches, objectMapper) {
|
) : Multistream(chain, upstreams as MutableList<Upstream>, caches) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val log = LoggerFactory.getLogger(AggregatedEthereumUpstreams::class.java)
|
private val log = LoggerFactory.getLogger(EthereumMultistream::class.java)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var head: Head? = null
|
private var head: Head? = null
|
||||||
@@ -92,7 +96,7 @@ open class AggregatedEthereumUpstreams(
|
|||||||
val newHead = MergedHead(upstreams.map { it.getHead() }).apply {
|
val newHead = MergedHead(upstreams.map { it.getHead() }).apply {
|
||||||
this.start()
|
this.start()
|
||||||
}
|
}
|
||||||
val lagObserver = EthereumHeadLagObserver(newHead, upstreams as Collection<Upstream<EthereumApi>>).apply {
|
val lagObserver = EthereumHeadLagObserver(newHead, upstreams as Collection<Upstream>).apply {
|
||||||
this.start()
|
this.start()
|
||||||
}
|
}
|
||||||
this.lagObserver = lagObserver
|
this.lagObserver = lagObserver
|
||||||
@@ -107,18 +111,15 @@ open class AggregatedEthereumUpstreams(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
override fun <T : Upstream<TA>, TA : UpstreamApi> cast(selfType: Class<T>, apiType: Class<TA>): T {
|
override fun <T : Upstream> cast(selfType: Class<T>): T {
|
||||||
if (!selfType.isAssignableFrom(this.javaClass)) {
|
if (!selfType.isAssignableFrom(this.javaClass)) {
|
||||||
throw ClassCastException("Cannot cast ${this.javaClass} to $selfType")
|
throw ClassCastException("Cannot cast ${this.javaClass} to $selfType")
|
||||||
}
|
}
|
||||||
return castApi(apiType) as T
|
return this as T
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun <A : UpstreamApi> castApi(apiType: Class<A>): Upstream<A> {
|
override fun getRoutedApi(matcher: Selector.Matcher): Mono<Reader<JsonRpcRequest, JsonRpcResponse>> {
|
||||||
if (!apiType.isAssignableFrom(EthereumApi::class.java)) {
|
return Mono.just(NativeCallRouter(objectMapper, reader, getMethods()))
|
||||||
throw ClassCastException("Cannot cast ${EthereumApi::class.java} to $apiType")
|
|
||||||
}
|
|
||||||
return this as Upstream<A>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -18,24 +18,21 @@ package io.emeraldpay.dshackle.upstream.ethereum
|
|||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
import io.emeraldpay.dshackle.Defaults
|
import io.emeraldpay.dshackle.Defaults
|
||||||
import io.emeraldpay.dshackle.cache.Caches
|
import io.emeraldpay.dshackle.cache.Caches
|
||||||
import io.emeraldpay.dshackle.cache.CachesEnabled
|
|
||||||
import io.emeraldpay.dshackle.cache.CurrentBlockCache
|
import io.emeraldpay.dshackle.cache.CurrentBlockCache
|
||||||
import io.emeraldpay.dshackle.data.BlockContainer
|
import io.emeraldpay.dshackle.data.*
|
||||||
import io.emeraldpay.dshackle.data.BlockId
|
|
||||||
import io.emeraldpay.dshackle.data.TxContainer
|
|
||||||
import io.emeraldpay.dshackle.data.TxId
|
|
||||||
import io.emeraldpay.dshackle.reader.*
|
import io.emeraldpay.dshackle.reader.*
|
||||||
import io.emeraldpay.dshackle.upstream.Head
|
import io.emeraldpay.dshackle.upstream.Multistream
|
||||||
import io.emeraldpay.dshackle.upstream.Selector
|
import io.emeraldpay.dshackle.upstream.Selector
|
||||||
import io.emeraldpay.dshackle.upstream.Upstream
|
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
|
||||||
|
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
|
||||||
import io.infinitape.etherjar.domain.Address
|
import io.infinitape.etherjar.domain.Address
|
||||||
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.domain.Wei
|
import io.infinitape.etherjar.domain.Wei
|
||||||
import io.infinitape.etherjar.rpc.Commands
|
import io.infinitape.etherjar.hex.HexQuantity
|
||||||
import io.infinitape.etherjar.rpc.RpcCall
|
import io.infinitape.etherjar.rpc.RpcException
|
||||||
|
import io.infinitape.etherjar.rpc.RpcResponseError
|
||||||
import io.infinitape.etherjar.rpc.json.BlockJson
|
import io.infinitape.etherjar.rpc.json.BlockJson
|
||||||
import io.infinitape.etherjar.rpc.json.BlockTag
|
|
||||||
import io.infinitape.etherjar.rpc.json.TransactionJson
|
import io.infinitape.etherjar.rpc.json.TransactionJson
|
||||||
import io.infinitape.etherjar.rpc.json.TransactionRefJson
|
import io.infinitape.etherjar.rpc.json.TransactionRefJson
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
@@ -48,7 +45,7 @@ import java.util.concurrent.TimeoutException
|
|||||||
import java.util.function.Function
|
import java.util.function.Function
|
||||||
|
|
||||||
open class EthereumReader(
|
open class EthereumReader(
|
||||||
private val up: Upstream<EthereumApi>,
|
private val up: Multistream,
|
||||||
private val caches: Caches,
|
private val caches: Caches,
|
||||||
private val objectMapper: ObjectMapper
|
private val objectMapper: ObjectMapper
|
||||||
) : Lifecycle {
|
) : Lifecycle {
|
||||||
@@ -57,51 +54,109 @@ open class EthereumReader(
|
|||||||
private val log = LoggerFactory.getLogger(EthereumReader::class.java)
|
private val log = LoggerFactory.getLogger(EthereumReader::class.java)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var headListener: Disposable? = null
|
|
||||||
private val balanceCache = CurrentBlockCache<Address, Wei>()
|
private val balanceCache = CurrentBlockCache<Address, Wei>()
|
||||||
|
|
||||||
private val extractBlock = Function<BlockContainer, BlockJson<TransactionRefJson>> { block ->
|
val extractBlock = Function<BlockContainer, BlockJson<TransactionRefJson>> { block ->
|
||||||
objectMapper
|
val existing = block.getParsed(BlockJson::class.java)
|
||||||
.readValue(block.json, BlockJson::class.java)
|
if (existing != null) {
|
||||||
.withoutTransactionDetails()
|
existing.withoutTransactionDetails()
|
||||||
|
} else {
|
||||||
|
objectMapper
|
||||||
|
.readValue(block.json, BlockJson::class.java)
|
||||||
|
.withoutTransactionDetails()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val extractTx = Function<TxContainer, TransactionJson> { tx ->
|
val extractTx = Function<TxContainer, TransactionJson> { tx ->
|
||||||
objectMapper
|
tx.getParsed(TransactionJson::class.java) ?: objectMapper.readValue(tx.json, TransactionJson::class.java)
|
||||||
.readValue(tx.json, TransactionJson::class.java)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private val blocksDirect: Reader<BlockHash, BlockJson<TransactionRefJson>>
|
val asRaw = Function<SourceContainer, ByteArray> { tx ->
|
||||||
private val txDirect: Reader<TransactionId, TransactionJson>
|
tx.json ?: ByteArray(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
val jsonToRaw = Function<Any, ByteArray> { json ->
|
||||||
|
objectMapper.writeValueAsBytes(json)
|
||||||
|
}
|
||||||
|
|
||||||
|
val blockAsContainer = Function<BlockJson<*>, BlockContainer> { block ->
|
||||||
|
BlockContainer.from(block.withoutTransactionDetails(), objectMapper)
|
||||||
|
}
|
||||||
|
val txAsContainer = Function<TransactionJson, TxContainer> { tx ->
|
||||||
|
TxContainer.from(tx, objectMapper)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val blocksDirect: Reader<BlockHash, BlockContainer>
|
||||||
|
private val blocksByHeightDirect: Reader<Long, BlockContainer>
|
||||||
|
private val txDirect: Reader<TransactionId, TxContainer>
|
||||||
private val balanceDirect: Reader<Address, Wei>
|
private val balanceDirect: Reader<Address, Wei>
|
||||||
|
|
||||||
private val idToBlockHash = Function<BlockId, BlockHash> { id -> BlockHash.from(id.value) }
|
private val idToBlockHash = Function<BlockId, BlockHash> { id -> BlockHash.from(id.value) }
|
||||||
private val blockHashToId = Function<BlockHash, BlockId> { hash -> BlockId.from(hash) }
|
private val blockHashToId = Function<BlockHash, BlockId> { hash -> BlockId.from(hash) }
|
||||||
|
|
||||||
private val txHashToId = Function<TransactionId, TxId> { hash -> TxId.from(hash) }
|
private val txHashToId = Function<TransactionId, TxId> { hash -> TxId.from(hash) }
|
||||||
|
private val idToTxHash = Function<TxId, TransactionId> { id -> TransactionId.from(id.value) }
|
||||||
|
|
||||||
|
private val directResponseBytes = Function<JsonRpcResponse, ByteArray> { resp ->
|
||||||
|
if (resp.error != null) {
|
||||||
|
throw resp.error.asException()
|
||||||
|
} else {
|
||||||
|
resp.getResult()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
blocksDirect = object : Reader<BlockHash, BlockJson<TransactionRefJson>> {
|
blocksDirect = object : Reader<BlockHash, BlockContainer> {
|
||||||
override fun read(key: BlockHash): Mono<BlockJson<TransactionRefJson>> {
|
override fun read(key: BlockHash): Mono<BlockContainer> {
|
||||||
return up.getApi(Selector.empty).flatMap { api ->
|
return up.getDirectApi(Selector.empty).flatMap { api ->
|
||||||
api.executeAndConvert(Commands.eth().getBlock(key))
|
val request = JsonRpcRequest("eth_getBlockByHash", listOf(key.toHex(), false))
|
||||||
|
api.read(request)
|
||||||
.timeout(Defaults.timeoutInternal, Mono.error(TimeoutException("Block not read $key")))
|
.timeout(Defaults.timeoutInternal, Mono.error(TimeoutException("Block not read $key")))
|
||||||
|
.map(directResponseBytes)
|
||||||
.retryWhen(Retry.backoff(3, Duration.ofSeconds(1)))
|
.retryWhen(Retry.backoff(3, Duration.ofSeconds(1)))
|
||||||
|
.map { blockbytes ->
|
||||||
|
val block = objectMapper.readValue(blockbytes, BlockJson::class.java) as BlockJson<TransactionRefJson>
|
||||||
|
BlockContainer.from(block, blockbytes)
|
||||||
|
}
|
||||||
.doOnNext { block ->
|
.doOnNext { block ->
|
||||||
caches.cache(Caches.Tag.REQUESTED, BlockContainer.from(block, objectMapper))
|
caches.cache(Caches.Tag.REQUESTED, block)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
txDirect = object : Reader<TransactionId, TransactionJson> {
|
blocksByHeightDirect = object : Reader<Long, BlockContainer> {
|
||||||
override fun read(key: TransactionId): Mono<TransactionJson> {
|
override fun read(key: Long): Mono<BlockContainer> {
|
||||||
return up.getApi(Selector.empty).flatMap { api ->
|
return up.getDirectApi(Selector.empty).flatMap { api ->
|
||||||
api.executeAndConvert(Commands.eth().getTransaction(key))
|
val request = JsonRpcRequest("eth_getBlockByNumber", listOf(HexQuantity.from(key).toHex(), false))
|
||||||
.timeout(Defaults.timeoutInternal, Mono.error(TimeoutException("Tx not read $key")))
|
api.read(request)
|
||||||
|
.timeout(Defaults.timeoutInternal, Mono.error(TimeoutException("Block not read $key")))
|
||||||
|
.map(directResponseBytes)
|
||||||
.retryWhen(Retry.backoff(3, Duration.ofSeconds(1)))
|
.retryWhen(Retry.backoff(3, Duration.ofSeconds(1)))
|
||||||
|
.map { blockbytes ->
|
||||||
|
val block = objectMapper.readValue(blockbytes, BlockJson::class.java) as BlockJson<TransactionRefJson>
|
||||||
|
BlockContainer.from(block, blockbytes)
|
||||||
|
}
|
||||||
|
.doOnNext { block ->
|
||||||
|
caches.cache(Caches.Tag.REQUESTED, block)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
txDirect = object : Reader<TransactionId, TxContainer> {
|
||||||
|
override fun read(key: TransactionId): Mono<TxContainer> {
|
||||||
|
return up.getDirectApi(Selector.empty).flatMap { api ->
|
||||||
|
val request = JsonRpcRequest("eth_getTransactionByHash", listOf(key.toHex()))
|
||||||
|
api.read(request)
|
||||||
|
.timeout(Defaults.timeoutInternal, Mono.error(TimeoutException("Tx not read $key")))
|
||||||
|
.map(directResponseBytes)
|
||||||
|
.retryWhen(Retry.backoff(3, Duration.ofSeconds(1)))
|
||||||
|
.map { txbytes ->
|
||||||
|
val tx = objectMapper.readValue(txbytes, TransactionJson::class.java)
|
||||||
|
TxContainer.from(tx, txbytes)
|
||||||
|
}
|
||||||
.doOnNext { tx ->
|
.doOnNext { tx ->
|
||||||
if (tx.blockNumber != null && tx.blockHash != null) {
|
if (tx.blockId != null) {
|
||||||
caches.cache(Caches.Tag.REQUESTED, TxContainer.from(tx, objectMapper))
|
caches.cache(Caches.Tag.REQUESTED, tx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -109,9 +164,19 @@ open class EthereumReader(
|
|||||||
}
|
}
|
||||||
balanceDirect = object : Reader<Address, Wei> {
|
balanceDirect = object : Reader<Address, Wei> {
|
||||||
override fun read(key: Address): Mono<Wei> {
|
override fun read(key: Address): Mono<Wei> {
|
||||||
return up.getApi(Selector.empty).flatMap { api ->
|
return up.getDirectApi(Selector.empty).flatMap { api ->
|
||||||
api.executeAndConvert(Commands.eth().getBalance(key, BlockTag.LATEST))
|
val request = JsonRpcRequest("eth_getBalance", listOf(key.toHex(), "latest"))
|
||||||
|
api.read(request)
|
||||||
.timeout(Defaults.timeoutInternal, Mono.error(TimeoutException("Balance not read $key")))
|
.timeout(Defaults.timeoutInternal, Mono.error(TimeoutException("Balance not read $key")))
|
||||||
|
.map(directResponseBytes)
|
||||||
|
.map {
|
||||||
|
val str = String(it)
|
||||||
|
if (str.startsWith("\"") && str.endsWith("\"")) {
|
||||||
|
Wei.from(str.substring(1, str.length - 1))
|
||||||
|
} else {
|
||||||
|
throw RpcException(RpcResponseError.CODE_UPSTREAM_INVALID_RESPONSE, "Not Wei value")
|
||||||
|
}
|
||||||
|
}
|
||||||
.retryWhen(Retry.backoff(3, Duration.ofSeconds(1)))
|
.retryWhen(Retry.backoff(3, Duration.ofSeconds(1)))
|
||||||
.doOnNext { value ->
|
.doOnNext { value ->
|
||||||
balanceCache.put(key, value)
|
balanceCache.put(key, value)
|
||||||
@@ -121,30 +186,61 @@ open class EthereumReader(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun blocksById(): Reader<BlockId, BlockJson<TransactionRefJson>> {
|
fun blocksByHash(): Reader<BlockHash, BlockJson<TransactionRefJson>> {
|
||||||
return CompoundReader(
|
return TransformingReader(
|
||||||
TransformingReader(caches.getBlocksByHash(), extractBlock),
|
CompoundReader(
|
||||||
RekeyingReader(idToBlockHash, blocksDirect)
|
RekeyingReader(blockHashToId, caches.getBlocksByHash()),
|
||||||
|
blocksDirect
|
||||||
|
),
|
||||||
|
extractBlock
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun blocksByHash(): Reader<BlockHash, BlockJson<TransactionRefJson>> {
|
fun blocksById(): Reader<BlockId, BlockJson<TransactionRefJson>> {
|
||||||
return CompoundReader(
|
return TransformingReader(
|
||||||
RekeyingReader(
|
CompoundReader(
|
||||||
blockHashToId,
|
caches.getBlocksByHash(),
|
||||||
TransformingReader(caches.getBlocksByHash(), extractBlock)
|
RekeyingReader(idToBlockHash, blocksDirect)
|
||||||
),
|
),
|
||||||
blocksDirect
|
extractBlock
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun blocksByHashAsCont(): Reader<BlockHash, BlockContainer> {
|
||||||
|
return TransformingReader(
|
||||||
|
blocksByHash(),
|
||||||
|
blockAsContainer
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun blocksByIdAsCont(): Reader<BlockId, BlockContainer> {
|
||||||
|
return TransformingReader(
|
||||||
|
blocksById(),
|
||||||
|
blockAsContainer
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun blocksByHeightAsCont(): Reader<Long, BlockContainer> {
|
||||||
|
return CompoundReader(
|
||||||
|
caches.getBlocksByHeight(),
|
||||||
|
blocksByHeightDirect
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun txByHash(): Reader<TransactionId, TransactionJson> {
|
fun txByHash(): Reader<TransactionId, TransactionJson> {
|
||||||
return CompoundReader(
|
return TransformingReader(
|
||||||
RekeyingReader(
|
CompoundReader(
|
||||||
txHashToId,
|
RekeyingReader(txHashToId, caches.getTxByHash()),
|
||||||
TransformingReader(caches.getTxByHash(), extractTx)
|
txDirect
|
||||||
),
|
),
|
||||||
txDirect
|
extractTx
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun txByHashAsCont(): Reader<TxId, TxContainer> {
|
||||||
|
return CompoundReader(
|
||||||
|
caches.getTxByHash(),
|
||||||
|
RekeyingReader(idToTxHash, txDirect)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,18 +251,17 @@ open class EthereumReader(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun isRunning(): Boolean {
|
override fun isRunning(): Boolean {
|
||||||
return this.headListener != null
|
//TODO should be always running?
|
||||||
|
return up.isRunning
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun start() {
|
override fun start() {
|
||||||
this.headListener = up.getHead().getFlux().subscribe {
|
val evictCaches: Runnable = Runnable {
|
||||||
balanceCache.evict()
|
balanceCache.evict()
|
||||||
}
|
}
|
||||||
|
up.getHead().onBeforeBlock(evictCaches)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun stop() {
|
override fun stop() {
|
||||||
val headListener = this.headListener
|
|
||||||
this.headListener = null
|
|
||||||
headListener?.dispose()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -19,6 +19,10 @@ package io.emeraldpay.dshackle.upstream.ethereum
|
|||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
import io.emeraldpay.dshackle.Defaults
|
import io.emeraldpay.dshackle.Defaults
|
||||||
import io.emeraldpay.dshackle.data.BlockContainer
|
import io.emeraldpay.dshackle.data.BlockContainer
|
||||||
|
import io.emeraldpay.dshackle.reader.Reader
|
||||||
|
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
|
||||||
|
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
|
||||||
|
import io.infinitape.etherjar.hex.HexQuantity
|
||||||
import io.infinitape.etherjar.rpc.Commands
|
import io.infinitape.etherjar.rpc.Commands
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import org.springframework.context.Lifecycle
|
import org.springframework.context.Lifecycle
|
||||||
@@ -31,7 +35,7 @@ import java.time.Duration
|
|||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
|
|
||||||
class EthereumRpcHead(
|
class EthereumRpcHead(
|
||||||
private val api: DirectEthereumApi,
|
private val api: Reader<in JsonRpcRequest, out JsonRpcResponse>,
|
||||||
private val objectMapper: ObjectMapper,
|
private val objectMapper: ObjectMapper,
|
||||||
private val interval: Duration = Duration.ofSeconds(10)
|
private val interval: Duration = Duration.ofSeconds(10)
|
||||||
): DefaultEthereumHead(), Lifecycle {
|
): DefaultEthereumHead(), Lifecycle {
|
||||||
@@ -48,21 +52,27 @@ class EthereumRpcHead(
|
|||||||
val base = Flux.interval(interval)
|
val base = Flux.interval(interval)
|
||||||
.publishOn(scheduler)
|
.publishOn(scheduler)
|
||||||
.flatMap {
|
.flatMap {
|
||||||
api.rpcClient
|
api.read(JsonRpcRequest("eth_blockNumber", emptyList()))
|
||||||
.execute(Commands.eth().blockNumber)
|
|
||||||
.subscribeOn(scheduler)
|
.subscribeOn(scheduler)
|
||||||
.timeout(Defaults.timeout, Mono.error(Exception("Block number not received")))
|
.timeout(Defaults.timeout, Mono.error(Exception("Block number not received")))
|
||||||
|
.flatMap {
|
||||||
|
if (it.error != null) {
|
||||||
|
Mono.error(it.error.asException())
|
||||||
|
} else {
|
||||||
|
val value = it.getResultAsProcessedString()
|
||||||
|
Mono.just(HexQuantity.from(value))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.flatMap {
|
.flatMap {
|
||||||
//fetching by Block Height here, critical to use same upstream,
|
//fetching by Block Height here, critical to use same upstream,
|
||||||
//different upstreams may have different blocks on the same height
|
//different upstreams may have different blocks on the same height
|
||||||
api.rpcClient
|
api.read(JsonRpcRequest("eth_getBlockByNumber", listOf(it.toHex(), false)))
|
||||||
.execute(Commands.eth().getBlock(it))
|
|
||||||
.subscribeOn(scheduler)
|
.subscribeOn(scheduler)
|
||||||
.timeout(Defaults.timeout, Mono.error(Exception("Block data not received")))
|
.timeout(Defaults.timeout, Mono.error(Exception("Block data not received")))
|
||||||
}
|
}
|
||||||
.map {
|
.map {
|
||||||
BlockContainer.from(it, objectMapper)
|
BlockContainer.from(it.getResult(), objectMapper)
|
||||||
}
|
}
|
||||||
.onErrorContinue { err, _ ->
|
.onErrorContinue { err, _ ->
|
||||||
log.debug("RPC error ${err.message}")
|
log.debug("RPC error ${err.message}")
|
||||||
|
|||||||
@@ -20,10 +20,13 @@ import com.fasterxml.jackson.databind.ObjectMapper
|
|||||||
import io.emeraldpay.dshackle.cache.Caches
|
import io.emeraldpay.dshackle.cache.Caches
|
||||||
import io.emeraldpay.dshackle.cache.CachesEnabled
|
import io.emeraldpay.dshackle.cache.CachesEnabled
|
||||||
import io.emeraldpay.dshackle.config.UpstreamsConfig
|
import io.emeraldpay.dshackle.config.UpstreamsConfig
|
||||||
|
import io.emeraldpay.dshackle.reader.Reader
|
||||||
import io.emeraldpay.dshackle.startup.QuorumForLabels
|
import io.emeraldpay.dshackle.startup.QuorumForLabels
|
||||||
import io.emeraldpay.dshackle.upstream.*
|
import io.emeraldpay.dshackle.upstream.*
|
||||||
import io.emeraldpay.dshackle.upstream.calls.CallMethods
|
import io.emeraldpay.dshackle.upstream.calls.CallMethods
|
||||||
import io.emeraldpay.dshackle.upstream.calls.DirectCallMethods
|
import io.emeraldpay.dshackle.upstream.calls.DirectCallMethods
|
||||||
|
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
|
||||||
|
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
|
||||||
import io.emeraldpay.grpc.Chain
|
import io.emeraldpay.grpc.Chain
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import org.springframework.context.Lifecycle
|
import org.springframework.context.Lifecycle
|
||||||
@@ -34,15 +37,15 @@ import java.time.Duration
|
|||||||
open class EthereumUpstream(
|
open class EthereumUpstream(
|
||||||
id: String,
|
id: String,
|
||||||
val chain: Chain,
|
val chain: Chain,
|
||||||
private val api: DirectEthereumApi,
|
private val directReader: Reader<JsonRpcRequest, JsonRpcResponse>,
|
||||||
private val ethereumWs: EthereumWs? = null,
|
private val ethereumWsFactory: EthereumWsFactory? = null,
|
||||||
options: UpstreamsConfig.Options,
|
options: UpstreamsConfig.Options,
|
||||||
val node: QuorumForLabels.QuorumItem,
|
val node: QuorumForLabels.QuorumItem,
|
||||||
targets: CallMethods,
|
targets: CallMethods,
|
||||||
private val objectMapper: ObjectMapper
|
private val objectMapper: ObjectMapper
|
||||||
) : DefaultUpstream<EthereumApi>(id, options, targets), Upstream<EthereumApi>, CachesEnabled, Lifecycle {
|
) : DefaultUpstream(id, options, targets), Upstream, CachesEnabled, Lifecycle {
|
||||||
|
|
||||||
constructor(id: String, chain: Chain, api: DirectEthereumApi, objectMapper: ObjectMapper) : this(id, chain, api, null,
|
constructor(id: String, chain: Chain, api: Reader<JsonRpcRequest, JsonRpcResponse>, objectMapper: ObjectMapper) : this(id, chain, api, null,
|
||||||
UpstreamsConfig.Options.getDefaults(), QuorumForLabels.QuorumItem(1, UpstreamsConfig.Labels()),
|
UpstreamsConfig.Options.getDefaults(), QuorumForLabels.QuorumItem(1, UpstreamsConfig.Labels()),
|
||||||
DirectCallMethods(), objectMapper)
|
DirectCallMethods(), objectMapper)
|
||||||
|
|
||||||
@@ -52,12 +55,7 @@ open class EthereumUpstream(
|
|||||||
private val head: Head = this.createHead()
|
private val head: Head = this.createHead()
|
||||||
private var validatorSubscription: Disposable? = null
|
private var validatorSubscription: Disposable? = null
|
||||||
|
|
||||||
init {
|
|
||||||
api.upstream = this
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun setCaches(caches: Caches) {
|
override fun setCaches(caches: Caches) {
|
||||||
api.caches = caches;
|
|
||||||
if (head is CachesEnabled) {
|
if (head is CachesEnabled) {
|
||||||
head.setCaches(caches)
|
head.setCaches(caches)
|
||||||
}
|
}
|
||||||
@@ -70,7 +68,7 @@ open class EthereumUpstream(
|
|||||||
this.setLag(0)
|
this.setLag(0)
|
||||||
this.setStatus(UpstreamAvailability.OK)
|
this.setStatus(UpstreamAvailability.OK)
|
||||||
} else {
|
} else {
|
||||||
val validator = EthereumUpstreamValidator(this, getOptions())
|
val validator = EthereumUpstreamValidator(this, getOptions(), objectMapper)
|
||||||
validatorSubscription = validator.start()
|
validatorSubscription = validator.start()
|
||||||
.subscribe(this::setStatus)
|
.subscribe(this::setStatus)
|
||||||
}
|
}
|
||||||
@@ -89,21 +87,24 @@ open class EthereumUpstream(
|
|||||||
}
|
}
|
||||||
|
|
||||||
open fun createHead(): Head {
|
open fun createHead(): Head {
|
||||||
return if (ethereumWs != null) {
|
return if (ethereumWsFactory != null) {
|
||||||
val ws = EthereumWsHead(ethereumWs).apply {
|
val ws = ethereumWsFactory.create(this).apply {
|
||||||
this.start()
|
connect()
|
||||||
}
|
}
|
||||||
// receive bew blocks through Websockets, but periodically verify with RPC
|
val wsHead = EthereumWsHead(ws).apply {
|
||||||
val rpc = EthereumRpcHead(api, objectMapper, Duration.ofSeconds(30)).apply {
|
start()
|
||||||
this.start()
|
|
||||||
}
|
}
|
||||||
MergedHead(listOf(rpc, ws)).apply {
|
// receive bew blocks through WebSockets, but also periodically verify with RPC in case if WS failed
|
||||||
this.start()
|
val rpcHead = EthereumRpcHead(getApi(), objectMapper, Duration.ofSeconds(60)).apply {
|
||||||
|
start()
|
||||||
|
}
|
||||||
|
MergedHead(listOf(rpcHead, wsHead)).apply {
|
||||||
|
start()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log.warn("Setting up upstream ${this.getId()} with RPC-only access, less effective than WS+RPC")
|
log.warn("Setting up upstream ${this.getId()} with RPC-only access, less effective than WS+RPC")
|
||||||
EthereumRpcHead(api, objectMapper).apply {
|
EthereumRpcHead(getApi(), objectMapper).apply {
|
||||||
this.start()
|
start()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -112,8 +113,8 @@ open class EthereumUpstream(
|
|||||||
return head
|
return head
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getApi(matcher: Selector.Matcher): Mono<DirectEthereumApi> {
|
override fun getApi(): Reader<JsonRpcRequest, JsonRpcResponse> {
|
||||||
return Mono.just(api)
|
return directReader
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getLabels(): Collection<UpstreamsConfig.Labels> {
|
override fun getLabels(): Collection<UpstreamsConfig.Labels> {
|
||||||
@@ -121,18 +122,11 @@ open class EthereumUpstream(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("unchecked")
|
@Suppress("unchecked")
|
||||||
override fun <T : Upstream<TA>, TA : UpstreamApi> cast(selfType: Class<T>, apiType: Class<TA>): T {
|
override fun <T : Upstream> cast(selfType: Class<T>): T {
|
||||||
if (!selfType.isAssignableFrom(this.javaClass)) {
|
if (!selfType.isAssignableFrom(this.javaClass)) {
|
||||||
throw ClassCastException("Cannot cast ${this.javaClass} to $selfType")
|
throw ClassCastException("Cannot cast ${this.javaClass} to $selfType")
|
||||||
}
|
}
|
||||||
return castApi(apiType) as T
|
return this as T
|
||||||
}
|
|
||||||
|
|
||||||
override fun <A : UpstreamApi> castApi(apiType: Class<A>): Upstream<A> {
|
|
||||||
if (!apiType.isAssignableFrom(EthereumApi::class.java)) {
|
|
||||||
throw ClassCastException("Cannot cast ${EthereumApi::class.java} to $apiType")
|
|
||||||
}
|
|
||||||
return this as Upstream<A>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -16,11 +16,13 @@
|
|||||||
*/
|
*/
|
||||||
package io.emeraldpay.dshackle.upstream.ethereum
|
package io.emeraldpay.dshackle.upstream.ethereum
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
import io.emeraldpay.dshackle.Defaults
|
import io.emeraldpay.dshackle.Defaults
|
||||||
import io.emeraldpay.dshackle.config.UpstreamsConfig
|
import io.emeraldpay.dshackle.config.UpstreamsConfig
|
||||||
import io.emeraldpay.dshackle.upstream.Selector
|
|
||||||
import io.emeraldpay.dshackle.upstream.UpstreamAvailability
|
import io.emeraldpay.dshackle.upstream.UpstreamAvailability
|
||||||
import io.infinitape.etherjar.rpc.*
|
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
|
||||||
|
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
|
||||||
|
import io.infinitape.etherjar.rpc.json.SyncingJson
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import org.springframework.scheduling.concurrent.CustomizableThreadFactory
|
import org.springframework.scheduling.concurrent.CustomizableThreadFactory
|
||||||
import reactor.core.publisher.Flux
|
import reactor.core.publisher.Flux
|
||||||
@@ -30,8 +32,9 @@ import java.time.Duration
|
|||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
|
|
||||||
class EthereumUpstreamValidator(
|
class EthereumUpstreamValidator(
|
||||||
private val ethereumUpstream: EthereumUpstream,
|
private val upstream: EthereumUpstream,
|
||||||
private val options: UpstreamsConfig.Options
|
private val options: UpstreamsConfig.Options,
|
||||||
|
private val objectMapper: ObjectMapper
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
private val log = LoggerFactory.getLogger(EthereumUpstreamValidator::class.java)
|
private val log = LoggerFactory.getLogger(EthereumUpstreamValidator::class.java)
|
||||||
@@ -39,30 +42,32 @@ class EthereumUpstreamValidator(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun validate(): Mono<UpstreamAvailability> {
|
fun validate(): Mono<UpstreamAvailability> {
|
||||||
return ethereumUpstream
|
return upstream
|
||||||
.getApi(Selector.empty)
|
.getApi()
|
||||||
.flatMapMany { api ->
|
.read(JsonRpcRequest("eth_syncing", listOf()))
|
||||||
api.rpcClient
|
.flatMap(JsonRpcResponse::requireResult)
|
||||||
.execute(Commands.eth().syncing())
|
.map { objectMapper.readValue(it, SyncingJson::class.java) }
|
||||||
.timeout(Defaults.timeoutInternal, Mono.error(Exception("Validation timeout for Syncing")))
|
.timeout(Defaults.timeoutInternal, Mono.error(Exception("Validation timeout for Syncing")))
|
||||||
.flatMap { value ->
|
.flatMap { value ->
|
||||||
if (value.isSyncing) {
|
if (value.isSyncing) {
|
||||||
Mono.just(UpstreamAvailability.SYNCING)
|
Mono.just(UpstreamAvailability.SYNCING)
|
||||||
} else {
|
} else {
|
||||||
api.rpcClient.execute(Commands.net().peerCount())
|
upstream
|
||||||
.timeout(Defaults.timeoutInternal, Mono.error(Exception("Validation timeout for Peers")))
|
.getApi()
|
||||||
.map { count ->
|
.read(JsonRpcRequest("net_peerCount", listOf()))
|
||||||
val minPeers = options.minPeers ?: 1
|
.flatMap(JsonRpcResponse::requireStringResult)
|
||||||
if (count < minPeers) {
|
.map(Integer::decode)
|
||||||
UpstreamAvailability.IMMATURE
|
.timeout(Defaults.timeoutInternal, Mono.error(Exception("Validation timeout for Peers")))
|
||||||
} else {
|
.map { count ->
|
||||||
UpstreamAvailability.OK
|
val minPeers = options.minPeers ?: 1
|
||||||
}
|
if (count < minPeers) {
|
||||||
}
|
UpstreamAvailability.IMMATURE
|
||||||
|
} else {
|
||||||
|
UpstreamAvailability.OK
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.single()
|
|
||||||
.onErrorReturn(UpstreamAvailability.UNAVAILABLE)
|
.onErrorReturn(UpstreamAvailability.UNAVAILABLE)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,108 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2020 EmeraldPay, Inc
|
|
||||||
* Copyright (c) 2019 ETCDEV GmbH
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
package io.emeraldpay.dshackle.upstream.ethereum
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
|
||||||
import io.emeraldpay.dshackle.Defaults
|
|
||||||
import io.emeraldpay.dshackle.cache.Caches
|
|
||||||
import io.emeraldpay.dshackle.cache.CachesEnabled
|
|
||||||
import io.emeraldpay.dshackle.config.AuthConfig
|
|
||||||
import io.emeraldpay.dshackle.data.BlockContainer
|
|
||||||
import io.emeraldpay.dshackle.data.BlockId
|
|
||||||
import io.emeraldpay.dshackle.reader.EmptyReader
|
|
||||||
import io.emeraldpay.dshackle.reader.Reader
|
|
||||||
import io.infinitape.etherjar.rpc.Commands
|
|
||||||
import io.infinitape.etherjar.rpc.json.BlockJson
|
|
||||||
import io.infinitape.etherjar.rpc.json.TransactionRefJson
|
|
||||||
import io.infinitape.etherjar.rpc.ws.WebsocketClient
|
|
||||||
import org.slf4j.LoggerFactory
|
|
||||||
import reactor.core.publisher.Flux
|
|
||||||
import reactor.core.publisher.Mono
|
|
||||||
import reactor.core.publisher.TopicProcessor
|
|
||||||
import reactor.retry.Repeat
|
|
||||||
import java.net.URI
|
|
||||||
import java.time.Duration
|
|
||||||
|
|
||||||
class EthereumWs(
|
|
||||||
private val uri: URI,
|
|
||||||
private val origin: URI,
|
|
||||||
private val api: EthereumApi,
|
|
||||||
private val objectMapper: ObjectMapper
|
|
||||||
): CachesEnabled {
|
|
||||||
|
|
||||||
private val log = LoggerFactory.getLogger(EthereumWs::class.java)
|
|
||||||
private val topic = TopicProcessor
|
|
||||||
.builder<BlockContainer>()
|
|
||||||
.name("new-blocks")
|
|
||||||
.build()
|
|
||||||
var basicAuth: AuthConfig.ClientBasicAuth? = null
|
|
||||||
|
|
||||||
private var blockCache: Reader<BlockId, BlockContainer> = EmptyReader()
|
|
||||||
|
|
||||||
fun connect() {
|
|
||||||
log.info("Connecting to WebSocket: $uri")
|
|
||||||
val clientBuilder = WebsocketClient.newBuilder()
|
|
||||||
.connectTo(uri)
|
|
||||||
.origin(origin)
|
|
||||||
basicAuth?.let { auth ->
|
|
||||||
clientBuilder.basicAuth(auth.username, auth.password)
|
|
||||||
}
|
|
||||||
val client = clientBuilder.build()
|
|
||||||
try {
|
|
||||||
client.connect()
|
|
||||||
client.onNewBlock(this::onNewBlock)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
log.error("Failed to connect to websocket at $uri. Error: ${e.message}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onNewBlock(block: BlockJson<TransactionRefJson>) {
|
|
||||||
// WS returns incomplete blocks
|
|
||||||
if (block.difficulty == null || block.transactions == null) {
|
|
||||||
Mono.just(block.hash).flatMap { hash ->
|
|
||||||
val hash = BlockId.from(hash)
|
|
||||||
// first check in cache, if empty then check api
|
|
||||||
blockCache.read(hash)
|
|
||||||
.switchIfEmpty(request(hash))
|
|
||||||
}.repeatWhenEmpty { n ->
|
|
||||||
Repeat.times<Any>(10)
|
|
||||||
.exponentialBackoff(Duration.ofMillis(50), Duration.ofMillis(250))
|
|
||||||
.apply(n)
|
|
||||||
}
|
|
||||||
.timeout(Defaults.timeout, Mono.empty())
|
|
||||||
.subscribe(topic::onNext)
|
|
||||||
|
|
||||||
} else {
|
|
||||||
topic.onNext(BlockContainer.from(block, objectMapper))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun request(hash: BlockId): Mono<BlockContainer> {
|
|
||||||
return api
|
|
||||||
.executeAndConvert(Commands.eth().getBlock(io.infinitape.etherjar.domain.BlockHash(hash.value)))
|
|
||||||
.map { BlockContainer.from(it, objectMapper) }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getFlux(): Flux<BlockContainer> {
|
|
||||||
return Flux.from(this.topic)
|
|
||||||
.onBackpressureLatest()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun setCaches(caches: Caches) {
|
|
||||||
blockCache = caches.getBlocksByHash()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2020 EmeraldPay, Inc
|
||||||
|
* Copyright (c) 2019 ETCDEV GmbH
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package io.emeraldpay.dshackle.upstream.ethereum
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
|
import io.emeraldpay.dshackle.Defaults
|
||||||
|
import io.emeraldpay.dshackle.SilentException
|
||||||
|
import io.emeraldpay.dshackle.config.AuthConfig
|
||||||
|
import io.emeraldpay.dshackle.data.BlockContainer
|
||||||
|
import io.emeraldpay.dshackle.data.BlockId
|
||||||
|
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
|
||||||
|
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
|
||||||
|
import io.infinitape.etherjar.rpc.json.BlockJson
|
||||||
|
import io.infinitape.etherjar.rpc.json.TransactionRefJson
|
||||||
|
import io.infinitape.etherjar.rpc.ws.WebsocketClient
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import reactor.core.publisher.Flux
|
||||||
|
import reactor.core.publisher.Mono
|
||||||
|
import reactor.core.publisher.TopicProcessor
|
||||||
|
import reactor.retry.Repeat
|
||||||
|
import java.net.URI
|
||||||
|
import java.time.Duration
|
||||||
|
|
||||||
|
class EthereumWsFactory(
|
||||||
|
private val uri: URI,
|
||||||
|
private val origin: URI,
|
||||||
|
private val objectMapper: ObjectMapper
|
||||||
|
) {
|
||||||
|
|
||||||
|
var basicAuth: AuthConfig.ClientBasicAuth? = null
|
||||||
|
|
||||||
|
fun create(upstream: EthereumUpstream): EthereumWs {
|
||||||
|
return EthereumWs(uri, origin, upstream, objectMapper, basicAuth)
|
||||||
|
}
|
||||||
|
|
||||||
|
class EthereumWs(
|
||||||
|
private val uri: URI,
|
||||||
|
private val origin: URI,
|
||||||
|
private val upstream: EthereumUpstream,
|
||||||
|
private val objectMapper: ObjectMapper,
|
||||||
|
private val basicAuth: AuthConfig.ClientBasicAuth?
|
||||||
|
) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val log = LoggerFactory.getLogger(EthereumWs::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val topic = TopicProcessor
|
||||||
|
.builder<BlockContainer>()
|
||||||
|
.name("new-blocks")
|
||||||
|
.build()
|
||||||
|
|
||||||
|
fun connect() {
|
||||||
|
log.info("Connecting to WebSocket: $uri")
|
||||||
|
val clientBuilder = WebsocketClient.newBuilder()
|
||||||
|
.connectTo(uri)
|
||||||
|
.origin(origin)
|
||||||
|
basicAuth?.let { auth ->
|
||||||
|
clientBuilder.basicAuth(auth.username, auth.password)
|
||||||
|
}
|
||||||
|
val client = clientBuilder.build()
|
||||||
|
try {
|
||||||
|
client.connect()
|
||||||
|
client.onNewBlock(this::onNewBlock)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
log.error("Failed to connect to websocket at $uri. Error: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onNewBlock(block: BlockJson<TransactionRefJson>) {
|
||||||
|
// WS returns incomplete blocks
|
||||||
|
if (block.difficulty == null || block.transactions == null) {
|
||||||
|
Mono.just(block.hash)
|
||||||
|
.flatMap { hash ->
|
||||||
|
upstream.getApi()
|
||||||
|
.read(JsonRpcRequest("eth_getBlockByHash", listOf(hash.toHex(), false)))
|
||||||
|
.flatMap { resp ->
|
||||||
|
if (resp.isNull()) {
|
||||||
|
Mono.error(SilentException("Received null for block $hash"))
|
||||||
|
} else {
|
||||||
|
Mono.just(resp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.flatMap(JsonRpcResponse::requireResult)
|
||||||
|
.map { BlockContainer.from(it, objectMapper) }
|
||||||
|
}.repeatWhenEmpty { n ->
|
||||||
|
Repeat.times<Any>(5)
|
||||||
|
.exponentialBackoff(Duration.ofMillis(50), Duration.ofMillis(500))
|
||||||
|
.apply(n)
|
||||||
|
}
|
||||||
|
.timeout(Defaults.timeout, Mono.empty())
|
||||||
|
.onErrorResume { Mono.empty() }
|
||||||
|
.subscribe(topic::onNext)
|
||||||
|
|
||||||
|
} else {
|
||||||
|
topic.onNext(BlockContainer.from(block, objectMapper))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getFlux(): Flux<BlockContainer> {
|
||||||
|
return Flux.from(this.topic)
|
||||||
|
.onBackpressureLatest()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -16,15 +16,13 @@
|
|||||||
*/
|
*/
|
||||||
package io.emeraldpay.dshackle.upstream.ethereum
|
package io.emeraldpay.dshackle.upstream.ethereum
|
||||||
|
|
||||||
import io.emeraldpay.dshackle.cache.Caches
|
|
||||||
import io.emeraldpay.dshackle.cache.CachesEnabled
|
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import org.springframework.context.Lifecycle
|
import org.springframework.context.Lifecycle
|
||||||
import reactor.core.Disposable
|
import reactor.core.Disposable
|
||||||
|
|
||||||
class EthereumWsHead(
|
class EthereumWsHead(
|
||||||
private val ws: EthereumWs
|
private val ws: EthereumWsFactory.EthereumWs
|
||||||
): DefaultEthereumHead(), Lifecycle, CachesEnabled {
|
) : DefaultEthereumHead(), Lifecycle {
|
||||||
|
|
||||||
private val log = LoggerFactory.getLogger(EthereumWsHead::class.java)
|
private val log = LoggerFactory.getLogger(EthereumWsHead::class.java)
|
||||||
|
|
||||||
@@ -43,8 +41,4 @@ class EthereumWsHead(
|
|||||||
subscription = null
|
subscription = null
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setCaches(caches: Caches) {
|
|
||||||
ws.setCaches(caches)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2020 EmeraldPay, Inc
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package io.emeraldpay.dshackle.upstream.ethereum
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
|
import io.emeraldpay.dshackle.data.BlockId
|
||||||
|
import io.emeraldpay.dshackle.data.TxId
|
||||||
|
import io.emeraldpay.dshackle.reader.Reader
|
||||||
|
import io.emeraldpay.dshackle.upstream.calls.CallMethods
|
||||||
|
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
|
||||||
|
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
|
||||||
|
import io.infinitape.etherjar.hex.HexQuantity
|
||||||
|
import io.infinitape.etherjar.rpc.RpcException
|
||||||
|
import io.infinitape.etherjar.rpc.RpcResponseError
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import reactor.core.publisher.Mono
|
||||||
|
import java.math.BigInteger
|
||||||
|
|
||||||
|
class NativeCallRouter(
|
||||||
|
private val objectMapper: ObjectMapper,
|
||||||
|
private val reader: EthereumReader,
|
||||||
|
private val methods: CallMethods
|
||||||
|
) : Reader<JsonRpcRequest, JsonRpcResponse> {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val log = LoggerFactory.getLogger(NativeCallRouter::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val fullBlocksReader = EthereumFullBlocksReader(
|
||||||
|
objectMapper,
|
||||||
|
reader.blocksByIdAsCont(),
|
||||||
|
reader.txByHashAsCont()
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun read(key: JsonRpcRequest): Mono<JsonRpcResponse> {
|
||||||
|
if (methods.isHardcoded(key.method)) {
|
||||||
|
return Mono.just(methods.executeHardcoded(key.method))
|
||||||
|
.map { JsonRpcResponse(it, null) }
|
||||||
|
}
|
||||||
|
if (!methods.isAllowed(key.method)) {
|
||||||
|
return Mono.error(RpcException(RpcResponseError.CODE_METHOD_NOT_EXIST, "Unsupported method"))
|
||||||
|
}
|
||||||
|
val common = commonRequests(key)
|
||||||
|
if (common != null) {
|
||||||
|
return common.map { JsonRpcResponse(it, null) }
|
||||||
|
}
|
||||||
|
return Mono.empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare RpcCall with data types specific for that particular requests. In general it may return a call that just
|
||||||
|
* parses JSON into Map. But the purpose of further processing and caching for some of the requests we want
|
||||||
|
* to have actual data types.
|
||||||
|
*/
|
||||||
|
fun commonRequests(key: JsonRpcRequest): Mono<ByteArray>? {
|
||||||
|
val method = key.method
|
||||||
|
val params = key.params
|
||||||
|
return when {
|
||||||
|
method == "eth_getTransactionByHash" -> {
|
||||||
|
if (params.size != 1) {
|
||||||
|
throw RpcException(RpcResponseError.CODE_INVALID_METHOD_PARAMS, "Must provide 1 parameter")
|
||||||
|
}
|
||||||
|
val hash: TxId
|
||||||
|
try {
|
||||||
|
hash = TxId.from(params[0].toString())
|
||||||
|
} catch (e: IllegalArgumentException) {
|
||||||
|
throw RpcException(RpcResponseError.CODE_INVALID_METHOD_PARAMS, "[0] must be transaction id")
|
||||||
|
}
|
||||||
|
reader.txByHashAsCont().read(hash).map { it.json!! }
|
||||||
|
}
|
||||||
|
method == "eth_getBlockByHash" -> {
|
||||||
|
if (params.size != 2) {
|
||||||
|
throw RpcException(RpcResponseError.CODE_INVALID_METHOD_PARAMS, "Must provide 2 parameters")
|
||||||
|
}
|
||||||
|
val hash: BlockId
|
||||||
|
try {
|
||||||
|
hash = BlockId.from(params[0].toString())
|
||||||
|
} catch (e: IllegalArgumentException) {
|
||||||
|
throw RpcException(RpcResponseError.CODE_INVALID_METHOD_PARAMS, "[0] must be block hash")
|
||||||
|
}
|
||||||
|
val withTx = params[1].toString().toBoolean()
|
||||||
|
if (withTx) {
|
||||||
|
fullBlocksReader.read(hash).map { it.json!! }
|
||||||
|
} else {
|
||||||
|
reader.blocksByIdAsCont().read(hash).map { it.json!! }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
method == "eth_getBlockByNumber" -> {
|
||||||
|
if (params.size != 2) {
|
||||||
|
throw RpcException(RpcResponseError.CODE_INVALID_METHOD_PARAMS, "Must provide 2 parameters")
|
||||||
|
}
|
||||||
|
val number: Long
|
||||||
|
try {
|
||||||
|
val quantity = HexQuantity.from(params[0].toString()) ?: throw IllegalArgumentException()
|
||||||
|
number = quantity.value.let {
|
||||||
|
if (it < BigInteger.valueOf(Long.MAX_VALUE) && it >= BigInteger.ZERO) {
|
||||||
|
it.toLong()
|
||||||
|
} else {
|
||||||
|
throw IllegalArgumentException()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: IllegalArgumentException) {
|
||||||
|
throw RpcException(RpcResponseError.CODE_INVALID_METHOD_PARAMS, "[0] must be block number")
|
||||||
|
}
|
||||||
|
val withTx = params[1].toString().toBoolean()
|
||||||
|
if (withTx) {
|
||||||
|
log.warn("Block by number is not implemented")
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
reader.blocksByHeightAsCont().read(number).map { it.json!! }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,22 +22,21 @@ import io.emeraldpay.api.proto.BlockchainOuterClass
|
|||||||
import io.emeraldpay.api.proto.Common
|
import io.emeraldpay.api.proto.Common
|
||||||
import io.emeraldpay.api.proto.ReactorBlockchainGrpc
|
import io.emeraldpay.api.proto.ReactorBlockchainGrpc
|
||||||
import io.emeraldpay.dshackle.Defaults
|
import io.emeraldpay.dshackle.Defaults
|
||||||
import io.emeraldpay.dshackle.cache.Caches
|
|
||||||
import io.emeraldpay.dshackle.cache.CachesEnabled
|
|
||||||
import io.emeraldpay.dshackle.config.UpstreamsConfig
|
import io.emeraldpay.dshackle.config.UpstreamsConfig
|
||||||
import io.emeraldpay.dshackle.data.BlockContainer
|
import io.emeraldpay.dshackle.data.BlockContainer
|
||||||
import io.emeraldpay.dshackle.data.BlockId
|
import io.emeraldpay.dshackle.data.BlockId
|
||||||
|
import io.emeraldpay.dshackle.reader.Reader
|
||||||
import io.emeraldpay.dshackle.startup.QuorumForLabels
|
import io.emeraldpay.dshackle.startup.QuorumForLabels
|
||||||
import io.emeraldpay.dshackle.upstream.*
|
import io.emeraldpay.dshackle.upstream.*
|
||||||
import io.emeraldpay.dshackle.upstream.calls.CallMethods
|
import io.emeraldpay.dshackle.upstream.calls.CallMethods
|
||||||
import io.emeraldpay.dshackle.upstream.calls.DirectCallMethods
|
import io.emeraldpay.dshackle.upstream.calls.DirectCallMethods
|
||||||
import io.emeraldpay.dshackle.upstream.ethereum.DefaultEthereumHead
|
import io.emeraldpay.dshackle.upstream.ethereum.*
|
||||||
import io.emeraldpay.dshackle.upstream.ethereum.DirectEthereumApi
|
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcGrpcClient
|
||||||
import io.emeraldpay.dshackle.upstream.ethereum.EthereumApi
|
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
|
||||||
|
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
|
||||||
import io.emeraldpay.grpc.Chain
|
import io.emeraldpay.grpc.Chain
|
||||||
import io.infinitape.etherjar.domain.BlockHash
|
import io.infinitape.etherjar.domain.BlockHash
|
||||||
import io.infinitape.etherjar.rpc.*
|
import io.infinitape.etherjar.rpc.*
|
||||||
import io.infinitape.etherjar.rpc.emerald.ReactorEmeraldClient
|
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import org.springframework.context.Lifecycle
|
import org.springframework.context.Lifecycle
|
||||||
import reactor.core.Disposable
|
import reactor.core.Disposable
|
||||||
@@ -58,16 +57,15 @@ open class EthereumGrpcUpstream(
|
|||||||
private val chain: Chain,
|
private val chain: Chain,
|
||||||
private val blockchainStub: ReactorBlockchainGrpc.ReactorBlockchainStub,
|
private val blockchainStub: ReactorBlockchainGrpc.ReactorBlockchainStub,
|
||||||
private val objectMapper: ObjectMapper,
|
private val objectMapper: ObjectMapper,
|
||||||
private val rpcClient: ReactorEmeraldClient
|
private val client: JsonRpcGrpcClient
|
||||||
) : DefaultUpstream<EthereumApi>(
|
) : DefaultUpstream(
|
||||||
"$parentId/${chain.chainCode}",
|
"$parentId/${chain.chainCode}",
|
||||||
UpstreamsConfig.Options.getDefaults(),
|
UpstreamsConfig.Options.getDefaults(),
|
||||||
null
|
null
|
||||||
), CachesEnabled, Lifecycle {
|
), Lifecycle {
|
||||||
|
|
||||||
private var allLabels: Collection<UpstreamsConfig.Labels> = ArrayList<UpstreamsConfig.Labels>()
|
private var allLabels: Collection<UpstreamsConfig.Labels> = ArrayList<UpstreamsConfig.Labels>()
|
||||||
private val log = LoggerFactory.getLogger(EthereumGrpcUpstream::class.java)
|
private val log = LoggerFactory.getLogger(EthereumGrpcUpstream::class.java)
|
||||||
private var caches: Caches? = null
|
|
||||||
|
|
||||||
private val nodes = AtomicReference<QuorumForLabels>(QuorumForLabels())
|
private val nodes = AtomicReference<QuorumForLabels>(QuorumForLabels())
|
||||||
private val head = DefaultEthereumHead()
|
private val head = DefaultEthereumHead()
|
||||||
@@ -76,16 +74,7 @@ open class EthereumGrpcUpstream(
|
|||||||
|
|
||||||
var timeout = Defaults.timeout
|
var timeout = Defaults.timeout
|
||||||
|
|
||||||
open fun createApi(matcher: Selector.Matcher): DirectEthereumApi {
|
private val defaultReader: Reader<JsonRpcRequest, JsonRpcResponse> = client.forSelector(Selector.empty)
|
||||||
val targets = this.getMethods()
|
|
||||||
val client = Selector.extractLabels(matcher)?.let { selector ->
|
|
||||||
rpcClient.copyWithSelector(selector.asProto())
|
|
||||||
} ?: rpcClient
|
|
||||||
return DirectEthereumApi(client, caches, objectMapper, targets).let {
|
|
||||||
it.upstream = this
|
|
||||||
it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun start() {
|
override fun start() {
|
||||||
if (this.isRunning) return
|
if (this.isRunning) return
|
||||||
@@ -123,6 +112,7 @@ open class EthereumGrpcUpstream(
|
|||||||
BigInteger(1, value.weight.toByteArray()),
|
BigInteger(1, value.weight.toByteArray()),
|
||||||
Instant.ofEpochMilli(value.timestamp),
|
Instant.ofEpochMilli(value.timestamp),
|
||||||
false,
|
false,
|
||||||
|
null,
|
||||||
null
|
null
|
||||||
)
|
)
|
||||||
block
|
block
|
||||||
@@ -132,9 +122,11 @@ open class EthereumGrpcUpstream(
|
|||||||
val curr = head.getCurrent()
|
val curr = head.getCurrent()
|
||||||
curr == null || curr.difficulty < block.difficulty
|
curr == null || curr.difficulty < block.difficulty
|
||||||
}.flatMap {
|
}.flatMap {
|
||||||
getApi(Selector.EmptyMatcher())
|
defaultReader.read(JsonRpcRequest("eth_getBlockByHash", listOf(it.hash.toHexWithPrefix(), false)))
|
||||||
.flatMap { api -> api.executeAndConvert(Commands.eth().getBlock(BlockHash(it.hash.value))) }
|
.flatMap(JsonRpcResponse::requireResult)
|
||||||
.map { BlockContainer.from(it, objectMapper) }
|
.map {
|
||||||
|
BlockContainer.from(it, objectMapper)
|
||||||
|
}
|
||||||
.timeout(timeout, Mono.error(TimeoutException("Timeout from upstream")))
|
.timeout(timeout, Mono.error(TimeoutException("Timeout from upstream")))
|
||||||
.doOnError { t ->
|
.doOnError { t ->
|
||||||
setStatus(UpstreamAvailability.UNAVAILABLE)
|
setStatus(UpstreamAvailability.UNAVAILABLE)
|
||||||
@@ -208,26 +200,16 @@ open class EthereumGrpcUpstream(
|
|||||||
return head
|
return head
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getApi(matcher: Selector.Matcher): Mono<DirectEthereumApi> {
|
override fun getApi(): Reader<JsonRpcRequest, JsonRpcResponse> {
|
||||||
return Mono.just(createApi(matcher))
|
return defaultReader
|
||||||
}
|
|
||||||
|
|
||||||
override fun setCaches(caches: Caches) {
|
|
||||||
this.caches = caches
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
override fun <T : Upstream<TA>, TA : UpstreamApi> cast(selfType: Class<T>, apiType: Class<TA>): T {
|
override fun <T : Upstream> cast(selfType: Class<T>): T {
|
||||||
if (!selfType.isAssignableFrom(this.javaClass)) {
|
if (!selfType.isAssignableFrom(this.javaClass)) {
|
||||||
throw ClassCastException("Cannot cast ${this.javaClass} to $selfType")
|
throw ClassCastException("Cannot cast ${this.javaClass} to $selfType")
|
||||||
}
|
}
|
||||||
return castApi(apiType) as T
|
return this as T
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun <A : UpstreamApi> castApi(apiType: Class<A>): Upstream<A> {
|
|
||||||
if (!apiType.isAssignableFrom(EthereumApi::class.java)) {
|
|
||||||
throw ClassCastException("Cannot cast ${EthereumApi::class.java} to $apiType")
|
|
||||||
}
|
|
||||||
return this as Upstream<A>
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -25,10 +25,10 @@ import io.emeraldpay.dshackle.FileResolver
|
|||||||
import io.emeraldpay.dshackle.config.AuthConfig
|
import io.emeraldpay.dshackle.config.AuthConfig
|
||||||
import io.emeraldpay.dshackle.upstream.UpstreamAvailability
|
import io.emeraldpay.dshackle.upstream.UpstreamAvailability
|
||||||
import io.emeraldpay.dshackle.startup.UpstreamChange
|
import io.emeraldpay.dshackle.startup.UpstreamChange
|
||||||
|
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcGrpcClient
|
||||||
import io.emeraldpay.grpc.Chain
|
import io.emeraldpay.grpc.Chain
|
||||||
import io.grpc.ManagedChannelBuilder
|
import io.grpc.ManagedChannelBuilder
|
||||||
import io.grpc.netty.NettyChannelBuilder
|
import io.grpc.netty.NettyChannelBuilder
|
||||||
import io.infinitape.etherjar.rpc.emerald.ReactorEmeraldClient
|
|
||||||
import io.netty.handler.ssl.*
|
import io.netty.handler.ssl.*
|
||||||
import org.apache.commons.lang3.StringUtils
|
import org.apache.commons.lang3.StringUtils
|
||||||
import org.apache.commons.lang3.exception.ExceptionUtils
|
import org.apache.commons.lang3.exception.ExceptionUtils
|
||||||
@@ -57,7 +57,6 @@ class GrpcUpstreams(
|
|||||||
private var client: ReactorBlockchainGrpc.ReactorBlockchainStub? = null
|
private var client: ReactorBlockchainGrpc.ReactorBlockchainStub? = null
|
||||||
private val known = HashMap<Chain, EthereumGrpcUpstream>()
|
private val known = HashMap<Chain, EthereumGrpcUpstream>()
|
||||||
private val lock = ReentrantLock()
|
private val lock = ReentrantLock()
|
||||||
private var grpcTransport: ReactorEmeraldClient? = null
|
|
||||||
|
|
||||||
fun start(): Flux<UpstreamChange> {
|
fun start(): Flux<UpstreamChange> {
|
||||||
val channel: ManagedChannelBuilder<*> = if (auth != null && StringUtils.isNotEmpty(auth.ca)) {
|
val channel: ManagedChannelBuilder<*> = if (auth != null && StringUtils.isNotEmpty(auth.ca)) {
|
||||||
@@ -74,10 +73,6 @@ class GrpcUpstreams(
|
|||||||
|
|
||||||
val client = ReactorBlockchainGrpc.newReactorStub(channel.build())
|
val client = ReactorBlockchainGrpc.newReactorStub(channel.build())
|
||||||
this.client = client
|
this.client = client
|
||||||
this.grpcTransport = ReactorEmeraldClient.newBuilder()
|
|
||||||
.connectUsing(client.channel)
|
|
||||||
.objectMapper(objectMapper)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
val statusSubscription = AtomicReference<Disposable>()
|
val statusSubscription = AtomicReference<Disposable>()
|
||||||
|
|
||||||
@@ -162,7 +157,8 @@ class GrpcUpstreams(
|
|||||||
lock.withLock {
|
lock.withLock {
|
||||||
val current = known[chain]
|
val current = known[chain]
|
||||||
return if (current == null) {
|
return if (current == null) {
|
||||||
val created = EthereumGrpcUpstream(id, chain, client!!, objectMapper, grpcTransport!!.copyForChain(chain))
|
val rpcClient = JsonRpcGrpcClient(client!!, chain, objectMapper)
|
||||||
|
val created = EthereumGrpcUpstream(id, chain, client!!, objectMapper, rpcClient)
|
||||||
created.timeout = this.timeout
|
created.timeout = this.timeout
|
||||||
known[chain] = created
|
known[chain] = created
|
||||||
created.start()
|
created.start()
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2020 EmeraldPay, Inc
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package io.emeraldpay.dshackle.upstream.rpcclient
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
|
import com.google.protobuf.ByteString
|
||||||
|
import io.emeraldpay.api.proto.BlockchainOuterClass
|
||||||
|
import io.emeraldpay.api.proto.ReactorBlockchainGrpc
|
||||||
|
import io.emeraldpay.dshackle.reader.Reader
|
||||||
|
import io.emeraldpay.dshackle.upstream.Selector
|
||||||
|
import io.emeraldpay.grpc.Chain
|
||||||
|
import io.grpc.Channel
|
||||||
|
import io.infinitape.etherjar.rpc.RpcException
|
||||||
|
import io.infinitape.etherjar.rpc.RpcResponseError
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import reactor.core.publisher.Mono
|
||||||
|
|
||||||
|
class JsonRpcGrpcClient(
|
||||||
|
private val stub: ReactorBlockchainGrpc.ReactorBlockchainStub,
|
||||||
|
private val chain: Chain,
|
||||||
|
private val objectMapper: ObjectMapper
|
||||||
|
) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val log = LoggerFactory.getLogger(JsonRpcGrpcClient::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun forSelector(matcher: Selector.Matcher): Reader<JsonRpcRequest, JsonRpcResponse> {
|
||||||
|
return Executor(stub, chain, matcher, objectMapper)
|
||||||
|
}
|
||||||
|
|
||||||
|
class Executor(
|
||||||
|
private val stub: ReactorBlockchainGrpc.ReactorBlockchainStub,
|
||||||
|
private val chain: Chain,
|
||||||
|
private val matcher: Selector.Matcher,
|
||||||
|
private val objectMapper: ObjectMapper
|
||||||
|
) : Reader<JsonRpcRequest, JsonRpcResponse> {
|
||||||
|
|
||||||
|
private val parser = JsonRpcParser()
|
||||||
|
|
||||||
|
override fun read(key: JsonRpcRequest): Mono<JsonRpcResponse> {
|
||||||
|
val req = BlockchainOuterClass.NativeCallRequest.newBuilder()
|
||||||
|
.setChainValue(chain.id)
|
||||||
|
|
||||||
|
if (matcher != Selector.empty) {
|
||||||
|
Selector.extractLabels(matcher)?.asProto().let {
|
||||||
|
req.setSelector(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BlockchainOuterClass.NativeCallItem.newBuilder()
|
||||||
|
.setId(1)
|
||||||
|
.setMethod(key.method)
|
||||||
|
.setPayload(ByteString.copyFrom(objectMapper.writeValueAsBytes(key.params)))
|
||||||
|
.build().let {
|
||||||
|
req.addItems(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
return stub.nativeCall(req.build())
|
||||||
|
.single()
|
||||||
|
.flatMap { resp ->
|
||||||
|
if (resp.succeed) {
|
||||||
|
val bytes = resp.payload.toByteArray()
|
||||||
|
Mono.just(JsonRpcResponse(bytes, null))
|
||||||
|
} else {
|
||||||
|
Mono.error(RpcException(RpcResponseError.CODE_UPSTREAM_CONNECTION_ERROR, resp.errorMessage))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -13,27 +13,41 @@
|
|||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package io.emeraldpay.dshackle.upstream.bitcoin
|
package io.emeraldpay.dshackle.upstream.rpcclient
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
import io.emeraldpay.dshackle.config.AuthConfig
|
import io.emeraldpay.dshackle.config.AuthConfig
|
||||||
|
import io.emeraldpay.dshackle.reader.Reader
|
||||||
import io.netty.buffer.Unpooled
|
import io.netty.buffer.Unpooled
|
||||||
import io.netty.handler.codec.http.HttpHeaderNames
|
import io.netty.handler.codec.http.HttpHeaderNames
|
||||||
import io.netty.handler.codec.http.HttpHeaders
|
import io.netty.handler.codec.http.HttpHeaders
|
||||||
|
import io.netty.handler.ssl.SslContextBuilder
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import reactor.core.publisher.Mono
|
import reactor.core.publisher.Mono
|
||||||
import reactor.netty.http.client.HttpClient
|
import reactor.netty.http.client.HttpClient
|
||||||
|
import reactor.netty.tcp.SslProvider
|
||||||
|
import java.io.ByteArrayInputStream
|
||||||
|
import java.security.KeyStore
|
||||||
|
import java.security.cert.CertificateFactory
|
||||||
|
import java.security.cert.X509Certificate
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.function.Consumer
|
import java.util.function.Consumer
|
||||||
|
|
||||||
open class BitcoinRpcClient(
|
/**
|
||||||
|
* JSON RPC client
|
||||||
|
*/
|
||||||
|
class JsonRpcHttpClient(
|
||||||
private val target: String,
|
private val target: String,
|
||||||
basicAuth: AuthConfig.ClientBasicAuth?
|
private val objectMapper: ObjectMapper,
|
||||||
) {
|
basicAuth: AuthConfig.ClientBasicAuth? = null,
|
||||||
|
tlsCAAuth: ByteArray? = null
|
||||||
|
) : Reader<JsonRpcRequest, JsonRpcResponse> {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val log = LoggerFactory.getLogger(BitcoinRpcClient::class.java)
|
private val log = LoggerFactory.getLogger(JsonRpcHttpClient::class.java)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val parser = JsonRpcParser()
|
||||||
private val httpClient: HttpClient
|
private val httpClient: HttpClient
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@@ -43,14 +57,27 @@ open class BitcoinRpcClient(
|
|||||||
h.add(HttpHeaderNames.CONTENT_TYPE, "application/json")
|
h.add(HttpHeaderNames.CONTENT_TYPE, "application/json")
|
||||||
}
|
}
|
||||||
|
|
||||||
basicAuth?.let { basicAuth ->
|
basicAuth?.let { auth ->
|
||||||
val authString: String = basicAuth.username + ":" + basicAuth.password
|
val authString: String = auth.username + ":" + auth.password
|
||||||
val authBase64 = Base64.getEncoder().encodeToString(authString.toByteArray())
|
val authBase64 = Base64.getEncoder().encodeToString(authString.toByteArray())
|
||||||
val auth = "Basic $authBase64"
|
val encodedAuth = "Basic $authBase64"
|
||||||
val headers = Consumer { h: HttpHeaders -> h.add(HttpHeaderNames.AUTHORIZATION, auth) }
|
val headers = Consumer { h: HttpHeaders -> h.add(HttpHeaderNames.AUTHORIZATION, encodedAuth) }
|
||||||
build = build.headers(headers)
|
build = build.headers(headers)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tlsCAAuth?.let { auth ->
|
||||||
|
val cf = CertificateFactory.getInstance("X.509")
|
||||||
|
val cert = cf.generateCertificate(ByteArrayInputStream(auth)) as X509Certificate
|
||||||
|
val ks = KeyStore.getInstance(KeyStore.getDefaultType())
|
||||||
|
ks.load(null, "".toCharArray())
|
||||||
|
ks.setCertificateEntry("server", cert)
|
||||||
|
val sslContext = SslContextBuilder.forClient().trustManager(cert).build()
|
||||||
|
|
||||||
|
build.secure { spec ->
|
||||||
|
spec.sslContext(sslContext)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.httpClient = build
|
this.httpClient = build
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,5 +92,10 @@ open class BitcoinRpcClient(
|
|||||||
.asByteArray()
|
.asByteArray()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun read(key: JsonRpcRequest): Mono<JsonRpcResponse> {
|
||||||
|
return Mono.just(key)
|
||||||
|
.map { it.toJson(objectMapper) }
|
||||||
|
.flatMap(this@JsonRpcHttpClient::execute)
|
||||||
|
.map(parser::parse)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2020 EmeraldPay, Inc
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package io.emeraldpay.dshackle.upstream.rpcclient
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.JsonFactory
|
||||||
|
import com.fasterxml.jackson.core.JsonParser
|
||||||
|
import com.fasterxml.jackson.core.JsonToken
|
||||||
|
import io.infinitape.etherjar.rpc.RpcResponseError
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
|
||||||
|
class JsonRpcParser() {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val log = LoggerFactory.getLogger(JsonRpcParser::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val jsonFactory = JsonFactory()
|
||||||
|
|
||||||
|
fun parse(json: ByteArray): JsonRpcResponse {
|
||||||
|
val parser: JsonParser = jsonFactory.createParser(json)
|
||||||
|
parser.nextToken()
|
||||||
|
if (parser.currentToken != JsonToken.START_OBJECT) {
|
||||||
|
return JsonRpcResponse(null, JsonRpcResponse.ResponseError(RpcResponseError.CODE_UPSTREAM_INVALID_RESPONSE, "Invalid JSON"))
|
||||||
|
}
|
||||||
|
var nullResponse: JsonRpcResponse? = null
|
||||||
|
while (parser.nextToken() != JsonToken.END_OBJECT) {
|
||||||
|
val field = parser.currentName
|
||||||
|
if (field == "jsonrpc" || field == "id") {
|
||||||
|
if (!parser.nextToken().isScalarValue) {
|
||||||
|
return JsonRpcResponse(null, JsonRpcResponse.ResponseError(RpcResponseError.CODE_UPSTREAM_INVALID_RESPONSE, "Invalid JSON (id/type)"))
|
||||||
|
}
|
||||||
|
// just skip the field
|
||||||
|
} else if (field == "result") {
|
||||||
|
val value = parser.nextToken()
|
||||||
|
val start = parser.tokenLocation
|
||||||
|
if (value.isScalarValue) {
|
||||||
|
val text = parser.text
|
||||||
|
if (value == JsonToken.VALUE_STRING) {
|
||||||
|
return JsonRpcResponse(("\"" + text + "\"").toByteArray(), null)
|
||||||
|
} else if (value == JsonToken.VALUE_NULL) {
|
||||||
|
//if null we should check if error is present
|
||||||
|
nullResponse = JsonRpcResponse(text.toByteArray(), null)
|
||||||
|
} else {
|
||||||
|
return JsonRpcResponse(text.toByteArray(), null)
|
||||||
|
}
|
||||||
|
} else if (value == JsonToken.START_OBJECT || value == JsonToken.START_ARRAY) {
|
||||||
|
parser.skipChildren()
|
||||||
|
val end = parser.currentLocation.byteOffset.toInt()
|
||||||
|
val copy = ByteArray((end - start.byteOffset).toInt())
|
||||||
|
System.arraycopy(json, start.byteOffset.toInt(), copy, 0, copy.size)
|
||||||
|
return JsonRpcResponse(copy, null)
|
||||||
|
}
|
||||||
|
} else if (field == "error") {
|
||||||
|
val err = readError(parser)
|
||||||
|
if (err != null) {
|
||||||
|
return JsonRpcResponse(null, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (nullResponse != null) {
|
||||||
|
return nullResponse
|
||||||
|
}
|
||||||
|
return JsonRpcResponse(null, JsonRpcResponse.ResponseError(RpcResponseError.CODE_UPSTREAM_INVALID_RESPONSE, "Invalid JSON structure"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun readError(parser: JsonParser): JsonRpcResponse.ResponseError? {
|
||||||
|
var code = 0
|
||||||
|
var message = ""
|
||||||
|
|
||||||
|
while (parser.nextToken() != JsonToken.END_OBJECT) {
|
||||||
|
if (parser.currentToken() == JsonToken.VALUE_NULL) {
|
||||||
|
// error is just null
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
val field = parser.currentName()
|
||||||
|
if (field == "code" && parser.currentToken == JsonToken.VALUE_NUMBER_INT) {
|
||||||
|
code = parser.intValue
|
||||||
|
} else if (field == "message" && parser.currentToken == JsonToken.VALUE_STRING) {
|
||||||
|
message = parser.valueAsString
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return JsonRpcResponse.ResponseError(code, message)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2020 EmeraldPay, Inc
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package io.emeraldpay.dshackle.upstream.rpcclient
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
|
|
||||||
|
class JsonRpcRequest(
|
||||||
|
val method: String,
|
||||||
|
val params: List<Any>
|
||||||
|
) {
|
||||||
|
|
||||||
|
fun toJson(objectMapper: ObjectMapper): ByteArray {
|
||||||
|
val json = mapOf(
|
||||||
|
"jsonrpc" to "2.0",
|
||||||
|
"id" to 1,
|
||||||
|
"method" to method,
|
||||||
|
"params" to params
|
||||||
|
)
|
||||||
|
return objectMapper.writeValueAsBytes(json)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (other !is JsonRpcRequest) return false
|
||||||
|
|
||||||
|
if (method != other.method) return false
|
||||||
|
if (params != other.params) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = method.hashCode()
|
||||||
|
result = 31 * result + params.hashCode()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2020 EmeraldPay, Inc
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package io.emeraldpay.dshackle.upstream.rpcclient
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.JsonGenerator
|
||||||
|
import com.fasterxml.jackson.databind.JsonSerializer
|
||||||
|
import com.fasterxml.jackson.databind.SerializerProvider
|
||||||
|
import io.infinitape.etherjar.rpc.RpcException
|
||||||
|
import reactor.core.publisher.Mono
|
||||||
|
|
||||||
|
class JsonRpcResponse(
|
||||||
|
private val result: ByteArray?,
|
||||||
|
val error: ResponseError?
|
||||||
|
) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val NULL_VALUE = "null".toByteArray()
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun ok(value: ByteArray): JsonRpcResponse {
|
||||||
|
return JsonRpcResponse(value, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun ok(value: String): JsonRpcResponse {
|
||||||
|
return JsonRpcResponse(value.toByteArray(), null)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun error(code: Int, msg: String): JsonRpcResponse {
|
||||||
|
return JsonRpcResponse(null, ResponseError(code, msg))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun hasResult(): Boolean {
|
||||||
|
return result != null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun hasError(): Boolean {
|
||||||
|
return error != null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isNull(): Boolean {
|
||||||
|
return result != null && NULL_VALUE.contentEquals(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getResult(): ByteArray {
|
||||||
|
return result ?: ByteArray(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getResultAsRawString(): String {
|
||||||
|
return String(getResult())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getResultAsProcessedString(): String {
|
||||||
|
val str = getResultAsRawString()
|
||||||
|
if (str.startsWith("\"") && str.endsWith("\"")) {
|
||||||
|
return str.substring(1, str.length - 1)
|
||||||
|
}
|
||||||
|
throw IllegalStateException("Not as JS string")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun requireResult(): Mono<ByteArray> {
|
||||||
|
return if (error != null) {
|
||||||
|
Mono.error(error.asException())
|
||||||
|
} else {
|
||||||
|
Mono.just(getResult())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun requireStringResult(): Mono<String> {
|
||||||
|
return if (error != null) {
|
||||||
|
Mono.error(error.asException())
|
||||||
|
} else {
|
||||||
|
Mono.just(getResultAsProcessedString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (other !is JsonRpcResponse) return false
|
||||||
|
|
||||||
|
if (result != null) {
|
||||||
|
if (other.result == null) return false
|
||||||
|
if (!result.contentEquals(other.result)) return false
|
||||||
|
} else if (other.result != null) return false
|
||||||
|
if (error != other.error) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result1 = result?.contentHashCode() ?: 0
|
||||||
|
result1 = 31 * result1 + (error?.hashCode() ?: 0)
|
||||||
|
return result1
|
||||||
|
}
|
||||||
|
|
||||||
|
class ResponseError(val code: Int, val message: String) {
|
||||||
|
fun asException(): RpcException {
|
||||||
|
return RpcException(code, message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ResponseJsonSerializer : JsonSerializer<JsonRpcResponse>() {
|
||||||
|
override fun serialize(value: JsonRpcResponse, gen: JsonGenerator, serializers: SerializerProvider) {
|
||||||
|
gen.writeStartObject()
|
||||||
|
gen.writeStringField("jsonrpc", "2.0")
|
||||||
|
gen.writeNumberField("id", 0)
|
||||||
|
if (value.error != null) {
|
||||||
|
gen.writeObjectFieldStart("error")
|
||||||
|
gen.writeNumberField("code", value.error.code)
|
||||||
|
gen.writeStringField("message", value.error.message)
|
||||||
|
gen.writeEndObject()
|
||||||
|
} else {
|
||||||
|
if (value.result == null) {
|
||||||
|
throw IllegalStateException("No result set")
|
||||||
|
}
|
||||||
|
gen.writeRawUTF8String(value.result, 0, value.result.size)
|
||||||
|
}
|
||||||
|
gen.writeEndObject()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -63,6 +63,7 @@ class BlocksRedisCacheSpec extends Specification {
|
|||||||
Instant.ofEpochSecond(10501050),
|
Instant.ofEpochSecond(10501050),
|
||||||
false,
|
false,
|
||||||
"test".bytes,
|
"test".bytes,
|
||||||
|
null,
|
||||||
[TxId.from(hash2), TxId.from(hash1)]
|
[TxId.from(hash2), TxId.from(hash1)]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -63,7 +63,8 @@ class TxRedisCacheSpec extends Specification {
|
|||||||
2000,
|
2000,
|
||||||
TxId.from(hash1),
|
TxId.from(hash1),
|
||||||
BlockId.from(hash2),
|
BlockId.from(hash2),
|
||||||
"test".bytes
|
"test".bytes,
|
||||||
|
null
|
||||||
)
|
)
|
||||||
when:
|
when:
|
||||||
def enc = cache.toProto(cont)
|
def enc = cache.toProto(cont)
|
||||||
|
|||||||
@@ -20,16 +20,16 @@ import io.emeraldpay.dshackle.test.TestingCommons
|
|||||||
import io.emeraldpay.dshackle.upstream.Head
|
import io.emeraldpay.dshackle.upstream.Head
|
||||||
import io.emeraldpay.dshackle.upstream.Upstream
|
import io.emeraldpay.dshackle.upstream.Upstream
|
||||||
import io.emeraldpay.dshackle.quorum.BroadcastQuorum
|
import io.emeraldpay.dshackle.quorum.BroadcastQuorum
|
||||||
|
import io.infinitape.etherjar.rpc.RpcException
|
||||||
import spock.lang.Specification
|
import spock.lang.Specification
|
||||||
|
|
||||||
class BroadcastQuorumSpec extends Specification {
|
class BroadcastQuorumSpec extends Specification {
|
||||||
|
|
||||||
def rpcConverted = TestingCommons.rpcConverter()
|
|
||||||
def objectMapper = TestingCommons.objectMapper()
|
def objectMapper = TestingCommons.objectMapper()
|
||||||
|
|
||||||
def "Resolved with first after 3 tries"() {
|
def "Resolved with first after 3 tries"() {
|
||||||
setup:
|
setup:
|
||||||
def q = Spy(new BroadcastQuorum(rpcConverted, 3))
|
def q = Spy(new BroadcastQuorum(objectMapper, 3))
|
||||||
def upstream1 = Stub(Upstream)
|
def upstream1 = Stub(Upstream)
|
||||||
def upstream2 = Stub(Upstream)
|
def upstream2 = Stub(Upstream)
|
||||||
def upstream3 = Stub(Upstream)
|
def upstream3 = Stub(Upstream)
|
||||||
@@ -40,28 +40,28 @@ class BroadcastQuorumSpec extends Specification {
|
|||||||
!q.isResolved()
|
!q.isResolved()
|
||||||
|
|
||||||
when:
|
when:
|
||||||
q.record(objectMapper.writeValueAsBytes([result: "0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c"]), upstream1)
|
q.record('"0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c"'.bytes, upstream1)
|
||||||
then:
|
then:
|
||||||
!q.isResolved()
|
!q.isResolved()
|
||||||
1 * q.recordValue(_, "0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c", _)
|
1 * q.recordValue(_, "0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c", _)
|
||||||
|
|
||||||
when:
|
when:
|
||||||
q.record(objectMapper.writeValueAsBytes([result: "0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c"]), upstream2)
|
q.record('"0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c"'.bytes, upstream2)
|
||||||
then:
|
then:
|
||||||
!q.isResolved()
|
!q.isResolved()
|
||||||
1 * q.recordValue(_, "0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c", _)
|
1 * q.recordValue(_, "0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c", _)
|
||||||
|
|
||||||
when:
|
when:
|
||||||
q.record(objectMapper.writeValueAsBytes([error: [message: "Nonce too low"]]), upstream3)
|
q.record(new RpcException(1, "Nonce too low"), upstream3)
|
||||||
then:
|
then:
|
||||||
1 * q.recordError(_, _, _)
|
1 * q.recordError(_, _, _)
|
||||||
q.isResolved()
|
q.isResolved()
|
||||||
objectMapper.readValue(q.result, Map) == [result: "0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c"]
|
objectMapper.readValue(q.result, Object) == "0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c"
|
||||||
}
|
}
|
||||||
|
|
||||||
def "Remembers first response"() {
|
def "Remembers first response"() {
|
||||||
setup:
|
setup:
|
||||||
def q = Spy(new BroadcastQuorum(rpcConverted, 3))
|
def q = Spy(new BroadcastQuorum(objectMapper, 3))
|
||||||
def upstream1 = Stub(Upstream)
|
def upstream1 = Stub(Upstream)
|
||||||
def upstream2 = Stub(Upstream)
|
def upstream2 = Stub(Upstream)
|
||||||
def upstream3 = Stub(Upstream)
|
def upstream3 = Stub(Upstream)
|
||||||
@@ -72,22 +72,22 @@ class BroadcastQuorumSpec extends Specification {
|
|||||||
!q.isResolved()
|
!q.isResolved()
|
||||||
|
|
||||||
when:
|
when:
|
||||||
q.record(objectMapper.writeValueAsBytes([error: [message: "Internal error"]]), upstream1)
|
q.record(new RpcException(1, "Internal error"), upstream1)
|
||||||
then:
|
then:
|
||||||
!q.isResolved()
|
!q.isResolved()
|
||||||
1 * q.recordError(_, _, _)
|
1 * q.recordError(_, _, _)
|
||||||
|
|
||||||
when:
|
when:
|
||||||
q.record(objectMapper.writeValueAsBytes([result: "0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c"]), upstream2)
|
q.record('"0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c"'.bytes, upstream2)
|
||||||
then:
|
then:
|
||||||
!q.isResolved()
|
!q.isResolved()
|
||||||
1 * q.recordValue(_, "0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c", _)
|
1 * q.recordValue(_, "0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c", _)
|
||||||
|
|
||||||
when:
|
when:
|
||||||
q.record(objectMapper.writeValueAsBytes([error: [message: "Nonce too low"]]), upstream3)
|
q.record(new RpcException(1, "Nonce too low"), upstream3)
|
||||||
then:
|
then:
|
||||||
1 * q.recordError(_, _, _)
|
1 * q.recordError(_, _, _)
|
||||||
q.isResolved()
|
q.isResolved()
|
||||||
objectMapper.readValue(q.result, Map) == [result: "0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c"]
|
objectMapper.readValue(q.result, Object) == "0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,133 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2020 EmeraldPay, Inc
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package io.emeraldpay.dshackle.quorum
|
||||||
|
|
||||||
|
import io.emeraldpay.dshackle.test.TestingCommons
|
||||||
|
import io.emeraldpay.dshackle.upstream.Head
|
||||||
|
import io.emeraldpay.dshackle.upstream.Upstream
|
||||||
|
import io.infinitape.etherjar.rpc.RpcException
|
||||||
|
import spock.lang.Specification
|
||||||
|
|
||||||
|
class NonEmptyQuorumSpec extends Specification {
|
||||||
|
|
||||||
|
def "Fail if too many errors"() {
|
||||||
|
setup:
|
||||||
|
def q = Spy(new NonEmptyQuorum(TestingCommons.objectMapper(), 3))
|
||||||
|
def upstream1 = Stub(Upstream)
|
||||||
|
def upstream2 = Stub(Upstream)
|
||||||
|
def upstream3 = Stub(Upstream)
|
||||||
|
|
||||||
|
when:
|
||||||
|
q.init(Stub(Head))
|
||||||
|
then:
|
||||||
|
!q.isResolved()
|
||||||
|
!q.isFailed()
|
||||||
|
|
||||||
|
when:
|
||||||
|
q.record(new RpcException(1, "Internal"), upstream1)
|
||||||
|
then:
|
||||||
|
!q.isResolved()
|
||||||
|
!q.isFailed()
|
||||||
|
|
||||||
|
when:
|
||||||
|
q.record(new RpcException(1, "Internal"), upstream2)
|
||||||
|
then:
|
||||||
|
!q.isResolved()
|
||||||
|
!q.isFailed()
|
||||||
|
|
||||||
|
when:
|
||||||
|
q.record(new RpcException(1, "Internal"), upstream3)
|
||||||
|
then:
|
||||||
|
q.isFailed()
|
||||||
|
!q.isResolved()
|
||||||
|
}
|
||||||
|
|
||||||
|
def "Fail first if not error"() {
|
||||||
|
setup:
|
||||||
|
def q = Spy(new NonEmptyQuorum(TestingCommons.objectMapper(), 3))
|
||||||
|
def upstream1 = Stub(Upstream)
|
||||||
|
def upstream2 = Stub(Upstream)
|
||||||
|
def upstream3 = Stub(Upstream)
|
||||||
|
|
||||||
|
when:
|
||||||
|
q.init(Stub(Head))
|
||||||
|
then:
|
||||||
|
!q.isResolved()
|
||||||
|
!q.isFailed()
|
||||||
|
|
||||||
|
when:
|
||||||
|
q.record('"0x11"'.bytes, upstream1)
|
||||||
|
then:
|
||||||
|
q.isResolved()
|
||||||
|
!q.isFailed()
|
||||||
|
}
|
||||||
|
|
||||||
|
def "Fail second if first is error"() {
|
||||||
|
setup:
|
||||||
|
def q = Spy(new NonEmptyQuorum(TestingCommons.objectMapper(), 3))
|
||||||
|
def upstream1 = Stub(Upstream)
|
||||||
|
def upstream2 = Stub(Upstream)
|
||||||
|
def upstream3 = Stub(Upstream)
|
||||||
|
|
||||||
|
when:
|
||||||
|
q.init(Stub(Head))
|
||||||
|
then:
|
||||||
|
!q.isResolved()
|
||||||
|
!q.isFailed()
|
||||||
|
|
||||||
|
when:
|
||||||
|
q.record(new RpcException(1, "Internal"), upstream1)
|
||||||
|
then:
|
||||||
|
!q.isFailed()
|
||||||
|
!q.isResolved()
|
||||||
|
|
||||||
|
|
||||||
|
when:
|
||||||
|
q.record('"0x11"'.bytes, upstream2)
|
||||||
|
then:
|
||||||
|
q.isResolved()
|
||||||
|
!q.isFailed()
|
||||||
|
}
|
||||||
|
|
||||||
|
def "Fail second if first is null"() {
|
||||||
|
setup:
|
||||||
|
def q = Spy(new NonEmptyQuorum(TestingCommons.objectMapper(), 3))
|
||||||
|
def upstream1 = Stub(Upstream)
|
||||||
|
def upstream2 = Stub(Upstream)
|
||||||
|
def upstream3 = Stub(Upstream)
|
||||||
|
|
||||||
|
when:
|
||||||
|
q.init(Stub(Head))
|
||||||
|
then:
|
||||||
|
!q.isResolved()
|
||||||
|
!q.isFailed()
|
||||||
|
|
||||||
|
when:
|
||||||
|
q.record('null'.bytes, upstream2)
|
||||||
|
then:
|
||||||
|
!q.isFailed()
|
||||||
|
!q.isResolved()
|
||||||
|
|
||||||
|
|
||||||
|
when:
|
||||||
|
q.record('"0x11"'.bytes, upstream2)
|
||||||
|
then:
|
||||||
|
q.isResolved()
|
||||||
|
!q.isFailed()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -20,16 +20,16 @@ import io.emeraldpay.dshackle.test.TestingCommons
|
|||||||
import io.emeraldpay.dshackle.upstream.Head
|
import io.emeraldpay.dshackle.upstream.Head
|
||||||
import io.emeraldpay.dshackle.upstream.Upstream
|
import io.emeraldpay.dshackle.upstream.Upstream
|
||||||
import io.emeraldpay.dshackle.quorum.NonceQuorum
|
import io.emeraldpay.dshackle.quorum.NonceQuorum
|
||||||
|
import io.infinitape.etherjar.rpc.RpcException
|
||||||
import spock.lang.Specification
|
import spock.lang.Specification
|
||||||
|
|
||||||
class NonceQuorumSpec extends Specification {
|
class NonceQuorumSpec extends Specification {
|
||||||
|
|
||||||
def rpcConverted = TestingCommons.rpcConverter()
|
|
||||||
def objectMapper = TestingCommons.objectMapper()
|
def objectMapper = TestingCommons.objectMapper()
|
||||||
|
|
||||||
def "Gets max value"() {
|
def "Gets max value"() {
|
||||||
setup:
|
setup:
|
||||||
def q = Spy(new NonceQuorum(rpcConverted, 3))
|
def q = Spy(new NonceQuorum(objectMapper, 3))
|
||||||
def upstream1 = Stub(Upstream)
|
def upstream1 = Stub(Upstream)
|
||||||
def upstream2 = Stub(Upstream)
|
def upstream2 = Stub(Upstream)
|
||||||
def upstream3 = Stub(Upstream)
|
def upstream3 = Stub(Upstream)
|
||||||
@@ -40,28 +40,28 @@ class NonceQuorumSpec extends Specification {
|
|||||||
!q.isResolved()
|
!q.isResolved()
|
||||||
|
|
||||||
when:
|
when:
|
||||||
q.record(objectMapper.writeValueAsBytes([result: "0x10"]), upstream1)
|
q.record('"0x10"'.bytes, upstream1)
|
||||||
then:
|
then:
|
||||||
!q.isResolved()
|
!q.isResolved()
|
||||||
1 * q.recordValue(_, "0x10", _)
|
1 * q.recordValue(_, "0x10", _)
|
||||||
|
|
||||||
when:
|
when:
|
||||||
q.record(objectMapper.writeValueAsBytes([result: "0x11"]), upstream2)
|
q.record('"0x11"'.bytes, upstream2)
|
||||||
then:
|
then:
|
||||||
!q.isResolved()
|
!q.isResolved()
|
||||||
1 * q.recordValue(_, "0x11", _)
|
1 * q.recordValue(_, "0x11", _)
|
||||||
|
|
||||||
when:
|
when:
|
||||||
q.record(objectMapper.writeValueAsBytes([result: "0x10"]), upstream3)
|
q.record('"0x10"'.bytes, upstream3)
|
||||||
then:
|
then:
|
||||||
1 * q.recordValue(_, "0x10", _)
|
1 * q.recordValue(_, "0x10", _)
|
||||||
q.isResolved()
|
q.isResolved()
|
||||||
objectMapper.readValue(q.result, Map) == [result: "0x11"]
|
objectMapper.readValue(q.result, Object) == "0x11"
|
||||||
}
|
}
|
||||||
|
|
||||||
def "Ignores errors"() {
|
def "Ignores errors"() {
|
||||||
setup:
|
setup:
|
||||||
def q = Spy(new NonceQuorum(rpcConverted, 3))
|
def q = Spy(new NonceQuorum(objectMapper, 3))
|
||||||
def upstream1 = Stub(Upstream)
|
def upstream1 = Stub(Upstream)
|
||||||
def upstream2 = Stub(Upstream)
|
def upstream2 = Stub(Upstream)
|
||||||
def upstream3 = Stub(Upstream)
|
def upstream3 = Stub(Upstream)
|
||||||
@@ -72,28 +72,60 @@ class NonceQuorumSpec extends Specification {
|
|||||||
!q.isResolved()
|
!q.isResolved()
|
||||||
|
|
||||||
when:
|
when:
|
||||||
q.record(objectMapper.writeValueAsBytes([error: [error: "Internal"]]), upstream1)
|
q.record(new RpcException(1, "Internal"), upstream1)
|
||||||
then:
|
then:
|
||||||
!q.isResolved()
|
!q.isResolved()
|
||||||
1 * q.recordError(_, _, _)
|
1 * q.recordError(_, _, _)
|
||||||
|
|
||||||
when:
|
when:
|
||||||
q.record(objectMapper.writeValueAsBytes([result: "0x11"]), upstream2)
|
q.record('"0x11"'.bytes, upstream2)
|
||||||
then:
|
then:
|
||||||
!q.isResolved()
|
!q.isResolved()
|
||||||
1 * q.recordValue(_, "0x11", _)
|
1 * q.recordValue(_, "0x11", _)
|
||||||
|
|
||||||
when:
|
when:
|
||||||
q.record(objectMapper.writeValueAsBytes([result: "0x10"]), upstream3)
|
q.record('"0x10"'.bytes, upstream3)
|
||||||
then:
|
then:
|
||||||
1 * q.recordValue(_, "0x10", _)
|
1 * q.recordValue(_, "0x10", _)
|
||||||
!q.isResolved()
|
!q.isResolved()
|
||||||
|
|
||||||
when:
|
when:
|
||||||
q.record(objectMapper.writeValueAsBytes([result: "0x11"]), upstream1)
|
q.record('"0x11"'.bytes, upstream1)
|
||||||
then:
|
then:
|
||||||
1 * q.recordValue(_, "0x11", _)
|
1 * q.recordValue(_, "0x11", _)
|
||||||
q.isResolved()
|
q.isResolved()
|
||||||
objectMapper.readValue(q.result, Map) == [result: "0x11"]
|
objectMapper.readValue(q.result, Object) == "0x11"
|
||||||
|
}
|
||||||
|
|
||||||
|
def "Fail if too many errors"() {
|
||||||
|
setup:
|
||||||
|
def q = Spy(new NonceQuorum(objectMapper, 3))
|
||||||
|
def upstream1 = Stub(Upstream)
|
||||||
|
def upstream2 = Stub(Upstream)
|
||||||
|
def upstream3 = Stub(Upstream)
|
||||||
|
|
||||||
|
when:
|
||||||
|
q.init(Stub(Head))
|
||||||
|
then:
|
||||||
|
!q.isResolved()
|
||||||
|
!q.isFailed()
|
||||||
|
|
||||||
|
when:
|
||||||
|
q.record(new RpcException(1, "Internal"), upstream1)
|
||||||
|
then:
|
||||||
|
!q.isResolved()
|
||||||
|
!q.isFailed()
|
||||||
|
|
||||||
|
when:
|
||||||
|
q.record(new RpcException(1, "Internal"), upstream2)
|
||||||
|
then:
|
||||||
|
!q.isResolved()
|
||||||
|
!q.isFailed()
|
||||||
|
|
||||||
|
when:
|
||||||
|
q.record(new RpcException(1, "Internal"), upstream3)
|
||||||
|
then:
|
||||||
|
q.isFailed()
|
||||||
|
!q.isResolved()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,206 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2020 EmeraldPay, Inc
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package io.emeraldpay.dshackle.quorum
|
||||||
|
|
||||||
|
import io.emeraldpay.dshackle.reader.Reader
|
||||||
|
import io.emeraldpay.dshackle.test.TestingCommons
|
||||||
|
import io.emeraldpay.dshackle.upstream.FilteredApis
|
||||||
|
import io.emeraldpay.dshackle.upstream.Selector
|
||||||
|
import io.emeraldpay.dshackle.upstream.Upstream
|
||||||
|
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
|
||||||
|
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
|
||||||
|
import reactor.core.publisher.Mono
|
||||||
|
import reactor.test.StepVerifier
|
||||||
|
import spock.lang.Specification
|
||||||
|
|
||||||
|
import java.time.Duration
|
||||||
|
|
||||||
|
class QuorumRpcReaderSpec extends Specification {
|
||||||
|
|
||||||
|
def "always-quorum - get the result if ok"() {
|
||||||
|
setup:
|
||||||
|
def up = Mock(Upstream) {
|
||||||
|
_ * isAvailable() >> true
|
||||||
|
1 * getApi() >> Mock(Reader) {
|
||||||
|
1 * read(new JsonRpcRequest("eth_test", [])) >> Mono.just(JsonRpcResponse.ok("1"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
def apis = new FilteredApis(
|
||||||
|
[up], Selector.empty
|
||||||
|
)
|
||||||
|
def reader = new QuorumRpcReader(apis, new AlwaysQuorum())
|
||||||
|
|
||||||
|
when:
|
||||||
|
def act = reader.read(new JsonRpcRequest("eth_test", []))
|
||||||
|
.map {
|
||||||
|
new String(it.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
then:
|
||||||
|
StepVerifier.create(act)
|
||||||
|
.expectNext("1")
|
||||||
|
.expectComplete()
|
||||||
|
.verify(Duration.ofSeconds(1))
|
||||||
|
}
|
||||||
|
|
||||||
|
def "always-quorum - retry upstream error"() {
|
||||||
|
setup:
|
||||||
|
def up = Mock(Upstream) {
|
||||||
|
_ * isAvailable() >> true
|
||||||
|
_ * getApi() >> Mock(Reader) {
|
||||||
|
2 * read(new JsonRpcRequest("eth_test", [])) >>> [
|
||||||
|
Mono.just(JsonRpcResponse.error(1, "test")),
|
||||||
|
Mono.just(JsonRpcResponse.ok("1"))
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
def apis = new FilteredApis(
|
||||||
|
[up], Selector.empty
|
||||||
|
)
|
||||||
|
def reader = new QuorumRpcReader(apis, new AlwaysQuorum())
|
||||||
|
|
||||||
|
when:
|
||||||
|
def act = reader.read(new JsonRpcRequest("eth_test", []))
|
||||||
|
.map {
|
||||||
|
new String(it.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
then:
|
||||||
|
StepVerifier.create(act)
|
||||||
|
.expectNext("1")
|
||||||
|
.expectComplete()
|
||||||
|
.verify(Duration.ofSeconds(1))
|
||||||
|
}
|
||||||
|
|
||||||
|
def "non-empty-quorum - get the second result if first is null"() {
|
||||||
|
setup:
|
||||||
|
def up = Mock(Upstream) {
|
||||||
|
_ * isAvailable() >> true
|
||||||
|
_ * getApi() >> Mock(Reader) {
|
||||||
|
2 * read(new JsonRpcRequest("eth_test", [])) >>> [
|
||||||
|
Mono.just(JsonRpcResponse.ok("null")),
|
||||||
|
Mono.just(JsonRpcResponse.ok("1"))
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
def apis = new FilteredApis(
|
||||||
|
[up], Selector.empty
|
||||||
|
)
|
||||||
|
def reader = new QuorumRpcReader(apis, new NonEmptyQuorum(TestingCommons.objectMapper(), 3))
|
||||||
|
|
||||||
|
when:
|
||||||
|
def act = reader.read(new JsonRpcRequest("eth_test", []))
|
||||||
|
.map {
|
||||||
|
new String(it.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
then:
|
||||||
|
StepVerifier.create(act)
|
||||||
|
.expectNext("1")
|
||||||
|
.expectComplete()
|
||||||
|
.verify(Duration.ofSeconds(1))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def "non-empty-quorum - get the second result if first is error"() {
|
||||||
|
setup:
|
||||||
|
def up = Mock(Upstream) {
|
||||||
|
_ * isAvailable() >> true
|
||||||
|
_ * getApi() >> Mock(Reader) {
|
||||||
|
2 * read(new JsonRpcRequest("eth_test", [])) >>> [
|
||||||
|
Mono.just(JsonRpcResponse.error(1, "test")),
|
||||||
|
Mono.just(JsonRpcResponse.ok("1"))
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
def apis = new FilteredApis(
|
||||||
|
[up], Selector.empty
|
||||||
|
)
|
||||||
|
def reader = new QuorumRpcReader(apis, new NonEmptyQuorum(TestingCommons.objectMapper(), 3))
|
||||||
|
|
||||||
|
when:
|
||||||
|
def act = reader.read(new JsonRpcRequest("eth_test", []))
|
||||||
|
.map {
|
||||||
|
new String(it.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
then:
|
||||||
|
StepVerifier.create(act)
|
||||||
|
.expectNext("1")
|
||||||
|
.expectComplete()
|
||||||
|
.verify(Duration.ofSeconds(1))
|
||||||
|
}
|
||||||
|
|
||||||
|
def "non-empty-quorum - get the third result if first two are not ok"() {
|
||||||
|
setup:
|
||||||
|
def up = Mock(Upstream) {
|
||||||
|
_ * isAvailable() >> true
|
||||||
|
_ * getApi() >> Mock(Reader) {
|
||||||
|
3 * read(new JsonRpcRequest("eth_test", [])) >>> [
|
||||||
|
Mono.just(JsonRpcResponse.ok("null")),
|
||||||
|
Mono.just(JsonRpcResponse.error(1, "test")),
|
||||||
|
Mono.just(JsonRpcResponse.ok("1"))
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
def apis = new FilteredApis(
|
||||||
|
[up], Selector.empty
|
||||||
|
)
|
||||||
|
def reader = new QuorumRpcReader(apis, new NonEmptyQuorum(TestingCommons.objectMapper(), 3))
|
||||||
|
|
||||||
|
when:
|
||||||
|
def act = reader.read(new JsonRpcRequest("eth_test", []))
|
||||||
|
.map {
|
||||||
|
new String(it.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
then:
|
||||||
|
StepVerifier.create(act)
|
||||||
|
.expectNext("1")
|
||||||
|
.expectComplete()
|
||||||
|
.verify(Duration.ofSeconds(1))
|
||||||
|
}
|
||||||
|
|
||||||
|
def "non-empty-quorum - no result if all failed"() {
|
||||||
|
setup:
|
||||||
|
def up = Mock(Upstream) {
|
||||||
|
_ * isAvailable() >> true
|
||||||
|
_ * getApi() >> Mock(Reader) {
|
||||||
|
3 * read(new JsonRpcRequest("eth_test", [])) >>> [
|
||||||
|
Mono.just(JsonRpcResponse.ok("null")),
|
||||||
|
Mono.just(JsonRpcResponse.error(1, "test")),
|
||||||
|
Mono.just(JsonRpcResponse.ok("null"))
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
def apis = new FilteredApis(
|
||||||
|
[up], Selector.empty
|
||||||
|
)
|
||||||
|
def reader = new QuorumRpcReader(apis, new NonEmptyQuorum(TestingCommons.objectMapper(), 3))
|
||||||
|
|
||||||
|
when:
|
||||||
|
def act = reader.read(new JsonRpcRequest("eth_test", []))
|
||||||
|
.map {
|
||||||
|
new String(it.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
then:
|
||||||
|
StepVerifier.create(act)
|
||||||
|
.expectComplete()
|
||||||
|
.verify(Duration.ofSeconds(1))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2020 EmeraldPay, Inc
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package io.emeraldpay.dshackle.quorum
|
||||||
|
|
||||||
|
import io.emeraldpay.dshackle.test.TestingCommons
|
||||||
|
import io.emeraldpay.dshackle.upstream.Head
|
||||||
|
import io.emeraldpay.dshackle.upstream.Upstream
|
||||||
|
import org.jetbrains.annotations.NotNull
|
||||||
|
import org.jetbrains.annotations.Nullable
|
||||||
|
import spock.lang.Specification
|
||||||
|
|
||||||
|
class ValueAwareQuorumSpec extends Specification {
|
||||||
|
|
||||||
|
def "Extract null"() {
|
||||||
|
setup:
|
||||||
|
def quorum = new ValueAwareQuorumImpl()
|
||||||
|
when:
|
||||||
|
def act = quorum.extractValue("null".bytes, Object)
|
||||||
|
then:
|
||||||
|
act == null
|
||||||
|
}
|
||||||
|
|
||||||
|
def "Extract string"() {
|
||||||
|
setup:
|
||||||
|
def quorum = new ValueAwareQuorumImpl()
|
||||||
|
when:
|
||||||
|
def act = quorum.extractValue("\"foo\"".bytes, Object)
|
||||||
|
then:
|
||||||
|
act == "foo"
|
||||||
|
}
|
||||||
|
|
||||||
|
def "Extract number"() {
|
||||||
|
setup:
|
||||||
|
def quorum = new ValueAwareQuorumImpl()
|
||||||
|
when:
|
||||||
|
def act = quorum.extractValue("100".bytes, Object)
|
||||||
|
then:
|
||||||
|
act == 100
|
||||||
|
}
|
||||||
|
|
||||||
|
def "Extract map"() {
|
||||||
|
setup:
|
||||||
|
def quorum = new ValueAwareQuorumImpl()
|
||||||
|
when:
|
||||||
|
def act = quorum.extractValue("{\"foo\": 1}".bytes, Object)
|
||||||
|
then:
|
||||||
|
act == [foo: 1]
|
||||||
|
}
|
||||||
|
|
||||||
|
class ValueAwareQuorumImpl extends ValueAwareQuorum {
|
||||||
|
ValueAwareQuorumImpl() {
|
||||||
|
super(TestingCommons.objectMapper(), Object)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
void recordValue(@NotNull byte[] response, @Nullable Object responseValue, @NotNull Upstream upstream) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
void recordError(@Nullable byte[] response, @Nullable String errorMessage, @NotNull Upstream upstream) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
void init(@NotNull Head head) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
boolean isResolved() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
byte[] getResult() {
|
||||||
|
return new byte[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
boolean isFailed() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,21 +19,25 @@ package io.emeraldpay.dshackle.rpc
|
|||||||
|
|
||||||
import io.emeraldpay.api.proto.BlockchainOuterClass
|
import io.emeraldpay.api.proto.BlockchainOuterClass
|
||||||
import io.emeraldpay.dshackle.quorum.BroadcastQuorum
|
import io.emeraldpay.dshackle.quorum.BroadcastQuorum
|
||||||
|
import io.emeraldpay.dshackle.quorum.QuorumReaderFactory
|
||||||
|
import io.emeraldpay.dshackle.quorum.QuorumRpcReader
|
||||||
|
import io.emeraldpay.dshackle.reader.Reader
|
||||||
import io.emeraldpay.dshackle.test.TestingCommons
|
import io.emeraldpay.dshackle.test.TestingCommons
|
||||||
import io.emeraldpay.dshackle.quorum.AlwaysQuorum
|
import io.emeraldpay.dshackle.quorum.AlwaysQuorum
|
||||||
import io.emeraldpay.dshackle.upstream.CachingEthereumApi
|
|
||||||
import io.emeraldpay.dshackle.upstream.ethereum.DirectEthereumApi
|
|
||||||
import io.emeraldpay.dshackle.quorum.NonEmptyQuorum
|
import io.emeraldpay.dshackle.quorum.NonEmptyQuorum
|
||||||
|
import io.emeraldpay.dshackle.upstream.Multistream
|
||||||
import io.emeraldpay.dshackle.upstream.Selector
|
import io.emeraldpay.dshackle.upstream.Selector
|
||||||
import io.emeraldpay.dshackle.upstream.Upstream
|
import io.emeraldpay.dshackle.upstream.MultistreamHolder
|
||||||
import io.emeraldpay.dshackle.upstream.Upstreams
|
import io.emeraldpay.dshackle.upstream.ethereum.NativeCallRouter
|
||||||
|
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
|
||||||
|
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
|
||||||
import io.emeraldpay.grpc.Chain
|
import io.emeraldpay.grpc.Chain
|
||||||
import io.infinitape.etherjar.rpc.ReactorRpcClient
|
import io.infinitape.etherjar.rpc.ReactorRpcClient
|
||||||
import io.infinitape.etherjar.rpc.RpcException
|
import io.infinitape.etherjar.rpc.RpcException
|
||||||
import io.infinitape.etherjar.rpc.RpcResponseError
|
import io.infinitape.etherjar.rpc.RpcResponseError
|
||||||
import io.infinitape.etherjar.rpc.RpcResponseException
|
|
||||||
import reactor.core.publisher.Mono
|
import reactor.core.publisher.Mono
|
||||||
import reactor.test.StepVerifier
|
import reactor.test.StepVerifier
|
||||||
|
import spock.lang.Ignore
|
||||||
import spock.lang.Specification
|
import spock.lang.Specification
|
||||||
|
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
@@ -43,140 +47,99 @@ class NativeCallSpec extends Specification {
|
|||||||
|
|
||||||
def objectMapper = TestingCommons.objectMapper()
|
def objectMapper = TestingCommons.objectMapper()
|
||||||
|
|
||||||
|
def "Tries router first"() {
|
||||||
|
def routedApi = Mock(Reader) {
|
||||||
|
1 * read(new JsonRpcRequest("eth_test", [])) >> Mono.just(new JsonRpcResponse("1".bytes, null))
|
||||||
|
}
|
||||||
|
def upstream = Mock(Multistream) {
|
||||||
|
1 * getRoutedApi(_) >> Mono.just(routedApi)
|
||||||
|
}
|
||||||
|
def upstreams = Stub(MultistreamHolder)
|
||||||
|
|
||||||
|
def nativeCall = new NativeCall(upstreams, TestingCommons.objectMapper())
|
||||||
|
def ctx = new NativeCall.CallContext<NativeCall.ParsedCallDetails>(
|
||||||
|
1, upstream, Selector.empty, new AlwaysQuorum(),
|
||||||
|
new NativeCall.ParsedCallDetails("eth_test", [])
|
||||||
|
)
|
||||||
|
|
||||||
|
when:
|
||||||
|
def act = nativeCall.fetch(ctx).block(Duration.ofSeconds(1))
|
||||||
|
then:
|
||||||
|
act.payload == "1".bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
def "Return error if router denied the requests"() {
|
||||||
|
def routedApi = Mock(Reader) {
|
||||||
|
1 * read(new JsonRpcRequest("eth_test", [])) >> Mono.error(new RpcException(RpcResponseError.CODE_METHOD_NOT_EXIST, "Test message"))
|
||||||
|
}
|
||||||
|
def upstream = Mock(Multistream) {
|
||||||
|
1 * getRoutedApi(_) >> Mono.just(routedApi)
|
||||||
|
}
|
||||||
|
def upstreams = Stub(MultistreamHolder)
|
||||||
|
|
||||||
|
def nativeCall = new NativeCall(upstreams, TestingCommons.objectMapper())
|
||||||
|
def ctx = new NativeCall.CallContext<NativeCall.ParsedCallDetails>(
|
||||||
|
15, upstream, Selector.empty, new AlwaysQuorum(),
|
||||||
|
new NativeCall.ParsedCallDetails("eth_test", [])
|
||||||
|
)
|
||||||
|
|
||||||
|
when:
|
||||||
|
def act = nativeCall.fetch(ctx) //.block(Duration.ofSeconds(1))
|
||||||
|
then:
|
||||||
|
StepVerifier.create(act)
|
||||||
|
.expectErrorMatches { t ->
|
||||||
|
t instanceof NativeCall.CallFailure &&
|
||||||
|
t.id == 15 &&
|
||||||
|
t.reason instanceof RpcException &&
|
||||||
|
t.reason.rpcMessage == "Test message"
|
||||||
|
}
|
||||||
|
.verify(Duration.ofSeconds(1))
|
||||||
|
}
|
||||||
|
|
||||||
def "Quorum is applied"() {
|
def "Quorum is applied"() {
|
||||||
setup:
|
setup:
|
||||||
def quorum = Spy(new AlwaysQuorum())
|
def quorum = new AlwaysQuorum()
|
||||||
def upstreams = Stub(Upstreams)
|
|
||||||
ReactorRpcClient rpcClient = Stub(ReactorRpcClient)
|
|
||||||
def apiMock = TestingCommons.api(rpcClient)
|
|
||||||
apiMock.upstream = Stub(Upstream)
|
|
||||||
|
|
||||||
apiMock.answer("eth_test", [], "foo")
|
def nativeCall = new NativeCall(Stub(MultistreamHolder), TestingCommons.objectMapper())
|
||||||
|
nativeCall.quorumReaderFactory = Mock(QuorumReaderFactory) {
|
||||||
def nativeCall = new NativeCall(upstreams, TestingCommons.objectMapper())
|
1 * create(_, _) >> Mock(Reader) {
|
||||||
def call = new NativeCall.CallContext(1, TestingCommons.aggregatedUpstream(apiMock),
|
1 * read(_) >> Mono.just(new QuorumRpcReader.Result("\"foo\"".bytes, 1))
|
||||||
Selector.empty, quorum,
|
}
|
||||||
|
}
|
||||||
|
def call = new NativeCall.CallContext(1, TestingCommons.aggregatedUpstream(TestingCommons.api()), Selector.empty, quorum,
|
||||||
new NativeCall.ParsedCallDetails("eth_test", []))
|
new NativeCall.ParsedCallDetails("eth_test", []))
|
||||||
|
|
||||||
when:
|
when:
|
||||||
def resp = nativeCall.executeOnRemote(call).block(Duration.ofSeconds(2))
|
def resp = nativeCall.executeOnRemote(call).block(Duration.ofSeconds(1))
|
||||||
def act = objectMapper.readValue(resp.payload, Map)
|
def act = objectMapper.readValue(resp.payload, Object)
|
||||||
then:
|
then:
|
||||||
act == [jsonrpc:"2.0", id:1, result: "foo"]
|
act == "foo"
|
||||||
1 * quorum.record(_, _)
|
|
||||||
1 * quorum.getResult()
|
|
||||||
}
|
|
||||||
|
|
||||||
def "Quorum may return not first received value"() {
|
|
||||||
setup:
|
|
||||||
def quorum = Spy(new NonEmptyQuorum(TestingCommons.rpcConverter(), 3))
|
|
||||||
|
|
||||||
def upstreams = Stub(Upstreams)
|
|
||||||
ReactorRpcClient rpcClient = Stub(ReactorRpcClient)
|
|
||||||
def apiMock = TestingCommons.api(rpcClient)
|
|
||||||
apiMock.upstream = Stub(Upstream)
|
|
||||||
|
|
||||||
apiMock.answerOnce("eth_test", [], null)
|
|
||||||
apiMock.answerOnce("eth_test", [], "bar")
|
|
||||||
apiMock.answerOnce("eth_test", [], null)
|
|
||||||
|
|
||||||
def nativeCall = new NativeCall(upstreams, TestingCommons.objectMapper())
|
|
||||||
def call = new NativeCall.CallContext(1, TestingCommons.aggregatedUpstream(apiMock),
|
|
||||||
Selector.empty, quorum,
|
|
||||||
new NativeCall.ParsedCallDetails("eth_test", []))
|
|
||||||
|
|
||||||
|
|
||||||
when:
|
|
||||||
def resp = nativeCall.executeOnRemote(call).block(Duration.ofSeconds(2))
|
|
||||||
def act = objectMapper.readValue(resp.payload, Map)
|
|
||||||
then:
|
|
||||||
act == [jsonrpc:"2.0", id:1, result: "bar"]
|
|
||||||
2 * quorum.record(_, _)
|
|
||||||
1 * quorum.getResult()
|
|
||||||
}
|
|
||||||
|
|
||||||
def "Have pause between repeats"() {
|
|
||||||
setup:
|
|
||||||
def quorum = Spy(new NonEmptyQuorum(TestingCommons.rpcConverter(), 3))
|
|
||||||
|
|
||||||
def upstreams = Stub(Upstreams)
|
|
||||||
ReactorRpcClient rpcClient = Stub(ReactorRpcClient)
|
|
||||||
def apiMock = TestingCommons.api(rpcClient)
|
|
||||||
apiMock.upstream = Stub(Upstream)
|
|
||||||
|
|
||||||
apiMock.answerOnce("eth_test", [], null)
|
|
||||||
apiMock.answerOnce("eth_test", [], "bar")
|
|
||||||
|
|
||||||
def nativeCall = new NativeCall(upstreams, TestingCommons.objectMapper())
|
|
||||||
def call = new NativeCall.CallContext(1, TestingCommons.aggregatedUpstream(apiMock),
|
|
||||||
Selector.empty, quorum,
|
|
||||||
new NativeCall.ParsedCallDetails("eth_test", []))
|
|
||||||
|
|
||||||
|
|
||||||
when:
|
|
||||||
def t1 = System.currentTimeMillis()
|
|
||||||
def resp = nativeCall.executeOnRemote(call).block(Duration.ofSeconds(2))
|
|
||||||
def delta = System.currentTimeMillis() - t1
|
|
||||||
then:
|
|
||||||
delta > 95 // should be 100, but sometimes gives less ???
|
|
||||||
new String(resp.payload) == '{"jsonrpc":"2.0","id":1,"result":"bar"}'
|
|
||||||
}
|
|
||||||
|
|
||||||
def "One call has no pause"() {
|
|
||||||
setup:
|
|
||||||
def quorum = Spy(new NonEmptyQuorum(TestingCommons.rpcConverter(), 3))
|
|
||||||
|
|
||||||
def upstreams = Stub(Upstreams)
|
|
||||||
ReactorRpcClient rpcClient = Stub(ReactorRpcClient)
|
|
||||||
def apiMock = TestingCommons.api(rpcClient)
|
|
||||||
apiMock.upstream = Stub(Upstream)
|
|
||||||
|
|
||||||
apiMock.answerOnce("eth_test", [], "bar")
|
|
||||||
|
|
||||||
def nativeCall = new NativeCall(upstreams, TestingCommons.objectMapper())
|
|
||||||
def call = new NativeCall.CallContext(1, TestingCommons.aggregatedUpstream(apiMock),
|
|
||||||
Selector.empty, quorum,
|
|
||||||
new NativeCall.ParsedCallDetails("eth_test", []))
|
|
||||||
|
|
||||||
|
|
||||||
when:
|
|
||||||
def t1 = System.currentTimeMillis()
|
|
||||||
nativeCall.executeOnRemote(call).block(Duration.ofSeconds(2))
|
|
||||||
def delta = System.currentTimeMillis() - t1
|
|
||||||
then:
|
|
||||||
delta < 50
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def "Returns error if no quorum"() {
|
def "Returns error if no quorum"() {
|
||||||
setup:
|
setup:
|
||||||
def quorum = Spy(new NonEmptyQuorum(TestingCommons.rpcConverter(), 3))
|
def quorum = new AlwaysQuorum()
|
||||||
|
|
||||||
def upstreams = Stub(Upstreams)
|
def nativeCall = new NativeCall(Stub(MultistreamHolder), TestingCommons.objectMapper())
|
||||||
ReactorRpcClient rpcClient = Stub(ReactorRpcClient)
|
nativeCall.quorumReaderFactory = Mock(QuorumReaderFactory) {
|
||||||
def apiMock = TestingCommons.api(rpcClient)
|
1 * create(_, _) >> Mock(Reader) {
|
||||||
apiMock.upstream = Stub(Upstream)
|
1 * read(_) >> Mono.empty()
|
||||||
|
}
|
||||||
apiMock.answer("eth_test", [], null, 3)
|
}
|
||||||
apiMock.answerOnce("eth_test", [], "foo")
|
def call = new NativeCall.CallContext(1, TestingCommons.aggregatedUpstream(TestingCommons.api()), Selector.empty, quorum,
|
||||||
|
|
||||||
def nativeCall = new NativeCall(upstreams, TestingCommons.objectMapper())
|
|
||||||
def call = new NativeCall.CallContext(1, TestingCommons.aggregatedUpstream(apiMock), Selector.empty, quorum,
|
|
||||||
new NativeCall.ParsedCallDetails("eth_test", []))
|
new NativeCall.ParsedCallDetails("eth_test", []))
|
||||||
|
|
||||||
3 * quorum.record(_, _)
|
|
||||||
1 * quorum.getResult()
|
|
||||||
|
|
||||||
when:
|
when:
|
||||||
def resp = nativeCall.executeOnRemote(call)
|
def resp = nativeCall.executeOnRemote(call)
|
||||||
then:
|
then:
|
||||||
StepVerifier.create(resp)
|
StepVerifier.create(resp)
|
||||||
.expectErrorMatches({t -> t instanceof NativeCall.CallFailure && t.id == 1})
|
.expectErrorMatches({ t -> t instanceof NativeCall.CallFailure && t.id == 1 })
|
||||||
.verify(Duration.ofSeconds(1))
|
.verify(Duration.ofSeconds(1))
|
||||||
}
|
}
|
||||||
|
|
||||||
def "Packs call exception into response with id"() {
|
def "Packs call exception into response with id"() {
|
||||||
setup:
|
setup:
|
||||||
def upstreams = Stub(Upstreams)
|
def upstreams = Stub(MultistreamHolder)
|
||||||
def nativeCall = new NativeCall(upstreams, TestingCommons.objectMapper())
|
def nativeCall = new NativeCall(upstreams, TestingCommons.objectMapper())
|
||||||
when:
|
when:
|
||||||
def resp = nativeCall.processException(new NativeCall.CallFailure(5, new IllegalArgumentException("test test")))
|
def resp = nativeCall.processException(new NativeCall.CallFailure(5, new IllegalArgumentException("test test")))
|
||||||
@@ -193,7 +156,7 @@ class NativeCallSpec extends Specification {
|
|||||||
|
|
||||||
def "Packs unknown exception into response"() {
|
def "Packs unknown exception into response"() {
|
||||||
setup:
|
setup:
|
||||||
def upstreams = Stub(Upstreams)
|
def upstreams = Stub(MultistreamHolder)
|
||||||
def nativeCall = new NativeCall(upstreams, TestingCommons.objectMapper())
|
def nativeCall = new NativeCall(upstreams, TestingCommons.objectMapper())
|
||||||
when:
|
when:
|
||||||
def resp = nativeCall.processException(new IllegalArgumentException("test test"))
|
def resp = nativeCall.processException(new IllegalArgumentException("test test"))
|
||||||
@@ -209,13 +172,13 @@ class NativeCallSpec extends Specification {
|
|||||||
|
|
||||||
def "Builds normal response"() {
|
def "Builds normal response"() {
|
||||||
setup:
|
setup:
|
||||||
def upstreams = Stub(Upstreams)
|
def upstreams = Stub(MultistreamHolder)
|
||||||
def nativeCall = new NativeCall(upstreams, TestingCommons.objectMapper())
|
def nativeCall = new NativeCall(upstreams, TestingCommons.objectMapper())
|
||||||
def json = [jsonrpc:"2.0", id:1, result: "foo"]
|
def json = [jsonrpc:"2.0", id:1, result: "foo"]
|
||||||
|
|
||||||
when:
|
when:
|
||||||
def resp = nativeCall.buildResponse(
|
def resp = nativeCall.buildResponse(
|
||||||
new NativeCall.CallContext<byte[]>(1561, TestingCommons.aggregatedUpstream(Stub(DirectEthereumApi)), Selector.empty, new AlwaysQuorum(), objectMapper.writeValueAsBytes(json))
|
new NativeCall.CallContext<byte[]>(1561, TestingCommons.aggregatedUpstream(TestingCommons.api()), Selector.empty, new AlwaysQuorum(), objectMapper.writeValueAsBytes(json))
|
||||||
)
|
)
|
||||||
then:
|
then:
|
||||||
resp.id == 1561
|
resp.id == 1561
|
||||||
@@ -225,7 +188,7 @@ class NativeCallSpec extends Specification {
|
|||||||
|
|
||||||
def "Returns error for invalid chain"() {
|
def "Returns error for invalid chain"() {
|
||||||
setup:
|
setup:
|
||||||
def upstreams = Stub(Upstreams)
|
def upstreams = Stub(MultistreamHolder)
|
||||||
def nativeCall = new NativeCall(upstreams, TestingCommons.objectMapper())
|
def nativeCall = new NativeCall(upstreams, TestingCommons.objectMapper())
|
||||||
|
|
||||||
def req = BlockchainOuterClass.NativeCallRequest.newBuilder()
|
def req = BlockchainOuterClass.NativeCallRequest.newBuilder()
|
||||||
@@ -248,7 +211,7 @@ class NativeCallSpec extends Specification {
|
|||||||
|
|
||||||
def "Returns error for unsupported chain"() {
|
def "Returns error for unsupported chain"() {
|
||||||
setup:
|
setup:
|
||||||
def upstreams = Mock(Upstreams)
|
def upstreams = Mock(MultistreamHolder)
|
||||||
def nativeCall = new NativeCall(upstreams, TestingCommons.objectMapper())
|
def nativeCall = new NativeCall(upstreams, TestingCommons.objectMapper())
|
||||||
|
|
||||||
def req = BlockchainOuterClass.NativeCallRequest.newBuilder()
|
def req = BlockchainOuterClass.NativeCallRequest.newBuilder()
|
||||||
@@ -270,14 +233,14 @@ class NativeCallSpec extends Specification {
|
|||||||
.verify(Duration.ofSeconds(1))
|
.verify(Duration.ofSeconds(1))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Ignore
|
||||||
|
//TODO
|
||||||
def "Calls cache before remote"() {
|
def "Calls cache before remote"() {
|
||||||
setup:
|
setup:
|
||||||
def upstreams = Stub(Upstreams)
|
def upstreams = Stub(MultistreamHolder)
|
||||||
def nativeCall = new NativeCall(upstreams, TestingCommons.objectMapper())
|
def nativeCall = new NativeCall(upstreams, TestingCommons.objectMapper())
|
||||||
def api = Mock(DirectEthereumApi)
|
def api = TestingCommons.api()
|
||||||
def upstream = TestingCommons.aggregatedUpstream(api)
|
def upstream = TestingCommons.aggregatedUpstream(api)
|
||||||
def cacheMock = Mock(CachingEthereumApi)
|
|
||||||
upstream.cache = cacheMock
|
|
||||||
|
|
||||||
def ctx = new NativeCall.CallContext<NativeCall.ParsedCallDetails>(10,
|
def ctx = new NativeCall.CallContext<NativeCall.ParsedCallDetails>(10,
|
||||||
upstream,
|
upstream,
|
||||||
@@ -289,13 +252,13 @@ class NativeCallSpec extends Specification {
|
|||||||
1 * cacheMock.execute(10, "eth_test", []) >> Mono.empty()
|
1 * cacheMock.execute(10, "eth_test", []) >> Mono.empty()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Ignore
|
||||||
|
//TODO
|
||||||
def "Uses cached value"() {
|
def "Uses cached value"() {
|
||||||
setup:
|
setup:
|
||||||
def upstreams = Stub(Upstreams)
|
def upstreams = Stub(MultistreamHolder)
|
||||||
def nativeCall = new NativeCall(upstreams, TestingCommons.objectMapper())
|
def nativeCall = new NativeCall(upstreams, TestingCommons.objectMapper())
|
||||||
def upstream = TestingCommons.aggregatedUpstream(Stub(DirectEthereumApi))
|
def upstream = TestingCommons.aggregatedUpstream(TestingCommons.api())
|
||||||
def cacheMock = Mock(CachingEthereumApi)
|
|
||||||
upstream.cache = cacheMock
|
|
||||||
|
|
||||||
def ctx = new NativeCall.CallContext<NativeCall.ParsedCallDetails>(10,
|
def ctx = new NativeCall.CallContext<NativeCall.ParsedCallDetails>(10,
|
||||||
upstream,
|
upstream,
|
||||||
@@ -307,63 +270,4 @@ class NativeCallSpec extends Specification {
|
|||||||
1 * cacheMock.execute(10, "eth_test", []) >> Mono.just('{"result": "foo"}'.bytes)
|
1 * cacheMock.execute(10, "eth_test", []) >> Mono.just('{"result": "foo"}'.bytes)
|
||||||
new String(act.block().payload) == '{"result": "foo"}'
|
new String(act.block().payload) == '{"result": "foo"}'
|
||||||
}
|
}
|
||||||
|
|
||||||
def "Retries on error"() {
|
|
||||||
setup:
|
|
||||||
def quorum = Spy(new AlwaysQuorum())
|
|
||||||
|
|
||||||
def upstreams = Stub(Upstreams)
|
|
||||||
ReactorRpcClient rpcClient = Stub(ReactorRpcClient)
|
|
||||||
def apiMock = TestingCommons.api(rpcClient)
|
|
||||||
apiMock.upstream = Stub(Upstream)
|
|
||||||
|
|
||||||
apiMock.answer("eth_test", [], null, 1, new TimeoutException("test 1"))
|
|
||||||
apiMock.answer("eth_test", [], null, 1, new TimeoutException("test 2"))
|
|
||||||
apiMock.answerOnce("eth_test", [], "bar")
|
|
||||||
|
|
||||||
def nativeCall = new NativeCall(upstreams, TestingCommons.objectMapper())
|
|
||||||
def call = new NativeCall.CallContext(1, TestingCommons.aggregatedUpstream(apiMock),
|
|
||||||
Selector.empty, quorum,
|
|
||||||
new NativeCall.ParsedCallDetails("eth_test", []))
|
|
||||||
|
|
||||||
|
|
||||||
when:
|
|
||||||
def resp = nativeCall.executeOnRemote(call).block(Duration.ofSeconds(2))
|
|
||||||
def act = objectMapper.readValue(resp.payload, Map)
|
|
||||||
then:
|
|
||||||
act == [jsonrpc:"2.0", id:1, result: "bar"]
|
|
||||||
1 * quorum.record(_, _)
|
|
||||||
1 * quorum.getResult()
|
|
||||||
}
|
|
||||||
|
|
||||||
def "Send raw retries 3 times"() {
|
|
||||||
setup:
|
|
||||||
def quorum = Spy(new BroadcastQuorum(TestingCommons.rpcConverter(), 3))
|
|
||||||
|
|
||||||
def upstreams = Stub(Upstreams)
|
|
||||||
ReactorRpcClient rpcClient = Stub(ReactorRpcClient)
|
|
||||||
def apiMock = TestingCommons.api(rpcClient)
|
|
||||||
apiMock.upstream = Stub(Upstream)
|
|
||||||
|
|
||||||
apiMock.answer("eth_sendRawTransaction", ["0x1234"],
|
|
||||||
"0x4b66b555df9faed6f0711f2104d183736c8e2dc7434626dd2622e243f041d41b", 1)
|
|
||||||
apiMock.answer("eth_sendRawTransaction", ["0x1234"], null, 10,
|
|
||||||
new RpcException(RpcResponseError.CODE_INVALID_REQUEST, "Transaction with the same hash was already imported"))
|
|
||||||
// apiMock.answer("eth_sendRawTransaction", ["0x1234"],
|
|
||||||
// new RpcResponseError(RpcResponseError.CODE_INVALID_REQUEST, "Transaction with the same hash was already imported"), 10)
|
|
||||||
|
|
||||||
def nativeCall = new NativeCall(upstreams, TestingCommons.objectMapper())
|
|
||||||
def call = new NativeCall.CallContext(1, TestingCommons.aggregatedUpstream(apiMock),
|
|
||||||
Selector.empty, quorum,
|
|
||||||
new NativeCall.ParsedCallDetails("eth_sendRawTransaction", ["0x1234"]))
|
|
||||||
|
|
||||||
|
|
||||||
when:
|
|
||||||
def resp = nativeCall.executeOnRemote(call).block(Duration.ofSeconds(2))
|
|
||||||
def act = objectMapper.readValue(resp.payload, Map)
|
|
||||||
then:
|
|
||||||
act == [jsonrpc:"2.0", id:1, result: "0x4b66b555df9faed6f0711f2104d183736c8e2dc7434626dd2622e243f041d41b"]
|
|
||||||
1 * quorum.record(_ as byte[], _)
|
|
||||||
2 * quorum.record(_ as RpcException, _)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,13 +23,10 @@ import io.emeraldpay.api.proto.Common
|
|||||||
import io.emeraldpay.dshackle.data.BlockContainer
|
import io.emeraldpay.dshackle.data.BlockContainer
|
||||||
import io.emeraldpay.dshackle.test.EthereumUpstreamMock
|
import io.emeraldpay.dshackle.test.EthereumUpstreamMock
|
||||||
import io.emeraldpay.dshackle.test.TestingCommons
|
import io.emeraldpay.dshackle.test.TestingCommons
|
||||||
import io.emeraldpay.dshackle.test.UpstreamsMock
|
import io.emeraldpay.dshackle.test.MultistreamHolderMock
|
||||||
import io.emeraldpay.dshackle.upstream.ethereum.DirectEthereumApi
|
|
||||||
import io.emeraldpay.dshackle.upstream.Upstream
|
|
||||||
import io.emeraldpay.dshackle.upstream.ethereum.EthereumUpstream
|
import io.emeraldpay.dshackle.upstream.ethereum.EthereumUpstream
|
||||||
import io.emeraldpay.grpc.Chain
|
import io.emeraldpay.grpc.Chain
|
||||||
import io.infinitape.etherjar.domain.BlockHash
|
import io.infinitape.etherjar.domain.BlockHash
|
||||||
import io.infinitape.etherjar.domain.TransactionId
|
|
||||||
import io.infinitape.etherjar.rpc.json.BlockJson
|
import io.infinitape.etherjar.rpc.json.BlockJson
|
||||||
import io.infinitape.etherjar.rpc.json.TransactionRefJson
|
import io.infinitape.etherjar.rpc.json.TransactionRefJson
|
||||||
import reactor.core.publisher.Mono
|
import reactor.core.publisher.Mono
|
||||||
@@ -45,7 +42,7 @@ class StreamHeadSpec extends Specification {
|
|||||||
|
|
||||||
def "Errors on unavailable chain"() {
|
def "Errors on unavailable chain"() {
|
||||||
setup:
|
setup:
|
||||||
def upstreams = new UpstreamsMock(Chain.ETHEREUM, Stub(EthereumUpstream))
|
def upstreams = new MultistreamHolderMock(Chain.ETHEREUM, Stub(EthereumUpstream))
|
||||||
def streamHead = new StreamHead(upstreams)
|
def streamHead = new StreamHead(upstreams)
|
||||||
when:
|
when:
|
||||||
def flux = streamHead.add(
|
def flux = streamHead.add(
|
||||||
@@ -80,8 +77,8 @@ class StreamHeadSpec extends Specification {
|
|||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
def upstream = new EthereumUpstreamMock(Chain.ETHEREUM, Stub(DirectEthereumApi.class))
|
def upstream = new EthereumUpstreamMock(Chain.ETHEREUM, TestingCommons.api())
|
||||||
def upstreams = new UpstreamsMock(Chain.ETHEREUM, upstream)
|
def upstreams = new MultistreamHolderMock(Chain.ETHEREUM, upstream)
|
||||||
def streamHead = new StreamHead(upstreams)
|
def streamHead = new StreamHead(upstreams)
|
||||||
when:
|
when:
|
||||||
def flux = streamHead.add(
|
def flux = streamHead.add(
|
||||||
|
|||||||
@@ -20,11 +20,11 @@ import io.emeraldpay.api.proto.Common
|
|||||||
import io.emeraldpay.dshackle.data.BlockContainer
|
import io.emeraldpay.dshackle.data.BlockContainer
|
||||||
import io.emeraldpay.dshackle.data.BlockId
|
import io.emeraldpay.dshackle.data.BlockId
|
||||||
import io.emeraldpay.dshackle.test.TestingCommons
|
import io.emeraldpay.dshackle.test.TestingCommons
|
||||||
import io.emeraldpay.dshackle.upstream.AggregatedUpstream
|
import io.emeraldpay.dshackle.test.MultistreamHolderMock
|
||||||
import io.emeraldpay.dshackle.upstream.Head
|
import io.emeraldpay.dshackle.upstream.Head
|
||||||
import io.emeraldpay.dshackle.upstream.Upstream
|
import io.emeraldpay.dshackle.upstream.MultistreamHolder
|
||||||
import io.emeraldpay.dshackle.upstream.Upstreams
|
import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinMultistream
|
||||||
import io.emeraldpay.dshackle.upstream.bitcoin.DirectBitcoinApi
|
import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinReader
|
||||||
import io.emeraldpay.grpc.Chain
|
import io.emeraldpay.grpc.Chain
|
||||||
import reactor.core.publisher.Flux
|
import reactor.core.publisher.Flux
|
||||||
import reactor.core.publisher.Mono
|
import reactor.core.publisher.Mono
|
||||||
@@ -43,7 +43,7 @@ class TrackBitcoinAddressSpec extends Specification {
|
|||||||
setup:
|
setup:
|
||||||
def json = this.class.getClassLoader().getResourceAsStream("bitcoin/unspent-one-addr.json")
|
def json = this.class.getClassLoader().getResourceAsStream("bitcoin/unspent-one-addr.json")
|
||||||
def unspents = TestingCommons.objectMapper().readValue(json, List)
|
def unspents = TestingCommons.objectMapper().readValue(json, List)
|
||||||
TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(Upstreams))
|
TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(MultistreamHolder))
|
||||||
when:
|
when:
|
||||||
def total = track.getTotal(Chain.BITCOIN, ["1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK"], unspents)
|
def total = track.getTotal(Chain.BITCOIN, ["1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK"], unspents)
|
||||||
|
|
||||||
@@ -58,7 +58,7 @@ class TrackBitcoinAddressSpec extends Specification {
|
|||||||
setup:
|
setup:
|
||||||
def json = this.class.getClassLoader().getResourceAsStream("bitcoin/unspent-two-addr.json")
|
def json = this.class.getClassLoader().getResourceAsStream("bitcoin/unspent-two-addr.json")
|
||||||
def unspents = TestingCommons.objectMapper().readValue(json, List)
|
def unspents = TestingCommons.objectMapper().readValue(json, List)
|
||||||
TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(Upstreams))
|
TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(MultistreamHolder))
|
||||||
when:
|
when:
|
||||||
def total = track.getTotal(Chain.BITCOIN, ["1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK"], unspents)
|
def total = track.getTotal(Chain.BITCOIN, ["1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK"], unspents)
|
||||||
|
|
||||||
@@ -73,7 +73,7 @@ class TrackBitcoinAddressSpec extends Specification {
|
|||||||
setup:
|
setup:
|
||||||
def json = this.class.getClassLoader().getResourceAsStream("bitcoin/unspent-two-addr.json")
|
def json = this.class.getClassLoader().getResourceAsStream("bitcoin/unspent-two-addr.json")
|
||||||
def unspents = TestingCommons.objectMapper().readValue(json, List)
|
def unspents = TestingCommons.objectMapper().readValue(json, List)
|
||||||
TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(Upstreams))
|
TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(MultistreamHolder))
|
||||||
when:
|
when:
|
||||||
def total = track.getTotal(Chain.BITCOIN, ["1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP"], unspents).sort { it.address.address }
|
def total = track.getTotal(Chain.BITCOIN, ["1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", "35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP"], unspents).sort { it.address.address }
|
||||||
|
|
||||||
@@ -93,7 +93,7 @@ class TrackBitcoinAddressSpec extends Specification {
|
|||||||
|
|
||||||
def "Zero for empty unspents"() {
|
def "Zero for empty unspents"() {
|
||||||
setup:
|
setup:
|
||||||
TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(Upstreams))
|
TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(MultistreamHolder))
|
||||||
when:
|
when:
|
||||||
def total = track.getTotal(Chain.BITCOIN, ["1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK"], [])
|
def total = track.getTotal(Chain.BITCOIN, ["1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK"], [])
|
||||||
|
|
||||||
@@ -108,7 +108,7 @@ class TrackBitcoinAddressSpec extends Specification {
|
|||||||
setup:
|
setup:
|
||||||
def json = this.class.getClassLoader().getResourceAsStream("bitcoin/unspent-two-addr.json")
|
def json = this.class.getClassLoader().getResourceAsStream("bitcoin/unspent-two-addr.json")
|
||||||
def unspents = TestingCommons.objectMapper().readValue(json, List)
|
def unspents = TestingCommons.objectMapper().readValue(json, List)
|
||||||
TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(Upstreams))
|
TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(MultistreamHolder))
|
||||||
when:
|
when:
|
||||||
def total = track.getTotal(Chain.BITCOIN, ["16rCmCmbuWDhPjWTrpQGaU3EPdZF7MTdUk", "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK"], unspents).sort { it.address.address }
|
def total = track.getTotal(Chain.BITCOIN, ["16rCmCmbuWDhPjWTrpQGaU3EPdZF7MTdUk", "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK"], unspents).sort { it.address.address }
|
||||||
|
|
||||||
@@ -126,7 +126,7 @@ class TrackBitcoinAddressSpec extends Specification {
|
|||||||
|
|
||||||
def "One address for single provided"() {
|
def "One address for single provided"() {
|
||||||
setup:
|
setup:
|
||||||
TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(Upstreams))
|
TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(MultistreamHolder))
|
||||||
def req = BlockchainOuterClass.BalanceRequest.newBuilder()
|
def req = BlockchainOuterClass.BalanceRequest.newBuilder()
|
||||||
.setAddress(
|
.setAddress(
|
||||||
Common.AnyAddress.newBuilder()
|
Common.AnyAddress.newBuilder()
|
||||||
@@ -144,7 +144,7 @@ class TrackBitcoinAddressSpec extends Specification {
|
|||||||
|
|
||||||
def "Sorted addresses for multiple provided"() {
|
def "Sorted addresses for multiple provided"() {
|
||||||
setup:
|
setup:
|
||||||
TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(Upstreams))
|
TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(MultistreamHolder))
|
||||||
def req = BlockchainOuterClass.BalanceRequest.newBuilder()
|
def req = BlockchainOuterClass.BalanceRequest.newBuilder()
|
||||||
.setAddress(
|
.setAddress(
|
||||||
Common.AnyAddress.newBuilder()
|
Common.AnyAddress.newBuilder()
|
||||||
@@ -165,7 +165,7 @@ class TrackBitcoinAddressSpec extends Specification {
|
|||||||
|
|
||||||
def "Null for no address provided"() {
|
def "Null for no address provided"() {
|
||||||
setup:
|
setup:
|
||||||
TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(Upstreams))
|
TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(MultistreamHolder))
|
||||||
def req = BlockchainOuterClass.BalanceRequest.newBuilder()
|
def req = BlockchainOuterClass.BalanceRequest.newBuilder()
|
||||||
.build()
|
.build()
|
||||||
when:
|
when:
|
||||||
@@ -176,7 +176,7 @@ class TrackBitcoinAddressSpec extends Specification {
|
|||||||
|
|
||||||
def "Build proto for common balance"() {
|
def "Build proto for common balance"() {
|
||||||
setup:
|
setup:
|
||||||
TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(Upstreams))
|
TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(MultistreamHolder))
|
||||||
def balance = new TrackBitcoinAddress.AddressBalance(Chain.BITCOIN, "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", BigInteger.valueOf(123456))
|
def balance = new TrackBitcoinAddress.AddressBalance(Chain.BITCOIN, "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", BigInteger.valueOf(123456))
|
||||||
when:
|
when:
|
||||||
def act = track.buildResponse(balance)
|
def act = track.buildResponse(balance)
|
||||||
@@ -189,7 +189,7 @@ class TrackBitcoinAddressSpec extends Specification {
|
|||||||
|
|
||||||
def "Build proto for zero balance"() {
|
def "Build proto for zero balance"() {
|
||||||
setup:
|
setup:
|
||||||
TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(Upstreams))
|
TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(MultistreamHolder))
|
||||||
def balance = new TrackBitcoinAddress.AddressBalance(Chain.BITCOIN, "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", BigInteger.ZERO)
|
def balance = new TrackBitcoinAddress.AddressBalance(Chain.BITCOIN, "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", BigInteger.ZERO)
|
||||||
when:
|
when:
|
||||||
def act = track.buildResponse(balance)
|
def act = track.buildResponse(balance)
|
||||||
@@ -202,7 +202,7 @@ class TrackBitcoinAddressSpec extends Specification {
|
|||||||
|
|
||||||
def "Build proto for all bitcoins"() {
|
def "Build proto for all bitcoins"() {
|
||||||
setup:
|
setup:
|
||||||
TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(Upstreams))
|
TrackBitcoinAddress track = new TrackBitcoinAddress(Stub(MultistreamHolder))
|
||||||
def balance = new TrackBitcoinAddress.AddressBalance(Chain.BITCOIN, "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", BigInteger.valueOf(21_000_000).multiply(BigInteger.TEN.pow(8)))
|
def balance = new TrackBitcoinAddress.AddressBalance(Chain.BITCOIN, "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", BigInteger.valueOf(21_000_000).multiply(BigInteger.TEN.pow(8)))
|
||||||
when:
|
when:
|
||||||
def act = track.buildResponse(balance)
|
def act = track.buildResponse(balance)
|
||||||
@@ -216,24 +216,24 @@ class TrackBitcoinAddressSpec extends Specification {
|
|||||||
def "Get update for a balance"() {
|
def "Get update for a balance"() {
|
||||||
setup:
|
setup:
|
||||||
|
|
||||||
DirectBitcoinApi api = Mock(DirectBitcoinApi) {
|
|
||||||
2 * executeAndResult(0, "listunspent", [], List) >>> [
|
|
||||||
Mono.just([]), Mono.just([[address: "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", amount: 0.0123]])
|
|
||||||
]
|
|
||||||
}
|
|
||||||
def blocks = TopicProcessor.create()
|
def blocks = TopicProcessor.create()
|
||||||
Head head = Mock(Head) {
|
Head head = Mock(Head) {
|
||||||
1 * getFlux() >> Flux.from(blocks)
|
1 * getFlux() >> Flux.from(blocks)
|
||||||
}
|
}
|
||||||
Upstream upstream
|
def upstream = null
|
||||||
upstream = Mock(AggregatedUpstream) {
|
upstream = Mock(BitcoinMultistream) {
|
||||||
_ * getApi(_) >> Mono.just(api)
|
_ * getReader() >> Mock(BitcoinReader) {
|
||||||
|
2 * listUnspent() >>> [
|
||||||
|
Mono.just([]),
|
||||||
|
Mono.just([[address: "1K7xkspJg7DDKNwzXgoRSDCUxiFsRegsSK", amount: 0.0123]])
|
||||||
|
]
|
||||||
|
}
|
||||||
_ * getHead() >> head
|
_ * getHead() >> head
|
||||||
_ * castApi(_) >> { return upstream }
|
_ * cast(_) >> {
|
||||||
}
|
upstream
|
||||||
Upstreams upstreams = Mock(Upstreams) {
|
}
|
||||||
_ * getUpstream(Chain.BITCOIN) >> upstream
|
|
||||||
}
|
}
|
||||||
|
MultistreamHolder upstreams = new MultistreamHolderMock(Chain.BITCOIN, upstream)
|
||||||
TrackBitcoinAddress track = new TrackBitcoinAddress(upstreams)
|
TrackBitcoinAddress track = new TrackBitcoinAddress(upstreams)
|
||||||
|
|
||||||
when:
|
when:
|
||||||
@@ -253,7 +253,7 @@ class TrackBitcoinAddressSpec extends Specification {
|
|||||||
StepVerifier.create(resp)
|
StepVerifier.create(resp)
|
||||||
.expectNext("0")
|
.expectNext("0")
|
||||||
.then {
|
.then {
|
||||||
blocks.onNext(new BlockContainer(1L, BlockId.from(hash1), BigInteger.ONE, Instant.now(), false, null, []))
|
blocks.onNext(new BlockContainer(1L, BlockId.from(hash1), BigInteger.ONE, Instant.now(), false, null, null, []))
|
||||||
}
|
}
|
||||||
.expectNext("1230000")
|
.expectNext("1230000")
|
||||||
.then {
|
.then {
|
||||||
|
|||||||
@@ -18,11 +18,9 @@ package io.emeraldpay.dshackle.rpc
|
|||||||
import io.emeraldpay.dshackle.data.BlockContainer
|
import io.emeraldpay.dshackle.data.BlockContainer
|
||||||
import io.emeraldpay.dshackle.data.BlockId
|
import io.emeraldpay.dshackle.data.BlockId
|
||||||
import io.emeraldpay.dshackle.upstream.Head
|
import io.emeraldpay.dshackle.upstream.Head
|
||||||
import io.emeraldpay.dshackle.upstream.Upstream
|
import io.emeraldpay.dshackle.upstream.MultistreamHolder
|
||||||
import io.emeraldpay.dshackle.upstream.Upstreams
|
import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinMultistream
|
||||||
import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinReader
|
import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinReader
|
||||||
import io.emeraldpay.dshackle.upstream.bitcoin.DirectBitcoinApi
|
|
||||||
import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinUpstream
|
|
||||||
import io.emeraldpay.dshackle.upstream.bitcoin.CachingMempoolData
|
import io.emeraldpay.dshackle.upstream.bitcoin.CachingMempoolData
|
||||||
import io.emeraldpay.grpc.Chain
|
import io.emeraldpay.grpc.Chain
|
||||||
import reactor.core.publisher.Flux
|
import reactor.core.publisher.Flux
|
||||||
@@ -37,7 +35,7 @@ class TrackBitcoinTxSpec extends Specification {
|
|||||||
|
|
||||||
def "loadMempool() returns not found when not found"() {
|
def "loadMempool() returns not found when not found"() {
|
||||||
setup:
|
setup:
|
||||||
TrackBitcoinTx track = new TrackBitcoinTx(Stub(Upstreams))
|
TrackBitcoinTx track = new TrackBitcoinTx(Stub(MultistreamHolder))
|
||||||
|
|
||||||
CachingMempoolData mempoolAccess = Mock(CachingMempoolData) {
|
CachingMempoolData mempoolAccess = Mock(CachingMempoolData) {
|
||||||
1 * get() >> Mono.just([
|
1 * get() >> Mono.just([
|
||||||
@@ -45,8 +43,8 @@ class TrackBitcoinTxSpec extends Specification {
|
|||||||
"d296c6d47335a7f283574b06f1d6303b30ac75631e081ab128346a549ad93350"
|
"d296c6d47335a7f283574b06f1d6303b30ac75631e081ab128346a549ad93350"
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
BitcoinUpstream upstream = Mock(BitcoinUpstream) {
|
BitcoinMultistream upstream = Mock(BitcoinMultistream) {
|
||||||
_ * getData() >> Mock(BitcoinReader) {
|
_ * getReader() >> Mock(BitcoinReader) {
|
||||||
_ * getMempool() >> mempoolAccess
|
_ * getMempool() >> mempoolAccess
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -64,15 +62,15 @@ class TrackBitcoinTxSpec extends Specification {
|
|||||||
|
|
||||||
def "loadMempool() returns ok when found"() {
|
def "loadMempool() returns ok when found"() {
|
||||||
setup:
|
setup:
|
||||||
TrackBitcoinTx track = new TrackBitcoinTx(Stub(Upstreams))
|
TrackBitcoinTx track = new TrackBitcoinTx(Stub(MultistreamHolder))
|
||||||
CachingMempoolData mempoolAccess = Mock(CachingMempoolData) {
|
CachingMempoolData mempoolAccess = Mock(CachingMempoolData) {
|
||||||
1 * get() >> Mono.just([
|
1 * get() >> Mono.just([
|
||||||
"69cd44d7c641db82e69824523c7ac0c5c1e5628f025474529cf5ffe64527efc9",
|
"69cd44d7c641db82e69824523c7ac0c5c1e5628f025474529cf5ffe64527efc9",
|
||||||
"d296c6d47335a7f283574b06f1d6303b30ac75631e081ab128346a549ad93350"
|
"d296c6d47335a7f283574b06f1d6303b30ac75631e081ab128346a549ad93350"
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
BitcoinUpstream upstream = Mock(BitcoinUpstream) {
|
BitcoinMultistream upstream = Mock(BitcoinMultistream) {
|
||||||
_ * getData() >> Mock(BitcoinReader) {
|
_ * getReader() >> Mock(BitcoinReader) {
|
||||||
_ * getMempool() >> mempoolAccess
|
_ * getMempool() >> mempoolAccess
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -90,15 +88,17 @@ class TrackBitcoinTxSpec extends Specification {
|
|||||||
|
|
||||||
def "loadExiting() returns not found if not mined"() {
|
def "loadExiting() returns not found if not mined"() {
|
||||||
setup:
|
setup:
|
||||||
TrackBitcoinTx track = new TrackBitcoinTx(Stub(Upstreams))
|
TrackBitcoinTx track = new TrackBitcoinTx(Stub(MultistreamHolder))
|
||||||
def txid = "69cd44d7c641db82e69824523c7ac0c5c1e5628f025474529cf5ffe64527efc9"
|
def txid = "69cd44d7c641db82e69824523c7ac0c5c1e5628f025474529cf5ffe64527efc9"
|
||||||
DirectBitcoinApi api = Mock(DirectBitcoinApi) {
|
BitcoinMultistream upstream = Mock(BitcoinMultistream) {
|
||||||
1 * getTx(txid) >> Mono.just([
|
_ * getReader() >> Mock(BitcoinReader) {
|
||||||
txid: txid
|
1 * getTx(txid) >> Mono.just([
|
||||||
])
|
txid: txid
|
||||||
|
])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
when:
|
when:
|
||||||
def act = track.loadExisting(api, txid)
|
def act = track.loadExisting(upstream, txid)
|
||||||
|
|
||||||
then:
|
then:
|
||||||
StepVerifier.create(act)
|
StepVerifier.create(act)
|
||||||
@@ -111,17 +111,19 @@ class TrackBitcoinTxSpec extends Specification {
|
|||||||
|
|
||||||
def "loadExiting() returns block if mined"() {
|
def "loadExiting() returns block if mined"() {
|
||||||
setup:
|
setup:
|
||||||
TrackBitcoinTx track = new TrackBitcoinTx(Stub(Upstreams))
|
TrackBitcoinTx track = new TrackBitcoinTx(Stub(MultistreamHolder))
|
||||||
def txid = "69cd44d7c641db82e69824523c7ac0c5c1e5628f025474529cf5ffe64527efc9"
|
def txid = "69cd44d7c641db82e69824523c7ac0c5c1e5628f025474529cf5ffe64527efc9"
|
||||||
DirectBitcoinApi api = Mock(DirectBitcoinApi) {
|
BitcoinMultistream upstream = Mock(BitcoinMultistream) {
|
||||||
1 * getTx(txid) >> Mono.just([
|
_ * getReader() >> Mock(BitcoinReader) {
|
||||||
txid : txid,
|
1 * getTx(txid) >> Mono.just([
|
||||||
blockhash: "0000000000000000000895d1b9d3898700e1deecc3b0e69f439aa77875e6042f",
|
txid : txid,
|
||||||
height : 100
|
blockhash: "0000000000000000000895d1b9d3898700e1deecc3b0e69f439aa77875e6042f",
|
||||||
])
|
height : 100
|
||||||
|
])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
when:
|
when:
|
||||||
def act = track.loadExisting(api, txid)
|
def act = track.loadExisting(upstream, txid)
|
||||||
|
|
||||||
then:
|
then:
|
||||||
StepVerifier.create(act)
|
StepVerifier.create(act)
|
||||||
@@ -136,16 +138,16 @@ class TrackBitcoinTxSpec extends Specification {
|
|||||||
|
|
||||||
def "Goes with confirmations"() {
|
def "Goes with confirmations"() {
|
||||||
setup:
|
setup:
|
||||||
TrackBitcoinTx track = new TrackBitcoinTx(Stub(Upstreams))
|
TrackBitcoinTx track = new TrackBitcoinTx(Stub(MultistreamHolder))
|
||||||
def txid = "69cd44d7c641db82e69824523c7ac0c5c1e5628f025474529cf5ffe64527efc9"
|
def txid = "69cd44d7c641db82e69824523c7ac0c5c1e5628f025474529cf5ffe64527efc9"
|
||||||
// start with the current block
|
// start with the current block
|
||||||
def next = Flux.fromIterable([10, 12, 13, 14, 15]).map { h ->
|
def next = Flux.fromIterable([10, 12, 13, 14, 15]).map { h ->
|
||||||
new BlockContainer(h.longValue(), BlockId.from("0000000000000000000895d1b9d3898700e1deecc3b0e69f439aa77875e6042f"), BigInteger.ONE, Instant.now(), false, null, [])
|
new BlockContainer(h.longValue(), BlockId.from("0000000000000000000895d1b9d3898700e1deecc3b0e69f439aa77875e6042f"), BigInteger.ONE, Instant.now(), false, null, null, [])
|
||||||
}
|
}
|
||||||
Head head = Mock(Head) {
|
Head head = Mock(Head) {
|
||||||
1 * getFlux() >> next
|
1 * getFlux() >> next
|
||||||
}
|
}
|
||||||
Upstream upstream = Mock(BitcoinUpstream) {
|
BitcoinMultistream upstream = Mock(BitcoinMultistream) {
|
||||||
1 * getHead() >> head
|
1 * getHead() >> head
|
||||||
}
|
}
|
||||||
def status = new TrackBitcoinTx.TxStatus(
|
def status = new TrackBitcoinTx.TxStatus(
|
||||||
@@ -167,16 +169,16 @@ class TrackBitcoinTxSpec extends Specification {
|
|||||||
|
|
||||||
def "Wait until mined"() {
|
def "Wait until mined"() {
|
||||||
setup:
|
setup:
|
||||||
TrackBitcoinTx track = new TrackBitcoinTx(Stub(Upstreams))
|
TrackBitcoinTx track = new TrackBitcoinTx(Stub(MultistreamHolder))
|
||||||
def txid = "69cd44d7c641db82e69824523c7ac0c5c1e5628f025474529cf5ffe64527efc9"
|
def txid = "69cd44d7c641db82e69824523c7ac0c5c1e5628f025474529cf5ffe64527efc9"
|
||||||
// start with the current block
|
// start with the current block
|
||||||
def next = Flux.fromIterable([10, 12, 13]).map { h ->
|
def next = Flux.fromIterable([10, 12, 13]).map { h ->
|
||||||
new BlockContainer(h.longValue(), BlockId.from("0000000000000000000895d1b9d3898700e1deecc3b0e69f439aa77875e6042f"), BigInteger.ONE, Instant.now(), false, null, [])
|
new BlockContainer(h.longValue(), BlockId.from("0000000000000000000895d1b9d3898700e1deecc3b0e69f439aa77875e6042f"), BigInteger.ONE, Instant.now(), false, null, null, [])
|
||||||
}
|
}
|
||||||
Head head = Mock(Head) {
|
Head head = Mock(Head) {
|
||||||
1 * getFlux() >> next
|
1 * getFlux() >> next
|
||||||
}
|
}
|
||||||
DirectBitcoinApi api = Mock(DirectBitcoinApi) {
|
BitcoinReader api = Mock(BitcoinReader) {
|
||||||
3 * getTx(txid) >>> [
|
3 * getTx(txid) >>> [
|
||||||
Mono.just([
|
Mono.just([
|
||||||
txid: txid
|
txid: txid
|
||||||
@@ -191,9 +193,9 @@ class TrackBitcoinTxSpec extends Specification {
|
|||||||
])
|
])
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
Upstream upstream = Mock(BitcoinUpstream) {
|
BitcoinMultistream upstream = Mock(BitcoinMultistream) {
|
||||||
1 * getHead() >> head
|
1 * getHead() >> head
|
||||||
_ * getApi(_) >> Mono.just(api)
|
_ * getReader() >> api
|
||||||
}
|
}
|
||||||
def status = new TrackBitcoinTx.TxStatus(
|
def status = new TrackBitcoinTx.TxStatus(
|
||||||
txid, false, null, false, null, null, null, 0
|
txid, false, null, false, null, null, null, 0
|
||||||
@@ -210,13 +212,9 @@ class TrackBitcoinTxSpec extends Specification {
|
|||||||
|
|
||||||
def "Check mempool until found"() {
|
def "Check mempool until found"() {
|
||||||
setup:
|
setup:
|
||||||
TrackBitcoinTx track = new TrackBitcoinTx(Stub(Upstreams))
|
TrackBitcoinTx track = new TrackBitcoinTx(Stub(MultistreamHolder))
|
||||||
def txid = "69cd44d7c641db82e69824523c7ac0c5c1e5628f025474529cf5ffe64527efc9"
|
def txid = "69cd44d7c641db82e69824523c7ac0c5c1e5628f025474529cf5ffe64527efc9"
|
||||||
DirectBitcoinApi api = Mock(DirectBitcoinApi) {
|
|
||||||
1 * getTx(txid) >> Mono.just([
|
|
||||||
txid: txid
|
|
||||||
])
|
|
||||||
}
|
|
||||||
Head head = Mock(Head) {
|
Head head = Mock(Head) {
|
||||||
_ * getFlux() >> Flux.empty()
|
_ * getFlux() >> Flux.empty()
|
||||||
}
|
}
|
||||||
@@ -228,17 +226,20 @@ class TrackBitcoinTxSpec extends Specification {
|
|||||||
Mono.just(["4523c7ac0c5c1e5628f025474529c69cd44d7c641db82e6982f5ffe64527efc9", txid]) //second call when started over
|
Mono.just(["4523c7ac0c5c1e5628f025474529c69cd44d7c641db82e6982f5ffe64527efc9", txid]) //second call when started over
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
BitcoinUpstream upstream = Mock(BitcoinUpstream) {
|
BitcoinReader api = Mock(BitcoinReader) {
|
||||||
_ * getApi(_) >> Mono.just(api)
|
1 * getTx(txid) >> Mono.just([
|
||||||
|
txid: txid
|
||||||
|
])
|
||||||
|
_ * getMempool() >> mempoolAccess
|
||||||
|
}
|
||||||
|
BitcoinMultistream upstream = Mock(BitcoinMultistream) {
|
||||||
_ * getHead() >> head
|
_ * getHead() >> head
|
||||||
_ * getData() >> Mock(BitcoinReader) {
|
_ * getReader() >> api
|
||||||
_ * getMempool() >> mempoolAccess
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
when:
|
when:
|
||||||
def steps = StepVerifier.withVirtualTime {
|
def steps = StepVerifier.withVirtualTime {
|
||||||
track.untilFound(Chain.BITCOIN, api, upstream, txid).take(1)
|
track.untilFound(Chain.BITCOIN, upstream, txid).take(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
then:
|
then:
|
||||||
@@ -252,9 +253,9 @@ class TrackBitcoinTxSpec extends Specification {
|
|||||||
|
|
||||||
def "Subscribe to an existing tx"() {
|
def "Subscribe to an existing tx"() {
|
||||||
setup:
|
setup:
|
||||||
TrackBitcoinTx track = new TrackBitcoinTx(Stub(Upstreams))
|
TrackBitcoinTx track = new TrackBitcoinTx(Stub(MultistreamHolder))
|
||||||
def txid = "69cd44d7c641db82e69824523c7ac0c5c1e5628f025474529cf5ffe64527efc9"
|
def txid = "69cd44d7c641db82e69824523c7ac0c5c1e5628f025474529cf5ffe64527efc9"
|
||||||
DirectBitcoinApi api = Mock(DirectBitcoinApi) {
|
BitcoinReader api = Mock(BitcoinReader) {
|
||||||
_ * getTx(txid) >> Mono.just([
|
_ * getTx(txid) >> Mono.just([
|
||||||
txid : txid,
|
txid : txid,
|
||||||
blockhash: "0000000000000000000895d1b9d3898700e1deecc3b0e69f439aa77875e6042f",
|
blockhash: "0000000000000000000895d1b9d3898700e1deecc3b0e69f439aa77875e6042f",
|
||||||
@@ -267,18 +268,18 @@ class TrackBitcoinTxSpec extends Specification {
|
|||||||
])
|
])
|
||||||
}
|
}
|
||||||
def next = Flux.fromIterable([10, 11, 12]).map { h ->
|
def next = Flux.fromIterable([10, 11, 12]).map { h ->
|
||||||
new BlockContainer(h.longValue(), BlockId.from("0000000000000000000895d1b9d3898700e1deecc3b0e69f439aa77875e6042f"), BigInteger.ONE, Instant.now(), false, null, [])
|
new BlockContainer(h.longValue(), BlockId.from("0000000000000000000895d1b9d3898700e1deecc3b0e69f439aa77875e6042f"), BigInteger.ONE, Instant.now(), false, null, null, [])
|
||||||
}
|
}
|
||||||
Head head = Mock(Head) {
|
Head head = Mock(Head) {
|
||||||
_ * getFlux() >> next
|
_ * getFlux() >> next
|
||||||
}
|
}
|
||||||
BitcoinUpstream upstream = Mock(BitcoinUpstream) {
|
BitcoinMultistream upstream = Mock(BitcoinMultistream) {
|
||||||
_ * getApi(_) >> Mono.just(api)
|
_ * getReader() >> api
|
||||||
_ * getHead() >> head
|
_ * getHead() >> head
|
||||||
}
|
}
|
||||||
|
|
||||||
when:
|
when:
|
||||||
def act = track.subscribe(Chain.BITCOIN, api, upstream, txid)
|
def act = track.subscribe(Chain.BITCOIN, upstream, txid)
|
||||||
|
|
||||||
then:
|
then:
|
||||||
StepVerifier.create(act)
|
StepVerifier.create(act)
|
||||||
|
|||||||
@@ -19,21 +19,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.data.BlockContainer
|
import io.emeraldpay.dshackle.data.BlockContainer
|
||||||
import io.emeraldpay.dshackle.reader.Reader
|
|
||||||
import io.emeraldpay.dshackle.test.TestingCommons
|
import io.emeraldpay.dshackle.test.TestingCommons
|
||||||
import io.emeraldpay.dshackle.test.UpstreamsMock
|
import io.emeraldpay.dshackle.test.MultistreamHolderMock
|
||||||
import io.emeraldpay.dshackle.upstream.Upstreams
|
import io.emeraldpay.dshackle.upstream.MultistreamHolder
|
||||||
import io.emeraldpay.dshackle.upstream.ethereum.EthereumReader
|
|
||||||
import io.emeraldpay.grpc.Chain
|
import io.emeraldpay.grpc.Chain
|
||||||
import io.infinitape.etherjar.domain.Address
|
|
||||||
import io.infinitape.etherjar.domain.BlockHash
|
import io.infinitape.etherjar.domain.BlockHash
|
||||||
import io.infinitape.etherjar.rpc.ReactorRpcClient
|
|
||||||
import io.infinitape.etherjar.rpc.RpcCall
|
|
||||||
import io.infinitape.etherjar.rpc.json.BlockJson
|
import io.infinitape.etherjar.rpc.json.BlockJson
|
||||||
import reactor.core.publisher.Flux
|
|
||||||
import reactor.core.publisher.Mono
|
|
||||||
import reactor.core.publisher.TopicProcessor
|
|
||||||
import reactor.core.scheduler.Schedulers
|
|
||||||
import reactor.test.StepVerifier
|
import reactor.test.StepVerifier
|
||||||
import spock.lang.Specification
|
import spock.lang.Specification
|
||||||
|
|
||||||
@@ -64,9 +55,9 @@ class TrackEthereumAddressSpec extends Specification {
|
|||||||
.setBalance("1234567890")
|
.setBalance("1234567890")
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
def apiMock = TestingCommons.api(Stub(ReactorRpcClient))
|
def apiMock = TestingCommons.api()
|
||||||
def upstreamMock = TestingCommons.upstream(apiMock)
|
def upstreamMock = TestingCommons.upstream(apiMock)
|
||||||
Upstreams upstreams = new UpstreamsMock(Chain.ETHEREUM, upstreamMock)
|
MultistreamHolder upstreams = new MultistreamHolderMock(Chain.ETHEREUM, upstreamMock)
|
||||||
TrackEthereumAddress trackAddress = new TrackEthereumAddress(upstreams)
|
TrackEthereumAddress trackAddress = new TrackEthereumAddress(upstreams)
|
||||||
|
|
||||||
apiMock.answer("eth_getBalance", ["0xe2c8fa8120d813cd0b5e6add120295bf20cfa09f", "latest"], "0x499602D2")
|
apiMock.answer("eth_getBalance", ["0xe2c8fa8120d813cd0b5e6add120295bf20cfa09f", "latest"], "0x499602D2")
|
||||||
@@ -104,9 +95,9 @@ class TrackEthereumAddressSpec extends Specification {
|
|||||||
return it
|
return it
|
||||||
}
|
}
|
||||||
|
|
||||||
def apiMock = TestingCommons.api(Stub(ReactorRpcClient))
|
def apiMock = TestingCommons.api()
|
||||||
def upstreamMock = TestingCommons.upstream(apiMock)
|
def upstreamMock = TestingCommons.upstream(apiMock)
|
||||||
Upstreams upstreams = new UpstreamsMock(Chain.ETHEREUM, upstreamMock)
|
MultistreamHolder upstreams = new MultistreamHolderMock(Chain.ETHEREUM, upstreamMock)
|
||||||
TrackEthereumAddress trackAddress = new TrackEthereumAddress(upstreams)
|
TrackEthereumAddress trackAddress = new TrackEthereumAddress(upstreams)
|
||||||
|
|
||||||
apiMock.answerOnce("eth_getBalance", ["0xe2c8fa8120d813cd0b5e6add120295bf20cfa09f", "latest"], "0x499602D2")
|
apiMock.answerOnce("eth_getBalance", ["0xe2c8fa8120d813cd0b5e6add120295bf20cfa09f", "latest"], "0x499602D2")
|
||||||
@@ -121,6 +112,6 @@ class TrackEthereumAddressSpec extends Specification {
|
|||||||
}
|
}
|
||||||
.expectNext(exp2).as("Second block")
|
.expectNext(exp2).as("Second block")
|
||||||
.thenCancel()
|
.thenCancel()
|
||||||
.verify(Duration.ofSeconds(1))
|
.verify(Duration.ofSeconds(2))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,15 +23,13 @@ import io.emeraldpay.dshackle.data.BlockContainer
|
|||||||
import io.emeraldpay.dshackle.data.BlockId
|
import io.emeraldpay.dshackle.data.BlockId
|
||||||
import io.emeraldpay.dshackle.data.TxId
|
import io.emeraldpay.dshackle.data.TxId
|
||||||
import io.emeraldpay.dshackle.test.TestingCommons
|
import io.emeraldpay.dshackle.test.TestingCommons
|
||||||
import io.emeraldpay.dshackle.test.UpstreamsMock
|
import io.emeraldpay.dshackle.test.MultistreamHolderMock
|
||||||
import io.emeraldpay.dshackle.upstream.Head
|
import io.emeraldpay.dshackle.upstream.Head
|
||||||
import io.emeraldpay.dshackle.upstream.Upstreams
|
import io.emeraldpay.dshackle.upstream.MultistreamHolder
|
||||||
import io.emeraldpay.dshackle.upstream.ethereum.EthereumApi
|
import io.emeraldpay.dshackle.upstream.ethereum.EthereumMultistream
|
||||||
import io.emeraldpay.dshackle.upstream.ethereum.AggregatedEthereumUpstreams
|
|
||||||
import io.emeraldpay.grpc.Chain
|
import io.emeraldpay.grpc.Chain
|
||||||
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.ReactorRpcClient
|
|
||||||
import io.infinitape.etherjar.rpc.json.BlockJson
|
import io.infinitape.etherjar.rpc.json.BlockJson
|
||||||
import io.infinitape.etherjar.rpc.json.TransactionJson
|
import io.infinitape.etherjar.rpc.json.TransactionJson
|
||||||
import io.infinitape.etherjar.rpc.json.TransactionRefJson
|
import io.infinitape.etherjar.rpc.json.TransactionRefJson
|
||||||
@@ -98,9 +96,9 @@ class TrackEthereumTxSpec extends Specification {
|
|||||||
.setTimestamp(blockJson.timestamp.toEpochMilli())
|
.setTimestamp(blockJson.timestamp.toEpochMilli())
|
||||||
).build()
|
).build()
|
||||||
|
|
||||||
def apiMock = TestingCommons.api(Stub(ReactorRpcClient))
|
def apiMock = TestingCommons.api()
|
||||||
def upstreamMock = TestingCommons.upstream(apiMock)
|
def upstreamMock = TestingCommons.upstream(apiMock)
|
||||||
Upstreams upstreams = new UpstreamsMock(Chain.ETHEREUM, upstreamMock)
|
MultistreamHolder upstreams = new MultistreamHolderMock(Chain.ETHEREUM, upstreamMock)
|
||||||
TrackEthereumTx trackTx = new TrackEthereumTx(upstreams)
|
TrackEthereumTx trackTx = new TrackEthereumTx(upstreams)
|
||||||
|
|
||||||
apiMock.answer("eth_getTransactionByHash", [txId], txJson)
|
apiMock.answer("eth_getTransactionByHash", [txId], txJson)
|
||||||
@@ -118,10 +116,10 @@ class TrackEthereumTxSpec extends Specification {
|
|||||||
|
|
||||||
def "Wait for unknown transaction"() {
|
def "Wait for unknown transaction"() {
|
||||||
setup:
|
setup:
|
||||||
def apiMock = TestingCommons.api(Stub(ReactorRpcClient))
|
def apiMock = TestingCommons.api()
|
||||||
def upstreamMock = TestingCommons.upstream(apiMock)
|
def upstreamMock = TestingCommons.upstream(apiMock)
|
||||||
Upstreams upstreams = new UpstreamsMock(Chain.ETHEREUM, upstreamMock)
|
MultistreamHolder upstreams = new MultistreamHolderMock(Chain.ETHEREUM, upstreamMock)
|
||||||
((AggregatedEthereumUpstreams) upstreams.getUpstream(Chain.ETHEREUM)).head = Mock(Head) {
|
((EthereumMultistream) upstreams.getUpstream(Chain.ETHEREUM)).head = Mock(Head) {
|
||||||
_ * getFlux() >> Flux.empty()
|
_ * getFlux() >> Flux.empty()
|
||||||
}
|
}
|
||||||
TrackEthereumTx trackTx = new TrackEthereumTx(upstreams)
|
TrackEthereumTx trackTx = new TrackEthereumTx(upstreams)
|
||||||
@@ -133,7 +131,7 @@ class TrackEthereumTxSpec extends Specification {
|
|||||||
when:
|
when:
|
||||||
def tx = new TrackEthereumTx.TxDetails(Chain.ETHEREUM, Instant.now(), TransactionId.from(txId), 6)
|
def tx = new TrackEthereumTx.TxDetails(Chain.ETHEREUM, Instant.now(), TransactionId.from(txId), 6)
|
||||||
def act = StepVerifier.withVirtualTime(
|
def act = StepVerifier.withVirtualTime(
|
||||||
{ trackTx.subscribe(tx, upstreams.getUpstream(Chain.ETHEREUM).castApi(EthereumApi.class)) },
|
{ trackTx.subscribe(tx, upstreams.getUpstream(Chain.ETHEREUM).cast(EthereumMultistream)) },
|
||||||
{ scheduler },
|
{ scheduler },
|
||||||
5)
|
5)
|
||||||
|
|
||||||
@@ -168,9 +166,9 @@ class TrackEthereumTxSpec extends Specification {
|
|||||||
it
|
it
|
||||||
}
|
}
|
||||||
|
|
||||||
def apiMock = TestingCommons.api(Stub(ReactorRpcClient))
|
def apiMock = TestingCommons.api()
|
||||||
def upstreamMock = TestingCommons.upstream(apiMock)
|
def upstreamMock = TestingCommons.upstream(apiMock)
|
||||||
Upstreams upstreams = new UpstreamsMock(Chain.ETHEREUM, upstreamMock)
|
MultistreamHolder upstreams = new MultistreamHolderMock(Chain.ETHEREUM, upstreamMock)
|
||||||
TrackEthereumTx trackTx = new TrackEthereumTx(upstreams)
|
TrackEthereumTx trackTx = new TrackEthereumTx(upstreams)
|
||||||
def scheduler = VirtualTimeScheduler.create(true)
|
def scheduler = VirtualTimeScheduler.create(true)
|
||||||
trackTx.scheduler = scheduler
|
trackTx.scheduler = scheduler
|
||||||
@@ -193,14 +191,14 @@ class TrackEthereumTxSpec extends Specification {
|
|||||||
|
|
||||||
def "New block makes tx mined"() {
|
def "New block makes tx mined"() {
|
||||||
setup:
|
setup:
|
||||||
def apiMock = TestingCommons.api(Stub(ReactorRpcClient))
|
def apiMock = TestingCommons.api()
|
||||||
def upstreamMock = TestingCommons.upstream(apiMock)
|
def upstreamMock = TestingCommons.upstream(apiMock)
|
||||||
Upstreams upstreams = new UpstreamsMock(Chain.ETHEREUM, upstreamMock)
|
MultistreamHolder upstreams = new MultistreamHolderMock(Chain.ETHEREUM, upstreamMock)
|
||||||
TrackEthereumTx trackTx = new TrackEthereumTx(upstreams)
|
TrackEthereumTx trackTx = new TrackEthereumTx(upstreams)
|
||||||
|
|
||||||
def tx = new TrackEthereumTx.TxDetails(Chain.ETHEREUM, Instant.now(), TransactionId.from(txId), 6)
|
def tx = new TrackEthereumTx.TxDetails(Chain.ETHEREUM, Instant.now(), TransactionId.from(txId), 6)
|
||||||
def block = new BlockContainer(
|
def block = new BlockContainer(
|
||||||
100, BlockId.from(txId), BigInteger.ONE, Instant.now(), false, "".bytes,
|
100, BlockId.from(txId), BigInteger.ONE, Instant.now(), false, "".bytes, null,
|
||||||
[TxId.from(txId)]
|
[TxId.from(txId)]
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -215,14 +213,14 @@ class TrackEthereumTxSpec extends Specification {
|
|||||||
|
|
||||||
def "New block without current tx requires a call"() {
|
def "New block without current tx requires a call"() {
|
||||||
setup:
|
setup:
|
||||||
def apiMock = TestingCommons.api(Stub(ReactorRpcClient))
|
def apiMock = TestingCommons.api()
|
||||||
def upstreamMock = TestingCommons.upstream(apiMock)
|
def upstreamMock = TestingCommons.upstream(apiMock)
|
||||||
Upstreams upstreams = new UpstreamsMock(Chain.ETHEREUM, upstreamMock)
|
MultistreamHolder upstreams = new MultistreamHolderMock(Chain.ETHEREUM, upstreamMock)
|
||||||
TrackEthereumTx trackTx = new TrackEthereumTx(upstreams)
|
TrackEthereumTx trackTx = new TrackEthereumTx(upstreams)
|
||||||
|
|
||||||
def tx = new TrackEthereumTx.TxDetails(Chain.ETHEREUM, Instant.now(), TransactionId.from(txId), 6)
|
def tx = new TrackEthereumTx.TxDetails(Chain.ETHEREUM, Instant.now(), TransactionId.from(txId), 6)
|
||||||
def block = new BlockContainer(
|
def block = new BlockContainer(
|
||||||
100, BlockId.from(txId), BigInteger.ONE, Instant.now(), false, "".bytes,
|
100, BlockId.from(txId), BigInteger.ONE, Instant.now(), false, "".bytes, null,
|
||||||
[TxId.from("0xa0e65cbc1b52a8ca60562112c6060552d882f16f34a9dba2ccdc05c0a6a27c22")]
|
[TxId.from("0xa0e65cbc1b52a8ca60562112c6060552d882f16f34a9dba2ccdc05c0a6a27c22")]
|
||||||
)
|
)
|
||||||
apiMock.answer("eth_getTransactionByHash", [txId], null)
|
apiMock.answer("eth_getTransactionByHash", [txId], null)
|
||||||
@@ -288,9 +286,9 @@ class TrackEthereumTxSpec extends Specification {
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def apiMock = TestingCommons.api(Stub(ReactorRpcClient))
|
def apiMock = TestingCommons.api()
|
||||||
def upstreamMock = TestingCommons.upstream(apiMock)
|
def upstreamMock = TestingCommons.upstream(apiMock)
|
||||||
Upstreams upstreams = new UpstreamsMock(Chain.ETHEREUM, upstreamMock)
|
MultistreamHolder upstreams = new MultistreamHolderMock(Chain.ETHEREUM, upstreamMock)
|
||||||
TrackEthereumTx trackTx = new TrackEthereumTx(upstreams)
|
TrackEthereumTx trackTx = new TrackEthereumTx(upstreams)
|
||||||
|
|
||||||
apiMock.answerOnce("eth_getTransactionByHash", [txId], null)
|
apiMock.answerOnce("eth_getTransactionByHash", [txId], null)
|
||||||
|
|||||||
@@ -19,11 +19,10 @@ package io.emeraldpay.dshackle.test
|
|||||||
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.calls.DirectCallMethods
|
import io.emeraldpay.dshackle.reader.Reader
|
||||||
import io.emeraldpay.dshackle.upstream.ethereum.DirectEthereumApi
|
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
|
||||||
import io.emeraldpay.grpc.Chain
|
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
|
||||||
import io.grpc.stub.StreamObserver
|
import io.grpc.stub.StreamObserver
|
||||||
import io.infinitape.etherjar.rpc.ReactorRpcClient
|
|
||||||
import io.infinitape.etherjar.rpc.RpcResponseError
|
import io.infinitape.etherjar.rpc.RpcResponseError
|
||||||
import io.infinitape.etherjar.rpc.json.ResponseJson
|
import io.infinitape.etherjar.rpc.json.ResponseJson
|
||||||
import org.jetbrains.annotations.NotNull
|
import org.jetbrains.annotations.NotNull
|
||||||
@@ -31,16 +30,18 @@ import org.slf4j.Logger
|
|||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import reactor.core.publisher.Mono
|
import reactor.core.publisher.Mono
|
||||||
|
|
||||||
|
import java.time.Duration
|
||||||
import java.util.concurrent.Callable
|
import java.util.concurrent.Callable
|
||||||
|
|
||||||
class EthereumApiMock extends DirectEthereumApi {
|
class EthereumApiMock implements Reader<JsonRpcRequest, JsonRpcResponse> {
|
||||||
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(this)
|
private static final Logger log = LoggerFactory.getLogger(this)
|
||||||
List<PredefinedResponse> predefined = []
|
List<PredefinedResponse> predefined = []
|
||||||
private ObjectMapper objectMapper
|
private ObjectMapper objectMapper
|
||||||
|
|
||||||
EthereumApiMock(@NotNull ReactorRpcClient rpcClient, @NotNull ObjectMapper objectMapper, @NotNull Chain chain) {
|
String id = "default"
|
||||||
super(rpcClient, null, objectMapper, new DirectCallMethods())
|
|
||||||
|
EthereumApiMock(@NotNull ObjectMapper objectMapper) {
|
||||||
this.objectMapper = objectMapper
|
this.objectMapper = objectMapper
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,10 +56,11 @@ class EthereumApiMock extends DirectEthereumApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
Mono<byte[]> execute(int id, @NotNull String method, @NotNull List<?> params) {
|
Mono<JsonRpcResponse> read(JsonRpcRequest request) {
|
||||||
Callable<byte[]> call = {
|
Callable<JsonRpcResponse> call = {
|
||||||
def predefined = predefined.find { it.isSame(id, method, params) }
|
def predefined = predefined.find { it.isSame(request.method, request.params) }
|
||||||
ResponseJson json = new ResponseJson<Object, Integer>(id: id)
|
byte[] result = null
|
||||||
|
JsonRpcResponse.ResponseError error = null
|
||||||
if (predefined != null) {
|
if (predefined != null) {
|
||||||
if (predefined.exception != null) {
|
if (predefined.exception != null) {
|
||||||
predefined.onCalled()
|
predefined.onCalled()
|
||||||
@@ -66,32 +68,37 @@ class EthereumApiMock extends DirectEthereumApi {
|
|||||||
throw predefined.exception
|
throw predefined.exception
|
||||||
}
|
}
|
||||||
if (predefined.result instanceof RpcResponseError) {
|
if (predefined.result instanceof RpcResponseError) {
|
||||||
json.error = predefined.result
|
((RpcResponseError) predefined.result).with { err ->
|
||||||
|
error = new JsonRpcResponse.ResponseError(err.code, err.message)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
json.result = predefined.result
|
// ResponseJson json = new ResponseJson<Object, Integer>(id: 1, result: predefined.result)
|
||||||
|
result = objectMapper.writeValueAsBytes(predefined.result)
|
||||||
}
|
}
|
||||||
predefined.onCalled()
|
predefined.onCalled()
|
||||||
predefined.print()
|
predefined.print()
|
||||||
} else {
|
} else {
|
||||||
log.error("Method ${method} with ${params} is not mocked")
|
log.error("Method ${request.method} with ${request.params} is not mocked")
|
||||||
json.error = new RpcResponseError(-32601, "Method ${method} with ${params} is not mocked")
|
error = new JsonRpcResponse.ResponseError(-32601, "Method ${request.method} with ${request.params} is not mocked")
|
||||||
}
|
}
|
||||||
byte[] result = objectMapper.writeValueAsBytes(json)
|
return new JsonRpcResponse(result, error)
|
||||||
return result
|
} as Callable<JsonRpcResponse>
|
||||||
} as Callable<byte[]>
|
|
||||||
return Mono.fromCallable(call)
|
return Mono.fromCallable(call)
|
||||||
}
|
}
|
||||||
|
|
||||||
def nativeCall(BlockchainOuterClass.NativeCallRequest request, StreamObserver<BlockchainOuterClass.NativeCallReplyItem> responseObserver) {
|
def nativeCall(BlockchainOuterClass.NativeCallRequest request, StreamObserver<BlockchainOuterClass.NativeCallReplyItem> responseObserver) {
|
||||||
request.itemsList.forEach { req ->
|
request.itemsList.forEach { req ->
|
||||||
def resp = execute(req.id, req.method, objectMapper.readerFor(List).readValue(req.payload.toByteArray()))
|
JsonRpcResponse resp = read(new JsonRpcRequest(req.method, objectMapper.readerFor(List).readValue(req.payload.toByteArray())))
|
||||||
resp.subscribe {
|
.block(Duration.ofSeconds(5))
|
||||||
def proto = BlockchainOuterClass.NativeCallReplyItem.newBuilder()
|
def proto = BlockchainOuterClass.NativeCallReplyItem.newBuilder()
|
||||||
.setId(req.id)
|
.setId(req.id)
|
||||||
.setSucceed(true)
|
.setSucceed(resp.hasResult())
|
||||||
.setPayload(ByteString.copyFrom(resp.block()))
|
.setPayload(ByteString.copyFrom(resp.getResult()))
|
||||||
responseObserver.onNext(proto.build())
|
|
||||||
|
resp.error?.with { err ->
|
||||||
|
proto.setErrorMessage(err.message)
|
||||||
}
|
}
|
||||||
|
responseObserver.onNext(proto.build())
|
||||||
}
|
}
|
||||||
responseObserver.onCompleted()
|
responseObserver.onCompleted()
|
||||||
}
|
}
|
||||||
@@ -103,7 +110,7 @@ class EthereumApiMock extends DirectEthereumApi {
|
|||||||
Integer limit
|
Integer limit
|
||||||
Throwable exception
|
Throwable exception
|
||||||
|
|
||||||
boolean isSame(int id, String method, List<?> params) {
|
boolean isSame(String method, List<?> params) {
|
||||||
if (limit != null) {
|
if (limit != null) {
|
||||||
if (limit <= 0) {
|
if (limit <= 0) {
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -16,28 +16,20 @@
|
|||||||
*/
|
*/
|
||||||
package io.emeraldpay.dshackle.test
|
package io.emeraldpay.dshackle.test
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
import io.emeraldpay.dshackle.reader.Reader
|
||||||
import io.emeraldpay.dshackle.upstream.calls.DirectCallMethods
|
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
|
||||||
import io.emeraldpay.dshackle.upstream.ethereum.DirectEthereumApi
|
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
|
||||||
import io.infinitape.etherjar.rpc.ReactorBatch
|
|
||||||
import io.infinitape.etherjar.rpc.ReactorRpcClient
|
|
||||||
import io.infinitape.etherjar.rpc.RpcCall
|
|
||||||
import io.infinitape.etherjar.rpc.RpcCallResponse
|
|
||||||
import reactor.core.publisher.Flux
|
|
||||||
import reactor.core.publisher.Mono
|
import reactor.core.publisher.Mono
|
||||||
|
|
||||||
class EthereumApiStub extends DirectEthereumApi {
|
class EthereumApiStub implements Reader<JsonRpcRequest, JsonRpcResponse> {
|
||||||
|
|
||||||
private String id
|
private String id
|
||||||
private static ObjectMapper objectMapper = TestingCommons.objectMapper()
|
|
||||||
private static ReactorRpcClient rpcClient = new RpcClientMock();
|
|
||||||
|
|
||||||
EthereumApiStub(Integer id) {
|
EthereumApiStub(Integer id) {
|
||||||
this(id.toString())
|
this(id.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
EthereumApiStub(String id) {
|
EthereumApiStub(String id) {
|
||||||
super(rpcClient, null, objectMapper, new DirectCallMethods())
|
|
||||||
this.id = id
|
this.id = id
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,16 +38,9 @@ class EthereumApiStub extends DirectEthereumApi {
|
|||||||
return "API Stub $id"
|
return "API Stub $id"
|
||||||
}
|
}
|
||||||
|
|
||||||
static class RpcClientMock implements ReactorRpcClient {
|
@Override
|
||||||
|
Mono<JsonRpcResponse> read(JsonRpcRequest key) {
|
||||||
@Override
|
return Mono.error(new Exception("Not implemented in mock"))
|
||||||
Flux<RpcCallResponse> execute(ReactorBatch batch) {
|
|
||||||
return Flux.error(new Exception("Not implemented in mock"))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
def <JS, RES> Mono<RES> execute(RpcCall<JS, RES> call) {
|
|
||||||
return Mono.error(new Exception("Not implemented in mock"))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ package io.emeraldpay.dshackle.test
|
|||||||
|
|
||||||
import io.emeraldpay.dshackle.data.BlockContainer
|
import io.emeraldpay.dshackle.data.BlockContainer
|
||||||
import io.emeraldpay.dshackle.upstream.Head
|
import io.emeraldpay.dshackle.upstream.Head
|
||||||
|
import org.jetbrains.annotations.NotNull
|
||||||
import org.reactivestreams.Publisher
|
import org.reactivestreams.Publisher
|
||||||
import reactor.core.publisher.Flux
|
import reactor.core.publisher.Flux
|
||||||
import reactor.core.publisher.Mono
|
import reactor.core.publisher.Mono
|
||||||
@@ -28,9 +29,14 @@ class EthereumHeadMock implements Head {
|
|||||||
private TopicProcessor<BlockContainer> bus = TopicProcessor.create()
|
private TopicProcessor<BlockContainer> bus = TopicProcessor.create()
|
||||||
private Publisher<BlockContainer> predefined = null
|
private Publisher<BlockContainer> predefined = null
|
||||||
private BlockContainer latest
|
private BlockContainer latest
|
||||||
|
private List<Runnable> handlers = []
|
||||||
|
|
||||||
void nextBlock(BlockContainer block) {
|
void nextBlock(BlockContainer block) {
|
||||||
|
handlers.forEach {
|
||||||
|
it.run()
|
||||||
|
}
|
||||||
assert block != null
|
assert block != null
|
||||||
|
println("New block: ${block.height} / ${block.hash}")
|
||||||
latest = block
|
latest = block
|
||||||
bus.onNext(block)
|
bus.onNext(block)
|
||||||
}
|
}
|
||||||
@@ -51,4 +57,9 @@ class EthereumHeadMock implements Head {
|
|||||||
return Flux.concat(Mono.justOrEmpty(latest), bus).distinctUntilChanged()
|
return Flux.concat(Mono.justOrEmpty(latest), bus).distinctUntilChanged()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
void onBeforeBlock(@NotNull Runnable handler) {
|
||||||
|
handlers.add(handler)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,18 +16,22 @@
|
|||||||
*/
|
*/
|
||||||
package io.emeraldpay.dshackle.test
|
package io.emeraldpay.dshackle.test
|
||||||
|
|
||||||
|
|
||||||
import io.emeraldpay.dshackle.config.UpstreamsConfig
|
import io.emeraldpay.dshackle.config.UpstreamsConfig
|
||||||
import io.emeraldpay.dshackle.data.BlockContainer
|
import io.emeraldpay.dshackle.data.BlockContainer
|
||||||
import io.emeraldpay.dshackle.upstream.Head
|
import io.emeraldpay.dshackle.upstream.Head
|
||||||
|
import io.emeraldpay.dshackle.upstream.calls.AggregatedCallMethods
|
||||||
import io.emeraldpay.dshackle.upstream.calls.CallMethods
|
import io.emeraldpay.dshackle.upstream.calls.CallMethods
|
||||||
import io.emeraldpay.dshackle.startup.QuorumForLabels
|
import io.emeraldpay.dshackle.startup.QuorumForLabels
|
||||||
|
import io.emeraldpay.dshackle.upstream.calls.DefaultBitcoinMethods
|
||||||
import io.emeraldpay.dshackle.upstream.calls.DefaultEthereumMethods
|
import io.emeraldpay.dshackle.upstream.calls.DefaultEthereumMethods
|
||||||
import io.emeraldpay.dshackle.upstream.ethereum.DirectEthereumApi
|
import io.emeraldpay.dshackle.upstream.calls.DirectCallMethods
|
||||||
import io.emeraldpay.dshackle.upstream.ethereum.EthereumUpstream
|
import io.emeraldpay.dshackle.upstream.ethereum.EthereumUpstream
|
||||||
import io.emeraldpay.dshackle.upstream.UpstreamAvailability
|
import io.emeraldpay.dshackle.upstream.UpstreamAvailability
|
||||||
|
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
|
||||||
|
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
|
||||||
|
import io.emeraldpay.dshackle.reader.Reader
|
||||||
import io.emeraldpay.grpc.Chain
|
import io.emeraldpay.grpc.Chain
|
||||||
import io.infinitape.etherjar.domain.TransactionId
|
|
||||||
import io.infinitape.etherjar.rpc.json.BlockJson
|
|
||||||
import org.jetbrains.annotations.NotNull
|
import org.jetbrains.annotations.NotNull
|
||||||
import org.reactivestreams.Publisher
|
import org.reactivestreams.Publisher
|
||||||
|
|
||||||
@@ -35,24 +39,33 @@ class EthereumUpstreamMock extends EthereumUpstream {
|
|||||||
|
|
||||||
EthereumHeadMock ethereumHeadMock = new EthereumHeadMock()
|
EthereumHeadMock ethereumHeadMock = new EthereumHeadMock()
|
||||||
|
|
||||||
EthereumUpstreamMock(@NotNull Chain chain, @NotNull DirectEthereumApi api) {
|
static CallMethods allMethods() {
|
||||||
this(chain, api, new DefaultEthereumMethods(TestingCommons.objectMapper(), chain))
|
new AggregatedCallMethods([
|
||||||
|
new DefaultEthereumMethods(TestingCommons.objectMapper(), Chain.ETHEREUM),
|
||||||
|
new DefaultBitcoinMethods(TestingCommons.objectMapper()),
|
||||||
|
new DirectCallMethods(["eth_test"])
|
||||||
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
EthereumUpstreamMock(@NotNull String id, @NotNull Chain chain, @NotNull DirectEthereumApi api) {
|
EthereumUpstreamMock(@NotNull Chain chain, @NotNull Reader<JsonRpcRequest, JsonRpcResponse> api) {
|
||||||
this(id, chain, api, new DefaultEthereumMethods(TestingCommons.objectMapper(), chain))
|
this(chain, api, allMethods())
|
||||||
}
|
}
|
||||||
|
|
||||||
EthereumUpstreamMock(@NotNull Chain chain, @NotNull DirectEthereumApi api, CallMethods methods) {
|
EthereumUpstreamMock(@NotNull String id, @NotNull Chain chain, @NotNull Reader<JsonRpcRequest, JsonRpcResponse> api) {
|
||||||
|
this(id, chain, api, allMethods())
|
||||||
|
}
|
||||||
|
|
||||||
|
EthereumUpstreamMock(@NotNull Chain chain, @NotNull Reader<JsonRpcRequest, JsonRpcResponse> api, CallMethods methods) {
|
||||||
this("test", chain, api, methods)
|
this("test", chain, api, methods)
|
||||||
}
|
}
|
||||||
|
|
||||||
EthereumUpstreamMock(@NotNull String id, @NotNull Chain chain, @NotNull DirectEthereumApi api, CallMethods methods) {
|
EthereumUpstreamMock(@NotNull String id, @NotNull Chain chain, @NotNull Reader<JsonRpcRequest, JsonRpcResponse> api, CallMethods methods) {
|
||||||
super(id, chain, api, null,
|
super(id, chain, api, null,
|
||||||
UpstreamsConfig.Options.getDefaults(), new QuorumForLabels.QuorumItem(1, new UpstreamsConfig.Labels()),
|
UpstreamsConfig.Options.getDefaults(), new QuorumForLabels.QuorumItem(1, new UpstreamsConfig.Labels()),
|
||||||
methods, TestingCommons.objectMapper())
|
methods, TestingCommons.objectMapper())
|
||||||
setLag(0)
|
setLag(0)
|
||||||
setStatus(UpstreamAvailability.OK)
|
setStatus(UpstreamAvailability.OK)
|
||||||
|
start()
|
||||||
}
|
}
|
||||||
|
|
||||||
void nextBlock(BlockContainer block) {
|
void nextBlock(BlockContainer block) {
|
||||||
|
|||||||
@@ -16,48 +16,60 @@
|
|||||||
*/
|
*/
|
||||||
package io.emeraldpay.dshackle.test
|
package io.emeraldpay.dshackle.test
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
|
||||||
|
import io.emeraldpay.dshackle.BlockchainType
|
||||||
import io.emeraldpay.dshackle.cache.Caches
|
import io.emeraldpay.dshackle.cache.Caches
|
||||||
import io.emeraldpay.dshackle.upstream.AggregatedUpstream
|
import io.emeraldpay.dshackle.upstream.Multistream
|
||||||
|
import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinMultistream
|
||||||
|
import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinUpstream
|
||||||
import io.emeraldpay.dshackle.upstream.calls.DefaultEthereumMethods
|
import io.emeraldpay.dshackle.upstream.calls.DefaultEthereumMethods
|
||||||
import io.emeraldpay.dshackle.upstream.Upstream
|
import io.emeraldpay.dshackle.upstream.Upstream
|
||||||
import io.emeraldpay.dshackle.upstream.Upstreams
|
import io.emeraldpay.dshackle.upstream.MultistreamHolder
|
||||||
import io.emeraldpay.dshackle.upstream.ethereum.AggregatedEthereumUpstreams
|
import io.emeraldpay.dshackle.upstream.ethereum.EthereumMultistream
|
||||||
import io.emeraldpay.dshackle.upstream.ethereum.EthereumReader
|
import io.emeraldpay.dshackle.upstream.ethereum.EthereumReader
|
||||||
import io.emeraldpay.dshackle.upstream.ethereum.EthereumUpstream
|
import io.emeraldpay.dshackle.upstream.ethereum.EthereumUpstream
|
||||||
import io.emeraldpay.grpc.Chain
|
import io.emeraldpay.grpc.Chain
|
||||||
import org.jetbrains.annotations.NotNull
|
import org.jetbrains.annotations.NotNull
|
||||||
import reactor.core.publisher.Flux
|
import reactor.core.publisher.Flux
|
||||||
|
|
||||||
class UpstreamsMock implements Upstreams {
|
class MultistreamHolderMock implements MultistreamHolder {
|
||||||
|
|
||||||
private Map<Chain, DefaultEthereumMethods> target = [:]
|
private Map<Chain, DefaultEthereumMethods> target = [:]
|
||||||
private Map<Chain, AggregatedEthereumUpstreamsMock> upstreams = [:]
|
private Map<Chain, Multistream> upstreams = [:]
|
||||||
|
|
||||||
UpstreamsMock(Chain chain, Upstream up) {
|
MultistreamHolderMock(Chain chain, Upstream up) {
|
||||||
addUpstream(chain, up)
|
addUpstream(chain, up)
|
||||||
}
|
}
|
||||||
UpstreamsMock(Chain chain1, Upstream up1, Chain chain2, Upstream up2) {
|
|
||||||
addUpstream(chain1, up1)
|
|
||||||
addUpstream(chain2, up2)
|
|
||||||
}
|
|
||||||
|
|
||||||
AggregatedUpstream addUpstream(@NotNull Chain chain, @NotNull EthereumUpstream up) {
|
Multistream addUpstream(@NotNull Chain chain, @NotNull Upstream up) {
|
||||||
if (!upstreams.containsKey(chain)) {
|
if (!upstreams.containsKey(chain)) {
|
||||||
upstreams[chain] = new AggregatedEthereumUpstreamsMock(chain, [up], Caches.default(TestingCommons.objectMapper()), TestingCommons.objectMapper())
|
if (BlockchainType.fromBlockchain(chain) == BlockchainType.ETHEREUM) {
|
||||||
upstreams[chain].start()
|
if (up instanceof EthereumMultistream) {
|
||||||
|
upstreams[chain] = up
|
||||||
|
} else if (up instanceof EthereumUpstream) {
|
||||||
|
upstreams[chain] = new EthereumMultistreamMock(chain, [up as EthereumUpstream], Caches.default(TestingCommons.objectMapper()))
|
||||||
|
} else {
|
||||||
|
throw new IllegalArgumentException("Unsupported upstream type ${up.class}")
|
||||||
|
}
|
||||||
|
upstreams[chain].start()
|
||||||
|
} else if (BlockchainType.fromBlockchain(chain) == BlockchainType.BITCOIN) {
|
||||||
|
if (up instanceof BitcoinMultistream) {
|
||||||
|
upstreams[chain] = up
|
||||||
|
} else if (up instanceof BitcoinUpstream) {
|
||||||
|
upstreams[chain] = new BitcoinMultistream(chain, [up as BitcoinUpstream], Caches.default(TestingCommons.objectMapper()), TestingCommons.objectMapper())
|
||||||
|
} else {
|
||||||
|
throw new IllegalArgumentException("Unsupported upstream type ${up.class}")
|
||||||
|
}
|
||||||
|
upstreams[chain].start()
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
upstreams[chain].addUpstream(up)
|
upstreams[chain].addUpstream(up)
|
||||||
}
|
}
|
||||||
return upstreams[chain]
|
return upstreams[chain]
|
||||||
}
|
}
|
||||||
|
|
||||||
void setReader(@NotNull Chain chain, EthereumReader reader) {
|
|
||||||
upstreams[chain].customReader = reader
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
AggregatedUpstream getUpstream(@NotNull Chain chain) {
|
Multistream getUpstream(@NotNull Chain chain) {
|
||||||
return upstreams[chain]
|
return upstreams[chain]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,12 +97,12 @@ class UpstreamsMock implements Upstreams {
|
|||||||
return upstreams.containsKey(chain)
|
return upstreams.containsKey(chain)
|
||||||
}
|
}
|
||||||
|
|
||||||
static class AggregatedEthereumUpstreamsMock extends AggregatedEthereumUpstreams {
|
static class EthereumMultistreamMock extends EthereumMultistream {
|
||||||
|
|
||||||
EthereumReader customReader = null
|
EthereumReader customReader = null
|
||||||
|
|
||||||
AggregatedEthereumUpstreamsMock(@NotNull Chain chain, @NotNull List<EthereumUpstream> upstreams, @NotNull Caches caches, @NotNull ObjectMapper objectMapper) {
|
EthereumMultistreamMock(@NotNull Chain chain, @NotNull List<EthereumUpstream> upstreams, @NotNull Caches caches) {
|
||||||
super(chain, upstreams, caches, objectMapper)
|
super(chain, upstreams, caches, TestingCommons.objectMapper())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2020 EmeraldPay, Inc
|
* Copyright (c) 2020 EmeraldPay, Inc
|
||||||
* Copyright (c) 2020 ETCDEV GmbH
|
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@@ -14,20 +13,25 @@
|
|||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package io.emeraldpay.dshackle.upstream
|
package io.emeraldpay.dshackle.test
|
||||||
|
|
||||||
|
import io.emeraldpay.dshackle.reader.Reader
|
||||||
import reactor.core.publisher.Mono
|
import reactor.core.publisher.Mono
|
||||||
|
|
||||||
/**
|
class ReaderMock<K, D> implements Reader<K, D> {
|
||||||
* A general interface to make a request to an Upstream API
|
|
||||||
*/
|
|
||||||
interface UpstreamApi {
|
|
||||||
|
|
||||||
/**
|
private Map<K, D> mapping = new HashMap<K, D>()
|
||||||
* @param id an internal uniq id, if multiple requests are made in batch
|
|
||||||
* @param method JSON RPC method name
|
|
||||||
* @param params JSON RPC parameters, must be serializable into a JSON array
|
|
||||||
*/
|
|
||||||
fun execute(id: Int, method: String, params: List<Any>): Mono<ByteArray>
|
|
||||||
|
|
||||||
|
ReaderMock() {
|
||||||
|
}
|
||||||
|
|
||||||
|
ReaderMock with(K key, D data) {
|
||||||
|
mapping[key] = data
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
Mono<D> read(K key) {
|
||||||
|
return Mono.justOrEmpty(mapping.get(key))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -24,14 +24,15 @@ import io.emeraldpay.dshackle.FileResolver
|
|||||||
import io.emeraldpay.dshackle.cache.Caches
|
import io.emeraldpay.dshackle.cache.Caches
|
||||||
import io.emeraldpay.dshackle.cache.CachesFactory
|
import io.emeraldpay.dshackle.cache.CachesFactory
|
||||||
import io.emeraldpay.dshackle.config.CacheConfig
|
import io.emeraldpay.dshackle.config.CacheConfig
|
||||||
import io.emeraldpay.dshackle.upstream.AggregatedUpstream
|
import io.emeraldpay.dshackle.reader.Reader
|
||||||
|
import io.emeraldpay.dshackle.upstream.Multistream
|
||||||
import io.emeraldpay.dshackle.upstream.calls.DirectCallMethods
|
import io.emeraldpay.dshackle.upstream.calls.DirectCallMethods
|
||||||
import io.emeraldpay.dshackle.upstream.ethereum.DirectEthereumApi
|
import io.emeraldpay.dshackle.upstream.ethereum.EthereumMultistream
|
||||||
import io.emeraldpay.dshackle.upstream.ethereum.AggregatedEthereumUpstreams
|
|
||||||
import io.emeraldpay.dshackle.upstream.ethereum.EthereumUpstream
|
import io.emeraldpay.dshackle.upstream.ethereum.EthereumUpstream
|
||||||
|
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
|
||||||
|
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
|
||||||
import io.emeraldpay.grpc.Chain
|
import io.emeraldpay.grpc.Chain
|
||||||
import io.infinitape.etherjar.rpc.JacksonRpcConverter
|
import io.infinitape.etherjar.rpc.JacksonRpcConverter
|
||||||
import io.infinitape.etherjar.rpc.ReactorRpcClient
|
|
||||||
|
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
|
|
||||||
@@ -50,32 +51,34 @@ class TestingCommons {
|
|||||||
return objectMapper
|
return objectMapper
|
||||||
}
|
}
|
||||||
|
|
||||||
static EthereumApiMock api(ReactorRpcClient rpcClient) {
|
static EthereumApiMock api() {
|
||||||
return new EthereumApiMock(rpcClient, objectMapper(), Chain.ETHEREUM)
|
return new EthereumApiMock(objectMapper())
|
||||||
}
|
}
|
||||||
|
|
||||||
static JacksonRpcConverter rpcConverter() {
|
static JacksonRpcConverter rpcConverter() {
|
||||||
return new JacksonRpcConverter(objectMapper())
|
return new JacksonRpcConverter(objectMapper())
|
||||||
}
|
}
|
||||||
|
|
||||||
static EthereumUpstreamMock upstream(DirectEthereumApi api) {
|
static EthereumUpstreamMock upstream(Reader<JsonRpcRequest, JsonRpcResponse> api) {
|
||||||
return new EthereumUpstreamMock(Chain.ETHEREUM, api)
|
return new EthereumUpstreamMock(Chain.ETHEREUM, api)
|
||||||
}
|
}
|
||||||
|
|
||||||
static EthereumUpstreamMock upstream(DirectEthereumApi api, String method) {
|
static EthereumUpstreamMock upstream(Reader<JsonRpcRequest, JsonRpcResponse> api, String method) {
|
||||||
return upstream(api, [method])
|
return upstream(api, [method])
|
||||||
}
|
}
|
||||||
|
|
||||||
static EthereumUpstreamMock upstream(DirectEthereumApi api, List<String> methods) {
|
static EthereumUpstreamMock upstream(Reader<JsonRpcRequest, JsonRpcResponse> api, List<String> methods) {
|
||||||
return new EthereumUpstreamMock(Chain.ETHEREUM, api, new DirectCallMethods(methods))
|
return new EthereumUpstreamMock(Chain.ETHEREUM, api, new DirectCallMethods(methods))
|
||||||
}
|
}
|
||||||
|
|
||||||
static AggregatedUpstream aggregatedUpstream(DirectEthereumApi api) {
|
static Multistream aggregatedUpstream(Reader<JsonRpcRequest, JsonRpcResponse> api) {
|
||||||
return aggregatedUpstream(upstream(api))
|
return aggregatedUpstream(upstream(api))
|
||||||
}
|
}
|
||||||
|
|
||||||
static AggregatedUpstream aggregatedUpstream(EthereumUpstream up) {
|
static Multistream aggregatedUpstream(EthereumUpstream up) {
|
||||||
return new AggregatedEthereumUpstreams(Chain.ETHEREUM, [up], Caches.default(objectMapper()), objectMapper())
|
return new EthereumMultistream(Chain.ETHEREUM, [up], Caches.default(objectMapper()), objectMapper()).tap {
|
||||||
|
start()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static CachesFactory emptyCaches() {
|
static CachesFactory emptyCaches() {
|
||||||
|
|||||||
@@ -0,0 +1,117 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2020 EmeraldPay, Inc
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package io.emeraldpay.dshackle.upstream
|
||||||
|
|
||||||
|
import io.emeraldpay.dshackle.data.BlockContainer
|
||||||
|
import io.emeraldpay.dshackle.data.BlockId
|
||||||
|
import reactor.core.publisher.Flux
|
||||||
|
import reactor.core.publisher.TopicProcessor
|
||||||
|
import reactor.test.StepVerifier
|
||||||
|
import spock.lang.Specification
|
||||||
|
|
||||||
|
import java.time.Duration
|
||||||
|
import java.time.Instant
|
||||||
|
|
||||||
|
class AbstractHeadSpec extends Specification {
|
||||||
|
|
||||||
|
def blocks = [1L, 2, 3, 4].collect { i ->
|
||||||
|
byte[] hash = new byte[32]
|
||||||
|
hash[0] = i as byte
|
||||||
|
new BlockContainer(i, BlockId.from(hash), BigInteger.valueOf(i), Instant.now(), false, null, null, [])
|
||||||
|
}
|
||||||
|
|
||||||
|
def "Calls beforeBlock on each block"() {
|
||||||
|
setup:
|
||||||
|
TopicProcessor<BlockContainer> source = TopicProcessor.create()
|
||||||
|
def head = new TestHead()
|
||||||
|
def called = false
|
||||||
|
when:
|
||||||
|
head.follow(Flux.from(source))
|
||||||
|
head.onBeforeBlock {
|
||||||
|
called = true
|
||||||
|
}
|
||||||
|
def act = head.flux
|
||||||
|
source.onNext(blocks[0])
|
||||||
|
then:
|
||||||
|
StepVerifier.create(act)
|
||||||
|
.expectNext(blocks[0])
|
||||||
|
.then {
|
||||||
|
assert called
|
||||||
|
called = false
|
||||||
|
source.onNext(blocks[1])
|
||||||
|
}
|
||||||
|
.expectNext(blocks[1])
|
||||||
|
.then {
|
||||||
|
assert called
|
||||||
|
source.onComplete()
|
||||||
|
}
|
||||||
|
.expectComplete()
|
||||||
|
.verify(Duration.ofSeconds(1))
|
||||||
|
}
|
||||||
|
|
||||||
|
def "Follows source"() {
|
||||||
|
setup:
|
||||||
|
TopicProcessor<BlockContainer> source = TopicProcessor.create()
|
||||||
|
def head = new TestHead()
|
||||||
|
when:
|
||||||
|
head.follow(Flux.from(source))
|
||||||
|
def act = head.flux
|
||||||
|
source.onNext(blocks[0])
|
||||||
|
then:
|
||||||
|
StepVerifier.create(act)
|
||||||
|
.expectNext(blocks[0])
|
||||||
|
.then { source.onNext(blocks[1]) }
|
||||||
|
.expectNext(blocks[1])
|
||||||
|
.then { source.onNext(blocks[2]) }
|
||||||
|
.expectNext(blocks[2])
|
||||||
|
.then { source.onNext(blocks[3]) }
|
||||||
|
.expectNext(blocks[3])
|
||||||
|
.then { source.onComplete() }
|
||||||
|
.expectComplete()
|
||||||
|
.verify(Duration.ofSeconds(1))
|
||||||
|
}
|
||||||
|
|
||||||
|
def "Ignores block will less difficulty"() {
|
||||||
|
setup:
|
||||||
|
TopicProcessor<BlockContainer> source = TopicProcessor.create()
|
||||||
|
def head = new TestHead()
|
||||||
|
def wrongblock = new BlockContainer(
|
||||||
|
blocks[1].height, BlockId.from(blocks[1].hash.value.clone().tap { it[1] = 0xff as byte }),
|
||||||
|
blocks[1].difficulty - 1,
|
||||||
|
Instant.now(),
|
||||||
|
false, null, null, []
|
||||||
|
)
|
||||||
|
when:
|
||||||
|
head.follow(Flux.from(source))
|
||||||
|
def act = head.flux
|
||||||
|
source.onNext(blocks[0])
|
||||||
|
then:
|
||||||
|
StepVerifier.create(act)
|
||||||
|
.expectNext(blocks[0])
|
||||||
|
.then { source.onNext(blocks[1]) }
|
||||||
|
.expectNext(blocks[1])
|
||||||
|
.then { source.onNext(wrongblock) }
|
||||||
|
.then { source.onNext(blocks[3]) }
|
||||||
|
.expectNext(blocks[3])
|
||||||
|
.then { source.onComplete() }
|
||||||
|
.expectComplete()
|
||||||
|
.verify(Duration.ofSeconds(1))
|
||||||
|
}
|
||||||
|
|
||||||
|
class TestHead extends AbstractHead {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user