32
foundation/build.gradle
Normal file
32
foundation/build.gradle
Normal file
@@ -0,0 +1,32 @@
|
||||
plugins {
|
||||
id 'org.jetbrains.kotlin.jvm' version '1.9.10'
|
||||
id 'maven-publish'
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenLocal()
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
group = 'dshackle'
|
||||
|
||||
dependencies {
|
||||
implementation 'org.yaml:snakeyaml:1.24'
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter:5.9.1'
|
||||
}
|
||||
|
||||
test {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
version '1.0.0'
|
||||
|
||||
publishing {
|
||||
publications {
|
||||
mavenJava(MavenPublication) {
|
||||
from components.java
|
||||
}
|
||||
}
|
||||
repositories {
|
||||
mavenLocal()
|
||||
}
|
||||
}
|
||||
0
foundation/settings.gradle
Normal file
0
foundation/settings.gradle
Normal file
@@ -0,0 +1,90 @@
|
||||
package io.emeraldpay.dshackle.foundation
|
||||
|
||||
import java.time.Duration
|
||||
|
||||
class ChainOptions {
|
||||
data class Options(
|
||||
val disableUpstreamValidation: Boolean,
|
||||
val disableValidation: Boolean,
|
||||
val validationInterval: Int,
|
||||
val timeout: Duration,
|
||||
val providesBalance: Boolean?,
|
||||
val validatePeers: Boolean,
|
||||
val minPeers: Int,
|
||||
val validateSyncing: Boolean,
|
||||
val validateCallLimit: Boolean,
|
||||
val validateChain: Boolean,
|
||||
)
|
||||
|
||||
open class DefaultOptions : PartialOptions() {
|
||||
var chains: List<String>? = null
|
||||
var options: PartialOptions? = null
|
||||
}
|
||||
|
||||
open class PartialOptions {
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun getDefaults(): PartialOptions {
|
||||
val options = PartialOptions()
|
||||
options.minPeers = 1
|
||||
return options
|
||||
}
|
||||
}
|
||||
|
||||
var disableValidation: Boolean? = null
|
||||
var disableUpstreamValidation: Boolean? = null
|
||||
var validationInterval: Int? = null
|
||||
set(value) {
|
||||
require(value == null || value > 0) {
|
||||
"validation-interval must be a positive number: $value"
|
||||
}
|
||||
field = value
|
||||
}
|
||||
var timeout: Duration? = null
|
||||
var providesBalance: Boolean? = null
|
||||
var validatePeers: Boolean? = null
|
||||
var validateCalllimit: Boolean? = null
|
||||
var minPeers: Int? = null
|
||||
set(value) {
|
||||
require(value == null || value >= 0) {
|
||||
"min-peers must be a positive number: $value"
|
||||
}
|
||||
field = value
|
||||
}
|
||||
var validateSyncing: Boolean? = null
|
||||
var validateChain: Boolean? = null
|
||||
|
||||
fun merge(overwrites: PartialOptions?): PartialOptions {
|
||||
if (overwrites == null) {
|
||||
return this
|
||||
}
|
||||
val copy = PartialOptions()
|
||||
copy.validatePeers = overwrites.validatePeers ?: this.validatePeers
|
||||
copy.minPeers = overwrites.minPeers ?: this.minPeers
|
||||
copy.disableValidation = overwrites.disableValidation ?: this.disableValidation
|
||||
copy.validationInterval = overwrites.validationInterval ?: this.validationInterval
|
||||
copy.providesBalance = overwrites.providesBalance ?: this.providesBalance
|
||||
copy.validateSyncing = overwrites.validateSyncing ?: this.validateSyncing
|
||||
copy.validateCalllimit = overwrites.validateCalllimit ?: this.validateCalllimit
|
||||
copy.timeout = overwrites.timeout ?: this.timeout
|
||||
copy.validateChain = overwrites.validateChain ?: this.validateChain
|
||||
copy.disableUpstreamValidation =
|
||||
overwrites.disableUpstreamValidation ?: this.disableUpstreamValidation
|
||||
return copy
|
||||
}
|
||||
|
||||
fun buildOptions(): Options =
|
||||
Options(
|
||||
this.disableUpstreamValidation ?: false,
|
||||
this.disableValidation ?: false,
|
||||
this.validationInterval ?: 30,
|
||||
this.timeout ?: Duration.ofSeconds(60),
|
||||
this.providesBalance,
|
||||
this.validatePeers ?: true,
|
||||
this.minPeers ?: 1,
|
||||
this.validateSyncing ?: true,
|
||||
this.validateCalllimit ?: true,
|
||||
this.validateChain ?: true,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package io.emeraldpay.dshackle.foundation
|
||||
|
||||
import org.yaml.snakeyaml.nodes.MappingNode
|
||||
import java.time.Duration
|
||||
|
||||
class ChainOptionsReader : YamlConfigReader<ChainOptions.PartialOptions>() {
|
||||
override fun read(upNode: MappingNode?): ChainOptions.PartialOptions? {
|
||||
return if (hasAny(upNode, "options")) {
|
||||
return getMapping(upNode, "options")?.let { values ->
|
||||
readOptions(values)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun readOptions(values: MappingNode): ChainOptions.PartialOptions {
|
||||
val options = ChainOptions.PartialOptions()
|
||||
getValueAsBool(values, "validate-peers")?.let {
|
||||
options.validatePeers = it
|
||||
}
|
||||
getValueAsBool(values, "validate-syncing")?.let {
|
||||
options.validateSyncing = it
|
||||
}
|
||||
getValueAsBool(values, "validate-call-limit")?.let {
|
||||
options.validateCalllimit = it
|
||||
}
|
||||
getValueAsBool(values, "validate-chain")?.let {
|
||||
options.validateChain = it
|
||||
}
|
||||
getValueAsInt(values, "min-peers")?.let {
|
||||
options.minPeers = it
|
||||
}
|
||||
getValueAsInt(values, "timeout")?.let {
|
||||
options.timeout = Duration.ofSeconds(it.toLong())
|
||||
}
|
||||
getValueAsBool(values, "disable-validation")?.let {
|
||||
options.disableValidation = it
|
||||
}
|
||||
getValueAsInt(values, "validation-interval")?.let {
|
||||
options.validationInterval = it
|
||||
}
|
||||
getValueAsBool(values, "balance")?.let {
|
||||
options.providesBalance = it
|
||||
}
|
||||
return options
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* Copyright (c) 2020 EmeraldPay, Inc
|
||||
*
|
||||
* 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.foundation
|
||||
|
||||
import org.yaml.snakeyaml.nodes.MappingNode
|
||||
|
||||
interface ConfigReader<T> {
|
||||
|
||||
fun read(input: MappingNode?): T?
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* Copyright (c) 2020 EmeraldPay, Inc
|
||||
*
|
||||
* 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.foundation
|
||||
|
||||
/**
|
||||
* Update configuration value from environment variables. Format: ${ENV_VAR_NAME}
|
||||
*/
|
||||
class EnvVariables {
|
||||
|
||||
companion object {
|
||||
private val envRegex = Regex("\\$\\{(\\w+?)}")
|
||||
}
|
||||
|
||||
fun postProcess(value: String): String {
|
||||
return envRegex.replace(value) { m ->
|
||||
m.groups[1]?.let { g ->
|
||||
System.getProperty(g.value) ?: System.getenv(g.value) ?: ""
|
||||
} ?: ""
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
/**
|
||||
* Copyright (c) 2020 EmeraldPay, Inc
|
||||
* Copyright (c) 2020 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.foundation
|
||||
|
||||
import org.yaml.snakeyaml.Yaml
|
||||
import org.yaml.snakeyaml.nodes.CollectionNode
|
||||
import org.yaml.snakeyaml.nodes.MappingNode
|
||||
import org.yaml.snakeyaml.nodes.Node
|
||||
import org.yaml.snakeyaml.nodes.NodeTuple
|
||||
import org.yaml.snakeyaml.nodes.ScalarNode
|
||||
import java.io.InputStream
|
||||
import java.io.InputStreamReader
|
||||
import java.util.Locale
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.toJavaDuration
|
||||
|
||||
abstract class YamlConfigReader<T> : ConfigReader<T> {
|
||||
private val envVariables = EnvVariables()
|
||||
|
||||
val filename = "dshackle.yaml"
|
||||
|
||||
fun read(input: InputStream): T? {
|
||||
val configNode = readNode(input)
|
||||
return read(configNode)
|
||||
}
|
||||
fun readNode(input: String): MappingNode {
|
||||
return readNode(input.byteInputStream())
|
||||
}
|
||||
|
||||
fun readNode(input: InputStream): MappingNode {
|
||||
val yaml = Yaml()
|
||||
return asMappingNode(yaml.compose(InputStreamReader(input)))
|
||||
}
|
||||
|
||||
fun mergeMappingNode(a: MappingNode?, b: MappingNode?): MappingNode? = when {
|
||||
a == null -> b
|
||||
b == null -> a
|
||||
else -> {
|
||||
val mergedTuples = a.value.toMutableList()
|
||||
|
||||
mergedTuples.addAll(
|
||||
b.value.filter { tupleB ->
|
||||
mergedTuples.none { it.keyNode.valueAsString() == tupleB.keyNode.valueAsString() } && (tupleB.valueNode is MappingNode || tupleB.valueNode is ScalarNode)
|
||||
},
|
||||
)
|
||||
|
||||
a.value.forEach { tupleA ->
|
||||
b.value.find { it.keyNode.valueAsString() == tupleA.keyNode.valueAsString() }?.let { tupleB ->
|
||||
if (tupleA.valueNode is MappingNode && tupleB.valueNode is MappingNode) {
|
||||
mergedTuples[mergedTuples.indexOf(tupleA)] = NodeTuple(tupleA.keyNode, mergeMappingNode(tupleA.valueNode as MappingNode, tupleB.valueNode as MappingNode))
|
||||
} else if (tupleA.valueNode is ScalarNode && tupleB.valueNode is ScalarNode) {
|
||||
mergedTuples[mergedTuples.indexOf(tupleA)] = NodeTuple(tupleA.keyNode, tupleB.valueNode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MappingNode(a.tag, mergedTuples, a.flowStyle)
|
||||
}
|
||||
}
|
||||
|
||||
protected fun hasAny(mappingNode: MappingNode?, key: String): Boolean =
|
||||
mappingNode?.let { node ->
|
||||
node.value
|
||||
.stream()
|
||||
.anyMatch { it.keyNode.valueAsString() == key }
|
||||
} ?: false
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private fun <T> getValue(mappingNode: MappingNode?, key: String, type: Class<T>): T? {
|
||||
if (mappingNode == null) {
|
||||
return null
|
||||
}
|
||||
return mappingNode.value
|
||||
.stream()
|
||||
.filter { type.isAssignableFrom(it.valueNode.javaClass) }
|
||||
.filter { it.keyNode.valueAsString() == key }
|
||||
.map { n -> n.valueNode as T }
|
||||
.findFirst()
|
||||
.orElse(null)
|
||||
}
|
||||
|
||||
fun Node.valueAsString(): String? = if (this is ScalarNode) this.value else null
|
||||
|
||||
protected fun getMapping(mappingNode: MappingNode?, key: String): MappingNode? {
|
||||
return getValue(mappingNode, key, MappingNode::class.java)
|
||||
}
|
||||
|
||||
private fun getValue(mappingNode: MappingNode?, key: String): ScalarNode? {
|
||||
return getValue(mappingNode, key, ScalarNode::class.java)
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
protected fun <T> getList(mappingNode: MappingNode?, key: String): CollectionNode<T>? {
|
||||
val value = getValue(mappingNode, key, CollectionNode::class.java) ?: return null
|
||||
return value as CollectionNode<T>
|
||||
}
|
||||
|
||||
protected fun getListOfString(mappingNode: MappingNode?, key: String): List<String>? {
|
||||
return getList<ScalarNode>(mappingNode, key)?.value
|
||||
?.map { it.value }
|
||||
?.map(envVariables::postProcess)
|
||||
}
|
||||
|
||||
protected fun getValueAsString(mappingNode: MappingNode?, key: String): String? {
|
||||
return getValue(mappingNode, key)?.let {
|
||||
return@let it.value
|
||||
}?.let(envVariables::postProcess)
|
||||
}
|
||||
|
||||
protected fun getValueAsDuration(mappingNode: MappingNode?, key: String): java.time.Duration? {
|
||||
return getValue(mappingNode, key)?.let {
|
||||
return@let if (it.isPlain) {
|
||||
Duration.parse(it.value).toJavaDuration()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected fun getValueAsInt(mappingNode: MappingNode?, key: String): Int? {
|
||||
return getValue(mappingNode, key)?.let {
|
||||
return@let if (it.isPlain) {
|
||||
it.value.toIntOrNull()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected fun getValueAsLong(mappingNode: MappingNode?, key: String): Long? {
|
||||
return getValue(mappingNode, key)?.let {
|
||||
return@let if (it.isPlain) {
|
||||
it.value.toLongOrNull()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected fun getValueAsBool(mappingNode: MappingNode?, key: String): Boolean? {
|
||||
return getValue(mappingNode, key)?.let {
|
||||
return@let if (it.isPlain) {
|
||||
it.value.lowercase(Locale.getDefault()) == "true"
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected fun asMappingNode(node: Node): MappingNode {
|
||||
return if (MappingNode::class.java.isAssignableFrom(node.javaClass)) {
|
||||
node as MappingNode
|
||||
} else {
|
||||
throw IllegalArgumentException("Not a map")
|
||||
}
|
||||
}
|
||||
|
||||
fun getValueAsBytes(mappingNode: MappingNode?, key: String): Int? {
|
||||
return getValueAsString(mappingNode, key)?.let(envVariables::postProcess)?.let {
|
||||
val m = Regex("^(\\d+)(m|mb|k|kb|b)?$").find(it.lowercase().trim())
|
||||
?: throw IllegalArgumentException("Not a data size: $it. Example of correct values: '1024', '1kb', '5mb'")
|
||||
val multiplier = m.groups[2]?.let {
|
||||
when (it.value) {
|
||||
"k", "kb" -> 1024
|
||||
"m", "mb" -> 1024 * 1024
|
||||
else -> 1
|
||||
}
|
||||
} ?: 1
|
||||
val base = m.groups[1]!!.value.toInt()
|
||||
base * multiplier
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package io.emeraldpay.dshackle.config
|
||||
|
||||
import io.emeraldpay.dshackle.foundation.ChainOptions
|
||||
import java.time.Duration
|
||||
|
||||
data class ChainsConfig(private val chains: List<ChainConfig>) : Iterable<ChainsConfig.ChainConfig> {
|
||||
private val chainMap: Map<String, ChainConfig> = chains.fold(emptyMap()) { acc, item ->
|
||||
acc.plus(item.shortNames.map { Pair(it, item) })
|
||||
}
|
||||
|
||||
override fun iterator(): Iterator<ChainConfig> {
|
||||
return chains.iterator()
|
||||
}
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun default(): ChainsConfig = ChainsConfig(emptyList())
|
||||
}
|
||||
|
||||
data class ChainConfig(
|
||||
val expectedBlockTime: Duration,
|
||||
val syncingLagSize: Int,
|
||||
val laggingLagSize: Int,
|
||||
val options: ChainOptions.PartialOptions,
|
||||
val chainId: String,
|
||||
val netVersion: Long,
|
||||
val grpcId: Int,
|
||||
val code: String,
|
||||
val shortNames: List<String>,
|
||||
val callLimitContract: String?,
|
||||
val id: String,
|
||||
val blockchain: String,
|
||||
) {
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun default() = ChainConfig(
|
||||
Duration.ofSeconds(12),
|
||||
6,
|
||||
1,
|
||||
ChainOptions.PartialOptions(),
|
||||
"0x0",
|
||||
0,
|
||||
0,
|
||||
"UNKNOWN",
|
||||
emptyList(),
|
||||
null,
|
||||
"undefined",
|
||||
"undefined",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun resolve(chain: String): ChainConfig {
|
||||
return chainMap[chain] ?: ChainConfig.default()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
package io.emeraldpay.dshackle.config
|
||||
|
||||
import io.emeraldpay.dshackle.foundation.ChainOptions
|
||||
import io.emeraldpay.dshackle.foundation.ChainOptionsReader
|
||||
import io.emeraldpay.dshackle.foundation.YamlConfigReader
|
||||
import org.yaml.snakeyaml.DumperOptions
|
||||
import org.yaml.snakeyaml.nodes.MappingNode
|
||||
import org.yaml.snakeyaml.nodes.NodeTuple
|
||||
import org.yaml.snakeyaml.nodes.ScalarNode
|
||||
import org.yaml.snakeyaml.nodes.Tag
|
||||
|
||||
class ChainsConfigReader(
|
||||
private val chainsOptionsReader: ChainOptionsReader,
|
||||
) : YamlConfigReader<ChainsConfig>() {
|
||||
|
||||
private val defaultConfig = this::class.java.getResourceAsStream("/chains.yaml")!!
|
||||
|
||||
private fun readChains(input: MappingNode?): Map<Int, Pair<String, MappingNode>> {
|
||||
return getMapping(input, "chain-settings")?.let { config ->
|
||||
val default = getMapping(config, "default")
|
||||
getList<MappingNode>(config, "protocols")?.let { protocols ->
|
||||
protocols.value.fold(emptyMap()) { acc, protocol ->
|
||||
val blockchain = getValueAsString(protocol, "id")
|
||||
?: throw IllegalArgumentException("Blockchain id is not defined")
|
||||
val settings = mergeMappingNode(default, getMapping(protocol, "settings"))
|
||||
acc.plus(
|
||||
getList<MappingNode>(protocol, "chains")?.let { chains ->
|
||||
chains.value.map { chain ->
|
||||
val chainSettings = mergeMappingNode(settings, getMapping(chain, "settings"))
|
||||
val updatedChain = mergeMappingNode(
|
||||
chain,
|
||||
MappingNode(
|
||||
chain.tag,
|
||||
listOf(
|
||||
NodeTuple(ScalarNode(Tag.STR, "settings", null, null, DumperOptions.ScalarStyle.LITERAL), chainSettings),
|
||||
NodeTuple(
|
||||
ScalarNode(Tag.STR, "blockchain", null, null, DumperOptions.ScalarStyle.LITERAL),
|
||||
ScalarNode(Tag.STR, blockchain, null, null, DumperOptions.ScalarStyle.LITERAL),
|
||||
),
|
||||
),
|
||||
chain.flowStyle,
|
||||
),
|
||||
)
|
||||
val grpcId = getValueAsInt(updatedChain, "grpcId")
|
||||
?: throw IllegalArgumentException("grpcId for chain is not defined")
|
||||
Pair(grpcId, Pair(blockchain, updatedChain!!))
|
||||
}
|
||||
} ?: listOf(),
|
||||
)
|
||||
}
|
||||
}
|
||||
} ?: emptyMap()
|
||||
}
|
||||
|
||||
private fun parseChain(blockchain: String, node: MappingNode): ChainsConfig.ChainConfig {
|
||||
val id = getValueAsString(node, "id")
|
||||
?: throw IllegalArgumentException("undefined id for $blockchain")
|
||||
val settings = getMapping(node, "settings") ?: throw IllegalArgumentException("undefined settings for $blockchain")
|
||||
val lags = getMapping(settings, "lags")?.let { lagConfig ->
|
||||
Pair(
|
||||
getValueAsInt(lagConfig, "syncing")
|
||||
?: throw IllegalArgumentException("undefined syncing for $blockchain"),
|
||||
getValueAsInt(lagConfig, "lagging")
|
||||
?: throw IllegalArgumentException("undefined lagging for $blockchain"),
|
||||
)
|
||||
} ?: throw IllegalArgumentException("undefined lags for $blockchain")
|
||||
val expectedBlockTime = getValueAsDuration(settings, "expected-block-time")
|
||||
?: throw IllegalArgumentException("undefined expected block time")
|
||||
val validateContract = getValueAsString(node, "call-validate-contract")
|
||||
val options = chainsOptionsReader.read(settings) ?: ChainOptions.PartialOptions()
|
||||
val chainId = getValueAsString(node, "chain-id")
|
||||
?: throw IllegalArgumentException("undefined chain id for $blockchain")
|
||||
val code = getValueAsString(node, "code")
|
||||
?: throw IllegalArgumentException("undefined code for $blockchain")
|
||||
val grpcId = getValueAsInt(node, "grpcId")
|
||||
?: throw IllegalArgumentException("undefined code for $blockchain")
|
||||
val netVersion = getValueAsLong(node, "net-version") ?: chainId.drop(2).toLong(radix = 16)
|
||||
val shortNames = getListOfString(node, "short-names")
|
||||
?: throw IllegalArgumentException("undefined shortnames for $blockchain")
|
||||
return ChainsConfig.ChainConfig(
|
||||
expectedBlockTime = expectedBlockTime,
|
||||
syncingLagSize = lags.first,
|
||||
laggingLagSize = lags.second,
|
||||
options = options,
|
||||
callLimitContract = validateContract,
|
||||
chainId = chainId,
|
||||
code = code,
|
||||
grpcId = grpcId,
|
||||
netVersion = netVersion,
|
||||
shortNames = shortNames,
|
||||
id = id,
|
||||
blockchain = blockchain,
|
||||
)
|
||||
}
|
||||
|
||||
override fun read(input: MappingNode?): ChainsConfig {
|
||||
val default = readChains(readNode(defaultConfig))
|
||||
val current = readChains(input)
|
||||
val chains = default.keys.toSet().plus(current.keys).map {
|
||||
val defChain = default[it]
|
||||
val curChain = current[it]
|
||||
when {
|
||||
curChain == null && defChain != null -> parseChain(defChain.first, defChain.second)
|
||||
curChain != null && defChain == null -> parseChain(curChain.first, curChain.second)
|
||||
curChain != null && defChain != null -> {
|
||||
val merged = mergeMappingNode(defChain.second, curChain.second)
|
||||
parseChain(defChain.first, merged!!)
|
||||
}
|
||||
else -> ChainsConfig.ChainConfig.default()
|
||||
}
|
||||
}
|
||||
return ChainsConfig(chains)
|
||||
}
|
||||
}
|
||||
407
foundation/src/main/resources/chains.yaml
Normal file
407
foundation/src/main/resources/chains.yaml
Normal file
@@ -0,0 +1,407 @@
|
||||
version: v1
|
||||
|
||||
chain-settings:
|
||||
default:
|
||||
expected-block-time: 12s
|
||||
lags:
|
||||
syncing: 6
|
||||
lagging: 1
|
||||
protocols:
|
||||
- id: bitcoin
|
||||
settings:
|
||||
expected-block-time: 10m
|
||||
lags:
|
||||
syncing: 3
|
||||
lagging: 1
|
||||
chains:
|
||||
- id: mainnet
|
||||
chain-id: 0x0
|
||||
short-names: [bitcoin, btc]
|
||||
code: BTC
|
||||
grpcId: 1
|
||||
- id: testnet
|
||||
chain-id: 0x0
|
||||
short-names: [bitcoin-testnet]
|
||||
code: TESTNET_BITCOIN
|
||||
grpcId: 10003
|
||||
- id: ethereum
|
||||
settings:
|
||||
expected-block-time: 12s
|
||||
lags:
|
||||
syncing: 6
|
||||
lagging: 1
|
||||
chains:
|
||||
- id: mainnet
|
||||
chain-id: 0x1
|
||||
short-names: [eth, ethereum, homestead]
|
||||
code: ETH
|
||||
grpcId: 100
|
||||
call-validate-contract: 0x32268860cAAc2948Ab5DdC7b20db5a420467Cf96
|
||||
- id: goerli
|
||||
chain-id: 0x5
|
||||
code: GOERLI
|
||||
grpcId: 10005
|
||||
short-names: [goerli, goerli-testnet]
|
||||
call-validate-contract: 0xCD9303A1F6da2a68f465A579a24cc2Ee5AE2192f
|
||||
- id: ropsten
|
||||
code: ROPSTEN
|
||||
grpcId: 10006
|
||||
chain-id: 0x3
|
||||
short-names: [ropsten, ropsten-testnet]
|
||||
- id: sepolia
|
||||
code: SEPOLIA
|
||||
grpcId: 10008
|
||||
chain-id: 0xaa36a7
|
||||
short-names: [sepolia, sepolia-testnet]
|
||||
- id: holesky
|
||||
code: ETHEREUM_HOLESKY
|
||||
grpcId: 10027
|
||||
chain-id: 0x4268
|
||||
short-names: [holesky, ethereum-holesky]
|
||||
- id: ethereum-classic
|
||||
chains:
|
||||
- id: mainnet
|
||||
short-names: [ethereum-classic, etc]
|
||||
chain-id: 0x3d
|
||||
net-version: 1
|
||||
code: ETC
|
||||
grpcId: 101
|
||||
- id: fantom
|
||||
settings:
|
||||
expected-block-time: 3s
|
||||
options:
|
||||
validate-peers: false
|
||||
lags:
|
||||
syncing: 10
|
||||
lagging: 5
|
||||
chains:
|
||||
- id: mainnet
|
||||
short-names: [fantom]
|
||||
code: FTM
|
||||
grpcId: 102
|
||||
chain-id: 0xfa
|
||||
- id: testnet
|
||||
code: FANTOM_TESTNET
|
||||
grpcId: 10016
|
||||
short-names: [fantom-testnet]
|
||||
chain-id: 0xfa2
|
||||
- id: polygon
|
||||
settings:
|
||||
expected-block-time: 2.7s
|
||||
lags:
|
||||
syncing: 20
|
||||
lagging: 10
|
||||
chains:
|
||||
- id: mainnet
|
||||
call-validate-contract: 0x53Daa71B04d589429f6d3DF52db123913B818F23
|
||||
code: POLYGON
|
||||
grpcId: 1002
|
||||
chain-id: 0x89
|
||||
short-names: [polygon, matic]
|
||||
- id: mumbai
|
||||
call-validate-contract: 0x53Daa71B04d589429f6d3DF52db123913B818F23
|
||||
code: POLYGON_POS_MUMBAI
|
||||
grpcId: 10013
|
||||
chain-id: 0x13881
|
||||
short-names: [polygon-mumbai]
|
||||
- id: arbitrum
|
||||
settings:
|
||||
expected-block-time: 260ms
|
||||
options:
|
||||
validate-peers: false
|
||||
lags:
|
||||
syncing: 40
|
||||
lagging: 20
|
||||
chains:
|
||||
- id: mainnet
|
||||
code: ARBITRUM
|
||||
grpcId: 1004
|
||||
short-names: [arbitrum, arb]
|
||||
chain-id: 0xa4b1
|
||||
- id: goerli
|
||||
code: ARBITRUM_TESTNET
|
||||
grpcId: 10009
|
||||
short-names: [arbitrum-testnet, arbitrum-goerli]
|
||||
chain-id: 0x66eed
|
||||
settings:
|
||||
expected-block-time: 1s
|
||||
- id: optimism
|
||||
settings:
|
||||
expected-block-time: 2s
|
||||
options:
|
||||
validate-peers: false
|
||||
lags:
|
||||
syncing: 40
|
||||
lagging: 20
|
||||
chains:
|
||||
- id: mainnet
|
||||
code: OPTIMISM
|
||||
grpcId: 1005
|
||||
short-names: [optimism]
|
||||
chain-id: 0xa
|
||||
- id: goerli
|
||||
code: OPTIMISM_TESTNET
|
||||
grpcId: 10010
|
||||
short-names: [optimism-testnet, optimism-goerli]
|
||||
chain-id: 0x1A4
|
||||
- id: bsc
|
||||
settings:
|
||||
expected-block-time: 3s
|
||||
lags:
|
||||
syncing: 20
|
||||
lagging: 10
|
||||
chains:
|
||||
- id: mainnet
|
||||
code: BSC
|
||||
grpcId: 1006
|
||||
chain-id: 0x38
|
||||
short-names: [bsc, binance, bnb-smart-chain]
|
||||
- id: testnet
|
||||
code: BSC_TESTNET
|
||||
grpcId: 10026
|
||||
short-names: [bsc-testnet]
|
||||
chain-id: 0x61
|
||||
- id: polygon-zkevm
|
||||
settings:
|
||||
expected-block-time: 2.7s
|
||||
options:
|
||||
disable-validation: true
|
||||
lags:
|
||||
syncing: 40
|
||||
lagging: 20
|
||||
chains:
|
||||
- id: mainnet
|
||||
code: POLYGON_ZKEVM
|
||||
grpcId: 1007
|
||||
short-names: [polygon-zkevm]
|
||||
chain-id: 0x44d
|
||||
- id: testnet
|
||||
code: POLYGON_ZKEVM_TESTNET
|
||||
grpcId: 10011
|
||||
short-names: [polygon-zkevm-testnet]
|
||||
chain-id: 0x5a2
|
||||
settings:
|
||||
expected-block-time: 1m
|
||||
- id: arbitrum-nova
|
||||
settings:
|
||||
expected-block-time: 1s
|
||||
options:
|
||||
disable-validation: true
|
||||
lags:
|
||||
syncing: 40
|
||||
lagging: 20
|
||||
chains:
|
||||
- id: mainnet
|
||||
code: ARBITRUM_NOVA
|
||||
grpcId: 1008
|
||||
short-names: [arbitrum-nova]
|
||||
chain-id: 0xa4ba
|
||||
- id: zksync
|
||||
settings:
|
||||
expected-block-time: 5s
|
||||
options:
|
||||
disable-validation: true
|
||||
lags:
|
||||
syncing: 40
|
||||
lagging: 20
|
||||
chains:
|
||||
- id: mainnet
|
||||
code: ZKSYNC
|
||||
grpcId: 1009
|
||||
chain-id: 0x144
|
||||
short-names: [zksync]
|
||||
- id: testnet
|
||||
code: ZKS_TESTNET
|
||||
grpcId: 10012
|
||||
chain-id: 0x118
|
||||
short-names: [zksync-testnet]
|
||||
- id: base
|
||||
settings:
|
||||
expected-block-time: 2s
|
||||
options:
|
||||
validate-peers: false
|
||||
lags:
|
||||
syncing: 40
|
||||
lagging: 20
|
||||
chains:
|
||||
- id: mainnet
|
||||
code: BASE
|
||||
grpcId: 1010
|
||||
short-names: [base]
|
||||
chain-id: 0x2105
|
||||
- id: goerli
|
||||
code: BASE_GOERLI
|
||||
grpcId: 10014
|
||||
short-names: [base-goerli]
|
||||
chain-id: 0x14a33
|
||||
- id: linea
|
||||
settings:
|
||||
expected-block-time: 12s
|
||||
lags:
|
||||
syncing: 6
|
||||
lagging: 1
|
||||
chains:
|
||||
- id: mainnet
|
||||
code: LINEA
|
||||
grpcId: 1011
|
||||
short-names: [linea]
|
||||
chain-id: 0xe708
|
||||
- id: goerli
|
||||
code: LINEA_GOERLI
|
||||
grpcId: 10015
|
||||
short-names: [linea-goerli]
|
||||
chain-id: 0xe704
|
||||
- id: gnosis
|
||||
settings:
|
||||
expected-block-time: 6s
|
||||
options:
|
||||
validate-peers: false
|
||||
lags:
|
||||
syncing: 10
|
||||
lagging: 5
|
||||
chains:
|
||||
- id: mainnet
|
||||
code: GNOSIS
|
||||
grpcId: 1012
|
||||
short-names: [gnosis]
|
||||
chain-id: 0x64
|
||||
- id: chiado
|
||||
code: GNOSIS_CHIADO
|
||||
grpcId: 10017
|
||||
short-names: [gnosis-chiado]
|
||||
chain-id: 0x27d8
|
||||
- id: avalanche
|
||||
settings:
|
||||
expected-block-time: 2s
|
||||
options:
|
||||
validate-peers: false
|
||||
validate-syncing: false
|
||||
lags:
|
||||
syncing: 10
|
||||
lagging: 5
|
||||
chains:
|
||||
- id: mainnet
|
||||
code: AVALANCHE
|
||||
grpcId: 1013
|
||||
short-names: [avalanche]
|
||||
chain-id: 0xa86a
|
||||
- id: fuji
|
||||
code: AVALANCHE_FUJI
|
||||
grpcId: 10018
|
||||
short-names: [avalanche-fuji]
|
||||
chain-id: 0xa869
|
||||
- id: aurora
|
||||
settings:
|
||||
expected-block-time: 1s
|
||||
options:
|
||||
validate-peers: false
|
||||
lags:
|
||||
syncing: 40
|
||||
lagging: 20
|
||||
chains:
|
||||
- id: mainnet
|
||||
code: AURORA
|
||||
grpcId: 1015
|
||||
short-names: [aurora]
|
||||
chain-id: 0x4e454152
|
||||
- id: testnet
|
||||
code: AURORA_TESTNET
|
||||
grpcId: 10021
|
||||
short-names: [aurora-testnet]
|
||||
chain-id: 0x4e454153
|
||||
- id: mantle
|
||||
settings:
|
||||
expected-block-time: 500ms
|
||||
options:
|
||||
validate-peers: false
|
||||
lags:
|
||||
syncing: 40
|
||||
lagging: 20
|
||||
chains:
|
||||
- id: mainnet
|
||||
code: MANTLE
|
||||
grpcId: 1017
|
||||
short-names: [mantle]
|
||||
chain-id: 0x1388
|
||||
- id: testnet
|
||||
code: MANTLE_TESTNET
|
||||
grpcId: 10023
|
||||
short-names: [mantle-testnet]
|
||||
chain-id: 0x1389
|
||||
- id: klaytn
|
||||
settings:
|
||||
expected-block-time: 1s
|
||||
options:
|
||||
validate-peers: false
|
||||
lags:
|
||||
syncing: 40
|
||||
lagging: 20
|
||||
chains:
|
||||
- id: mainnet
|
||||
code: KLAYTN
|
||||
grpcId: 1018
|
||||
short-names: [klaytn]
|
||||
chain-id: 0x2019
|
||||
- id: baobab
|
||||
code: KLAYTN_BAOBAB
|
||||
grpcId: 10024
|
||||
short-names: [klaytn-baobab]
|
||||
chain-id: 0x3e9
|
||||
- id: celo
|
||||
settings:
|
||||
expected-block-time: 5s
|
||||
lags:
|
||||
syncing: 10
|
||||
lagging: 5
|
||||
chains:
|
||||
- id: mainnet
|
||||
code: CELO
|
||||
grpcId: 1019
|
||||
short-names: [celo]
|
||||
chain-id: 0xa4ec
|
||||
- id: alfajores
|
||||
code: CELO_ALFAJORES
|
||||
grpcId: 10028
|
||||
short-names: [celo-alfajores]
|
||||
chain-id: 0xaef3
|
||||
- id: moonbeam
|
||||
settings:
|
||||
expected-block-time: 12s
|
||||
lags:
|
||||
syncing: 6
|
||||
lagging: 1
|
||||
chains:
|
||||
- id: mainnet
|
||||
code: MOONBEAM
|
||||
grpcId: 1020
|
||||
short-names: [moonbeam]
|
||||
chain-id: 0x504
|
||||
- id: moonriver
|
||||
code: MOONRIVER
|
||||
grpcId: 1021
|
||||
short-names: [moonriver]
|
||||
chain-id: 0x505
|
||||
- id: moonbase-alpha
|
||||
code: MOONBEAM_ALPHA
|
||||
grpcId: 10029
|
||||
short-names: [moonbase-alpha]
|
||||
chain-id: 0x507
|
||||
- id: scroll
|
||||
settings:
|
||||
expected-block-time: 3s
|
||||
options:
|
||||
validate-peers: false
|
||||
lags:
|
||||
syncing: 10
|
||||
lagging: 5
|
||||
chains:
|
||||
- id: alphanet
|
||||
code: SCROLL_ALPHANET
|
||||
grpcId: 10022
|
||||
short-names: [scroll-alphanet]
|
||||
chain-id: 0x82751
|
||||
- id: sepolia
|
||||
code: SCROLL_SEPOLIA
|
||||
grpcId: 10025
|
||||
short-names: [scroll-sepolia]
|
||||
chain-id: 0x8274f
|
||||
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* Copyright (c) 2020 EmeraldPay, Inc
|
||||
*
|
||||
* 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.config
|
||||
|
||||
import io.emeraldpay.dshackle.foundation.EnvVariables
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.params.ParameterizedTest
|
||||
import org.junit.jupiter.params.provider.CsvSource
|
||||
import org.junit.jupiter.params.provider.ValueSource
|
||||
|
||||
class EnvVariablesTest {
|
||||
|
||||
lateinit var reader: EnvVariables
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
reader = EnvVariables()
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = ["", "a", "13143", "/etc/client1.myservice.com.key", "true", "1a68f20154fc258fe4149c199ad8f281"])
|
||||
fun `Post process for usual strings`(s: String) {
|
||||
assertEquals(s, reader.postProcess(s))
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@CsvSource(
|
||||
"p_\${id}, p_1",
|
||||
"home: \${HOME}, home: /home/user",
|
||||
"\${PASSWORD}, 1a68f20154fc258fe4149c199ad8f281",
|
||||
)
|
||||
fun `Post process replaces from env`(orig: String, replaced: String) {
|
||||
System.setProperty("id", "1")
|
||||
System.setProperty("HOME", "/home/user")
|
||||
System.setProperty("PASSWORD", "1a68f20154fc258fe4149c199ad8f281")
|
||||
assertEquals(replaced, reader.postProcess(orig))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package io.emeraldpay.dshackle.foundation
|
||||
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.params.ParameterizedTest
|
||||
import org.junit.jupiter.params.provider.CsvSource
|
||||
import org.yaml.snakeyaml.Yaml
|
||||
import org.yaml.snakeyaml.nodes.MappingNode
|
||||
import java.io.StringReader
|
||||
|
||||
class YamlConfigReaderTest {
|
||||
|
||||
@ParameterizedTest
|
||||
@CsvSource(
|
||||
"1024, 1024",
|
||||
"1k, 1024",
|
||||
"1kb, 1024",
|
||||
"1K, 1024",
|
||||
"16kb, 16384",
|
||||
"1M, 1048576",
|
||||
"4mb, 4194304",
|
||||
)
|
||||
fun `reads bytes values`(input: String, expected: Int) {
|
||||
val rdr = Impl()
|
||||
assertEquals(expected, rdr.getValueAsBytes(rdr.asNode("test", input), "test"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `mergeMappingNode should merge nodes correctly`() {
|
||||
val yaml = Yaml()
|
||||
|
||||
val a = yaml.compose(
|
||||
StringReader(
|
||||
"""
|
||||
key1: value1
|
||||
key2:
|
||||
subkey1: subvalue1
|
||||
""",
|
||||
),
|
||||
) as MappingNode
|
||||
|
||||
val b = yaml.compose(
|
||||
StringReader(
|
||||
"""
|
||||
key1: value3
|
||||
key2:
|
||||
subkey1: subvalue1Modified
|
||||
subkey2: subvalue2
|
||||
key3: value3
|
||||
""",
|
||||
),
|
||||
) as MappingNode
|
||||
|
||||
val result = Impl().mergeMappingNode(a, b)!!
|
||||
val yml = Impl()
|
||||
|
||||
assertEquals(3, result.value.size)
|
||||
assertEquals("value3", yml.getNodeValue(result, "key1"))
|
||||
assertEquals("subvalue1Modified", yml.getNodeValue(yml.getNode(result, "key2") as MappingNode, "subkey1"))
|
||||
assertEquals("subvalue2", yml.getNodeValue(yml.getNode(result, "key2") as MappingNode, "subkey2"))
|
||||
assertEquals("value3", yml.getNodeValue(result, "key3"))
|
||||
}
|
||||
|
||||
class Impl : YamlConfigReader<Any>() {
|
||||
override fun read(input: MappingNode?): Any? {
|
||||
return null
|
||||
}
|
||||
fun getNode(node: MappingNode, key: String): Any? {
|
||||
return Impl().getMapping(node, key)
|
||||
}
|
||||
|
||||
fun getNodeValue(node: MappingNode, key: String): String? {
|
||||
return Impl().getValueAsString(node, key)
|
||||
}
|
||||
|
||||
fun asNode(key: String, value: String): MappingNode {
|
||||
return Yaml().compose(StringReader("$key: $value")) as MappingNode
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package org.drpc.chainsconfig
|
||||
|
||||
import io.emeraldpay.dshackle.config.ChainsConfigReader
|
||||
import io.emeraldpay.dshackle.foundation.ChainOptionsReader
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
internal class ChainsConfigReaderTest {
|
||||
|
||||
@Test
|
||||
fun `read standard config without custom`() {
|
||||
val reader = ChainsConfigReader(ChainOptionsReader())
|
||||
val config = reader.read(null)
|
||||
val arb = config.resolve("arbitrum-goerli")
|
||||
assertEquals(arb.chainId, "0x66eed")
|
||||
assertEquals(arb.expectedBlockTime.seconds, 1L)
|
||||
assertEquals(arb.options.validatePeers, false)
|
||||
assertEquals(arb.id, "goerli")
|
||||
|
||||
val ethc = config.resolve("ethereum-classic")
|
||||
assertEquals(ethc.chainId, "0x3d")
|
||||
assertEquals(ethc.netVersion, 1)
|
||||
assertEquals(ethc.code, "ETC")
|
||||
assertEquals(ethc.grpcId, 101)
|
||||
assertEquals(ethc.expectedBlockTime.seconds, 12)
|
||||
assertEquals(ethc.laggingLagSize, 1)
|
||||
assertEquals(ethc.syncingLagSize, 6)
|
||||
assertEquals(ethc.id, "mainnet")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `read standard config with custom one`() {
|
||||
val reader = ChainsConfigReader(ChainOptionsReader())
|
||||
val chains = reader.read(this.javaClass.classLoader.getResourceAsStream("configs/chains-basic.yaml")!!)!!
|
||||
val config = chains.resolve("fantom")
|
||||
assertEquals(config.expectedBlockTime.seconds, 10)
|
||||
assertEquals(config.chainId, "0xfb")
|
||||
assertEquals(config.syncingLagSize, 11)
|
||||
assertEquals(config.laggingLagSize, 4)
|
||||
assertEquals(config.code, "FTA")
|
||||
assertEquals(config.grpcId, 102)
|
||||
assertEquals(config.netVersion, 251)
|
||||
}
|
||||
}
|
||||
18
foundation/src/test/resources/configs/chains-basic.yaml
Normal file
18
foundation/src/test/resources/configs/chains-basic.yaml
Normal file
@@ -0,0 +1,18 @@
|
||||
version: v1
|
||||
|
||||
chain-settings:
|
||||
protocols:
|
||||
- id: fantom
|
||||
settings:
|
||||
expected-block-time: 10s
|
||||
options:
|
||||
validate-peers: true
|
||||
lags:
|
||||
syncing: 11
|
||||
lagging: 4
|
||||
chains:
|
||||
- id: mainnet
|
||||
short-names: [fantom]
|
||||
code: FTA
|
||||
grpcId: 102
|
||||
chain-id: 0xfb
|
||||
Reference in New Issue
Block a user