diff --git a/buildSrc/src/main/kotlin/chainsconfig.codegen.gradle.kts b/buildSrc/src/main/kotlin/chainsconfig.codegen.gradle.kts index 57c0c5168..df141126a 100644 --- a/buildSrc/src/main/kotlin/chainsconfig.codegen.gradle.kts +++ b/buildSrc/src/main/kotlin/chainsconfig.codegen.gradle.kts @@ -135,6 +135,7 @@ open class CodeGen(private val config: ChainsConfig) { "solana" -> "BlockchainType.SOLANA" "near" -> "BlockchainType.NEAR" "eth-beacon-chain" -> "BlockchainType.ETHEREUM_BEACON_CHAIN" + "ton-center" -> "BlockchainType.TON_CENTER" "cosmos" -> "BlockchainType.COSMOS" else -> throw IllegalArgumentException("unknown blockchain type $type") } diff --git a/emerald-grpc b/emerald-grpc index 77ed85be8..ee2e55934 160000 --- a/emerald-grpc +++ b/emerald-grpc @@ -1 +1 @@ -Subproject commit 77ed85be8087689288182841855db8e0fa542c85 +Subproject commit ee2e5593482171830564f3f039fe868e801b60d3 diff --git a/foundation/src/main/kotlin/io/emeraldpay/dshackle/BlockchainType.kt b/foundation/src/main/kotlin/io/emeraldpay/dshackle/BlockchainType.kt index 55e94eac0..06bd4b850 100644 --- a/foundation/src/main/kotlin/io/emeraldpay/dshackle/BlockchainType.kt +++ b/foundation/src/main/kotlin/io/emeraldpay/dshackle/BlockchainType.kt @@ -11,7 +11,8 @@ enum class BlockchainType( SOLANA(ApiType.JSON_RPC), NEAR(ApiType.JSON_RPC), ETHEREUM_BEACON_CHAIN(ApiType.REST), - COSMOS(ApiType.JSON_RPC); + COSMOS(ApiType.JSON_RPC), + TON_CENTER(ApiType.REST); } enum class ApiType { diff --git a/foundation/src/main/resources/public b/foundation/src/main/resources/public index 583c93ae0..6c597df34 160000 --- a/foundation/src/main/resources/public +++ b/foundation/src/main/resources/public @@ -1 +1 @@ -Subproject commit 583c93ae0e5daa275e6df32844b83939b65b10d1 +Subproject commit 6c597df3476868475293a15a14299124ffcb46da diff --git a/src/main/kotlin/io/emeraldpay/dshackle/Global.kt b/src/main/kotlin/io/emeraldpay/dshackle/Global.kt index 3cefefc96..faef3e409 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/Global.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/Global.kt @@ -33,6 +33,8 @@ import io.emeraldpay.dshackle.upstream.bitcoin.data.RpcUnspent import io.emeraldpay.dshackle.upstream.bitcoin.data.RpcUnspentDeserializer import io.emeraldpay.dshackle.upstream.ethereum.domain.TransactionId import io.emeraldpay.dshackle.upstream.ethereum.subscribe.json.TransactionIdSerializer +import io.emeraldpay.dshackle.upstream.toncenter.TonCenterMasterchainInfo +import io.emeraldpay.dshackle.upstream.toncenter.TonCenterMasterchainInfoDeserializer import java.text.SimpleDateFormat import java.util.Locale import java.util.TimeZone @@ -81,6 +83,7 @@ class Global { module.addDeserializer(RpcUnspent::class.java, RpcUnspentDeserializer()) module.addDeserializer(ChainRequest::class.java, ChainRequest.Deserializer()) module.addDeserializer(BeaconChainBlockHeader::class.java, BeaconChainBlockHeaderDeserializer()) + module.addDeserializer(TonCenterMasterchainInfo::class.java, TonCenterMasterchainInfoDeserializer()) val objectMapper = ObjectMapper() objectMapper.registerModule(module) diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/CallTargetsHolder.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/CallTargetsHolder.kt index f4b5f46ac..5566c32a0 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/CallTargetsHolder.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/CallTargetsHolder.kt @@ -8,6 +8,7 @@ import io.emeraldpay.dshackle.BlockchainType.NEAR import io.emeraldpay.dshackle.BlockchainType.POLKADOT import io.emeraldpay.dshackle.BlockchainType.SOLANA import io.emeraldpay.dshackle.BlockchainType.STARKNET +import io.emeraldpay.dshackle.BlockchainType.TON_CENTER import io.emeraldpay.dshackle.BlockchainType.UNKNOWN import io.emeraldpay.dshackle.Chain import io.emeraldpay.dshackle.foundation.ChainOptions @@ -18,6 +19,7 @@ import io.emeraldpay.dshackle.upstream.calls.DefaultCosmosMethods 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.DefaultTonCenterHttpMethods import org.springframework.stereotype.Component @Component @@ -38,6 +40,7 @@ class CallTargetsHolder { NEAR -> DefaultNearMethods() ETHEREUM_BEACON_CHAIN -> DefaultBeaconChainMethods() COSMOS -> DefaultCosmosMethods() + TON_CENTER -> DefaultTonCenterHttpMethods() UNKNOWN -> throw IllegalArgumentException("unknown chain") } callTargets[chain] = created diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/calls/DefaultTonCenterHttpMethods.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/calls/DefaultTonCenterHttpMethods.kt new file mode 100644 index 000000000..3a1e222fe --- /dev/null +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/calls/DefaultTonCenterHttpMethods.kt @@ -0,0 +1,100 @@ +package io.emeraldpay.dshackle.upstream.calls + +import io.emeraldpay.dshackle.quorum.AlwaysQuorum +import io.emeraldpay.dshackle.quorum.CallQuorum +import io.emeraldpay.dshackle.upstream.ethereum.rpc.RpcException + +class DefaultTonCenterHttpMethods : CallMethods { + + // HTTP API section + private val accountHttpMethods = setOf( + getMethod("/getAddressInformation"), + getMethod("/getExtendedAddressInformation"), + getMethod("/getWalletInformation"), + getMethod("/getAddressBalance"), + getMethod("/getAddressState"), + getMethod("/packAddress"), + getMethod("/unpackAddress"), + getMethod("/getTokenData"), + getMethod("/detectAddress"), + ) + + private val blockHttpMethods = setOf( + getMethod("/getMasterchainInfo"), + getMethod("/getMasterchainBlockSignatures"), + getMethod("/getShardBlockProof"), + getMethod("/getConsensusBlock"), + getMethod("/lookupBlock"), + getMethod("/shards"), + getMethod("/getBlockHeader"), + getMethod("/getOutMsgQueueSizes"), + ) + + private val transactionHttpMethods = setOf( + getMethod("/getTransactions"), + getMethod("/getBlockTransactions"), + getMethod("/getBlockTransactionsExt"), + getMethod("/tryLocateTx"), + getMethod("/tryLocateResultTx"), + getMethod("/tryLocateSourceTx"), + ) + + private val getConfigHttpMethods = setOf( + getMethod("/getConfigParam"), + getMethod("/getConfigAll"), + ) + + private val runMethodHttpMethods = setOf( + postMethod("/runGetMethod"), + ) + + private val sendHttpMethods = setOf( + postMethod("/sendBoc"), + postMethod("/sendBocReturnHash"), + postMethod("/sendQuery"), + postMethod("/estimateFee"), + ) + + private val jsonRpcHttpMethods = setOf( + postMethod("/jsonRPC"), + ) + + private val allowedHttpMethods: Set = accountHttpMethods + + blockHttpMethods + + transactionHttpMethods + + getConfigHttpMethods + + runMethodHttpMethods + + sendHttpMethods + + jsonRpcHttpMethods + + override fun createQuorumFor(method: String): CallQuorum { + return AlwaysQuorum() + } + + override fun isCallable(method: String): Boolean { + return allowedHttpMethods.contains(method) + } + + override fun getSupportedMethods(): Set { + return allowedHttpMethods.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 { + return when (groupName) { + "default" -> allowedHttpMethods + else -> emptyList() + }.toSet() + } + + private fun getMethod(method: String) = "GET#$method" + + private fun postMethod(method: String) = "POST#$method" +} 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 2b5c86ed1..bbbb0238f 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/generic/ChainSpecific.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/generic/ChainSpecific.kt @@ -8,6 +8,7 @@ import io.emeraldpay.dshackle.BlockchainType.NEAR import io.emeraldpay.dshackle.BlockchainType.POLKADOT import io.emeraldpay.dshackle.BlockchainType.SOLANA import io.emeraldpay.dshackle.BlockchainType.STARKNET +import io.emeraldpay.dshackle.BlockchainType.TON_CENTER import io.emeraldpay.dshackle.BlockchainType.UNKNOWN import io.emeraldpay.dshackle.Chain import io.emeraldpay.dshackle.cache.Caches @@ -42,6 +43,7 @@ import io.emeraldpay.dshackle.upstream.near.NearChainSpecific import io.emeraldpay.dshackle.upstream.polkadot.PolkadotChainSpecific import io.emeraldpay.dshackle.upstream.solana.SolanaChainSpecific import io.emeraldpay.dshackle.upstream.starknet.StarknetChainSpecific +import io.emeraldpay.dshackle.upstream.toncenter.TonCenterHttpSpecific import org.apache.commons.collections4.Factory import org.springframework.cloud.sleuth.Tracer import reactor.core.publisher.Mono @@ -110,6 +112,7 @@ object ChainSpecificRegistry { SOLANA -> SolanaChainSpecific NEAR -> NearChainSpecific ETHEREUM_BEACON_CHAIN -> BeaconChainSpecific + TON_CENTER -> TonCenterHttpSpecific COSMOS -> CosmosChainSpecific BITCOIN -> throw IllegalArgumentException("bitcoin should use custom streams implementation") UNKNOWN -> throw IllegalArgumentException("unknown chain") diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/toncenter/TonCenterHttpSpecific.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/toncenter/TonCenterHttpSpecific.kt new file mode 100644 index 000000000..e8b524bf9 --- /dev/null +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/toncenter/TonCenterHttpSpecific.kt @@ -0,0 +1,136 @@ +package io.emeraldpay.dshackle.upstream.toncenter + +import com.fasterxml.jackson.core.JsonParser +import com.fasterxml.jackson.databind.DeserializationContext +import com.fasterxml.jackson.databind.JsonDeserializer +import com.fasterxml.jackson.databind.JsonNode +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.SingleValidator +import io.emeraldpay.dshackle.upstream.Upstream +import io.emeraldpay.dshackle.upstream.UpstreamAvailability +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 + +fun generateRandomString(length: Int): String { + val allowedChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" + return (1..length) + .map { allowedChars.random() } + .joinToString("") +} + +object TonCenterHttpSpecific : AbstractPollChainSpecific() { + override fun getFromHeader(data: ByteArray, upstreamId: String, api: ChainReader): Mono { + throw NotImplementedError() + } + + override fun listenNewHeadsRequest(): ChainRequest { + throw NotImplementedError() + } + + override fun unsubscribeNewHeadsRequest(subId: String): ChainRequest { + throw NotImplementedError() + } + + override fun lowerBoundService(chain: Chain, upstream: Upstream): LowerBoundService { + return TonCenterLowerBoundService(chain, upstream) + } + + override fun latestBlockRequest(): ChainRequest { + return ChainRequest("GET#/getMasterchainInfo", RestParams.emptyParams()) + } + + override fun parseBlock(data: ByteArray, upstreamId: String, api: ChainReader): Mono { + val blockHeader = Global.objectMapper.readValue(data) + + + + return Mono.just( + BlockContainer( + height = blockHeader.result.last.seqno, + hash = BlockId.fromBase64(blockHeader.result.last.root_hash), + difficulty = BigInteger.ZERO, + timestamp = Instant.EPOCH, + full = false, + json = data, + parsed = blockHeader, + transactions = emptyList(), + upstreamId = upstreamId, + parentHash = BlockId.fromBase64(generateRandomString(32)), + ), + ) + } + + override fun upstreamValidators( + chain: Chain, + upstream: Upstream, + options: Options, + config: ChainConfig, + ): List> { + return emptyList() + } + + override fun upstreamSettingsValidators( + chain: Chain, + upstream: Upstream, + options: Options, + config: ChainConfig, + ): List> { + // add check generic block + return emptyList() + } +} + +data class TonCenterMasterchainInfoResultLast( + val workchain: Int, + val shard: String, + val seqno: Long, + val root_hash: String, + val file_hash: String, +) + +data class TonCenterMasterchainInfoResult( + val last: TonCenterMasterchainInfoResultLast, +) + +data class TonCenterMasterchainInfo( + val ok: Boolean, + val result: TonCenterMasterchainInfoResult, +) + +class TonCenterMasterchainInfoDeserializer : JsonDeserializer() { + override fun deserialize(p: JsonParser, ctxt: DeserializationContext): TonCenterMasterchainInfo { + val node = p.readValueAsTree() + val ok = node["ok"].booleanValue() + val workchain = node["result"]["last"]["workchain"].intValue() + val shard = node["result"]["last"]["shard"].textValue() + val seqno = node["result"]["last"]["seqno"].longValue() + val root_hash = node["result"]["last"]["root_hash"].textValue() + val file_hash = node["result"]["last"]["file_hash"].textValue() + + return TonCenterMasterchainInfo( + ok, + TonCenterMasterchainInfoResult( + TonCenterMasterchainInfoResultLast( + workchain, + shard, + seqno, + root_hash, + file_hash, + ), + ), + ) + } +} diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/toncenter/TonCenterLowerBoundService.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/toncenter/TonCenterLowerBoundService.kt new file mode 100644 index 000000000..3c30c9291 --- /dev/null +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/toncenter/TonCenterLowerBoundService.kt @@ -0,0 +1,15 @@ +package io.emeraldpay.dshackle.upstream.toncenter + +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 TonCenterLowerBoundService( + chain: Chain, + upstream: Upstream, +) : LowerBoundService(chain, upstream) { + override fun detectors(): List { + return listOf() + } +}