From 8c5e69a7f5bd5846e2bb944d27123ecca1e52722 Mon Sep 17 00:00:00 2001 From: a10zn8 Date: Mon, 26 Feb 2024 22:26:38 +0300 Subject: [PATCH] near protocol support --- .../kotlin/chainsconfig.codegen.gradle.kts | 1 + emerald-grpc | 2 +- .../io/emeraldpay/dshackle/BlockchainType.kt | 4 +- foundation/src/main/resources/chains.yaml | 28 +++++ .../dshackle/upstream/CallTargetsHolder.kt | 2 + .../dshackle/upstream/DefaultNearMethods.kt | 63 ++++++++++ .../upstream/generic/ChainSpecific.kt | 3 + .../upstream/near/NearChainSpecific.kt | 110 ++++++++++++++++++ .../near/NearLowerBoundBlockDetector.kt | 25 ++++ .../upstream/rpcclient/JsonRpcRequest.kt | 8 +- 10 files changed, 242 insertions(+), 4 deletions(-) create mode 100644 src/main/kotlin/io/emeraldpay/dshackle/upstream/DefaultNearMethods.kt create mode 100644 src/main/kotlin/io/emeraldpay/dshackle/upstream/near/NearChainSpecific.kt create mode 100644 src/main/kotlin/io/emeraldpay/dshackle/upstream/near/NearLowerBoundBlockDetector.kt diff --git a/buildSrc/src/main/kotlin/chainsconfig.codegen.gradle.kts b/buildSrc/src/main/kotlin/chainsconfig.codegen.gradle.kts index f616ed3a..8a2c63ac 100644 --- a/buildSrc/src/main/kotlin/chainsconfig.codegen.gradle.kts +++ b/buildSrc/src/main/kotlin/chainsconfig.codegen.gradle.kts @@ -118,6 +118,7 @@ open class CodeGen(private val config: ChainsConfig) { "starknet" -> "BlockchainType.STARKNET" "polkadot" -> "BlockchainType.POLKADOT" "solana" -> "BlockchainType.SOLANA" + "near" -> "BlockchainType.NEAR" else -> throw IllegalArgumentException("unknown blockchain type $type") } } diff --git a/emerald-grpc b/emerald-grpc index 50776f99..8e188a16 160000 --- a/emerald-grpc +++ b/emerald-grpc @@ -1 +1 @@ -Subproject commit 50776f993e34b20574ff71b022620ece5b6faf7b +Subproject commit 8e188a16345cbf13de16b4353553d32fa016a513 diff --git a/foundation/src/main/kotlin/io/emeraldpay/dshackle/BlockchainType.kt b/foundation/src/main/kotlin/io/emeraldpay/dshackle/BlockchainType.kt index 5c20c906..e2ef746a 100644 --- a/foundation/src/main/kotlin/io/emeraldpay/dshackle/BlockchainType.kt +++ b/foundation/src/main/kotlin/io/emeraldpay/dshackle/BlockchainType.kt @@ -1,5 +1,5 @@ package io.emeraldpay.dshackle enum class BlockchainType { - UNKNOWN, BITCOIN, ETHEREUM, STARKNET, POLKADOT, SOLANA; -} + UNKNOWN, BITCOIN, ETHEREUM, STARKNET, POLKADOT, SOLANA, NEAR; +} \ No newline at end of file diff --git a/foundation/src/main/resources/chains.yaml b/foundation/src/main/resources/chains.yaml index 19d4b98d..97629847 100644 --- a/foundation/src/main/resources/chains.yaml +++ b/foundation/src/main/resources/chains.yaml @@ -1275,3 +1275,31 @@ chain-settings: grpcId: 10063 short-names: [ telos-testnet ] chain-id: 0x29 + - id: near + label: Near + type: near + settings: + fork-choice: quorum + expected-block-time: 1s + lags: + syncing: 40 + lagging: 20 + chains: + - id: Mainnet + priority: 100 + code: NEAR_MAINNET + grpcId: 1050 + short-names: [ near ] + chain-id: 0x0 + - id: Testnet + priority: 5 + code: NEAR_TESTNET + grpcId: 10064 + short-names: [ near-testnet ] + chain-id: 0x0 + - id: Betanet + priority: 1 + code: NEAR_BETANET + grpcId: 10065 + short-names: [ near-betanet ] + chain-id: 0x0 \ No newline at end of file diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/CallTargetsHolder.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/CallTargetsHolder.kt index 56bddb33..e3557614 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/CallTargetsHolder.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/CallTargetsHolder.kt @@ -2,6 +2,7 @@ package io.emeraldpay.dshackle.upstream import io.emeraldpay.dshackle.BlockchainType.BITCOIN import io.emeraldpay.dshackle.BlockchainType.ETHEREUM +import io.emeraldpay.dshackle.BlockchainType.NEAR import io.emeraldpay.dshackle.BlockchainType.POLKADOT import io.emeraldpay.dshackle.BlockchainType.SOLANA import io.emeraldpay.dshackle.BlockchainType.STARKNET @@ -29,6 +30,7 @@ class CallTargetsHolder { STARKNET -> DefaultStarknetMethods(chain) POLKADOT -> DefaultPolkadotMethods() SOLANA -> DefaultSolanaMethods() + NEAR -> DefaultNearMethods() UNKNOWN -> throw IllegalArgumentException("unknown chain") } callTargets[chain] = created diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/DefaultNearMethods.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/DefaultNearMethods.kt new file mode 100644 index 00000000..3853c1d2 --- /dev/null +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/DefaultNearMethods.kt @@ -0,0 +1,63 @@ +package io.emeraldpay.dshackle.upstream + +import io.emeraldpay.dshackle.quorum.AlwaysQuorum +import io.emeraldpay.dshackle.quorum.BroadcastQuorum +import io.emeraldpay.dshackle.quorum.CallQuorum +import io.emeraldpay.dshackle.upstream.calls.CallMethods +import io.emeraldpay.dshackle.upstream.ethereum.rpc.RpcException + +class DefaultNearMethods : CallMethods { + + private val all = setOf( + "view_access_key", + "query", + "EXPERIMENTAL_changes", + "block", + "chunk", + "EXPERIMENTAL_changes_in_block", + "gas_price", + "status", + "network_info", + "validators", + "tx", + "EXPERIMENTAL_tx_status", + "EXPERIMENTAL_receipt", + ) + + private val add = setOf( + "broadcast_tx_async", + "broadcast_tx_commit", + ) + + private val allowedMethods: Set = 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 = + when (groupName) { + "default" -> getSupportedMethods() + else -> emptyList() + }.toSet() + + override fun getSupportedMethods(): Set { + return allowedMethods.toSortedSet() + } +} diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/generic/ChainSpecific.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/generic/ChainSpecific.kt index c3dd83ac..07632114 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/generic/ChainSpecific.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/generic/ChainSpecific.kt @@ -2,6 +2,7 @@ package io.emeraldpay.dshackle.upstream.generic import io.emeraldpay.dshackle.BlockchainType.BITCOIN import io.emeraldpay.dshackle.BlockchainType.ETHEREUM +import io.emeraldpay.dshackle.BlockchainType.NEAR import io.emeraldpay.dshackle.BlockchainType.POLKADOT import io.emeraldpay.dshackle.BlockchainType.SOLANA import io.emeraldpay.dshackle.BlockchainType.STARKNET @@ -26,6 +27,7 @@ import io.emeraldpay.dshackle.upstream.calls.CallMethods import io.emeraldpay.dshackle.upstream.calls.CallSelector import io.emeraldpay.dshackle.upstream.ethereum.EthereumChainSpecific import io.emeraldpay.dshackle.upstream.ethereum.WsSubscriptions +import io.emeraldpay.dshackle.upstream.near.NearChainSpecific import io.emeraldpay.dshackle.upstream.polkadot.PolkadotChainSpecific import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest import io.emeraldpay.dshackle.upstream.solana.SolanaChainSpecific @@ -74,6 +76,7 @@ object ChainSpecificRegistry { STARKNET -> StarknetChainSpecific POLKADOT -> PolkadotChainSpecific SOLANA -> SolanaChainSpecific + NEAR -> NearChainSpecific BITCOIN -> throw IllegalArgumentException("bitcoin should use custom streams implementation") UNKNOWN -> throw IllegalArgumentException("unknown chain") } diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/near/NearChainSpecific.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/near/NearChainSpecific.kt new file mode 100644 index 00000000..14279a11 --- /dev/null +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/near/NearChainSpecific.kt @@ -0,0 +1,110 @@ +package io.emeraldpay.dshackle.upstream.near + +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.upstream.LowerBoundBlockDetector +import io.emeraldpay.dshackle.upstream.SingleCallValidator +import io.emeraldpay.dshackle.upstream.Upstream +import io.emeraldpay.dshackle.upstream.UpstreamAvailability +import io.emeraldpay.dshackle.upstream.UpstreamValidator +import io.emeraldpay.dshackle.upstream.generic.AbstractPollChainSpecific +import io.emeraldpay.dshackle.upstream.generic.GenericUpstreamValidator +import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest +import java.math.BigInteger +import java.time.Instant +import java.util.concurrent.TimeUnit + +object NearChainSpecific : AbstractPollChainSpecific() { + override fun parseBlock(data: ByteArray, upstreamId: String): BlockContainer { + val block = Global.objectMapper.readValue(data, NearBlock::class.java).header + + return BlockContainer( + height = block.height, + hash = BlockId.fromBase64(block.hash), + difficulty = BigInteger.ZERO, + timestamp = Instant.ofEpochMilli(TimeUnit.MILLISECONDS.convert(block.timestamp, TimeUnit.NANOSECONDS)), + full = false, + json = data, + parsed = block, + transactions = emptyList(), + upstreamId = upstreamId, + parentHash = BlockId.fromBase64(block.prevHash), + ) + } + + override fun parseHeader(data: ByteArray, upstreamId: String): BlockContainer { + throw NotImplementedError() + } + + override fun listenNewHeadsRequest(): JsonRpcRequest { + throw NotImplementedError() + } + + override fun unsubscribeNewHeadsRequest(subId: String): JsonRpcRequest { + throw NotImplementedError() + } + + override fun validator( + chain: Chain, + upstream: Upstream, + options: Options, + config: ChainConfig, + ): UpstreamValidator { + return GenericUpstreamValidator( + upstream, + options, + SingleCallValidator( + JsonRpcRequest("status", listOf()), + ) { data -> + validate(data) + }, + ) + } + + override fun lowerBoundBlockDetector(chain: Chain, upstream: Upstream): LowerBoundBlockDetector { + return NearLowerBoundBlockDetector(chain, upstream) + } + + fun validate(data: ByteArray): UpstreamAvailability { + val resp = Global.objectMapper.readValue(data, NearStatus::class.java) + return if (resp.syncInfo.syncing) { + UpstreamAvailability.SYNCING + } else { + UpstreamAvailability.OK + } + } + + override fun latestBlockRequest(): JsonRpcRequest = + JsonRpcRequest("block", mapOf("finality" to "optimistic")) +} + +@JsonIgnoreProperties(ignoreUnknown = true) +data class NearBlock( + @JsonProperty("header") var header: NearHeader, +) + +@JsonIgnoreProperties(ignoreUnknown = true) +data class NearHeader( + @JsonProperty("height") var height: Long, + @JsonProperty("hash") var hash: String, + @JsonProperty("prev_hash") var prevHash: String, + @JsonProperty("timestamp") var timestamp: Long, +) + +@JsonIgnoreProperties(ignoreUnknown = true) +data class NearStatus( + @JsonProperty("sync_info") var syncInfo: NearSync, +) + +@JsonIgnoreProperties(ignoreUnknown = true) +data class NearSync( + @JsonProperty("syncing") var syncing: Boolean, + @JsonProperty("earliest_block_height") var earliestHeight: Long, + @JsonProperty("earliest_block_time") var earliestBlockTime: Instant, +) diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/near/NearLowerBoundBlockDetector.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/near/NearLowerBoundBlockDetector.kt new file mode 100644 index 00000000..13a0fda8 --- /dev/null +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/near/NearLowerBoundBlockDetector.kt @@ -0,0 +1,25 @@ +package io.emeraldpay.dshackle.upstream.near + +import io.emeraldpay.dshackle.Chain +import io.emeraldpay.dshackle.Global +import io.emeraldpay.dshackle.upstream.LowerBoundBlockDetector +import io.emeraldpay.dshackle.upstream.Upstream +import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest +import reactor.core.publisher.Mono + +class NearLowerBoundBlockDetector( + chain: Chain, + val upstream: Upstream, +) : LowerBoundBlockDetector(chain, upstream) { + + override fun lowerBlockDetect(): Mono { + return upstream.getIngressReader().read(JsonRpcRequest("status", listOf())).map { + val resp = Global.objectMapper.readValue(it.getResult(), NearStatus::class.java) + LowerBlockData(resp.syncInfo.earliestHeight, null, resp.syncInfo.earliestBlockTime.toEpochMilli()) + } + } + + override fun periodRequest(): Long { + return 120 + } +} diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/rpcclient/JsonRpcRequest.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/rpcclient/JsonRpcRequest.kt index ad7f4ff7..1e28ad0a 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/rpcclient/JsonRpcRequest.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/rpcclient/JsonRpcRequest.kt @@ -29,6 +29,7 @@ data class JsonRpcRequest( val nonce: Long?, val selector: BlockchainOuterClass.Selector?, val isStreamed: Boolean = false, + val objParams: Map? = null, ) { @JvmOverloads constructor( @@ -39,12 +40,17 @@ data class JsonRpcRequest( isStreamed: Boolean = false, ) : this(method, params, 1, nonce, selectors, isStreamed) + constructor( + method: String, + objParams: Map, + ) : this(method, listOf(), 1, null, null, false, objParams) + fun toJson(): ByteArray { val json = mapOf( "jsonrpc" to "2.0", "id" to id, "method" to method, - "params" to params, + "params" to (objParams ?: params), ) return Global.objectMapper.writeValueAsBytes(json) }