add base support of polkadot/substrate/vara chain (#332)

This commit is contained in:
a10zn8
2023-11-02 16:18:54 +03:00
committed by GitHub
parent 08cbffe7d1
commit 66b9b00454
42 changed files with 464 additions and 425 deletions

View File

@@ -1,5 +1,6 @@
import com.squareup.kotlinpoet.* import com.squareup.kotlinpoet.*
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
import io.emeraldpay.dshackle.BlockchainType
import io.emeraldpay.dshackle.config.ChainsConfig import io.emeraldpay.dshackle.config.ChainsConfig
import io.emeraldpay.dshackle.config.ChainsConfigReader import io.emeraldpay.dshackle.config.ChainsConfigReader
import io.emeraldpay.dshackle.foundation.ChainOptionsReader import io.emeraldpay.dshackle.foundation.ChainOptionsReader
@@ -18,7 +19,7 @@ open class CodeGen(private val config: ChainsConfig) {
builder.addEnumConstant( builder.addEnumConstant(
"UNSPECIFIED", "UNSPECIFIED",
TypeSpec.anonymousClassBuilder() TypeSpec.anonymousClassBuilder()
.addSuperclassConstructorParameter("%L, %S, %S, %S, %L, %L", 0, "UNSPECIFIED", "Unknown", "0x0", "BigInteger.ZERO", "emptyList()") .addSuperclassConstructorParameter("%L, %S, %S, %S, %L, %L, %L", 0, "UNSPECIFIED", "Unknown", "0x0", "BigInteger.ZERO", "emptyList()", "BlockchainType.UNKNOWN")
.build(), .build(),
) )
for (chain in config) { for (chain in config) {
@@ -27,13 +28,14 @@ open class CodeGen(private val config: ChainsConfig) {
.replace(' ', '_'), .replace(' ', '_'),
TypeSpec.anonymousClassBuilder() TypeSpec.anonymousClassBuilder()
.addSuperclassConstructorParameter( .addSuperclassConstructorParameter(
"%L, %S, %S, %S, %L, %L", "%L, %S, %S, %S, %L, %L, %L",
chain.grpcId, chain.grpcId,
chain.code, chain.code,
chain.blockchain.replaceFirstChar { it.uppercase() } + " " + chain.id.replaceFirstChar { it.uppercase() }, chain.blockchain.replaceFirstChar { it.uppercase() } + " " + chain.id.replaceFirstChar { it.uppercase() },
chain.chainId, chain.chainId,
"BigInteger(\"" + chain.netVersion + "\")", "BigInteger(\"" + chain.netVersion + "\")",
"listOf(" + chain.shortNames.map { "\"${it}\"" }.joinToString() + ")", "listOf(" + chain.shortNames.map { "\"${it}\"" }.joinToString() + ")",
type(chain.type)
) )
.build(), .build(),
) )
@@ -65,6 +67,7 @@ open class CodeGen(private val config: ChainsConfig) {
.addParameter("chainId", String::class) .addParameter("chainId", String::class)
.addParameter("netVersion", BigInteger::class) .addParameter("netVersion", BigInteger::class)
.addParameter("shortNames", List::class.asClassName().parameterizedBy(String::class.asClassName())) .addParameter("shortNames", List::class.asClassName().parameterizedBy(String::class.asClassName()))
.addParameter("type", BlockchainType::class)
.build(), .build(),
) )
.addProperty( .addProperty(
@@ -96,12 +99,27 @@ open class CodeGen(private val config: ChainsConfig) {
PropertySpec.builder("shortNames", List::class.asClassName().parameterizedBy(String::class.asClassName())) PropertySpec.builder("shortNames", List::class.asClassName().parameterizedBy(String::class.asClassName()))
.initializer("shortNames") .initializer("shortNames")
.build(), .build(),
), )
.addProperty(
PropertySpec.builder("type", BlockchainType::class)
.initializer("type")
.build(),
)
).build() ).build()
return FileSpec.builder("io.emeraldpay.dshackle", "Chain") return FileSpec.builder("io.emeraldpay.dshackle", "Chain")
.addType(chainType) .addType(chainType)
.build() .build()
} }
private fun type(type: String): String {
return when(type) {
"eth" -> "BlockchainType.ETHEREUM"
"bitcoin" -> "BlockchainType.BITCOIN"
"starknet" -> "BlockchainType.STARKNET"
"polkadot" -> "BlockchainType.POLKADOT"
else -> throw IllegalArgumentException("unknown blockchain type $type")
}
}
} }
open class ChainsCodeGenTask : DefaultTask() { open class ChainsCodeGenTask : DefaultTask() {

View File

@@ -0,0 +1,5 @@
package io.emeraldpay.dshackle
enum class BlockchainType {
UNKNOWN, BITCOIN, ETHEREUM, STARKNET, POLKADOT;
}

View File

@@ -30,6 +30,7 @@ data class ChainsConfig(private val chains: List<ChainConfig>) : Iterable<Chains
val callLimitContract: String?, val callLimitContract: String?,
val id: String, val id: String,
val blockchain: String, val blockchain: String,
val type: String
) { ) {
companion object { companion object {
@JvmStatic @JvmStatic
@@ -49,6 +50,7 @@ data class ChainsConfig(private val chains: List<ChainConfig>) : Iterable<Chains
callLimitContract, callLimitContract,
"undefined", "undefined",
"undefined", "undefined",
"unknown"
) )
} }

View File

