Kadena support (#650)

* kadena support

* add kadena mempool endpoits

* createQuorumFor fix
This commit is contained in:
Andrey Bronin
2025-04-09 14:54:40 +03:00
committed by GitHub
parent 9ca2c921b5
commit 8b999d5439
12 changed files with 304 additions and 3 deletions

View File

@@ -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

View File

@@ -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"
}

View File

@@ -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")
}

View File

@@ -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,
)

View File

@@ -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))
}
}

View File

@@ -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)
}
}

View File

@@ -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
}

View File

@@ -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))
}
}