From 4e408d5293a7a9423b01c2ab9b673aec0e232fe8 Mon Sep 17 00:00:00 2001 From: Vladimir Logachev Date: Tue, 19 Nov 2024 16:14:20 +0400 Subject: [PATCH] Refactor from ficus to pureconfig (#3976) --- .../com/wavesplatform/state/Settings.scala | 6 +- build.sbt | 12 +- .../api/grpc/GRPCServerExtension.scala | 7 +- .../events/BlockchainUpdates.scala | 7 +- .../generator/TransactionsGeneratorApp.scala | 55 ++-- .../wavesplatform/it/BaseTargetChecker.scala | 4 +- .../scala/com/wavesplatform/it/Docker.scala | 24 +- .../scala/com/wavesplatform/it/Node.scala | 2 +- .../scala/com/wavesplatform/Application.scala | 8 +- .../wavesplatform/GenesisBlockGenerator.scala | 9 +- .../wavesplatform/account/PrivateKey.scala | 4 +- .../wavesplatform/network/NetworkServer.scala | 17 +- .../network/PeerDatabaseImpl.scala | 2 +- .../settings/BlockchainSettings.scala | 59 ++-- .../settings/CustomValueReaders.scala | 8 - .../wavesplatform/settings/DBSettings.scala | 2 +- .../settings/NetworkSettings.scala | 102 ++----- .../settings/WavesSettings.scala | 44 +-- .../com/wavesplatform/settings/package.scala | 66 ++--- .../utils/ConfigSettingsValidator.scala | 103 ------- .../com/wavesplatform/transaction/Asset.scala | 8 +- .../assets/exchange/AssetPair.scala | 3 - .../generator/BlockchainGeneratorApp.scala | 11 +- .../generator/MinerChallengeSimulator.scala | 10 +- .../http/RestAPISettingsHelper.scala | 10 +- .../BlacklistParallelSpecification.scala | 5 +- .../network/BlacklistSpecification.scala | 14 +- .../peer/PeerDatabaseImplSpecification.scala | 9 +- .../BlockchainSettingsSpecification.scala | 2 +- .../settings/DbSettingsSpecification.scala | 75 +++++ .../FeaturesSettingsSpecification.scala | 24 +- .../settings/MinerSettingsSpecification.scala | 6 +- .../NetworkSettingsSpecification.scala | 93 +++--- .../RestAPISettingsSpecification.scala | 6 +- ...SynchronizationSettingsSpecification.scala | 6 +- .../settings/UtxSettingsSpecification.scala | 36 +-- .../WalletSettingsSpecification.scala | 14 +- project/Dependencies.scala | 2 +- .../settings/RideRunnerGlobalSettings.scala | 7 +- .../runner/entrypoints/settings/package.scala | 37 --- .../runner/input/PureconfigImplicits.scala | 37 +++ .../runner/input/RideRunnerInputParser.scala | 273 ++++++++++-------- .../RideRunnerInputParserTestSuite.scala | 11 +- 43 files changed, 599 insertions(+), 641 deletions(-) delete mode 100644 node/src/main/scala/com/wavesplatform/settings/CustomValueReaders.scala delete mode 100644 node/src/main/scala/com/wavesplatform/settings/utils/ConfigSettingsValidator.scala create mode 100644 node/tests/src/test/scala/com/wavesplatform/settings/DbSettingsSpecification.scala delete mode 100644 ride-runner/src/main/scala/com/wavesplatform/ride/runner/entrypoints/settings/package.scala create mode 100644 ride-runner/src/main/scala/com/wavesplatform/ride/runner/input/PureconfigImplicits.scala diff --git a/benchmark/src/main/scala/com/wavesplatform/state/Settings.scala b/benchmark/src/main/scala/com/wavesplatform/state/Settings.scala index c5540ddc182..7539a6fdfcb 100644 --- a/benchmark/src/main/scala/com/wavesplatform/state/Settings.scala +++ b/benchmark/src/main/scala/com/wavesplatform/state/Settings.scala @@ -1,7 +1,8 @@ package com.wavesplatform.state import com.typesafe.config.Config -import net.ceedubs.ficus.Ficus._ +import pureconfig.ConfigSource +import pureconfig.generic.auto.* case class Settings( networkConfigFile: String, @@ -15,7 +16,6 @@ case class Settings( object Settings { def fromConfig(config: Config): Settings = { - import net.ceedubs.ficus.readers.ArbitraryTypeReader._ - config.as[Settings]("waves.benchmark.state") + ConfigSource.fromConfig(config).at("waves.benchmark.state").loadOrThrow[Settings] } } diff --git a/build.sbt b/build.sbt index f30f3432938..3d597af7d6b 100644 --- a/build.sbt +++ b/build.sbt @@ -82,10 +82,14 @@ lazy val `node-tests` = project lazy val `grpc-server` = project.dependsOn(node % "compile;runtime->provided", `node-testkit`, `node-tests` % "test->test") -lazy val `ride-runner` = project.dependsOn(node, `grpc-server`, `node-tests` % "test->test") -lazy val `node-it` = project.dependsOn(`repl-jvm`, `grpc-server`, `node-tests` % "test->test") -lazy val `node-generator` = project.dependsOn(node, `node-testkit`, `node-tests` % "compile->test") -lazy val benchmark = project.dependsOn(node, `node-tests` % "test->test") +lazy val `ride-runner` = project.dependsOn(node, `grpc-server`, `node-tests` % "test->test") +lazy val `node-it` = project.dependsOn(`repl-jvm`, `grpc-server`, `node-tests` % "test->test") +lazy val `node-generator` = project + .dependsOn(node, `node-testkit`, `node-tests` % "compile->test") + .settings( + libraryDependencies += "com.iheart" %% "ficus" % "1.5.2" + ) +lazy val benchmark = project.dependsOn(node, `node-tests` % "test->test") lazy val repl = crossProject(JSPlatform, JVMPlatform) .withoutSuffixFor(JVMPlatform) diff --git a/grpc-server/src/main/scala/com/wavesplatform/api/grpc/GRPCServerExtension.scala b/grpc-server/src/main/scala/com/wavesplatform/api/grpc/GRPCServerExtension.scala index efd9aaa2300..9fa7b00e647 100644 --- a/grpc-server/src/main/scala/com/wavesplatform/api/grpc/GRPCServerExtension.scala +++ b/grpc-server/src/main/scala/com/wavesplatform/api/grpc/GRPCServerExtension.scala @@ -8,16 +8,15 @@ import io.grpc.Server import io.grpc.netty.NettyServerBuilder import io.grpc.protobuf.services.ProtoReflectionService import monix.execution.Scheduler -import net.ceedubs.ficus.readers.namemappers.implicits.hyphenCase -import net.ceedubs.ficus.Ficus.* -import net.ceedubs.ficus.readers.ArbitraryTypeReader.* +import pureconfig.ConfigSource +import pureconfig.generic.auto.* import java.net.InetSocketAddress import java.util.concurrent.Executors import scala.concurrent.Future class GRPCServerExtension(context: ExtensionContext) extends Extension with ScorexLogging { - private val settings = context.settings.config.as[GRPCSettings]("waves.grpc") + private val settings = ConfigSource.fromConfig(context.settings.config).at("waves.grpc").loadOrThrow[GRPCSettings] private val executor = Executors.newFixedThreadPool(settings.workerThreads, new ThreadFactoryBuilder().setDaemon(true).setNameFormat("grpc-server-worker-%d").build()) private implicit val apiScheduler: Scheduler = Scheduler(executor) private val bindAddress = new InetSocketAddress(settings.host, settings.port) diff --git a/grpc-server/src/main/scala/com/wavesplatform/events/BlockchainUpdates.scala b/grpc-server/src/main/scala/com/wavesplatform/events/BlockchainUpdates.scala index 7cf25acde8c..ab717ef7b4b 100644 --- a/grpc-server/src/main/scala/com/wavesplatform/events/BlockchainUpdates.scala +++ b/grpc-server/src/main/scala/com/wavesplatform/events/BlockchainUpdates.scala @@ -12,10 +12,9 @@ import io.grpc.protobuf.services.ProtoReflectionService import io.grpc.{Metadata, Server, ServerStreamTracer, Status} import monix.execution.schedulers.SchedulerService import monix.execution.{ExecutionModel, Scheduler, UncaughtExceptionReporter} -import net.ceedubs.ficus.readers.namemappers.implicits.hyphenCase -import net.ceedubs.ficus.Ficus.* -import net.ceedubs.ficus.readers.ArbitraryTypeReader.* import org.rocksdb.RocksDB +import pureconfig.ConfigSource +import pureconfig.generic.auto.* import java.net.InetSocketAddress import java.util.concurrent.TimeUnit @@ -24,7 +23,7 @@ import scala.concurrent.duration.* import scala.util.Try class BlockchainUpdates(private val context: Context) extends Extension with ScorexLogging with BlockchainUpdateTriggers { - private[this] val settings = context.settings.config.as[BlockchainUpdatesSettings]("waves.blockchain-updates") + private[this] val settings = ConfigSource.fromConfig(context.settings.config).at("waves.blockchain-updates").loadOrThrow[BlockchainUpdatesSettings] private[this] implicit val scheduler: SchedulerService = Schedulers.fixedPool( settings.workerThreads, "blockchain-updates", diff --git a/node-generator/src/main/scala/com/wavesplatform/generator/TransactionsGeneratorApp.scala b/node-generator/src/main/scala/com/wavesplatform/generator/TransactionsGeneratorApp.scala index 70427421764..6854b83f448 100644 --- a/node-generator/src/main/scala/com/wavesplatform/generator/TransactionsGeneratorApp.scala +++ b/node-generator/src/main/scala/com/wavesplatform/generator/TransactionsGeneratorApp.scala @@ -1,17 +1,15 @@ package com.wavesplatform.generator import java.io.File -import java.net.InetSocketAddress +import java.net.{InetSocketAddress, URI} import java.util.concurrent.Executors - -import scala.concurrent._ -import scala.concurrent.duration._ +import scala.concurrent.* +import scala.concurrent.duration.* import scala.util.{Failure, Random, Success} - import cats.implicits.showInterpolator -import com.typesafe.config.ConfigFactory +import com.typesafe.config.{Config, ConfigFactory} import com.wavesplatform.account.AddressScheme -import com.wavesplatform.features.EstimatorProvider._ +import com.wavesplatform.features.EstimatorProvider.* import com.wavesplatform.generator.GeneratorSettings.NodeAddress import com.wavesplatform.generator.Preconditions.{PGenSettings, UniverseHolder} import com.wavesplatform.generator.cli.ScoptImplicits @@ -22,9 +20,9 @@ import com.wavesplatform.transaction.Transaction import com.wavesplatform.utils.{LoggerFacade, NTP} import com.wavesplatform.Application import monix.execution.Scheduler -import net.ceedubs.ficus.Ficus._ +import net.ceedubs.ficus.Ficus.* import net.ceedubs.ficus.readers.{EnumerationReader, NameMapper, ValueReader} -import net.ceedubs.ficus.readers.ArbitraryTypeReader._ +import net.ceedubs.ficus.readers.ArbitraryTypeReader.* import org.asynchttpclient.AsyncHttpClient import org.asynchttpclient.Dsl.asyncHttpClient import org.slf4j.LoggerFactory @@ -32,10 +30,14 @@ import scopt.OptionParser object TransactionsGeneratorApp extends App with ScoptImplicits with FicusImplicits with EnumerationReader { + implicit val inetSocketAddressReader: ValueReader[InetSocketAddress] = { (config: Config, path: String) => + val uri = new URI(s"my://${config.getString(path)}") + new InetSocketAddress(uri.getHost, uri.getPort) + } + // IDEA bugs - implicit val inetSocketAddressReader: ValueReader[InetSocketAddress] = com.wavesplatform.settings.inetSocketAddressReader - implicit val readConfigInHyphen: NameMapper = net.ceedubs.ficus.readers.namemappers.implicits.hyphenCase - implicit val httpClient: AsyncHttpClient = asyncHttpClient() + implicit val readConfigInHyphen: NameMapper = net.ceedubs.ficus.readers.namemappers.implicits.hyphenCase + implicit val httpClient: AsyncHttpClient = asyncHttpClient() val log = LoggerFacade(LoggerFactory.getLogger("generator")) @@ -228,21 +230,20 @@ object TransactionsGeneratorApp extends App with ScoptImplicits with FicusImplic log.info(s"Universe precondition tail transactions size: ${initialTailTransactions.size}") log.info(s"Generator precondition tail transactions size: ${initialGenTailTransactions.size}") - val workers = finalConfig.sendTo.map { - case NodeAddress(node, nodeRestUrl) => - log.info(s"Creating worker: ${node.getHostString}:${node.getPort}") - // new Worker(finalConfig.worker, sender, node, generator, initialTransactions.map(RawBytes.from)) - new Worker( - finalConfig.worker, - Iterator.continually(generator.next()).flatten, - sender, - node, - nodeRestUrl, - () => canContinue, - initialUniTransactions ++ initialGenTransactions, - finalConfig.privateKeyAccounts.map(_.toAddress.toString), - initialTailTransactions ++ initialGenTailTransactions - ) + val workers = finalConfig.sendTo.map { case NodeAddress(node, nodeRestUrl) => + log.info(s"Creating worker: ${node.getHostString}:${node.getPort}") + // new Worker(finalConfig.worker, sender, node, generator, initialTransactions.map(RawBytes.from)) + new Worker( + finalConfig.worker, + Iterator.continually(generator.next()).flatten, + sender, + node, + nodeRestUrl, + () => canContinue, + initialUniTransactions ++ initialGenTransactions, + finalConfig.privateKeyAccounts.map(_.toAddress.toString), + initialTailTransactions ++ initialGenTailTransactions + ) } def close(status: Int): Unit = { diff --git a/node-it/src/test/scala/com/wavesplatform/it/BaseTargetChecker.scala b/node-it/src/test/scala/com/wavesplatform/it/BaseTargetChecker.scala index 945e1d1439f..578ce4b127a 100644 --- a/node-it/src/test/scala/com/wavesplatform/it/BaseTargetChecker.scala +++ b/node-it/src/test/scala/com/wavesplatform/it/BaseTargetChecker.scala @@ -12,7 +12,7 @@ import com.wavesplatform.history.StorageFactory import com.wavesplatform.settings.* import com.wavesplatform.transaction.Asset.Waves import com.wavesplatform.utils.NTP -import net.ceedubs.ficus.Ficus.* +import pureconfig.ConfigSource object BaseTargetChecker { def main(args: Array[String]): Unit = { @@ -41,7 +41,7 @@ object BaseTargetChecker { blockchainUpdater.processBlock(genesisBlock, genesisBlock.header.generationSignature, None) NodeConfigs.Default.map(_.withFallback(sharedConfig)).collect { - case cfg if cfg.as[Boolean]("waves.miner.enable") => + case cfg if ConfigSource.fromConfig(cfg).at("waves.miner.enable").loadOrThrow[Boolean] => val account = KeyPair.fromSeed(cfg.getString("account-seed")).explicitGet() val address = account.toAddress val balance = blockchainUpdater.balance(address, Waves) diff --git a/node-it/src/test/scala/com/wavesplatform/it/Docker.scala b/node-it/src/test/scala/com/wavesplatform/it/Docker.scala index 04f496b7c07..43f8c195215 100644 --- a/node-it/src/test/scala/com/wavesplatform/it/Docker.scala +++ b/node-it/src/test/scala/com/wavesplatform/it/Docker.scala @@ -17,12 +17,12 @@ import com.wavesplatform.it.util.GlobalTimer.instance as timer import com.wavesplatform.settings.* import com.wavesplatform.utils.ScorexLogging import monix.eval.Coeval -import net.ceedubs.ficus.Ficus.* -import net.ceedubs.ficus.readers.ArbitraryTypeReader.* import org.apache.commons.compress.archivers.ArchiveStreamFactory import org.apache.commons.compress.archivers.tar.TarArchiveEntry import org.apache.commons.io.IOUtils import org.asynchttpclient.Dsl.* +import pureconfig.ConfigSource +import pureconfig.generic.auto.* import java.io.{FileOutputStream, IOException} import java.net.{InetAddress, InetSocketAddress, URL} @@ -306,7 +306,7 @@ class Docker( private def getNodeInfo(containerId: String, settings: WavesSettings): NodeInfo = { val restApiPort = settings.restAPISettings.port // assume test nodes always have an open port - val networkPort = settings.networkSettings.bindAddress.get.getPort + val networkPort = settings.networkSettings.derivedBindAddress.get.getPort val containerInfo = inspectContainer(containerId) val wavesIpAddress = containerInfo.networkSettings().networks().get(wavesNetwork.name()).ipAddress() @@ -581,14 +581,19 @@ object Docker { |}""".stripMargin) val genesisConfig = timestampOverrides.withFallback(configTemplate) - val gs = genesisConfig.as[GenesisSettings]("waves.blockchain.custom.genesis") - val features = featuresConfig + val gs = ConfigSource.fromConfig(genesisConfig).at("waves.blockchain.custom.genesis").loadOrThrow[GenesisSettings] + val featuresConfigAdjusted = featuresConfig .map(_.withFallback(configTemplate)) .getOrElse(configTemplate) .resolve() - .getAs[Map[Short, Int]]("waves.blockchain.custom.functionality.pre-activated-features") - val isRideV6Activated = features.exists(_.get(BlockchainFeatures.RideV6.id).contains(0)) - val isTxStateSnapshotActivated = features.exists(_.get(BlockchainFeatures.LightNode.id).contains(0)) + val features = + ConfigSource + .fromConfig(featuresConfigAdjusted) + .at("waves.blockchain.custom.functionality.pre-activated-features") + .loadOrThrow[Map[Short, Int]] + + val isRideV6Activated = features.get(BlockchainFeatures.RideV6.id).contains(0) + val isTxStateSnapshotActivated = features.get(BlockchainFeatures.LightNode.id).contains(0) val genesisSignature = Block.genesis(gs, isRideV6Activated, isTxStateSnapshotActivated).explicitGet().id() @@ -596,7 +601,8 @@ object Docker { } AddressScheme.current = new AddressScheme { - override val chainId: Byte = configTemplate.as[String]("waves.blockchain.custom.address-scheme-character").charAt(0).toByte + override val chainId: Byte = + ConfigSource.fromConfig(configTemplate).at("waves.blockchain.custom.address-scheme-character").loadOrThrow[String].charAt(0).toByte } def apply(owner: Class[?]): Docker = new Docker(tag = owner.getSimpleName) diff --git a/node-it/src/test/scala/com/wavesplatform/it/Node.scala b/node-it/src/test/scala/com/wavesplatform/it/Node.scala index 903f56213ee..27488b168ef 100644 --- a/node-it/src/test/scala/com/wavesplatform/it/Node.scala +++ b/node-it/src/test/scala/com/wavesplatform/it/Node.scala @@ -55,7 +55,7 @@ abstract class Node(val config: Config) extends AutoCloseable { object Node { implicit class NodeExt(val n: Node) extends AnyVal { - def name: String = n.settings.networkSettings.nodeName + def name: String = n.settings.networkSettings.derivedNodeName def publicKeyStr: String = n.publicKey.toString def fee(txTypeId: Byte): Long = FeeValidation.FeeConstants(TransactionType(txTypeId)) * FeeValidation.FeeUnit def blockDelay: FiniteDuration = n.settings.blockchainSettings.genesisSettings.averageBlockDelay diff --git a/node/src/main/scala/com/wavesplatform/Application.scala b/node/src/main/scala/com/wavesplatform/Application.scala index ab767812d59..b3ac210cfd9 100644 --- a/node/src/main/scala/com/wavesplatform/Application.scala +++ b/node/src/main/scala/com/wavesplatform/Application.scala @@ -481,7 +481,7 @@ class Application(val actorSystem: ActorSystem, val settings: WavesSettings, con log.info(s"REST API was bound on ${settings.restAPISettings.bindAddress}:${settings.restAPISettings.port}") } - for (addr <- settings.networkSettings.declaredAddress if settings.networkSettings.uPnPSettings.enable) { + for (addr <- settings.networkSettings.derivedDeclaredAddress if settings.networkSettings.uPnPSettings.enable) { upnp.addPort(addr.getPort) } @@ -501,7 +501,7 @@ class Application(val actorSystem: ActorSystem, val settings: WavesSettings, con log.info("Closing REST API") if (settings.restAPISettings.enable) Try(Await.ready(serverBinding.unbind(), 2.minutes)).failed.map(e => log.error("Failed to unbind REST API port", e)) - for (addr <- settings.networkSettings.declaredAddress if settings.networkSettings.uPnPSettings.enable) upnp.deletePort(addr.getPort) + for (addr <- settings.networkSettings.derivedDeclaredAddress if settings.networkSettings.uPnPSettings.enable) upnp.deletePort(addr.getPort) log.debug("Closing peer database") peerDatabase.close() @@ -604,12 +604,12 @@ object Application extends ScorexLogging { } private[wavesplatform] def loadBlockAt(rdb: RDB, blockchainUpdater: BlockchainUpdaterImpl)( - height: Int + height: Int ): Option[(BlockMeta, Seq[(TxMeta, Transaction)])] = loadBlockInfoAt(rdb, blockchainUpdater)(height) private[wavesplatform] def loadBlockInfoAt(rdb: RDB, blockchainUpdater: BlockchainUpdaterImpl)( - height: Int + height: Int ): Option[(BlockMeta, Seq[(TxMeta, Transaction)])] = loadBlockMetaAt(rdb.db, blockchainUpdater)(height).map { meta => meta -> blockchainUpdater diff --git a/node/src/main/scala/com/wavesplatform/GenesisBlockGenerator.scala b/node/src/main/scala/com/wavesplatform/GenesisBlockGenerator.scala index 51444fddf50..413f937a4f4 100644 --- a/node/src/main/scala/com/wavesplatform/GenesisBlockGenerator.scala +++ b/node/src/main/scala/com/wavesplatform/GenesisBlockGenerator.scala @@ -9,12 +9,12 @@ import com.wavesplatform.consensus.PoSCalculator.{generationSignature, hit} import com.wavesplatform.consensus.{FairPoSCalculator, NxtPoSCalculator} import com.wavesplatform.crypto.* import com.wavesplatform.features.{BlockchainFeature, BlockchainFeatures} -import com.wavesplatform.settings.{FunctionalitySettings, GenesisSettings, GenesisTransactionSettings} +import com.wavesplatform.settings.* import com.wavesplatform.transaction.{GenesisTransaction, TxNonNegativeAmount} import com.wavesplatform.utils.* import com.wavesplatform.wallet.Wallet -import net.ceedubs.ficus.Ficus.* -import net.ceedubs.ficus.readers.ArbitraryTypeReader.* +import pureconfig.ConfigSource +import pureconfig.generic.auto.* import java.io.{File, FileNotFoundException} import java.nio.file.Files @@ -102,8 +102,7 @@ object GenesisBlockGenerator { } def parseSettings(config: Config): Settings = { - import net.ceedubs.ficus.readers.namemappers.implicits.hyphenCase - config.as[Settings]("genesis-generator") + ConfigSource.fromConfig(config).at("genesis-generator").loadOrThrow[Settings] } def createConfig(settings: Settings): String = { diff --git a/node/src/main/scala/com/wavesplatform/account/PrivateKey.scala b/node/src/main/scala/com/wavesplatform/account/PrivateKey.scala index 5634d33cbcc..80a68623ae3 100644 --- a/node/src/main/scala/com/wavesplatform/account/PrivateKey.scala +++ b/node/src/main/scala/com/wavesplatform/account/PrivateKey.scala @@ -3,8 +3,8 @@ package com.wavesplatform.account import com.wavesplatform.common.state.ByteStr import com.wavesplatform.crypto.KeyLength import play.api.libs.json.{Format, Writes} -import supertagged._ -import supertagged.postfix._ +import supertagged.* +import supertagged.postfix.* object PrivateKey extends TaggedType[ByteStr] { def apply(privateKey: ByteStr): PrivateKey = { diff --git a/node/src/main/scala/com/wavesplatform/network/NetworkServer.scala b/node/src/main/scala/com/wavesplatform/network/NetworkServer.scala index fab60197436..f8d9dd26a96 100644 --- a/node/src/main/scala/com/wavesplatform/network/NetworkServer.scala +++ b/node/src/main/scala/com/wavesplatform/network/NetworkServer.scala @@ -39,7 +39,7 @@ object NetworkServer extends ScorexLogging { peerDatabase: PeerDatabase, allChannels: ChannelGroup, peerInfo: ConcurrentHashMap[Channel, PeerInfo], - protocolSpecificPipeline: => Seq[ChannelHandlerAdapter], + protocolSpecificPipeline: => Seq[ChannelHandlerAdapter] ): NetworkServer = { @volatile var shutdownInitiated = false @@ -48,13 +48,13 @@ object NetworkServer extends ScorexLogging { val handshake = Handshake( applicationName, Version.VersionTuple, - networkSettings.nodeName, - networkSettings.nonce, - networkSettings.declaredAddress + networkSettings.derivedNodeName, + networkSettings.derivedNonce, + networkSettings.derivedDeclaredAddress ) val excludedAddresses: Set[InetSocketAddress] = - networkSettings.bindAddress.fold(Set.empty[InetSocketAddress]) { bindAddress => + networkSettings.derivedBindAddress.fold(Set.empty[InetSocketAddress]) { bindAddress => val isLocal = Option(bindAddress.getAddress).exists(_.isAnyLocalAddress) val localAddresses = if (isLocal) { NetworkInterface.getNetworkInterfaces.asScala @@ -62,7 +62,7 @@ object NetworkServer extends ScorexLogging { .toSet } else Set(bindAddress) - localAddresses ++ networkSettings.declaredAddress.toSet + localAddresses ++ networkSettings.derivedDeclaredAddress.toSet } val lengthFieldPrepender = new LengthFieldPrepender(4) @@ -90,7 +90,7 @@ object NetworkServer extends ScorexLogging { ) ++ protocolSpecificPipeline ++ Seq(writeErrorHandler, channelClosedHandler, fatalErrorHandler) - val serverChannel = networkSettings.bindAddress.map { bindAddress => + val serverChannel = networkSettings.derivedBindAddress.map { bindAddress => new ServerBootstrap() .group(bossGroup, workerGroup) .channel(classOf[NioServerSocketChannel]) @@ -203,7 +203,8 @@ object NetworkServer extends ScorexLogging { } def scheduleConnectTask(): Unit = if (!shutdownInitiated) { - val delay = (if (peerConnectionsMap.isEmpty || networkSettings.minConnections.exists(_ > peerConnectionsMap.size())) AverageHandshakePeriod else 5.seconds) + + val delay = (if (peerConnectionsMap.isEmpty || networkSettings.minConnections.exists(_ > peerConnectionsMap.size())) AverageHandshakePeriod + else 5.seconds) + (Random.nextInt(1000) - 500).millis // add some noise so that nodes don't attempt to connect to each other simultaneously workerGroup.schedule(delay) { diff --git a/node/src/main/scala/com/wavesplatform/network/PeerDatabaseImpl.scala b/node/src/main/scala/com/wavesplatform/network/PeerDatabaseImpl.scala index 1330e241fd3..5b69b98ae23 100644 --- a/node/src/main/scala/com/wavesplatform/network/PeerDatabaseImpl.scala +++ b/node/src/main/scala/com/wavesplatform/network/PeerDatabaseImpl.scala @@ -49,7 +49,7 @@ class PeerDatabaseImpl(settings: NetworkSettings, ticker: Ticker = Ticker.system override def addCandidate(socketAddress: InetSocketAddress): Boolean = unverifiedPeers.synchronized { val r = !socketAddress.getAddress.isAnyLocalAddress && - !(socketAddress.getAddress.isLoopbackAddress && settings.bindAddress.exists(_.getPort == socketAddress.getPort)) && + !(socketAddress.getAddress.isLoopbackAddress && settings.derivedBindAddress.exists(_.getPort == socketAddress.getPort)) && Option(peersPersistence.getIfPresent(socketAddress)).isEmpty && !unverifiedPeers.contains(socketAddress) if (r) unverifiedPeers.add(socketAddress) diff --git a/node/src/main/scala/com/wavesplatform/settings/BlockchainSettings.scala b/node/src/main/scala/com/wavesplatform/settings/BlockchainSettings.scala index d2c33fa514f..81e5c2dd655 100644 --- a/node/src/main/scala/com/wavesplatform/settings/BlockchainSettings.scala +++ b/node/src/main/scala/com/wavesplatform/settings/BlockchainSettings.scala @@ -5,9 +5,8 @@ import cats.syntax.traverse.* import com.typesafe.config.Config import com.wavesplatform.account.Address import com.wavesplatform.common.state.ByteStr -import net.ceedubs.ficus.Ficus.* -import net.ceedubs.ficus.readers.ArbitraryTypeReader.* -import net.ceedubs.ficus.readers.ValueReader +import pureconfig.* +import pureconfig.generic.auto.* import scala.concurrent.duration.* @@ -245,35 +244,31 @@ private[settings] object BlockchainType { } object BlockchainSettings { - implicit val valueReader: ValueReader[BlockchainSettings] = - (cfg: Config, path: String) => fromConfig(cfg.getConfig(path)) + def fromRootConfig(config: Config): BlockchainSettings = + ConfigSource.fromConfig(config).at("waves.blockchain").loadOrThrow[BlockchainSettings] - // @deprecated("Use config.as[BlockchainSettings]", "0.17.0") - def fromRootConfig(config: Config): BlockchainSettings = config.as[BlockchainSettings]("waves.blockchain") + implicit val configReader: ConfigReader[BlockchainSettings] = ConfigReader.fromCursor(cur => + for { + objCur <- cur.asObjectCursor + blockchainTypeString <- objCur.atKey("type").flatMap(_.asString).map(_.toUpperCase) + (addressSchemeCharacter, functionalitySettings, genesisSettings, rewardsSettings) <- blockchainTypeString match { + case BlockchainType.STAGENET => Right(('S', FunctionalitySettings.STAGENET, GenesisSettings.STAGENET, RewardsSettings.STAGENET)) + case BlockchainType.TESTNET => Right(('T', FunctionalitySettings.TESTNET, GenesisSettings.TESTNET, RewardsSettings.TESTNET)) + case BlockchainType.MAINNET => Right(('W', FunctionalitySettings.MAINNET, GenesisSettings.MAINNET, RewardsSettings.MAINNET)) + case _ => + // Custom + for { + customObjCur <- objCur.atKey("custom").flatMap(_.asObjectCursor) + networkId <- customObjCur.atKey("address-scheme-character").flatMap(_.asString).map(_.charAt(0)) + functionality <- customObjCur.atKey("functionality").flatMap(ConfigReader[FunctionalitySettings].from) + genesis <- customObjCur.atKey("genesis").flatMap(ConfigReader[GenesisSettings].from) + rewards <- customObjCur.atKey("rewards").flatMap(ConfigReader[RewardsSettings].from) + } yield { + require(functionality.minBlockTime <= genesis.averageBlockDelay, "minBlockTime should be <= averageBlockDelay") + (networkId, functionality, genesis, rewards) + } + } - private[this] def fromConfig(config: Config): BlockchainSettings = { - val blockchainType = config.as[String]("type").toUpperCase - val (addressSchemeCharacter, functionalitySettings, genesisSettings, rewardsSettings) = blockchainType match { - case BlockchainType.STAGENET => - ('S', FunctionalitySettings.STAGENET, GenesisSettings.STAGENET, RewardsSettings.STAGENET) - case BlockchainType.TESTNET => - ('T', FunctionalitySettings.TESTNET, GenesisSettings.TESTNET, RewardsSettings.TESTNET) - case BlockchainType.MAINNET => - ('W', FunctionalitySettings.MAINNET, GenesisSettings.MAINNET, RewardsSettings.MAINNET) - case _ => // Custom - val networkId = config.as[String](s"custom.address-scheme-character").charAt(0) - val functionality = config.as[FunctionalitySettings](s"custom.functionality") - val genesis = config.as[GenesisSettings](s"custom.genesis") - val rewards = config.as[RewardsSettings](s"custom.rewards") - require(functionality.minBlockTime <= genesis.averageBlockDelay, "minBlockTime should be <= averageBlockDelay") - (networkId, functionality, genesis, rewards) - } - - BlockchainSettings( - addressSchemeCharacter = addressSchemeCharacter, - functionalitySettings = functionalitySettings, - genesisSettings = genesisSettings, - rewardsSettings = rewardsSettings - ) - } + } yield BlockchainSettings(addressSchemeCharacter, functionalitySettings, genesisSettings, rewardsSettings) + ) } diff --git a/node/src/main/scala/com/wavesplatform/settings/CustomValueReaders.scala b/node/src/main/scala/com/wavesplatform/settings/CustomValueReaders.scala deleted file mode 100644 index 19987e5d386..00000000000 --- a/node/src/main/scala/com/wavesplatform/settings/CustomValueReaders.scala +++ /dev/null @@ -1,8 +0,0 @@ -package com.wavesplatform.settings - -import net.ceedubs.ficus.readers.ValueReader - -trait CustomValueReaders { - implicit val networkSettingsValueReader: ValueReader[NetworkSettings] = NetworkSettings.valueReader - implicit val blockchainSettingsValueReader: ValueReader[BlockchainSettings] = BlockchainSettings.valueReader -} diff --git a/node/src/main/scala/com/wavesplatform/settings/DBSettings.scala b/node/src/main/scala/com/wavesplatform/settings/DBSettings.scala index 906e085557c..adf14e49b1b 100644 --- a/node/src/main/scala/com/wavesplatform/settings/DBSettings.scala +++ b/node/src/main/scala/com/wavesplatform/settings/DBSettings.scala @@ -9,5 +9,5 @@ case class DBSettings( maxCacheSize: Int, maxRollbackDepth: Int, cleanupInterval: Option[Int] = None, - rocksdb: RocksDBSettings, + rocksdb: RocksDBSettings ) diff --git a/node/src/main/scala/com/wavesplatform/settings/NetworkSettings.scala b/node/src/main/scala/com/wavesplatform/settings/NetworkSettings.scala index a7fd766a0d0..db7ffd670c9 100644 --- a/node/src/main/scala/com/wavesplatform/settings/NetworkSettings.scala +++ b/node/src/main/scala/com/wavesplatform/settings/NetworkSettings.scala @@ -1,12 +1,7 @@ package com.wavesplatform.settings -import com.typesafe.config.Config import com.wavesplatform.network.TrafficLogger import com.wavesplatform.utils.* -import net.ceedubs.ficus.Ficus.* -import net.ceedubs.ficus.readers.ArbitraryTypeReader.* -import net.ceedubs.ficus.readers.ValueReader - import java.io.File import java.net.{InetSocketAddress, URI} import scala.concurrent.duration.FiniteDuration @@ -16,17 +11,18 @@ case class UPnPSettings(enable: Boolean, gatewayTimeout: FiniteDuration, discove case class NetworkSettings( file: Option[File], - bindAddress: Option[InetSocketAddress], - declaredAddress: Option[InetSocketAddress], - nodeName: String, - nonce: Long, + bindAddress: Option[String], + port: Option[Int], + declaredAddress: Option[String], + nodeName: Option[String], + nonce: Option[Long], knownPeers: Seq[String], peersDataResidenceTime: FiniteDuration, blackListResidenceTime: FiniteDuration, breakIdleConnectionsTimeout: FiniteDuration, maxInboundConnections: Int, maxOutboundConnections: Int, - maxConnectionsPerHost: Int, + maxSingleHostConnections: Int, minConnections: Option[Int], connectionTimeout: FiniteDuration, maxUnverifiedPeers: Int, @@ -36,76 +32,38 @@ case class NetworkSettings( handshakeTimeout: FiniteDuration, suspensionResidenceTime: FiniteDuration, receivedTxsCacheTimeout: FiniteDuration, - uPnPSettings: UPnPSettings, + upnp: UPnPSettings, trafficLogger: TrafficLogger.Settings -) +) { -object NetworkSettings { - private val MaxNodeNameBytesLength = 127 + val derivedDeclaredAddress: Option[InetSocketAddress] = declaredAddress.map { address => + val uri = new URI(s"my://$address") + new InetSocketAddress(uri.getHost, uri.getPort) + } - implicit val valueReader: ValueReader[NetworkSettings] = - (cfg: Config, path: String) => fromConfig(cfg.getConfig(path)) + val derivedNonce: Long = nonce.getOrElse(NetworkSettings.randomNonce) - private[this] def fromConfig(config: Config): NetworkSettings = { - val file = config.getAs[File]("file") - val bindAddress = config.getAs[String]("bind-address").map(addr => new InetSocketAddress(addr, config.as[Int]("port"))) - val nonce = config.getOrElse("nonce", randomNonce) - val nodeName = config.getOrElse("node-name", s"Node-$nonce") - require(nodeName.utf8Bytes.length <= MaxNodeNameBytesLength, s"Node name should have length less than $MaxNodeNameBytesLength bytes") - val declaredAddress = config.getAs[String]("declared-address").map { address => - val uri = new URI(s"my://$address") - new InetSocketAddress(uri.getHost, uri.getPort) - } + val derivedNodeName: String = nodeName.getOrElse(s"Node-$derivedNonce") + require( + derivedNodeName.utf8Bytes.length <= NetworkSettings.MaxNodeNameBytesLength, + s"Node name should have length less than ${NetworkSettings.MaxNodeNameBytesLength} bytes" + ) - val knownPeers = config.as[Seq[String]]("known-peers") - val peersDataResidenceTime = config.as[FiniteDuration]("peers-data-residence-time") - val blackListResidenceTime = config.as[FiniteDuration]("black-list-residence-time") - val breakIdleConnectionsTimeout = config.as[FiniteDuration]("break-idle-connections-timeout") - val maxInboundConnections = config.as[Int]("max-inbound-connections") - val maxOutboundConnections = config.as[Int]("max-outbound-connections") - val maxConnectionsFromSingleHost = config.as[Int]("max-single-host-connections") - val minConnections = config.getAs[Int]("min-connections") - val connectionTimeout = config.as[FiniteDuration]("connection-timeout") - val maxUnverifiedPeers = config.as[Int]("max-unverified-peers") - val enablePeersExchange = config.as[Boolean]("enable-peers-exchange") - val enableBlacklisting = config.as[Boolean]("enable-blacklisting") - val peersBroadcastInterval = config.as[FiniteDuration]("peers-broadcast-interval") - val handshakeTimeout = config.as[FiniteDuration]("handshake-timeout") - val suspensionResidenceTime = config.as[FiniteDuration]("suspension-residence-time") - val receivedTxsCacheTimeout = config.as[FiniteDuration]("received-txs-cache-timeout") - val uPnPSettings = config.as[UPnPSettings]("upnp") - val trafficLogger = config.as[TrafficLogger.Settings]("traffic-logger") + val derivedBindAddress: Option[InetSocketAddress] = for { + addr <- bindAddress + p <- port + } yield new InetSocketAddress(addr, p) - NetworkSettings( - file, - bindAddress, - declaredAddress, - nodeName, - nonce, - knownPeers, - peersDataResidenceTime, - blackListResidenceTime, - breakIdleConnectionsTimeout, - maxInboundConnections, - maxOutboundConnections, - maxConnectionsFromSingleHost, - minConnections, - connectionTimeout, - maxUnverifiedPeers, - enablePeersExchange, - enableBlacklisting, - peersBroadcastInterval, - handshakeTimeout, - suspensionResidenceTime, - receivedTxsCacheTimeout, - uPnPSettings, - trafficLogger - ) - } + val maxConnectionsPerHost: Int = maxSingleHostConnections - private def randomNonce: Long = { - val base = 1000 + val uPnPSettings: UPnPSettings = upnp +} + +object NetworkSettings { + val MaxNodeNameBytesLength = 127 + def randomNonce: Long = { + val base = 1000 (Random.nextInt(base) + base) * Random.nextInt(base) + Random.nextInt(base) } } diff --git a/node/src/main/scala/com/wavesplatform/settings/WavesSettings.scala b/node/src/main/scala/com/wavesplatform/settings/WavesSettings.scala index ff2cbdb55e8..375adb61c62 100644 --- a/node/src/main/scala/com/wavesplatform/settings/WavesSettings.scala +++ b/node/src/main/scala/com/wavesplatform/settings/WavesSettings.scala @@ -2,10 +2,9 @@ package com.wavesplatform.settings import com.typesafe.config.{Config, ConfigFactory} import com.wavesplatform.metrics.Metrics -import net.ceedubs.ficus.Ficus.* -import net.ceedubs.ficus.readers.ArbitraryTypeReader.* - import scala.concurrent.duration.FiniteDuration +import pureconfig.* +import pureconfig.generic.auto.* case class WavesSettings( directory: String, @@ -28,27 +27,28 @@ case class WavesSettings( config: Config ) -object WavesSettings extends CustomValueReaders { +object WavesSettings { def fromRootConfig(rootConfig: Config): WavesSettings = { - val waves = rootConfig.getConfig("waves") + val waves = rootConfig.getConfig("waves") + val wavesConfigSource = ConfigSource.fromConfig(waves) - val directory = waves.as[String]("directory") - val enableLightMode = waves.as[Boolean]("enable-light-mode") - val ntpServer = waves.as[String]("ntp-server") - val maxTxErrorLogSize = waves.as[Int]("max-tx-error-log-size") - val dbSettings = waves.as[DBSettings]("db") - val extensions = waves.as[Seq[String]]("extensions") - val extensionsShutdownTimeout = waves.as[FiniteDuration]("extensions-shutdown-timeout") - val networkSettings = waves.as[NetworkSettings]("network") - val walletSettings = waves.as[WalletSettings]("wallet") - val blockchainSettings = waves.as[BlockchainSettings]("blockchain") - val minerSettings = waves.as[MinerSettings]("miner") - val restAPISettings = waves.as[RestAPISettings]("rest-api") - val synchronizationSettings = waves.as[SynchronizationSettings]("synchronization") - val utxSettings = waves.as[UtxSettings]("utx") - val featuresSettings = waves.as[FeaturesSettings]("features") - val rewardsSettings = waves.as[RewardsVotingSettings]("rewards") - val metrics = rootConfig.as[Metrics.Settings]("metrics") // TODO: Move to waves section + val directory = wavesConfigSource.at("directory").loadOrThrow[String] + val ntpServer = wavesConfigSource.at("ntp-server").loadOrThrow[String] + val maxTxErrorLogSize = wavesConfigSource.at("max-tx-error-log-size").loadOrThrow[Int] + val dbSettings = wavesConfigSource.at("db").loadOrThrow[DBSettings] + val extensions = wavesConfigSource.at("extensions").loadOrThrow[Seq[String]] + val extensionsShutdownTimeout = wavesConfigSource.at("extensions-shutdown-timeout").loadOrThrow[FiniteDuration] + val networkSettings = wavesConfigSource.at("network").loadOrThrow[NetworkSettings] + val walletSettings = wavesConfigSource.at("wallet").loadOrThrow[WalletSettings] + val blockchainSettings = wavesConfigSource.at("blockchain").loadOrThrow[BlockchainSettings] + val minerSettings = wavesConfigSource.at("miner").loadOrThrow[MinerSettings] + val restAPISettings = wavesConfigSource.at("rest-api").loadOrThrow[RestAPISettings] + val synchronizationSettings = wavesConfigSource.at("synchronization").loadOrThrow[SynchronizationSettings] + val utxSettings = wavesConfigSource.at("utx").loadOrThrow[UtxSettings] + val featuresSettings = wavesConfigSource.at("features").loadOrThrow[FeaturesSettings] + val rewardsSettings = wavesConfigSource.at("rewards").loadOrThrow[RewardsVotingSettings] + val metrics = ConfigSource.fromConfig(rootConfig).at("metrics").loadOrThrow[Metrics.Settings] // TODO: Move to waves section + val enableLightMode = wavesConfigSource.at("enable-light-mode").loadOrThrow[Boolean] WavesSettings( directory, diff --git a/node/src/main/scala/com/wavesplatform/settings/package.scala b/node/src/main/scala/com/wavesplatform/settings/package.scala index b21278620e8..9abd9b2b201 100644 --- a/node/src/main/scala/com/wavesplatform/settings/package.scala +++ b/node/src/main/scala/com/wavesplatform/settings/package.scala @@ -1,65 +1,35 @@ package com.wavesplatform -import java.io.File -import java.net.{InetSocketAddress, URI} -import cats.data.NonEmptyList -import com.typesafe.config.{Config, ConfigException, ConfigFactory, ConfigValueType} +import com.typesafe.config.{Config, ConfigFactory} import com.wavesplatform.account.PrivateKey import com.wavesplatform.common.state.ByteStr -import net.ceedubs.ficus.Ficus.traversableReader -import net.ceedubs.ficus.readers.namemappers.HyphenNameMapper -import net.ceedubs.ficus.readers.{NameMapper, ValueReader} +import pureconfig.ConfigReader +import pureconfig.ConvertHelpers.catchReadError +import pureconfig.configurable.genericMapReader +import pureconfig.error.CannotConvert import supertagged.TaggedType -import scala.jdk.CollectionConverters.* import scala.util.Try package object settings { - implicit val hyphenCase: NameMapper = HyphenNameMapper - - implicit val fileReader: ValueReader[File] = (cfg, path) => new File(cfg.getString(path)) - implicit val byteStrReader: ValueReader[ByteStr] = (cfg, path) => ByteStr.decodeBase58(cfg.getString(path)).get - implicit val shortValueReader: ValueReader[Short] = (cfg, path) => cfg.getLong(path).toShort - implicit val preactivatedFeaturesReader: ValueReader[Map[Short, Int]] = (config: Config, path: String) => - if (config.getIsNull(path)) Map.empty - else { - config.getValue(path).valueType() match { - case ConfigValueType.OBJECT => - val paf = config.getConfig(path) - (for { - featureId <- paf.root().keySet().asScala - } yield featureId.toShort -> paf.getInt(featureId)).toMap - case ConfigValueType.STRING if config.getString(path).isEmpty => - Map.empty - case other => - throw new ConfigException.WrongType(config.getValue(path).origin(), path, ConfigValueType.OBJECT.name(), other.name()) - } - } - - implicit val byteReader: ValueReader[Byte] = { (cfg: Config, path: String) => - val x = cfg.getInt(path) - if (x.isValidByte) x.toByte - else throw new ConfigException.WrongType(cfg.origin(), s"$path has an invalid value: '$x' expected to be a byte") - } - - implicit val inetSocketAddressReader: ValueReader[InetSocketAddress] = { (config: Config, path: String) => - val uri = new URI(s"my://${config.getString(path)}") - new InetSocketAddress(uri.getHost, uri.getPort) - } + implicit val byteStrReader: ConfigReader[ByteStr] = + ConfigReader.fromString(str => ByteStr.decodeBase58(str).toEither.left.map(e => CannotConvert(str, "ByteStr", e.getMessage))) + implicit val preactivatedFeaturesReader: ConfigReader[Map[Short, Int]] = genericMapReader(catchReadError(_.toShort)) - implicit val privateKeyReader: ValueReader[PrivateKey] = byteStrReader.map(PrivateKey(_)) - - implicit def nonEmptyListReader[T: ValueReader]: ValueReader[NonEmptyList[T]] = implicitly[ValueReader[List[T]]].map { - case Nil => throw new IllegalArgumentException("Expected at least one element") - case x :: xs => NonEmptyList(x, xs) - } + implicit val privateKeyReader: ConfigReader[PrivateKey] = ConfigReader[ByteStr].map(PrivateKey(_)) object SizeInBytes extends TaggedType[Long] type SizeInBytes = SizeInBytes.Type - implicit val sizeInBytesReader: ValueReader[SizeInBytes] = {(cfg: Config, path: String) => - SizeInBytes(cfg.getBytes(path).toLong) - } + implicit val sizeInBytesConfigReader: ConfigReader[SizeInBytes] = ConfigReader.fromCursor(cur => + for { + configValue <- cur.asConfigValue + } yield { + val config = ConfigFactory.empty().withValue("stubKey", configValue) + val bytes: Long = config.getBytes("stubKey") + SizeInBytes(bytes) + } + ) def loadConfig(userConfig: Config): Config = { loadConfig(Some(userConfig)) diff --git a/node/src/main/scala/com/wavesplatform/settings/utils/ConfigSettingsValidator.scala b/node/src/main/scala/com/wavesplatform/settings/utils/ConfigSettingsValidator.scala deleted file mode 100644 index 33a3de7608b..00000000000 --- a/node/src/main/scala/com/wavesplatform/settings/utils/ConfigSettingsValidator.scala +++ /dev/null @@ -1,103 +0,0 @@ -package com.wavesplatform.settings.utils - -import cats.data.{NonEmptyList, Validated, ValidatedNel} -import cats.instances.list._ -import cats.syntax.foldable._ -import cats.syntax.traverse._ -import com.typesafe.config.{Config, ConfigException} -import com.wavesplatform.transaction.assets.exchange.AssetPair -import net.ceedubs.ficus.Ficus._ -import net.ceedubs.ficus.readers.ValueReader - -import scala.jdk.CollectionConverters._ -import scala.util.Try - -object ConfigSettingsValidator { - - type ErrorsListOr[A] = ValidatedNel[String, A] - - def apply(config: Config): ConfigSettingsValidator = new ConfigSettingsValidator(config) - - implicit class ErrorListOrOps[A](validatedValue: ErrorsListOr[A]) { - def getValueOrThrowErrors: A = validatedValue valueOr (errorsAcc => throw new Exception(errorsAcc.mkString_(", "))) - } - - object AdhocValidation { - def validateAssetPairKey(key: String): Validated[String, AssetPair] = - Validated.fromTry(AssetPair.fromString(key)) leftMap (_ => s"Can't parse asset pair '$key'") - } -} - -class ConfigSettingsValidator(config: Config) { - - import ConfigSettingsValidator.ErrorsListOr - - private def createError[T](settingName: String, errorMsg: String, showError: Boolean = true, showValue: Boolean = true): NonEmptyList[String] = { - - lazy val value = config.getValue(settingName).unwrapped - - lazy val msg = (showValue, showError) match { - case (true, true) => s"$value ($errorMsg)" - case (true, false) => s"$value" - case (false, true) => s"$errorMsg" - case _ => "" - } - - NonEmptyList.one(s"Invalid setting $settingName value: $msg") - } - - def validate[T: ValueReader](settingName: String, showError: Boolean = false): ErrorsListOr[T] = { - Validated fromTry Try(config.as[T](settingName)) leftMap (ex => createError(settingName, ex.getMessage, showError)) - } - - def validateByPredicate[T: ValueReader](settingName: String)(predicate: T => Boolean, errorMsg: String): ErrorsListOr[T] = { - validate[T](settingName, showError = true).ensure(createError(settingName, errorMsg))(predicate) - } - - def validatePercent(settingName: String): ErrorsListOr[Double] = { - validateByPredicate[Double](settingName)(p => 0 < p && p <= 100, "required 0 < percent <= 100") - } - - def validateList[T: ValueReader](settingName: String): ErrorsListOr[List[T]] = { - config - .getList(settingName) - .asScala - .toList - .zipWithIndex - .traverse { - case (cfg, index) => - val elemPath = s"$settingName.$index" - Validated fromTry Try(cfg.atPath(elemPath).as[T](elemPath)) leftMap (ex => List(ex.getMessage)) - } - .leftMap(errorsInList => createError(settingName, errorsInList.mkString(", "), showValue = false)) - } - - def validateMap[K, V: ValueReader](settingName: String)(keyValidator: String => Validated[String, K]): ErrorsListOr[Map[K, V]] = { - config - .getConfig(settingName) - .root() - .entrySet() - .asScala - .toList - .traverse { entry => - val elemPath = s"$settingName.${entry.getKey}" - val k = keyValidator(entry.getKey).leftMap(List(_)) - val v = Validated fromTry Try(entry.getValue.atPath(elemPath).as[V](elemPath)) leftMap (ex => List(ex.getMessage)) - k.product(v) - } - .map(_.toMap) - .leftMap(errorsInList => createError(settingName, errorsInList.mkString(", "), showValue = false)) - } - - def validateWithDefault[T: ValueReader](settingName: String, defaultValue: T, showError: Boolean = false): ErrorsListOr[T] = { - Validated - .fromTry(Try(config.as[T](settingName)).recover { case _: ConfigException.Missing => defaultValue }) - .leftMap(ex => createError(settingName, ex.getMessage, showError)) - } - - def validateByPredicateWithDefault[T: ValueReader]( - settingName: String - )(predicate: T => Boolean, errorMsg: String, defaultValue: T): ErrorsListOr[T] = { - validateWithDefault[T](settingName, defaultValue, showError = true).ensure(createError(settingName, errorMsg))(predicate) - } -} diff --git a/node/src/main/scala/com/wavesplatform/transaction/Asset.scala b/node/src/main/scala/com/wavesplatform/transaction/Asset.scala index 4b2d8dbe320..d33b80456a9 100644 --- a/node/src/main/scala/com/wavesplatform/transaction/Asset.scala +++ b/node/src/main/scala/com/wavesplatform/transaction/Asset.scala @@ -4,8 +4,9 @@ import com.google.common.collect.Interners import com.wavesplatform.common.state.ByteStr import com.wavesplatform.common.utils.Base58 import com.wavesplatform.transaction.assets.exchange.AssetPair -import net.ceedubs.ficus.readers.ValueReader import play.api.libs.json.* +import pureconfig.ConfigReader +import pureconfig.error.CannotConvert import scala.util.Success @@ -50,9 +51,8 @@ object Asset { implicit val assetIdJsonFormat: Format[Asset] = Format(assetIdReads, assetIdWrites) } - implicit val assetReader: ValueReader[Asset] = { (cfg, path) => - AssetPair.extractAssetId(cfg getString path).fold(ex => throw new Exception(ex.getMessage), identity) - } + implicit val assetConfigReader: ConfigReader[Asset] = + ConfigReader[String].emap(s => AssetPair.extractAssetId(s).fold(ex => Left(CannotConvert(s, "Asset", ex.getMessage)), Right(_))) def fromString(maybeStr: Option[String]): Asset = { maybeStr.map(x => IssuedAsset(ByteStr.decodeBase58(x).get)).getOrElse(Waves) diff --git a/node/src/main/scala/com/wavesplatform/transaction/assets/exchange/AssetPair.scala b/node/src/main/scala/com/wavesplatform/transaction/assets/exchange/AssetPair.scala index 27ec45bd677..99c8c4d9b18 100644 --- a/node/src/main/scala/com/wavesplatform/transaction/assets/exchange/AssetPair.scala +++ b/node/src/main/scala/com/wavesplatform/transaction/assets/exchange/AssetPair.scala @@ -6,7 +6,6 @@ import com.wavesplatform.serialization.Deser import com.wavesplatform.transaction.* import com.wavesplatform.transaction.Asset.{IssuedAsset, Waves, WavesName} import com.wavesplatform.transaction.assets.exchange.Validation.booleanOperators -import net.ceedubs.ficus.readers.ValueReader import play.api.libs.json.{JsObject, Json} import scala.util.{Failure, Success, Try} @@ -66,6 +65,4 @@ object AssetPair { case Array(amtAssetStr, prcAssetStr) => AssetPair.createAssetPair(amtAssetStr, prcAssetStr) case xs => Failure(new Exception(s"$s (incorrect assets count, expected 2 but got ${xs.size}: ${xs.mkString(", ")})")) } - - implicit val assetPairReader: ValueReader[AssetPair] = (cfg, path) => fromString(cfg.getString(path)).get } diff --git a/node/src/main/scala/com/wavesplatform/utils/generator/BlockchainGeneratorApp.scala b/node/src/main/scala/com/wavesplatform/utils/generator/BlockchainGeneratorApp.scala index fe592d1246f..9c6a2df3819 100644 --- a/node/src/main/scala/com/wavesplatform/utils/generator/BlockchainGeneratorApp.scala +++ b/node/src/main/scala/com/wavesplatform/utils/generator/BlockchainGeneratorApp.scala @@ -2,7 +2,6 @@ package com.wavesplatform.utils.generator import java.io.{File, FileOutputStream, PrintWriter} import java.util.concurrent.TimeUnit - import cats.implicits.* import com.typesafe.config.{ConfigFactory, ConfigParseOptions} import com.wavesplatform.{GenesisBlockGenerator, Version} @@ -22,8 +21,8 @@ import com.wavesplatform.utx.UtxPoolImpl import com.wavesplatform.wallet.Wallet import io.netty.channel.group.DefaultChannelGroup import monix.reactive.subjects.ConcurrentSubject -import net.ceedubs.ficus.Ficus.* -import net.ceedubs.ficus.readers.ArbitraryTypeReader.* +import pureconfig.ConfigSource +import pureconfig.generic.auto.* import play.api.libs.json.Json import scopt.OParser @@ -96,7 +95,11 @@ object BlockchainGeneratorApp extends ScorexLogging { val config = readConfFile(options.genesisConfigFile) val genSettings = GenesisBlockGenerator.parseSettings(config) - val genesis = ConfigFactory.parseString(GenesisBlockGenerator.createConfig(genSettings)).as[GenesisSettings]("genesis") + val genesis = + ConfigSource + .fromConfig(ConfigFactory.parseString(GenesisBlockGenerator.createConfig(genSettings))) + .at("genesis") + .loadOrThrow[GenesisSettings] log.info(s"Initial base target is ${genesis.initialBaseTarget}") diff --git a/node/src/main/scala/com/wavesplatform/utils/generator/MinerChallengeSimulator.scala b/node/src/main/scala/com/wavesplatform/utils/generator/MinerChallengeSimulator.scala index aede189071c..d59f2355883 100644 --- a/node/src/main/scala/com/wavesplatform/utils/generator/MinerChallengeSimulator.scala +++ b/node/src/main/scala/com/wavesplatform/utils/generator/MinerChallengeSimulator.scala @@ -25,8 +25,8 @@ import io.netty.channel.group.DefaultChannelGroup import monix.eval.Task import monix.execution.schedulers.SchedulerService import monix.reactive.subjects.ConcurrentSubject -import net.ceedubs.ficus.Ficus.* -import net.ceedubs.ficus.readers.ArbitraryTypeReader.* +import pureconfig.ConfigSource +import pureconfig.generic.auto.* import org.apache.commons.io.FileUtils import java.io.{File, FileNotFoundException} @@ -56,7 +56,11 @@ object MinerChallengeSimulator { val config = readConfFile(genesisConfFile) val genSettings = GenesisBlockGenerator.parseSettings(config) - val genesis = ConfigFactory.parseString(GenesisBlockGenerator.createConfig(genSettings)).as[GenesisSettings]("genesis") + val genesis = + ConfigSource + .fromConfig(ConfigFactory.parseString(GenesisBlockGenerator.createConfig(genSettings))) + .at("genesis") + .loadOrThrow[GenesisSettings] val blockchainSettings = BlockchainSettings( genSettings.chainId.toChar, diff --git a/node/tests/src/test/scala/com/wavesplatform/http/RestAPISettingsHelper.scala b/node/tests/src/test/scala/com/wavesplatform/http/RestAPISettingsHelper.scala index 5e994ebd51f..a8e7023b064 100644 --- a/node/tests/src/test/scala/com/wavesplatform/http/RestAPISettingsHelper.scala +++ b/node/tests/src/test/scala/com/wavesplatform/http/RestAPISettingsHelper.scala @@ -4,9 +4,9 @@ import com.typesafe.config.ConfigFactory import com.wavesplatform.api.http.`X-Api-Key` import com.wavesplatform.common.utils.Base58 import com.wavesplatform.crypto -import com.wavesplatform.settings._ -import net.ceedubs.ficus.Ficus._ -import net.ceedubs.ficus.readers.ArbitraryTypeReader._ +import com.wavesplatform.settings.* +import pureconfig.ConfigSource +import pureconfig.generic.auto.* trait RestAPISettingsHelper { private val apiKey: String = "test_api_key" @@ -20,7 +20,7 @@ trait RestAPISettingsHelper { lazy val restAPISettings = { val keyHash = Base58.encode(crypto.secureHash(apiKey.getBytes("UTF-8"))) - ConfigFactory + val config = ConfigFactory .parseString( s"""waves.rest-api { | api-key-hash = $keyHash @@ -32,6 +32,6 @@ trait RestAPISettingsHelper { """.stripMargin ) .withFallback(ConfigFactory.load()) - .as[RestAPISettings]("waves.rest-api") + ConfigSource.fromConfig(config).at("waves.rest-api").loadOrThrow[RestAPISettings] } } diff --git a/node/tests/src/test/scala/com/wavesplatform/network/BlacklistParallelSpecification.scala b/node/tests/src/test/scala/com/wavesplatform/network/BlacklistParallelSpecification.scala index 4c86f05d1d6..41ab01726b8 100644 --- a/node/tests/src/test/scala/com/wavesplatform/network/BlacklistParallelSpecification.scala +++ b/node/tests/src/test/scala/com/wavesplatform/network/BlacklistParallelSpecification.scala @@ -3,8 +3,9 @@ package com.wavesplatform.network import com.typesafe.config.ConfigFactory import com.wavesplatform.settings.{NetworkSettings, loadConfig} import com.wavesplatform.test.FeatureSpec -import net.ceedubs.ficus.Ficus.* import org.scalatest.{GivenWhenThen, ParallelTestExecution} +import pureconfig.ConfigSource +import pureconfig.generic.auto.* import java.net.{InetAddress, InetSocketAddress} @@ -16,7 +17,7 @@ class BlacklistParallelSpecification extends FeatureSpec with GivenWhenThen with | black-list-residence-time: 1s |}""".stripMargin)) - private val networkSettings = config.as[NetworkSettings]("waves.network") + private val networkSettings = ConfigSource.fromConfig(config).at("waves.network").loadOrThrow[NetworkSettings] info("As a Peer") info("I want to blacklist other peers for certain time") diff --git a/node/tests/src/test/scala/com/wavesplatform/network/BlacklistSpecification.scala b/node/tests/src/test/scala/com/wavesplatform/network/BlacklistSpecification.scala index 1aaf9696a93..5b61ba6c92b 100644 --- a/node/tests/src/test/scala/com/wavesplatform/network/BlacklistSpecification.scala +++ b/node/tests/src/test/scala/com/wavesplatform/network/BlacklistSpecification.scala @@ -4,8 +4,9 @@ import com.google.common.base.Ticker import com.typesafe.config.ConfigFactory import com.wavesplatform.settings.NetworkSettings import com.wavesplatform.test.FeatureSpec -import net.ceedubs.ficus.Ficus.* import org.scalatest.GivenWhenThen +import pureconfig.ConfigSource +import pureconfig.generic.auto.* import java.net.{InetAddress, InetSocketAddress} @@ -19,7 +20,7 @@ class BlacklistSpecification extends FeatureSpec with GivenWhenThen { .withFallback(ConfigFactory.load()) .resolve() - private val networkSettings = config.as[NetworkSettings]("waves.network") + private val networkSettings = ConfigSource.fromConfig(config).at("waves.network").loadOrThrow[NetworkSettings] private var timestamp = 0L info("As a Peer") @@ -29,9 +30,12 @@ class BlacklistSpecification extends FeatureSpec with GivenWhenThen { Feature("Blacklist") { Scenario("Peer blacklist another peer") { Given("Peer database is empty") - val peerDatabase = new PeerDatabaseImpl(networkSettings, new Ticker { - override def read(): Long = timestamp - }) + val peerDatabase = new PeerDatabaseImpl( + networkSettings, + new Ticker { + override def read(): Long = timestamp + } + ) def isBlacklisted(address: InetSocketAddress) = peerDatabase.isBlacklisted(address.getAddress) diff --git a/node/tests/src/test/scala/com/wavesplatform/network/peer/PeerDatabaseImplSpecification.scala b/node/tests/src/test/scala/com/wavesplatform/network/peer/PeerDatabaseImplSpecification.scala index d6cbe832e23..22665b63d75 100644 --- a/node/tests/src/test/scala/com/wavesplatform/network/peer/PeerDatabaseImplSpecification.scala +++ b/node/tests/src/test/scala/com/wavesplatform/network/peer/PeerDatabaseImplSpecification.scala @@ -5,7 +5,8 @@ import com.typesafe.config.ConfigFactory import com.wavesplatform.network.{PeerDatabase, PeerDatabaseImpl} import com.wavesplatform.settings.NetworkSettings import com.wavesplatform.test.FreeSpec -import net.ceedubs.ficus.Ficus.* +import pureconfig.ConfigSource +import pureconfig.generic.auto.* import java.net.InetSocketAddress import scala.concurrent.duration.* @@ -25,7 +26,7 @@ class PeerDatabaseImplSpecification extends FreeSpec { |}""".stripMargin) .withFallback(ConfigFactory.load()) .resolve() - private val settings1 = config1.as[NetworkSettings]("waves.network") + private val settings1 = ConfigSource.fromConfig(config1).at("waves.network").loadOrThrow[NetworkSettings] private val config2 = ConfigFactory .parseString("""waves.network { @@ -35,7 +36,7 @@ class PeerDatabaseImplSpecification extends FreeSpec { |}""".stripMargin) .withFallback(ConfigFactory.load()) .resolve() - private val settings2 = config2.as[NetworkSettings]("waves.network") + private val settings2 = ConfigSource.fromConfig(config2).at("waves.network").loadOrThrow[NetworkSettings] private var ts = 0L private def sleepLong(): Unit = { ts += 2200.millis.toNanos } @@ -136,7 +137,7 @@ class PeerDatabaseImplSpecification extends FreeSpec { |}""".stripMargin) .withFallback(ConfigFactory.load()) .resolve() - val settings = config.as[NetworkSettings]("waves.network") + val settings = ConfigSource.fromConfig(config).at("waves.network").loadOrThrow[NetworkSettings] val database = new PeerDatabaseImpl(settings) database.blacklist(address1.getAddress, "I don't like it") diff --git a/node/tests/src/test/scala/com/wavesplatform/settings/BlockchainSettingsSpecification.scala b/node/tests/src/test/scala/com/wavesplatform/settings/BlockchainSettingsSpecification.scala index 2aa32030826..c00415bdab2 100644 --- a/node/tests/src/test/scala/com/wavesplatform/settings/BlockchainSettingsSpecification.scala +++ b/node/tests/src/test/scala/com/wavesplatform/settings/BlockchainSettingsSpecification.scala @@ -4,7 +4,7 @@ import com.typesafe.config.ConfigFactory import com.wavesplatform.common.state.ByteStr import com.wavesplatform.test.FlatSpec -import scala.concurrent.duration._ +import scala.concurrent.duration.* class BlockchainSettingsSpecification extends FlatSpec { "BlockchainSettings" should "read custom values" in { diff --git a/node/tests/src/test/scala/com/wavesplatform/settings/DbSettingsSpecification.scala b/node/tests/src/test/scala/com/wavesplatform/settings/DbSettingsSpecification.scala new file mode 100644 index 00000000000..7e47f9ee363 --- /dev/null +++ b/node/tests/src/test/scala/com/wavesplatform/settings/DbSettingsSpecification.scala @@ -0,0 +1,75 @@ +package com.wavesplatform.settings + +import com.typesafe.config.ConfigFactory +import com.wavesplatform.test.FlatSpec +import pureconfig.ConfigSource +import pureconfig.generic.auto.* +import com.typesafe.config.ConfigException.BadValue + +class DbSettingsSpecification extends FlatSpec { + "SizeInBytes" should "should successfully read bytes values" in { + val config = loadConfig(ConfigFactory.parseString("size-in-bytes-value = 512M")) + val actualValue = ConfigSource.fromConfig(config).at("size-in-bytes-value").loadOrThrow[SizeInBytes] + val expectedValue = SizeInBytes(512L * 1024 * 1024) + actualValue should be(expectedValue) + } + + "SizeInBytes" should "should fail on invalid values" in { + val config = loadConfig(ConfigFactory.parseString("size-in-bytes-value = 512X")) + assertThrows[BadValue] { + ConfigSource.fromConfig(config).at("size-in-bytes-value").loadOrThrow[SizeInBytes] + } + } + + "DbSettingsSpecification" should "read values from config" in { + val config = loadConfig(ConfigFactory.parseString("""waves.db { + | directory = "/data" + | store-transactions-by-address = true + | store-lease-states-by-address = true + | store-invoke-script-results = true + | store-state-hashes = false + | max-cache-size = 100000 + | max-rollback-depth = 2000 + | cleanup-interval = 500 + | rocksdb { + | main-cache-size = 512M + | tx-cache-size = 16M + | tx-meta-cache-size = 16M + | tx-snapshot-cache-size = 16M + | api-cache-size=16M + | write-buffer-size = 128M + | enable-statistics = false + | allow-mmap-reads = off + | parallelism = 2 + | max-open-files = 100 + | } + |}""".stripMargin)) + + val actualDbSettings = ConfigSource.fromConfig(config).at("waves.db").loadOrThrow[DBSettings] + + val expectedDbSettings: DBSettings = DBSettings( + directory = "/data", + storeTransactionsByAddress = true, + storeLeaseStatesByAddress = true, + storeInvokeScriptResults = true, + storeStateHashes = false, + maxCacheSize = 100000, + maxRollbackDepth = 2000, + cleanupInterval = Some(500), + rocksdb = RocksDBSettings( + mainCacheSize = SizeInBytes(512L * 1024 * 1024), + txCacheSize = SizeInBytes(16L * 1024 * 1024), + txMetaCacheSize = SizeInBytes(16L * 1024 * 1024), + txSnapshotCacheSize = SizeInBytes(16L * 1024 * 1024), + apiCacheSize = SizeInBytes(16L * 1024 * 1024), + writeBufferSize = SizeInBytes(128L * 1024 * 1024), + enableStatistics = false, + allowMmapReads = false, + parallelism = 2, + maxOpenFiles = 100 + ) + ) + + actualDbSettings should be(expectedDbSettings) + } +} diff --git a/node/tests/src/test/scala/com/wavesplatform/settings/FeaturesSettingsSpecification.scala b/node/tests/src/test/scala/com/wavesplatform/settings/FeaturesSettingsSpecification.scala index c313d209ddb..762f6b636e3 100644 --- a/node/tests/src/test/scala/com/wavesplatform/settings/FeaturesSettingsSpecification.scala +++ b/node/tests/src/test/scala/com/wavesplatform/settings/FeaturesSettingsSpecification.scala @@ -2,21 +2,23 @@ package com.wavesplatform.settings import com.typesafe.config.ConfigFactory import com.wavesplatform.test.FlatSpec -import net.ceedubs.ficus.Ficus._ -import net.ceedubs.ficus.readers.ArbitraryTypeReader._ +import pureconfig.ConfigSource +import pureconfig.generic.auto.* class FeaturesSettingsSpecification extends FlatSpec { "FeaturesSettings" should "read values" in { - val config = ConfigFactory.parseString(""" - |waves { - | features { - | auto-shutdown-on-unsupported-feature = yes - | supported = [123,124,135] - | } - |} - """.stripMargin).resolve() + val config = ConfigFactory + .parseString(""" + |waves { + | features { + | auto-shutdown-on-unsupported-feature = yes + | supported = [123,124,135] + | } + |} + """.stripMargin) + .resolve() - val settings = config.as[FeaturesSettings]("waves.features") + val settings = ConfigSource.fromConfig(config).at("waves.features").loadOrThrow[FeaturesSettings] settings.autoShutdownOnUnsupportedFeature should be(true) settings.supported shouldEqual List(123, 124, 135) diff --git a/node/tests/src/test/scala/com/wavesplatform/settings/MinerSettingsSpecification.scala b/node/tests/src/test/scala/com/wavesplatform/settings/MinerSettingsSpecification.scala index 6836adbbca9..26d7a382a51 100644 --- a/node/tests/src/test/scala/com/wavesplatform/settings/MinerSettingsSpecification.scala +++ b/node/tests/src/test/scala/com/wavesplatform/settings/MinerSettingsSpecification.scala @@ -3,8 +3,8 @@ package com.wavesplatform.settings import com.typesafe.config.ConfigFactory import com.wavesplatform.test.FlatSpec import com.wavesplatform.transaction.TxHelpers -import net.ceedubs.ficus.Ficus.* -import net.ceedubs.ficus.readers.ArbitraryTypeReader.* +import pureconfig.ConfigSource +import pureconfig.generic.auto.* import scala.concurrent.duration.* @@ -28,7 +28,7 @@ class MinerSettingsSpecification extends FlatSpec { """.stripMargin) .resolve() - val settings = config.as[MinerSettings]("waves.miner") + val settings = ConfigSource.fromConfig(config).at("waves.miner").loadOrThrow[MinerSettings] settings.enable should be(true) settings.quorum should be(1) diff --git a/node/tests/src/test/scala/com/wavesplatform/settings/NetworkSettingsSpecification.scala b/node/tests/src/test/scala/com/wavesplatform/settings/NetworkSettingsSpecification.scala index b2a7acda6ce..0df0a8293df 100644 --- a/node/tests/src/test/scala/com/wavesplatform/settings/NetworkSettingsSpecification.scala +++ b/node/tests/src/test/scala/com/wavesplatform/settings/NetworkSettingsSpecification.scala @@ -1,51 +1,50 @@ package com.wavesplatform.settings import java.net.InetSocketAddress - import com.typesafe.config.ConfigFactory import com.wavesplatform.test.FlatSpec -import net.ceedubs.ficus.Ficus._ - -import scala.concurrent.duration._ +import pureconfig.ConfigSource +import pureconfig.generic.auto.* +import scala.concurrent.duration.* class NetworkSettingsSpecification extends FlatSpec { "NetworkSpecification" should "read values from config" in { - val config = loadConfig(ConfigFactory.parseString("""waves.network { - | bind-address: "127.0.0.1" - | port: 6868 - | node-name: "default-node-name" - | declared-address: "127.0.0.1:6868" - | nonce: 0 - | known-peers = ["8.8.8.8:6868", "4.4.8.8:6868"] - | local-only: no - | peers-data-residence-time: 1d - | black-list-residence-time: 10m - | break-idle-connections-timeout: 53s - | max-inbound-connections: 30 - | max-outbound-connections = 20 - | max-single-host-connections = 2 - | connection-timeout: 30s - | max-unverified-peers: 0 - | peers-broadcast-interval: 2m - | black-list-threshold: 50 - | unrequested-packets-threshold: 100 - | upnp { - | enable: yes - | gateway-timeout: 10s - | discover-timeout: 10s - | } - | traffic-logger { - | ignore-tx-messages = [28] - | ignore-rx-messages = [23] - | } - |}""".stripMargin)) - val networkSettings = config.as[NetworkSettings]("waves.network") + val config = loadConfig(ConfigFactory.parseString("""waves.network { + | bind-address: "127.0.0.1" + | port: 6868 + | node-name: "default-node-name" + | declared-address: "127.0.0.1:6868" + | nonce: 0 + | known-peers = ["8.8.8.8:6868", "4.4.8.8:6868"] + | local-only: no + | peers-data-residence-time: 1d + | black-list-residence-time: 10m + | break-idle-connections-timeout: 53s + | max-inbound-connections: 30 + | max-outbound-connections = 20 + | max-single-host-connections = 2 + | connection-timeout: 30s + | max-unverified-peers: 0 + | peers-broadcast-interval: 2m + | black-list-threshold: 50 + | unrequested-packets-threshold: 100 + | upnp { + | enable: yes + | gateway-timeout: 10s + | discover-timeout: 10s + | } + | traffic-logger { + | ignore-tx-messages = [28] + | ignore-rx-messages = [23] + | } + |}""".stripMargin)) + val networkSettings = ConfigSource.fromConfig(config).at("waves.network").loadOrThrow[NetworkSettings] - networkSettings.bindAddress should be(Some(new InetSocketAddress("127.0.0.1", 6868))) - networkSettings.nodeName should be("default-node-name") - networkSettings.declaredAddress should be(Some(new InetSocketAddress("127.0.0.1", 6868))) - networkSettings.nonce should be(0) + networkSettings.derivedBindAddress should be(Some(new InetSocketAddress("127.0.0.1", 6868))) + networkSettings.derivedNodeName should be("default-node-name") + networkSettings.derivedDeclaredAddress should be(Some(new InetSocketAddress("127.0.0.1", 6868))) + networkSettings.derivedNonce should be(0) networkSettings.knownPeers should be(List("8.8.8.8:6868", "4.4.8.8:6868")) networkSettings.peersDataResidenceTime should be(1.day) networkSettings.blackListResidenceTime should be(10.minutes) @@ -65,25 +64,25 @@ class NetworkSettingsSpecification extends FlatSpec { it should "generate random nonce" in { val config = loadConfig(ConfigFactory.empty()) - val networkSettings = config.as[NetworkSettings]("waves.network") + val networkSettings = ConfigSource.fromConfig(config).at("waves.network").loadOrThrow[NetworkSettings] - networkSettings.nonce should not be 0 + networkSettings.derivedNonce should not be 0 } it should "build node name using nonce" in { val config = loadConfig(ConfigFactory.parseString("waves.network.nonce = 12345")) - val networkSettings = config.as[NetworkSettings]("waves.network") + val networkSettings = ConfigSource.fromConfig(config).at("waves.network").loadOrThrow[NetworkSettings] - networkSettings.nonce should be(12345) - networkSettings.nodeName should be("Node-12345") + networkSettings.derivedNonce should be(12345) + networkSettings.derivedNodeName should be("Node-12345") } it should "build node name using random nonce" in { val config = loadConfig(ConfigFactory.empty()) - val networkSettings = config.as[NetworkSettings]("waves.network") + val networkSettings = ConfigSource.fromConfig(config).at("waves.network").loadOrThrow[NetworkSettings] - networkSettings.nonce should not be 0 - networkSettings.nodeName should be(s"Node-${networkSettings.nonce}") + networkSettings.derivedNonce should not be 0 + networkSettings.derivedNodeName should be(s"Node-${networkSettings.derivedNonce}") } it should "fail with IllegalArgumentException on too long node name" in { @@ -93,7 +92,7 @@ class NetworkSettingsSpecification extends FlatSpec { ) ) intercept[IllegalArgumentException] { - config.as[NetworkSettings]("waves.network") + ConfigSource.fromConfig(config).at("waves.network").loadOrThrow[NetworkSettings] } } } diff --git a/node/tests/src/test/scala/com/wavesplatform/settings/RestAPISettingsSpecification.scala b/node/tests/src/test/scala/com/wavesplatform/settings/RestAPISettingsSpecification.scala index eec31490532..4d0bc2b9e01 100644 --- a/node/tests/src/test/scala/com/wavesplatform/settings/RestAPISettingsSpecification.scala +++ b/node/tests/src/test/scala/com/wavesplatform/settings/RestAPISettingsSpecification.scala @@ -3,8 +3,8 @@ package com.wavesplatform.settings import akka.http.scaladsl.model.HttpMethods.* import com.typesafe.config.ConfigFactory import com.wavesplatform.test.FlatSpec -import net.ceedubs.ficus.Ficus.* -import net.ceedubs.ficus.readers.ArbitraryTypeReader.* +import pureconfig.ConfigSource +import pureconfig.generic.auto.* class RestAPISettingsSpecification extends FlatSpec { "RestAPISettings" should "read values" in { @@ -36,7 +36,7 @@ class RestAPISettingsSpecification extends FlatSpec { |} """.stripMargin ) - val settings = config.as[RestAPISettings]("waves.rest-api") + val settings = ConfigSource.fromConfig(config).at("waves.rest-api").loadOrThrow[RestAPISettings] settings.enable should be(true) settings.bindAddress should be("127.0.0.1") diff --git a/node/tests/src/test/scala/com/wavesplatform/settings/SynchronizationSettingsSpecification.scala b/node/tests/src/test/scala/com/wavesplatform/settings/SynchronizationSettingsSpecification.scala index 4385b152af7..19b1c6d3e08 100644 --- a/node/tests/src/test/scala/com/wavesplatform/settings/SynchronizationSettingsSpecification.scala +++ b/node/tests/src/test/scala/com/wavesplatform/settings/SynchronizationSettingsSpecification.scala @@ -4,8 +4,8 @@ import com.typesafe.config.ConfigFactory import com.wavesplatform.network.InvalidBlockStorageImpl.InvalidBlockStorageSettings import com.wavesplatform.settings.SynchronizationSettings.{HistoryReplierSettings, MicroblockSynchronizerSettings, UtxSynchronizerSettings} import com.wavesplatform.test.FlatSpec -import net.ceedubs.ficus.Ficus.* -import net.ceedubs.ficus.readers.ArbitraryTypeReader.* +import pureconfig.ConfigSource +import pureconfig.generic.auto.* import scala.concurrent.duration.* @@ -50,7 +50,7 @@ class SynchronizationSettingsSpecification extends FlatSpec { """.stripMargin) .resolve() - val settings = config.as[SynchronizationSettings]("waves.synchronization") + val settings = ConfigSource.fromConfig(config).at("waves.synchronization").loadOrThrow[SynchronizationSettings] settings.maxRollback should be(100) settings.synchronizationTimeout should be(30.seconds) settings.processedBlocksCacheTimeout should be(3.minutes) diff --git a/node/tests/src/test/scala/com/wavesplatform/settings/UtxSettingsSpecification.scala b/node/tests/src/test/scala/com/wavesplatform/settings/UtxSettingsSpecification.scala index 9535c11ff24..af2f906d6b5 100644 --- a/node/tests/src/test/scala/com/wavesplatform/settings/UtxSettingsSpecification.scala +++ b/node/tests/src/test/scala/com/wavesplatform/settings/UtxSettingsSpecification.scala @@ -2,27 +2,29 @@ package com.wavesplatform.settings import com.typesafe.config.ConfigFactory import com.wavesplatform.test.FlatSpec -import net.ceedubs.ficus.Ficus._ -import net.ceedubs.ficus.readers.ArbitraryTypeReader._ +import pureconfig.ConfigSource +import pureconfig.generic.auto.* class UtxSettingsSpecification extends FlatSpec { "UTXSettings" should "read values" in { - val config = ConfigFactory.parseString("""waves { - | utx { - | max-size = 100 - | max-bytes-size = 100 - | max-scripted-size = 100 - | blacklist-sender-addresses = ["a"] - | allow-blacklisted-transfer-to = ["b"] - | fast-lane-addresses = ["c"] - | allow-transactions-from-smart-accounts = false - | allow-skip-checks = false - | force-validate-in-cleanup = false - | always-unlimited-execution = true - | } - |}""".stripMargin).resolve() + val config = ConfigFactory + .parseString("""waves { + | utx { + | max-size = 100 + | max-bytes-size = 100 + | max-scripted-size = 100 + | blacklist-sender-addresses = ["a"] + | allow-blacklisted-transfer-to = ["b"] + | fast-lane-addresses = ["c"] + | allow-transactions-from-smart-accounts = false + | allow-skip-checks = false + | force-validate-in-cleanup = false + | always-unlimited-execution = true + | } + |}""".stripMargin) + .resolve() - val settings = config.as[UtxSettings]("waves.utx") + val settings = ConfigSource.fromConfig(config).at("waves.utx").loadOrThrow[UtxSettings] settings.maxSize shouldBe 100 settings.maxBytesSize shouldBe 100L settings.maxScriptedSize shouldBe 100 diff --git a/node/tests/src/test/scala/com/wavesplatform/settings/WalletSettingsSpecification.scala b/node/tests/src/test/scala/com/wavesplatform/settings/WalletSettingsSpecification.scala index a0f17b130bd..78286bc8521 100644 --- a/node/tests/src/test/scala/com/wavesplatform/settings/WalletSettingsSpecification.scala +++ b/node/tests/src/test/scala/com/wavesplatform/settings/WalletSettingsSpecification.scala @@ -3,16 +3,16 @@ package com.wavesplatform.settings import com.typesafe.config.ConfigFactory import com.wavesplatform.common.state.ByteStr import com.wavesplatform.test.FlatSpec -import net.ceedubs.ficus.Ficus._ -import net.ceedubs.ficus.readers.ArbitraryTypeReader._ +import pureconfig.ConfigSource +import pureconfig.generic.auto.* class WalletSettingsSpecification extends FlatSpec { "WalletSettings" should "read values from config" in { - val config = loadConfig(ConfigFactory.parseString("""waves.wallet { - | password: "some string as password" - | seed: "BASE58SEED" - |}""".stripMargin)) - val settings = config.as[WalletSettings]("waves.wallet") + val config = loadConfig(ConfigFactory.parseString("""waves.wallet { + | password: "some string as password" + | seed: "BASE58SEED" + |}""".stripMargin)) + val settings = ConfigSource.fromConfig(config).at("waves.wallet").loadOrThrow[WalletSettings] settings.seed should be(Some(ByteStr.decodeBase58("BASE58SEED").get)) settings.password should be(Some("some string as password")) diff --git a/project/Dependencies.scala b/project/Dependencies.scala index ad232f5a971..96750a0fd8d 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -108,7 +108,7 @@ object Dependencies { ("org.rudogma" %%% "supertagged" % "2.0-RC2").exclude("org.scala-js", "scalajs-library_2.13"), "commons-net" % "commons-net" % "3.11.1", "commons-io" % "commons-io" % "2.17.0", - "com.iheart" %% "ficus" % "1.5.2", + "com.github.pureconfig" %% "pureconfig" % "0.17.7", "net.logstash.logback" % "logstash-logback-encoder" % "8.0" % Runtime, kamonCore, kamonModule("system-metrics"), diff --git a/ride-runner/src/main/scala/com/wavesplatform/ride/runner/entrypoints/settings/RideRunnerGlobalSettings.scala b/ride-runner/src/main/scala/com/wavesplatform/ride/runner/entrypoints/settings/RideRunnerGlobalSettings.scala index f6051c6ac71..3adf204580e 100644 --- a/ride-runner/src/main/scala/com/wavesplatform/ride/runner/entrypoints/settings/RideRunnerGlobalSettings.scala +++ b/ride-runner/src/main/scala/com/wavesplatform/ride/runner/entrypoints/settings/RideRunnerGlobalSettings.scala @@ -7,8 +7,9 @@ import com.wavesplatform.ride.runner.caches.mem.MemBlockchainDataCache import com.wavesplatform.ride.runner.entrypoints.{Heights, WavesRideRunnerCompareService} import com.wavesplatform.ride.runner.requests.DefaultRequestService import com.wavesplatform.settings.* -import net.ceedubs.ficus.Ficus.* -import net.ceedubs.ficus.readers.ArbitraryTypeReader.* +import com.wavesplatform.ride.runner.input.PureconfigImplicits.* +import pureconfig.* +import pureconfig.generic.auto.* import scala.concurrent.duration.DurationInt @@ -50,5 +51,5 @@ case class RideRunnerGlobalSettings( } object RideRunnerGlobalSettings { - def fromRootConfig(config: Config): RideRunnerGlobalSettings = config.getConfig("waves").as[RideRunnerGlobalSettings] + def fromRootConfig(config: Config): RideRunnerGlobalSettings = ConfigSource.fromConfig(config).at("waves").loadOrThrow[RideRunnerGlobalSettings] } diff --git a/ride-runner/src/main/scala/com/wavesplatform/ride/runner/entrypoints/settings/package.scala b/ride-runner/src/main/scala/com/wavesplatform/ride/runner/entrypoints/settings/package.scala deleted file mode 100644 index 00c70384f98..00000000000 --- a/ride-runner/src/main/scala/com/wavesplatform/ride/runner/entrypoints/settings/package.scala +++ /dev/null @@ -1,37 +0,0 @@ -package com.wavesplatform.ride.runner.entrypoints - -import com.typesafe.config.* -import com.wavesplatform.account.Address -import net.ceedubs.ficus.Ficus.* -import net.ceedubs.ficus.readers.{CollectionReaders, ValueReader} -import play.api.libs.json.{JsObject, Json} - -package object settings { - implicit val configMemorySizeValueReader: ValueReader[ConfigMemorySize] = (config: Config, path: String) => config.getMemorySize(path) - - implicit val testRequestsValueReader: ValueReader[List[(Address, JsObject)]] = CollectionReaders - .traversableReader[List, ConfigValue] - .map { xs => - xs.map { - case xs: ConfigList if xs.unwrapped().size() == 2 => - val key = xs.get(0).unwrapped() match { - case k: String => - Address.fromString(k) match { - case Right(x) => x - case Left(e) => throw new RuntimeException(s"Can't parse '$k' as Address: ${e.toString}") - } - case k => throw new RuntimeException(s"Can't parse as Address: $k") - } - - val strV = xs.get(1).render(ConfigRenderOptions.concise()) - val value = Json.parse(strV) match { - case x: JsObject => x - case x => throw new RuntimeException(s"Can't parse value as JsObject: $x") - } - - key -> value - - case xs => throw new RuntimeException(s"Expected two elements, got: $xs") - } - } -} diff --git a/ride-runner/src/main/scala/com/wavesplatform/ride/runner/input/PureconfigImplicits.scala b/ride-runner/src/main/scala/com/wavesplatform/ride/runner/input/PureconfigImplicits.scala new file mode 100644 index 00000000000..14bf9ade5c2 --- /dev/null +++ b/ride-runner/src/main/scala/com/wavesplatform/ride/runner/input/PureconfigImplicits.scala @@ -0,0 +1,37 @@ +package com.wavesplatform.ride.runner.input + +import com.typesafe.config.{ConfigFactory, ConfigRenderOptions} +import com.wavesplatform.account.Address +import com.wavesplatform.json.JsonManipulations +import play.api.libs.json.{JsError, JsObject, JsSuccess, JsValue, Json, Reads} +import pureconfig.ConfigReader +import pureconfig.error.CannotConvert + +object PureconfigImplicits { + + implicit val jsObjectConfigReader: ConfigReader[JsObject] = playJsonConfigReader + + implicit val jsValueConfigReader: ConfigReader[JsValue] = playJsonConfigReader + + private def playJsonConfigReader[T: Reads]: ConfigReader[T] = ConfigReader.fromCursor { cur => + for { + configValue <- cur.asConfigValue + } yield { + val stubKey = "stubKey" + val config = ConfigFactory.empty().withValue(stubKey, configValue) + val jsonStr = config.root().render(ConfigRenderOptions.concise()) + JsonManipulations + .pick(Json.parse(jsonStr), stubKey) + .getOrElse(fail(s"Expected a value")) + .validate[T] match { + case JsSuccess(value, _) => value + case JsError(errors) => fail(s"Can't parse: ${errors.mkString("\n")}") + } + } + } + + implicit val addressConfigReader: ConfigReader[Address] = + ConfigReader.fromString(s => Address.fromString(s).left.map(_ => CannotConvert(s, "Address", "invalid address"))) + + private def fail(message: String, cause: Throwable = null) = throw new IllegalArgumentException(message, cause) +} diff --git a/ride-runner/src/main/scala/com/wavesplatform/ride/runner/input/RideRunnerInputParser.scala b/ride-runner/src/main/scala/com/wavesplatform/ride/runner/input/RideRunnerInputParser.scala index 9ca9dc40390..da1689d5307 100644 --- a/ride-runner/src/main/scala/com/wavesplatform/ride/runner/input/RideRunnerInputParser.scala +++ b/ride-runner/src/main/scala/com/wavesplatform/ride/runner/input/RideRunnerInputParser.scala @@ -2,31 +2,25 @@ package com.wavesplatform.ride.runner.input import cats.syntax.either.* import cats.syntax.option.* -import com.google.protobuf.{ByteString, UnsafeByteOperations} -import com.typesafe.config.{Config, ConfigFactory, ConfigRenderOptions} +import com.typesafe.config.{Config, ConfigFactory} import com.wavesplatform.account.* import com.wavesplatform.account.PublicKeys.EmptyPublicKey import com.wavesplatform.block.Block.BlockId import com.wavesplatform.common.state.ByteStr import com.wavesplatform.common.utils.{Base58, Base64} -import com.wavesplatform.json.JsonManipulations -import com.wavesplatform.lang.directives.values.StdLibVersion import com.wavesplatform.lang.script.{Script, ScriptReader} import com.wavesplatform.ride.ScriptUtil -import com.wavesplatform.state.Height -import com.wavesplatform.transaction.Asset.IssuedAsset -import com.wavesplatform.transaction.transfer.TransferTransactionLike -import com.wavesplatform.transaction.{TransactionFactory, TxNonNegativeAmount, TxValidationError} -import com.wavesplatform.utils.byteArrayFromString -import net.ceedubs.ficus.Ficus.* -import net.ceedubs.ficus.readers.{ArbitraryTypeReader, ValueReader} +import com.wavesplatform.transaction.Asset.{IssuedAsset, Waves} +import com.wavesplatform.transaction.{Asset, TxNonNegativeAmount, TxValidationError} +import pureconfig.* +import pureconfig.generic.auto.* import play.api.libs.json.* +import com.wavesplatform.ride.runner.input.PureconfigImplicits.* import java.nio.charset.StandardCharsets -import scala.jdk.CollectionConverters.CollectionHasAsScala import scala.util.Try -object RideRunnerInputParser extends ArbitraryTypeReader { +object RideRunnerInputParser { val Base58Prefix = "base58:" def prepare(config: Config): Config = @@ -36,75 +30,143 @@ object RideRunnerInputParser extends ArbitraryTypeReader { /** Use after "prepare" */ - def from(config: Config): RideRunnerInput = config.as[RideRunnerInput] - - def getChainId(x: Config): Char = x.getAs[Char]("chainId").getOrElse(fail("chainId is not specified or wrong")) - - implicit val shortMapKeyValueReader: MapKeyValueReader[Short] = { key => - key.toShortOption.getOrElse(fail(s"Expected an integer value between ${Short.MinValue} and ${Short.MaxValue}")) + def from(config: Config): RideRunnerInput = { + val address = ConfigSource.fromConfig(config).at("address").loadOrThrow[Address] + val request = ConfigSource.fromConfig(config).at("request").loadOrThrow[JsObject] + val chainId = getChainId(config) + val intAsString = ConfigSource.fromConfig(config).at("intAsString").load[Boolean].getOrElse(false) + val trace = ConfigSource.fromConfig(config).at("trace").load[Boolean].getOrElse(false) + val evaluateScriptComplexityLimit = + ConfigSource.fromConfig(config).at("evaluateScriptComplexityLimit").load[Int].getOrElse(Int.MaxValue) + val maxTxErrorLogSize = ConfigSource.fromConfig(config).at("maxTxErrorLogSize").load[Int].getOrElse(1024) + val state = ConfigSource.fromConfig(config).at("state").loadOrThrow[RideRunnerBlockchainState] + val postProcessing = ConfigSource.fromConfig(config).at("postProcessing").load[List[RideRunnerPostProcessingMethod]].getOrElse(List.empty) + val test = ConfigSource.fromConfig(config).at("test.expected").load[JsValue].map(RideRunnerTest.apply).toOption + + RideRunnerInput( + address = address, + request = request, + chainId = chainId, + intAsString = intAsString, + trace = trace, + evaluateScriptComplexityLimit = evaluateScriptComplexityLimit, + maxTxErrorLogSize = maxTxErrorLogSize, + state = state, + postProcessing = postProcessing, + test = test + ) } - implicit val intMapKeyValueReader: MapKeyValueReader[Int] = { key => + def getChainId(x: Config): Char = ConfigSource.fromConfig(x).at("chainId").load[Char].getOrElse(fail("chainId is not specified or wrong")) + + implicit val intMapKeyConfigReader: MapKeyConfigReader[Int] = { key => key.toIntOption.getOrElse(fail(s"Expected an integer value between ${Int.MinValue} and ${Int.MaxValue}")) } - implicit val byteStrMapKeyValueReader: MapKeyValueReader[ByteStr] = byteStrDefaultBase58FromString(_) + implicit val byteStrMapKeyConfigReader: MapKeyConfigReader[ByteStr] = byteStrDefaultBase58FromString(_) - implicit val addressMapKeyValueReader: MapKeyValueReader[Address] = Address.fromString(_).getOrFail + implicit val addressMapKeyConfigReader: MapKeyConfigReader[Address] = Address.fromString(_).getOrFail - implicit val issuedAssetMapKeyValueReader: MapKeyValueReader[IssuedAsset] = IssuedAsset.fromString(_, identity, fail(_)) + implicit val issuedAssetMapKeyConfigReader: MapKeyConfigReader[IssuedAsset] = IssuedAsset.fromString(_, identity, fail(_)) - implicit val optBlockIdMapKeyValueReader: MapKeyValueReader[Option[BlockId]] = { x => + implicit val optBlockIdMapKeyConfigReader: MapKeyConfigReader[Option[BlockId]] = { x => if (x.isEmpty) None else byteStrDefaultBase58FromString(x).some } - implicit val charValueReader: ValueReader[Char] = ValueReader[String].map { x => - if (x.length == 1) x.head else fail(s"Expected one char, got: $x") - } - - implicit val byteValueReader: ValueReader[Byte] = ValueReader[Int].map { x => - if (x.isValidByte) x.toByte - else fail(s"Expected an integer value between ${Byte.MinValue} and ${Byte.MaxValue}") - } - - implicit val shortValueReader: ValueReader[Short] = ValueReader[Int].map { x => - if (x.isValidShort) x.toShort - else fail(s"Expected a value between ${Short.MinValue} and ${Short.MaxValue}") + implicit val shortMapKeyConfigReader: MapKeyConfigReader[Short] = { key => + key.toShortOption.getOrElse(fail(s"Expected an integer value between ${Short.MinValue} and ${Short.MaxValue}")) } - implicit val heightValueReader: ValueReader[Height] = ValueReader[Int].map(Height(_)) + implicit val txNonNegativeAmountConfigReader: ConfigReader[TxNonNegativeAmount] = ConfigReader[Long].map(TxNonNegativeAmount.unsafeFrom) - implicit val stdLibVersionValueReader: ValueReader[StdLibVersion] = ValueReader[Int].map(StdLibVersion.VersionDic.idMap.apply) + implicit val byteStrConfigReader: ConfigReader[ByteStr] = ConfigReader[String].map(byteStrDefaultBase58FromString) - implicit val jsValueValueReader: ValueReader[JsValue] = jsValueReader - implicit val jsObjectValueReader: ValueReader[JsObject] = jsValueReader - - implicit val txNonNegativeAmountValueReader: ValueReader[TxNonNegativeAmount] = ValueReader[Long].map(TxNonNegativeAmount.unsafeFrom) - - implicit val byteArrayValueReader: ValueReader[Array[Byte]] = ValueReader[String].map(byteArrayFromString(_, identity, fail(_))) - - implicit val byteStringValueReader: ValueReader[ByteString] = byteArrayValueReader.map(UnsafeByteOperations.unsafeWrap) - - implicit val byteStrValueReader: ValueReader[ByteStr] = byteArrayValueReader.map(ByteStr(_)) - - implicit val stringOrBytesAsByteArratValueReader: ValueReader[StringOrBytesAsByteArray] = ValueReader[String].map { x => + implicit val stringOrBytesAsByteArrayConfigReader: ConfigReader[StringOrBytesAsByteArray] = ConfigReader[String].map { x => StringOrBytesAsByteArray(byteArrayDefaultUtf8FromString(x)) } - implicit val scriptValueReader: ValueReader[Script] = ValueReader[String].map { x => + implicit val scriptConfigReader: ConfigReader[Script] = ConfigReader[String].map { x => if (x.startsWith(Base64.Prefix)) ScriptReader.fromBytes(Base64.decode(x)).getOrFail else ScriptUtil.from(x) } type SrcOrCompiledScript = Either[String, Script] - implicit val srcOrCompiledScriptValueReader: ValueReader[SrcOrCompiledScript] = ValueReader[String].map { x => + + implicit val srcOrCompiledScriptConfigReader: ConfigReader[SrcOrCompiledScript] = ConfigReader[String].map { x => if (x.startsWith(Base64.Prefix)) ScriptReader.fromBytes(Base64.decode(x)).getOrFail.asRight else x.asLeft } - implicit val addressValueReader: ValueReader[Address] = ValueReader[String].map(Address.fromString(_).getOrFail) + implicit val accountConfigReader: ConfigReader[RideRunnerAccount] = ConfigReader.fromCursor { cur => + for { + objCur <- cur.asObjectCursor + assetBalances <- ConfigReader[Option[Map[IssuedAsset, TxNonNegativeAmount]]] + .from(objCur.atKeyOrUndefined("assetBalances")) + .map(_.getOrElse(Map.empty)) + regularBalance <- ConfigReader[Option[TxNonNegativeAmount]].from(objCur.atKeyOrUndefined("regularBalance")) + leasing <- ConfigReader[Option[RideRunnerLeaseBalance]].from(objCur.atKeyOrUndefined("leasing")) + generatingBalance <- ConfigReader[Option[TxNonNegativeAmount]].from(objCur.atKeyOrUndefined("generatingBalance")) + data <- ConfigReader[Option[Map[String, RideRunnerDataEntry]]].from(objCur.atKeyOrUndefined("data")) + aliases <- ConfigReader[Option[List[Alias]]].from(objCur.atKeyOrUndefined("aliases")).map(_.getOrElse(Nil)) + scriptInfo <- ConfigReader[Option[RideRunnerScriptInfo]].from(objCur.atKeyOrUndefined("scriptInfo")) + } yield RideRunnerAccount(assetBalances, regularBalance, leasing, generatingBalance, data, aliases, scriptInfo) + } + + implicit val assetConfigReader: ConfigReader[RideRunnerAsset] = ConfigReader.fromCursor { cur => + for { + objCur <- cur.asObjectCursor + issuerPublicKey <- ConfigReader[Option[PublicKey]].from(objCur.atKeyOrUndefined("issuerPublicKey")).map(_.getOrElse(EmptyPublicKey)) + name <- ConfigReader[Option[StringOrBytesAsByteArray]].from(objCur.atKeyOrUndefined("name")).map(_.getOrElse(RideRunnerAsset.DefaultName)) + description <- ConfigReader[Option[StringOrBytesAsByteArray]] + .from(objCur.atKeyOrUndefined("description")) + .map(_.getOrElse(RideRunnerAsset.DefaultDescription)) + decimals <- ConfigReader[Option[Int]].from(objCur.atKeyOrUndefined("decimals")).map(_.getOrElse(8)) + reissuable <- ConfigReader[Option[Boolean]].from(objCur.atKeyOrUndefined("reissuable")).map(_.getOrElse(false)) + quantity <- ConfigReader[Option[Long]].from(objCur.atKeyOrUndefined("quantity")).map(_.getOrElse(9007199254740991L)) + script <- ConfigReader[Option[Script]].from(objCur.atKeyOrUndefined("script")) + minSponsoredAssetFee <- ConfigReader[Option[Long]].from(objCur.atKeyOrUndefined("minSponsoredAssetFee")).map(_.getOrElse(0L)) + } yield RideRunnerAsset(issuerPublicKey, name, description, decimals, reissuable, quantity, script, minSponsoredAssetFee) + } - implicit val aliasValueReader: ValueReader[Alias] = ValueReader[String].map { x => + implicit val blockConfigReader: ConfigReader[RideRunnerBlock] = ConfigReader.fromCursor { cur => + for { + objCur <- cur.asObjectCursor + // Note: `System.currentTimeMillis()` is a side effect, as well as the default value for the `timestamp` field in case class is. + // It would be a good idea not to use side effects in the default values of case class fields. + timestamp <- ConfigReader[Option[Long]].from(objCur.atKeyOrUndefined("timestamp")).map(_.getOrElse(System.currentTimeMillis())) + baseTarget <- ConfigReader[Option[Long]].from(objCur.atKeyOrUndefined("baseTarget")).map(_.getOrElse(130L)) + generationSignature <- ConfigReader[Option[ByteStr]] + .from(objCur.atKeyOrUndefined("generationSignature")) + .map(_.getOrElse(ByteStr(new Array[Byte](64)))) + generatorPublicKey <- ConfigReader[Option[PublicKey]].from(objCur.atKeyOrUndefined("generatorPublicKey")).map(_.getOrElse(EmptyPublicKey)) + + vrf <- ConfigReader[Option[ByteStr]].from(objCur.atKeyOrUndefined("VRF")) + blockReward <- ConfigReader[Option[Long]].from(objCur.atKeyOrUndefined("blockReward")).map(_.getOrElse(600_000_000L)) + } yield RideRunnerBlock(timestamp, baseTarget, generationSignature, generatorPublicKey, vrf, blockReward) + } + + implicit val transactionConfigReader: ConfigReader[RideRunnerTransaction] = ConfigReader.fromCursor { cur => + for { + objCur <- cur.asObjectCursor + amount <- ConfigReader[Option[Long]].from(objCur.atKeyOrUndefined("amount")).map(_.getOrElse(1L)) + assetId <- ConfigReader[Option[Asset]].from(objCur.atKeyOrUndefined("assetId")).map(_.getOrElse(Waves)) + fee <- ConfigReader[Option[Long]].from(objCur.atKeyOrUndefined("fee")).map(_.getOrElse(100_000L)) + feeAssetId <- ConfigReader[Option[Asset]].from(objCur.atKeyOrUndefined("feeAssetId")).map(_.getOrElse(Waves)) + recipient <- objCur.atKey("recipient").flatMap(ConfigReader[AddressOrAlias].from) + senderPublicKey <- ConfigReader[Option[PublicKey]].from(objCur.atKeyOrUndefined("senderPublicKey")).map(_.getOrElse(EmptyPublicKey)) + height <- ConfigReader[Option[Int]].from(objCur.atKeyOrUndefined("height")) + // Note: `System.currentTimeMillis()` is a side effect, as well as the default value for the `timestamp` field in case class is. + // It would be a good idea not to use side effects in the default values of case class fields. + timestamp <- ConfigReader[Option[Long]].from(objCur.atKeyOrUndefined("timestamp")).map(_.getOrElse(System.currentTimeMillis())) + proofs <- ConfigReader[Option[List[StringOrBytesAsByteArray]]].from(objCur.atKeyOrUndefined("proofs")).map(_.getOrElse(Nil)) + version <- ConfigReader[Option[Byte]].from(objCur.atKeyOrUndefined("version")).map(_.getOrElse(3: Byte)) + attachment <- ConfigReader[Option[StringOrBytesAsByteArray]] + .from(objCur.atKeyOrUndefined("attachment")) + .map(_.getOrElse(StringOrBytesAsByteArray(Array.empty[Byte]))) + } yield RideRunnerTransaction(amount, assetId, fee, feeAssetId, recipient, senderPublicKey, height, timestamp, proofs, version, attachment) + } + + implicit val aliasConfigReader: ConfigReader[Alias] = ConfigReader[String].map { x => val chainId = AddressScheme.current.chainId val separatorNumber = x.count(_ == ':') @@ -116,7 +178,7 @@ object RideRunnerInputParser extends ArbitraryTypeReader { alias.flatMap { x => Either.cond(x.chainId == chainId, x, TxValidationError.WrongChain(chainId, x.chainId)) }.getOrFail } - implicit val addressOrAliasValueReader: ValueReader[AddressOrAlias] = ValueReader[String].map { x => + implicit val addressOrAliasConfigReader: ConfigReader[AddressOrAlias] = ConfigReader[String].map { x => val chainId = AddressScheme.current.chainId val separatorNumber = x.count(_ == ':') @@ -128,55 +190,52 @@ object RideRunnerInputParser extends ArbitraryTypeReader { addressOrAlias.flatMap { x => Either.cond(x.chainId == chainId, x, TxValidationError.WrongChain(chainId, x.chainId)) }.getOrFail } - implicit val publicKeyValueReader: ValueReader[PublicKey] = ValueReader[ByteStr].map(PublicKey(_)) - - implicit val transferTransactionLikeValueReader: ValueReader[TransferTransactionLike] = jsObjectValueReader.map { js => - TransactionFactory - .fromSignedRequest(js) - .flatMap { - case tx: TransferTransactionLike => Right(tx) - case _ => Left(TxValidationError.UnsupportedTransactionType) + implicit val publicKeyConfigReader: ConfigReader[PublicKey] = ConfigReader[ByteStr].map(PublicKey(_)) + + implicit val rideRunnerDataEntryConfigReader: ConfigReader[RideRunnerDataEntry] = ConfigReader.fromCursor { cur => + for { + objCur <- cur.asObjectCursor + dataType <- objCur.atKey("type").flatMap(ConfigReader[String].from) + data <- dataType match { + case "integer" => objCur.atKey("value").flatMap(ConfigReader[Long].from).map(IntegerRideRunnerDataEntry.apply) + case "boolean" => objCur.atKey("value").flatMap(ConfigReader[Boolean].from).map(BooleanRideRunnerDataEntry.apply) + case "string" => objCur.atKey("value").flatMap(ConfigReader[String].from).map(StringRideRunnerDataEntry.apply) + case "binary" => + objCur.atKey("value").flatMap(ConfigReader[String].from).map(x => BinaryRideRunnerDataEntry(ByteStr(byteArrayDefaultUtf8FromString(x)))) + case x => fail(s"Expected one of types: integer, boolean, string, binary. Got $x") } - .getOrFail + } yield data } - implicit val rideRunnerDataEntryValueReader: ValueReader[RideRunnerDataEntry] = ValueReader.relative[RideRunnerDataEntry] { config => - config.getString("type") match { - case "integer" => IntegerRideRunnerDataEntry(config.getLong("value")) - case "boolean" => BooleanRideRunnerDataEntry(config.getBoolean("value")) - case "string" => StringRideRunnerDataEntry(config.getString("value")) - case "binary" => BinaryRideRunnerDataEntry(ByteStr(byteArrayDefaultUtf8FromString(config.getString("value")))) - case x => fail(s"Expected one of types: integer, boolean, string, binary. Got $x") - } - } - - implicit val rideRunnerPostProcessingMethodValueReader: ValueReader[RideRunnerPostProcessingMethod] = - ValueReader.relative[RideRunnerPostProcessingMethod] { config => - config.getString("type") match { - case "pick" => RideRunnerPostProcessingMethod.Pick(config.getString("path")) - case "pickAll" => RideRunnerPostProcessingMethod.PickAll(config.getStringList("paths").asScala.toList) - case "prune" => RideRunnerPostProcessingMethod.Prune(config.getStringList("paths").asScala.toList) + implicit val rideRunnerPostProcessingMethodConfigReader: ConfigReader[RideRunnerPostProcessingMethod] = ConfigReader.fromCursor { cur => + for { + objCur <- cur.asObjectCursor + method <- objCur.atKey("type").flatMap(ConfigReader[String].from) + data <- method match { + case "pick" => objCur.atKey("path").flatMap(ConfigReader[String].from).map(RideRunnerPostProcessingMethod.Pick.apply) + case "pickAll" => objCur.atKey("paths").flatMap(ConfigReader[List[String]].from).map(RideRunnerPostProcessingMethod.PickAll.apply) + case "prune" => objCur.atKey("paths").flatMap(ConfigReader[List[String]].from).map(RideRunnerPostProcessingMethod.Prune.apply) case "regex" => - RideRunnerPostProcessingMethod.Regex( - path = config.getString("path"), - find = config.getString("find"), - replace = config.getString("replace") - ) + for { + path <- objCur.atKey("path").flatMap(ConfigReader[String].from) + find <- objCur.atKey("find").flatMap(ConfigReader[String].from) + replace <- objCur.atKey("replace").flatMap(ConfigReader[String].from) + } yield RideRunnerPostProcessingMethod.Regex(path, find, replace) case x => fail(s"Expected one of types: pick, pickAll, prune. Got $x") } - } - - implicit val rideRunnerScriptInfoValueReader: ValueReader[RideRunnerScriptInfo] = ValueReader.relative[RideRunnerScriptInfo] { config => - val pk = config.as[Option[PublicKey]]("publicKey") - val script = config.as[SrcOrCompiledScript]("script") - val imports = config.as[Option[Map[String, String]]]("imports") - - val compiledScript = script match { - case Right(x) => x - case Left(src) => ScriptUtil.from(src, imports.getOrElse(Map.empty)) - } + } yield data + } - RideRunnerScriptInfo(pk.getOrElse(EmptyPublicKey), compiledScript) + implicit val rideRunnerScriptInfoConfigReader: ConfigReader[RideRunnerScriptInfo] = ConfigReader.fromCursor { cur => + for { + objCur <- cur.asObjectCursor + pk <- ConfigReader[Option[PublicKey]].from(objCur.atKeyOrUndefined("publicKey")).map(_.getOrElse(EmptyPublicKey)) + imports <- ConfigReader[Option[Map[String, String]]].from(objCur.atKeyOrUndefined("imports")).map(_.getOrElse(Map.empty)) + compiledScript <- ConfigReader[SrcOrCompiledScript].from(objCur.atKeyOrUndefined("script")).map { + case Right(x) => x + case Left(src) => ScriptUtil.from(src, imports) + } + } yield RideRunnerScriptInfo(pk, compiledScript) } private def byteArrayDefaultUtf8FromString(x: String): Array[Byte] = Try { @@ -193,32 +252,18 @@ object RideRunnerInputParser extends ArbitraryTypeReader { else Base58.tryDecodeWithLimit(x).fold(e => fail(s"Error parsing base58: ${e.getMessage}"), identity) } - private def jsValueReader[T: Reads]: ValueReader[T] = { (config: Config, path: String) => - // config.getObject(path) doesn't work for primitive values. - // atPath("x") allows a consistent rendering for all types of content at specified path. - val fixedPath = if (path == "") "x" else s"x.$path" - val jsonStr = config.atPath("x").root().render(ConfigRenderOptions.concise()) - JsonManipulations - .pick(Json.parse(jsonStr), fixedPath) - .getOrElse(fail(s"Expected a value at $path")) - .validate[T] match { - case JsSuccess(value, _) => value - case JsError(errors) => fail(s"Can't parse: ${errors.mkString("\n")}") - } - } - private implicit final class ValidationErrorOps[E, T](private val self: Either[E, T]) extends AnyVal { def getOrFail: T = self.fold(e => fail(e.toString), identity) } private def fail(message: String, cause: Throwable = null) = throw new IllegalArgumentException(message, cause) - implicit def arbitraryKeyMapValueReader[K, V: ValueReader](implicit kReader: MapKeyValueReader[K]): ValueReader[Map[K, V]] = - ValueReader[Map[String, V]].map { xs => + implicit def arbitraryKeyMapConfigReader[K, V: ConfigReader](implicit kReader: MapKeyConfigReader[K]): ConfigReader[Map[K, V]] = + ConfigReader[Map[String, V]].map { xs => xs.map { case (k, v) => kReader.readKey(k) -> v } } - trait MapKeyValueReader[T] { + trait MapKeyConfigReader[T] { def readKey(key: String): T } } diff --git a/ride-runner/src/test/scala/com/wavesplatform/ride/runner/input/RideRunnerInputParserTestSuite.scala b/ride-runner/src/test/scala/com/wavesplatform/ride/runner/input/RideRunnerInputParserTestSuite.scala index dae96773697..8ebd68a41f7 100644 --- a/ride-runner/src/test/scala/com/wavesplatform/ride/runner/input/RideRunnerInputParserTestSuite.scala +++ b/ride-runner/src/test/scala/com/wavesplatform/ride/runner/input/RideRunnerInputParserTestSuite.scala @@ -13,12 +13,13 @@ import com.wavesplatform.ride.{DiffXInstances, ScriptUtil} import com.wavesplatform.transaction.Asset.IssuedAsset import com.wavesplatform.transaction.TxNonNegativeAmount import com.wavesplatform.{BaseTestSuite, HasTestAccounts} -import net.ceedubs.ficus.Ficus.toFicusConfig -import net.ceedubs.ficus.readers.ValueReader import org.scalatest.prop.TableDrivenPropertyChecks +import com.wavesplatform.ride.runner.input.PureconfigImplicits.* import play.api.libs.json.* +import pureconfig.* import java.nio.charset.StandardCharsets +import scala.reflect.ClassTag import scala.util.{Success, Try} class RideRunnerInputParserTestSuite extends BaseTestSuite with TableDrivenPropertyChecks with HasTestAccounts with DiffXInstances { @@ -372,6 +373,8 @@ func bar () = { } } - private def parseQuotedStringAs[T: ValueReader](s: String): T = ConfigFactory.parseString(s"""x = \"\"\"$s\"\"\"""").as[T]("x") - private def parseAs[T: ValueReader](rawContent: String): T = ConfigFactory.parseString(s"""x = $rawContent""").as[T]("x") + private def parseQuotedStringAs[T: ConfigReader: ClassTag](s: String): T = + ConfigSource.fromConfig(ConfigFactory.parseString(s"""x = \"\"\"$s\"\"\"""")).at("x").loadOrThrow[T] + private def parseAs[T: ConfigReader: ClassTag](rawContent: String): T = + ConfigSource.fromConfig(ConfigFactory.parseString(s"""x = $rawContent""")).at("x").loadOrThrow[T] }