@@ -23,6 +23,8 @@ class ChainsConfigReader(
protocols.value.fold(emptyMap()) { acc, protocol -> protocols.value.fold(emptyMap()) { acc, protocol ->
val blockchain = getValueAsString(protocol, "id") val blockchain = getValueAsString(protocol, "id")
?: throw IllegalArgumentException("Blockchain id is not defined") ?: throw IllegalArgumentException("Blockchain id is not defined")
val type = getValueAsString(protocol, "type")
?: throw IllegalArgumentException("undefined type for $blockchain")
val settings = mergeMappingNode(default, getMapping(protocol, "settings")) val settings = mergeMappingNode(default, getMapping(protocol, "settings"))
acc.plus( acc.plus(
getList<MappingNode>(protocol, "chains")?.let { chains -> getList<MappingNode>(protocol, "chains")?.let { chains ->
@@ -38,6 +40,10 @@ class ChainsConfigReader(
ScalarNode(Tag.STR, "blockchain", null, null, DumperOptions.ScalarStyle.LITERAL), ScalarNode(Tag.STR, "blockchain", null, null, DumperOptions.ScalarStyle.LITERAL),
ScalarNode(Tag.STR, blockchain, null, null, DumperOptions.ScalarStyle.LITERAL), ScalarNode(Tag.STR, blockchain, null, null, DumperOptions.ScalarStyle.LITERAL),
), ),
NodeTuple(
ScalarNode(Tag.STR, "type", null, null, DumperOptions.ScalarStyle.LITERAL),
ScalarNode(Tag.STR, type, null, null, DumperOptions.ScalarStyle.LITERAL),
),
), ),
chain.flowStyle, chain.flowStyle,
), ),
@@ -78,6 +84,8 @@ class ChainsConfigReader(
val netVersion = getValueAsLong(node, "net-version")?.toBigInteger() ?: BigInteger(chainId.drop(2), 16) val netVersion = getValueAsLong(node, "net-version")?.toBigInteger() ?: BigInteger(chainId.drop(2), 16)
val shortNames = getListOfString(node, "short-names") val shortNames = getListOfString(node, "short-names")
?: throw IllegalArgumentException("undefined shortnames for $blockchain") ?: throw IllegalArgumentException("undefined shortnames for $blockchain")
val type = getValueAsString(node, "type")
?: throw IllegalArgumentException("undefined type for $blockchain")
return ChainsConfig.ChainConfig( return ChainsConfig.ChainConfig(
expectedBlockTime = expectedBlockTime, expectedBlockTime = expectedBlockTime,
syncingLagSize = lags.first, syncingLagSize = lags.first,
@@ -91,6 +99,7 @@ class ChainsConfigReader(
shortNames = shortNames, shortNames = shortNames,
id = id, id = id,
blockchain = blockchain, blockchain = blockchain,
type = type
) )
} }

View File

@@ -641,3 +641,26 @@ chain-settings:
short-names: [ astar-zkatana ] short-names: [ astar-zkatana ]
chain-id: 0x133e40 chain-id: 0x133e40
grpcId: 10035 grpcId: 10035
- id: vara
label: varanet
type: polkadot
settings:
expected-block-time: 3s
options:
validate-peers: false
lags:
syncing: 10
lagging: 5
chains:
- id: Mainnet
priority: 1
code: VARA_MAINNET
short-names: [ vara ]
chain-id: 0x0
grpcId: 1027
- id: Testnet
priority: 1
code: VARA_TESTMET
short-names: [ vara-testnet ]
chain-id: 0x0
grpcId: 10036

View File

@@ -3,6 +3,7 @@ version: v1
chain-settings: chain-settings:
protocols: protocols:
- id: fantom - id: fantom
type: eth
settings: settings:
expected-block-time: 10s expected-block-time: 10s
options: options:

View File

@@ -1,22 +0,0 @@
package io.emeraldpay.dshackle
enum class BlockchainType {
BITCOIN, ETHEREUM, STARKNET;
companion object {
val bitcoin = setOf(Chain.BITCOIN__MAINNET, Chain.BITCOIN__TESTNET)
val starknet = setOf(Chain.STARKNET__MAINNET, Chain.STARKNET__TESTNET, Chain.STARKNET__TESTNET_2)
@JvmStatic
fun from(chain: Chain): BlockchainType {
return if (bitcoin.contains(chain)) {
BITCOIN
} else if (starknet.contains(chain)) {
STARKNET
} else {
ETHEREUM
}
}
}
}

View File

@@ -41,7 +41,7 @@ class TokensConfig(
type == null -> type type == null -> type
address.isNullOrBlank() -> "address" address.isNullOrBlank() -> "address"
blockchain != null && blockchain != null &&
(BlockchainType.from(blockchain!!) == BlockchainType.ETHEREUM) && (blockchain!!.type == BlockchainType.ETHEREUM) &&
!Address.isValidAddress(address) -> "address" !Address.isValidAddress(address) -> "address"
else -> null else -> null
} }

View File

@@ -1,6 +1,5 @@
package io.emeraldpay.dshackle.config.context package io.emeraldpay.dshackle.config.context
import io.emeraldpay.dshackle.BlockchainType
import io.emeraldpay.dshackle.BlockchainType.BITCOIN import io.emeraldpay.dshackle.BlockchainType.BITCOIN
import io.emeraldpay.dshackle.Chain import io.emeraldpay.dshackle.Chain
import io.emeraldpay.dshackle.cache.CachesFactory import io.emeraldpay.dshackle.cache.CachesFactory
@@ -30,7 +29,7 @@ open class MultistreamsConfig(val beanFactory: ConfigurableListableBeanFactory)
return Chain.entries return Chain.entries
.filterNot { it == Chain.UNSPECIFIED } .filterNot { it == Chain.UNSPECIFIED }
.map { chain -> .map { chain ->
if (BlockchainType.from(chain) == BITCOIN) { if (chain.type == BITCOIN) {
bitcoinMultistream(chain, cachesFactory, headScheduler) bitcoinMultistream(chain, cachesFactory, headScheduler)
} else { } else {
genericMultistream(chain, cachesFactory, headScheduler, tracer) genericMultistream(chain, cachesFactory, headScheduler, tracer)

View File

@@ -43,14 +43,10 @@ class AccessHandlerGrpc(
): ServerCall.Listener<ReqT> { ): ServerCall.Listener<ReqT> {
return when (val method = call.methodDescriptor.bareMethodName) { return when (val method = call.methodDescriptor.bareMethodName) {
"SubscribeHead" -> processSubscribeHead(call, headers, next) "SubscribeHead" -> processSubscribeHead(call, headers, next)
"SubscribeBalance" -> processSubscribeBalance(call, headers, next, true)
"SubscribeTxStatus" -> processSubscribeTxStatus(call, headers, next)
"GetBalance" -> processSubscribeBalance(call, headers, next, false)
"NativeCall" -> processNativeCall(call, headers, next) "NativeCall" -> processNativeCall(call, headers, next)
"NativeSubscribe" -> processNativeSubscribe(call, headers, next) "NativeSubscribe" -> processNativeSubscribe(call, headers, next)
"Describe" -> processDescribe(call, headers, next) "Describe" -> processDescribe(call, headers, next)
"SubscribeStatus" -> processStatus(call, headers, next) "SubscribeStatus" -> processStatus(call, headers, next)
"EstimateFee" -> processEstimateFee(call, headers, next)
else -> { else -> {
log.warn("unsupported method `{}`", method) log.warn("unsupported method `{}`", method)
next.startCall(call, headers) next.startCall(call, headers)
@@ -90,35 +86,6 @@ class AccessHandlerGrpc(
) )
} }
@Suppress("UNCHECKED_CAST")
private fun <ReqT : Any, RespT : Any> processSubscribeBalance(
call: ServerCall<ReqT, RespT>,
headers: Metadata,
next: ServerCallHandler<ReqT, RespT>,
subscribe: Boolean,
): ServerCall.Listener<ReqT> {
return process(
call,
headers,
next,
EventsBuilder.SubscribeBalance(subscribe) as EventsBuilder.RequestReply<*, ReqT, RespT>,
)
}
@Suppress("UNCHECKED_CAST")
private fun <ReqT : Any, RespT : Any> processSubscribeTxStatus(
call: ServerCall<ReqT, RespT>,
headers: Metadata,
next: ServerCallHandler<ReqT, RespT>,
): ServerCall.Listener<ReqT> {
return process(
call,
headers,
next,
EventsBuilder.TxStatus() as EventsBuilder.RequestReply<*, ReqT, RespT>,
)
}
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
private fun <ReqT : Any, RespT : Any> processNativeCall( private fun <ReqT : Any, RespT : Any> processNativeCall(
call: ServerCall<ReqT, RespT>, call: ServerCall<ReqT, RespT>,
@@ -175,20 +142,6 @@ class AccessHandlerGrpc(
) )
} }
@Suppress("UNCHECKED_CAST")
private fun <ReqT : Any, RespT : Any> processEstimateFee(
call: ServerCall<ReqT, RespT>,
headers: Metadata,
next: ServerCallHandler<ReqT, RespT>,
): ServerCall.Listener<ReqT> {
return process(
call,
headers,
next,
EventsBuilder.EstimateFee() as EventsBuilder.RequestReply<*, ReqT, RespT>,
)
}
open class StdCallListener<Req, EB : EventsBuilder.RequestReply<*, Req, *>>( open class StdCallListener<Req, EB : EventsBuilder.RequestReply<*, Req, *>>(
val next: ServerCall.Listener<Req>, val next: ServerCall.Listener<Req>,
val builder: EB, val builder: EB,

View File

@@ -32,7 +32,6 @@ import java.net.InetAddress
import java.net.InetSocketAddress import java.net.InetSocketAddress
import java.time.Duration import java.time.Duration
import java.time.Instant import java.time.Instant
import java.util.Locale
import java.util.UUID import java.util.UUID
class EventsBuilder { class EventsBuilder {
@@ -244,69 +243,6 @@ class EventsBuilder {
} }
} }
class SubscribeBalance(val subscribe: Boolean) :
Base<SubscribeBalance>(),
RequestReply<Events.SubscribeBalance, BlockchainOuterClass.BalanceRequest, BlockchainOuterClass.AddressBalance> {
private var index = 0
private var balanceRequest: Events.BalanceRequest? = null
override fun getT(): SubscribeBalance {
return this
}
override fun onRequest(msg: BlockchainOuterClass.BalanceRequest) {
balanceRequest = Events.BalanceRequest(
msg.asset.code.uppercase(Locale.getDefault()),
msg.address.addrTypeCase.name,
)
}
override fun onReply(msg: BlockchainOuterClass.AddressBalance): Events.SubscribeBalance {
if (balanceRequest == null) {
throw IllegalStateException("Request is not initialized")
}
val addressBalance = Events.AddressBalance(msg.asset.code, msg.address.address)
val chain = Chain.byId(msg.asset.chain.number)
return Events.SubscribeBalance(
chain,
UUID.randomUUID(),
subscribe,
requestDetails,
balanceRequest!!,
addressBalance,
index++,
)
}
}
class TxStatus :
Base<TxStatus>(),
RequestReply<Events.TxStatus, BlockchainOuterClass.TxStatusRequest, BlockchainOuterClass.TxStatus> {
private var index = 0
private var txStatusRequest: Events.TxStatusRequest? = null
override fun onRequest(msg: BlockchainOuterClass.TxStatusRequest) {
this.txStatusRequest = Events.TxStatusRequest(msg.txId)
withChain(msg.chainValue)
}
override fun onReply(msg: BlockchainOuterClass.TxStatus): Events.TxStatus {
return Events.TxStatus(
chain,
UUID.randomUUID(),
requestDetails,
txStatusRequest!!,
Events.TxStatusResponse(msg.confirmations),
index++,
)
}
override fun getT(): TxStatus {
return this
}
}
class NativeCall(private val startTs: Instant) : class NativeCall(private val startTs: Instant) :
Base<NativeCall>(), Base<NativeCall>(),
RequestReply<Events.NativeCall, BlockchainOuterClass.NativeCallRequest, BlockchainOuterClass.NativeCallReplyItem> { RequestReply<Events.NativeCall, BlockchainOuterClass.NativeCallRequest, BlockchainOuterClass.NativeCallReplyItem> {
@@ -496,34 +432,4 @@ class EventsBuilder {
) )
} }
} }
class EstimateFee :
Base<EstimateFee>(),
RequestReply<Events.EstimateFee, BlockchainOuterClass.EstimateFeeRequest, BlockchainOuterClass.EstimateFeeResponse> {
private var mode: String = "UNKNOWN"
private var blocks: Int = 0
override fun getT(): EstimateFee {
return this
}
override fun onRequest(msg: BlockchainOuterClass.EstimateFeeRequest) {
this.chain = Chain.byId(msg.chain.number)
this.mode = msg.mode.name
this.blocks = msg.blocks
}
override fun onReply(msg: BlockchainOuterClass.EstimateFeeResponse): Events.EstimateFee {
return Events.EstimateFee(
blockchain = chain,
request = requestDetails,
id = UUID.randomUUID(),
estimateFee = Events.EstimateFeeDetails(
mode = mode,
blocks = blocks,
),
)
}
}
} }

View File

@@ -19,7 +19,6 @@ package io.emeraldpay.dshackle.rpc
import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.ObjectMapper
import com.google.protobuf.ByteString import com.google.protobuf.ByteString
import io.emeraldpay.api.proto.BlockchainOuterClass import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.dshackle.BlockchainType
import io.emeraldpay.dshackle.BlockchainType.ETHEREUM import io.emeraldpay.dshackle.BlockchainType.ETHEREUM
import io.emeraldpay.dshackle.Chain import io.emeraldpay.dshackle.Chain
import io.emeraldpay.dshackle.Global import io.emeraldpay.dshackle.Global
@@ -84,7 +83,7 @@ open class NativeCall(
@EventListener @EventListener
fun onUpstreamChangeEvent(event: UpstreamChangeEvent) { fun onUpstreamChangeEvent(event: UpstreamChangeEvent) {
multistreamHolder.getUpstream(event.chain).let { up -> multistreamHolder.getUpstream(event.chain).let { up ->
if (BlockchainType.from(up.chain) == ETHEREUM) { if (up.chain.type == ETHEREUM) {
ethereumCallSelectors.putIfAbsent( ethereumCallSelectors.putIfAbsent(
event.chain, event.chain,
EthereumCallSelector(up.caches), EthereumCallSelector(up.caches),
@@ -306,7 +305,7 @@ open class NativeCall(
} }
// for ethereum the actual block needed for the call may be specified in the call parameters // for ethereum the actual block needed for the call may be specified in the call parameters
val callSpecificMatcher: Mono<Selector.Matcher> = val callSpecificMatcher: Mono<Selector.Matcher> =
if (BlockchainType.from(upstream.chain) == ETHEREUM) { if (upstream.chain.type == ETHEREUM) {
ethereumCallSelectors[chain]?.getMatcher(method, params, upstream.getHead(), passthrough) ethereumCallSelectors[chain]?.getMatcher(method, params, upstream.getHead(), passthrough)
} else { } else {
null null

View File

@@ -57,7 +57,7 @@ open class NativeSubscribe(
fun start(request: BlockchainOuterClass.NativeSubscribeRequest): Publisher<ResponseHolder> { fun start(request: BlockchainOuterClass.NativeSubscribeRequest): Publisher<ResponseHolder> {
val chain = Chain.byId(request.chainValue) val chain = Chain.byId(request.chainValue)
if (BlockchainType.from(chain) != BlockchainType.ETHEREUM) { if (chain.type != BlockchainType.ETHEREUM) {
return Mono.error(UnsupportedOperationException("Native subscribe is not supported for ${chain.chainCode}")) return Mono.error(UnsupportedOperationException("Native subscribe is not supported for ${chain.chainCode}"))
} }

View File

@@ -43,7 +43,7 @@ class StreamHead(
.getFlux() .getFlux()
.map { asProto(chain, it!!) } .map { asProto(chain, it!!) }
.onErrorContinue { t, _ -> .onErrorContinue { t, _ ->
log.warn("Head subscription error: ${t.message}") log.warn("Head subscription error", t)
} }
} }
} }

View File

@@ -18,7 +18,6 @@ package io.emeraldpay.dshackle.startup
import brave.grpc.GrpcTracing import brave.grpc.GrpcTracing
import com.google.common.annotations.VisibleForTesting import com.google.common.annotations.VisibleForTesting
import io.emeraldpay.dshackle.BlockchainType
import io.emeraldpay.dshackle.BlockchainType.BITCOIN import io.emeraldpay.dshackle.BlockchainType.BITCOIN
import io.emeraldpay.dshackle.BlockchainType.ETHEREUM import io.emeraldpay.dshackle.BlockchainType.ETHEREUM
import io.emeraldpay.dshackle.Chain import io.emeraldpay.dshackle.Chain
@@ -135,7 +134,7 @@ open class ConfiguredUpstreams(
.merge(defaultOptions[chain] ?: ChainOptions.PartialOptions.getDefaults()) .merge(defaultOptions[chain] ?: ChainOptions.PartialOptions.getDefaults())
.merge(up.options ?: ChainOptions.PartialOptions()) .merge(up.options ?: ChainOptions.PartialOptions())
.buildOptions() .buildOptions()
val upstream = when (BlockchainType.from(chain)) { val upstream = when (chain.type) {
BITCOIN -> { BITCOIN -> {
buildBitcoinUpstream( buildBitcoinUpstream(
up.cast(BitcoinConnection::class.java), up.cast(BitcoinConnection::class.java),

View File

@@ -1,10 +1,15 @@
package io.emeraldpay.dshackle.upstream package io.emeraldpay.dshackle.upstream
import io.emeraldpay.dshackle.BlockchainType import io.emeraldpay.dshackle.BlockchainType.BITCOIN
import io.emeraldpay.dshackle.BlockchainType.ETHEREUM
import io.emeraldpay.dshackle.BlockchainType.POLKADOT
import io.emeraldpay.dshackle.BlockchainType.STARKNET
import io.emeraldpay.dshackle.BlockchainType.UNKNOWN
import io.emeraldpay.dshackle.Chain import io.emeraldpay.dshackle.Chain
import io.emeraldpay.dshackle.upstream.calls.CallMethods import io.emeraldpay.dshackle.upstream.calls.CallMethods
import io.emeraldpay.dshackle.upstream.calls.DefaultBitcoinMethods 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.calls.DefaultPolkadotMethods
import io.emeraldpay.dshackle.upstream.calls.DefaultStarknetMethods import io.emeraldpay.dshackle.upstream.calls.DefaultStarknetMethods
import org.springframework.stereotype.Component import org.springframework.stereotype.Component
@@ -17,10 +22,12 @@ class CallTargetsHolder {
} }
private fun setupDefaultMethods(chain: Chain): CallMethods { private fun setupDefaultMethods(chain: Chain): CallMethods {
val created = when (BlockchainType.from(chain)) { val created = when (chain.type) {
BlockchainType.BITCOIN -> DefaultBitcoinMethods() BITCOIN -> DefaultBitcoinMethods()
BlockchainType.ETHEREUM -> DefaultEthereumMethods(chain) ETHEREUM -> DefaultEthereumMethods(chain)
BlockchainType.STARKNET -> DefaultStarknetMethods(chain) STARKNET -> DefaultStarknetMethods(chain)
POLKADOT -> DefaultPolkadotMethods()
UNKNOWN -> throw IllegalArgumentException("unknown chain")
} }
callTargets[chain] = created callTargets[chain] = created
return created return created

View File

@@ -1,22 +1,15 @@
package io.emeraldpay.dshackle.upstream.bitcoin package io.emeraldpay.dshackle.upstream.bitcoin
import io.emeraldpay.api.proto.BlockchainOuterClass.BalanceRequest
import io.emeraldpay.dshackle.upstream.Capability import io.emeraldpay.dshackle.upstream.Capability
import io.emeraldpay.dshackle.upstream.Selector import io.emeraldpay.dshackle.upstream.Selector
import io.emeraldpay.dshackle.upstream.bitcoin.data.SimpleUnspent import io.emeraldpay.dshackle.upstream.bitcoin.data.SimpleUnspent
import io.emeraldpay.dshackle.upstream.grpc.BitcoinGrpcUpstream
import org.bitcoinj.core.Address import org.bitcoinj.core.Address
import org.slf4j.LoggerFactory
import reactor.core.publisher.Mono import reactor.core.publisher.Mono
class RemoteUnspentReader( class RemoteUnspentReader(
val upstreams: BitcoinMultistream, val upstreams: BitcoinMultistream,
) : UnspentReader { ) : UnspentReader {
companion object {
private val log = LoggerFactory.getLogger(RemoteUnspentReader::class.java)
}
private val selector = Selector.MultiMatcher( private val selector = Selector.MultiMatcher(
listOf( listOf(
Selector.GrpcMatcher(), Selector.GrpcMatcher(),
@@ -27,24 +20,6 @@ class RemoteUnspentReader(
override fun read(key: Address): Mono<List<SimpleUnspent>> { override fun read(key: Address): Mono<List<SimpleUnspent>> {
val apis = upstreams.getApiSource(selector) val apis = upstreams.getApiSource(selector)
apis.request(1) apis.request(1)
return Mono.from(apis) return Mono.empty()
.map { up ->
up.cast(BitcoinGrpcUpstream::class.java).remote
}
.flatMapMany {
val request = BalanceRequest.newBuilder()
.build()
it.getBalance(request)
}
.map { resp ->
resp.utxoList.map { utxo ->
SimpleUnspent(
utxo.txId,
utxo.index.toInt(),
utxo.balance.toLong(),
)
}
}
.reduce(List<SimpleUnspent>::plus)
} }
} }

View File

@@ -0,0 +1,130 @@
/**
* 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.calls
import io.emeraldpay.dshackle.quorum.AlwaysQuorum
import io.emeraldpay.dshackle.quorum.BroadcastQuorum
import io.emeraldpay.dshackle.quorum.CallQuorum
import io.emeraldpay.etherjar.rpc.RpcException
/**
* Default configuration for Ethereum based RPC. Defines optimal Quorum strategies for different methods, and provides
* hardcoded results for base methods, such as `net_version`, `web3_clientVersion` and similar
*/
class DefaultPolkadotMethods : CallMethods {
private val all = setOf(
"author_pendingExtrinsics",
"author_removeExtrinsic",
"chain_getBlock",
"chain_getBlockHash",
"chain_getFinalisedHead",
"chain_getFinalizedHead",
"chain_getHead",
"chain_getHeader",
"chain_getRuntimeVersion",
"chain_subscribeAllHeads",
"chain_subscribeFinalizedHeads",
"chain_subscribeNewHeads",
"chain_subscribeRuntimeVersion",
"chain_unsubscribeAllHeads",
"chain_unsubscribeFinalisedHeads",
"chain_unsubscribeFinalizedHeads",
"chain_unsubscribeNewHead",
"chain_unsubscribeNewHeads",
"chain_unsubscribeRuntimeVersion",
"childstate_getKeys",
"childstate_getKeysPaged",
"childstate_getKeysPagedAt",
"childstate_getStorage",
"childstate_getStorageEntries",
"childstate_getStorageHash",
"childstate_getStorageSize",
"gear_calculateHandleGas",
"gear_calculateInitCreateGas",
"gear_calculateInitUploadGas",
"gear_calculateReplyGas",
"gear_readMetahash",
"gear_readState",
"gear_readStateBatch",
"gear_readStateUsingWasm",
"gear_readStateUsingWasmBatch",
"grandpa_proveFinality",
"grandpa_roundState",
"payment_queryFeeDetails",
"payment_queryInfo",
"state_call",
"state_callAt",
"state_getChildReadProof",
"state_getKeys",
"state_getKeysPaged",
"state_getKeysPagedAt",
"state_getMetadata",
"state_getPairs",
"state_getReadProof",
"state_getRuntimeVersion",
"state_getStorage",
"state_getStorageAt",
"state_getStorageHash",
"state_getStorageHashAt",
"state_getStorageSize",
"state_getStorageSizeAt",
"state_queryStorage",
"state_queryStorageAt",
"state_traceBlock",
"state_trieMigrationStatus",
"subscribe_newHead",
"system_chain",
"unsubscribe_newHead",
)
private val add = setOf(
"author_submitExtrinsic",
)
private val allowedMethods: Set<String> = all + add
override fun createQuorumFor(method: String): CallQuorum {
return when {
add.contains(method) -> BroadcastQuorum()
all.contains(method) -> AlwaysQuorum()
else -> AlwaysQuorum()
}
}
override fun isCallable(method: String): Boolean {
return allowedMethods.contains(method)
}
override fun isHardcoded(method: String): Boolean {
return false
}
override fun executeHardcoded(method: String): ByteArray {
throw RpcException(-32601, "Method not found")
}
override fun getGroupMethods(groupName: String): Set<String> =
when (groupName) {
"default" -> getSupportedMethods()
else -> emptyList()
}.toSet()
override fun getSupportedMethods(): Set<String> {
return allowedMethods.toSortedSet()
}
}

View File

@@ -22,17 +22,22 @@ import io.emeraldpay.dshackle.upstream.generic.CachingReaderBuilder
import io.emeraldpay.dshackle.upstream.generic.ChainSpecific import io.emeraldpay.dshackle.upstream.generic.ChainSpecific
import io.emeraldpay.dshackle.upstream.generic.GenericUpstream import io.emeraldpay.dshackle.upstream.generic.GenericUpstream
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
import org.springframework.cloud.sleuth.Tracer import org.springframework.cloud.sleuth.Tracer
import reactor.core.publisher.Mono import reactor.core.publisher.Mono
import reactor.core.scheduler.Scheduler import reactor.core.scheduler.Scheduler
object EthereumChainSpecific : ChainSpecific { object EthereumChainSpecific : ChainSpecific {
override fun parseBlock(data: JsonRpcResponse, upstreamId: String): BlockContainer { override fun parseBlock(data: ByteArray, upstreamId: String): BlockContainer {
return BlockContainer.fromEthereumJson(data.getResult(), upstreamId) return BlockContainer.fromEthereumJson(data, upstreamId)
}
override fun parseHeader(data: ByteArray, upstreamId: String): BlockContainer {
return parseBlock(data, upstreamId)
} }
override fun latestBlockRequest() = JsonRpcRequest("eth_getBlockByNumber", listOf("latest", false)) override fun latestBlockRequest() = JsonRpcRequest("eth_getBlockByNumber", listOf("latest", false))
override fun listenNewHeadsRequest(): JsonRpcRequest = JsonRpcRequest("eth_subscribe", listOf("newHeads"))
override fun localReaderBuilder( override fun localReaderBuilder(
cachingReader: CachingReader, cachingReader: CachingReader,
methods: CallMethods, methods: CallMethods,

View File

@@ -16,41 +16,32 @@
*/ */
package io.emeraldpay.dshackle.upstream.ethereum package io.emeraldpay.dshackle.upstream.ethereum
import io.emeraldpay.dshackle.Global
import io.emeraldpay.dshackle.SilentException
import io.emeraldpay.dshackle.ThrottledLogger
import io.emeraldpay.dshackle.data.BlockContainer import io.emeraldpay.dshackle.data.BlockContainer
import io.emeraldpay.dshackle.reader.JsonRpcReader import io.emeraldpay.dshackle.reader.JsonRpcReader
import io.emeraldpay.dshackle.reader.Reader
import io.emeraldpay.dshackle.upstream.BlockValidator import io.emeraldpay.dshackle.upstream.BlockValidator
import io.emeraldpay.dshackle.upstream.DefaultUpstream import io.emeraldpay.dshackle.upstream.DefaultUpstream
import io.emeraldpay.dshackle.upstream.Lifecycle import io.emeraldpay.dshackle.upstream.Lifecycle
import io.emeraldpay.dshackle.upstream.UpstreamAvailability import io.emeraldpay.dshackle.upstream.UpstreamAvailability
import io.emeraldpay.dshackle.upstream.ethereum.json.BlockJson
import io.emeraldpay.dshackle.upstream.forkchoice.ForkChoice import io.emeraldpay.dshackle.upstream.forkchoice.ForkChoice
import io.emeraldpay.dshackle.upstream.generic.ChainSpecific import io.emeraldpay.dshackle.upstream.generic.ChainSpecific
import io.emeraldpay.dshackle.upstream.generic.GenericHead import io.emeraldpay.dshackle.upstream.generic.GenericHead
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
import io.emeraldpay.etherjar.domain.BlockHash
import io.emeraldpay.etherjar.rpc.json.TransactionRefJson
import reactor.core.Disposable import reactor.core.Disposable
import reactor.core.publisher.Flux import reactor.core.publisher.Flux
import reactor.core.publisher.Mono import reactor.core.publisher.Mono
import reactor.core.publisher.Sinks import reactor.core.publisher.Sinks
import reactor.core.scheduler.Scheduler import reactor.core.scheduler.Scheduler
import java.time.Duration import java.time.Duration
import java.util.concurrent.atomic.AtomicInteger
class EthereumWsHead( class GenericWsHead(
forkChoice: ForkChoice, forkChoice: ForkChoice,
blockValidator: BlockValidator, blockValidator: BlockValidator,
private val api: JsonRpcReader, private val api: JsonRpcReader,
private val wsSubscriptions: WsSubscriptions, private val wsSubscriptions: WsSubscriptions,
private val skipEnhance: Boolean,
private val wsConnectionResubscribeScheduler: Scheduler, private val wsConnectionResubscribeScheduler: Scheduler,
private val headScheduler: Scheduler, headScheduler: Scheduler,
private val upstream: DefaultUpstream, private val upstream: DefaultUpstream,
chainSpecific: ChainSpecific, private val chainSpecific: ChainSpecific,
) : GenericHead(upstream.getId(), forkChoice, blockValidator, headScheduler, chainSpecific), Lifecycle { ) : GenericHead(upstream.getId(), forkChoice, blockValidator, headScheduler, chainSpecific), Lifecycle {
private var connectionId: String? = null private var connectionId: String? = null
@@ -98,50 +89,7 @@ class EthereumWsHead(
Flux.concat(it.next().doOnNext { upstream.setStatus(UpstreamAvailability.OK) }, it) Flux.concat(it.next().doOnNext { upstream.setStatus(UpstreamAvailability.OK) }, it)
} }
.map { .map {
val block = Global.objectMapper.readValue(it, BlockJson::class.java) as BlockJson<TransactionRefJson> chainSpecific.parseHeader(it, "unknown")
if (!block.checkExtraData() && skipEnhance) {
ThrottledLogger.log(log, "$upstreamId recieved block with empty extradata through ws subscription")
}
return@map block
}
.flatMap { block ->
// newHeads returns incomplete blocks, i.e. without some fields and without transaction hashes,
// so we need to fetch the full block data
if (!skipEnhance && (
block.difficulty == null ||
block.transactions == null ||
block.transactions.isEmpty() ||
block.totalDifficulty == null
)
) {
EthereumBlockEnricher.enrich(
block.hash,
object :
Reader<BlockHash, BlockContainer> {
override fun read(key: BlockHash): Mono<BlockContainer> {
return api.read(JsonRpcRequest("eth_getBlockByHash", listOf(block.hash.toHex(), false)))
.flatMap { resp ->
if (resp.isNull()) {
Mono.error(SilentException("Received null for block ${block.hash}"))
} else {
Mono.just(resp)
}
}
.flatMap(JsonRpcResponse::requireResult)
.map {
val parsedBlock = BlockContainer.fromEthereumJson(it, upstreamId)
if (parsedBlock.parsed is BlockJson<*> && !parsedBlock.parsed.checkExtraData() && !skipEnhance) {
ThrottledLogger.log(log, "$upstreamId recieved block with empty extradata from block enrichment")
}
return@map parsedBlock
}
}
},
headScheduler,
)
} else {
Mono.just(BlockContainer.from(block))
}
} }
.timeout(Duration.ofSeconds(60), Mono.error(RuntimeException("No response from subscribe to newHeads"))) .timeout(Duration.ofSeconds(60), Mono.error(RuntimeException("No response from subscribe to newHeads")))
.onErrorResume { .onErrorResume {
@@ -158,9 +106,11 @@ class EthereumWsHead(
noHeadUpdatesSink.tryEmitComplete() noHeadUpdatesSink.tryEmitComplete()
} }
private val ids = AtomicInteger(1)
private fun subscribe(): Flux<ByteArray> { private fun subscribe(): Flux<ByteArray> {
return try { return try {
wsSubscriptions.subscribe("newHeads") wsSubscriptions.subscribe(chainSpecific.listenNewHeadsRequest().copy(id = ids.getAndIncrement()))
.also { .also {
connectionId = it.connectionId connectionId = it.connectionId
if (!connected) { if (!connected) {

View File

@@ -15,6 +15,7 @@
*/ */
package io.emeraldpay.dshackle.upstream.ethereum package io.emeraldpay.dshackle.upstream.ethereum
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
import reactor.core.publisher.Flux import reactor.core.publisher.Flux
/** /**
@@ -37,7 +38,7 @@ interface WsSubscriptions {
/** /**
* Subscribe on remote * Subscribe on remote
*/ */
fun subscribe(method: String): SubscribeData fun subscribe(request: JsonRpcRequest): SubscribeData
fun connectionInfoFlux(): Flux<WsConnection.ConnectionInfo> fun connectionInfoFlux(): Flux<WsConnection.ConnectionInfo>

View File

@@ -20,7 +20,6 @@ import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import reactor.core.publisher.Flux import reactor.core.publisher.Flux
import reactor.core.publisher.Mono import reactor.core.publisher.Mono
import java.util.concurrent.atomic.AtomicLong
import java.util.concurrent.atomic.AtomicReference import java.util.concurrent.atomic.AtomicReference
class WsSubscriptionsImpl( class WsSubscriptionsImpl(
@@ -31,9 +30,7 @@ class WsSubscriptionsImpl(
private val log = LoggerFactory.getLogger(WsSubscriptionsImpl::class.java) private val log = LoggerFactory.getLogger(WsSubscriptionsImpl::class.java)
} }
private val ids = AtomicLong(1) override fun subscribe(request: JsonRpcRequest): WsSubscriptions.SubscribeData {
override fun subscribe(method: String): WsSubscriptions.SubscribeData {
val subscriptionId = AtomicReference("") val subscriptionId = AtomicReference("")
val conn = wsPool.getConnection() val conn = wsPool.getConnection()
val messages = conn.getSubscribeResponses() val messages = conn.getSubscribeResponses()
@@ -41,10 +38,10 @@ class WsSubscriptionsImpl(
.filter { it.result != null } // should never happen .filter { it.result != null } // should never happen
.map { it.result!! } .map { it.result!! }
val messageFlux = conn.callRpc(JsonRpcRequest("eth_subscribe", listOf(method), ids.incrementAndGet())) val messageFlux = conn.callRpc(request)
.flatMapMany { .flatMapMany {
if (it.hasError()) { if (it.hasError()) {
log.warn("Failed to establish ETH Subscription: ${it.error?.message}") log.warn("Failed to establish subscription: ${it.error?.message}")
Mono.error(JsonRpcException(it.id, it.error!!)) Mono.error(JsonRpcException(it.id, it.error!!))
} else { } else {
subscriptionId.set(it.getResultAsProcessedString()) subscriptionId.set(it.getResultAsProcessedString())

View File

@@ -18,6 +18,7 @@ package io.emeraldpay.dshackle.upstream.ethereum.subscribe
import io.emeraldpay.dshackle.upstream.SubscriptionConnect import io.emeraldpay.dshackle.upstream.SubscriptionConnect
import io.emeraldpay.dshackle.upstream.ethereum.EthereumEgressSubscription import io.emeraldpay.dshackle.upstream.ethereum.EthereumEgressSubscription
import io.emeraldpay.dshackle.upstream.ethereum.WsSubscriptions import io.emeraldpay.dshackle.upstream.ethereum.WsSubscriptions
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
import io.emeraldpay.etherjar.domain.TransactionId import io.emeraldpay.etherjar.domain.TransactionId
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import reactor.core.publisher.Flux import reactor.core.publisher.Flux
@@ -33,7 +34,7 @@ class WebsocketPendingTxes(
} }
override fun createConnection(): Flux<TransactionId> { override fun createConnection(): Flux<TransactionId> {
return wsSubscriptions.subscribe(EthereumEgressSubscription.METHOD_PENDING_TXES) return wsSubscriptions.subscribe(JsonRpcRequest("eth_subscribe", listOf(EthereumEgressSubscription.METHOD_PENDING_TXES)))
.data .data
.timeout(Duration.ofSeconds(60), Mono.empty()) .timeout(Duration.ofSeconds(60), Mono.empty())
.map { .map {

View File

@@ -1,7 +1,10 @@
package io.emeraldpay.dshackle.upstream.generic package io.emeraldpay.dshackle.upstream.generic
import io.emeraldpay.dshackle.BlockchainType import io.emeraldpay.dshackle.BlockchainType.BITCOIN
import io.emeraldpay.dshackle.BlockchainType.ETHEREUM
import io.emeraldpay.dshackle.BlockchainType.POLKADOT
import io.emeraldpay.dshackle.BlockchainType.STARKNET import io.emeraldpay.dshackle.BlockchainType.STARKNET
import io.emeraldpay.dshackle.BlockchainType.UNKNOWN
import io.emeraldpay.dshackle.Chain import io.emeraldpay.dshackle.Chain
import io.emeraldpay.dshackle.cache.Caches import io.emeraldpay.dshackle.cache.Caches
import io.emeraldpay.dshackle.config.ChainsConfig.ChainConfig import io.emeraldpay.dshackle.config.ChainsConfig.ChainConfig
@@ -17,8 +20,8 @@ import io.emeraldpay.dshackle.upstream.Upstream
import io.emeraldpay.dshackle.upstream.UpstreamValidator import io.emeraldpay.dshackle.upstream.UpstreamValidator
import io.emeraldpay.dshackle.upstream.calls.CallMethods import io.emeraldpay.dshackle.upstream.calls.CallMethods
import io.emeraldpay.dshackle.upstream.ethereum.EthereumChainSpecific import io.emeraldpay.dshackle.upstream.ethereum.EthereumChainSpecific
import io.emeraldpay.dshackle.upstream.polkadot.PolkadotChainSpecific
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
import io.emeraldpay.dshackle.upstream.starknet.StarknetChainSpecific import io.emeraldpay.dshackle.upstream.starknet.StarknetChainSpecific
import org.apache.commons.collections4.Factory import org.apache.commons.collections4.Factory
import org.springframework.cloud.sleuth.Tracer import org.springframework.cloud.sleuth.Tracer
@@ -30,10 +33,14 @@ typealias LocalReaderBuilder = (CachingReader, CallMethods, Head) -> Mono<JsonRp
typealias CachingReaderBuilder = (Multistream, Caches, Factory<CallMethods>) -> CachingReader typealias CachingReaderBuilder = (Multistream, Caches, Factory<CallMethods>) -> CachingReader
interface ChainSpecific { interface ChainSpecific {
fun parseBlock(data: JsonRpcResponse, upstreamId: String): BlockContainer fun parseBlock(data: ByteArray, upstreamId: String): BlockContainer
fun parseHeader(data: ByteArray, upstreamId: String): BlockContainer
fun latestBlockRequest(): JsonRpcRequest fun latestBlockRequest(): JsonRpcRequest
fun listenNewHeadsRequest(): JsonRpcRequest
fun localReaderBuilder(cachingReader: CachingReader, methods: CallMethods, head: Head): Mono<JsonRpcReader> fun localReaderBuilder(cachingReader: CachingReader, methods: CallMethods, head: Head): Mono<JsonRpcReader>
fun subscriptionBuilder(headScheduler: Scheduler): (Multistream) -> EgressSubscription fun subscriptionBuilder(headScheduler: Scheduler): (Multistream) -> EgressSubscription
@@ -51,9 +58,12 @@ object ChainSpecificRegistry {
@JvmStatic @JvmStatic
fun resolve(chain: Chain): ChainSpecific { fun resolve(chain: Chain): ChainSpecific {
if (BlockchainType.from(chain) == STARKNET) { return when (chain.type) {
return StarknetChainSpecific ETHEREUM -> EthereumChainSpecific
STARKNET -> StarknetChainSpecific
POLKADOT -> PolkadotChainSpecific
BITCOIN -> throw IllegalArgumentException("bitcoin should use custom streams implementation")
UNKNOWN -> throw IllegalArgumentException("unknown chain")
} }
return EthereumChainSpecific
} }
} }

View File

@@ -37,7 +37,9 @@ open class GenericHead(
return api.read(chainSpecific.latestBlockRequest()) return api.read(chainSpecific.latestBlockRequest())
.subscribeOn(headScheduler) .subscribeOn(headScheduler)
.timeout(Defaults.timeout, Mono.error(Exception("Block data not received"))) .timeout(Defaults.timeout, Mono.error(Exception("Block data not received")))
.map { chainSpecific.parseBlock(it, upstreamId) } .map {
chainSpecific.parseBlock(it.getResult(), upstreamId)
}
.onErrorResume { err -> .onErrorResume { err ->
log.error("Failed to fetch latest block: ${err.message} $upstreamId", err) log.error("Failed to fetch latest block: ${err.message} $upstreamId", err)
Mono.empty() Mono.empty()

View File

@@ -49,7 +49,7 @@ open class GenericUpstream(
private var validationSettingsSubscription: Disposable? = null private var validationSettingsSubscription: Disposable? = null
private val hasLiveSubscriptionHead: AtomicBoolean = AtomicBoolean(false) private val hasLiveSubscriptionHead: AtomicBoolean = AtomicBoolean(false)
protected val connector: GenericConnector = connectorFactory.create(this, chain, true) protected val connector: GenericConnector = connectorFactory.create(this, chain)
private var livenessSubscription: Disposable? = null private var livenessSubscription: Disposable? = null
private val labelsDetector = labelsDetectorBuilder(chain, this.getIngressReader()) private val labelsDetector = labelsDetectorBuilder(chain, this.getIngressReader())

View File

@@ -7,7 +7,6 @@ interface ConnectorFactory {
fun create( fun create(
upstream: DefaultUpstream, upstream: DefaultUpstream,
chain: Chain, chain: Chain,
skipEnhance: Boolean,
): GenericConnector ): GenericConnector
fun isValid(): Boolean fun isValid(): Boolean

View File

@@ -48,7 +48,6 @@ open class GenericConnectorFactory(
override fun create( override fun create(
upstream: DefaultUpstream, upstream: DefaultUpstream,
chain: Chain, chain: Chain,
skipEnhance: Boolean,
): GenericConnector { ): GenericConnector {
val specific = ChainSpecificRegistry.resolve(chain) val specific = ChainSpecificRegistry.resolve(chain)
if (wsFactory != null && connectorType == WS_ONLY) { if (wsFactory != null && connectorType == WS_ONLY) {
@@ -57,7 +56,6 @@ open class GenericConnectorFactory(
upstream, upstream,
forkChoice, forkChoice,
blockValidator, blockValidator,
skipEnhance,
wsConnectionResubscribeScheduler, wsConnectionResubscribeScheduler,
headScheduler, headScheduler,
expectedBlockTime, expectedBlockTime,
@@ -74,7 +72,6 @@ open class GenericConnectorFactory(
upstream, upstream,
forkChoice, forkChoice,
blockValidator, blockValidator,
skipEnhance,
wsConnectionResubscribeScheduler, wsConnectionResubscribeScheduler,
headScheduler, headScheduler,
expectedBlockTime, expectedBlockTime,

View File

@@ -10,7 +10,7 @@ import io.emeraldpay.dshackle.upstream.Head
import io.emeraldpay.dshackle.upstream.IngressSubscription import io.emeraldpay.dshackle.upstream.IngressSubscription
import io.emeraldpay.dshackle.upstream.Lifecycle import io.emeraldpay.dshackle.upstream.Lifecycle
import io.emeraldpay.dshackle.upstream.MergedHead import io.emeraldpay.dshackle.upstream.MergedHead
import io.emeraldpay.dshackle.upstream.ethereum.EthereumWsHead import io.emeraldpay.dshackle.upstream.ethereum.GenericWsHead
import io.emeraldpay.dshackle.upstream.ethereum.HeadLivenessValidator import io.emeraldpay.dshackle.upstream.ethereum.HeadLivenessValidator
import io.emeraldpay.dshackle.upstream.ethereum.NoEthereumIngressSubscription import io.emeraldpay.dshackle.upstream.ethereum.NoEthereumIngressSubscription
import io.emeraldpay.dshackle.upstream.ethereum.WsConnectionPool import io.emeraldpay.dshackle.upstream.ethereum.WsConnectionPool
@@ -37,7 +37,6 @@ class GenericRpcConnector(
upstream: DefaultUpstream, upstream: DefaultUpstream,
forkChoice: ForkChoice, forkChoice: ForkChoice,
blockValidator: BlockValidator, blockValidator: BlockValidator,
skipEnhance: Boolean,
wsConnectionResubscribeScheduler: Scheduler, wsConnectionResubscribeScheduler: Scheduler,
headScheduler: Scheduler, headScheduler: Scheduler,
expectedBlockTime: Duration, expectedBlockTime: Duration,
@@ -71,12 +70,11 @@ class GenericRpcConnector(
RPC_REQUESTS_WITH_MIXED_HEAD -> { RPC_REQUESTS_WITH_MIXED_HEAD -> {
val wsHead = val wsHead =
EthereumWsHead( GenericWsHead(
AlwaysForkChoice(), AlwaysForkChoice(),
blockValidator, blockValidator,
getIngressReader(), getIngressReader(),
WsSubscriptionsImpl(pool!!), WsSubscriptionsImpl(pool!!),
skipEnhance,
wsConnectionResubscribeScheduler, wsConnectionResubscribeScheduler,
headScheduler, headScheduler,
upstream, upstream,
@@ -97,12 +95,11 @@ class GenericRpcConnector(
} }
RPC_REQUESTS_WITH_WS_HEAD -> { RPC_REQUESTS_WITH_WS_HEAD -> {
EthereumWsHead( GenericWsHead(
AlwaysForkChoice(), AlwaysForkChoice(),
blockValidator, blockValidator,
getIngressReader(), getIngressReader(),
WsSubscriptionsImpl(pool!!), WsSubscriptionsImpl(pool!!),
skipEnhance,
wsConnectionResubscribeScheduler, wsConnectionResubscribeScheduler,
headScheduler, headScheduler,
upstream, upstream,

View File

@@ -6,7 +6,7 @@ import io.emeraldpay.dshackle.upstream.DefaultUpstream
import io.emeraldpay.dshackle.upstream.Head import io.emeraldpay.dshackle.upstream.Head
import io.emeraldpay.dshackle.upstream.IngressSubscription import io.emeraldpay.dshackle.upstream.IngressSubscription
import io.emeraldpay.dshackle.upstream.ethereum.EthereumIngressSubscription import io.emeraldpay.dshackle.upstream.ethereum.EthereumIngressSubscription
import io.emeraldpay.dshackle.upstream.ethereum.EthereumWsHead import io.emeraldpay.dshackle.upstream.ethereum.GenericWsHead
import io.emeraldpay.dshackle.upstream.ethereum.HeadLivenessValidator import io.emeraldpay.dshackle.upstream.ethereum.HeadLivenessValidator
import io.emeraldpay.dshackle.upstream.ethereum.WsConnectionPool import io.emeraldpay.dshackle.upstream.ethereum.WsConnectionPool
import io.emeraldpay.dshackle.upstream.ethereum.WsConnectionPoolFactory import io.emeraldpay.dshackle.upstream.ethereum.WsConnectionPoolFactory
@@ -24,7 +24,6 @@ class GenericWsConnector(
upstream: DefaultUpstream, upstream: DefaultUpstream,
forkChoice: ForkChoice, forkChoice: ForkChoice,
blockValidator: BlockValidator, blockValidator: BlockValidator,
skipEnhance: Boolean,
wsConnectionResubscribeScheduler: Scheduler, wsConnectionResubscribeScheduler: Scheduler,
headScheduler: Scheduler, headScheduler: Scheduler,
expectedBlockTime: Duration, expectedBlockTime: Duration,
@@ -32,19 +31,18 @@ class GenericWsConnector(
) : GenericConnector { ) : GenericConnector {
private val pool: WsConnectionPool private val pool: WsConnectionPool
private val reader: JsonRpcReader private val reader: JsonRpcReader
private val head: EthereumWsHead private val head: GenericWsHead
private val subscriptions: EthereumIngressSubscription private val subscriptions: EthereumIngressSubscription
private val liveness: HeadLivenessValidator private val liveness: HeadLivenessValidator
init { init {
pool = wsFactory.create(upstream) pool = wsFactory.create(upstream)
reader = JsonRpcWsClient(pool) reader = JsonRpcWsClient(pool)
val wsSubscriptions = WsSubscriptionsImpl(pool) val wsSubscriptions = WsSubscriptionsImpl(pool)
head = EthereumWsHead( head = GenericWsHead(
forkChoice, forkChoice,
blockValidator, blockValidator,
reader, reader,
wsSubscriptions, wsSubscriptions,
skipEnhance,
wsConnectionResubscribeScheduler, wsConnectionResubscribeScheduler,
headScheduler, headScheduler,
upstream, upstream,

View File

@@ -24,7 +24,6 @@ import io.emeraldpay.api.proto.Common
import io.emeraldpay.api.proto.Common.ChainRef.UNRECOGNIZED import io.emeraldpay.api.proto.Common.ChainRef.UNRECOGNIZED
import io.emeraldpay.api.proto.ReactorAuthGrpc import io.emeraldpay.api.proto.ReactorAuthGrpc
import io.emeraldpay.api.proto.ReactorBlockchainGrpc import io.emeraldpay.api.proto.ReactorBlockchainGrpc
import io.emeraldpay.dshackle.BlockchainType
import io.emeraldpay.dshackle.BlockchainType.BITCOIN import io.emeraldpay.dshackle.BlockchainType.BITCOIN
import io.emeraldpay.dshackle.Chain import io.emeraldpay.dshackle.Chain
import io.emeraldpay.dshackle.Defaults import io.emeraldpay.dshackle.Defaults
@@ -248,7 +247,7 @@ class GrpcUpstreams(
private fun getOrCreate(chain: Chain): UpstreamChangeEvent { private fun getOrCreate(chain: Chain): UpstreamChangeEvent {
val metrics = makeMetrics(chain) val metrics = makeMetrics(chain)
val creator = if (BlockchainType.from(chain) != BITCOIN) { val creator = if (chain.type != BITCOIN) {
{ ch: Chain, rpcClient: JsonRpcGrpcClient -> { ch: Chain, rpcClient: JsonRpcGrpcClient ->
GenericGrpcUpstream( GenericGrpcUpstream(
id, id,

View File

@@ -0,0 +1,122 @@
package io.emeraldpay.dshackle.upstream.polkadot
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import com.fasterxml.jackson.annotation.JsonProperty
import io.emeraldpay.dshackle.Chain
import io.emeraldpay.dshackle.Global
import io.emeraldpay.dshackle.config.ChainsConfig.ChainConfig
import io.emeraldpay.dshackle.data.BlockContainer
import io.emeraldpay.dshackle.data.BlockId
import io.emeraldpay.dshackle.foundation.ChainOptions.Options
import io.emeraldpay.dshackle.reader.JsonRpcReader
import io.emeraldpay.dshackle.upstream.CachingReader
import io.emeraldpay.dshackle.upstream.EgressSubscription
import io.emeraldpay.dshackle.upstream.EmptyEgressSubscription
import io.emeraldpay.dshackle.upstream.Head
import io.emeraldpay.dshackle.upstream.LabelsDetector
import io.emeraldpay.dshackle.upstream.Multistream
import io.emeraldpay.dshackle.upstream.NoopCachingReader
import io.emeraldpay.dshackle.upstream.Upstream
import io.emeraldpay.dshackle.upstream.UpstreamValidator
import io.emeraldpay.dshackle.upstream.calls.CallMethods
import io.emeraldpay.dshackle.upstream.generic.CachingReaderBuilder
import io.emeraldpay.dshackle.upstream.generic.ChainSpecific
import io.emeraldpay.dshackle.upstream.generic.GenericUpstream
import io.emeraldpay.dshackle.upstream.generic.LocalReader
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
import org.springframework.cloud.sleuth.Tracer
import reactor.core.publisher.Mono
import reactor.core.scheduler.Scheduler
import java.math.BigInteger
import java.time.Instant
object PolkadotChainSpecific : ChainSpecific {
override fun parseBlock(data: ByteArray, upstreamId: String): BlockContainer {
val response = Global.objectMapper.readValue(data, PolkadotBlockResponse::class.java)
return makeBlock(response.block.header, data, upstreamId)
}
override fun parseHeader(data: ByteArray, upstreamId: String): BlockContainer {
val header = Global.objectMapper.readValue(data, PolkadotHeader::class.java)
return makeBlock(header, data, upstreamId)
}
private fun makeBlock(header: PolkadotHeader, data: ByteArray, upstreamId: String): BlockContainer {
return BlockContainer(
height = header.number.substring(2).toLong(16),
hash = BlockId.from(header.parentHash), // todo
difficulty = BigInteger.ZERO,
timestamp = Instant.EPOCH,
full = false,
json = data,
parsed = header,
transactions = emptyList(),
upstreamId = upstreamId,
parentHash = BlockId.from(header.parentHash),
)
}
override fun latestBlockRequest(): JsonRpcRequest =
JsonRpcRequest("chain_getBlock", listOf())
override fun listenNewHeadsRequest(): JsonRpcRequest =
JsonRpcRequest("chain_subscribeNewHeads", listOf())
override fun localReaderBuilder(
cachingReader: CachingReader,
methods: CallMethods,
head: Head,
): Mono<JsonRpcReader> {
return Mono.just(LocalReader(methods))
}
override fun subscriptionBuilder(headScheduler: Scheduler): (Multistream) -> EgressSubscription {
return { _ -> EmptyEgressSubscription }
}
override fun makeCachingReaderBuilder(tracer: Tracer): CachingReaderBuilder {
return { _, _, _ -> NoopCachingReader }
}
override fun validator(
chain: Chain,
upstream: Upstream,
options: Options,
config: ChainConfig,
): UpstreamValidator? {
return null
}
override fun labelDetector(chain: Chain, reader: JsonRpcReader): LabelsDetector? {
return null
}
override fun subscriptionTopics(upstream: GenericUpstream): List<String> {
return emptyList()
}
}
@JsonIgnoreProperties(ignoreUnknown = true)
data class PolkadotBlockResponse(
@JsonProperty("block") var block: PolkadotBlock,
)
@JsonIgnoreProperties(ignoreUnknown = true)
data class PolkadotBlock(
@JsonProperty("header") var header: PolkadotHeader,
)
@JsonIgnoreProperties(ignoreUnknown = true)
data class PolkadotHeader(
@JsonProperty("parentHash") var parentHash: String,
@JsonProperty("number") var number: String,
@JsonProperty("stateRoot") var stateRoot: String,
@JsonProperty("extrinsicsRoot") var extrinsicsRoot: String,
@JsonProperty("digest") var digest: PolkadotDigest,
)
data class PolkadotDigest(
@JsonProperty("logs") var logs: List<String>,
)

View File

@@ -24,7 +24,6 @@ import io.emeraldpay.dshackle.upstream.generic.ChainSpecific
import io.emeraldpay.dshackle.upstream.generic.GenericUpstream import io.emeraldpay.dshackle.upstream.generic.GenericUpstream
import io.emeraldpay.dshackle.upstream.generic.LocalReader import io.emeraldpay.dshackle.upstream.generic.LocalReader
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
import org.springframework.cloud.sleuth.Tracer import org.springframework.cloud.sleuth.Tracer
import reactor.core.publisher.Mono import reactor.core.publisher.Mono
import reactor.core.scheduler.Scheduler import reactor.core.scheduler.Scheduler
@@ -32,9 +31,8 @@ import java.math.BigInteger
import java.time.Instant import java.time.Instant
object StarknetChainSpecific : ChainSpecific { object StarknetChainSpecific : ChainSpecific {
override fun parseBlock(data: JsonRpcResponse, upstreamId: String): BlockContainer { override fun parseBlock(data: ByteArray, upstreamId: String): BlockContainer {
val raw = data.getResult() val block = Global.objectMapper.readValue(data, StarknetBlock::class.java)
val block = Global.objectMapper.readValue(raw, StarknetBlock::class.java)
return BlockContainer( return BlockContainer(
height = block.number, height = block.number,
@@ -42,7 +40,7 @@ object StarknetChainSpecific : ChainSpecific {
difficulty = BigInteger.ZERO, difficulty = BigInteger.ZERO,
timestamp = block.timestamp, timestamp = block.timestamp,
full = false, full = false,
json = raw, json = data,
parsed = block, parsed = block,
transactions = emptyList(), transactions = emptyList(),
upstreamId = upstreamId, upstreamId = upstreamId,
@@ -50,9 +48,17 @@ object StarknetChainSpecific : ChainSpecific {
) )
} }
override fun parseHeader(data: ByteArray, upstreamId: String): BlockContainer {
throw NotImplementedError()
}
override fun latestBlockRequest(): JsonRpcRequest = override fun latestBlockRequest(): JsonRpcRequest =
JsonRpcRequest("starknet_getBlockWithTxHashes", listOf("latest")) JsonRpcRequest("starknet_getBlockWithTxHashes", listOf("latest"))
override fun listenNewHeadsRequest(): JsonRpcRequest {
throw NotImplementedError()
}
override fun localReaderBuilder( override fun localReaderBuilder(
cachingReader: CachingReader, cachingReader: CachingReader,
methods: CallMethods, methods: CallMethods,

View File

@@ -1,83 +0,0 @@
package io.emeraldpay.dshackle.monitoring.accesslog
import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.api.proto.Common
import io.emeraldpay.dshackle.Chain
import spock.lang.Specification
class EventsBuilderSubscribeBalanceSpec extends Specification {
def "Basic ethereum event"() {
setup:
def request = BlockchainOuterClass.BalanceRequest.newBuilder()
.setAddress(
Common.AnyAddress.newBuilder()
.setAddressSingle(
Common.SingleAddress.newBuilder()
.setAddress("0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D")
)
)
.setAsset(
Common.Asset.newBuilder()
.setChainValue(100)
.setCode("ETHER")
)
.build()
def resp = BlockchainOuterClass.AddressBalance.newBuilder()
.setAddress(Common.SingleAddress.newBuilder()
.setAddress("0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D"))
.setAsset(Common.Asset.newBuilder()
.setChainValue(100)
.setCode("ETHER"))
.setBalance("1234560000000000000")
.build()
when:
def act = new EventsBuilder.SubscribeBalance(true).tap {
it.onRequest(request)
}.onReply(resp)
then:
act.index == 0
act.blockchain == Chain.ETHEREUM__MAINNET
act.balanceRequest.asset == "ETHER"
act.balanceRequest.addressType == "ADDRESS_SINGLE"
act.addressBalance.asset == "ETHER"
act.addressBalance.address == "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D"
}
def "Basic bitcoin event"() {
setup:
def request = BlockchainOuterClass.BalanceRequest.newBuilder()
.setAddress(
Common.AnyAddress.newBuilder()
.setAddressSingle(
Common.SingleAddress.newBuilder()
.setAddress("1NDyJtNTjmwk5xPNhjgAMu4HDHigtobu1s")
)
)
.setAsset(
Common.Asset.newBuilder()
.setChainValue(1)
.setCode("BTC")
)
.build()
def resp = BlockchainOuterClass.AddressBalance.newBuilder()
.setAddress(Common.SingleAddress.newBuilder()
.setAddress("1NDyJtNTjmwk5xPNhjgAMu4HDHigtobu1s"))
.setAsset(Common.Asset.newBuilder()
.setChainValue(1)
.setCode("BTC"))
.setBalance("12345600000000")
.build()
when:
def act = new EventsBuilder.SubscribeBalance(true).tap {
it.onRequest(request)
}.onReply(resp)
then:
act.index == 0
act.blockchain == Chain.BITCOIN__MAINNET
act.balanceRequest.asset == "BTC"
act.balanceRequest.addressType == "ADDRESS_SINGLE"
act.addressBalance.asset == "BTC"
act.addressBalance.address == "1NDyJtNTjmwk5xPNhjgAMu4HDHigtobu1s"
}
}

View File

@@ -23,7 +23,7 @@ class ConnectorFactoryMock implements ConnectorFactory {
return true return true
} }
GenericConnector create(DefaultUpstream upstream, Chain chain, boolean skipEnhance) { GenericConnector create(DefaultUpstream upstream, Chain chain) {
return new GenericConnectorMock(api, head) return new GenericConnectorMock(api, head)
} }
} }

View File

@@ -42,7 +42,7 @@ class MultistreamHolderMock implements MultistreamHolder {
Multistream addUpstream(@NotNull Chain chain, @NotNull Upstream up) { Multistream addUpstream(@NotNull Chain chain, @NotNull Upstream up) {
if (!upstreams.containsKey(chain)) { if (!upstreams.containsKey(chain)) {
if (BlockchainType.from(chain) == BlockchainType.ETHEREUM) { if (chain.type == BlockchainType.ETHEREUM) {
if (up instanceof GenericMultistream) { if (up instanceof GenericMultistream) {
upstreams[chain] = up upstreams[chain] = up
} else if (up instanceof GenericUpstream) { } else if (up instanceof GenericUpstream) {
@@ -57,7 +57,7 @@ class MultistreamHolderMock implements MultistreamHolder {
throw new IllegalArgumentException("Unsupported upstream type ${up.class}") throw new IllegalArgumentException("Unsupported upstream type ${up.class}")
} }
upstreams[chain].start() upstreams[chain].start()
} else if (BlockchainType.from(chain) == BlockchainType.BITCOIN) { } else if (chain.type == BlockchainType.BITCOIN) {
if (up instanceof BitcoinMultistream) { if (up instanceof BitcoinMultistream) {
upstreams[chain] = up upstreams[chain] = up
} else if (up instanceof BitcoinRpcUpstream) { } else if (up instanceof BitcoinRpcUpstream) {

View File

@@ -27,6 +27,7 @@ import io.emeraldpay.dshackle.upstream.forkchoice.AlwaysForkChoice
import io.emeraldpay.etherjar.domain.BlockHash import io.emeraldpay.etherjar.domain.BlockHash
import io.emeraldpay.etherjar.domain.TransactionId import io.emeraldpay.etherjar.domain.TransactionId
import io.emeraldpay.etherjar.rpc.json.TransactionRefJson import io.emeraldpay.etherjar.rpc.json.TransactionRefJson
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
import reactor.core.publisher.Flux import reactor.core.publisher.Flux
import reactor.core.publisher.Mono import reactor.core.publisher.Mono
import reactor.core.publisher.Sinks import reactor.core.publisher.Sinks
@@ -38,7 +39,7 @@ import java.time.Duration
import java.time.Instant import java.time.Instant
import java.time.temporal.ChronoUnit import java.time.temporal.ChronoUnit
class EthereumWsHeadSpec extends Specification { class GenericWsHeadSpec extends Specification {
BlockHash parent = BlockHash.from("0x3ec2ebf5d0ec474d0ac6bc50d2770d8409ad76e119968e7919f85d5ec8915200") BlockHash parent = BlockHash.from("0x3ec2ebf5d0ec474d0ac6bc50d2770d8409ad76e119968e7919f85d5ec8915200")
DefaultUpstream upstream = new GenericUpstreamMock(Chain.ETHEREUM__MAINNET, TestingCommons.api()) DefaultUpstream upstream = new GenericUpstreamMock(Chain.ETHEREUM__MAINNET, TestingCommons.api())
@@ -50,38 +51,29 @@ class EthereumWsHeadSpec extends Specification {
block.hash = BlockHash.from("0x3ec2ebf5d0ec474d0ac6bc50d2770d8409ad76e119968e7919f85d5ec8915200") block.hash = BlockHash.from("0x3ec2ebf5d0ec474d0ac6bc50d2770d8409ad76e119968e7919f85d5ec8915200")
block.parentHash = parent block.parentHash = parent
block.timestamp = Instant.now().truncatedTo(ChronoUnit.SECONDS) block.timestamp = Instant.now().truncatedTo(ChronoUnit.SECONDS)
block.transactions = [
new TransactionRefJson(TransactionId.from("0x29229361dc5aa1ec66c323dc7a299e2b61a8c8dd2a3522d41255ec10eca25dd8")),
new TransactionRefJson(TransactionId.from("0xebe8f22a55a9e26892a8545b93cbb2bfa4fd81c3184e50e5cf6276025bb42b93"))
]
block.uncles = [] block.uncles = []
block.totalDifficulty = BigInteger.ONE block.totalDifficulty = BigInteger.ONE
def headBlock = block.copy().tap { def headBlock = block.copy().with {
it.transactions = null
}.with {
Global.objectMapper.writeValueAsBytes(it) Global.objectMapper.writeValueAsBytes(it)
} }
def apiMock = TestingCommons.api() def apiMock = TestingCommons.api()
apiMock.answerOnce("eth_getBlockByHash", ["0x3ec2ebf5d0ec474d0ac6bc50d2770d8409ad76e119968e7919f85d5ec8915200", false], block)
def ws = Mock(WsSubscriptions) { def ws = Mock(WsSubscriptions) {
1 * it.connectionInfoFlux() >> Flux.empty() 1 * it.connectionInfoFlux() >> Flux.empty()
} }
def head = new EthereumWsHead(new AlwaysForkChoice(), BlockValidator.ALWAYS_VALID, apiMock, ws, false, Schedulers.boundedElastic(), Schedulers.boundedElastic(), upstream, EthereumChainSpecific.INSTANCE) def head = new GenericWsHead(new AlwaysForkChoice(), BlockValidator.ALWAYS_VALID, apiMock, ws, Schedulers.boundedElastic(), Schedulers.boundedElastic(), upstream, EthereumChainSpecific.INSTANCE)
def res = BlockContainer.from(block)
when: when:
def act = head.listenNewHeads().blockFirst() def act = head.listenNewHeads().blockFirst()
then: then:
act == BlockContainer.from(block) act == res
act.transactions.size() == 2
act.transactions[0].toHexWithPrefix() == "0x29229361dc5aa1ec66c323dc7a299e2b61a8c8dd2a3522d41255ec10eca25dd8"
act.transactions[1].toHexWithPrefix() == "0xebe8f22a55a9e26892a8545b93cbb2bfa4fd81c3184e50e5cf6276025bb42b93"
1 * ws.subscribe("newHeads") >> new WsSubscriptions.SubscribeData( 1 * ws.subscribe(_) >> new WsSubscriptions.SubscribeData(
Flux.fromIterable([headBlock]), "id" Flux.fromIterable([headBlock]), "id"
) )
} }
@@ -99,19 +91,17 @@ class EthereumWsHeadSpec extends Specification {
} }
def apiMock = TestingCommons.api() def apiMock = TestingCommons.api()
apiMock.answerOnce("eth_getBlockByHash", ["0x29229361dc5aa1ec66c323dc7a299e2b61a8c8dd2a3522d41255ec10eca25dd8", false], null)
apiMock.answerOnce("eth_blockNumber", [], Mono.empty())
def connectionInfoSink = Sinks.many().multicast().directBestEffort() def connectionInfoSink = Sinks.many().multicast().directBestEffort()
def ws = Mock(WsSubscriptions) { def ws = Mock(WsSubscriptions) {
1 * it.connectionInfoFlux() >> connectionInfoSink.asFlux() 1 * it.connectionInfoFlux() >> connectionInfoSink.asFlux()
2 * subscribe("newHeads") >>> [ 2 * subscribe(_) >>> [
new WsSubscriptions.SubscribeData(Flux.error(new RuntimeException()), "id"), new WsSubscriptions.SubscribeData(Flux.error(new RuntimeException()), "id"),
new WsSubscriptions.SubscribeData(Flux.fromIterable([secondHeadBlock]), "id") new WsSubscriptions.SubscribeData(Flux.fromIterable([secondHeadBlock]), "id")
] ]
} }
def head = new EthereumWsHead(new AlwaysForkChoice(), BlockValidator.ALWAYS_VALID, apiMock, ws, true, Schedulers.boundedElastic(), Schedulers.boundedElastic(), upstream, EthereumChainSpecific.INSTANCE) def head = new GenericWsHead(new AlwaysForkChoice(), BlockValidator.ALWAYS_VALID, apiMock, ws, Schedulers.boundedElastic(), Schedulers.boundedElastic(), upstream, EthereumChainSpecific.INSTANCE)
when: when:
def act = head.getFlux() def act = head.getFlux()
@@ -159,13 +149,13 @@ class EthereumWsHeadSpec extends Specification {
def ws = Mock(WsSubscriptions) { def ws = Mock(WsSubscriptions) {
1 * it.connectionInfoFlux() >> connectionInfoSink.asFlux() 1 * it.connectionInfoFlux() >> connectionInfoSink.asFlux()
2 * subscribe("newHeads") >>> [ 2 * subscribe(_) >>> [
new WsSubscriptions.SubscribeData(Flux.fromIterable([firstHeadBlock]), "id"), new WsSubscriptions.SubscribeData(Flux.fromIterable([firstHeadBlock]), "id"),
new WsSubscriptions.SubscribeData(Flux.fromIterable([secondHeadBlock]), "id") new WsSubscriptions.SubscribeData(Flux.fromIterable([secondHeadBlock]), "id")
] ]
} }
def head = new EthereumWsHead(new AlwaysForkChoice(), BlockValidator.ALWAYS_VALID, apiMock, ws, true, Schedulers.boundedElastic(), Schedulers.boundedElastic(), upstream, EthereumChainSpecific.INSTANCE) def head = new GenericWsHead(new AlwaysForkChoice(), BlockValidator.ALWAYS_VALID, apiMock, ws, Schedulers.boundedElastic(), Schedulers.boundedElastic(), upstream, EthereumChainSpecific.INSTANCE)
when: when:
def act = head.getFlux() def act = head.getFlux()
@@ -200,12 +190,12 @@ class EthereumWsHeadSpec extends Specification {
def ws = Mock(WsSubscriptions) { def ws = Mock(WsSubscriptions) {
1 * it.connectionInfoFlux() >> connectionInfoSink.asFlux() 1 * it.connectionInfoFlux() >> connectionInfoSink.asFlux()
1 * subscribe("newHeads") >>> [ 1 * subscribe(_) >>> [
new WsSubscriptions.SubscribeData(Flux.fromIterable([firstHeadBlock]), "id"), new WsSubscriptions.SubscribeData(Flux.fromIterable([firstHeadBlock]), "id"),
] ]
} }
def head = new EthereumWsHead( new AlwaysForkChoice(), BlockValidator.ALWAYS_VALID, apiMock, ws, true, Schedulers.boundedElastic(), Schedulers.boundedElastic(), upstream, EthereumChainSpecific.INSTANCE) def head = new GenericWsHead( new AlwaysForkChoice(), BlockValidator.ALWAYS_VALID, apiMock, ws, Schedulers.boundedElastic(), Schedulers.boundedElastic(), upstream, EthereumChainSpecific.INSTANCE)
when: when:
def act = head.getFlux() def act = head.getFlux()
@@ -239,12 +229,12 @@ class EthereumWsHeadSpec extends Specification {
def ws = Mock(WsSubscriptions) { def ws = Mock(WsSubscriptions) {
1 * it.connectionInfoFlux() >> connectionInfoSink.asFlux() 1 * it.connectionInfoFlux() >> connectionInfoSink.asFlux()
1 * subscribe("newHeads") >>> [ 1 * subscribe(_) >>> [
new WsSubscriptions.SubscribeData(Flux.fromIterable([firstHeadBlock]), "id"), new WsSubscriptions.SubscribeData(Flux.fromIterable([firstHeadBlock]), "id"),
] ]
} }
def head = new EthereumWsHead(new AlwaysForkChoice(), BlockValidator.ALWAYS_VALID, apiMock, ws, true, Schedulers.boundedElastic(), Schedulers.boundedElastic(), upstream, EthereumChainSpecific.INSTANCE) def head = new GenericWsHead(new AlwaysForkChoice(), BlockValidator.ALWAYS_VALID, apiMock, ws, Schedulers.boundedElastic(), Schedulers.boundedElastic(), upstream, EthereumChainSpecific.INSTANCE)
when: when:
def act = head.getFlux() def act = head.getFlux()
@@ -291,13 +281,13 @@ class EthereumWsHeadSpec extends Specification {
def ws = Mock(WsSubscriptions) { def ws = Mock(WsSubscriptions) {
1 * it.connectionInfoFlux() >> connectionInfoSink.asFlux() 1 * it.connectionInfoFlux() >> connectionInfoSink.asFlux()
2 * subscribe("newHeads") >>> [ 2 * subscribe(_) >>> [
new WsSubscriptions.SubscribeData(Flux.fromIterable([firstHeadBlock]), "id"), new WsSubscriptions.SubscribeData(Flux.fromIterable([firstHeadBlock]), "id"),
new WsSubscriptions.SubscribeData(Flux.fromIterable([secondHeadBlock]), "id"), new WsSubscriptions.SubscribeData(Flux.fromIterable([secondHeadBlock]), "id"),
] ]
} }
def head = new EthereumWsHead(new AlwaysForkChoice(), BlockValidator.ALWAYS_VALID, apiMock, ws, true, Schedulers.boundedElastic(), Schedulers.boundedElastic(), upstream, EthereumChainSpecific.INSTANCE) def head = new GenericWsHead(new AlwaysForkChoice(), BlockValidator.ALWAYS_VALID, apiMock, ws, Schedulers.boundedElastic(), Schedulers.boundedElastic(), upstream, EthereumChainSpecific.INSTANCE)
when: when:
def act = head.getFlux() def act = head.getFlux()

View File

@@ -45,7 +45,7 @@ class WsSubscriptionsImplSpec extends Specification {
def ws = new WsSubscriptionsImpl(pool) def ws = new WsSubscriptionsImpl(pool)
when: when:
def act = ws.subscribe("foo_bar") def act = ws.subscribe(new JsonRpcRequest("eth_subscribe", ["foo_bar"]))
.data .data
.map { new String(it) } .map { new String(it) }
.take(3) .take(3)
@@ -83,7 +83,7 @@ class WsSubscriptionsImplSpec extends Specification {
def ws = new WsSubscriptionsImpl(pool) def ws = new WsSubscriptionsImpl(pool)
when: when:
def act = ws.subscribe("foo_bar") def act = ws.subscribe(new JsonRpcRequest("eth_subscribe", ["foo_bar"]))
.data .data
.map { new String(it) } .map { new String(it) }
.take(3) .take(3)

View File

@@ -15,6 +15,7 @@
*/ */
package io.emeraldpay.dshackle.upstream.ethereum.subscribe package io.emeraldpay.dshackle.upstream.ethereum.subscribe
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
import io.emeraldpay.dshackle.upstream.Selector import io.emeraldpay.dshackle.upstream.Selector
import io.emeraldpay.dshackle.upstream.ethereum.WsSubscriptions import io.emeraldpay.dshackle.upstream.ethereum.WsSubscriptions
import reactor.core.publisher.Flux import reactor.core.publisher.Flux
@@ -40,7 +41,7 @@ class WebsocketPendingTxesSpec extends Specification {
.collectList().block(Duration.ofSeconds(1)) .collectList().block(Duration.ofSeconds(1))
then: then:
1 * ws.subscribe("newPendingTransactions") >> new WsSubscriptions.SubscribeData( 1 * ws.subscribe(new JsonRpcRequest("eth_subscribe", ["newPendingTransactions"])) >> new WsSubscriptions.SubscribeData(
Flux.fromIterable(responses), "id" Flux.fromIterable(responses), "id"
) )
txes.collect {it.toHex() } == [ txes.collect {it.toHex() } == [

File diff suppressed because one or more lines are too long

View File

@@ -1,7 +1,6 @@
package io.emeraldpay.dshackle.upstream.starknet package io.emeraldpay.dshackle.upstream.starknet
import io.emeraldpay.dshackle.data.BlockId import io.emeraldpay.dshackle.data.BlockId
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
import org.assertj.core.api.Assertions import org.assertj.core.api.Assertions
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
@@ -23,7 +22,7 @@ val example = """
class StarknetChainSpecificTest { class StarknetChainSpecificTest {
@Test @Test
fun parseResponse() { fun parseResponse() {
val result = StarknetChainSpecific.parseBlock(JsonRpcResponse.ok(example), "1") val result = StarknetChainSpecific.parseBlock(example.toByteArray(), "1")
Assertions.assertThat(result.height).isEqualTo(304789) Assertions.assertThat(result.height).isEqualTo(304789)
Assertions.assertThat(result.hash).isEqualTo(BlockId.from("046fa6638dc7fae06cece980ce4195436a79ef314ca49d99e0cef552d6f13c4e")) Assertions.assertThat(result.hash).isEqualTo(BlockId.from("046fa6638dc7fae06cece980ce4195436a79ef314ca49d99e0cef552d6f13c4e"))