Kadena support (#650)
* kadena support * add kadena mempool endpoits * createQuorumFor fix
This commit is contained in:
@@ -138,6 +138,7 @@ open class CodeGen(private val config: ChainsConfig) {
|
||||
"ton" -> "BlockchainType.TON"
|
||||
"cosmos" -> "BlockchainType.COSMOS"
|
||||
"ripple" -> "BlockchainType.RIPPLE"
|
||||
"kadena" -> "BlockchainType.KADENA"
|
||||
else -> throw IllegalArgumentException("unknown blockchain type $type")
|
||||
}
|
||||
}
|
||||
|
||||
Submodule emerald-grpc updated: cea302c0d7...1b37beea67
@@ -13,7 +13,8 @@ enum class BlockchainType(
|
||||
ETHEREUM_BEACON_CHAIN(ApiType.REST),
|
||||
COSMOS(ApiType.JSON_RPC),
|
||||
TON(ApiType.REST),
|
||||
RIPPLE(ApiType.JSON_RPC),;
|
||||
RIPPLE(ApiType.JSON_RPC),
|
||||
KADENA(ApiType.REST),;
|
||||
}
|
||||
|
||||
enum class ApiType {
|
||||
|
||||
Submodule foundation/src/main/resources/public updated: 88313c0c55...cc548cfe19
@@ -4,6 +4,7 @@ import io.emeraldpay.dshackle.BlockchainType.BITCOIN
|
||||
import io.emeraldpay.dshackle.BlockchainType.COSMOS
|
||||
import io.emeraldpay.dshackle.BlockchainType.ETHEREUM
|
||||
import io.emeraldpay.dshackle.BlockchainType.ETHEREUM_BEACON_CHAIN
|
||||
import io.emeraldpay.dshackle.BlockchainType.KADENA
|
||||
import io.emeraldpay.dshackle.BlockchainType.NEAR
|
||||
import io.emeraldpay.dshackle.BlockchainType.POLKADOT
|
||||
import io.emeraldpay.dshackle.BlockchainType.RIPPLE
|
||||
@@ -19,6 +20,7 @@ import io.emeraldpay.dshackle.upstream.calls.DefaultBeaconChainMethods
|
||||
import io.emeraldpay.dshackle.upstream.calls.DefaultBitcoinMethods
|
||||
import io.emeraldpay.dshackle.upstream.calls.DefaultCosmosMethods
|
||||
import io.emeraldpay.dshackle.upstream.calls.DefaultEthereumMethods
|
||||
import io.emeraldpay.dshackle.upstream.calls.DefaultKadenaMethods
|
||||
import io.emeraldpay.dshackle.upstream.calls.DefaultNearMethods
|
||||
import io.emeraldpay.dshackle.upstream.calls.DefaultPolkadotMethods
|
||||
import io.emeraldpay.dshackle.upstream.calls.DefaultRippleMethods
|
||||
@@ -54,6 +56,7 @@ class CallTargetsHolder {
|
||||
COSMOS -> DefaultCosmosMethods()
|
||||
TON -> DefaultTonHttpMethods(connection)
|
||||
RIPPLE -> DefaultRippleMethods()
|
||||
KADENA -> DefaultKadenaMethods()
|
||||
UNKNOWN -> throw IllegalArgumentException("unknown chain")
|
||||
}
|
||||
callTargets[chain] = created
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
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.dshackle.upstream.ethereum.rpc.RpcException
|
||||
|
||||
class DefaultKadenaMethods : CallMethods {
|
||||
|
||||
private val kadenaMethods = setOf(
|
||||
getMethod("/chain/*/hash"),
|
||||
postMethod("/chain/*/hash"),
|
||||
getMethod("/chain/*/header"),
|
||||
getMethod("/chain/*/header/*"),
|
||||
postMethod("/chain/*/header/branch"),
|
||||
getMethod("/chain/*/payload/*"),
|
||||
postMethod("/chain/*/payload/batch"),
|
||||
getMethod("/chain/*/payload/*/outputs"),
|
||||
postMethod("/chain/*/payload/*/outputs/batch"),
|
||||
getMethod("/chain/*/payload/*"),
|
||||
postMethod("/chain/*/pact/local"),
|
||||
postMethod("/chain/*/pact/send"),
|
||||
postMethod("/chain/*/pact/poll"),
|
||||
postMethod("/chain/*/pact/listen"),
|
||||
postMethod("/chain/*/pact/private"),
|
||||
postMethod("/chain/*/pact/spv"),
|
||||
postMethod("/chain/*/mempool/getPending"),
|
||||
postMethod("/chain/*/mempool/member"),
|
||||
postMethod("/chain/*/mempool/lookup"),
|
||||
)
|
||||
|
||||
private val insert = setOf(
|
||||
putMethod("/chain/*/mempool/insert"),
|
||||
)
|
||||
|
||||
private val allowedMethods: Set<String> = kadenaMethods + insert
|
||||
|
||||
override fun createQuorumFor(method: String): CallQuorum {
|
||||
if (insert.contains(method)) {
|
||||
return BroadcastQuorum()
|
||||
}
|
||||
return AlwaysQuorum()
|
||||
}
|
||||
|
||||
override fun isCallable(method: String): Boolean {
|
||||
return allowedMethods.contains(method)
|
||||
}
|
||||
|
||||
override fun getSupportedMethods(): Set<String> {
|
||||
return allowedMethods.toSortedSet()
|
||||
}
|
||||
|
||||
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> {
|
||||
return when (groupName) {
|
||||
"default" -> getSupportedMethods()
|
||||
else -> emptyList()
|
||||
}.toSet()
|
||||
}
|
||||
|
||||
private fun getMethod(method: String) = "GET#$method"
|
||||
|
||||
private fun postMethod(method: String) = "POST#$method"
|
||||
|
||||
private fun putMethod(method: String) = "PUT#$method"
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import io.emeraldpay.dshackle.BlockchainType.BITCOIN
|
||||
import io.emeraldpay.dshackle.BlockchainType.COSMOS
|
||||
import io.emeraldpay.dshackle.BlockchainType.ETHEREUM
|
||||
import io.emeraldpay.dshackle.BlockchainType.ETHEREUM_BEACON_CHAIN
|
||||
import io.emeraldpay.dshackle.BlockchainType.KADENA
|
||||
import io.emeraldpay.dshackle.BlockchainType.NEAR
|
||||
import io.emeraldpay.dshackle.BlockchainType.POLKADOT
|
||||
import io.emeraldpay.dshackle.BlockchainType.RIPPLE
|
||||
@@ -38,6 +39,7 @@ import io.emeraldpay.dshackle.upstream.cosmos.CosmosChainSpecific
|
||||
import io.emeraldpay.dshackle.upstream.ethereum.EthereumChainSpecific
|
||||
import io.emeraldpay.dshackle.upstream.ethereum.WsSubscriptions
|
||||
import io.emeraldpay.dshackle.upstream.finalization.FinalizationDetector
|
||||
import io.emeraldpay.dshackle.upstream.kadena.KadenaChainSpecific
|
||||
import io.emeraldpay.dshackle.upstream.lowerbound.LowerBoundService
|
||||
import io.emeraldpay.dshackle.upstream.near.NearChainSpecific
|
||||
import io.emeraldpay.dshackle.upstream.polkadot.PolkadotChainSpecific
|
||||
@@ -115,6 +117,7 @@ object ChainSpecificRegistry {
|
||||
TON -> TonHttpSpecific
|
||||
COSMOS -> CosmosChainSpecific
|
||||
RIPPLE -> RippleChainSpecific
|
||||
KADENA -> KadenaChainSpecific
|
||||
BITCOIN -> throw IllegalArgumentException("bitcoin should use custom streams implementation")
|
||||
UNKNOWN -> throw IllegalArgumentException("unknown chain")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
package io.emeraldpay.dshackle.upstream.kadena
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.fasterxml.jackson.module.kotlin.readValue
|
||||
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.ChainReader
|
||||
import io.emeraldpay.dshackle.upstream.ChainRequest
|
||||
import io.emeraldpay.dshackle.upstream.GenericSingleCallValidator
|
||||
import io.emeraldpay.dshackle.upstream.SingleValidator
|
||||
import io.emeraldpay.dshackle.upstream.Upstream
|
||||
import io.emeraldpay.dshackle.upstream.UpstreamAvailability
|
||||
import io.emeraldpay.dshackle.upstream.UpstreamSettingsDetector
|
||||
import io.emeraldpay.dshackle.upstream.ValidateUpstreamSettingsResult
|
||||
import io.emeraldpay.dshackle.upstream.generic.AbstractPollChainSpecific
|
||||
import io.emeraldpay.dshackle.upstream.lowerbound.LowerBoundService
|
||||
import io.emeraldpay.dshackle.upstream.rpcclient.RestParams
|
||||
import reactor.core.publisher.Mono
|
||||
import java.math.BigInteger
|
||||
import java.time.Instant
|
||||
import kotlin.io.encoding.Base64
|
||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||
|
||||
object KadenaChainSpecific : AbstractPollChainSpecific() {
|
||||
@OptIn(ExperimentalEncodingApi::class)
|
||||
override fun parseBlock(data: ByteArray, upstreamId: String, api: ChainReader): Mono<BlockContainer> {
|
||||
val block = Global.objectMapper.readValue<KadenaHeader>(data)
|
||||
|
||||
val blockHash = Base64.encode(block.id.encodeToByteArray())
|
||||
return Mono.just(
|
||||
BlockContainer(
|
||||
height = block.height,
|
||||
hash = BlockId.fromBase64(blockHash),
|
||||
difficulty = BigInteger.ZERO,
|
||||
timestamp = Instant.EPOCH,
|
||||
full = false,
|
||||
json = data,
|
||||
parsed = block,
|
||||
transactions = emptyList(),
|
||||
upstreamId = upstreamId,
|
||||
parentHash = BlockId.fromBase64(blockHash),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
override fun getFromHeader(data: ByteArray, upstreamId: String, api: ChainReader): Mono<BlockContainer> {
|
||||
throw NotImplementedError()
|
||||
}
|
||||
|
||||
override fun listenNewHeadsRequest(): ChainRequest {
|
||||
throw NotImplementedError()
|
||||
}
|
||||
|
||||
override fun unsubscribeNewHeadsRequest(subId: String): ChainRequest {
|
||||
throw NotImplementedError()
|
||||
}
|
||||
|
||||
override fun upstreamValidators(
|
||||
chain: Chain,
|
||||
upstream: Upstream,
|
||||
options: Options,
|
||||
config: ChainConfig,
|
||||
): List<SingleValidator<UpstreamAvailability>> {
|
||||
var validators = listOf(
|
||||
GenericSingleCallValidator(
|
||||
ChainRequest("GET#/cut", RestParams.emptyParams()),
|
||||
upstream,
|
||||
) { data ->
|
||||
val block = Global.objectMapper.readValue<KadenaHeader>(data)
|
||||
if (block.id != "") {
|
||||
UpstreamAvailability.OK
|
||||
} else {
|
||||
UpstreamAvailability.UNAVAILABLE
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
return validators
|
||||
}
|
||||
|
||||
override fun upstreamSettingsValidators(
|
||||
chain: Chain,
|
||||
upstream: Upstream,
|
||||
options: Options,
|
||||
config: ChainConfig,
|
||||
): List<SingleValidator<ValidateUpstreamSettingsResult>> {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
override fun lowerBoundService(chain: Chain, upstream: Upstream): LowerBoundService {
|
||||
return KadenaLowerBoundService(chain, upstream)
|
||||
}
|
||||
|
||||
override fun upstreamSettingsDetector(chain: Chain, upstream: Upstream): UpstreamSettingsDetector {
|
||||
return KadenaUpstreamSettingsDetector(upstream)
|
||||
}
|
||||
|
||||
override fun latestBlockRequest(): ChainRequest {
|
||||
return ChainRequest("GET#/cut", RestParams.emptyParams())
|
||||
}
|
||||
}
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
data class KadenaHeader(
|
||||
@JsonProperty("height") var height: Long,
|
||||
@JsonProperty("weight") var weight: String,
|
||||
@JsonProperty("instance") var instance: String,
|
||||
@JsonProperty("id") var id: String,
|
||||
)
|
||||
@@ -0,0 +1,15 @@
|
||||
package io.emeraldpay.dshackle.upstream.kadena
|
||||
|
||||
import io.emeraldpay.dshackle.Chain
|
||||
import io.emeraldpay.dshackle.upstream.Upstream
|
||||
import io.emeraldpay.dshackle.upstream.lowerbound.LowerBoundDetector
|
||||
import io.emeraldpay.dshackle.upstream.lowerbound.LowerBoundService
|
||||
|
||||
class KadenaLowerBoundService(
|
||||
private val chain: Chain,
|
||||
upstream: Upstream,
|
||||
) : LowerBoundService(chain, upstream) {
|
||||
override fun detectors(): List<LowerBoundDetector> {
|
||||
return listOf(KadenaLowerBoundStateDetector(chain))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package io.emeraldpay.dshackle.upstream.kadena
|
||||
|
||||
import io.emeraldpay.dshackle.Chain
|
||||
import io.emeraldpay.dshackle.upstream.lowerbound.LowerBoundData
|
||||
import io.emeraldpay.dshackle.upstream.lowerbound.LowerBoundDetector
|
||||
import io.emeraldpay.dshackle.upstream.lowerbound.LowerBoundType
|
||||
import reactor.core.publisher.Flux
|
||||
|
||||
class KadenaLowerBoundStateDetector(
|
||||
chain: Chain,
|
||||
) : LowerBoundDetector(chain) {
|
||||
|
||||
override fun period(): Long {
|
||||
return 120
|
||||
}
|
||||
|
||||
override fun internalDetectLowerBound(): Flux<LowerBoundData> {
|
||||
return Flux.just(LowerBoundData(1, LowerBoundType.STATE))
|
||||
}
|
||||
|
||||
override fun types(): Set<LowerBoundType> {
|
||||
return setOf(LowerBoundType.STATE)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package io.emeraldpay.dshackle.upstream.kadena
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.fasterxml.jackson.databind.JsonNode
|
||||
import com.fasterxml.jackson.module.kotlin.readValue
|
||||
import io.emeraldpay.dshackle.Global
|
||||
import io.emeraldpay.dshackle.upstream.BasicUpstreamSettingsDetector
|
||||
import io.emeraldpay.dshackle.upstream.ChainRequest
|
||||
import io.emeraldpay.dshackle.upstream.NodeTypeRequest
|
||||
import io.emeraldpay.dshackle.upstream.UNKNOWN_CLIENT_VERSION
|
||||
import io.emeraldpay.dshackle.upstream.Upstream
|
||||
import io.emeraldpay.dshackle.upstream.rpcclient.RestParams
|
||||
import reactor.core.publisher.Flux
|
||||
|
||||
class KadenaUpstreamSettingsDetector(
|
||||
upstream: Upstream,
|
||||
) : BasicUpstreamSettingsDetector(upstream) {
|
||||
override fun internalDetectLabels(): Flux<Pair<String, String>> {
|
||||
return Flux.merge(
|
||||
detectNodeType(),
|
||||
)
|
||||
}
|
||||
|
||||
override fun clientVersionRequest(): ChainRequest {
|
||||
return ChainRequest("GET#/cut", RestParams.emptyParams())
|
||||
}
|
||||
|
||||
override fun parseClientVersion(data: ByteArray): String {
|
||||
return Global.objectMapper.readValue<KadenaHeader>(data).instance
|
||||
}
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
data class KadenaHeader(
|
||||
@JsonProperty("instance") var instance: String,
|
||||
)
|
||||
|
||||
override fun nodeTypeRequest(): NodeTypeRequest = NodeTypeRequest(clientVersionRequest())
|
||||
|
||||
override fun clientType(node: JsonNode): String = "kadena"
|
||||
|
||||
override fun clientVersion(node: JsonNode): String =
|
||||
node.get("version")?.get("version")?.asText() ?: UNKNOWN_CLIENT_VERSION
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package io.emeraldpay.dshackle.upstream.kadena
|
||||
|
||||
import io.emeraldpay.dshackle.Chain
|
||||
import io.emeraldpay.dshackle.upstream.lowerbound.LowerBoundData
|
||||
import io.emeraldpay.dshackle.upstream.lowerbound.LowerBoundType
|
||||
import org.junit.jupiter.api.Test
|
||||
import reactor.test.StepVerifier
|
||||
import java.time.Duration
|
||||
|
||||
class KadenaLowerBoundStateDetectorTest {
|
||||
|
||||
@Test
|
||||
fun `kadena lower block is 1`() {
|
||||
val detector = KadenaLowerBoundStateDetector(Chain.KADENA__MAINNET)
|
||||
|
||||
StepVerifier.withVirtualTime { detector.detectLowerBound() }
|
||||
.expectSubscription()
|
||||
.expectNoEvent(Duration.ofSeconds(15))
|
||||
.expectNext(LowerBoundData(1, LowerBoundType.STATE))
|
||||
.thenCancel()
|
||||
.verify(Duration.ofSeconds(3))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user