[
  {
    "path": ".gitignore",
    "content": "*.class\n*.log\n\n# config\napplication.conf\napplication.properties\n\n# generated by scalajs\nlmchain.js\ndev.js\nprod.js\nclient-fastopt-bundle.js\nclient-fastopt-bundle.js.map\n\n# sway db data folder\nsway\n\n# workshheets\n*.worksheet.sc\n\n# gateway log\nunsent-deposits.json\nsent-deposits.logs\nlast-block-read.json\n\n# npm\nnode_modules\n\n# parcel\n.parcel-cache\ndist\ndist.zip\n.env\n\n# settings\nSettings.scala\n\n# mac\n.DS_Store\n\n# archive\ntxs.archive\ntxs1.archive\n\n# bulk-insert\ninvalid-txs.csv\n"
  },
  {
    "path": ".jvmopts",
    "content": "-Xms1g\n-Xmx4g\n"
  },
  {
    "path": ".scalafix.conf",
    "content": "OrganizeImports {\n  coalesceToWildcardImportThreshold = 10 # Int.MaxValue\n  expandRelative = false\n  groupExplicitlyImportedImplicitsSeparately = false\n  groupedImports = Merge\n  groups = [\"re:javax?\\\\.\", \"scala.\", \"cats.\", \"*\", \"hedgehog.\"]\n  importSelectorsOrder = Ascii\n  importsOrder = SymbolsFirst\n  removeUnused = false\n  targetDialect = Scala3\n}\n"
  },
  {
    "path": ".scalafmt.conf",
    "content": "version = 3.7.3\nrunner.dialect = scala3\n\nalign.preset = more\nrewrite.trailingCommas.style = always\nnewlines.beforeTypeBounds = fold\nrewrite.scala3 {\n  convertToNewSyntax = true\n  removeOptionalBraces = yes\n}\nfileOverride {\n  \"glob:**/*.sbt\" {\n    runner.dialect = sbt1\n  }\n  \"glob:**/**/*.sbt\" {\n    runner.dialect = sbt1\n  }\n  \"glob:**/**/**/*.sbt\" {\n    runner.dialect = sbt1\n  }\n}\n"
  },
  {
    "path": "README.md",
    "content": "# LeisureMetaverse Blockchain \nLeisureMetaverse Blockchain is to present a practical solution to limitations of existing blockchains by combining the existing computer engineering methodology with the structure of the blockchain. \n\nScala is a statically typed programming language which supports both object-oriented programming and functional programming. Building a blockchain on SCALA can reduce many thread safety concerns and be suited for component-based applications that support distribution and concurrency. \n\nLeisureMetaverse Blockchain uses a mixed data structure that combines UTXO and account. The proposed structure uses Merkle-Patricia Trie to record the UTXOs of all the accounts. It means that the blockchain has a snapshot of the latest state in the block. By comparing cryptographic signatures in the UTXO in the latest block, it is possible to verify new transaction request without synchronizing the old data. \n\n\n## LM Scan\n\n### Dev Mode\n\n#### Run Backend\n\n```bash\nsbt ~lmscanBackend/reStart\n```\n\n#### Run ScalaJS\n\n```bash\nsbt ~lmscanFrontend/fastLinkJS\n```\n\n#### Run Frontend\n\n```bash\ncd modules/lmscan-frontend\nyarn start\n```\n\n### Build Mode\n\n#### Assembly Backend\n\n```bash\nsbt lmscanBackend/assembly\n```\n\n#### Build ScalaJS\n\n```bash\nsbt lmscanFrontend/fullLinkJS\n```\n\n#### Build Frontend\n\n```bash\ncd modules/lmscan-frontend\nyarn build\n```\n"
  },
  {
    "path": "build.sbt",
    "content": "val V = new {\n  val Scala      = \"3.4.3\"\n  val ScalaGroup = \"3.4\"\n\n  val catsEffect = \"3.5.4\"\n  val tapir      = \"1.10.6\"\n  val sttp       = \"3.9.6\"\n  val circe      = \"0.15.0-M1\"\n  val refined    = \"0.11.1\"\n  val iron       = \"2.5.0\"\n  val scodecBits = \"1.1.38\"\n  val shapeless  = \"3.4.1\"\n  val fs2        = \"3.10.2\"\n\n  val typesafeConfig = \"1.4.3\"\n  val pureconfig     = \"0.17.6\"\n  val bouncycastle   = \"1.70\"\n  val sway           = \"0.16.2\"\n  val lettuce        = \"6.3.2.RELEASE\"\n  val jasync         = \"2.2.4\"\n\n  val okhttp3LoggingInterceptor = \"4.12.0\"\n\n  val web3J = \"4.9.6\"\n\n  val awsSdk = \"2.20.75\"\n\n  val scribe          = \"3.13.4\"\n  val hedgehog        = \"0.10.1\"\n  val organiseImports = \"0.6.0\"\n  val munitCatsEffect = \"2.0.0-RC1\"\n\n  val tyrian = \"0.9.0\"\n\n  val scalaJavaTime = \"2.3.0\"\n  val jsSha3        = \"0.8.0\"\n  val elliptic      = \"6.5.4\"\n  val typesElliptic = \"6.4.12\"\n  val pgEmbedded    = \"1.0.3\"\n  val quill         = \"4.8.0\"\n  val postgres      = \"42.7.3\"\n  val flywayCore    = \"9.22.3\"\n  val sqlite        = \"3.48.0.0\"\n  val doobieVersion = \"1.0.0-RC5\"\n  val hikari        = \"6.2.1\"\n}\n\nval Dependencies = new {\n\n  lazy val node = Seq(\n    libraryDependencies ++= Seq(\n      \"com.softwaremill.sttp.tapir\" %% \"tapir-armeria-server-cats\" % V.tapir,\n      \"com.softwaremill.sttp.tapir\" %% \"tapir-json-circe\"          % V.tapir,\n      \"com.outr\"                    %% \"scribe-slf4j\"              % V.scribe,\n      \"com.typesafe\" % \"config\"       % V.typesafeConfig,\n      (\"io.swaydb\"  %% \"swaydb\"       % V.sway).cross(CrossVersion.for3Use2_13),\n      \"io.lettuce\"   % \"lettuce-core\" % V.lettuce,\n    ),\n    excludeDependencies ++= Seq(\n      \"org.scala-lang.modules\" % \"scala-collection-compat_2.13\",\n      \"org.scala-lang.modules\" % \"scala-java8-compat_2.13\",\n    ),\n  )\n\n  lazy val jvmClient = Seq(\n    libraryDependencies ++= Seq(\n      \"com.softwaremill.sttp.client3\" %% \"armeria-backend-cats\" % V.sttp,\n      \"com.softwaremill.sttp.tapir\"   %% \"tapir-sttp-client\"    % V.tapir,\n    ),\n    excludeDependencies ++= Seq(\n      \"org.scala-lang.modules\" % \"scala-collection-compat_2.13\",\n      \"org.scala-lang.modules\" % \"scala-java8-compat_2.13\",\n    ),\n  )\n\n  lazy val ethGateway = Seq(\n    libraryDependencies ++= Seq(\n      \"com.softwaremill.sttp.tapir\"   %% \"tapir-armeria-server-cats\" % V.tapir,\n      \"com.softwaremill.sttp.tapir\"   %% \"tapir-json-circe\"          % V.tapir,\n      \"com.softwaremill.sttp.client3\" %% \"armeria-backend-cats\"      % V.sttp,\n      \"com.softwaremill.sttp.tapir\"   %% \"tapir-sttp-client\"         % V.tapir,\n      \"com.outr\"                      %% \"scribe-slf4j\"              % V.scribe,\n      \"com.github.pureconfig\" %% \"pureconfig-core\" % V.pureconfig,\n      \"com.typesafe\"           % \"config\"          % V.typesafeConfig,\n      \"org.web3j\"              % \"core\"            % V.web3J,\n      \"org.web3j\"              % \"contracts\"       % V.web3J,\n      \"com.squareup.okhttp3\" % \"logging-interceptor\" % V.okhttp3LoggingInterceptor,\n      \"com.github.jasync-sql\"  % \"jasync-mysql\" % V.jasync,\n      \"software.amazon.awssdk\" % \"kms\"          % V.awsSdk,\n    ),\n  )\n\n  lazy val archive = Seq(\n    libraryDependencies ++= Seq(\n      \"com.softwaremill.sttp.client3\" %% \"armeria-backend-cats\" % V.sttp,\n      \"com.outr\"                      %% \"scribe-slf4j\"         % V.scribe,\n      \"com.typesafe\" % \"config\" % V.typesafeConfig,\n    ),\n  )\n\n  lazy val api = Seq(\n    libraryDependencies ++= Seq(\n      \"org.typelevel\"                 %%% \"shapeless3-deriving\" % V.shapeless,\n      \"org.typelevel\"                 %%% \"cats-effect\"         % V.catsEffect,\n      \"com.softwaremill.sttp.tapir\"   %%% \"tapir-json-circe\"    % V.tapir,\n      \"com.softwaremill.sttp.client3\" %%% \"core\"                % V.sttp,\n    ),\n  )\n\n  lazy val lib = Seq(\n    libraryDependencies ++= Seq(\n      \"org.typelevel\"      %%% \"cats-effect\"         % V.catsEffect,\n      \"io.circe\"           %%% \"circe-generic\"       % V.circe,\n      \"io.circe\"           %%% \"circe-parser\"        % V.circe,\n      \"io.circe\"           %%% \"circe-refined\"       % V.circe,\n      \"eu.timepit\"         %%% \"refined\"             % V.refined,\n      \"io.github.iltotore\" %%% \"iron\"                % V.iron,\n      \"io.github.iltotore\" %%% \"iron-circe\"          % V.iron,\n      \"org.scodec\"         %%% \"scodec-bits\"         % V.scodecBits,\n      \"org.typelevel\"      %%% \"shapeless3-typeable\" % V.shapeless,\n      \"co.fs2\"             %%% \"fs2-core\"            % V.fs2,\n    ),\n  )\n\n  lazy val libJVM = Seq(\n    libraryDependencies ++= Seq(\n      \"org.bouncycastle\" % \"bcprov-jdk15on\" % V.bouncycastle,\n      \"com.outr\"        %% \"scribe-slf4j\"   % V.scribe,\n    ),\n  )\n\n  lazy val libJS = Seq(\n    libraryDependencies ++= Seq(\n      \"com.outr\" %%% \"scribe\" % V.scribe,\n    ),\n    Compile / npmDependencies ++= Seq(\n      \"js-sha3\"         -> V.jsSha3,\n      \"elliptic\"        -> V.elliptic,\n      \"@types/elliptic\" -> V.typesElliptic,\n    ),\n  )\n\n  lazy val catsEffectTests = Def.settings(\n    libraryDependencies ++= Seq(\n      \"org.typelevel\" %% \"munit-cats-effect\" % V.munitCatsEffect % Test,\n    ),\n    Test / fork := true,\n  )\n\n  lazy val tests = Def.settings(\n    libraryDependencies ++= Seq(\n      \"qa.hedgehog\"            %%% \"hedgehog-munit\"  % V.hedgehog   % Test,\n      \"com.opentable.components\" % \"otj-pg-embedded\" % V.pgEmbedded % Test,\n      \"org.flywaydb\"             % \"flyway-core\"     % V.flywayCore,\n    ),\n    Test / fork := true,\n  )\n\n  lazy val lmscanCommon = Seq(\n    libraryDependencies ++= Seq(\n      \"org.typelevel\"               %%% \"cats-effect\"           % V.catsEffect,\n      \"io.circe\"                    %%% \"circe-generic\"         % V.circe,\n      \"io.circe\"                    %%% \"circe-parser\"          % V.circe,\n      \"io.circe\"                    %%% \"circe-refined\"         % V.circe,\n      \"eu.timepit\"                  %%% \"refined\"               % V.refined,\n      \"com.softwaremill.sttp.tapir\" %%% \"tapir-core\"            % V.tapir,\n      \"com.softwaremill.sttp.tapir\" %%% \"tapir-json-circe\"      % V.tapir,\n      \"org.scodec\"                  %%% \"scodec-bits\"           % V.scodecBits,\n      \"co.fs2\"                      %%% \"fs2-core\"              % V.fs2,\n      \"io.getquill\"                  %% \"quill-jasync-postgres\" % V.quill,\n      \"com.outr\"                     %% \"scribe\"                % V.scribe,\n    ),\n  )\n\n  lazy val lmscanFrontend = Seq(\n    libraryDependencies ++= Seq(\n      \"io.indigoengine\" %%% \"tyrian-io\"      % V.tyrian,\n      \"qa.hedgehog\"     %%% \"hedgehog-munit\" % V.hedgehog % Test,\n    ),\n  )\n\n  lazy val lmscanBackend = Seq(\n    libraryDependencies ++= Seq(\n      \"com.softwaremill.sttp.tapir\"   %% \"tapir-armeria-server-cats\" % V.tapir,\n      \"com.softwaremill.sttp.client3\" %% \"armeria-backend-cats\"      % V.sttp,\n      \"com.typesafe\"             % \"config\"          % V.typesafeConfig,\n      \"org.postgresql\"           % \"postgresql\"      % V.postgres,\n      \"com.opentable.components\" % \"otj-pg-embedded\" % V.pgEmbedded,\n    ),\n  )\n\n  lazy val lmscanAgent = Seq(\n    libraryDependencies ++= Seq(\n      \"com.outr\"                      %% \"scribe-file\"     % V.scribe,\n      \"org.slf4j\"                      % \"slf4j-nop\"       % \"2.0.17\",\n      \"org.xerial\"                     % \"sqlite-jdbc\"     % V.sqlite,\n      \"com.softwaremill.sttp.client3\" %% \"fs2\"             % V.sttp,\n      \"com.github.pureconfig\"         %% \"pureconfig-core\" % V.pureconfig,\n      \"org.tpolecat\"                  %% \"doobie-core\"     % V.doobieVersion,\n      \"org.tpolecat\"                  %% \"doobie-postgres\" % V.doobieVersion,\n      \"org.tpolecat\"                  %% \"doobie-specs2\"   % V.doobieVersion,\n      \"org.tpolecat\"                  %% \"doobie-hikari\"   % V.doobieVersion,\n      \"com.zaxxer\"                     % \"HikariCP\"        % V.hikari,\n    ),\n  )\n\n  lazy val nodeProxy = Seq(\n    libraryDependencies ++= Seq(\n      \"com.outr\"    %% \"scribe-slf4j\" % V.scribe,\n      \"com.typesafe\" % \"config\"       % V.typesafeConfig,\n      \"com.softwaremill.sttp.tapir\"   %% \"tapir-armeria-server-cats\" % V.tapir,\n      \"com.softwaremill.sttp.client3\" %% \"armeria-backend-cats\"      % V.sttp,\n      \"io.circe\"                      %% \"circe-generic\"             % V.circe,\n      \"io.circe\"                      %% \"circe-parser\"              % V.circe,\n      \"io.circe\"                      %% \"circe-refined\"             % V.circe,\n      \"com.squareup.okhttp3\" % \"logging-interceptor\" % V.okhttp3LoggingInterceptor,\n      \"org.typelevel\" %% \"cats-effect\" % V.catsEffect,\n      \"co.fs2\"       %%% \"fs2-core\"    % V.fs2,\n    ),\n  )\n}\n\nThisBuild / organization := \"org.leisuremeta\"\nThisBuild / version      := \"0.0.1-SNAPSHOT\"\nThisBuild / scalaVersion := V.Scala\nThisBuild / scalafixDependencies += \"com.github.liancheng\" %% \"organize-imports\" % V.organiseImports\nThisBuild / semanticdbEnabled := true\n\nlazy val root = (project in file(\".\"))\n  .aggregate(\n    node,\n    api.jvm,\n    api.js,\n    lib.jvm,\n    lib.js,\n    archive,\n    bulkInsert,\n    ethGatewaySetup,\n    ethGatewayCommon,\n    ethGatewayDeposit,\n    ethGatewayWithdraw,\n    lmscanCommon.jvm,\n    lmscanCommon.js,\n    lmscanFrontend,\n    lmscanBackend,\n    lmscanAgent,\n    nodeProxy,\n  )\n\nlazy val node = (project in file(\"modules/node\"))\n  .settings(Dependencies.node)\n  .settings(Dependencies.tests)\n  .settings(Dependencies.catsEffectTests)\n  .settings(\n    name := \"leisuremeta-chain-node\",\n    assemblyMergeStrategy := {\n      case x if x `contains` \"reactor/core\" =>\n        MergeStrategy.first\n      case x if x `contains` \"io.netty.versions.properties\" =>\n        MergeStrategy.first\n      case PathList(\"META-INF\", \"native\", \"lib\", xs @ _*) =>\n        MergeStrategy.first\n      case x if x `contains` \"module-info.class\" => MergeStrategy.discard\n      case x =>\n        val oldStrategy = (ThisBuild / assemblyMergeStrategy).value\n        oldStrategy(x)\n    },\n  )\n  .dependsOn(api.jvm)\n\nlazy val ethGatewayCommon = (project in file(\"modules/eth-gateway-common\"))\n  .settings(Dependencies.ethGateway)\n  .settings(Dependencies.catsEffectTests)\n  .settings(\n    name := \"leisuremeta-chain-eth-gateway-common\",\n    Compile / compile / wartremoverErrors ++= Warts\n      .allBut(Wart.Any, Wart.AsInstanceOf, Wart.Nothing, Wart.Recursion),\n  )\n  .dependsOn(api.jvm)\n\nlazy val ethGatewaySetup = (project in file(\"modules/eth-gateway-setup\"))\n  .settings(Dependencies.ethGateway)\n  .settings(Dependencies.catsEffectTests)\n  .settings(\n    name := \"leisuremeta-chain-eth-gateway-setup\",\n  )\n  .dependsOn(ethGatewayCommon)\n\nlazy val ethGatewayDeposit = (project in file(\"modules/eth-gateway-deposit\"))\n  .settings(Dependencies.ethGateway)\n  .settings(Dependencies.tests)\n  .settings(\n    name := \"leisuremeta-chain-eth-gateway-deposit\",\n    assemblyMergeStrategy := {\n      case x if x `contains` \"okio.kotlin_module\" => MergeStrategy.first\n      case x if x `contains` \"io.netty.versions.properties\" =>\n        MergeStrategy.first\n      case x if x `contains` \"native/lib/libnetty-unix-common.a\" =>\n        MergeStrategy.first\n      case x if x `contains` \"module-info.class\" => MergeStrategy.discard\n      case x =>\n        val oldStrategy = (ThisBuild / assemblyMergeStrategy).value\n        oldStrategy(x)\n    },\n    Compile / compile / wartremoverErrors ++= Warts\n      .allBut(\n        Wart.Any,\n        Wart.AsInstanceOf,\n        Wart.Nothing,\n        Wart.Recursion,\n        Wart.SeqApply,\n      ),\n  )\n  .dependsOn(ethGatewayCommon)\n\nlazy val ethGatewayWithdraw = (project in file(\"modules/eth-gateway-withdraw\"))\n  .settings(Dependencies.ethGateway)\n  .settings(Dependencies.tests)\n  .settings(\n    name := \"leisuremeta-chain-eth-gateway-withdraw\",\n    assemblyMergeStrategy := {\n      case x if x `contains` \"okio.kotlin_module\" => MergeStrategy.first\n      case x if x `contains` \"io.netty.versions.properties\" =>\n        MergeStrategy.first\n      case x if x `contains` \"module-info.class\" => MergeStrategy.discard\n      case x =>\n        val oldStrategy = (ThisBuild / assemblyMergeStrategy).value\n        oldStrategy(x)\n    },\n    Compile / compile / wartremoverErrors ++= Warts\n      .allBut(\n        Wart.Any,\n        Wart.AsInstanceOf,\n        Wart.Nothing,\n        Wart.Recursion,\n        Wart.TripleQuestionMark,\n      ),\n  )\n  .dependsOn(ethGatewayCommon)\n\nlazy val archive = (project in file(\"modules/archive\"))\n  .settings(Dependencies.archive)\n  .settings(Dependencies.tests)\n  .settings(\n    name                 := \"leisuremeta-chain-archive\",\n    Compile / run / fork := true,\n    assemblyMergeStrategy := {\n      case x if x `contains` \"io.netty.versions.properties\" =>\n        MergeStrategy.first\n      case PathList(\"META-INF\", \"native\", \"lib\", xs @ _*) =>\n        MergeStrategy.first\n      case x if x `contains` \"module-info.class\" => MergeStrategy.discard\n      case x =>\n        val oldStrategy = (ThisBuild / assemblyMergeStrategy).value\n        oldStrategy(x)\n    },\n  )\n  .dependsOn(api.jvm)\n\nlazy val bulkInsert = (project in file(\"modules/bulk-insert\"))\n  .dependsOn(node)\n  .settings(\n    name                 := \"leisuremeta-chain-bulk-insert\",\n    Compile / run / fork := true,\n    excludeDependencies ++= Seq(\n      \"org.scala-lang.modules\" % \"scala-collection-compat_2.13\",\n      \"org.scala-lang.modules\" % \"scala-java8-compat_2.13\",\n    ),\n    assemblyMergeStrategy := {\n      case PathList(\"reactor\", \"core\", \"scheduler\", xs @ _*) =>\n        MergeStrategy.preferProject\n      case x if x `contains` \"libnetty-unix-common.a\" =>\n        MergeStrategy.first\n      case x if x `contains` \"io.netty.versions.properties\" =>\n        MergeStrategy.first\n      case PathList(\"META-INF\", \"native\", \"lib\", xs @ _*) =>\n        MergeStrategy.first\n      case PathList(\"reactor\", \"core\", \"scheduler\", xs @ _*) =>\n        MergeStrategy.first\n      case x if x `contains` \"module-info.class\" => MergeStrategy.discard\n      case x =>\n        val oldStrategy = (ThisBuild / assemblyMergeStrategy).value\n        oldStrategy(x)\n    },\n  )\n\nlazy val jvmClient = (project in file(\"modules/jvm-client\"))\n  .settings(Dependencies.jvmClient)\n  .dependsOn(node)\n  .settings(\n    name                 := \"leisuremeta-chain-jvm-client\",\n    Compile / run / fork := true,\n  )\n\nlazy val api = crossProject(JSPlatform, JVMPlatform)\n  .crossType(CrossType.Pure)\n  .in(file(\"modules/api\"))\n  .settings(Dependencies.api)\n  .settings(\n    scalacOptions ++= Seq(\n      \"-Xmax-inlines:64\",\n    ),\n    Compile / compile / wartremoverErrors ++= Warts.allBut(Wart.NoNeedImport),\n  )\n  .dependsOn(lib)\n\nlazy val lib = crossProject(JSPlatform, JVMPlatform)\n  .crossType(CrossType.Full)\n  .in(file(\"modules/lib\"))\n  .settings(Dependencies.lib)\n  .settings(Dependencies.tests)\n  .settings(\n    scalacOptions ++= Seq(\n      \"-Wconf:msg=Alphanumeric method .* is not declared infix:s\",\n    ),\n    Compile / compile / wartremoverErrors ++= Warts\n      .allBut(Wart.SeqApply, Wart.SeqUpdated),\n  )\n  .jvmSettings(Dependencies.libJVM)\n  .jsSettings(Dependencies.libJS)\n  .jsSettings(\n    useYarn := true,\n    Test / scalaJSLinkerConfig ~= {\n      _.withModuleKind(ModuleKind.CommonJSModule)\n    },\n    scalacOptions ++= Seq(\n      \"-scalajs\",\n    ),\n    Test / fork := false,\n  )\n  .jsConfigure { project =>\n    project\n      .enablePlugins(ScalaJSBundlerPlugin)\n      .enablePlugins(ScalablyTypedConverterPlugin)\n  }\n\nlazy val lmscanCommon = crossProject(JSPlatform, JVMPlatform)\n  .crossType(CrossType.Full)\n  .in(file(\"modules/lmscan-common\"))\n  .settings(Dependencies.lmscanCommon)\n  .settings(Dependencies.tests)\n  .jvmSettings(\n    scalacOptions ++= Seq(\n      \"-Xmax-inlines:64\",\n    ),\n    Test / fork := true,\n  )\n  .jsSettings(\n    useYarn := true,\n    scalaJSLinkerConfig ~= {\n      _.withModuleKind(ModuleKind.CommonJSModule)\n    },\n    scalacOptions ++= Seq(\n      \"-scalajs\",\n      \"-Xmax-inlines:64\",\n    ),\n    externalNpm := {\n      scala.sys.process.Process(\"yarn\", baseDirectory.value).!\n      baseDirectory.value\n    },\n    Test / fork := false,\n    // Compile / compile / wartremoverErrors ++= Warts.all,\n  )\n  .jsConfigure { project =>\n    project\n      .enablePlugins(ScalaJSBundlerPlugin)\n      .enablePlugins(ScalablyTypedConverterExternalNpmPlugin)\n  }\n\nlazy val lmscanFrontend = (project in file(\"modules/lmscan-frontend\"))\n  .enablePlugins(ScalaJSPlugin)\n  .enablePlugins(ScalablyTypedConverterExternalNpmPlugin)\n  .settings(Dependencies.lmscanFrontend)\n  .settings(\n    name := \"leisuremeta-chain-lmscan-frontend\",\n    scalaJSLinkerConfig ~= { _.withModuleKind(ModuleKind.CommonJSModule) },\n    externalNpm := {\n      scala.sys.process.Process(\"yarn\", baseDirectory.value).!\n      baseDirectory.value\n    },\n    scalacOptions ++= Seq(\n      \"-scalajs\",\n    ),\n  )\n  .dependsOn(lmscanCommon.js, api.js)\n\nlazy val lmscanBackend = (project in file(\"modules/lmscan-backend\"))\n  .enablePlugins(FlywayPlugin)\n  .settings(Dependencies.lmscanBackend)\n  .settings(Dependencies.tests)\n  .settings(\n    name := \"leisuremeta-chain-lmscan-backend\",\n    assemblyMergeStrategy := {\n      case PathList(\"scala\", \"tools\", \"asm\", xs @ _*) => MergeStrategy.first\n      case PathList(\"io\", \"getquill\", xs @ _*)        => MergeStrategy.first\n      case x if x `contains` \"io.netty.versions.properties\" =>\n        MergeStrategy.first\n      case x if x `contains` \"scala-asm.properties\" =>\n        MergeStrategy.first\n      case x if x `contains` \"compiler.properties\" =>\n        MergeStrategy.first\n      case x if x `contains` \"native/lib/libnetty-unix-common.a\" =>\n        MergeStrategy.first\n      case x if x `contains` \"module-info.class\" => MergeStrategy.discard\n      case x =>\n        val oldStrategy = (ThisBuild / assemblyMergeStrategy).value\n        oldStrategy(x)\n    },\n  )\n  .settings(\n    flywayUrl      := Settings.flywaySettings.url,\n    flywayUser     := Settings.flywaySettings.user,\n    flywayPassword := Settings.flywaySettings.pwd,\n    flywaySchemas  := Settings.flywaySettings.schemas,\n    flywayLocations ++= Settings.flywaySettings.locations,\n  )\n  .dependsOn(lmscanCommon.jvm)\n\nlazy val lmscanAgent = (project in file(\"modules/lmscan-agent\"))\n  .settings(Dependencies.lmscanAgent)\n  .settings(Dependencies.tests)\n  .settings(Dependencies.catsEffectTests)\n  .settings(\n    Compile / run / fork := true,\n  )\n  .settings(\n    name := \"leisuremeta-chain-lmscan-agent\",\n    assemblyMergeStrategy := {\n      case PathList(\"scala\", \"tools\", \"asm\", xs @ _*) => MergeStrategy.first\n      case PathList(\"io\", \"getquill\", xs @ _*)        => MergeStrategy.first\n      case x if x `contains` \"libnetty-unix-common.a\" =>\n        MergeStrategy.first\n      case x if x `contains` \"io.netty.versions.properties\" =>\n        MergeStrategy.first\n      case x if x `contains` \"scala-asm.properties\" =>\n        MergeStrategy.first\n      case x if x `contains` \"compiler.properties\" =>\n        MergeStrategy.first\n      case x if x `contains` \"module-info.class\" => MergeStrategy.discard\n      case x =>\n        val oldStrategy = (ThisBuild / assemblyMergeStrategy).value\n        oldStrategy(x)\n    },\n  )\n  .dependsOn(api.jvm)\n  .dependsOn(lmscanBackend)\n\nlazy val nodeProxy = (project in file(\"modules/node-proxy\"))\n  .settings(Dependencies.nodeProxy)\n  .settings(Dependencies.tests)\n  .settings(\n    name := \"leisuremeta-chain-node-proxy\",\n    assemblyMergeStrategy := {\n      case x if x `contains` \"okio.kotlin_module\" => MergeStrategy.first\n      case x if x `contains` \"io.netty.versions.properties\" =>\n        MergeStrategy.first\n      case x if x `contains` \"native/lib/libnetty-unix-common.a\" =>\n        MergeStrategy.first\n      case x if x `contains` \"module-info.class\" => MergeStrategy.discard\n      case x =>\n        val oldStrategy = (ThisBuild / assemblyMergeStrategy).value\n        oldStrategy(x)\n    },\n  )\n  .dependsOn(api.jvm)\n"
  },
  {
    "path": "docs/LeisureMeta_Chain_API.md",
    "content": "# LeisureMeta Chain API\n\n\n\n## API with Top Priority\n\n`GET` **/balance/{accountName}** 계정 잔고 조회\n\n> `param` movable: 잔고의 이동 가능성 여부\n>\n> * 'free': 유동 자산\n> * 'locked': 예치 자산\n\n*  Response: Map[TokenDefinitionID, BalanceInfo]\n  * Token Definition ID: 토큰 정의 ID (string)\n  * BalanceInfo\n    * Total Amount: 해당 토큰 총 금액/수량 (NFT의 경우 랜덤박스 갯수)\n    * Map[TxHash, Tx]: 사용하지 않은 트랜잭션 해시 목록\n\n`GET` **/nft-balance/{accountName}** 계정 NFT 잔고 조회\n\n> `param` *(optional)* movable: 잔고의 이동 가능성 여부\n>\n> * 'free': 유동 자산\n> * 'locked': 예치 자산\n> * 'all': 전체 자산\n\n*  Response: Map[TokenID, NftBalanceInfo]\n  *  NftBalanceInfo\n    *  TokenDefinitionId\n    *  TxHash\n    *  Tx\n\n`GET` **/activity/account/{account}** 계정 활동내역 조회\n\n* Response: Seq[ActivityInfo]\n  * ActivityInfo\n    * timestamp\n    * point\n    * description\n    * txHash\n\n\n`GET` **/activity/token/{tokenId}** 토큰이 받은 활동내역 조회\n\n* Response: Seq[ActivityInfo]\n  * ActivityInfo\n    * timestamp\n    * point\n    * description\n    * txHash\n\n\n`GET` **/reward/{accountName}** 보상 조회\n\n> `param` *(optional)* timestamp: 기준 시점. 없으면 가장 최근 보상. (월요일 0시 ~ 일요일 23시59분 주기)\n>\n> `param` *(optional)* dao-account: 마스터 다오 계정. 없으면  `DAO-M` 사용\n>\n> `param` *(optional)* reward-amount: 리워드 총량. 없으면 마스터 다오 계정의 현재 LM 밸런스. \n\n* Response:\n  * account: 계정이름 \n  * reward: 보상\n    * total: 총 보상량\n    * activity: 활동보상\n    * token: 토큰이 받은 사용자액션 보상\n    * rarity: 보유 토큰의 희귀도에 따른 보상\n    * bonus: 모더레이터인 경우 주어지는 추가 보상 총합 \n  * point: 활동내역에 따르는 보상 포인트(1/1000 포인트 단위의 정수)\n    * activity: 활동 내역.\n      * like: 좋아요\n      * comment: 댓글\n      * share: 공유\n      * report: 신고\n    * token: 토큰이 받은 내역\n      * like: 좋아요\n      * comment: 댓글\n      * share: 공유\n      * report: 신고\n    * rarity: Map[String, Number] 희귀도에 따르는 포인트\n  * timestamp: 기준 시점\n  * totalNumberOfDao: 시스템에 개설된 다오 총 수\n\n\n`GET`  **/dao/{groupID}** 특정 그룹의 DAO 정보 조회\n\n* Response: DaoInfo DAO 정보\n  * DaoInfo 현재까지 정해진 필드값들\n    * NumberOfModerator: 모더레이터 숫자\n\n`GET`  **/owners/{definitionID}** 특정 컬렉션 NFT들의 보유자 정보 조회\n\n* Response:\n  * Map[TokenID, AccountName]\n\n`GET`  **/snapshot/ownership/**  받을 토큰 소유보상 점수 조회\n\n> `param` *(optional)* from: 조회를 시작할 token id. 주어지지 않으면 \"\"\n>\n> `param` *(optional)* limit: 조회할 총 갯수. 디폴트값은 100\n\n* Response: [TokenId, OwnershipSnapshot]\n  * OwnershipSnapshot\n    * account\n    * timestamp 기준 시점\n    * point 포인트. 일반적으론 해당 NFT의 Rarity 점수\n    * definitionId 보상받을 토큰 종류. 일반적으론 LM\n    * amount 보상량\n\n`GET`  **/rewarded/ownership/{tokenID}**  최근에 받은 토큰 소유보상 조회\n\n* Response: OwnershipRewardLog\n  * OwnershipRewardLog\n    * OwnershipShapshot\n    * ExecuteReward TxHash\n\n`GET`  **/creator-dao/{daoID}** 특정 크리에이터 DAO 정보 조회\n\n* Response: CreatorDaoInfo 크리에이터 DAO 정보\n  * CreatorDaoInfo\n    * id: CreatorDaoId\n    * name: Utf8\n    * description: Utf8\n    * founder: Account\n    * coordinator: Account\n    * moderators: Set[Account]\n\n`GET`  **/creator-dao/{daoID}/member** 특정 크리에이터 DAO의 멤버 목록 조회\n\n> `param` *(optional)* from(Account): 기준 계정\n>\n> `param` *(optional)* limit: 갯수 제한. 기본값 100\n>\n\n* Response: Seq[Account] 회원 목록\n\n`POST` **/tx** 트랜잭션 제출\n\n* 아래의 트랜잭션 목록 참조\n* Array로 한 번에 여러개의 트랜잭션 제출 가능\n\n### Blockchain Explorer 지원용 API\n\n`GET` **/status** 블록체인 현재상태 조회 (최신 블록 hash, 블록 number 포함)\n\n`GET` **/block **블록 목록 조회\n\n> `param` *(optional)* from: 찾기 시작할 블록 해시. 없으면 최신 블록\n>\n> `param` *(optional)* limit: 가져올 블록 갯수. 디폴트 50.\n\n`GET` **/block/{blockHash}** 블록 상세정보 조회 (포함된 트랜잭션 해시 목록 포함)\n\n`GET` **/tx** 특정 블록에 포함된 트랜잭션 목록 조회\n\n> `param`  block: 찾을 블록 해시\n\n`GET` **/tx/{transactionHash}** 트랜잭션 상세정보 조회\n\n### Response HTTP Status Codes\n\n* 요청했을 때 해당 내용이 없는 경우: 404 Not Found\n* 서명이 올바르지 않은 경우: 401 Unauthorized\n* 트랜잭션이 invalid한 경우: 400 Bad Request\n* 블록체인 노드 내부 오류: 500 Internal Server Error\n\n## Transactions\n\n* 모든 트랜잭션 공통 필드\n  * \"networkId\": 다른 네트워크에 똑같은 트랜잭션을 보내는 것을 막기 위한 필드. \n  * \"createdAt\": 트랜잭션 생성시각\n* Format\n  * 서명주체\n  * Fields: 트랜잭션을 제출할 때 포함시켜야 하는 필드 목록\n  * *(optional)* Computed Fields: 블록에 기록될 때 노드에 의해 덧붙여지는 필드들\n\n\n### Account\n\n* CreateAccount 계정 생성\n  * > 사용자 서명\n  * Fields\n    * account: Account 계정 이름\n    * ethAddress: *(optional)* 이더리움 주소\n    * guardian: *(optional)* Account\n      * 계정에 공개키를 추가할 수 있는 권한을 가진 계정 지정. 일반적으로는 `playnomm`\n  \n  * Example (private key `b229e76b742616db3ac2c5c2418f44063fcc5fcc52a08e05d4285bdb31acba06` 으로 서명한 예시)\n  \n  ```json\n  [\n    {\n      \"sig\" : {\n        \"sig\" : {\n          \"v\" : 28,\n          \"r\" : \"495c3bcc143eea328c11b7ec55069dd4fb16c26463999f9dbc085094c3b59423\",\n          \"s\" : \"707a75e433abd208cfb76d4e0cdbc04b1ce2389e3a1f866348ef2e3ea5785e93\"\n        },\n        \"account\" : \"alice\"\n      },\n      \"value\" : {\n        \"AccountTx\" : {\n          \"CreateAccount\" : {\n            \"networkId\" : 1000,\n            \"createdAt\" : \"2020-05-22T09:00:00Z\",\n            \"account\" : \"alice\",\n            \"ethAddress\" : null,\n            \"guardian\" : null\n          }\n        }\n      }\n    }\n  ]\n  ```\n  \n  ```json\n  [\"822380e575e482e829fc9f45ffd0f99f4f0987ccbec0c0a5de5fd640f42a9100\"]\n  ```\n\n* CreateAccountWithExternalChainAddresses 외부 블록체인 주소를 가진 계정 생성\n  * > 사용자 서명\n  * Fields\n    * account: Account 계정 이름\n    * externalChainAddresses: 외부 블록체인 주소. 현재는 `eth`, `sol` 두 가지 지원.\n    * guardian: *(optional)* Account\n      * 계정에 공개키를 추가할 수 있는 권한을 가진 계정 지정. 일반적으로는 `playnomm`\n    * memo: *(optional)* 메모\n    \n  * Example (private key `b229e76b742616db3ac2c5c2418f44063fcc5fcc52a08e05d4285bdb31acba06` 으로 서명한 예시)\n  \n  ```json\n  [\n    {\n      \"sig\" : {\n        \"sig\" : {\n          \"v\" : 28,\n          \"r\" : \"2f7a53986a387961047566ab8d31fcdbbe6cc96529cdbfccb68fb268700f2bdf\",\n          \"s\" : \"56f993f2cca6a5f7410e04a5aa849c7698f1e0966d98b90638d7472cd9eb3210\"\n        },\n        \"account\" : \"alice\"\n      },\n      \"value\" : {\n        \"AccountTx\" : {\n          \"CreateAccountWithExternalChainAddresses\" : {\n            \"networkId\" : 2021,\n            \"createdAt\" : \"2023-01-11T19:01:30Z\",\n            \"account\" : \"bob\",\n            \"externalChainAddresses\" : {\n              \"eth\" : \"99f681d29754aeee1426ef991b745a4f662e620c\"\n            },\n            \"guardian\" : \"alice\",\n            \"memo\" : null\n          }\n        }\n      }\n    }\n  ```\n  \n  ```json\n  [\"d795dc9205ec5ecb3097fe0ca0326e6597c6ecae497a0876b8cc3737d823264a\"]\n  ```\n  \n* UpdateAccount 계정 생성\n  * > 사용자 서명 혹은 Guardian 서명\n  * Fields\n    * account: Account 계정 이름\n    * ethAddress: *(optional)* 이더리움 주소\n    * guardian: *(optional)* Account\n      * 계정에 공개키를 추가할 수 있는 권한을 가진 계정 지정. 일반적으로는 `playnomm`\n\n  * Example\n  \n  ```json\n  [\n    {\n      \"sig\" : {\n        \"sig\" : {\n          \"v\" : 28,\n          \"r\" : \"22c14ac6fbdce52c256640f1e36851ef901ea1b5cfebc3a430283a89df99bc11\",\n          \"s\" : \"3474ebcc861c2d31a60d363356c4c89c196d450432b33bedadfb94d66edf2ffd\"\n        },\n        \"account\" : \"alice\"\n      },\n      \"value\" : {\n        \"AccountTx\" : {\n          \"UpdateAccount\" : {\n            \"networkId\" : 1000,\n            \"createdAt\" : \"2020-05-22T09:00:00Z\",\n            \"account\" : \"alice\",\n            \"ethAddress\" : \"0xefD277f6da7ac53e709392044AE98220Df142753\",\n            \"guardian\" : null\n          }\n        }\n      }\n    }\n  ]\n  ```\n  \n  ```json\n  [\"7730dadeff5be3bfd63fdec8853d6301a5ec0e3b8c815a4d7e0ba20e8c52517d\"]\n  ```\n\n\n* UpdateAccountWithExternalChainAddresses 계정 생성\n  * > 사용자 서명 혹은 Guardian 서명\n  * Fields\n    * account: Account 계정 이름\n    * externalChainAddresses: 외부 블록체인 주소. 현재는 `eth`, `sol` 두 가지 지원.\n    * guardian: *(optional)* Account\n      * 계정에 공개키를 추가할 수 있는 권한을 가진 계정 지정. 일반적으로는 `playnomm`\n    * memo: *(optional)* 메모\n\n  * Example\n  \n  ```json\n  [\n    {\n      \"sig\" : {\n        \"sig\" : {\n          \"v\" : 27,\n          \"r\" : \"bf7dfb91669233e120a07707084f2b9879d9620cd297096f862d52d53fe9988d\",\n          \"s\" : \"10d652ba45a81ad572aeae6c25ffd2e6339e8f049d379ae24d5a947f531d5d65\"\n        },\n        \"account\" : \"alice\"\n      },\n      \"value\" : {\n        \"AccountTx\" : {\n          \"UpdateAccountWithExternalChainAddresses\" : {\n            \"networkId\" : 2021,\n            \"createdAt\" : \"2023-01-11T19:01:40Z\",\n            \"account\" : \"bob\",\n            \"externalChainAddresses\" : {\n              \"eth\" : \"99f681d29754aeee1426ef991b745a4f662e620c\"\n            },\n            \"guardian\" : \"alice\",\n            \"memo\" : \"bob updated\"\n          }\n        }\n      }\n    }\n  ]\n  ```\n  \n  ```json\n  [\"a0e28414a97b3fabb49cf3b77757219b8e1a7205ba3cc4f5e618019b36bc38c3\"]\n  ```\n  \n  \n* AddPublicKeySummaries 계정에 사용할 공개키요약 추가\n  * > 사용자 서명 혹은 Guardian 서명\n  \n  * Fields\n    * account: Account 계정 이름\n    * summaries: Map[PublicKeySummary, String]\n      * 추가할 공개키요약과 간단한 설명\n      * 만약 설명이 `\"permanant\"` 인 경우 해당 public key summary 는 유효기간 없이 무제한 사용\n    \n  * Result\n    * Removed: Map[PublicKeySummary, Descrption(string)]\n  \n  * Example\n  \n  ```json\n  [\n    {\n      \"sig\" : {\n        \"sig\" : {\n          \"v\" : 27,\n          \"r\" : \"816df20e4ff581fd2056689b48be73cca29e4f81977e5c42754e598757434c51\",\n          \"s\" : \"4e43aef8d836e79380067365cd7a4a452df5f52b73ec78463bdc7cdea2e11ca0\"\n        },\n        \"account\" : \"alice\"\n      },\n      \"value\" : {\n        \"AccountTx\" : {\n          \"AddPublicKeySummaries\" : {\n            \"networkId\" : 1000,\n            \"createdAt\" : \"2020-05-22T09:00:00Z\",\n            \"account\" : \"alice\",\n            \"summaries\" : {\n              \"5b6ed47b96cd913eb938b81ee3ea9e7dc9affbff\" : \"another key\"\n            }\n          }\n        }\n      }\n    }\n  ]\n  ```\n  \n  ```json\n  [\"e996dcbabcf8a86208bcc8d683778f5d6b5d1b8ff950c9e60cc72b66fc619cca\"]\n  ```\n  \n  \n  \n* RemovePublicKeySummaries 계정에 사용할 공개키요약 삭제\n  * > 사용자 서명 혹은 Guardian 서명\n  * Fields\n    * Account: AccountName (string)\n    * Summaries: Set[PublicKeySummary]\n  \n* RemoveAccount 계정 삭제\n  * > 사용자 서명 혹은 Guardian 서명\n  * Fields\n    * Account: AccountName (string)\n\n\n### Group\n\n* CreateGroup 그룹 생성\n  * > Coordinator 서명\n  * Fields\n    * GroupID(string)\n    * Name: GroupName(string)\n    * Coordinator: AccountName(string)\n      * 그룹 조정자. 그룹에 계정 추가, 삭제 및 그룹 해산 권한을 가짐\n  \n  * Example\n  \n  ```json\n  [\n    {\n      \"sig\" : {\n        \"sig\" : {\n          \"v\" : 28,\n          \"r\" : \"aab6f7ccc108b8e75601c726d43270c1a60f38f830136dfe293a2633dc86a0dd\",\n          \"s\" : \"3cc1b610df7a421f9ae560853d5f07005a20c6ad225a00861a76e5e91aa183c0\"\n        },\n        \"account\" : \"alice\"\n      },\n      \"value\" : {\n        \"GroupTx\" : {\n          \"CreateGroup\" : {\n            \"networkId\" : 1000,\n            \"createdAt\" : \"2022-06-08T09:00:00Z\",\n            \"groupId\" : \"mint-group\",\n            \"name\" : \"mint group\",\n            \"coordinator\" : \"alice\"\n          }\n        }\n      }\n    }\n  ]\n  ```\n  \n  ```json\n  [\"adb9440aeef2de4697774657ebbcce9c1e5b01423e0a21da90da355458400c75\"]\n  ```\n  \n  \n  \n* DisbandGroup 그룹 해산\n  * > Coordinator 서명\n  * Fields\n    * GroupID(string)\n  \n* AddAccounts 그룹에 계정 추가\n  * > Coordinator 서명\n  * Fields\n    * GroupID(string)\n    * Accounts: Set[AccountName(string)]\n\n  * Example\n  \n  ```json\n  [\n    {\n      \"sig\" : {\n        \"sig\" : {\n          \"v\" : 28,\n          \"r\" : \"2dd00a2ebf07ff2d09d6e9bcd889ddc775c17989827e3e19b5e8d1744c021466\",\n          \"s\" : \"05bd60fef3d45463e22e5c157c814a7cbd1681410b67b0233c97ce7116d60729\"\n        },\n        \"account\" : \"alice\"\n      },\n      \"value\" : {\n        \"GroupTx\" : {\n          \"AddAccounts\" : {\n            \"networkId\" : 1000,\n            \"createdAt\" : \"2022-06-08T09:00:00Z\",\n            \"groupId\" : \"mint-group\",\n            \"accounts\" : [\n              \"alice\",\n              \"bob\"\n            ]\n          }\n        }\n      }\n    }\n  ]\n  \n  ```\n  \n  ```json\n  [\"015a8cced717ca40a528d9518e8494961a4c4e7fde1422304b751814ed181e00\"]\n  ```\n  \n  \n  \n* RemoveAccounts 그룹에 계정 삭제\n  * > Coordinator 서명\n  * Fields\n    * GroupID(string)\n    * Accounts: Set[AccountName(string)]\n\n* ReplaceCoordinator 그룹 조정자 변경\n  * > Coordinator 서명\n  * Fields\n    * GroupID(string)\n    * NewCoordinator: AccountName(string)\n\n\n### Token\n\n* DefineToken 토큰 정의. Fungible Token, NFT 공히 사용한다. (랜덤박스 포함)\n  * > MinterGroup에 속한 Account의 서명\n  * Fields\n    * definitionId: TokenDefinitionID(string)\n    * name: String\n    * *(optional)* Symbol(string)\n    * *(optional)* MinterGroup: GroupID(string) 신규토큰발행 권한을 가진 그룹\n    * *(optional)* NftInfo\n      * minter: AccountName(string)\n      * rarity: Map[(Rarity(string), Weight]\n      * *(optional)* DataUrl(string)\n      * *(optional)* ContentHash: uint256\n\n  * Example\n\n  ```json\n  [\n    {\n      \"sig\" : {\n        \"sig\" : {\n          \"v\" : 28,\n          \"r\" : \"ce2b48b7da96eef22a2b92170fb81865adb99cbcae99a2b81bb7ce9b4ba990b6\",\n          \"s\" : \"35a708c9ffc1b7ef4e88389255f883c96e551a404afc4627e3f6ca32a617bae6\"\n        },\n        \"account\" : \"alice\"\n      },\n      \"value\" : {\n        \"TokenTx\" : {\n          \"DefineToken\" : {\n            \"networkId\" : 1000,\n            \"createdAt\" : \"2020-05-22T09:01:00Z\",\n            \"definitionId\" : \"test-token\",\n            \"name\" : \"test-token\",\n            \"symbol\" : \"TT\",\n            \"minterGroup\" : \"mint-group\",\n            \"nftInfo\" : {\n              \"Some\" : {\n                \"value\" : {\n                  \"minter\" : \"alice\",\n                  \"rarity\" : {\n                    \"LGDY\" : 8,\n                    \"UNIQ\" : 4,\n                    \"EPIC\" : 2,\n                    \"RARE\" : 1\n                  },\n                  \"dataUrl\" : \"https://www.playnomm.com/data/test-token.json\",\n                  \"contentHash\" : \"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef\"\n                }\n              }\n            }\n          }\n        }\n      }\n    }\n  ]\n  ```\n\n  ```json\n  [\"b0cfd8da5ef347762b60162c772148902b54abca4760fb53e3eb752f8b953664\"]\n  ```\n\n\n* DefineTokenWithPrecision Precision이 있는 토큰 정의. Fungible Token, NFT 공히 사용한다. (랜덤박스 포함)\n  * > MinterGroup에 속한 Account의 서명\n  * Fields\n    * definitionId: TokenDefinitionID(string)\n    * name: String\n    * *(optional)* Symbol(string)\n    * *(optional)* MinterGroup: GroupID(string) 신규토큰발행 권한을 가진 그룹\n    * *(optional)* NftInfo\n      * minter: AccountName(string)\n      * rarity: Map[(Rarity(string), Weight]\n      * precision: int 소숫점 자릿수\n      * *(optional)* DataUrl(string)\n      * *(optional)* ContentHash: uint256\n\n  * Example\n\n  ```json\n  [\n    {\n      \"sig\" : {\n        \"sig\" : {\n          \"v\" : 27,\n          \"r\" : \"74a1fa40be985b0c9bcf92df0262317a336f585fa24e261780b2ab6ff89d3f6a\",\n          \"s\" : \"4cea4a8ad18df36a2c140366f8afee36441757921aa351fedf0b53d82307e9c2\"\n        },\n        \"account\" : \"alice\"\n      },\n      \"value\" : {\n        \"TokenTx\" : {\n          \"DefineTokenWithPrecision\" : {\n            \"networkId\" : 2021,\n            \"createdAt\" : \"2023-01-11T19:01:00Z\",\n            \"definitionId\" : \"nft-with-precision\",\n            \"name\" : \"NFT with precision\",\n            \"symbol\" : \"NFTWP\",\n            \"minterGroup\" : \"mint-group\",\n            \"nftInfo\" : {\n              \"Some\" : {\n                \"value\" : {\n                  \"minter\" : \"alice\",\n                  \"rarity\" : {\n                    \"LGDY\" : 100,\n                    \"UNIQ\" : 66,\n                    \"EPIC\" : 33,\n                    \"RARE\" : 10\n                  },\n                  \"precision\" : 2,\n                  \"dataUrl\" : \"https://www.playnomm.com/data/nft-with-precision.json\",\n                  \"contentHash\" : \"2475a387f22c248c5a3f09cea0ef624484431c1eaf8ffbbf98a4a27f43fabc84\"\n                }\n              }\n            }\n          }\n        }\n      }\n    }\n  ]\n  ```\n\n  ```json\n  [\"6d49236405972c01322db054338da2c7ab6fd9662d2a64c9bc1ab4026da9fb8f\"]\n  ```\n\n* MintFungibleToken\n  * > MinterGroup에 속한 Account의 서명\n  * Fields\n    * TokenDefinitionID(string)\n    * Outputs: Map[AccountName, Amount]\n\n  * Example\n\n  ```json\n  [\n    {\n      \"sig\" : {\n        \"sig\" : {\n          \"v\" : 28,\n          \"r\" : \"76fb1b3be81101638c9ce070628db035ad7d86d3363d664da0c5afe254494e90\",\n          \"s\" : \"7ffb1c751fe4f5341c75341e4a51373139a7f730a56a08078ac89b6e1a77fc76\"\n        },\n        \"account\" : \"alice\"\n      },\n      \"value\" : {\n        \"TokenTx\" : {\n          \"MintFungibleToken\" : {\n            \"networkId\" : 1000,\n            \"createdAt\" : \"2020-05-22T09:01:00Z\",\n            \"definitionId\" : \"test-token\",\n            \"outputs\" : {\n              \"alice\" : 100\n            }\n          }\n        }\n      }\n    }\n  ]\n  ```\n\n  ```json\n  [\"a3f35adb3d5d08692a7350e61aaa28da992a4280ad8e558953898ef96a0051ca\"]\n  ```\n\n* BurnFungibleToken\n\n  * MinterGroup에 속한 Account의 서명\n\n  * Fields\n    * definitionId: TokenDefinitionId\n\n    * amount\n\n    * Inputs: Set[Signed.TxHash]\n\n  * Result\n    * outputAmount\n\n* MintNFT\n\n  * > MinterGroup에 속한 Account의 서명\n  * Fields\n    * TokenDefinitionID(string)\n    * TokenID(string)\n    * Rarity(string)\n    * DataUrl(string)\n    * ContentHash: uint256\n    * Output: AccountName\n\n  * Example\n\n  ```json\n  [\n    {\n      \"sig\" : {\n        \"sig\" : {\n          \"v\" : 27,\n          \"r\" : \"0a914259cc0e8513512ea6356fc3056efe104e84756cf23a6c1c1aff7a580613\",\n          \"s\" : \"71a15b331b9e7337a018b442ee978a15f0d86e71ca53d2f54a9a8ccb92646cf9\"\n        },\n        \"account\" : \"alice\"\n      },\n      \"value\" : {\n        \"TokenTx\" : {\n          \"MintNFT\" : {\n            \"networkId\" : 1000,\n            \"createdAt\" : \"2022-06-08T09:00:00Z\",\n            \"tokenDefinitionId\" : \"test-token\",\n            \"tokenId\" : \"2022061710000513118\",\n            \"rarity\" : \"EPIC\",\n            \"dataUrl\" : \"https://d3j8b1jkcxmuqq.cloudfront.net/temp/collections/TEST_NOMM4/NFT_ITEM/F7A92FB1-B29F-4E6F-BEF1-47C6A1376D68.jpg\",\n            \"contentHash\" : \"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef\",\n            \"output\" : \"alice\"\n          }\n        }\n      }\n    }\n  ]\n  \n  ```\n\n  ```json\n  [\"6040003b0020245ce82f352bed95dee2636442efee4e5a15ee3911c67910b657\"]\n  ```\n\n* MintNFTWithMemo\n\n  * > MinterGroup에 속한 Account의 서명\n  * Fields\n    * TokenDefinitionID(string)\n    * TokenID(string)\n    * Rarity(string)\n    * DataUrl(string)\n    * ContentHash: uint256\n    * Output: AccountName\n\n  * Example\n\n  ```json\n  [\n    {\n      \"sig\" : {\n        \"sig\" : {\n          \"v\" : 28,\n          \"r\" : \"d1c7f699ff24b4767e3728f79b13d3d930fa1be02cb511481010fbbaecf538c0\",\n          \"s\" : \"298829e3f5b03d4b3f87766b655eb3632099f6ea737e5e0d02da6ba03fcd72dd\"\n        },\n        \"account\" : \"alice\"\n      },\n      \"value\" : {\n        \"TokenTx\" : {\n          \"MintNFTWithMemo\" : {\n            \"networkId\" : 2021,\n            \"createdAt\" : \"2023-01-11T19:05:00Z\",\n            \"tokenDefinitionId\" : \"nft-with-precision\",\n            \"tokenId\" : \"2022061710000513118\",\n            \"rarity\" : \"EPIC\",\n            \"dataUrl\" : \"https://d3j8b1jkcxmuqq.cloudfront.net/temp/collections/TEST_NOMM4/NFT_ITEM/F7A92FB1-B29F-4E6F-BEF1-47C6A1376D68.jpg\",\n            \"contentHash\" : \"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef\",\n            \"output\" : \"alice\",\n            \"memo\" : \"Test Minting NFT #2022061710000513118\"\n          }\n        }\n      }\n    }\n  ]\n  ```\n  \n  ```json\n  [\"018edc66aa45e303a2621e5a981c2a2ed5f262802498888814a1844c04b12bd3\"]\n  ```\n  \n* BurnNFT\n  * > 토큰 소유자 서명\n  * Fields\n    * TokenDefinitionID(string)\n    * Input: SignedTxHash\n\n* UpdateNFT\n\n\n  * > MinterGroup 에 속한 account 서명\n\n  * Fields\n    * TokenDefinitionID(string)\n\n  * Example\n\t```json\n\t[\n\t  {\n\t    \"sig\": {\n\t      \"sig\": {\n\t        \"v\": 28,\n\t        \"r\": \"1ec82ef3e977dd8e6857e6d77b7955e57bc8d7081730198372f4740c588f0c80\",\n\t        \"s\": \"65031c7011d8aceae4bfbd90049b2bb4c458050988368b3ea3017fb7402c0c03\"\n\t      },\n\t      \"account\": \"alice\"\n\t    },\n\t    \"value\": {\n\t      \"TokenTx\": {\n\t        \"UpdateNFT\": {\n\t          \"networkId\": 2021,\n\t          \"createdAt\": \"2023-01-11T19:06:00Z\",\n\t          \"tokenDefinitionId\": \"nft-with-precision\",\n\t          \"tokenId\": \"2022061710000513118\",\n\t          \"rarity\": \"EPIC\",\n\t          \"dataUrl\": \"https://d3j8b1jkcxmuqq.cloudfront.net/temp/collections/TEST_NOMM4/NFT_ITEM/F7A92FB1-B29F-4E6F-BEF1-47C6A1376D68.jpg\",\n\t          \"contentHash\": \"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef\",\n\t          \"output\": \"alice\",\n\t          \"memo\": \"Test Updating NFT #2022061710000513118\"\n\t        }\n\t      }\n\t    }\n\t]\n\t```\n\n  ```json\n  [\"e4d85bd90857a9be1e363a10c6543de0a7826966378e2bdb0195572a87e7c1be\"]\n  ```\n\n* TransferFungibleToken\n  * > 토큰 보유자 서명\n  * Fields\n    * TokenDefinitionID(string)\n    * Inputs: Set[SignedTxHash]: UTXO Hash, 모든 토큰 종류는 동일해야 함\n    * Outputs: Map[AccountName, Amount]\n    * *(optional)* Memo(string)\n\n  * Example\n\n  ```json\n  [\n    {\n      \"sig\" : {\n        \"sig\" : {\n          \"v\" : 28,\n          \"r\" : \"09a5f46d29bd8598f04cb6db32627aadd562e30e181135c2898594080db6aa79\",\n          \"s\" : \"340abd1b6618d3bbf4b586294a4f902942f597672330563a43591a14be0a6504\"\n        },\n        \"account\" : \"alice\"\n      },\n      \"value\" : {\n        \"TokenTx\" : {\n          \"TransferFungibleToken\" : {\n            \"networkId\" : 1000,\n            \"createdAt\" : \"2022-06-09T09:00:00Z\",\n            \"tokenDefinitionId\" : \"test-token\",\n            \"inputs\" : [\n              \"a3f35adb3d5d08692a7350e61aaa28da992a4280ad8e558953898ef96a0051ca\"\n            ],\n            \"outputs\" : {\n              \"bob\" : 10,\n              \"alice\" : 90\n            },\n            \"memo\" : \"transfer from alice to bob\"\n          }\n        }\n      }\n    }\n  ]\n  ```\n\n  ```json\n  [\"cb3848af6eb3c006c8aa663711d5fcfa2d6b1ccdcaf9837e273a96cc5386785e\"]\n  ```\n\n  \n\n* TransferNFT\n  * > 토큰 보유자 서명\n  * Fields\n    * TokenDefinitionID(string)\n    * TokenID(string)\n    * Input: SignedTxHash\n    * Output: AccountName\n    * *(optional)* Memo(string)\n\n  * Example\n\n  ```json\n  [\n    {\n      \"sig\" : {\n        \"sig\" : {\n          \"v\" : 27,\n          \"r\" : \"c443ed5eda3d484bcda7bf77f030d3f6c20e4130d9bc4e03ca75df3074b40239\",\n          \"s\" : \"2e7a19f1baee2099ccbef500e7ceb03c5053957a55085ef52b21c022c43242d9\"\n        },\n        \"account\" : \"alice\"\n      },\n      \"value\" : {\n        \"TokenTx\" : {\n          \"TransferNFT\" : {\n            \"networkId\" : 1000,\n            \"createdAt\" : \"2022-06-09T09:00:00Z\",\n            \"definitionId\" : \"test-token\",\n            \"tokenId\" : \"2022061710000513118\",\n            \"input\" : \"6040003b0020245ce82f352bed95dee2636442efee4e5a15ee3911c67910b657\",\n            \"output\" : \"bob\",\n            \"memo\" : null\n          }\n        }\n      }\n    }\n  ]\n  ```\n\n  ```json\n  [\"1e46633eb70ec8ea484aeb0ef2e7916021b4fcc591712c4ce0514c63c897c6c9\"]\n  ```\n\n* EntrustFungibleToken 토큰 위임\n  * > 토큰 보유자 서명\n\n  * Fields\n\n    * definitionId: TokenDefinitionId 맡길 토큰 종류\n    * amount: 맡길 토큰 수량 \n    * inputs: Set[SignedTxHash] 입력에 사용할 트랜잭션 해시값.\n    * to: Account 위임할 계정. 일반적으로 playnomm\n\n  * Results\n\n    * remainder: Amount 자신에게 돌아오는 수량\n\n  * Example\n\n    ```json\n    [\n      {\n        \"sig\" : {\n          \"sig\" : {\n            \"v\" : 27,\n            \"r\" : \"8d438670820bb788f0ef7106aa55c5fa2fa9c898eaded4d92f29d3c21a99c127\",\n            \"s\" : \"1545783ca442a5ae2fdd347c79286a1c62256cd91ac76cb392f28dc190ac9c8a\"\n          },\n          \"account\" : \"alice\"\n        },\n        \"value\" : {\n          \"TokenTx\" : {\n            \"EntrustFungibleToken\" : {\n              \"networkId\" : 1000,\n              \"createdAt\" : \"2022-06-09T09:00:00Z\",\n              \"definitionId\" : \"test-token\",\n              \"amount\" : 1000,\n              \"inputs\" : [\n                \"a3f35adb3d5d08692a7350e61aaa28da992a4280ad8e558953898ef96a0051ca\"\n              ],\n              \"to\" : \"alice\"\n            }\n          }\n        }\n      }\n    ]\n    ```\n\n    ```json\n    [\"45df6a88e74ea44f2d759251fed5a3c319e7cf9c37fafa7471418fec7b26acce\"]\n    ```\n\n    \n\n* EntrustNFT NFT 위임\n\n  * > NFT 보유자 서명\n\n  * Fields\n\n    * definitionId(string)\n    * tokenId(string)\n    * input: SignedTxHash\n    * to: Account 위임할 계정. 일반적으로 playnomm\n\n  * Example\n\n    ```json\n    [\n      {\n        \"sig\" : {\n          \"sig\" : {\n            \"v\" : 27,\n            \"r\" : \"05705f380f7a7fbad853094f69ff1527703476be30d2ac19f90a24a7900100c0\",\n            \"s\" : \"37fac4695829b188ebe3d8238259a212ba52588c4593a51ef81631ab9ab90581\"\n          },\n          \"account\" : \"alice\"\n        },\n        \"value\" : {\n          \"TokenTx\" : {\n            \"EntrustNFT\" : {\n              \"networkId\" : 1000,\n              \"createdAt\" : \"2020-06-09T09:00:00Z\",\n              \"definitionId\" : \"test-token\",\n              \"tokenId\" : \"2022061710000513118\",\n              \"input\" : \"6040003b0020245ce82f352bed95dee2636442efee4e5a15ee3911c67910b657\",\n              \"to\" : \"alice\"\n            }\n          }\n        }\n      }\n    ]\n    ```\n\n    ```json\n    [\"10cb0802f3dfc85abb502bad260120a424fc583016db84d384904c1c0a580955\"]\n    ```\n\n    \n\n* DisposeEntrustedFungibleToken 위임된 토큰 처분\n\n  * 위임받은 계정(일반적으로 playnomm) 서명\n\n  * Fields\n    * definitionID(string)\n    * inputs: Set[SignedTxHash]: EntrustFungibleToken 트랜잭션의 UTXO Hash\n    * outputs: Map[AccountName, Amount]\n      * 토큰을 받아갈 계정과 받아갈 양. 비어 있으면 전체를 원주인에게 반환한다.\n\n  * Example\n\n    ```json\n    [\n      {\n        \"sig\" : {\n          \"sig\" : {\n            \"v\" : 28,\n            \"r\" : \"fb6c99c0e26da04e8dc0855ea629708a17a8deabfabb5a488ba9faa001c4a31f\",\n            \"s\" : \"7de70d3fd15176451e46856af2dbedf05e58d7cfc0bfb0e0fac1b6d06550f5d3\"\n          },\n          \"account\" : \"alice\"\n        },\n        \"value\" : {\n          \"TokenTx\" : {\n            \"DisposeEntrustedFungibleToken\" : {\n              \"networkId\" : 1000,\n              \"createdAt\" : \"2020-06-10T09:00:00Z\",\n              \"definitionId\" : \"test-token\",\n              \"inputs\" : [\n                \"45df6a88e74ea44f2d759251fed5a3c319e7cf9c37fafa7471418fec7b26acce\"\n              ],\n              \"outputs\" : {\n                \"bob\" : 1000\n              }\n            }\n          }\n        }\n      }\n    ]\n    ```\n\n    ```json\n    [\"377fef6a1d85707bb7d84c9b3f5f2a2e409ce57084fbb15a6b200a1237d04119\"]\n    ```\n\n    \n\n* DisposeEntrustedNFT 위임된 NFT 처분\n\n  * 위임받은 계정(일반적으로 playnomm) 서명\n\n  * Fields\n    * definitionID(string)\n    * tokenID(string)\n    * input: SignedTxHash\n    * output: Option[AccountName]\n      * NFT를 받아갈 계정. 없으면 원주인에게로 반환한다.\n\n  * Example\n\n    ```json\n    [\n      {\n        \"sig\" : {\n          \"sig\" : {\n            \"v\" : 28,\n            \"r\" : \"a03080b98925010e241783482e83a5fdfc25343406564a4e3fc4e6b2535657d3\",\n            \"s\" : \"1de0ede5ebeba4aea455094ac1b58fc24ad943f0a5422a93f60a4f2b8b59b982\"\n          },\n          \"account\" : \"alice\"\n        },\n        \"value\" : {\n          \"TokenTx\" : {\n            \"DisposeEntrustedNFT\" : {\n              \"networkId\" : 1000,\n              \"createdAt\" : \"2020-06-10T09:00:00Z\",\n              \"definitionId\" : \"test-token\",\n              \"tokenId\" : \"2022061710000513118\",\n              \"input\" : \"10cb0802f3dfc85abb502bad260120a424fc583016db84d384904c1c0a580955\",\n              \"output\" : \"bob\"\n            }\n          }\n        }\n      }\n    ]\n    ```\n\n    ```json\n    [\"83c783f31b95cc4a713a921ec1df0725c6675b999ba6285a70c1f777615e4281\"]\n    ```\n\n* CreateSnapshots\n\n  * > MinterGroup에 속한 Account의 서명\n\n  * Fields\n    * definitionID(string)\n    * tokenID(string)\n    * definitionIds: Set[TokenDefinitionId]\n    * *(optional)* Memo(string)\n\n  * Example\n\n    ```json\n    [\n      {\n        \"sig\" : {\n          \"sig\" : {\n            \"v\" : 27,\n            \"r\" : \"2a771418871b3fcfa43a0b00821fce6d9ecec40a1cf2c5ebff4489377c7d0f01\",\n            \"s\" : \"640b2af02ee4a713d22d2e16e0acd2c61ee1195aa16254cc6481a926d772d866\"\n          },\n          \"account\" : \"alice\"\n        },\n        \"value\" : {\n          \"TokenTx\" : {\n            \"CreateSnapshots\" : {\n              \"networkId\" : 2021,\n              \"createdAt\" : \"2023-01-11T19:09:00Z\",\n              \"definitionIds\" : [\n                \"LM\",\n                \"nft-with-precision\"\n              ],\n              \"memo\" : \"Snapshot for NFT\"\n            }\n          }\n        }\n      }\n    ]\n    ```\n\n    ```json\n    [\"e9fecfafd40e655ac761730bcbb9be524f39370ffa9a272f875275d6cdc50818\"]\n    ```\n\n### Reward\n\n* RegisterDao 신규 DAO 등록. Group은 미리 생성해 두어야 한다.\n  * > Group Coordinator 서명. 일반적으로는 `playnomm`\n\n  * Fields\n    * GroupId(string)\n    * DaoAccountName(string)\n      * 다오 보상 충전용 계정. 여기에 들어온 금액을 매주 정해진 룰에 따라 보상한다. Unique account이어야 한다.\n    * Moderators: Set[Account]\n      * 최초 모더레이터 목록\n\n  * Example\n\n    ```json\n    [\n      {\n        \"sig\" : {\n          \"sig\" : {\n            \"v\" : 27,\n            \"r\" : \"d4b2d1cfe009e0e5b6dea67779fd898a7f1718e7b1869b5b36b6daacc68e88f6\",\n            \"s\" : \"42d8c69e964109ceab5996abdbc59d53661904e6b56337599e9c5beebe665d51\"\n          },\n          \"account\" : \"alice\"\n        },\n        \"value\" : {\n          \"RewardTx\" : {\n            \"RegisterDao\" : {\n              \"networkId\" : 1000,\n              \"createdAt\" : \"2020-06-09T09:00:00Z\",\n              \"groupId\" : \"sample-dao-group-id\",\n              \"daoAccountName\" : \"sample-dao-group-account\",\n              \"moderators\" : [\n                \"alice\"\n              ]\n            }\n          }\n        }\n      }\n    ]\n    ```\n\n    ```json\n    [\"dabd1e1603805080722c6397568e6fc4ef384736a2bf95bc52e0f53acd43bea3\"]\n    ```\n\n    \n\n* UpdateDao DAO 정보 업데이트. 그룹 조정자가 업데이트 권한을 갖는다.\n  * > Group Coordinator 서명. 일반적으로는 `playnomm`\n\n  * Fields\n    * GroupId(string)\n    * Moderators: Set[Account]\n      * 모더레이터 목록\n\n* RecordActivity 활동정보 추가. 그룹 조정자가 업데이트 권한을 갖는다.\n\n  * > Group Coordinator 서명. 일반적으로는 `playnomm`\n\n  * Fields\n\n    * timestamp: 기준시점\n    * userActivity: Map[AccountName, Seq[DaoActivity]] 사용자활동 요약 정보\n\n      * DaoActivity 활동정보\n        * point 총 점수\n        * description 어떤 활동으로 받은 점수인지 간략한 표시\n    * tokenReceived: Map[TokenId, Seq[DaoActivity]] 토큰이 받은 사용자활동 요약정보\n      * DaoActivity 활동정보\n        * point 총 점수\n        * description 어떤 활동으로 받은 점수인지 간략한 표시\n\n  * Example\n\n    ```json\n    [\n      {\n        \"sig\" : {\n          \"sig\" : {\n            \"v\" : 27,\n            \"r\" : \"95aff6586d03fa7c66165d9bb49f2a2fd54650f2776c728401c664622d5e2d4c\",\n            \"s\" : \"2cff82c55822d3266add84ea5853dbc86cf47f24e5787080b76e58681477ba09\"\n          },\n          \"account\" : \"alice\"\n        },\n        \"value\" : {\n          \"RewardTx\" : {\n            \"RecordActivity\" : {\n              \"networkId\" : 2021,\n              \"createdAt\" : \"2023-01-10T18:01:00Z\",\n              \"timestamp\" : \"2023-01-09T09:00:00Z\",\n              \"userActivity\" : {\n                \"bob\" : [\n                  {\n                    \"point\" : 3,\n                    \"description\" : \"like\"\n                  }\n                ],\n                \"carol\" : [\n                  {\n                    \"point\" : 3,\n                    \"description\" : \"like\"\n                  }\n                ]\n              },\n              \"tokenReceived\" : {\n                \"text-20230109-0000\" : [\n                  {\n                    \"point\" : 2,\n                    \"description\" : \"like\"\n                  }\n                ],\n                \"text-20230109-0001\" : [\n                  {\n                    \"point\" : 2,\n                    \"description\" : \"like\"\n                  }\n                ],\n                \"text-20230109-0002\" : [\n                  {\n                    \"point\" : 2,\n                    \"description\" : \"like\"\n                  }\n                ]\n              }\n            }\n          }\n        }\n      }\n    ]    \n    ```\n    \n    ```json\n    [\"f08043c06fa17ffaf5c86121db683f5aa879bbf0194de3cac703b0572feaa4cd\"]\n    ```\n    \n    \n\n* OfferReward 보상 제공. TransferFungibleToken과 같은 형태로 보상을 실행한다\n\n  * >  보상을 보낼 계정\n\n  * Fields\n\n    * TokenDefinitionID(string)\n    * Inputs: Set[SignedTxHash]: UTXO Hash, 모든 토큰 종류는 동일해야 함\n    * Outputs: Map[AccountName, Amount]\n    * *(optional)* Memo(string)\n\n  * Example\n\n    ```json\n    \n    ```\n    \n    ```json\n    \n    ```\n\n* BuildSnapshot: 보상을 위한 스냅샷 생성. 사용자가 한 활동, 토큰이 받은 활동, 토큰 소유보상의 세 가지 스냅샷을 동시에 만든다\n\n  * > 보상 실행 주체. 일반적으로 Playnomm\n\n  * Fields\n\n    * timestamp: 보상 기준 시점. 이 시점 일주일 전부터 현재 시점까지의 자료를 모아 스냅샷을 생성한다.\n    * accountAmount: 계정활동 총 보상량\n    * tokenAmount: 토큰이 받을 총 보상량\n    * ownershipAmount: 토큰 보유에 따르는 총 보상량\n\n  * Example\n\n    ```json\n    [\n      {\n        \"sig\" : {\n          \"sig\" : {\n            \"v\" : 28,\n            \"r\" : \"004b940e651bb950350157116fbfedf5ec98eed68068cea2b666a9e2b52b9588\",\n            \"s\" : \"17eb8460877a7d212fac4a59caf7abf1cb96c145f5cae41a8ffce55df226f003\"\n          },\n          \"account\" : \"alice\"\n        },\n        \"value\" : {\n          \"RewardTx\" : {\n            \"BuildSnapshot\" : {\n              \"networkId\" : 2021,\n              \"createdAt\" : \"2023-01-11T18:01:00Z\",\n              \"timestamp\" : \"2023-01-09T09:00:00Z\",\n              \"accountAmount\" : 0,\n              \"tokenAmount\" : 0,\n              \"ownershipAmount\" : 100000000000000000000000\n            }\n          }\n        }\n      }\n    ]\n    ```\n    \n    ```json\n    [\"da140a6816e9437c0583b34f64636ba9b3fca02721f2ff90b03460c061067cfa\"]\n    ```\n    \n    \n    \n\n* ExecuteOwnershipReward: 스냅샷의 자료를 기반으로 토큰 소유 보상 실행.\n\n  * 보상 실행 주체. 일반적으로 Playnomm\n\n  * Fields\n\n    * definitionId 보상에 지급할 토큰 정의 ID. 일반적으로 LM.\n    * inputs: Set[TxHash] 보상에 사용할 UTXO\n    * targets: Set[TokenId] 보상할 개별 NFT 토큰 ID\n    \n  * Results\n  \n    * outputs: Map[Account, Amount] 각 계정별 보상결과\n  \n  * Example\n  \n    ```json\n    [\n      {\n        \"sig\" : {\n          \"sig\" : {\n            \"v\" : 27,\n            \"r\" : \"2289a570405738a66d75c1eeae451f899cbcc3bd7fd98b4b4d5aaf807c965211\",\n            \"s\" : \"0364409abf9829ae5ca38b9c31ee0bcc5ce4dabcff3a5d0be180dd925ec51096\"\n          },\n          \"account\" : \"alice\"\n        },\n        \"value\" : {\n          \"RewardTx\" : {\n            \"ExecuteOwnershipReward\" : {\n              \"networkId\" : 2021,\n              \"createdAt\" : \"2023-01-11T18:01:00Z\",\n              \"inputs\" : [\n                \"270650f92f584d9dbbffb99f3a915dc908fbea28bc3dbf34b8cdbe49c4070611\"\n              ],\n              \"targets\" : [\n                \"1234567890\",\n                \"1234567891\"\n              ]\n            }\n          }\n        }\n      }\n    ]\n    ```\n    \n    ```json\n    [\"c7824fd901b71918f10663a2990988b3a933353aebc5d1b80f39d78ce43be1ca\"]\n    ```\n  \n\n### AgendaTx\n\n* SuggestSimpleAgenda 투표 의제 제안.\n\n  * >  투표 의제를 제안할 계정. 일반적으로 playNomm\n\n  * Fields\n\n\t\t* title(string)\n    * votingToken: TokenDefinitionId(string) 일반적으로 LM\n    * voteStart: Instant\n    * voteEnd: Instant\n    * voteOption: Map[String, String]\n\n  * Example\n\n    ```json\n    [\n      {\n        \"sig\" : {\n          \"sig\" : {\n            \"v\" : 27,\n            \"r\" : \"dc6e9660b33fdc71b14675e7a7a888fe32e4b3bb6264a3a4e90f572518e53aa8\",\n            \"s\" : \"069a1c0c2c602f2342384d545aab17e5dd2629efc9f8605dead14df599b5fc96\"\n          },\n          \"account\" : \"alice\"\n        },\n        \"value\" : {\n          \"AgendaTx\" : {\n            \"SuggestSimpleAgenda\" : {\n              \"networkId\" : 2021,\n              \"createdAt\" : \"2023-01-11T18:01:00Z\",\n              \"title\" : \"Let the world know about LeisureMeta!\",\n              \"votingToken\" : \"LM\",\n              \"voteStart\" : \"2023-01-11T18:01:00Z\",\n              \"voteEnd\" : \"2023-01-12T18:01:00Z\",\n              \"voteOptions\" : {\n                \"1\" : \"Yes\",\n                \"2\" : \"No\"\n              }\n            }\n          }\n        }\n      }\n    ]\n    ```\n    \n    ```json\n    [\"2475a387f22c248c5a3f09cea0ef624484431c1eaf8ffbbf98a4a27f43fabc84\"]\n    ```\n\n* VoteSimpleAgenda 투표.\n\n  * >  투표하는 사용자계정\n\n  * Fields\n\n\t\t* agendaTxHash: 투표할 SuggestSimpleAgenda 트랜잭션의 tx hash\n    * selectedOption: 투표내용\n    \n  * Example\n  \n    ```json\n    [\n      {\n        \"sig\" : {\n          \"sig\" : {\n            \"v\" : 28,\n            \"r\" : \"89a108a5a933a8d04486384dc90521d0ca5faba1d3a09524068c22936aa2b5ea\",\n            \"s\" : \"2347e77fa7d1a4f6d10712bb7c5cfb1746f0aef65825dbf03f201fe5e594ee2f\"\n          },\n          \"account\" : \"alice\"\n        },\n        \"value\" : {\n          \"AgendaTx\" : {\n            \"VoteSimpleAgenda\" : {\n              \"networkId\" : 2021,\n              \"createdAt\" : \"2023-01-11T19:01:00Z\",\n              \"agendaTxHash\" : \"2475a387f22c248c5a3f09cea0ef624484431c1eaf8ffbbf98a4a27f43fabc84\",\n              \"selectedOption\" : \"1\"\n            }\n          }\n        }\n      }\n    ]\n    ```\n    \n    ```json\n    [\"07dd86c19884881e1ef037eac4553b735545c03612c5fe368a07189464ad154b\"]\n    ```\n\n## Other API\n\n| Method | URL                               | Description                      |\n| ------ | --------------------------------- | -------------------------------- |\n| `GET`  | **/account/{accountName}**        | 계정정보 조회                    |\n| `GET`  | **/eth/{ethAddress}**             | 이더리움 주소와 연동된 계정 조회 |\n| `GET`  | **/dao**                          | DAO 목록 조회                    |\n| `GET`  | **/group/{groupID}**              | 그룹 정보 조회                   |\n| `GET`  | **/offering/{offeringID}**        | Offering 정보 조회               |\n| `GET`  | **/status**                       | 블록체인 상태 조회               |\n| `GET`  | **/token-def/{definitionID}**     | 토큰 정의 정보 조회              |\n| `GET`  | **/token/{tokenID}**              | 토큰 정보 조회                  |\n| `GET`  | **/token-hist/{txHash}**          | 토큰 과거 정보 조회                  |\n| `GET`  | **/snapshot/account/{account}**   | 보상받을 활동 조회               |\n| `GET`  | **/snapshot/token/{tokenID}**     | 보상받을 토큰 점수 조회          |\n| `GET`  | **/snapshot/ownership/{tokenID}** | 받을 토큰 소유보상 점수 조회     |\n| `GET`  | **/rewarded/account/{account}**   | 최근에 받은 활동보상 조회        |\n| `GET`  | **/rewarded/token/{tokenID}**     | 최근에 받은 토큰보상 조회        |\n| `GET`  | **/rewarded/ownership/{tokenID}** | 최근에 받은 토큰 소유보상 조회   |\n| `GET` | **/snapshot-state/{definitionID}** | 토큰정의 스냅샷 상태 조회 |\n| `GET` | **/snapshot-balance/ {Account}/{TokenDefinitionID}/{SnapshotID}** | 토큰 스냅샷 잔고 조회 |\n| `GET` | **/nft-snapshot-balance/ {Account}/{TokenDefinitionID}/{SnapshotID}** | NFT 스냅샷 잔고 조회 |\n\n\n\n## State\n\nMerkle Trie로 관리되는 블록체인 내부 상태들. 키가 사전식으로 정렬되어 있어서 순회 가능하고, StateRoot로 요약가능하다.\n\n### Account\n\n* NameState: AccountName => AccountData\n  * AccountData\n    * *(optional)* ethAddress\n    * *(optional)* guardian (account)\n\n* AccountKeyState: (AccountName, PublicKeySummary) => Desription\n  * Description에는 추가된 시각이 포함되어 있어야 함\n\n### Group\n\n* GroupState: GroupID => GroupInfo\n  * GroupInfo\n    * Group Name\n    * Coordinator\n* GroupAccountState: (GroupID, AccountName) => ()\n\n\n### Token\n\n* TokenDefinitionState: TokenDefinitionID(string)=> TokenDefinition\n  * TokenDefinition\n    * TokenDefinitionID(string)\n    * Name(string)\n    * *(optional)* Symbol(string)\n    * *(optional)* AdminGroup: GroupId\n    * TotalAmount\n    * *(optional)* NftInfo\n      * Minter: AccountName(string)\n      * Rarity: Map[(Rarity(string), Weight)]\n      * DataUrl(string)\n      * ContentHash: uint256\n* NftState: TokenID => NftState\n  * NftState\n    * TokenID\n    * TokenDefinitionID\n    * Rarity\n    * Weight\n    * CurrentOwner: Account\n* RarityState: (TokenDefinitionID, Rarity, TokenID) => ()\n* FungibleBalanceState: (AccountName, TokenDefinitionID, TransactionHash) => ()\n* NftBalanceState: (AccountName, TokenID, TransactionHash) => ()\n* EntrustFungibleBalanceState: (AccountName, AccountName, TokenDefinitionId, TransactionHash) => ()\n* EntrustNftBalanceState: (AccountName, AccountName, TokenId, TransactionHash) => ()\n\n### Reward\n\n* DaoState: GroupID => DaoInfo\n  * DaoInfo\n    * Moderators: Set[AccountName]\n* AccountActivityState: (Account, Instant) => Seq[ActivityLog]\n  * ActivityLog\n    * account 포인트를 획득한 계정\n    * point 총 점수\n    * description 묘사\n    * txHash 근거가 되는 RecordActivity 트랜잭션 해시값\n\n* TokenReceivedState: (TokenId, Instant) => Seq[ActivityLog]\n* AccountSnapshotState: (Account) => ActivitySnapshot\n  * ActivitySnapshot\n    * account\n    * from: Instant\n    * to: Instant\n    * point 총 포인트\n    * definitionId 보상받을 토큰 종류. 일반적으론 LM\n    * amount 보상량\n    * backlog: Set[TxHash] 해당 카운트의 근거 RecordActivity의 집합\n* TokenSnapshotState: (TokenId) => ActivitySnapshot\n* OwnershipSnapshotState: (TokenId) => OwnershipSnapshot\n  * OwnershipSnapshot\n    * account\n    * timestamp 기준 시점\n    * point 포인트. 일반적으론 해당 NFT의 Rarity 점수\n    * definitionId 보상받을 토큰 종류. 일반적으론 LM\n    * amount 보상량\n* AccountRewardedState: (Account) => ActivityRewardLog\n  * ActivityRewardLog\n    * ActivitySnapshot\n    * ExecuteReward  TxHash\n* TokenRewardedState: (TokenId) => ActivityRewardLog\n* OwnershipRewardedState: (TokenId) => OwnershipRewardLog\n  * OwnershipRewardLog\n    * OwnershipShapshot\n    * ExecuteReward TxHash\n\n"
  },
  {
    "path": "docs/api_with_example.md",
    "content": "# LeisureMeta Chain API with Example\n\n`POST` **/txhash** 트랜잭션 해시값 계산\n\n아직 해시값 계산 모듈을 제공하지 않으므로, 여기에 트랜잭션을 보내면 해시값을 계산해준다. 나온 해시값에 서명해서 정식으로 트랜잭션을 집어넣으면 된다.\n\n```json\n[\n\t{\n  \t\"AccountTx\" : {\n    \t\"CreateAccount\" : {\n      \t\"networkId\" : 1000,\n      \t\"createdAt\" : \"2020-05-22T09:00:00Z\",\n      \t\"account\" : \"alice\",\n      \t\"guardian\" : null\n    \t}\n  \t}\n\t}\n]\n```\n\n```json\n[\"396fb3ef2ecdb800126027a802e26eb2e7e1d47fee28f24287fb836cdafc6f1e\"]\n```\n\n\n\n`POST`**/tx** 트랜잭션 제출\n\n(Private Key `b229e76b742616db3ac2c5c2418f44063fcc5fcc52a08e05d4285bdb31acba06`으로 서명한 예시)\n\n```json\n[\n  {\n    \"sig\" : {\n      \"sig\" : {\n        \"v\" : 27,\n        \"r\" : \"c0cf8bb197d5f0a562fd76200f09480f676f31970e982f65bc1efd707504ef73\",\n        \"s\" : \"7ad50c3987ce4a9007d093d25caaf701436824dafc6290d8e477b8f1c8b6771d\"\n      },\n      \"account\" : \"alice\"\n    },\n    \"value\" : {\n      \"AccountTx\" : {\n        \"CreateAccount\" : {\n          \"networkId\" : 1000,\n          \"createdAt\" : \"2020-05-22T09:00:00Z\",\n          \"account\" : \"alice\",\n          \"guardian\" : null\n        }\n      }\n    }\n  }\n]\n```\n\n```json\n[\"396fb3ef2ecdb800126027a802e26eb2e7e1d47fee28f24287fb836cdafc6f1e\"]\n```\n\n\n\n`GET` **/status** 노드 현재상태조회\n\n트랜잭션 하나가 들어가서 블록이 하나 생성되었으므로 block number 값이 1이 된다.\n\n(추후 트랜잭션이 없는 빈 블록 하나를 더 찍어서 거래완결을 표시할 예정)\n\n```json\n{\n  \"networkId\": 1000,\n  \"genesisHash\": \"50f1634b0534d9eaff9bb4084b38839f710b5822a599a10c3b106a19a4315127\",\n  \"bestHash\": \"a9735ba3420e7d9be5f26b28b035f3141d4586e1015c237a67aa46c90a65b8ca\",\n  \"number\": 1\n}\n```\n\n\n\n`GET` **/block**/a9735ba3420e7d9be5f26b28b035f3141d4586e1015c237a67aa46c90a65b8ca 블록 정보 조회\n\nbest hash값을 넣어서 최신 블록의 정보를 조회한다\n\n```json\n{\n  \"header\": {\n    \"number\": 1,\n    \"parentHash\": \"50f1634b0534d9eaff9bb4084b38839f710b5822a599a10c3b106a19a4315127\",\n    \"stateRoot\": {\n      \"account\": {\n        \"namesRoot\": \"7a3a362149605d574b2eccdf85a0bfe7ca579fd2cfa0a2c19a2b601731d5ddbd\",\n        \"keyRoot\": null\n      }\n    },\n    \"transactionsRoot\": \"962dfb46a6439d48efd72e1a21356911f1f5882843c76a3c2b2a2709d44b25eb\",\n    \"timestamp\": \"2022-05-29T18:13:54.425Z\"\n  },\n  \"transactionHashes\": [\n    \"396fb3ef2ecdb800126027a802e26eb2e7e1d47fee28f24287fb836cdafc6f1e\"\n  ],\n  \"votes\": [\n    {\n      \"v\": 28,\n      \"r\": \"f6d37c7994cb9f5f84b2a100c2346d6a0aec7e48e14872096cd32a90dc3c43ec\",\n      \"s\": \"52ad670b041581c3d1d246fb09df36b8a1201db76f4cb2113e0759e16541be20\"\n    }\n  ]\n}\n```\n\n\n\n\n\n`GET` **/tx**/396fb3ef2ecdb800126027a802e26eb2e7e1d47fee28f24287fb836cdafc6f1e 트랜잭션 정보 조회\n\n블록에 기록된 트랜잭션 해시값 하나에 대한 정보를 취득한다\n\n```json\n{\n  \"signedTx\": {\n    \"sig\": {\n      \"sig\": {\n        \"v\": 27,\n        \"r\": \"c0cf8bb197d5f0a562fd76200f09480f676f31970e982f65bc1efd707504ef73\",\n        \"s\": \"7ad50c3987ce4a9007d093d25caaf701436824dafc6290d8e477b8f1c8b6771d\"\n      },\n      \"account\": \"alice\"\n    },\n    \"value\": {\n      \"AccountTx\": {\n        \"CreateAccount\": {\n          \"networkId\": 1000,\n          \"createdAt\": \"2020-05-22T09:00:00Z\",\n          \"account\": \"alice\",\n          \"guardian\": null\n        }\n      }\n    }\n  },\n  \"result\": null\n}\n```\n\n\n\n`GET` **/account**/alice 계정정보 조회\n\n```json\n{\n  \"guardian\": null,\n  \"publicKeySummaries\": {\n    \"99f681d29754aeee1426ef991b745a4f662e620c\": {\n      \"description\": \"Automatically added in account creation\",\n      \"addedAt\": \"2020-05-22T09:00:00Z\"\n    }\n  }\n}\n```\n\n \n"
  },
  {
    "path": "docs/creator-dao-documentation.md",
    "content": "# Creator Dao\n\n## DAO 정보\n* id(Utf8): DAO ID\n* name(Utf8): 이름\n* description(Utf8): 설명\n\n## DAO 참여자\n* Founder 창립자\n  * 크리에이터 본인\n  * DAO에 관한 모든 권한\n* Coordinator 시스템 관리자\n  * playNomm 계정으로 세팅\n  * DAO에 관한 모든 권한\n* Moderator 일반 관리자\n  * 해산 제외한 나머지 관리권한\n* Member 참여자\n  * 표결 참여\n* Applicant\n  * 가입 신청한 사람\n* 그 외\n  * 가입신청\n\n## 액션\n* Coordinator만 가능\n  * ReplaceCoordinator\n* Founder, Coordinator만 가능\n  * DAO 개설\n  * 관리자 임명, 해임, DAO 해산\n* Moderator 이상 가능\n  * DAO 정보변경\n  * 회원 가입, 탈퇴, 안건 발의\n* Member 이상 가능\n  * 표결 참여\n* 그 외\n  * Dao 가입신청\n\n## 트랜잭션\n\n### CreateCreatorDao (개설)\nFounder나 Coordinator가 새로운 DAO를 만든다.\n\n```json\n{\n  \"sig\": {\n    \"sig\": {\n      \"v\": 27,\n      \"r\": \"62d7c7ddf8bea783b8ed59906b2f5db00b9e53031d6407933d7c4a80c7157f35\",\n      \"s\": \"2d546c7d0f0fdf058e5bdf74b39cb2d3db34aa1dcdd6b2a76ea6504655b12b0f\"\n    }\n    \"account\": \"founder\",\n  },\n  \"value\": {\n    \"CreatorDaoTx\": {\n      \"CreateCreatorDao\": {\n        \"networkId\": 102,\n        \"createdAt\": \"2024-03-15T09:28:41.339Z\",\n        \"id\": \"dao_001\",\n        \"name\": \"Art Creators DAO\",\n        \"description\": \"A DAO for digital art creators\",\n        \"founder\": \"creator001\",\n        \"coordinator\": \"playnomm\"\n      }\n    }\n  }\n}\n```\n\n### UpdateCreatorDao (정보변경)\nModerator 이상 권한을 가진 사용자가 DAO 정보를 수정한다.\n\n```json\n{\n  \"sig\": {\n    \"sig\": {\n      \"v\": 27,\n      \"r\": \"72d7c7ddf8bea783b8ed59906b2f5db00b9e53031d6407933d7c4a80c7157f35\",\n      \"s\": \"3d546c7d0f0fdf058e5bdf74b39cb2d3db34aa1dcdd6b2a76ea6504655b12b0f\"\n    },\n    \"account\": \"moderator\"\n  },\n  \"value\": {\n    \"CreatorDaoTx\": {\n      \"UpdateCreatorDao\": {\n        \"networkId\": 102,\n        \"createdAt\": \"2024-03-15T10:28:41.339Z\",\n        \"id\": \"dao_001\",\n        \"name\": \"Digital Art Creators DAO\",\n        \"description\": \"A DAO for digital art creators and collectors\"\n      }\n    }\n  }\n}\n```\n\n### DisbandCreatorDao (해산)\nFounder나 Coordinator만 DAO를 해산할 수 있다.\n\n```json\n{\n  \"sig\": {\n    \"sig\": {\n      \"v\": 27,\n      \"r\": \"82d7c7ddf8bea783b8ed59906b2f5db00b9e53031d6407933d7c4a80c7157f35\",\n      \"s\": \"4d546c7d0f0fdf058e5bdf74b39cb2d3db34aa1dcdd6b2a76ea6504655b12b0f\"\n    },\n    \"account\": \"founder\"\n  },\n  \"value\": {\n    \"CreatorDaoTx\": {\n      \"DisbandCreatorDao\": {\n        \"networkId\": 102,\n        \"createdAt\": \"2024-03-15T11:28:41.339Z\",\n        \"id\": \"dao_001\"\n      }\n    }\n  }\n}\n```\n\n### ReplaceCoordinator (코디네이터 변경)\n현재 Coordinator만 새로운 Coordinator를 지정할 수 있다.\n\n```json\n{\n  \"sig\": {\n    \"sig\": {\n      \"v\": 27,\n      \"r\": \"92d7c7ddf8bea783b8ed59906b2f5db00b9e53031d6407933d7c4a80c7157f35\",\n      \"s\": \"5d546c7d0f0fdf058e5bdf74b39cb2d3db34aa1dcdd6b2a76ea6504655b12b0f\"\n    },\n    \"account\": \"coordinator\"\n  },\n  \"value\": {\n    \"CreatorDaoTx\": {\n      \"ReplaceCoordinator\": {\n        \"networkId\": 102,\n        \"createdAt\": \"2024-03-15T12:28:41.339Z\",\n        \"id\": \"dao_001\",\n        \"newCoordinator\": \"playnomm2\"\n      }\n    }\n  }\n}\n```\n\n### AddMembers (참여자 추가)\nModerator 이상 권한을 가진 사용자가 새로운 멤버를 추가할 수 있다.\n\n```json\n{\n  \"sig\": {\n    \"sig\": {\n      \"v\": 27,\n      \"r\": \"a2d7c7ddf8bea783b8ed59906b2f5db00b9e53031d6407933d7c4a80c7157f35\",\n      \"s\": \"6d546c7d0f0fdf058e5bdf74b39cb2d3db34aa1dcdd6b2a76ea6504655b12b0f\"\n    },\n    \"account\": \"moderator\"\n  },\n  \"value\": {\n    \"CreatorDaoTx\": {\n      \"AddMembers\": {\n        \"networkId\": 102,\n        \"createdAt\": \"2024-03-15T13:28:41.339Z\",\n        \"id\": \"dao_001\",\n        \"members\": [\"user001\", \"user002\", \"user003\"]\n      }\n    }\n  }\n}\n```\n\n### RemoveMembers (참여자 제외)\nModerator 이상 권한을 가진 사용자가 멤버를 제외할 수 있다.\n\n```json\n{\n  \"sig\": {\n      \"sig\": {\n        \"v\": 27,\n        \"r\": \"b2d7c7ddf8bea783b8ed59906b2f5db00b9e53031d6407933d7c4a80c7157f35\",\n        \"s\": \"7d546c7d0f0fdf058e5bdf74b39cb2d3db34aa1dcdd6b2a76ea6504655b12b0f\"\n      },\n      \"account\": \"moderator\",\n    }\n  },\n  \"value\": {\n    \"CreatorDaoTx\": {\n      \"RemoveMembers\": {\n        \"networkId\": 102,\n        \"createdAt\": \"2024-03-15T14:28:41.339Z\",\n        \"id\": \"dao_001\",\n        \"members\": [\"user003\"]\n      }\n    }\n  }\n}\n```\n\n### PromoteModerators (관리자 임명)\nFounder나 Coordinator가 일반 멤버를 Moderator로 승급시킬 수 있다.\n\n```json\n{\n  \"sig\": {\n    \"sig\": {\n      \"v\": 27,\n      \"r\": \"d2d7c7ddf8bea783b8ed59906b2f5db00b9e53031d6407933d7c4a80c7157f35\",\n      \"s\": \"9d546c7d0f0fdf058e5bdf74b39cb2d3db34aa1dcdd6b2a76ea6504655b12b0f\"\n    },\n    \"account\": \"founder\"\n  },\n  \"value\": {\n    \"CreatorDaoTx\": {\n      \"PromoteModerators\": {\n        \"networkId\": 102,\n        \"createdAt\": \"2024-03-15T15:28:41.339Z\",\n        \"id\": \"dao_001\",\n        \"members\": [\"user001\"]\n      }\n    }\n  }\n}\n```\n\n### DemoteModerators (관리자 해임)\nFounder나 Coordinator가 Moderator를 일반 멤버로 강등시킬 수 있다.\n\n```json\n{\n  \"sig\": {\n    \"sig\": {\n      \"v\": 27,\n      \"r\": \"d2d7c7ddf8bea783b8ed59906b2f5db00b9e53031d6407933d7c4a80c7157f35\",\n      \"s\": \"9d546c7d0f0fdf058e5bdf74b39cb2d3db34aa1dcdd6b2a76ea6504655b12b0f\"\n    },\n    \"account\": \"founder\"\n  },\n  \"value\": {\n    \"CreatorDaoTx\": {\n      \"DemoteModerators\": {\n        \"networkId\": 102,\n        \"createdAt\": \"2024-03-15T16:28:41.339Z\",\n        \"id\": \"dao_001\",\n        \"members\": [\"user001\"]\n      }\n    }\n  }\n}\n```\n\n## 공통 사항\n- 모든 트랜잭션에는 networkId와 createdAt을 포함한다.\n- 서명은 NamedSignature 형식을 사용하고, 권한에 맞는 이름을 포함한다.\n- 멤버 관련 작업(추가/제거/승급/강등)은 배열로 여러 계정을 한번에 처리할 수 있다.\n"
  },
  {
    "path": "docs/dao-voting-system-design-english.md",
    "content": "# LeisureMeta DAO Voting System Design Document\n\n## 1. Project Background and Objective\n\nLeisureMeta is an innovative blockchain platform that supports DAO projects composed of creators and fans. The DAO Voting system designed in this document aims to enable these DAOs to make important decisions, such as fund allocation, in a transparent and democratic manner.\n\nLeisureMeta Chain inherits the advantages of existing DAO systems while providing the following innovative features:\n\n1. High Scalability and Low Cost:\n   - Addresses the high gas fees and low throughput issues faced by existing Ethereum-based DAOs.\n   - Internal transactions on LeisureMeta Chain are gas-free, enabling frequent voting and decision-making in a cost-effective manner.\n2. Integrated Snapshot Functionality:\n   - While existing solutions like ERC20Snapshot supported snapshot-based voting, LeisureMeta Chain implements this at the native level.\n   - Snapshots can be created and queried consistently for all tokens and NFTs on the chain, enabling voting with various assets without complex smart contracts.\n3. Seamless Integration of Diverse Voting Methods:\n   - Supports various voting methods such as one person one vote, token-weighted, and NFT-based voting within a single system.\n   - Unlike existing solutions that required separate contracts or implementations for each method, LeisureMeta Chain allows easy implementation of all methods through a unified API.\n4. Optimized for Creator Economy:\n   - Enables closer relationships between creators and fans through governance utilizing NFTs and fan tokens.\n   - While existing DAO systems mainly focused on finance or protocol governance, LeisureMeta is specialized in building a new economic ecosystem centered around creators.\n5. Enhanced User Experience:\n   - All functions including voting, token trading, and NFT minting occur on a single chain, greatly improving user experience.\n   - Eliminates the complexity and high entry barriers of existing multi-chain solutions while utilizing the advantages of each function.\n6. Flexible Scalability:\n   - The modular structure of LeisureMeta Chain allows for easy addition of new voting methods or governance models in the future.\n   - This will be a crucial advantage in the rapidly evolving Web3 ecosystem.\n\nThis DAO Voting system aims to provide a governance platform where creators and fans can easily and effectively participate by maximizing these advantages of LeisureMeta Chain. While inheriting the strengths of existing solutions, we seek to present a new model of collaboration and value creation in the Web3 era through LeisureMeta's specialized features.\n\n## 2. System Overview\n\nLeisureMeta's DAO Voting system has the following characteristics:\n\n1. A blockchain-based transparent and tamper-proof voting system\n2. Voting based on token holdings at a specific point in time using snapshot functionality\n3. Support for three voting methods: One person one vote, Fungible Token-based, and NFT-based\n4. Flexible voting group settings using Token Definition ID\n5. High accessibility due to internal transactions without Gas Fees\n\n## 3. Technical Design\n\n### 3.1 Snapshot Functionality\n\nLeisureMeta's snapshot feature records token holdings at a specific point in time, allowing for balance inquiries regardless of subsequent token movements.\n\nRelated APIs:\n- `GET /snapshot-state/{definitionID}`: Query token definition snapshot state\n- `GET /snapshot-balance/{Account}/{TokenDefinitionID}/{SnapshotID}`: Query token snapshot balance\n- `GET /nft-snapshot-balance/{Account}/{TokenDefinitionID}/{SnapshotID}`: Query NFT snapshot balance\n\n### 3.2 DAO Voting System API\n\n#### 3.2.1 Create Vote Proposal\n\n```json\nPOST /tx\n\n{\n  \"VotingTx\": {\n    \"CreateVoteProposal\": {\n      \"networkId\": 2021,\n      \"createdAt\": \"2023-06-21T18:01:00Z\",\n      \"proposalId\": \"PROPOSAL-2023-001\",\n      \"title\": \"Community Fund Usage Proposal\",\n      \"description\": \"Fund allocation for Creator Support Program\",\n      \"votingPower\": {\n        \"LM\": 12345\n      },\n      \"voteStart\": \"2023-06-22T00:00:00Z\",\n      \"voteEnd\": \"2023-06-29T23:59:59Z\",\n      \"voteType\": \"TOKEN_WEIGHTED\",\n      \"voteOptions\": {\n        \"1\": \"Approve\",\n        \"2\": \"Reject\",\n        \"3\": \"Abstain\"\n      },\n      \"quorum\": 1000000, // Minimum participation (e.g., 1,000,000 LM)\n      \"passThreshold\": 0.66 // Approval threshold (66%)\n    }\n  }\n}\n```\n\nExample of NFT-based voting:\n\n```json\nPOST /tx\n\n{\n  \"VotingTx\": {\n    \"CreateVoteProposal\": {\n      \"networkId\": 2021,\n      \"createdAt\": \"2023-06-21T18:01:00Z\",\n      \"proposalId\": \"PROPOSAL-2023-002\",\n      \"title\": \"Approval for New NFT Collection Launch\",\n      \"description\": \"Voting for approval of a new NFT collection proposed by the community\",\n      \"votingPower\": {\n        \"NFT-COLLECTION-001\": 12347,\n        \"NFT-COLLECTION-002\": 12348\n      },\n      \"voteStart\": \"2023-06-22T00:00:00Z\",\n      \"voteEnd\": \"2023-06-29T23:59:59Z\",\n      \"voteType\": \"NFT_BASED\",\n      \"voteOptions\": {\n        \"1\": \"Approve\",\n        \"2\": \"Reject\"\n      },\n      \"quorum\": 100, // Minimum participation (number of NFTs)\n      \"passThresholdNumer\": 51, // Approval threshold numerator(51%)\n      \"passThresholdDemon\": 100, // Approval threshold denominator(100%)\n    }\n  }\n}\n```\n\n#### 3.2.2 Cast Vote\n\n```json\nPOST /tx\n\n{\n  \"VotingTx\": {\n    \"CastVote\": {\n      \"networkId\": 2021,\n      \"createdAt\": \"2023-06-23T10:30:00Z\",\n      \"proposalId\": \"PROPOSAL-2023-001\",\n      \"selectedOption\": \"1\"\n    }\n  }\n}\n```\n\n#### 3.2.3 Tally Votes\n\n```json\nPOST /tx\n\n{\n  \"VotingTx\": {\n    \"TallyVotes\": {\n      \"networkId\": 2021,\n      \"createdAt\": \"2023-06-30T00:01:00Z\",\n      \"proposalId\": \"PROPOSAL-2023-001\"\n    }\n  }\n}\n```\n\n#### 3.2.4 Query Vote Proposal\n\n```\nGET /vote-proposal/{proposalId}\n```\n\nResponse:\n```json\n{\n  \"proposalId\": \"PROPOSAL-2023-001\",\n  \"title\": \"Community Fund Usage Proposal\",\n  \"description\": \"Fund allocation for Creator Support Program\",\n  \"votingTokens\": [\"LM\"],\n  \"snapshotId\": 12345,\n  \"voteStart\": \"2023-06-22T00:00:00Z\",\n  \"voteEnd\": \"2023-06-29T23:59:59Z\",\n  \"voteOptions\": {\n    \"1\": \"Approve\",\n    \"2\": \"Reject\",\n    \"3\": \"Abstain\"\n  },\n  \"quorum\": 1000000,\n  \"passThreshold\": 0.66,\n  \"status\": \"In Progress\",\n  \"currentResults\": {\n    \"1\": 3500000,\n    \"2\": 1200000,\n    \"3\": 300000\n  },\n  \"totalVotes\": 5000000\n}\n```\n\n#### 3.2.5 Query User Voting History\n\n```\nGET /vote-history/{account}\n```\n\nResponse:\n```json\n[\n  {\n    \"proposalId\": \"PROPOSAL-2023-001\",\n    \"votedAt\": \"2023-06-23T10:30:00Z\",\n    \"selectedOption\": \"1\"\n  },\n  // ... other voting history\n]\n```\n\n### 3.3 Voting Type Implementation\n\nVoting types are specified through the `voteType` field in `CreateVoteProposal`:\n\n1. One person one vote (`ONE_PERSON_ONE_VOTE`): Implemented to allow only one vote per account\n2. Fungible Token-based voting (`TOKEN_WEIGHTED`): Voting rights proportional to LM token holdings\n3. NFT-based voting (`NFT_BASED`): Voting rights based on NFT holdings\n\nImplementation for each voting type:\n\n1. One person one vote:\n   - The `votingTokens` field is ignored.\n   - Each account can cast one vote if it exists at the snapshot point.\n\n2. Fungible Token-based voting (LM token):\n   - \"LM\" is specified in the `votingTokens`.\n   - Voting rights are granted based on LM token holdings at the snapshot point.\n\n3. NFT-based voting:\n   - The Token Definition ID of the NFT collection is specified in `votingTokens` (e.g., \"NFT-COLLECTION-001\").\n   - Voting rights are granted based on the holdings of the specified NFT collection at the snapshot point.\n   - Each NFT has equal weight.\n\n### 3.4 Utilizing Token Definition ID\n\nThe `votingTokens` field in `CreateVoteProposal` specifies the tokens used for voting. For TOKEN_WEIGHTED type, LM token is used, and for NFT_BASED type, the Token Definition ID of the relevant NFT collection is used.\n\n### 3.5 Utilizing Snapshot ID\n\nWhen creating a vote proposal, `snapshotId` is specified to grant voting rights based on token holdings at a specific snapshot point. This prevents the influence of token movements during the voting period and ensures fair voting.\n\n## 4. Security and Transparency\n\n- Transaction signatures: All transactions must be signed with the user's private key.\n- Blockchain records: All voting-related transactions are permanently recorded on the blockchain, ensuring transparency.\n- Snapshot-based voting: Voting rights are granted based on token holdings at a specific point in time, preventing vote manipulation.\n- API security: HTTPS is used to enhance API communication security, and appropriate authentication/authorization mechanisms are implemented.\n\n## 5. Socioeconomic Impact\n\nLeisureMeta's DAO Voting system is expected to have the following socioeconomic impacts:\n\n1. Democratization of the creator economy: Strengthening the relationship between creators and fans through direct participation in decision-making\n2. Transparent fund management: Ensuring transparency in fund execution through a blockchain-based voting system\n3. Community-driven growth: Encouraging active participation of community members through the DAO structure\n4. New utilization of digital assets: Increasing the utility of digital assets through governance participation using NFTs and tokens\n5. Decentralized decision-making: Providing opportunities for more stakeholders to participate in decision-making, moving away from centralized power structures\n\n## 6. Implementation Roadmap\n\n1. Implement and test snapshot functionality\n   - Build snapshot ID generation and management system\n2. Implement DAO Voting system API\n   - Implement transaction processing for `CreateVoteProposal`, `CastVote`, `TallyVotes`\n   - Implement logic for each voting type\n   - Implement APIs for querying vote proposals and history\n3. Implement blockchain state management\n   - Design state structure for storing vote proposals, participation, and results\n4. Develop client application\n   - Implement user interface\n   - Implement communication logic with API\n5. Testing and security audit\n6. Beta version release and feedback collection\n7. Official version release\n\n## 7. Legal Considerations\n\nThis system merely provides a tool for decision-making and does not directly involve fund-raising or management. However, users must comply with relevant regulations in their respective countries, and LeisureMeta will continuously review regulatory compliance through legal consultation.\n\nKey considerations:\n1. Data privacy: Protect user information and comply with relevant regulations such as GDPR\n2. Securities law: Ensure token-based voting does not violate securities regulations\n3. Anti-Money Laundering (AML): Consider introducing KYC procedures if necessary\n4. Smart contract audit: Regularly conduct security audits to eliminate vulnerabilities\n\n## 8. Conclusion\n\nLeisureMeta's DAO Voting system provides a fairer and more transparent decision-making platform by utilizing snapshot IDs. By supporting three voting methods (one person one vote, LM token-weighted, and NFT-based), it offers decision-making mechanisms suitable for various situations. This will foster democratic operation and sustainable growth of the LeisureMeta ecosystem.\n"
  },
  {
    "path": "modules/api/src/main/scala/io/leisuremeta/chain/api/LeisureMetaChainApi.scala",
    "content": "package io.leisuremeta.chain\npackage api\n\nimport java.time.Instant\nimport java.util.Locale\n\nimport io.circe.KeyEncoder\nimport io.circe.generic.auto.*\nimport scodec.bits.ByteVector\nimport sttp.model.StatusCode\nimport sttp.tapir.*\nimport sttp.tapir.CodecFormat.TextPlain\nimport sttp.tapir.generic.auto.*\nimport sttp.tapir.json.circe.*\n\nimport lib.crypto.{Hash, Signature}\nimport lib.datatype.{BigNat, UInt256, UInt256BigInt, UInt256Bytes, Utf8}\nimport api.model.{\n  Account,\n  AccountSignature,\n  Block,\n  GroupId,\n  NodeStatus,\n  Signed,\n  Transaction,\n  TransactionWithResult,\n}\nimport api.model.account.EthAddress\nimport api.model.api_model.{\n  AccountInfo,\n  ActivityInfo,\n  BalanceInfo,\n  BlockInfo,\n  CreatorDaoInfo,\n  GroupInfo,\n  NftBalanceInfo,\n  RewardInfo,\n  TxInfo,\n}\nimport api.model.creator_dao.CreatorDaoId\nimport api.model.token.{\n  NftState,\n  SnapshotState,\n  TokenDefinition,\n  TokenDefinitionId,\n  TokenId,\n}\nimport api.model.reward.{\n  ActivitySnapshot,\n  DaoInfo,\n  OwnershipSnapshot,\n  OwnershipRewardLog,\n}\nimport api.model.voting.{ProposalId, Proposal}\nimport api.model.token.SnapshotState.*\nimport io.leisuremeta.chain.api.model.Signed.TxHash\n\nobject LeisureMetaChainApi:\n\n  given Schema[UInt256Bytes]  = Schema.string\n  given Schema[UInt256BigInt] = Schema(SchemaType.SInteger())\n  given Schema[BigNat] = Schema.schemaForBigInt.map[BigNat] {\n    (bigint: BigInt) => BigNat.fromBigInt(bigint).toOption\n  } { (bignat: BigNat) => bignat.toBigInt }\n  given Schema[Utf8] = Schema.string\n  given [K: KeyEncoder, V: Schema]: Schema[Map[K, V]] =\n    Schema.schemaForMap[K, V](KeyEncoder[K].apply)\n  given [A]: Schema[Hash.Value[A]] = Schema.string\n  given Schema[Signature.Header]   = Schema(SchemaType.SInteger())\n  given Schema[Transaction]        = Schema.derived[Transaction]\n\n  given hashValueCodec[A]: Codec[String, Hash.Value[A], TextPlain] =\n    Codec.string.mapDecode { (s: String) =>\n      ByteVector\n        .fromHexDescriptive(s)\n        .left\n        .map(new Exception(_))\n        .flatMap(UInt256.from) match\n        case Left(e)  => DecodeResult.Error(s, e)\n        case Right(v) => DecodeResult.Value(Hash.Value(v))\n    }(_.toUInt256Bytes.toBytes.toHex)\n  given bignatCodec: Codec[String, BigNat, TextPlain] =\n    Codec.bigInt.mapDecode { (n: BigInt) =>\n      BigNat.fromBigInt(n) match\n        case Left(e)  => DecodeResult.Error(n.toString(10), new Exception(e))\n        case Right(v) => DecodeResult.Value(v)\n    }(_.toBigInt)\n\n  final case class ServerError(msg: String)\n\n  sealed trait UserError:\n    def msg: String\n  final case class Unauthorized(msg: String) extends UserError\n  final case class NotFound(msg: String)     extends UserError\n  final case class BadRequest(msg: String)   extends UserError\n\n  val baseEndpoint = endpoint.errorOut(\n    oneOf[Either[ServerError, UserError]](\n      oneOfVariantFromMatchType(\n        StatusCode.Unauthorized,\n        jsonBody[Right[ServerError, Unauthorized]]\n          .description(\"invalid signature\"),\n      ),\n      oneOfVariantFromMatchType(\n        StatusCode.NotFound,\n        jsonBody[Right[ServerError, NotFound]].description(\"not found\"),\n      ),\n      oneOfVariantFromMatchType(\n        StatusCode.BadRequest,\n        jsonBody[Right[ServerError, BadRequest]].description(\"bad request\"),\n      ),\n      oneOfVariantFromMatchType(\n        StatusCode.InternalServerError,\n        jsonBody[Left[ServerError, UserError]].description(\n          \"internal server error\",\n        ),\n      ),\n    ),\n  )\n\n  @SuppressWarnings(Array(\"org.wartremover.warts.Any\"))\n  val getTxSetEndpoint = baseEndpoint.get\n    .in(\"tx\" / query[Block.BlockHash](\"block\"))\n    .out(jsonBody[Set[TxInfo]])\n\n  @SuppressWarnings(Array(\"org.wartremover.warts.Any\"))\n  val getTxEndpoint = baseEndpoint.get\n    .in(\"tx\" / path[Signed.TxHash])\n    .out(jsonBody[TransactionWithResult])\n\n  @SuppressWarnings(Array(\"org.wartremover.warts.Any\"))\n  val postTxEndpoint =\n    baseEndpoint.post\n      .in(\"tx\")\n      .in(jsonBody[Seq[Signed.Tx]])\n      .out(jsonBody[Seq[Hash.Value[TransactionWithResult]]])\n\n  @SuppressWarnings(Array(\"org.wartremover.warts.Any\"))\n  val postTxHashEndpoint = baseEndpoint.post\n    .in(\"txhash\")\n    .in(jsonBody[Seq[Transaction]])\n    .out(jsonBody[Seq[Hash.Value[Transaction]]])\n\n  @SuppressWarnings(Array(\"org.wartremover.warts.Any\"))\n  val getStatusEndpoint =\n    baseEndpoint.get.in(\"status\").out(jsonBody[NodeStatus])\n\n  @SuppressWarnings(Array(\"org.wartremover.warts.Any\"))\n  val getAccountEndpoint =\n    baseEndpoint.get\n      .in(\"account\" / path[Account])\n      .out(jsonBody[AccountInfo])\n\n  @SuppressWarnings(Array(\"org.wartremover.warts.Any\"))\n  val getEthEndpoint =\n    baseEndpoint.get\n      .in(\"eth\" / path[EthAddress])\n      .out(jsonBody[Account])\n\n  @SuppressWarnings(Array(\"org.wartremover.warts.Any\"))\n  val getGroupEndpoint =\n    baseEndpoint.get\n      .in(\"group\" / path[GroupId])\n      .out(jsonBody[GroupInfo])\n\n  @SuppressWarnings(Array(\"org.wartremover.warts.Any\"))\n  val getBlockListEndpoint =\n    baseEndpoint.get\n      .in(\n        \"block\" / query[Option[Block.BlockHash]](\"from\")\n          .and(query[Option[Int]](\"limit\")),\n      )\n      .out(jsonBody[List[BlockInfo]])\n\n  @SuppressWarnings(Array(\"org.wartremover.warts.Any\"))\n  val getBlockEndpoint =\n    baseEndpoint.get\n      .in(\"block\" / path[Block.BlockHash])\n      .out(jsonBody[Block])\n\n  @SuppressWarnings(Array(\"org.wartremover.warts.Any\"))\n  val getTokenDefinitionEndpoint =\n    baseEndpoint.get\n      .in(\"token-def\" / path[TokenDefinitionId])\n      .out(jsonBody[TokenDefinition])\n\n  @SuppressWarnings(Array(\"org.wartremover.warts.Any\"))\n  val getBalanceEndpoint =\n    baseEndpoint.get\n      .in(\"balance\" / path[Account].and(query[Movable](\"movable\")))\n      .out(jsonBody[Map[TokenDefinitionId, BalanceInfo]])\n\n  @SuppressWarnings(Array(\"org.wartremover.warts.Any\"))\n  val getNftBalanceEndpoint =\n    baseEndpoint.get\n      .in(\"nft-balance\" / path[Account].and(query[Option[Movable]](\"movable\")))\n      .out(jsonBody[Map[TokenId, NftBalanceInfo]])\n\n  @SuppressWarnings(Array(\"org.wartremover.warts.Any\"))\n  val getTokenEndpoint =\n    baseEndpoint.get\n      .in(\"token\" / path[TokenId])\n      .out(jsonBody[NftState])\n\n  @SuppressWarnings(Array(\"org.wartremover.warts.Any\"))\n  val getTokenHistoryEndpoint =\n    baseEndpoint.get\n      .in(\"token-hist\" / path[Hash.Value[TransactionWithResult]])\n      .out(jsonBody[NftState])\n\n  @SuppressWarnings(Array(\"org.wartremover.warts.Any\"))\n  val getOwnersEndpoint =\n    baseEndpoint.get\n      .in(\"owners\" / path[TokenDefinitionId])\n      .out(jsonBody[Map[TokenId, Account]])\n\n  @SuppressWarnings(Array(\"org.wartremover.warts.Any\"))\n  val getAccountActivityEndpoint =\n    baseEndpoint.get\n      .in(\"activity\" / \"account\" / path[Account])\n      .out(jsonBody[Seq[ActivityInfo]])\n\n  @SuppressWarnings(Array(\"org.wartremover.warts.Any\"))\n  val getTokenActivityEndpoint =\n    baseEndpoint.get\n      .in(\"activity\" / \"token\" / path[TokenId])\n      .out(jsonBody[Seq[ActivityInfo]])\n\n  @SuppressWarnings(Array(\"org.wartremover.warts.Any\"))\n  val getAccountSnapshotEndpoint =\n    baseEndpoint.get\n      .in(\"snapshot\" / \"account\" / path[Account])\n      .out(jsonBody[ActivitySnapshot])\n\n  @SuppressWarnings(Array(\"org.wartremover.warts.Any\"))\n  val getTokenSnapshotEndpoint =\n    baseEndpoint.get\n      .in(\"snapshot\" / \"token\" / path[TokenId])\n      .out(jsonBody[ActivitySnapshot])\n\n  @SuppressWarnings(Array(\"org.wartremover.warts.Any\"))\n  val getOwnershipSnapshotEndpoint =\n    baseEndpoint.get\n      .in(\"snapshot\" / \"ownership\" / path[TokenId])\n      .out(jsonBody[OwnershipSnapshot])\n\n  @SuppressWarnings(Array(\"org.wartremover.warts.Any\"))\n  val getOwnershipSnapshotMapEndpoint =\n    baseEndpoint.get\n      .in {\n        \"snapshot\" / \"ownership\" / query[Option[TokenId]](\"from\")\n          .and(query[Option[Int]](\"limit\"))\n      }\n      .out(jsonBody[Map[TokenId, OwnershipSnapshot]])\n\n  @SuppressWarnings(Array(\"org.wartremover.warts.Any\"))\n  val getOwnershipRewardedEndpoint =\n    baseEndpoint.get\n      .in(\"rewarded\" / \"ownership\" / path[TokenId])\n      .out(jsonBody[OwnershipRewardLog])\n\n  @SuppressWarnings(Array(\"org.wartremover.warts.Any\"))\n  val getRewardEndpoint =\n    baseEndpoint.get\n      .in {\n        \"reward\" / path[Account]\n          .and(query[Option[Instant]](\"timestamp\"))\n          .and(query[Option[Account]](\"dao-account\"))\n          .and(query[Option[BigNat]](\"reward-amount\"))\n      }\n      .out(jsonBody[RewardInfo])\n\n  @SuppressWarnings(Array(\"org.wartremover.warts.Any\"))\n  val getDaoEndpoint =\n    baseEndpoint.get\n      .in(\"dao\" / path[GroupId])\n      .out(jsonBody[DaoInfo])\n\n  @SuppressWarnings(Array(\"org.wartremover.warts.Any\"))\n  val getSnapshotStateEndpoint =\n    baseEndpoint.get\n      .in(\"snapshot-state\" / path[TokenDefinitionId])\n      .out(jsonBody[SnapshotState])\n\n  @SuppressWarnings(Array(\"org.wartremover.warts.Any\"))\n  val getFungibleSnapshotBalanceEndpoint =\n    baseEndpoint.get\n      .in:\n        \"snapshot-balance\"\n          / path[Account]\n          / path[TokenDefinitionId]\n          / path[SnapshotState.SnapshotId]\n      .out(jsonBody[Map[Hash.Value[TransactionWithResult], BigNat]])\n\n  @SuppressWarnings(Array(\"org.wartremover.warts.Any\"))\n  val getNftSnapshotBalanceEndpoint =\n    baseEndpoint.get\n      .in:\n        \"nft-snapshot-balance\"\n          / path[Account]\n          / path[TokenDefinitionId]\n          / path[SnapshotState.SnapshotId]\n      .out(jsonBody[Set[TokenId]])\n\n  @SuppressWarnings(Array(\"org.wartremover.warts.Any\"))\n  val getVoteProposalEndpoint =\n    baseEndpoint.get\n      .in(\"vote\" / \"proposal\" / path[ProposalId])\n      .out(jsonBody[Proposal])\n\n  @SuppressWarnings(Array(\"org.wartremover.warts.Any\"))\n  val getAccountVotesEndpoint =\n    baseEndpoint.get\n      .in(\"vote\" / \"account\" / path[ProposalId] / path[Account])\n      .out(jsonBody[(Utf8, BigNat)])\n\n  @SuppressWarnings(Array(\"org.wartremover.warts.Any\"))\n  val getVoteCountEndpoint =\n    baseEndpoint.get\n      .in(\"vote\" / \"count\" / path[ProposalId])\n      .out(jsonBody[Map[Utf8, BigNat]])\n\n  @SuppressWarnings(Array(\"org.wartremover.warts.Any\"))\n  val getCreatorDaoInfoEndpoint =\n    baseEndpoint.get\n      .in(\"creator-dao\" / path[CreatorDaoId])\n      .out(jsonBody[CreatorDaoInfo])\n\n  \n  @SuppressWarnings(Array(\"org.wartremover.warts.Any\"))\n  val getCreatorDaoMemberEndpoint =\n    baseEndpoint.get\n      .in:\n        \"creator-dao\"\n          / path[CreatorDaoId]\n          / \"member\"\n          .and(query[Option[Account]](\"from\"))\n          .and(query[Option[Int]](\"limit\"))\n      .out(jsonBody[Seq[Account]])\n\n  enum Movable:\n    case Free, Locked\n\n  object Movable:\n    @SuppressWarnings(Array(\"org.wartremover.warts.ToString\"))\n    given Codec[String, Movable, TextPlain] = Codec.string.mapDecode {\n      (s: String) =>\n        s match\n          case \"free\"   => DecodeResult.Value(Movable.Free)\n          case \"locked\" => DecodeResult.Value(Movable.Locked)\n          case _ => DecodeResult.Error(s, new Exception(s\"invalid movable: $s\"))\n    }(_.toString.toLowerCase(Locale.ENGLISH))\n\n    @SuppressWarnings(Array(\"org.wartremover.warts.ToString\"))\n    given Codec[String, Option[Movable], TextPlain] = Codec.string.mapDecode {\n      (s: String) =>\n        s match\n          case \"free\"   => DecodeResult.Value(Some(Movable.Free))\n          case \"locked\" => DecodeResult.Value(Some(Movable.Locked))\n          case \"all\"    => DecodeResult.Value(None)\n          case _ => DecodeResult.Error(s, new Exception(s\"invalid movable: $s\"))\n    }(_.fold(\"\")(_.toString.toLowerCase(Locale.ENGLISH)))\n"
  },
  {
    "path": "modules/api/src/main/scala/io/leisuremeta/chain/api/model/Account.scala",
    "content": "package io.leisuremeta.chain\npackage api.model\n\nimport cats.Eq\n\nimport io.circe.{Decoder, Encoder, KeyDecoder, KeyEncoder}\nimport sttp.tapir.{Codec, DecodeResult, Schema}\nimport sttp.tapir.CodecFormat.TextPlain\n\nimport lib.codec.byte.{ByteDecoder, ByteEncoder}\nimport lib.datatype.Utf8\n\nopaque type Account = Utf8\nobject Account:\n  def apply(value: Utf8): Account = value\n\n  extension (a: Account)\n    def utf8: Utf8 = a\n\n  given Encoder[Account] = Encoder.encodeString.contramap(_.utf8.value)\n  given Decoder[Account] = Decoder.decodeString.emap(Utf8.from(_).left.map(_.getMessage)).map(apply)\n\n  given KeyDecoder[Account] = Utf8.utf8CirceKeyDecoder\n  given KeyEncoder[Account] = Utf8.utf8CirceKeyEncoder\n\n  given Codec[String, Account, TextPlain] = Codec.string.mapDecode{ (s: String) =>\n    Utf8.from(s) match\n      case Left(e) => DecodeResult.Error(s, e)\n      case Right(a) => DecodeResult.Value(Account(a))\n  }(_.utf8.value)\n  given Schema[Account] = Schema.string\n\n  given ByteDecoder[Account] = Utf8.utf8ByteDecoder\n  given ByteEncoder[Account] = Utf8.utf8ByteEncoder\n\n  given Eq[Account] = Eq.fromUniversalEquals\n"
  },
  {
    "path": "modules/api/src/main/scala/io/leisuremeta/chain/api/model/AccountData.scala",
    "content": "package io.leisuremeta.chain\npackage api.model\n\nimport java.time.Instant\n\nimport account.{ExternalChain, ExternalChainAddress}\nimport lib.datatype.Utf8\n\nfinal case class AccountData(\n    externalChainAddresses: Map[ExternalChain, ExternalChainAddress],\n    guardian: Option[Account],\n    lastChecked: Instant,\n    memo: Option[Utf8],\n)\n"
  },
  {
    "path": "modules/api/src/main/scala/io/leisuremeta/chain/api/model/AccountSignature.scala",
    "content": "package io.leisuremeta.chain\npackage api.model\n\nimport io.circe.{Decoder, Encoder}\nimport io.circe.generic.semiauto.*\n\nimport lib.crypto.Signature\n\nfinal case class AccountSignature(\n    sig: Signature,\n    account: Account,\n)\n\nobject AccountSignature:\n  given Decoder[AccountSignature] = deriveDecoder\n  given Encoder[AccountSignature] = deriveEncoder\n"
  },
  {
    "path": "modules/api/src/main/scala/io/leisuremeta/chain/api/model/Block.scala",
    "content": "package io.leisuremeta.chain\npackage api.model\n\nimport java.time.Instant\n\nimport cats.kernel.Eq\nimport scodec.bits.ByteVector\n\nimport lib.codec.byte.{ByteDecoder, ByteEncoder}\nimport lib.crypto.{Hash, Recover, Sign, Signature}\nimport lib.datatype.BigNat\nimport lib.merkle.MerkleTrieNode\n\nfinal case class Block(\n    header: Block.Header,\n    transactionHashes: Set[Signed.TxHash],\n    votes: Set[Signature],\n)\n\nobject Block:\n\n  type BlockHash = Hash.Value[Block]\n\n  final case class Header(\n      number: BigNat,\n      parentHash: BlockHash,\n      stateRoot: StateRoot,\n      transactionsRoot: Option[MerkleTrieNode.MerkleRoot],\n      timestamp: Instant,\n  )\n\n  object Header:\n    given eqHeader: Eq[Header] = Eq.fromUniversalEquals\n\n    given headerHash: Hash[Header] = Hash.build\n\n    given encoder: ByteEncoder[Header] with\n      def encode(header: Header): ByteVector =\n        ByteEncoder[BigNat].encode(header.number)\n          ++ ByteEncoder[BlockHash].encode(header.parentHash)\n          ++ ByteEncoder[StateRoot].encode(header.stateRoot)\n          ++ ByteEncoder[Option[MerkleTrieNode.MerkleRoot]].encode(header.transactionsRoot)\n          ++ ByteEncoder[Instant].encode(header.timestamp)\n    given decoder: ByteDecoder[Header] =\n      for\n        number     <- ByteDecoder[BigNat]\n        parentHash <- ByteDecoder[BlockHash]\n        stateRoot  <- ByteDecoder[StateRoot]\n        transactionsRoot <- ByteDecoder[Option[MerkleTrieNode.MerkleRoot]]\n        timestamp <- ByteDecoder[Instant]\n      yield Header(number, parentHash, stateRoot, transactionsRoot, timestamp)\n\n  object ops:\n    extension (blockHash: Hash.Value[Block])\n      def toHeaderHash: Hash.Value[Header] =\n        Hash.Value[Header](blockHash.toUInt256Bytes)\n\n    extension (headerHash: Hash.Value[Header])\n      def toBlockHash: Hash.Value[Block] =\n        Hash.Value[Block](headerHash.toUInt256Bytes)\n\n  given blockHash: Hash[Block] = Header.headerHash.contramap(_.header)\n\n  given signBlock: Sign[Block.Header] = Sign.build\n\n  given recoverBlockHeader: Recover[Block.Header] = Recover.build\n"
  },
  {
    "path": "modules/api/src/main/scala/io/leisuremeta/chain/api/model/GroupData.scala",
    "content": "package io.leisuremeta.chain\npackage api.model\n\nimport lib.datatype.Utf8\n\nfinal case class GroupData(\n    name: Utf8,\n    coordinator: Account,\n)\n"
  },
  {
    "path": "modules/api/src/main/scala/io/leisuremeta/chain/api/model/GroupId.scala",
    "content": "package io.leisuremeta.chain\npackage api.model\n\nimport io.circe.{Decoder, Encoder}\nimport sttp.tapir.{Codec, DecodeResult, Schema}\nimport sttp.tapir.CodecFormat.TextPlain\n\nimport lib.codec.byte.{ByteDecoder, ByteEncoder}\nimport lib.datatype.Utf8\n\nopaque type GroupId = Utf8\nobject GroupId:\n  def apply(utf8: Utf8): GroupId = utf8\n\n  extension (a: GroupId)\n    def utf8: Utf8 = a\n\n  given Encoder[GroupId] = Encoder.encodeString.contramap(_.utf8.value)\n  given Decoder[GroupId] = Decoder.decodeString.emap(Utf8.from(_).left.map(_.getMessage)).map(apply)\n  given Schema[GroupId] = Schema.string\n\n  given ByteDecoder[GroupId] = Utf8.utf8ByteDecoder.map(GroupId(_))\n  given ByteEncoder[GroupId] = Utf8.utf8ByteEncoder.contramap(_.utf8)\n\n  given Codec[String, GroupId, TextPlain] = Codec.string.mapDecode{ (s: String) =>\n    Utf8.from(s) match\n      case Left(e) => DecodeResult.Error(s, e)\n      case Right(a) => DecodeResult.Value(GroupId(a))\n  }(_.utf8.value)\n"
  },
  {
    "path": "modules/api/src/main/scala/io/leisuremeta/chain/api/model/NetworkId.scala",
    "content": "package io.leisuremeta.chain\npackage api.model\n\nimport io.circe.{Decoder, Encoder}\nimport sttp.tapir.Schema\n\nimport lib.codec.byte.{ByteDecoder, ByteEncoder}\nimport lib.datatype.BigNat\n\nopaque type NetworkId = BigNat\n\nobject NetworkId:\n  def apply(value: BigNat): NetworkId = value\n\n  given ByteDecoder[NetworkId] = BigNat.bignatByteDecoder\n  given ByteEncoder[NetworkId] = BigNat.bignatByteEncoder\n\n  given Decoder[NetworkId] = BigNat.bignatCirceDecoder\n  given Encoder[NetworkId] = BigNat.bignatCirceEncoder\n\n  given Schema[NetworkId] = Schema.schemaForBigInt.map[NetworkId] {\n    (bigint: BigInt) =>\n      BigNat.fromBigInt(bigint).toOption.map(apply)\n  }{(bignat: BigNat) => bignat.toBigInt}\n"
  },
  {
    "path": "modules/api/src/main/scala/io/leisuremeta/chain/api/model/NodeStatus.scala",
    "content": "package io.leisuremeta.chain\npackage api.model\n\nimport lib.datatype.BigNat\n\nfinal case class NodeStatus(\n    networkId: NetworkId,\n    genesisHash: Block.BlockHash,\n    bestHash: Block.BlockHash,\n    number: BigNat,\n)\n"
  },
  {
    "path": "modules/api/src/main/scala/io/leisuremeta/chain/api/model/PublicKeySummary.scala",
    "content": "package io.leisuremeta.chain\npackage api.model\n\nimport java.time.Instant\n\nimport cats.Eq\nimport cats.syntax.eq.given\nimport io.circe.{Decoder, Encoder, KeyDecoder, KeyEncoder}\nimport scodec.bits.ByteVector\nimport sttp.tapir.Schema\n\nimport lib.codec.byte.{ByteDecoder, ByteEncoder}\nimport lib.crypto.{Hash, PublicKey}\nimport lib.datatype.Utf8\n\nopaque type PublicKeySummary = ByteVector\n\nobject PublicKeySummary:\n\n  final case class Info(\n    description: Utf8,\n    addedAt: Instant,\n    expiresAt: Option[Instant],\n  )\n\n  def apply(bytes: ByteVector): Either[String, PublicKeySummary] =\n    Either.cond(\n      bytes.size === 20,\n      bytes,\n      \"PublicKeySummary must be 20 bytes\",\n    )\n\n  def fromHex(hexString: String): Either[String, PublicKeySummary] =\n    for\n      bytes <- ByteVector.fromHexDescriptive(hexString)\n      summary <- apply(bytes)\n    yield summary\n\n  def fromPublicKeyHash(hash: Hash.Value[PublicKey]): PublicKeySummary =\n    hash.toUInt256Bytes.toBytes takeRight 20\n\n  extension (pks: PublicKeySummary)\n    def toBytes: ByteVector = pks\n\n  given Eq[PublicKeySummary] = Eq.fromUniversalEquals\n\n  given Decoder[PublicKeySummary] = Decoder.decodeString.emap { (s: String) =>\n    val (f, b) = s `splitAt` 2\n    for\n      _ <- Either.cond(\n        f === \"0x\",\n        (),\n        s\"PublicKeySummary string not starting 0x: $f\",\n      )\n      summary <- fromHex(b)\n    yield summary\n  }\n  given Encoder[PublicKeySummary] =\n    Encoder.encodeString.contramap(summary => s\"0x${summary.toString}\")\n  \n  given KeyDecoder[PublicKeySummary] = KeyDecoder.instance(fromHex(_).toOption)\n  given KeyEncoder[PublicKeySummary] = KeyEncoder.encodeKeyString.contramap(_.toHex)\n\n  given Schema[PublicKeySummary] = Schema.string\n\n  given ByteDecoder[PublicKeySummary] = ByteDecoder.fromFixedSizeBytes(20)(identity)\n  given ByteEncoder[PublicKeySummary] = (bytes: ByteVector) => bytes\n"
  },
  {
    "path": "modules/api/src/main/scala/io/leisuremeta/chain/api/model/Signed.scala",
    "content": "package io.leisuremeta.chain\npackage api.model\n\nimport scodec.bits.ByteVector\nimport sttp.tapir.{Codec, DecodeResult}\nimport sttp.tapir.CodecFormat.TextPlain\n\nimport lib.crypto.Hash\nimport lib.datatype.UInt256\nimport io.circe.{Decoder, Encoder}\n\n\n\nfinal case class Signed[A](sig: AccountSignature, value: A)\n\nobject Signed:\n  type Tx = Signed[Transaction]\n\n  type TxHash = Hash.Value[Tx]\n  object TxHash{\n    given txHashCodec: Codec[String, TxHash, TextPlain] = Codec.string.mapDecode{ (s: String) =>\n      ByteVector.fromHexDescriptive(s).left.map(new Exception(_)).flatMap(UInt256.from) match\n        case Left(e) => DecodeResult.Error(s, e)\n        case Right(v) => DecodeResult.Value(Hash.Value(v))\n    }(_.toUInt256Bytes.toBytes.toHex)\n  }\n\n  given signedHash[A: Hash]: Hash[Signed[A]] = Hash[A].contramap(_.value)\n\n  given txhashDecoder: Decoder[TxHash] = Hash.Value.circeValueDecoder[Tx]\n  given txhashEncoder: Encoder[TxHash] = Hash.Value.circeValueEncoder[Tx]\n\n  import io.circe.generic.semiauto.*\n\n  given signedDecoder[A: Decoder]: Decoder[Signed[A]] = deriveDecoder\n  given signedEncoder[A: Encoder]: Encoder[Signed[A]] = deriveEncoder\n"
  },
  {
    "path": "modules/api/src/main/scala/io/leisuremeta/chain/api/model/StateRoot.scala",
    "content": "package io.leisuremeta.chain\npackage api.model\n\nimport cats.Eq\n\nimport io.circe.{Decoder, Encoder}\nimport sttp.tapir.*\n\nimport lib.codec.byte.{ByteDecoder, ByteEncoder}\nimport lib.crypto.Hash\nimport lib.merkle.MerkleTrieNode.MerkleRoot\n\nopaque type StateRoot = Option[MerkleRoot]\n\nobject StateRoot:\n\n  def apply(main: Option[MerkleRoot]): StateRoot = main\n\n  val empty: StateRoot = None\n\n  extension (sr: StateRoot) def main: Option[MerkleRoot] = sr\n\n  given byteDecoder: ByteDecoder[StateRoot] =\n    ByteDecoder.optionByteDecoder[MerkleRoot]\n  given byteEncoder: ByteEncoder[StateRoot] =\n    ByteEncoder.optionByteEncoder[MerkleRoot]\n\n  given circeDecoder: Decoder[StateRoot] = \n    Decoder.decodeOption[MerkleRoot]\n  given circeEncoder: Encoder[StateRoot] =\n    Encoder.encodeOption[MerkleRoot]\n\n  given tapirSchema: Schema[StateRoot] = Schema.string\n  given eqStateRoot: Eq[StateRoot]     = Eq.fromUniversalEquals\n"
  },
  {
    "path": "modules/api/src/main/scala/io/leisuremeta/chain/api/model/Transaction.scala",
    "content": "package io.leisuremeta.chain\npackage api.model\n\nimport java.time.Instant\n\nimport io.circe.{Decoder, Encoder}\nimport io.circe.generic.semiauto.*\nimport scodec.bits.ByteVector\n\nimport account.{EthAddress, ExternalChain, ExternalChainAddress}\n//import agenda.AgendaId\nimport creator_dao.CreatorDaoId\nimport reward.DaoActivity\nimport voting.{ProposalId, VoteType}\nimport lib.crypto.{Hash, Recover, Sign}\nimport lib.codec.byte.{ByteDecoder, ByteEncoder}\nimport lib.codec.byte.ByteEncoder.ops.*\nimport lib.datatype.{BigNat, UInt256Bytes, Utf8}\nimport token.{Rarity, NftInfo, NftInfoWithPrecision, TokenDefinitionId, TokenId}\n\nsealed trait TransactionResult\nobject TransactionResult:\n  given txResultByteEncoder: ByteEncoder[TransactionResult] =\n    (txr: TransactionResult) =>\n      txr match\n        case r: Transaction.AccountTx.AddPublicKeySummariesResult =>\n          ByteVector.fromByte(0) ++ r.toBytes\n        case r: Transaction.TokenTx.BurnFungibleTokenResult =>\n          ByteVector.fromByte(1) ++ r.toBytes\n        case r: Transaction.TokenTx.EntrustFungibleTokenResult =>\n          ByteVector.fromByte(2) ++ r.toBytes\n        case r: Transaction.RewardTx.ExecuteRewardResult =>\n          ByteVector.fromByte(3) ++ r.toBytes\n        case r: Transaction.RewardTx.ExecuteOwnershipRewardResult =>\n          ByteVector.fromByte(4) ++ r.toBytes\n        case r: Transaction.AgendaTx.VoteSimpleAgendaResult =>\n          ByteVector.fromByte(5) ++ r.toBytes\n\n  given txResultByteDecoder: ByteDecoder[TransactionResult] =\n    ByteDecoder.byteDecoder.flatMap {\n      case 0 =>\n        ByteDecoder[Transaction.AccountTx.AddPublicKeySummariesResult].widen\n      case 1 =>\n        ByteDecoder[Transaction.TokenTx.BurnFungibleTokenResult].widen\n      case 2 =>\n        ByteDecoder[Transaction.TokenTx.EntrustFungibleTokenResult].widen\n      case 3 =>\n        ByteDecoder[Transaction.RewardTx.ExecuteRewardResult].widen\n      case 4 =>\n        ByteDecoder[Transaction.RewardTx.ExecuteOwnershipRewardResult].widen\n      case 5 =>\n        ByteDecoder[Transaction.AgendaTx.VoteSimpleAgendaResult].widen\n    }\n\n  given txResultCirceEncoder: Encoder[TransactionResult] =\n    deriveEncoder[TransactionResult]\n\n  given txResultCirceDecoder: Decoder[TransactionResult] =\n    deriveDecoder[TransactionResult]\n\nsealed trait Transaction:\n  def networkId: NetworkId\n  def createdAt: Instant\n//  def memo: Option[Utf8]\n\nobject Transaction:\n  sealed trait AccountTx extends Transaction:\n    def account: Account\n  object AccountTx:\n    final case class CreateAccount(\n        networkId: NetworkId,\n        createdAt: Instant,\n        account: Account,\n        ethAddress: Option[EthAddress],\n        guardian: Option[Account],\n//        memo: Option[Utf8],\n    ) extends AccountTx\n\n    final case class CreateAccountWithExternalChainAddresses(\n        networkId: NetworkId,\n        createdAt: Instant,\n        account: Account,\n        externalChainAddresses: Map[ExternalChain, ExternalChainAddress],\n        guardian: Option[Account],\n        memo: Option[Utf8],\n    ) extends AccountTx\n\n    final case class UpdateAccount(\n        networkId: NetworkId,\n        createdAt: Instant,\n        account: Account,\n        ethAddress: Option[EthAddress],\n        guardian: Option[Account],\n//        memo: Option[Utf8],\n    ) extends AccountTx\n\n    final case class UpdateAccountWithExternalChainAddresses(\n        networkId: NetworkId,\n        createdAt: Instant,\n        account: Account,\n        externalChainAddresses: Map[ExternalChain, ExternalChainAddress],\n        guardian: Option[Account],\n        memo: Option[Utf8],\n    ) extends AccountTx\n\n    final case class AddPublicKeySummaries(\n        networkId: NetworkId,\n        createdAt: Instant,\n        account: Account,\n        summaries: Map[PublicKeySummary, Utf8],\n//        memo: Option[Utf8],\n    ) extends AccountTx\n\n    final case class AddPublicKeySummariesResult(\n        removed: Map[PublicKeySummary, Utf8],\n    ) extends TransactionResult\n\n//    final case class RemovePublicKeySummaries(\n//        networkId: NetworkId,\n//        createdAt: Instant,\n//        account: Account,\n//        summaries: Set[PublicKeySummary],\n//    ) extends AccountTx\n//\n//    final case class RemoveAccount(\n//        networkId: NetworkId,\n//        createdAt: Instant,\n//        account: Account,\n//    ) extends AccountTx\n\n    given txByteDecoder: ByteDecoder[AccountTx] = ByteDecoder[BigNat].flatMap {\n      bignat =>\n        bignat.toBigInt.toInt match\n          case 0 => ByteDecoder[CreateAccount].widen\n          case 1 => ByteDecoder[UpdateAccount].widen\n          case 2 => ByteDecoder[AddPublicKeySummaries].widen\n//          case 3 => ByteDecoder[RemovePublicKeySummaries].widen\n//          case 4 => ByteDecoder[RemoveAccount].widen\n          case 5 => ByteDecoder[CreateAccountWithExternalChainAddresses].widen\n          case 6 => ByteDecoder[UpdateAccountWithExternalChainAddresses].widen\n    }\n    given txByteEncoder: ByteEncoder[AccountTx] = (atx: AccountTx) =>\n      atx match\n        case tx: CreateAccount         => build(0)(tx)\n        case tx: UpdateAccount         => build(1)(tx)\n        case tx: AddPublicKeySummaries => build(2)(tx)\n//        case tx: RemovePublicKeySummaries => build(3)(tx)\n//        case tx: RemoveAccount            => build(4)(tx)\n        case tx: CreateAccountWithExternalChainAddresses => build(5)(tx)\n        case tx: UpdateAccountWithExternalChainAddresses => build(6)(tx)\n\n    given txCirceDecoder: Decoder[AccountTx] = deriveDecoder\n    given txCirceEncoder: Encoder[AccountTx] = deriveEncoder\n  end AccountTx\n\n  sealed trait GroupTx extends Transaction\n  object GroupTx:\n    final case class CreateGroup(\n        networkId: NetworkId,\n        createdAt: Instant,\n        groupId: GroupId,\n        name: Utf8,\n        coordinator: Account,\n//        memo: Option[Utf8],\n    ) extends GroupTx\n\n//    final case class DisbandGroup(\n//        networkId: NetworkId,\n//        createdAt: Instant,\n//        groupId: GroupId,\n//    ) extends GroupTx\n\n    final case class AddAccounts(\n        networkId: NetworkId,\n        createdAt: Instant,\n        groupId: GroupId,\n        accounts: Set[Account],\n//        memo: Option[Utf8],\n    ) extends GroupTx\n\n//    final case class RemoveAccounts(\n//        networkId: NetworkId,\n//        createdAt: Instant,\n//        groupId: GroupId,\n//        accounts: Set[Account],\n//    ) extends GroupTx\n\n//    final case class ReplaceCoordinator(\n//        networkId: NetworkId,\n//        createdAt: Instant,\n//        groupId: GroupId,\n//        newCoordinator: Account,\n//    ) extends GroupTx\n\n    given txByteDecoder: ByteDecoder[GroupTx] = ByteDecoder[BigNat].flatMap {\n      bignat =>\n        bignat.toBigInt.toInt match\n          case 0 => ByteDecoder[CreateGroup].widen\n          case 2 => ByteDecoder[AddAccounts].widen\n    }\n    given txByteEncoder: ByteEncoder[GroupTx] = (atx: GroupTx) =>\n      atx match\n        case tx: CreateGroup => build(0)(tx)\n        case tx: AddAccounts => build(2)(tx)\n\n    given txCirceDecoder: Decoder[GroupTx] = deriveDecoder\n    given txCirceEncoder: Encoder[GroupTx] = deriveEncoder\n  end GroupTx\n\n  sealed trait TokenTx extends Transaction\n  object TokenTx:\n    final case class DefineToken(\n        networkId: NetworkId,\n        createdAt: Instant,\n        definitionId: TokenDefinitionId,\n        name: Utf8,\n        symbol: Option[Utf8],\n        minterGroup: Option[GroupId],\n        nftInfo: Option[NftInfo],\n//        memo: Option[Utf8],\n    ) extends TokenTx\n\n    final case class DefineTokenWithPrecision(\n        networkId: NetworkId,\n        createdAt: Instant,\n        definitionId: TokenDefinitionId,\n        name: Utf8,\n        symbol: Option[Utf8],\n        minterGroup: Option[GroupId],\n        nftInfo: Option[NftInfoWithPrecision],\n//        memo: Option[Utf8],\n    ) extends TokenTx\n\n    final case class MintFungibleToken(\n        networkId: NetworkId,\n        createdAt: Instant,\n        definitionId: TokenDefinitionId,\n        outputs: Map[Account, BigNat],\n//        memo: Option[Utf8],\n    ) extends TokenTx\n        with FungibleBalance\n\n    final case class MintNFT(\n        networkId: NetworkId,\n        createdAt: Instant,\n        tokenDefinitionId: TokenDefinitionId,\n        tokenId: TokenId,\n        rarity: Rarity,\n        dataUrl: Utf8,\n        contentHash: UInt256Bytes,\n        output: Account,\n//        memo: Option[Utf8],\n    ) extends TokenTx\n        with NftBalance\n\n    final case class MintNFTWithMemo(\n        networkId: NetworkId,\n        createdAt: Instant,\n        tokenDefinitionId: TokenDefinitionId,\n        tokenId: TokenId,\n        rarity: Rarity,\n        dataUrl: Utf8,\n        contentHash: UInt256Bytes,\n        output: Account,\n        memo: Option[Utf8],\n    ) extends TokenTx\n        with NftBalance\n\n    final case class BurnFungibleToken(\n        networkId: NetworkId,\n        createdAt: Instant,\n        definitionId: TokenDefinitionId,\n        amount: BigNat,\n        inputs: Set[Signed.TxHash],\n//        memo: Option[Utf8],\n    ) extends TokenTx\n        with FungibleBalance\n\n    final case class BurnFungibleTokenResult(\n        outputAmount: BigNat,\n    ) extends TransactionResult\n\n    final case class BurnNFT(\n        networkId: NetworkId,\n        createdAt: Instant,\n        definitionId: TokenDefinitionId,\n        input: Signed.TxHash,\n//        memo: Option[Utf8],\n    ) extends TokenTx\n\n    final case class UpdateNFT(\n        networkId: NetworkId,\n        createdAt: Instant,\n        tokenDefinitionId: TokenDefinitionId,\n        tokenId: TokenId,\n        rarity: Rarity,\n        dataUrl: Utf8,\n        contentHash: UInt256Bytes,\n        output: Account,\n        memo: Option[Utf8],\n    ) extends TokenTx\n\n    final case class TransferFungibleToken(\n        networkId: NetworkId,\n        createdAt: Instant,\n        tokenDefinitionId: TokenDefinitionId,\n        inputs: Set[Signed.TxHash],\n        outputs: Map[Account, BigNat],\n        memo: Option[Utf8],\n    ) extends TokenTx\n        with FungibleBalance\n\n    final case class TransferNFT(\n        networkId: NetworkId,\n        createdAt: Instant,\n        definitionId: TokenDefinitionId,\n        tokenId: TokenId,\n        input: Signed.TxHash,\n        output: Account,\n        memo: Option[Utf8],\n    ) extends TokenTx\n        with NftBalance\n\n    final case class EntrustFungibleToken(\n        networkId: NetworkId,\n        createdAt: Instant,\n        definitionId: TokenDefinitionId,\n        amount: BigNat,\n        inputs: Set[Signed.TxHash],\n        to: Account,\n//        memo: Option[Utf8],\n    ) extends TokenTx\n        with FungibleBalance\n\n    final case class EntrustFungibleTokenResult(\n        remainder: BigNat,\n    ) extends TransactionResult\n\n    final case class EntrustNFT(\n        networkId: NetworkId,\n        createdAt: Instant,\n        definitionId: TokenDefinitionId,\n        tokenId: TokenId,\n        input: Signed.TxHash,\n        to: Account,\n//        memo: Option[Utf8],\n    ) extends TokenTx\n\n    final case class DisposeEntrustedFungibleToken(\n        networkId: NetworkId,\n        createdAt: Instant,\n        definitionId: TokenDefinitionId,\n        inputs: Set[Signed.TxHash],\n        outputs: Map[Account, BigNat],\n//        memo: Option[Utf8],\n    ) extends TokenTx\n        with FungibleBalance\n\n    final case class DisposeEntrustedNFT(\n        networkId: NetworkId,\n        createdAt: Instant,\n        definitionId: TokenDefinitionId,\n        tokenId: TokenId,\n        input: Signed.TxHash,\n        output: Option[Account],\n//        memo: Option[Utf8],\n    ) extends TokenTx\n        with NftBalance\n\n    final case class CreateSnapshots(\n        networkId: NetworkId,\n        createdAt: Instant,\n        definitionIds: Set[TokenDefinitionId],\n        memo: Option[Utf8],\n    ) extends TokenTx\n\n    given txByteDecoder: ByteDecoder[TokenTx] = ByteDecoder[BigNat].flatMap {\n      bignat =>\n        bignat.toBigInt.toInt match\n          case 0  => ByteDecoder[DefineToken].widen\n          case 1  => ByteDecoder[MintFungibleToken].widen\n          case 2  => ByteDecoder[MintNFT].widen\n          case 4  => ByteDecoder[TransferFungibleToken].widen\n          case 5  => ByteDecoder[TransferNFT].widen\n          case 6  => ByteDecoder[BurnFungibleToken].widen\n          case 7  => ByteDecoder[BurnNFT].widen\n          case 8  => ByteDecoder[EntrustFungibleToken].widen\n          case 9  => ByteDecoder[EntrustNFT].widen\n          case 10 => ByteDecoder[DisposeEntrustedFungibleToken].widen\n          case 11 => ByteDecoder[DisposeEntrustedNFT].widen\n          case 12 => ByteDecoder[DefineTokenWithPrecision].widen\n          case 13 => ByteDecoder[UpdateNFT].widen\n          case 14 => ByteDecoder[MintNFTWithMemo].widen\n          case 15 => ByteDecoder[CreateSnapshots].widen\n    }\n\n    given txByteEncoder: ByteEncoder[TokenTx] = (ttx: TokenTx) =>\n      ttx match\n        case tx: DefineToken                   => build(0)(tx)\n        case tx: MintFungibleToken             => build(1)(tx)\n        case tx: MintNFT                       => build(2)(tx)\n        case tx: TransferFungibleToken         => build(4)(tx)\n        case tx: TransferNFT                   => build(5)(tx)\n        case tx: BurnFungibleToken             => build(6)(tx)\n        case tx: BurnNFT                       => build(7)(tx)\n        case tx: EntrustFungibleToken          => build(8)(tx)\n        case tx: EntrustNFT                    => build(9)(tx)\n        case tx: DisposeEntrustedFungibleToken => build(10)(tx)\n        case tx: DisposeEntrustedNFT           => build(11)(tx)\n        case tx: DefineTokenWithPrecision      => build(12)(tx)\n        case tx: UpdateNFT                     => build(13)(tx)\n        case tx: MintNFTWithMemo               => build(14)(tx)\n        case tx: CreateSnapshots               => build(15)(tx)\n\n    given txCirceDecoder: Decoder[TokenTx] = deriveDecoder\n    given txCirceEncoder: Encoder[TokenTx] = deriveEncoder\n\n  end TokenTx\n\n  sealed trait RewardTx extends Transaction\n  object RewardTx:\n    final case class RegisterDao(\n        networkId: NetworkId,\n        createdAt: Instant,\n        groupId: GroupId,\n        daoAccountName: Account,\n        moderators: Set[Account],\n//        memo: Option[Utf8],\n    ) extends RewardTx\n\n    final case class UpdateDao(\n        networkId: NetworkId,\n        createdAt: Instant,\n        groupId: GroupId,\n        moderators: Set[Account],\n        memo: Option[Utf8],\n    ) extends RewardTx\n\n    final case class RecordActivity(\n        networkId: NetworkId,\n        createdAt: Instant,\n        timestamp: Instant,\n        userActivity: Map[Account, Seq[DaoActivity]],\n        tokenReceived: Map[TokenId, Seq[DaoActivity]],\n        memo: Option[Utf8],\n    ) extends RewardTx\n\n    final case class OfferReward(\n        networkId: NetworkId,\n        createdAt: Instant,\n        tokenDefinitionId: TokenDefinitionId,\n        inputs: Set[Signed.TxHash],\n        outputs: Map[Account, BigNat],\n        memo: Option[Utf8],\n    ) extends RewardTx\n        with FungibleBalance\n\n    final case class BuildSnapshot(\n        networkId: NetworkId,\n        createdAt: Instant,\n        timestamp: Instant,\n        accountAmount: BigNat,\n        tokenAmount: BigNat,\n        ownershipAmount: BigNat,\n        memo: Option[Utf8],\n    ) extends RewardTx\n\n    final case class ExecuteReward(\n        networkId: NetworkId,\n        createdAt: Instant,\n        daoAccount: Option[Account],\n        memo: Option[Utf8],\n    ) extends RewardTx\n        with FungibleBalance\n\n    final case class ExecuteRewardResult(\n        outputs: Map[Account, BigNat],\n    ) extends TransactionResult\n\n    final case class ExecuteOwnershipReward(\n        networkId: NetworkId,\n        createdAt: Instant,\n        definitionId: TokenDefinitionId,\n        inputs: Set[Hash.Value[TransactionWithResult]],\n        targets: Set[TokenId],\n        memo: Option[Utf8],\n    ) extends RewardTx\n        with FungibleBalance\n\n    final case class ExecuteOwnershipRewardResult(\n        outputs: Map[Account, BigNat],\n    ) extends TransactionResult\n\n    given txByteDecoder: ByteDecoder[RewardTx] = ByteDecoder[BigNat].flatMap {\n      bignat =>\n        bignat.toBigInt.toInt match\n          case 0 => ByteDecoder[RegisterDao].widen\n          case 1 => ByteDecoder[UpdateDao].widen\n          case 2 => ByteDecoder[RecordActivity].widen\n          case 3 => ByteDecoder[OfferReward].widen\n          case 4 => ByteDecoder[BuildSnapshot].widen\n          case 6 => ByteDecoder[ExecuteReward].widen\n          case 9 => ByteDecoder[ExecuteOwnershipReward].widen\n    }\n\n    given txByteEncoder: ByteEncoder[RewardTx] = (rtx: RewardTx) =>\n      rtx match\n        case tx: RegisterDao            => build(0)(tx)\n        case tx: UpdateDao              => build(1)(tx)\n        case tx: RecordActivity         => build(2)(tx)\n        case tx: OfferReward            => build(3)(tx)\n        case tx: BuildSnapshot          => build(4)(tx)\n        case tx: ExecuteReward          => build(6)(tx)\n        case tx: ExecuteOwnershipReward => build(9)(tx)\n\n    given txCirceDecoder: Decoder[RewardTx] = deriveDecoder\n    given txCirceEncoder: Encoder[RewardTx] = deriveEncoder\n\n  end RewardTx\n\n  sealed trait AgendaTx extends Transaction\n  object AgendaTx:\n    final case class SuggestSimpleAgenda(\n        networkId: NetworkId,\n        createdAt: Instant,\n        title: Utf8,\n        votingToken: TokenDefinitionId,\n        voteStart: Instant,\n        voteEnd: Instant,\n        voteOptions: Map[Utf8, Utf8],\n    ) extends AgendaTx\n\n    final case class VoteSimpleAgenda(\n        networkId: NetworkId,\n        createdAt: Instant,\n        agendaTxHash: Hash.Value[TransactionWithResult],\n        selectedOption: Utf8,\n        memo: Option[Utf8],\n    ) extends AgendaTx\n\n    final case class VoteSimpleAgendaResult(\n        votingAmount: BigNat,\n    ) extends TransactionResult\n\n    given txByteDecoder: ByteDecoder[AgendaTx] = ByteDecoder[BigNat].flatMap {\n      bignat =>\n        bignat.toBigInt.toInt match\n          case 0 => ByteDecoder[SuggestSimpleAgenda].widen\n          case 1 => ByteDecoder[VoteSimpleAgenda].widen\n    }\n\n    given txByteEncoder: ByteEncoder[AgendaTx] = (rtx: AgendaTx) =>\n      rtx match\n        case tx: SuggestSimpleAgenda => build(0)(tx)\n        case tx: VoteSimpleAgenda    => build(1)(tx)\n\n    given txCirceDecoder: Decoder[AgendaTx] = deriveDecoder\n    given txCirceEncoder: Encoder[AgendaTx] = deriveEncoder\n\n  end AgendaTx\n\n  sealed trait VotingTx extends Transaction\n  object VotingTx:\n    /*\n    \"CreateVoteProposal\": {\n      \"networkId\": 2021,\n      \"createdAt\": \"2023-06-21T18:01:00Z\",\n      \"proposalId\": \"PROPOSAL-2023-002\",\n      \"title\": \"Approval for New NFT Collection Launch\",\n      \"description\": \"Voting for approval of a new NFT collection proposed by the community\",\n      \"votingPower\": {\n        \"NFT-COLLECTION-001\": 12347,\n        \"NFT-COLLECTION-002\": 12348\n      },\n      \"voteStart\": \"2023-06-22T00:00:00Z\",\n      \"voteEnd\": \"2023-06-29T23:59:59Z\",\n      \"voteType\": \"NFT_BASED\",\n      \"voteOptions\": {\n        \"1\": \"Approve\",\n        \"2\": \"Reject\"\n      },\n      \"quorum\": 100, // Minimum participation (number of NFTs)\n      \"passThresholdNumer\": 51, // Approval threshold numerator(51%)\n      \"passThresholdDemon\": 100, // Approval threshold denominator(100%)\n    }\n     */\n    final case class CreateVoteProposal(\n        networkId: NetworkId,\n        createdAt: Instant,\n        proposalId: ProposalId,\n        title: Utf8,\n        description: Utf8,\n        votingPower: Map[TokenDefinitionId, BigNat],\n        voteStart: Instant,\n        voteEnd: Instant,\n        voteType: VoteType,\n        voteOptions: Map[Utf8, Utf8],\n        quorum: BigNat,\n        passThresholdNumer: BigNat,\n        passThresholdDenom: BigNat,\n    ) extends VotingTx\n\n    /*\n\n    \"CastVote\": {\n      \"networkId\": 2021,\n      \"createdAt\": \"2023-06-23T10:30:00Z\",\n      \"proposalId\": \"PROPOSAL-2023-001\",\n      \"selectedOption\": \"1\"\n    }\n\n     */\n    final case class CastVote(\n        networkId: NetworkId,\n        createdAt: Instant,\n        proposalId: ProposalId,\n        selectedOption: Utf8,\n    ) extends VotingTx\n\n    /*\n    \"TallyVotes\": {\n      \"networkId\": 2021,\n      \"createdAt\": \"2023-06-30T00:01:00Z\",\n      \"proposalId\": \"PROPOSAL-2023-001\"\n    }\n     */\n    final case class TallyVotes(\n        networkId: NetworkId,\n        createdAt: Instant,\n        proposalId: ProposalId,\n    ) extends VotingTx\n\n    given txByteDecoder: ByteDecoder[VotingTx] = ByteDecoder[BigNat].flatMap:\n      bignat =>\n        bignat.toBigInt.toInt match\n          case 0 => ByteDecoder[CreateVoteProposal].widen\n          case 1 => ByteDecoder[CastVote].widen\n          case 2 => ByteDecoder[TallyVotes].widen\n    given txByteEncoder: ByteEncoder[VotingTx] = (vtx: VotingTx) =>\n      vtx match\n        case tx: CreateVoteProposal => build(0)(tx)\n        case tx: CastVote           => build(1)(tx)\n        case tx: TallyVotes         => build(2)(tx)\n    given txCirceDecoder: Decoder[VotingTx] = deriveDecoder\n    given txCirceEncoder: Encoder[VotingTx] = deriveEncoder\n  end VotingTx\n\n  sealed trait CreatorDaoTx extends Transaction\n  object CreatorDaoTx:\n    /*\n    {\n  \"sig\": {\n    \"NamedSignature\": {\n      \"name\": \"founder\",\n      \"sig\": {\n        \"v\": 27,\n        \"r\": \"62d7c7ddf8bea783b8ed59906b2f5db00b9e53031d6407933d7c4a80c7157f35\",\n        \"s\": \"2d546c7d0f0fdf058e5bdf74b39cb2d3db34aa1dcdd6b2a76ea6504655b12b0f\"\n      }\n    }\n  },\n  \"value\": {\n    \"CreatorDaoTx\": {\n      \"CreateCreatorDao\": {\n        \"networkId\": 102,\n        \"createdAt\": \"2024-03-15T09:28:41.339Z\",\n        \"id\": \"dao_001\",\n        \"name\": \"Art Creators DAO\",\n        \"description\": \"A DAO for digital art creators\",\n        \"founder\": \"creator001\",\n        \"coordinator\": \"playnomm\"\n      }\n    }\n  }\n}\n     */\n    final case class CreateCreatorDao(\n        networkId: NetworkId,\n        createdAt: Instant,\n        id: CreatorDaoId,\n        name: Utf8,\n        description: Utf8,\n        founder: Account,\n        coordinator: Account,\n    ) extends CreatorDaoTx\n\n    /*\n```json\n{\n  \"sig\": {\n    \"NamedSignature\": {\n      \"name\": \"moderator\",\n      \"sig\": {\n        \"v\": 27,\n        \"r\": \"72d7c7ddf8bea783b8ed59906b2f5db00b9e53031d6407933d7c4a80c7157f35\",\n        \"s\": \"3d546c7d0f0fdf058e5bdf74b39cb2d3db34aa1dcdd6b2a76ea6504655b12b0f\"\n      }\n    }\n  },\n  \"value\": {\n    \"CreatorDaoTx\": {\n      \"UpdateCreatorDao\": {\n        \"networkId\": 102,\n        \"createdAt\": \"2024-03-15T10:28:41.339Z\",\n        \"id\": \"dao_001\",\n        \"name\": \"Digital Art Creators DAO\",\n        \"description\": \"A DAO for digital art creators and collectors\"\n      }\n    }\n  }\n}\n```\n     */\n    final case class UpdateCreatorDao(\n        networkId: NetworkId,\n        createdAt: Instant,\n        id: CreatorDaoId,\n        name: Utf8,\n        description: Utf8,\n    ) extends CreatorDaoTx\n    /*\n```json\n{\n  \"sig\": {\n    \"NamedSignature\": {\n      \"name\": \"founder\",\n      \"sig\": {\n        \"v\": 27,\n        \"r\": \"82d7c7ddf8bea783b8ed59906b2f5db00b9e53031d6407933d7c4a80c7157f35\",\n        \"s\": \"4d546c7d0f0fdf058e5bdf74b39cb2d3db34aa1dcdd6b2a76ea6504655b12b0f\"\n      }\n    }\n  },\n  \"value\": {\n    \"CreatorDaoTx\": {\n      \"DisbandCreatorDao\": {\n        \"networkId\": 102,\n        \"createdAt\": \"2024-03-15T11:28:41.339Z\",\n        \"id\": \"dao_001\"\n      }\n    }\n  }\n}\n```\n     */\n\n    final case class DisbandCreatorDao(\n        networkId: NetworkId,\n        createdAt: Instant,\n        id: CreatorDaoId,\n    ) extends CreatorDaoTx\n\n    final case class ReplaceCoordinator(\n        networkId: NetworkId,\n        createdAt: Instant,\n        id: CreatorDaoId,\n        newCoordinator: Account,\n    ) extends CreatorDaoTx\n\n    final case class AddMembers(\n        networkId: NetworkId,\n        createdAt: Instant,\n        id: CreatorDaoId,\n        members: Set[Account],\n    ) extends CreatorDaoTx\n\n    final case class RemoveMembers(\n        networkId: NetworkId,\n        createdAt: Instant,\n        id: CreatorDaoId,\n        members: Set[Account],\n    ) extends CreatorDaoTx\n\n    final case class PromoteModerators(\n        networkId: NetworkId,\n        createdAt: Instant,\n        id: CreatorDaoId,\n        members: Set[Account],\n    ) extends CreatorDaoTx\n\n    final case class DemoteModerators(\n        networkId: NetworkId,\n        createdAt: Instant,\n        id: CreatorDaoId,\n        members: Set[Account],\n    ) extends CreatorDaoTx\n\n    given txByteDecoder: ByteDecoder[CreatorDaoTx] =\n      ByteDecoder[BigNat].flatMap: bignat =>\n        bignat.toBigInt.toInt match\n          case 0 => ByteDecoder[CreateCreatorDao].widen\n          case 1 => ByteDecoder[UpdateCreatorDao].widen\n          case 2 => ByteDecoder[DisbandCreatorDao].widen\n          case 3 => ByteDecoder[ReplaceCoordinator].widen\n          case 4 => ByteDecoder[AddMembers].widen\n          case 5 => ByteDecoder[RemoveMembers].widen\n          case 6 => ByteDecoder[PromoteModerators].widen\n          case 7 => ByteDecoder[DemoteModerators].widen\n    given txByteEncoder: ByteEncoder[CreatorDaoTx] = (cdtx: CreatorDaoTx) =>\n      cdtx match\n        case tx: CreateCreatorDao   => build(0)(tx)\n        case tx: UpdateCreatorDao   => build(1)(tx)\n        case tx: DisbandCreatorDao  => build(2)(tx)\n        case tx: ReplaceCoordinator => build(3)(tx)\n        case tx: AddMembers         => build(4)(tx)\n        case tx: RemoveMembers      => build(5)(tx)\n        case tx: PromoteModerators  => build(6)(tx)\n        case tx: DemoteModerators   => build(7)(tx)\n    given txCirceDecoder: Decoder[CreatorDaoTx] = deriveDecoder\n    given txCirceEncoder: Encoder[CreatorDaoTx] = deriveEncoder\n\n  end CreatorDaoTx\n\n  private def build[A: ByteEncoder](discriminator: Long)(tx: A): ByteVector =\n    ByteEncoder[BigNat].encode(BigNat.unsafeFromLong(discriminator))\n      ++ ByteEncoder[A].encode(tx)\n\n  given txByteDecoder: ByteDecoder[Transaction] = ByteDecoder[BigNat].flatMap:\n    bignat =>\n      bignat.toBigInt.toInt match\n        case 0 => ByteDecoder[AccountTx].widen\n        case 1 => ByteDecoder[GroupTx].widen\n        case 2 => ByteDecoder[TokenTx].widen\n        case 3 => ByteDecoder[RewardTx].widen\n        case 4 => ByteDecoder[AgendaTx].widen\n        case 5 => ByteDecoder[VotingTx].widen\n        case 6 => ByteDecoder[CreatorDaoTx].widen\n\n  given txByteEncoder: ByteEncoder[Transaction] = (tx: Transaction) =>\n    tx match\n      case tx: AccountTx    => build(0)(tx)\n      case tx: GroupTx      => build(1)(tx)\n      case tx: TokenTx      => build(2)(tx)\n      case tx: RewardTx     => build(3)(tx)\n      case tx: AgendaTx     => build(4)(tx)\n      case tx: VotingTx     => build(5)(tx)\n      case tx: CreatorDaoTx => build(6)(tx)\n\n  given txHash: Hash[Transaction] = Hash.build\n\n  given txSign: Sign[Transaction] = Sign.build\n\n  given txRecover: Recover[Transaction] = Recover.build\n\n  given txCirceDecoder: Decoder[Transaction] = deriveDecoder\n  given txCirceEncoder: Encoder[Transaction] = deriveEncoder\n\n  sealed trait FungibleBalance\n\n  sealed trait NftBalance:\n    def tokenId: TokenId\n\n  sealed trait DealSuggestion:\n    def originalSuggestion: Option[Signed.TxHash]\n"
  },
  {
    "path": "modules/api/src/main/scala/io/leisuremeta/chain/api/model/TransactionWithResult.scala",
    "content": "package io.leisuremeta.chain\npackage api.model\n\nimport lib.crypto.Hash\n\nfinal case class TransactionWithResult (\n    signedTx: Signed.Tx,\n    result: Option[TransactionResult],\n)\n\nobject TransactionWithResult:\n  \n  @SuppressWarnings(Array(\"org.wartremover.warts.Overloading\"))\n  inline def apply[Tx <: Transaction](signedTx: Signed[Tx])(\n      inline resultOption: Option[TransactionResult],\n  ): TransactionWithResult =\n\n    def widenTx: Signed.Tx = Signed(signedTx.sig, signedTx.value)\n\n    import scala.compiletime.*\n\n    inline signedTx.value match\n      case ap: Transaction.AccountTx.AddPublicKeySummaries =>\n        inline resultOption match\n          case Some(Transaction.AccountTx.AddPublicKeySummariesResult(removed)) =>\n            TransactionWithResult(widenTx, resultOption)\n          case other =>\n            error(\"wrong result type: expected AddPublicKeySummariesResult\")\n        \n      case bt: Transaction.TokenTx.BurnFungibleToken =>\n        inline resultOption match\n          case Some(Transaction.TokenTx.BurnFungibleTokenResult(amount)) =>\n            TransactionWithResult(widenTx, resultOption)\n          case other =>\n            error(\"wrong result type: expected BurnFungibleTokenResult\")\n      case bt: Transaction.TokenTx.EntrustFungibleToken =>\n        inline resultOption match\n          case Some(Transaction.TokenTx.EntrustFungibleTokenResult(amount)) =>\n            TransactionWithResult(widenTx, resultOption)\n          case other =>\n            error(\"wrong result type: expected EntrustFungibleTokenResult\")\n      case xr: Transaction.RewardTx.ExecuteReward =>\n        inline resultOption match\n          case Some(Transaction.RewardTx.ExecuteRewardResult(outputs)) =>\n            TransactionWithResult(widenTx, resultOption)\n          case other =>\n            error(\"wrong result type: expected ExecuteRewardResult\")\n      case _ =>\n        inline resultOption match\n          case None =>\n            TransactionWithResult(widenTx, resultOption)\n          case other =>\n            error(\n              \"wrong result type: expected None but \" + codeOf(resultOption),\n            )\n\n  given Hash[TransactionWithResult] =\n    Hash[Transaction].contramap(_.signedTx.value)\n\n  object ops:\n    extension [A](txHash: Hash.Value[A])\n      def toResultHashValue: Hash.Value[TransactionWithResult] =\n        Hash.Value[TransactionWithResult](txHash.toUInt256Bytes)\n      def toSignedTxHash: Hash.Value[Signed.Tx] =\n        Hash.Value[Signed.Tx](txHash.toUInt256Bytes)\n"
  },
  {
    "path": "modules/api/src/main/scala/io/leisuremeta/chain/api/model/account/EthAddress.scala",
    "content": "package io.leisuremeta.chain\npackage api.model\npackage account\n\nimport io.circe.{Decoder, Encoder, KeyDecoder, KeyEncoder}\nimport sttp.tapir.{Codec, DecodeResult, Schema}\nimport sttp.tapir.CodecFormat.TextPlain\n\nimport lib.codec.byte.{ByteDecoder, ByteEncoder}\nimport lib.datatype.Utf8\n\nopaque type EthAddress = Utf8\nobject EthAddress:\n  def apply(utf8: Utf8): EthAddress = utf8\n\n  extension (a: EthAddress)\n    def utf8: Utf8 = a\n\n  given Decoder[EthAddress] = Utf8.utf8CirceDecoder\n  given Encoder[EthAddress] = Utf8.utf8CirceEncoder\n  given Schema[EthAddress] = Schema.string\n\n  given KeyDecoder[EthAddress] = Utf8.utf8CirceKeyDecoder\n  given KeyEncoder[EthAddress] = Utf8.utf8CirceKeyEncoder\n\n  given ByteDecoder[EthAddress] = Utf8.utf8ByteDecoder.map(EthAddress(_))\n  given ByteEncoder[EthAddress] = Utf8.utf8ByteEncoder.contramap(_.utf8)\n\n  given Codec[String, EthAddress, TextPlain] = Codec.string.mapDecode{ (s: String) =>\n    Utf8.from(s) match\n      case Left(e) => DecodeResult.Error(s, e)\n      case Right(a) => DecodeResult.Value(EthAddress(a))\n  }(_.utf8.value)\n\n"
  },
  {
    "path": "modules/api/src/main/scala/io/leisuremeta/chain/api/model/account/ExternalChain.scala",
    "content": "package io.leisuremeta.chain\npackage api.model.account\n\nimport cats.syntax.either.*\n\nimport io.circe.{Decoder, Encoder, KeyDecoder, KeyEncoder}\nimport sttp.tapir.{Codec, DecodeResult}//, Schema}\nimport sttp.tapir.CodecFormat.TextPlain\n\nimport lib.codec.byte.{ByteDecoder, ByteEncoder}\nimport lib.datatype.BigNat\nimport lib.failure.DecodingFailure\nimport java.util.Locale\n\nenum ExternalChain(val name: String, val abbr: String):\n  case ETH extends ExternalChain(\"Ethereum\", \"eth\")\n  case SOL extends ExternalChain(\"Solana\", \"sol\")\n\nobject ExternalChain :\n  def fromAbbr(abbr: String): Option[ExternalChain] =\n    abbr.toLowerCase(Locale.US) match\n      case \"eth\" => Some(ETH)\n      case \"sol\" => Some(SOL)\n      case _ => None\n\n  given Decoder[ExternalChain] = Decoder.decodeString.emap:\n    fromAbbr(_).toRight(\"Unknown public chain\")\n\n  given Encoder[ExternalChain] = Encoder.encodeString.contramap(_.abbr)\n\n  given KeyDecoder[ExternalChain] = KeyDecoder.instance(fromAbbr(_))\n  given KeyEncoder[ExternalChain] = KeyEncoder.encodeKeyString.contramap(_.abbr)\n\n  given ByteDecoder[ExternalChain] = BigNat.bignatByteDecoder.emap: (bn: BigNat) =>\n    bn.toBigInt.toInt match\n      case 0 => ExternalChain.ETH.asRight[DecodingFailure]\n      case 1 => ExternalChain.SOL.asRight[DecodingFailure]\n      case _ => DecodingFailure(\"Unknown public chain\").asLeft[ExternalChain]\n  given ByteEncoder[ExternalChain] = BigNat.bignatByteEncoder.contramap:\n    case ExternalChain.ETH => BigNat.Zero\n    case ExternalChain.SOL => BigNat.One\n\n  given Codec[String, ExternalChain, TextPlain] = Codec.string\n    .mapDecode: abbr =>\n      fromAbbr(abbr) match\n        case Some(chain) => DecodeResult.Value(chain)\n        case None => DecodeResult.Error(abbr, DecodingFailure(s\"Invalid public chain: $abbr\"))\n    .apply(_.abbr)\n"
  },
  {
    "path": "modules/api/src/main/scala/io/leisuremeta/chain/api/model/account/ExternalChainAddress.scala",
    "content": "package io.leisuremeta.chain\npackage api.model\npackage account\n\nimport io.circe.{Decoder, Encoder, KeyDecoder, KeyEncoder}\nimport sttp.tapir.{Codec, DecodeResult, Schema}\nimport sttp.tapir.CodecFormat.TextPlain\n\nimport lib.codec.byte.{ByteDecoder, ByteEncoder}\nimport lib.datatype.Utf8\n\nopaque type ExternalChainAddress = Utf8\nobject ExternalChainAddress:\n  def apply(utf8: Utf8): ExternalChainAddress = utf8\n\n  extension (a: ExternalChainAddress)\n    def utf8: Utf8 = a\n\n  given Decoder[ExternalChainAddress] = Utf8.utf8CirceDecoder\n  given Encoder[ExternalChainAddress] = Utf8.utf8CirceEncoder\n  given Schema[ExternalChainAddress] = Schema.string\n\n  given KeyDecoder[ExternalChainAddress] = Utf8.utf8CirceKeyDecoder\n  given KeyEncoder[ExternalChainAddress] = Utf8.utf8CirceKeyEncoder\n\n  given ByteDecoder[ExternalChainAddress] = Utf8.utf8ByteDecoder.map(ExternalChainAddress(_))\n  given ByteEncoder[ExternalChainAddress] = Utf8.utf8ByteEncoder.contramap(_.utf8)\n\n  given Codec[String, ExternalChainAddress, TextPlain] = Codec.string.mapDecode{ (s: String) =>\n    Utf8.from(s) match\n      case Left(e) => DecodeResult.Error(s, e)\n      case Right(a) => DecodeResult.Value(ExternalChainAddress(a))\n  }(_.utf8.value)\n"
  },
  {
    "path": "modules/api/src/main/scala/io/leisuremeta/chain/api/model/agenda/AgendaId.scala",
    "content": "package io.leisuremeta.chain\npackage api.model.agenda\n\nimport io.circe.{Decoder, Encoder, KeyDecoder, KeyEncoder}\nimport sttp.tapir.{Codec, DecodeResult, Schema}\nimport sttp.tapir.CodecFormat.TextPlain\n\nimport lib.codec.byte.{ByteDecoder, ByteEncoder}\nimport lib.datatype.Utf8\n\nopaque type AgendaId = Utf8\n\nobject AgendaId:\n  def apply(id: Utf8): AgendaId = id\n  extension (id: AgendaId) def utf8: Utf8 = id\n\n  given Decoder[AgendaId] = Utf8.utf8CirceDecoder\n  given Encoder[AgendaId] = Utf8.utf8CirceEncoder\n  given Schema[AgendaId] = Schema.string\n\n  given KeyDecoder[AgendaId] = Utf8.utf8CirceKeyDecoder\n  given KeyEncoder[AgendaId] = Utf8.utf8CirceKeyEncoder\n\n  given ByteDecoder[AgendaId] = Utf8.utf8ByteDecoder.map(AgendaId(_))\n  given ByteEncoder[AgendaId] = Utf8.utf8ByteEncoder.contramap(_.utf8)\n\n  given Codec[String, AgendaId, TextPlain] = Codec.string.mapDecode{ (s: String) =>\n    Utf8.from(s) match\n      case Left(e) => DecodeResult.Error(s, e)\n      case Right(a) => DecodeResult.Value(AgendaId(a))\n  }(_.utf8.value)\n\n  given cats.Eq[AgendaId] = cats.Eq.fromUniversalEquals\n"
  },
  {
    "path": "modules/api/src/main/scala/io/leisuremeta/chain/api/model/api_model/AccountInfo.scala",
    "content": "package io.leisuremeta.chain\npackage api.model\npackage api_model\n\nimport account.*\nimport lib.datatype.Utf8\n\nfinal case class AccountInfo(\n    externalChainAddresses: Map[ExternalChain, ExternalChainAddress],\n    ethAddress: Option[EthAddress],\n    guardian: Option[Account],\n    memo: Option[Utf8],\n    publicKeySummaries: Map[PublicKeySummary, PublicKeySummary.Info],\n)\n"
  },
  {
    "path": "modules/api/src/main/scala/io/leisuremeta/chain/api/model/api_model/ActivityInfo.scala",
    "content": "package io.leisuremeta.chain\npackage api.model\npackage api_model\n\nimport java.time.Instant\n\nimport lib.crypto.Hash\nimport lib.datatype.Utf8\n\nfinal case class ActivityInfo(\n    timestamp: Instant,\n    point: BigInt,\n    description: Utf8,\n    txHash: Hash.Value[TransactionWithResult],\n)\n"
  },
  {
    "path": "modules/api/src/main/scala/io/leisuremeta/chain/api/model/api_model/BalanceInfo.scala",
    "content": "package io.leisuremeta.chain\npackage api.model\npackage api_model\n\nimport io.circe.generic.semiauto.*\n\nimport lib.crypto.Hash\nimport lib.datatype.BigNat\n\nfinal case class BalanceInfo(\n    totalAmount: BigNat,\n    unused: Map[Hash.Value[TransactionWithResult], TransactionWithResult],\n)\n"
  },
  {
    "path": "modules/api/src/main/scala/io/leisuremeta/chain/api/model/api_model/BlockInfo.scala",
    "content": "package io.leisuremeta.chain\npackage api.model\npackage api_model\n\nimport java.time.Instant\n\nimport lib.datatype.BigNat\n\nfinal case class BlockInfo(\n    blockNumber: BigNat,\n    timestamp: Instant,\n    blockHash: Block.BlockHash,\n    txCount: Int,\n)\n"
  },
  {
    "path": "modules/api/src/main/scala/io/leisuremeta/chain/api/model/api_model/CreatorDaoInfo.scala",
    "content": "package io.leisuremeta.chain\npackage api.model\npackage api_model\n\nimport lib.datatype.Utf8\nimport creator_dao.CreatorDaoId\n\nfinal case class CreatorDaoInfo(\n    id: CreatorDaoId,\n    name: Utf8,\n    description: Utf8,\n    founder: Account,\n    coordinator: Account,\n    moderators: Set[Account],\n)\n"
  },
  {
    "path": "modules/api/src/main/scala/io/leisuremeta/chain/api/model/api_model/GroupInfo.scala",
    "content": "package io.leisuremeta.chain.api.model\npackage api_model\n\nfinal case class GroupInfo(\n    data: GroupData,\n    accounts: Set[Account],\n)\n"
  },
  {
    "path": "modules/api/src/main/scala/io/leisuremeta/chain/api/model/api_model/NftBalanceInfo.scala",
    "content": "package io.leisuremeta.chain\npackage api.model\npackage api_model\n\nimport io.circe.generic.semiauto.*\n\nimport token.TokenDefinitionId\nimport lib.crypto.Hash\nimport lib.datatype.Utf8\n\nfinal case class NftBalanceInfo(\n  tokenDefinitionId: TokenDefinitionId,\n  txHash: Hash.Value[TransactionWithResult],\n  tx: TransactionWithResult,\n  memo: Option[Utf8],\n)\n"
  },
  {
    "path": "modules/api/src/main/scala/io/leisuremeta/chain/api/model/api_model/RewardInfo.scala",
    "content": "package io.leisuremeta.chain\npackage api.model\npackage api_model\n\nimport java.time.Instant\n\nimport lib.datatype.BigNat\nimport token.Rarity\nimport reward.DaoActivity\n\nfinal case class RewardInfo(\n    account: Account,\n    reward: RewardInfo.Reward,\n    point: RewardInfo.Point,\n    timestamp: Instant,\n    totalNumberOfDao: BigNat,\n)\n\nobject RewardInfo:\n\n  final case class Reward(\n      total: BigNat,\n      activity: BigNat,\n      token: BigNat,\n      rarity: BigNat,\n      bonus: BigNat,\n  )\n\n  final case class Point(\n      activity: DaoActivity,\n      token: DaoActivity,\n      rarity: Map[Rarity, BigNat],\n  )\n"
  },
  {
    "path": "modules/api/src/main/scala/io/leisuremeta/chain/api/model/api_model/TxInfo.scala",
    "content": "package io.leisuremeta.chain\npackage api.model\npackage api_model\n\nimport java.time.Instant\n\nfinal case class TxInfo(\n    txHash: Signed.TxHash,\n    createdAt: Instant,\n    account: Account,\n    `type`: String,\n)\n"
  },
  {
    "path": "modules/api/src/main/scala/io/leisuremeta/chain/api/model/creator_dao/CreatorDaoData.scala",
    "content": "package io.leisuremeta.chain\npackage api.model\npackage creator_dao\n\nimport lib.datatype.Utf8\n\nfinal case class CreatorDaoData(\n    id: CreatorDaoId,\n    name: Utf8,\n    description: Utf8,\n    founder: Account,\n    coordinator: Account,\n)\n"
  },
  {
    "path": "modules/api/src/main/scala/io/leisuremeta/chain/api/model/creator_dao/CreatorDaoId.scala",
    "content": "package io.leisuremeta.chain\npackage api.model\npackage creator_dao\n\nimport io.circe.{Decoder, Encoder, KeyDecoder, KeyEncoder}\nimport sttp.tapir.{Codec, DecodeResult, Schema}\nimport sttp.tapir.CodecFormat.TextPlain\n\nimport lib.codec.byte.{ByteDecoder, ByteEncoder}\nimport lib.datatype.Utf8\n\nopaque type CreatorDaoId = Utf8\nobject CreatorDaoId:\n  def apply(utf8: Utf8): CreatorDaoId = utf8\n\n  extension (a: CreatorDaoId) def utf8: Utf8 = a\n\n  given Decoder[CreatorDaoId] = Utf8.utf8CirceDecoder\n  given Encoder[CreatorDaoId] = Utf8.utf8CirceEncoder\n  given Schema[CreatorDaoId]  = Schema.string\n\n  given KeyDecoder[CreatorDaoId] = Utf8.utf8CirceKeyDecoder\n  given KeyEncoder[CreatorDaoId] = Utf8.utf8CirceKeyEncoder\n\n  given ByteDecoder[CreatorDaoId] = Utf8.utf8ByteDecoder.map(CreatorDaoId(_))\n  given ByteEncoder[CreatorDaoId] = Utf8.utf8ByteEncoder.contramap(_.utf8)\n\n  given Codec[String, CreatorDaoId, TextPlain] = Codec.string.mapDecode {\n    (s: String) =>\n      Utf8.from(s) match\n        case Left(e)  => DecodeResult.Error(s, e)\n        case Right(a) => DecodeResult.Value(CreatorDaoId(a))\n  }(_.utf8.value)\n\n  given cats.Eq[CreatorDaoId] = cats.Eq.fromUniversalEquals\n"
  },
  {
    "path": "modules/api/src/main/scala/io/leisuremeta/chain/api/model/reward/ActivityLog.scala",
    "content": "package io.leisuremeta.chain\npackage api.model\npackage reward\n\nimport lib.crypto.Hash\nimport lib.datatype.Utf8\n\nfinal case class ActivityLog(\n  point: BigInt,\n  description: Utf8,\n  txHash: Hash.Value[TransactionWithResult],\n)\n"
  },
  {
    "path": "modules/api/src/main/scala/io/leisuremeta/chain/api/model/reward/ActivityRewardLog.scala",
    "content": "package io.leisuremeta.chain\npackage api.model\npackage reward\n\nimport lib.crypto.Hash\n\nfinal case class ActivityRewardLog(\n    activitySnapshot: ActivitySnapshot,\n    txHash: Hash.Value[TransactionWithResult],\n)\n"
  },
  {
    "path": "modules/api/src/main/scala/io/leisuremeta/chain/api/model/reward/ActivitySnapshot.scala",
    "content": "package io.leisuremeta.chain\npackage api.model\npackage reward\n\nimport java.time.Instant\n\nimport lib.datatype.BigNat\nimport token.TokenDefinitionId\n\nfinal case class ActivitySnapshot(\n    account: Account,\n    from: Instant,\n    to: Instant,\n    point: BigInt,\n    definitionId: TokenDefinitionId,\n    amount: BigNat,\n    backlogs: Set[Signed.TxHash],\n)\n"
  },
  {
    "path": "modules/api/src/main/scala/io/leisuremeta/chain/api/model/reward/DaoActivity.scala",
    "content": "package io.leisuremeta.chain\npackage api.model.reward\n\nimport io.circe.{Decoder, Encoder}\nimport io.circe.generic.semiauto.*\n\nimport lib.datatype.Utf8\n\nfinal case class DaoActivity(\n    point: BigInt,\n    description: Utf8,\n)\n\nobject DaoActivity:\n  given circeDecoder: Decoder[DaoActivity] = deriveDecoder\n  given circeEncoder: Encoder[DaoActivity] = deriveEncoder\n"
  },
  {
    "path": "modules/api/src/main/scala/io/leisuremeta/chain/api/model/reward/DaoInfo.scala",
    "content": "package io.leisuremeta.chain.api.model\npackage reward\n\nfinal case class DaoInfo(\n    moderators: Set[Account],\n)\n"
  },
  {
    "path": "modules/api/src/main/scala/io/leisuremeta/chain/api/model/reward/OwnershipRewardLog.scala",
    "content": "package io.leisuremeta.chain\npackage api.model\npackage reward\n\nimport lib.crypto.Hash\n\nfinal case class OwnershipRewardLog(\n    ownershipSnapshot: OwnershipSnapshot,\n    txHash: Hash.Value[TransactionWithResult],\n)\n"
  },
  {
    "path": "modules/api/src/main/scala/io/leisuremeta/chain/api/model/reward/OwnershipSnapshot.scala",
    "content": "package io.leisuremeta.chain\npackage api.model\npackage reward\n\nimport java.time.Instant\n\nimport lib.datatype.BigNat\nimport token.TokenDefinitionId\n\nfinal case class OwnershipSnapshot(\n    account: Account,\n    timestamp: Instant,\n    point: BigNat,\n    definitionId: TokenDefinitionId,\n    amount: BigNat,\n)\n"
  },
  {
    "path": "modules/api/src/main/scala/io/leisuremeta/chain/api/model/token/NftInfo.scala",
    "content": "package io.leisuremeta.chain\npackage api.model\npackage token\n\nimport io.circe.{Decoder, Encoder}\nimport io.circe.generic.semiauto.*\n\nimport lib.datatype.{BigNat, UInt256Bytes, Utf8}\n\nfinal case class NftInfo(\n    minter: Account,\n    rarity: Map[Rarity, BigNat],\n    dataUrl: Utf8,\n    contentHash: UInt256Bytes,\n)\nobject NftInfo:\n  given Decoder[NftInfo] = deriveDecoder\n  given Encoder[NftInfo] = deriveEncoder\n"
  },
  {
    "path": "modules/api/src/main/scala/io/leisuremeta/chain/api/model/token/NftInfoWithPrecision.scala",
    "content": "package io.leisuremeta.chain\npackage api.model\npackage token\n\nimport io.circe.{Decoder, Encoder}\nimport io.circe.generic.semiauto.*\n\nimport lib.datatype.{BigNat, UInt256Bytes, Utf8}\n\nfinal case class NftInfoWithPrecision(\n    minter: Account,\n    rarity: Map[Rarity, BigNat],\n    precision: BigNat,\n    dataUrl: Utf8,\n    contentHash: UInt256Bytes,\n)\n\nobject NftInfoWithPrecision:\n  def fromNftInfo(nftInfo: NftInfo): NftInfoWithPrecision =\n    NftInfoWithPrecision(\n      nftInfo.minter,\n      nftInfo.rarity,\n      BigNat.Zero,\n      nftInfo.dataUrl,\n      nftInfo.contentHash,\n    )\n\n  given Decoder[NftInfoWithPrecision] = deriveDecoder\n  given Encoder[NftInfoWithPrecision] = deriveEncoder\n"
  },
  {
    "path": "modules/api/src/main/scala/io/leisuremeta/chain/api/model/token/NftState.scala",
    "content": "package io.leisuremeta.chain\npackage api.model\npackage token\n\nimport lib.crypto.Hash\nimport lib.datatype.{BigNat, Utf8}\n\nfinal case class NftState(\n    tokenId: TokenId,\n    tokenDefinitionId: TokenDefinitionId,\n    rarity: Rarity,\n    weight: BigNat,\n    currentOwner: Account,\n    memo: Option[Utf8],\n    lastUpdateTx: Hash.Value[TransactionWithResult],\n    previousState: Option[Hash.Value[TransactionWithResult]],\n)\n"
  },
  {
    "path": "modules/api/src/main/scala/io/leisuremeta/chain/api/model/token/Rarity.scala",
    "content": "package io.leisuremeta.chain\npackage api.model\npackage token\n\nimport io.circe.{Decoder, Encoder, KeyDecoder, KeyEncoder}\nimport sttp.tapir.Schema\n\nimport lib.codec.byte.{ByteDecoder, ByteEncoder}\nimport lib.datatype.Utf8\n\nopaque type Rarity = Utf8\nobject Rarity:\n  def apply(value: Utf8): Rarity = value\n\n  extension (a: Rarity) def utf8: Utf8 = a\n\n  given Encoder[Rarity] = Utf8.utf8CirceEncoder\n  given Decoder[Rarity] = Utf8.utf8CirceDecoder\n\n  given KeyEncoder[Rarity] = Utf8.utf8CirceKeyEncoder\n  given KeyDecoder[Rarity] = Utf8.utf8CirceKeyDecoder\n  given Schema[Rarity]     = Schema.string\n\n  given ByteDecoder[Rarity] = Utf8.utf8ByteDecoder.map(Rarity(_))\n  given ByteEncoder[Rarity] = Utf8.utf8ByteEncoder.contramap(_.utf8)\n"
  },
  {
    "path": "modules/api/src/main/scala/io/leisuremeta/chain/api/model/token/SnapshotState.scala",
    "content": "package io.leisuremeta.chain\npackage api.model\npackage token\n\nimport java.time.Instant\nimport io.circe.{Decoder, Encoder}\nimport io.circe.generic.semiauto.*\nimport sttp.tapir.{Codec, DecodeResult, Schema}\nimport sttp.tapir.CodecFormat.TextPlain\n\nimport lib.codec.byte.{ByteDecoder, ByteEncoder}\nimport lib.datatype.{BigNat, Utf8}\n\nfinal case class SnapshotState(\n    snapshotId: SnapshotState.SnapshotId,\n    createdAt: Instant,\n    txHash: Signed.TxHash,\n    memo: Option[Utf8],\n)\n\nobject SnapshotState:\n  opaque type SnapshotId = BigNat\n  object SnapshotId:\n    def apply(value: BigNat): SnapshotId = value\n\n    given snapshotIdByteEncoder: ByteEncoder[SnapshotId] =\n      BigNat.bignatByteEncoder\n    given snapshotIdByteDecoder: ByteDecoder[SnapshotId] =\n      BigNat.bignatByteDecoder\n\n    given snapshotIdCirceDecoder: Decoder[SnapshotId] =\n      BigNat.bignatCirceDecoder\n    given snapshotIdCirceEncoder: Encoder[SnapshotId] =\n      BigNat.bignatCirceEncoder\n\n    given schema: Schema[SnapshotId] = Schema.schemaForBigInt\n      .map[BigNat]: (bigint: BigInt) =>\n        BigNat.fromBigInt(bigint).toOption\n      .apply: (bignat: BigNat) =>\n        bignat.toBigInt\n\n    given tapirCodec: Codec[String, SnapshotId, TextPlain] =\n      Codec.string\n        .mapDecode: (s: String) =>\n          BigNat.fromBigInt(BigInt(s)) match\n            case Right(bignat) => DecodeResult.Value(bignat)\n            case Left(msg)     => DecodeResult.Error(s, new Exception(msg))\n        .apply: (bignat: BigNat) =>\n          bignat.toBigInt.toString(10)\n\n    val Zero: SnapshotId = BigNat.Zero\n\n    extension (id: SnapshotId)\n      def inc: SnapshotId      = increase\n      def increase: SnapshotId = BigNat.add(id, BigNat.One)\n"
  },
  {
    "path": "modules/api/src/main/scala/io/leisuremeta/chain/api/model/token/TokenDefinition.scala",
    "content": "package io.leisuremeta.chain\npackage api.model\npackage token\n\nimport lib.datatype.{BigNat, Utf8}\n\nfinal case class TokenDefinition(\n    id: TokenDefinitionId,\n    name: Utf8,\n    symbol: Option[Utf8],\n    adminGroup: Option[GroupId],\n    totalAmount: BigNat,\n    nftInfo: Option[NftInfoWithPrecision],\n)\n"
  },
  {
    "path": "modules/api/src/main/scala/io/leisuremeta/chain/api/model/token/TokenDefinitionId.scala",
    "content": "package io.leisuremeta.chain\npackage api.model\npackage token\n\nimport cats.Eq\n\nimport io.circe.{Decoder, Encoder, KeyDecoder, KeyEncoder}\nimport sttp.tapir.{Codec, DecodeResult, Schema}\nimport sttp.tapir.CodecFormat.TextPlain\n\nimport lib.codec.byte.{ByteDecoder, ByteEncoder}\nimport lib.datatype.Utf8\n\nopaque type TokenDefinitionId = Utf8\nobject TokenDefinitionId:\n  def apply(utf8: Utf8): TokenDefinitionId = utf8\n\n  extension (a: TokenDefinitionId)\n    def utf8: Utf8 = a\n\n  given Decoder[TokenDefinitionId] = Utf8.utf8CirceDecoder\n  given Encoder[TokenDefinitionId] = Utf8.utf8CirceEncoder\n\n  given KeyDecoder[TokenDefinitionId] = Utf8.utf8CirceKeyDecoder\n  given KeyEncoder[TokenDefinitionId] = Utf8.utf8CirceKeyEncoder\n\n  given Schema[TokenDefinitionId] = Schema.string\n\n  given ByteDecoder[TokenDefinitionId] = Utf8.utf8ByteDecoder.map(TokenDefinitionId(_))\n  given ByteEncoder[TokenDefinitionId] = Utf8.utf8ByteEncoder.contramap(_.utf8)\n\n  given Codec[String, TokenDefinitionId, TextPlain] = Codec.string.mapDecode{ (s: String) =>\n    Utf8.from(s) match\n      case Left(e) => DecodeResult.Error(s, e)\n      case Right(a) => DecodeResult.Value(TokenDefinitionId(a))\n  }(_.utf8.value)\n\n  given Eq[TokenDefinitionId] = Eq.fromUniversalEquals\n"
  },
  {
    "path": "modules/api/src/main/scala/io/leisuremeta/chain/api/model/token/TokenDetail.scala",
    "content": "package io.leisuremeta.chain\npackage api.model.token\n\nimport io.circe.{Decoder, Encoder}\nimport io.circe.generic.semiauto.*\n\nimport lib.datatype.BigNat\n\nsealed trait TokenDetail\n\nobject TokenDetail:\n  final case class FungibleDetail(amount: BigNat) extends TokenDetail\n  final case class NftDetail(tokenId: TokenId) extends TokenDetail\n\n  given tokenDetailCirceEncoder: Encoder[TokenDetail] = deriveEncoder\n  given tokenDetailCirceDecoder: Decoder[TokenDetail] = deriveDecoder\n"
  },
  {
    "path": "modules/api/src/main/scala/io/leisuremeta/chain/api/model/token/TokenId.scala",
    "content": "package io.leisuremeta.chain\npackage api.model\npackage token\n\nimport io.circe.{Decoder, Encoder, KeyDecoder, KeyEncoder}\nimport sttp.tapir.{Codec, DecodeResult, Schema}\nimport sttp.tapir.CodecFormat.TextPlain\n\nimport lib.codec.byte.{ByteDecoder, ByteEncoder}\nimport lib.datatype.Utf8\n\nopaque type TokenId = Utf8\nobject TokenId:\n  def apply(utf8: Utf8): TokenId = utf8\n\n  extension (a: TokenId)\n    def utf8: Utf8 = a\n\n  given Decoder[TokenId] = Utf8.utf8CirceDecoder\n  given Encoder[TokenId] = Utf8.utf8CirceEncoder\n  given Schema[TokenId] = Schema.string\n\n  given KeyDecoder[TokenId] = Utf8.utf8CirceKeyDecoder\n  given KeyEncoder[TokenId] = Utf8.utf8CirceKeyEncoder\n\n  given ByteDecoder[TokenId] = Utf8.utf8ByteDecoder.map(TokenId(_))\n  given ByteEncoder[TokenId] = Utf8.utf8ByteEncoder.contramap(_.utf8)\n\n  given Codec[String, TokenId, TextPlain] = Codec.string.mapDecode{ (s: String) =>\n    Utf8.from(s) match\n      case Left(e) => DecodeResult.Error(s, e)\n      case Right(a) => DecodeResult.Value(TokenId(a))\n  }(_.utf8.value)\n\n  given cats.Eq[TokenId] = cats.Eq.fromUniversalEquals\n"
  },
  {
    "path": "modules/api/src/main/scala/io/leisuremeta/chain/api/model/voting/Proposal.scala",
    "content": "package io.leisuremeta.chain\npackage api.model\npackage voting\n\nimport java.time.Instant\nimport lib.datatype.{BigNat, Utf8}\nimport token.TokenDefinitionId\n\nfinal case class Proposal(\n    createdAt: Instant,\n    proposalId: ProposalId,\n    title: Utf8,\n    description: Utf8,\n    votingPower: Map[TokenDefinitionId, BigNat],\n    voteStart: Instant,\n    voteEnd: Instant,\n    voteType: VoteType,\n    voteOptions: Map[Utf8, Utf8],\n    quorum: BigNat,\n    passThresholdNumer: BigNat,\n    passThresholdDenom: BigNat,\n    isActive: Boolean,\n)\n"
  },
  {
    "path": "modules/api/src/main/scala/io/leisuremeta/chain/api/model/voting/ProposalId.scala",
    "content": "package io.leisuremeta.chain\npackage api.model.voting\n\nimport cats.Eq\nimport io.circe.{Decoder, Encoder}\nimport sttp.tapir.{Codec, DecodeResult, Schema}\nimport sttp.tapir.CodecFormat.TextPlain\n\nimport lib.codec.byte.{ByteDecoder, ByteEncoder}\nimport lib.datatype.Utf8\n\nopaque type ProposalId = Utf8\n\nobject ProposalId:\n\n  def apply(utf8: Utf8): ProposalId = utf8\n  extension (proposalId: ProposalId)\n    def value: Utf8 = proposalId\n  end extension\n\n  given byteEncoder: ByteEncoder[ProposalId] = Utf8.utf8ByteEncoder\n  given byteDecoder: ByteDecoder[ProposalId] = Utf8.utf8ByteDecoder\n\n  given circeEncoder: Encoder[ProposalId] = Utf8.utf8CirceEncoder\n  given circeDecoder: Decoder[ProposalId] = Utf8.utf8CirceDecoder\n\n  given eq: Eq[ProposalId] = Eq.fromUniversalEquals\n\n  given schema: Schema[ProposalId] = Schema.string\n\n  given bignatCodec: Codec[String, ProposalId, TextPlain] =\n    Codec.string\n      .mapDecode: (s: String) =>\n        Utf8.from(s) match\n          case Left(e)  => DecodeResult.Error(s, e)\n          case Right(v) => DecodeResult.Value(ProposalId(v))\n      .apply: (b: ProposalId) =>\n        b.value.value\n"
  },
  {
    "path": "modules/api/src/main/scala/io/leisuremeta/chain/api/model/voting/VoteType.scala",
    "content": "package io.leisuremeta.chain\npackage api.model.voting\n\nimport scala.util.Try\n\nimport cats.Eq\nimport cats.syntax.either.*\nimport cats.syntax.eq.catsSyntaxEq\nimport io.circe.{Decoder, Encoder}\n\nimport lib.codec.byte.{ByteDecoder, ByteEncoder}\nimport lib.datatype.BigNat\nimport lib.failure.DecodingFailure\n\nenum VoteType(val name: String):\n  case ONE_PERSON_ONE_VOTE extends VoteType(\"ONE_PERSON_ONE_VOTE\")\n  case TOKEN_WEIGHTED      extends VoteType(\"TOKEN_WEIGHTED\")\n  case NFT_BASED           extends VoteType(\"NFT_BASED\")\n\nobject VoteType:\n  given eq: Eq[VoteType]                = Eq.fromUniversalEquals\n  given circeEncoder: Encoder[VoteType] = Encoder.encodeString.contramap(_.name)\n  given circeDecoder: Decoder[VoteType] = Decoder.decodeString.emap: str =>\n    VoteType.values.find(_.name === str).toRight:\n      s\"VoteType $str is not valid\"\n  given byteEncoder: ByteEncoder[VoteType] =\n    BigNat.bignatByteEncoder.contramap: voteType =>\n      BigNat.unsafeFromBigInt(BigInt(voteType.ordinal))\n  given byteDecoder: ByteDecoder[VoteType] =\n    BigNat.bignatByteDecoder.emap: bignat =>\n      Try(VoteType.fromOrdinal(bignat.toBigInt.toInt)).toEither.leftMap: err =>\n        DecodingFailure:\n          s\"VoteType $bignat is not valid: ${err.getMessage}\"\n"
  },
  {
    "path": "modules/api/tx_type.txt",
    "content": "\n모든 트랜잭션 공통 create Transaction\nTransaction\n \t- TokenTx\n\t\t// LM Token\n\t\t- MintFungibleToken\n\t\t- TransferFungibleToken\n\t\t- EntrustFungibleToken\n\t\t- DisposeEntrustedFungibleToken\n\t\t- BurnFungibleToken\n\n\t\t// NFT Token\n\t\t- DefineToken\n\t\t- MintNFT\n\t\t- TransferNFT\n\t\t- EntrustNFT\n\t\t- DisposeEntrustedNFT\n\t\t- BurnNFT\n\n\t- AccountTx\n\t\t- CreateAccount\n\t\t\t=> insertAccount\n\t\t- UpdateAccount\n\t\t\t=> updateAccount\n\t\t- AddPublicKeySummaries\n\n\t- GroupTx\t\t\n\t\t- CreateGroup\n\t\t- AddAccounts\n\n\t- RewardTx\n\t\t- RegisterDao\n\t\t- UpdateDao\n\t\t- RecordActivity\n\t\t- BuildSnapshot\n\t\t- ExecuteAccountReward   (Fungible)\n\t\t- ExecuteTokenReward.   (Fungible) \n\t\t- ExecuteOwnershipReward.    (Fungible)\n\n\nNFT 테이블 NFT_TX 테이블 로 변경.\n\nbackend 서버에서 NFT_activities 리스트는 tx 테이블에서 조회해서 주기.\n\n\n\n"
  },
  {
    "path": "modules/archive/src/main/scala/io/leisuremeta/chain/archive/ArchiveMain.scala",
    "content": "package io.leisuremeta.chain\npackage archive\n\nimport java.nio.file.{Files, Paths, StandardOpenOption}\nimport java.time.Instant\n\nimport scala.concurrent.duration.*\n//import scala.io.Source\n\nimport cats.Monad\nimport cats.data.EitherT\nimport cats.effect.{ExitCode, IO, IOApp}\nimport cats.syntax.bifunctor.*\n//import cats.syntax.eq.*\n//import cats.syntax.flatMap.toFlatMapOps\nimport cats.syntax.functor.*\nimport cats.syntax.traverse.*\n\nimport io.circe.generic.auto.*\nimport io.circe.parser.decode\n//import io.circe.refined.*\nimport io.circe.syntax.*\n\nimport sttp.client3.*\nimport sttp.client3.armeria.cats.ArmeriaCatsBackend\nimport sttp.model.Uri\n\nimport api.model.*\nimport lib.crypto.{Hash, Signature}\nimport lib.datatype.*\n\nfinal case class PBlock(\n    header: PHeader,\n    transactionHashes: Set[Signed.TxHash],\n    votes: Set[Signature],\n)\nfinal case class PHeader(\n    number: BigNat,\n    parentHash: Block.BlockHash,\n    timestamp: Instant,\n)\n\nobject ArchiveMain extends IOApp:\n\n//  val baseUri = \"http://test.chain.leisuremeta.io:8080\"\n//  val baseUri = \"http://localhost:7080\"\n  val baseUri = \"http://localhost:8081\"\n\n  val archiveFileName = \"txs1.archive\"\n\n  def logTxs(contents: String): IO[Unit] = IO.blocking:\n    val path = Paths.get(archiveFileName)\n    val _ = Files.write(\n      path,\n      contents.getBytes,\n      StandardOpenOption.CREATE,\n      StandardOpenOption.WRITE,\n      StandardOpenOption.APPEND,\n    )\n\n  def get[F[_]: Monad, A: io.circe.Decoder](\n      backend: SttpBackend[F, Any],\n  )(uri: Uri): EitherT[F, String, A] = EitherT:\n    basicRequest\n      .get(uri)\n      .send(backend)\n      .map: response =>\n        for\n          body <- response.body\n          a    <- decode[A](body).leftMap(_.getMessage())\n        yield a\n\n  def put[F[_]: Monad](\n      backend: SttpBackend[F, Any],\n  )(uri: Uri)(line: String): EitherT[F, String, List[Signed.TxHash]] =\n    EitherT:\n//      println(s\"Req: $line\")\n\n      basicRequest\n        .post(uri)\n        .body(line)\n        .send(backend)\n        .map: response =>\n//          println(s\"Response: $response\")\n          val result = for\n            body <- response.body\n            a    <- decode[List[Signed.TxHash]](body).leftMap(_.getMessage())\n          yield a\n\n          result\n\n  def getTransaction[F[_]: Monad](\n      backend: SttpBackend[F, Any],\n  )(txHash: Signed.TxHash): EitherT[F, String, TransactionWithResult] =\n    get[F, TransactionWithResult](backend)\n      .apply:\n        uri\"$baseUri/tx/${txHash.toUInt256Bytes.toBytes.toHex}\"\n      .leftMap: msg =>\n        scribe.error(s\"error msg: $msg\")\n        msg\n\n  def getBlock[F[_]: Monad](\n      backend: SttpBackend[F, Any],\n  )(blockHash: Block.BlockHash): EitherT[F, String, PBlock] =\n    get[F, PBlock](backend):\n      uri\"$baseUri/block/${blockHash.toUInt256Bytes.toBytes.toHex}\"\n\n  def getStatus[F[_]: Monad](\n      backend: SttpBackend[F, Any],\n  ): EitherT[F, String, NodeStatus] =\n    get[F, NodeStatus](backend)(uri\"$baseUri/status\")\n\n  def loop[F[_]: Monad](\n      backend: SttpBackend[F, Any],\n  )(next: Block.BlockHash, genesis: Block.BlockHash, until: BigInt, count: Long)(\n      run: (\n          BigNat,\n          Block.BlockHash,\n          Set[Signed.TxHash],\n      ) => EitherT[F, String, Unit],\n  ): EitherT[F, String, Long] = for\n    block <- getBlock[F](backend)(next)\n    number = block.header.number.toBigInt\n    _     <- EitherT.pure(scribe.info(s\"block ${number}: $next\"))\n    _ <- EitherT.cond(\n      number > until,\n      (),\n      s\"block number $number is greater than $until\",\n    )\n    _ <- run(block.header.number, next, block.transactionHashes).recover: msg =>\n      scribe.error(s\"error msg: $msg\")\n      ()\n    count1 <- loop[F](backend)(block.header.parentHash, genesis, until, count + 1)(run)\n  yield count1\n\n  def run(args: List[String]): IO[ExitCode] =\n    val until = BigInt(\"0\")\n//    val until = BigInt(\"12751183\")\n    for _ <- ArmeriaCatsBackend\n        .resource[IO]:\n          SttpBackendOptions.Default.connectionTimeout(10.minutes)\n        .use: backend =>\n          val program = for\n            status <- getStatus[IO](backend)\n            block  <- getBlock[IO](backend)(status.bestHash)\n            count <- loop[IO](backend)(\n              status.bestHash,\n              status.genesisHash,\n              until,\n              0,\n            ): (blockNumber, blockHash, txSet) =>\n              txSet.toList\n                .sortBy(_.toUInt256Bytes.toHex)\n                .traverse: txHash =>\n                  for\n                    tx <- getTransaction[IO](backend)(txHash)\n                    txString = tx.signedTx.asJson.noSpaces\n                    _ <- EitherT.right:\n                      logTxs:\n                        s\"$blockNumber\\t${txHash.toUInt256Bytes.toHex}\\t$txString\\n\"\n                  yield ()\n                .as(())\n          yield println(s\"total number of block: $count\")\n\n//        val from = 0\n//        val to = 1000000\n//        Source.fromFile(archiveFileName).getLines.to(LazyList).zipWithIndex.take(to).drop(from).traverse{\n//          (line, i) =>\n////            println(s\"$i: $line\")\n//            put[IO](backend)(uri\"$baseUri/tx\")(line).recover{\n//              case msg: String =>\n//                println(s\"Error: $msg\")\n//                println(s\"Error Request: $line\")\n//                Nil\n//            }\n//        }\n          program.value\n    yield ExitCode.Success\n"
  },
  {
    "path": "modules/bulk-insert/src/main/scala/io/leisuremeta/chain/bulkinsert/BulkInsertMain.scala",
    "content": "package io.leisuremeta.chain\npackage bulkinsert\n\nimport scala.io.Source\n\nimport cats.data.{EitherT, Kleisli}\nimport cats.effect.{Async, ExitCode, IO, IOApp, Resource}\nimport cats.syntax.all.*\n\nimport fs2.Stream\nimport io.circe.parser.decode\nimport scodec.bits.ByteVector\n\nimport api.model.{Block, Signed, StateRoot}\nimport api.model.TransactionWithResult.ops.*\nimport lib.crypto.{CryptoOps, KeyPair}\nimport lib.crypto.Hash.ops.*\nimport lib.crypto.Sign.ops.*\nimport lib.datatype.BigNat\nimport lib.merkle.*\nimport lib.merkle.MerkleTrie.NodeStore\nimport node.NodeConfig\nimport node.dapp.{PlayNommDApp, PlayNommDAppFailure, PlayNommState}\nimport node.repository.{BlockRepository, StateRepository, TransactionRepository}\nimport node.service.NodeInitializationService\n\ndef bulkInsert[F[_]\n  : Async: BlockRepository: TransactionRepository: StateRepository: PlayNommState: InvalidTxLogger](\n    config: NodeConfig,\n    source: Source,\n    from: String,\n    until: String,\n): EitherT[F, String, Unit] = for\n  bestBlock <- NodeInitializationService\n    .initialize[F](config.genesis.timestamp)\n  merkleState = MerkleTrieState.fromRootOption(bestBlock.header.stateRoot.main)\n  indexWithTxsStream = Stream\n    .fromIterator[EitherT[F, String, *]](source.getLines(), 1)\n    .filterNot(_ === \"[]\")\n    .zipWithIndex\n    .evalMap: (line, index) =>\n      if index % 1000L === 0L then scribe.info(s\"Processing line #$index\")\n      line.split(\"\\t\").toList match\n        case blockNumber :: txHash :: jsonString :: Nil =>\n          EitherT\n            .fromEither[F]:\n              decode[Signed.Tx](jsonString)\n            .leftMap: e =>\n              scribe.error(s\"Error decoding line #$blockNumber: $txHash: $jsonString: $e\")\n              e.getMessage()\n            .map(tx => (blockNumber, tx))\n        case _ =>\n          scribe.error(s\"Error parsing line: $line\")\n          EitherT.leftT[F, (String, Signed.Tx)](s\"Error parsing line: $line\")\n    .groupAdjacentBy(_._1)\n    .dropWhile(_._1 =!= from)\n    .takeWhile(_._1 =!= until)\n    .map:\n      case (blockNumber, chunk) =>\n        (blockNumber, chunk.toList.map(_._2))\n  localKeyPair: KeyPair =\n    val privateKey = scala.sys.env\n      .get(\"LMNODE_PRIVATE_KEY\")\n      .map(BigInt(_, 16))\n      .orElse(config.local.`private`)\n      .get\n    CryptoOps.fromPrivate(privateKey)\n  stateStream = indexWithTxsStream.evalMapAccumulate((bestBlock, merkleState)):\n    case ((previousBlock, ms), (blockNumber, txs)) =>\n      val program = for\n        result <- Stream\n          .fromIterator[EitherT[F, PlayNommDAppFailure, *]](txs.iterator, 1)\n          .evalMapAccumulate(ms): (ms, tx) =>\n//            scribe.info(s\"signer: ${tx.sig.account}\")\n//            scribe.info(s\"tx: ${tx.value}\")\n//            PlayNommDApp[F](tx)\n//              .run(ms)\n//              .map: (ms, txWithResult) =>\n//                (ms, Option(txWithResult))\n//              .recoverWith: _ =>\n//                RecoverTx(ms, tx)\n            RecoverTx(ms, tx)\n              .recoverWith: failure =>\n                PlayNommDApp[F](tx)\n                  .run(ms)\n                  .map: (ms, txWithResult) =>\n                    (ms, Option(txWithResult))\n                  .leftMap: failure2 =>\n                    scribe.info(s\"Error: $failure\")\n                    failure2\n          .map: result =>\n            if BigInt(blockNumber) % 100 === 0 then scribe.info(s\"#$blockNumber: ${result._1.root}\")\n            result\n          .compile\n          .toList\n          .leftMap: e =>\n            scribe.error(s\"Error building txs #$blockNumber: $txs: $e\")\n            e\n          .leftSemiflatTap: e =>\n            StateRepository[F]\n              .put(ms)\n              .leftMap: f =>\n                scribe.error(s\"Fail to put state: ${f.msg}\")\n              .value\n        (states, txWithResultOptions) = result.unzip\n        finalState                    = states.last\n        txWithResults                 = txWithResultOptions.flatten\n        txHashes                      = txWithResults.map(_.toHash)\n        txState = txs\n          .map(_.toHash)\n          .sortBy(_.toUInt256Bytes.toBytes)\n          .foldLeft(MerkleTrieState.empty): (state, txHash) =>\n            given idNodeStore: NodeStore[cats.Id] = Kleisli.pure(None)\n            MerkleTrie\n              .put[cats.Id](\n                txHash.toUInt256Bytes.toBytes.toNibbles,\n                ByteVector.empty,\n              )\n              .runS(state)\n              .value\n              .getOrElse(state)\n        stateRoot1 = StateRoot(finalState.root)\n        now        = (previousBlock.header.timestamp :: txs.map(_.value.createdAt))\n          .maxBy(_.getEpochSecond())\n        blockNumber = BigNat.add(previousBlock.header.number, BigNat.One)\n        header = Block.Header(\n          number = blockNumber,\n          parentHash = previousBlock.toHash,\n          stateRoot = stateRoot1,\n          transactionsRoot = txState.root,\n          timestamp = now,\n        )\n        sig <- EitherT\n          .fromEither(header.toHash.signBy(localKeyPair))\n          .leftMap: msg =>\n            scribe.error(s\"Fail to sign header: $msg\")\n            PlayNommDAppFailure.internal(s\"Fail to sign header: $msg\")\n        block = Block(\n          header = header,\n          transactionHashes = txHashes.toSet.map(_.toSignedTxHash),\n          votes = Set(sig),\n        )\n        _ <- BlockRepository[F]\n          .put(block)\n          .leftMap: e =>\n            scribe.error(s\"Fail to put block: $e\")\n            PlayNommDAppFailure.internal(s\"Fail to put block: ${e.msg}\")\n        finalState1 <-\n          if blockNumber.toBigInt % 10000 === 0 then          \n            StateRepository[F]\n              .put(finalState)\n              .leftMap: e =>\n                scribe.error(s\"Fail to put state: $e\")\n                PlayNommDAppFailure.internal:\n                  s\"Fail to put state: ${e.msg}\"\n              .map: _ =>\n                MerkleTrieState.fromRootOption(finalState.root)\n          else EitherT.pure(finalState)\n        _ <- txWithResults.traverse: txWithResult =>\n          EitherT.liftF:\n            TransactionRepository[F].put(txWithResult)\n      yield ((block, finalState1), (blockNumber, txWithResults))\n\n      program\n        .leftMap: e =>\n          scribe.error(s\"Error applying txs #$blockNumber: $txs: $e\")\n          e.msg\n  result <- stateStream.last.compile.toList\n  finalState <- EitherT.fromOption[F](\n    result.headOption.flatten.map(_._1._2),\n    \"Fail to get final state\",\n  )\n  _ <- StateRepository[F]\n    .put(finalState)\n    .leftMap: e =>\n      scribe.error(s\"Fail to put state: $e\")\n      s\"Fail to put state: ${e.msg}\"\nyield\n  scribe.info(s\"Last: ${result.flatten}\")\n  ()\n\ndef fileResource[F[_]: Async](fileName: String): Resource[F, Source] =\n  Resource.fromAutoCloseable:\n    Async[F].delay(Source.fromFile(fileName))\n\n\nobject BulkInsertMain extends IOApp:\n\n  val from = \"1\"\n//  val from = \"3513172\"\n \n  val until =   \"100000000\"\n//  val until = \"3513173\"\n  \n  override def run(args: List[String]): IO[ExitCode] =\n\n    import com.typesafe.config.ConfigFactory\n    import node.NodeMain\n    import node.repository.StateRepository.given\n\n    NodeConfig\n      .load[IO](IO.blocking(ConfigFactory.load))\n      .value\n      .flatMap:\n        case Left(err) =>\n          IO(println(err)).as(ExitCode.Error)\n        case Right(config) =>\n          scribe.info(s\"Loaded config: $config\")\n\n          val program = for\n            source                          <- fileResource[IO](\"txs.archive\")\n            given BlockRepository[IO]       <- NodeMain.getBlockRepo(config)\n            given TransactionRepository[IO] <- NodeMain.getTransactionRepo(config)\n            given StateRepository[IO]       <- NodeMain.getStateRepo(config)\n            given InvalidTxLogger[IO] <- InvalidTxLogger.file[IO]:\n              \"invalid-txs.csv\"\n            result <- Resource.eval:\n              given PlayNommState[IO] = PlayNommState.build[IO]\n              bulkInsert[IO](config, source, from, until).value.map:\n                case Left(err) =>\n                  scribe.error(s\"Error: $err\")\n                  ExitCode.Error\n                case Right(_) =>\n                  scribe.info(s\"Done\")\n                  ExitCode.Success\n          yield result\n\n          program.use(IO.pure)\n\n//          fileResource[IO](\"txs.archive\")\n//            .use: source =>\n//              NftBalanceState.build(source).flatTap: state =>\n//                IO.pure:\n//                  state.free.foreach(println)\n//                  state.locked.foreach(println)\n//              FungibleBalanceState.build(source).flatTap: state =>\n//                IO.pure:\n//                  state.free.foreach(println)\n//                  state.locked.foreach(println)\n//            .as(ExitCode.Success)\n"
  },
  {
    "path": "modules/bulk-insert/src/main/scala/io/leisuremeta/chain/bulkinsert/FungibleBalanceState.scala",
    "content": "package io.leisuremeta.chain\npackage bulkinsert\n\nimport scala.io.Source\n\nimport cats.data.{EitherT}\nimport cats.effect.{IO, Sync}\nimport cats.syntax.all.*\n\nimport fs2.Stream\nimport io.circe.parser.decode\n\nimport api.model.{Account, Signed, Transaction}\nimport lib.crypto.Hash\nimport lib.crypto.Hash.ops.*\nimport lib.datatype.BigNat\n\nfinal case class FungibleBalanceState(\n    free: Map[Account, Map[Signed.TxHash, (Signed.Tx, BigNat)]],\n    locked: Map[Account, Map[Signed.TxHash, (Signed.Tx, BigNat, Account)]],\n):\n  def addFree(\n      account: Account,\n      tx: Signed.Tx,\n      amount: BigNat,\n  ): FungibleBalanceState =\n    val txHash     = tx.toHash\n    val accountMap = free.getOrElse(account, Map.empty)\n    val txMap      = accountMap.updated(txHash, (tx, amount))\n    copy(free = free.updated(account, txMap))\n  def addLocked(\n      account: Account,\n      tx: Signed.Tx,\n      amount: BigNat,\n      from: Account,\n  ): FungibleBalanceState =\n    val txHash     = tx.toHash\n    val accountMap = locked.getOrElse(account, Map.empty)\n    val txMap      = accountMap.updated(txHash, (tx, amount, from))\n    copy(locked = locked.updated(account, txMap))\n  def removeFree(\n      account: Account,\n      txHash: Signed.TxHash,\n  ): FungibleBalanceState =\n    val accountMap = free.getOrElse(account, Map.empty)\n    val txMap      = accountMap.removed(txHash)\n    copy(free = free.updated(account, txMap))\n  def removeLocked(\n      account: Account,\n      txHash: Signed.TxHash,\n  ): FungibleBalanceState =\n    val accountMap = locked.getOrElse(account, Map.empty)\n    val txMap      = accountMap.removed(txHash)\n    copy(locked = locked.updated(account, txMap))\n\nobject FungibleBalanceState:\n  def empty: FungibleBalanceState = FungibleBalanceState(Map.empty, Map.empty)\n\n  def build(\n      source: Source,\n  ): IO[FungibleBalanceState] =\n    val indexWithTxsStream = Stream\n      .fromIterator[EitherT[IO, String, *]](source.getLines(), 1)\n      .evalMap: line =>\n        line.split(\"\\t\").toList match\n          case blockNumber :: txHash :: jsonString :: Nil =>\n            EitherT\n              .fromEither[IO]:\n                decode[Signed.Tx](jsonString)\n              .leftMap: e =>\n                scribe.error(s\"Error decoding line #$blockNumber: $txHash: $jsonString: $e\")\n                e.getMessage()\n              .map(tx => (BigInt(blockNumber).longValue, tx))\n          case _ =>\n            scribe.error(s\"Error parsing line: $line\")\n            EitherT.leftT[IO, (Long, Signed.Tx)](s\"Error parsing line: $line\")\n      .groupAdjacentBy(_._1)\n      .map:\n        case (blockNumber, chunk) =>\n          (blockNumber, chunk.toList.map(_._2))\n\n    //val indexWithTxsStream = Stream\n    //  .fromIterator[EitherT[IO, String, *]](source.getLines(), 1)\n    //  .zipWithIndex\n    //  .filterNot(_._1 === \"[]\")\n    //  .evalMap: (line, index) =>\n    //    EitherT\n    //      .fromEither[IO]:\n    //        decode[Seq[Signed.Tx]](line)\n    //      .leftMap: e =>\n    //        scribe.error(s\"Error decoding line #$index: $line: $e\")\n    //        e.getMessage()\n    //      .map(txs => (index, txs))\n\n    def logWrongTx(\n        from: Account,\n        amount: BigInt,\n        tx: Signed.Tx,\n        inputs: Map[Signed.TxHash, BigNat],\n    ): Unit =\n      println(s\"$from\\t$amount\\t${tx.value.toHash}\\t$tx\")\n      inputs.foreach { (txHash, amount) => println(s\"===> $txHash : $amount\") }\n\n    val stateStream = indexWithTxsStream.evalMapAccumulate[EitherT[\n      IO,\n      String,\n      *,\n    ], FungibleBalanceState, (Long, Seq[Signed.Tx])](\n      FungibleBalanceState.empty,\n    ):\n      case (balanceState, (index, txs)) =>\n//      if index % 10000 === 0 then\n//        println(s\"Index: $index\")\n        val finalState = txs.foldLeft(balanceState): (state, tx) =>\n          tx.value match\n            case fb: Transaction.FungibleBalance =>\n              fb match\n                case mt: Transaction.TokenTx.MintFungibleToken =>\n                  mt.outputs.foldLeft(state):\n                    case (state, (to, amount)) =>\n                      state.addFree(to, tx, amount)\n                case tt: Transaction.TokenTx.TransferFungibleToken =>\n                  val inputList = tt.inputs.toList\n                  val inputAmounts = inputList.map: inputTxHash =>\n                    state.free\n                      .get(tx.sig.account)\n                      .getOrElse(Map.empty)\n                      .get(inputTxHash)\n                      .fold {\n//                    scribe.error(s\"input $inputTxHash is not exist in tx $txHash\")\n                        BigNat.Zero\n                      }(_._2)\n                  val inputs     = inputList.zip(inputAmounts).toMap\n                  val inputTotal = inputAmounts.fold(BigNat.Zero)(BigNat.add)\n                  val outputTotal =\n                    tt.outputs.map(_._2).fold(BigNat.Zero)(BigNat.add)\n                  val remainder = inputTotal.toBigInt - outputTotal.toBigInt\n                  if remainder < 0 then\n                    logWrongTx(\n                      tx.sig.account,\n                      -remainder,\n                      tx,\n                      inputs,\n                    )\n                  val afterRemovingInput = tt.inputs.foldLeft(state):\n                    case (state, inputTxHash) =>\n                      state.removeFree(tx.sig.account, inputTxHash)\n                  val afterAddingOutput =\n                    tt.outputs.foldLeft(afterRemovingInput):\n                      case (state, (to, amount)) =>\n                        state.addFree(to, tx, amount)\n                  afterAddingOutput\n                case bt: Transaction.TokenTx.BurnFungibleToken =>\n                  val inputList = bt.inputs.toList\n                  val inputAmounts = inputList.map: inputTxHash =>\n                    state.free\n                      .get(tx.sig.account)\n                      .getOrElse(Map.empty)\n                      .get(inputTxHash)\n                      .fold {\n//                    scribe.error(s\"input $inputTxHash is not exist in tx $txHash\")\n                        BigNat.Zero\n                      }(_._2)\n                  val inputs     = inputList.zip(inputAmounts).toMap\n                  val inputTotal = inputAmounts.fold(BigNat.Zero)(BigNat.add)\n                  val burnAmount = bt.amount.toBigInt\n                  val remainder  = inputTotal.toBigInt - burnAmount\n                  if remainder < 0 then\n                    logWrongTx(\n                      tx.sig.account,\n                      -remainder,\n                      tx,\n                      inputs,\n                    )\n                  val afterRemovingInput = bt.inputs.foldLeft(state):\n                    case (state, inputTxHash) =>\n                      state.removeFree(tx.sig.account, inputTxHash)\n                  afterRemovingInput.addFree(\n                    tx.sig.account,\n                    tx,\n                    BigNat.unsafeFromBigInt(remainder.max(0)),\n                  )\n                case et: Transaction.TokenTx.EntrustFungibleToken =>\n                  val inputList = et.inputs.toList\n                  val inputAmounts = inputList.map: inputTxHash =>\n                    state.free\n                      .get(tx.sig.account)\n                      .getOrElse(Map.empty)\n                      .get(inputTxHash)\n                      .fold {\n//                    scribe.error(s\"input $inputTxHash is not exist in tx $txHash\")\n                        BigNat.Zero\n                      }(_._2)\n                  val inputs        = inputList.zip(inputAmounts).toMap\n                  val inputTotal    = inputAmounts.fold(BigNat.Zero)(BigNat.add)\n                  val entrustAmount = et.amount.toBigInt\n                  val remainder     = inputTotal.toBigInt - entrustAmount\n                  if remainder < 0 then\n                    logWrongTx(\n                      tx.sig.account,\n                      -remainder,\n                      tx,\n                      inputs,\n                    )\n                  val afterRemovingInput = et.inputs.foldLeft(state):\n                    case (state, inputTxHash) =>\n                      state.removeFree(tx.sig.account, inputTxHash)\n                  val afterAddingOutput =\n                    afterRemovingInput\n                      .addLocked(et.to, tx, et.amount, tx.sig.account)\n                      .addFree(\n                        tx.sig.account,\n                        tx,\n                        BigNat.unsafeFromBigInt(remainder.max(0)),\n                      )\n                  afterAddingOutput\n                case dt: Transaction.TokenTx.DisposeEntrustedFungibleToken =>\n                  val inputList = dt.inputs.toList\n                  val inputAmounts = inputList.map: inputTxHash =>\n                    state.locked\n                      .get(tx.sig.account)\n                      .getOrElse(Map.empty)\n                      .get(inputTxHash)\n                      .fold {\n//                    scribe.error(s\"input $inputTxHash is not exist in tx $txHash\")\n                        BigNat.Zero\n                      }(_._2)\n                  val inputs     = inputList.zip(inputAmounts).toMap\n                  val inputTotal = inputAmounts.fold(BigNat.Zero)(BigNat.add)\n                  val outputTotal =\n                    dt.outputs.map(_._2).fold(BigNat.Zero)(BigNat.add)\n                  val remainder = inputTotal.toBigInt - outputTotal.toBigInt\n                  if remainder < 0 then\n                    logWrongTx(\n                      tx.sig.account,\n                      -remainder,\n                      tx,\n                      inputs,\n                    )\n                  val afterRemovingInput = dt.inputs.foldLeft(state):\n                    case (state, inputTxHash) =>\n                      state.removeLocked(tx.sig.account, inputTxHash)\n                  val afterAddingOutput =\n                    dt.outputs.foldLeft(afterRemovingInput):\n                      case (state, (to, amount)) =>\n                        state.addFree(to, tx, amount)\n                  afterAddingOutput\n                case or: Transaction.RewardTx.OfferReward =>\n                  val inputList = or.inputs.toList\n                  val inputAmounts = inputList.map: inputTxHash =>\n                    state.free\n                      .get(tx.sig.account)\n                      .getOrElse(Map.empty)\n                      .get(inputTxHash)\n                      .fold {\n//                    scribe.error(s\"input $inputTxHash is not exist in tx $txHash\")\n                        BigNat.Zero\n                      }(_._2)\n                  val inputs     = inputList.zip(inputAmounts).toMap\n                  val inputTotal = inputAmounts.fold(BigNat.Zero)(BigNat.add)\n                  val outputTotal =\n                    or.outputs.map(_._2).fold(BigNat.Zero)(BigNat.add)\n                  val remainder = inputTotal.toBigInt - outputTotal.toBigInt\n                  if remainder < 0 then\n                    logWrongTx(\n                      tx.sig.account,\n                      -remainder,\n                      tx,\n                      inputs,\n                    )\n                  val afterRemovingInput = or.inputs.foldLeft(state):\n                    case (state, inputTxHash) =>\n                      state.removeFree(tx.sig.account, inputTxHash)\n                  val afterAddingOutput =\n                    or.outputs.foldLeft(afterRemovingInput):\n                      case (state, (to, amount)) =>\n                        state.addFree(to, tx, amount)\n                  afterAddingOutput\n                case er: Transaction.RewardTx.ExecuteReward =>\n                  ???\n                case er: Transaction.RewardTx.ExecuteOwnershipReward =>\n                  ???\n            case _ => state\n        EitherT.pure[IO, String]((finalState, (index, txs)))\n\n    stateStream.last.compile.toList\n      .map(_.headOption.flatten.get._1)\n      .value\n      .map:\n        case Left(err) =>\n          scribe.error(s\"Error building balance map: $err\")\n          FungibleBalanceState.empty\n        case Right(balanceState) =>\n          balanceState\n"
  },
  {
    "path": "modules/bulk-insert/src/main/scala/io/leisuremeta/chain/bulkinsert/InvalidTx.scala",
    "content": "package io.leisuremeta.chain\npackage bulkinsert\n\nimport java.time.Instant\n\nimport cats.effect.{Async, Resource}\nimport cats.effect.std.Console\n\nimport api.model.{Account, Transaction}\nimport api.model.token.{TokenId}\nimport lib.datatype.BigNat\nimport lib.crypto.Hash.ops.*\n\nfinal case class InvalidTx(\n    signer: Account,\n    reason: InvalidReason,\n    amountToBurn: BigNat,\n    tx: Transaction,\n    wrongNftInput: Option[TokenId] = None,\n    createdAt: Instant,\n    memo: String = \"\",\n):\n  def txType: String = tx.getClass.getSimpleName\n\nenum InvalidReason:\n  case OutputMoreThanInput, InputAlreadyUsed, BalanceNotExist, CanceledBalance,\n    NoNftInfo\n\ntrait InvalidTxLogger[F[_]]:\n  def log(invalidTx: InvalidTx): F[Unit]\n\nobject InvalidTxLogger:\n  def apply[F[_]: InvalidTxLogger]: InvalidTxLogger[F] = summon\n\n  def console[F[_]: Async: Console]: InvalidTxLogger[F] =\n    invalidTx => Console[F].println(invalidTx)\n\n  def file[F[_]: Async](filename: String): Resource[F, InvalidTxLogger[F]] =\n    Resource\n      .make:\n        import java.io.{File, FileOutputStream, PrintWriter}\n        Async[F].delay(\n          new PrintWriter(new FileOutputStream(new File(filename), true)),\n        )\n      .apply: out =>\n        Async[F].delay:\n          out.flush()\n          out.close()\n      .map: out =>\n        case InvalidTx(signer, reason, amountToBurn, tx, wrongNftInput, createdAt, memo) =>\n          Async[F].delay:\n            val fields = Seq(\n              createdAt,\n              tx.toHash.toUInt256Bytes.toHex,\n              tx.getClass.getSimpleName,\n              signer,\n              reason,\n              amountToBurn,\n              wrongNftInput,\n              memo,\n            )\n            out.println(fields.mkString(\",\"))\n            out.flush()\n"
  },
  {
    "path": "modules/bulk-insert/src/main/scala/io/leisuremeta/chain/bulkinsert/NftBalanceState.scala",
    "content": "package io.leisuremeta.chain\npackage bulkinsert\n\nimport scala.io.Source\n\nimport cats.data.EitherT\nimport cats.effect.IO\nimport cats.syntax.all.*\n\nimport fs2.Stream\nimport io.circe.parser.decode\n\nimport api.model.*\nimport api.model.token.*\nimport lib.crypto.Hash.ops.*\nimport lib.datatype.Utf8\n\nfinal case class NftBalanceState(\n    free: Map[Account, Map[Signed.TxHash, (Signed.Tx, TokenId)]],\n    locked: Map[Account, Map[Signed.TxHash, (Signed.Tx, TokenId, Account)]],\n    tokenOwner: Map[TokenId, Account],\n):\n  def addFree(\n      account: Account,\n      tx: Signed.Tx,\n      tokenId: TokenId,\n  ): NftBalanceState =\n    val txHash     = tx.toHash\n    val accountMap = free.getOrElse(account, Map.empty)\n    val txMap      = accountMap.updated(txHash, (tx, tokenId))\n    val tokenOwner1 = tokenOwner.updated(tokenId, account)\n    copy(free = free.updated(account, txMap), tokenOwner = tokenOwner1)\n\n  def addLocked(\n      account: Account,\n      tx: Signed.Tx,\n      tokenId: TokenId,\n      from: Account,\n  ): NftBalanceState =\n    val txHash     = tx.toHash\n    val accountMap = locked.getOrElse(account, Map.empty)\n    val txMap      = accountMap.updated(txHash, (tx, tokenId, from))\n    copy(locked = locked.updated(account, txMap))\n\n  def removeFree(\n      account: Account,\n      txHash: Signed.TxHash,\n  ): NftBalanceState =\n    val accountMap = free.getOrElse(account, Map.empty)\n    val txMap      = accountMap.removed(txHash)\n    val tokenOwner1 = accountMap.get(txHash)\n      .fold:\n//        println(s\"No locked balance of account $account with $txHash\")\n        tokenOwner\n      .apply: (_, tokenId) =>\n        tokenOwner.removed(tokenId)\n\n    copy(free = free.updated(account, txMap), tokenOwner = tokenOwner1)\n\n  def removeLocked(\n      account: Account,\n      txHash: Signed.TxHash,\n  ): NftBalanceState =\n    val accountMap = locked.getOrElse(account, Map.empty)\n    val txMap      = accountMap.removed(txHash)\n    val tokenOwner1 = accountMap.get(txHash)\n      .fold:\n//        println(s\"No locked balance of account $account with $txHash\")\n        tokenOwner\n      .apply: (_, tokenId, _) =>\n        tokenOwner.removed(tokenId)\n\n    copy(locked = locked.updated(account, txMap), tokenOwner = tokenOwner1)\n\n\nobject NftBalanceState:\n  def empty: NftBalanceState = NftBalanceState(Map.empty, Map.empty, Map.empty)\n\n  def build(\n      source: Source,\n  ): IO[NftBalanceState] =\n    val indexWithTxsStream = Stream\n      .fromIterator[EitherT[IO, String, *]](source.getLines(), 1)\n      .evalMap: line =>\n        line.split(\"\\t\").toList match\n          case blockNumber :: txHash :: jsonString :: Nil =>\n            EitherT\n              .fromEither[IO]:\n                decode[Signed.Tx](jsonString)\n              .leftMap: e =>\n                scribe.error(s\"Error decoding line #$blockNumber: $txHash: $jsonString: $e\")\n                e.getMessage()\n              .map(tx => (BigInt(blockNumber).longValue, tx))\n          case _ =>\n            scribe.error(s\"Error parsing line: $line\")\n            EitherT.leftT[IO, (Long, Signed.Tx)](s\"Error parsing line: $line\")\n      .groupAdjacentBy(_._1)\n      .map:\n        case (blockNumber, chunk) =>\n          (blockNumber, chunk.toList.map(_._2))\n\n//    val indexWithTxsStream = Stream\n//      .fromIterator[EitherT[IO, String, *]](source.getLines(), 1)\n//      .zipWithIndex\n//      .filterNot(_._1 === \"[]\")\n//      .evalMap: (line, index) =>\n//        EitherT\n//          .fromEither[IO]:\n//            decode[Seq[Signed.Tx]](line)\n//          .leftMap: e =>\n//            scribe.error(s\"Error decoding line #$index: $line: $e\")\n//            e.getMessage()\n//          .map(txs => (index, txs))\n\n    def logWrongTx(\n        from: Account,\n        tokenId: TokenId,\n        tx: Signed.Tx,\n    ): Unit =\n      println(s\"No free NFT balance of $from with tokenId $tokenId: ${tx.toHash}: $tx\")\n      ()\n\n    def logWrongEntrustTx(\n        from: Account,\n        tokenId: TokenId,\n        tx: Signed.Tx,\n    ): Unit =\n      println(s\"No entrust NFT balance of $from with tokenId $tokenId: ${tx.toHash}: $tx\")\n      ()\n\n    val stateStream = indexWithTxsStream.evalMapAccumulate[EitherT[IO, String, *], NftBalanceState, (Long, Seq[Signed.Tx])](NftBalanceState.empty):\n      case (balanceState, (index, txs)) =>    \n        val finalState = txs.foldLeft(balanceState): (state, tx) =>\n          tx.value match\n            case mn: Transaction.TokenTx.MintNFT =>\n              state.addFree(mn.output, tx, mn.tokenId)\n            case bn: Transaction.TokenTx.BurnNFT =>\n              state.removeFree(tx.sig.account, bn.input)\n            case tn: Transaction.TokenTx.TransferNFT =>\n              val inputOption = for\n                nftBalance <- state.free.get(tx.sig.account)\n                txAndTokenId <- nftBalance.get(tn.input)\n              yield txAndTokenId\n              inputOption match\n                case Some((inputTx, tokenId)) =>\n                  state\n                    .removeFree(tx.sig.account, tn.input)\n                    .addFree(tn.output, tx, tokenId)\n                case None =>\n                  if tx.sig.account === Account(Utf8.unsafeFrom(\"playnomm\")) then\n                    val currentOwnerOption =\n                      state.free.toSeq\n                        .flatMap: (account, map) =>\n                          map.toSeq.map:\n                            case (txHash, (tx, tokenId)) => (tokenId, account, txHash)\n                        .find(_._1 === tn.tokenId)\n                    currentOwnerOption\n                      .fold(state): (_, owner, txHash) =>\n                        state.removeFree(owner, txHash)\n                      .addFree(tn.output, tx, tn.tokenId)\n                  else\n                    logWrongTx(tx.sig.account, tn.tokenId, tx)\n                    state\n            case en: Transaction.TokenTx.EntrustNFT =>\n              val inputOption = for\n                nftBalance <- state.free.get(tx.sig.account)\n                txAndTokenId <- nftBalance.find(_._2._2 === en.tokenId).map(_._2)\n              yield txAndTokenId\n//              if en.tokenId.utf8.value === \"2022101110000890000000405\" then\n//                scribe.info(s\"EntrustNFT(${tx.toHash}): ${tx}\")\n//                scribe.info(s\"Balance of signer: ${state.free.get(tx.sig.account)}\")\n//                scribe.info(s\"input: $inputOption\")\n              inputOption match\n                case Some((inputTx, tokenId)) =>\n                  state\n                    .removeFree(tx.sig.account, en.input)\n                    .addLocked(en.to, tx, tokenId, tx.sig.account)\n                case None =>\n                  logWrongTx(tx.sig.account, en.tokenId, tx)\n                  state\n            case dn: Transaction.TokenTx.DisposeEntrustedNFT =>\n              val inputOption = for\n                nftBalance <- state.locked.get(tx.sig.account)\n                txTokenIdAndFrom <- nftBalance.get(dn.input)\n              yield txTokenIdAndFrom\n              inputOption match\n                case Some((inputTx, tokenId, from)) =>\n                  // Remove the NFT from the locked state\n                  state\n                    .removeLocked(tx.sig.account, dn.input)\n                    // Add the NFT to the free state\n                    .addFree(dn.output.getOrElse(from), tx, tokenId)\n                case None =>\n                  // If the NFT is not in the locked state, check the free state\n                  val to = for\n                    output <- dn.output\n                    nftBalance <- state.free.get(output)\n                    txAndTokenId <- nftBalance.find(_._2._2 === dn.tokenId).map(_._2)\n                  yield txAndTokenId\n                  // If the NFT is not in the free state, check if the transaction is from the game owner\n                  if to.isEmpty then\n                    if tx.sig.account === Account(Utf8.unsafeFrom(\"playnomm\")) then\n                      // If the transaction is from the game owner, add the NFT to the free state\n                      val currentOwnerOption =\n                        state.free.toSeq\n                          .flatMap: (account, map) =>\n                            map.toSeq.map:\n                              case (txHash, (tx, tokenId)) => (tokenId, account, txHash)\n                          .find(_._1 === dn.tokenId)\n                      val state1 = currentOwnerOption\n                        .fold(state): (_, owner, txHash) =>\n                          state.removeFree(owner, txHash)\n                      dn.output.fold(state1): output =>\n                        state1.addFree(output, tx, dn.tokenId)\n                    else\n                      // If the transaction is not from the game owner, log an error\n                      logWrongEntrustTx(tx.sig.account, dn.tokenId, tx)\n                      state\n                  else\n                    // If the NFT is in the free state, return the existing state\n                    state  \n            case _ => state\n\n        EitherT.pure[IO, String]((finalState, (index, txs)))\n\n    stateStream.last.compile.toList\n      .map(_.headOption.flatten.get._1)\n      .value\n      .map:\n        case Left(err) =>\n          scribe.error(s\"Error building balance map: $err\")\n          NftBalanceState.empty\n        case Right(balanceState) =>\n          balanceState\n"
  },
  {
    "path": "modules/bulk-insert/src/main/scala/io/leisuremeta/chain/bulkinsert/RecoverTx.scala",
    "content": "package io.leisuremeta.chain\npackage bulkinsert\n\nimport java.time.temporal.ChronoUnit\n\nimport cats.Monad\nimport cats.data.{EitherT, OptionT, StateT}\nimport cats.effect.Async\nimport cats.syntax.all.*\n\nimport api.model.{\n  Account,\n  AccountData,\n  PublicKeySummary,\n  Signed,\n  Transaction,\n  TransactionWithResult,\n}\nimport api.model.TransactionWithResult.ops.*\nimport api.model.account.{ExternalChain, ExternalChainAddress}\nimport api.model.token.{NftState, Rarity, TokenDefinitionId, TokenId}\nimport lib.codec.byte.ByteEncoder.ops.*\nimport lib.crypto.Hash\nimport lib.crypto.Hash.ops.*\nimport lib.datatype.{BigNat, Utf8}\nimport lib.merkle.MerkleTrieState\nimport node.dapp.{PlayNommDApp, PlayNommDAppFailure, PlayNommState}\nimport node.dapp.submodule.*\nimport node.repository.TransactionRepository\n\nobject RecoverTx:\n  def apply[F[_]: Async: TransactionRepository: PlayNommState: InvalidTxLogger](\n      ms: MerkleTrieState,\n      signedTx: Signed.Tx,\n  ): EitherT[\n    F,\n    PlayNommDAppFailure,\n    (MerkleTrieState, Option[TransactionWithResult]),\n  ] =\n    val sig = signedTx.sig\n    val tx  = signedTx.value\n    tx match\n      case ca: Transaction.AccountTx.CreateAccount =>\n        val program = for\n          accountInfoOption <- PlayNommDAppAccount.getAccountInfo(\n            ca.account,\n          )\n//          _ <- checkExternal(\n//            accountInfoOption.isEmpty,\n//            s\"${ca.account} already exists\",\n//          )\n//          _ <- checkExternal(\n//            sig.account == ca.account ||\n//              Some(sig.account) == ca.guardian,\n//            s\"Signer ${sig.account} is neither ${ca.account} nor its guardian\",\n//          )\n          initialPKS <- PlayNommDAppAccount.getPKS(sig, ca)\n          keyInfo = PublicKeySummary.Info(\n            addedAt = ca.createdAt,\n            description =\n              Utf8.unsafeFrom(s\"automatically added at account creation\"),\n            expiresAt = Some(ca.createdAt.plus(40, ChronoUnit.DAYS)),\n          )\n          _ <-\n            if Option(sig.account) === ca.guardian then unit\n            else\n              PlayNommState[F].account.key\n                .put((ca.account, initialPKS), keyInfo)\n                .mapK:\n                  PlayNommDAppFailure.mapInternal:\n                    s\"Fail to put account key ${ca.account}\"\n          accountData = AccountData(\n            guardian = ca.guardian,\n            externalChainAddresses = ca.ethAddress.fold(Map.empty):\n              ethAddress =>\n                Map(\n                  ExternalChain.ETH -> ExternalChainAddress(ethAddress.utf8),\n                )\n            ,\n            lastChecked = ca.createdAt,\n            memo = None,\n          )\n          _ <- PlayNommState[F].account.name\n            .put(ca.account, accountData)\n            .mapK:\n              PlayNommDAppFailure.mapInternal:\n                s\"Fail to put account ${ca.account}\"\n          _ <- ca.ethAddress.fold(unit): ethAddress =>\n            PlayNommState[F].account.externalChainAddresses\n              .put(\n                (ExternalChain.ETH, ExternalChainAddress(ethAddress.utf8)),\n                ca.account,\n              )\n              .mapK:\n                PlayNommDAppFailure.mapInternal:\n                  s\"Fail to update eth address ${ca.ethAddress}\"\n        yield TransactionWithResult(Signed(sig, ca))(None)\n\n        program\n          .run(ms)\n          .map: (ms, txWithResult) =>\n            (ms, Option(txWithResult))\n          .leftMap: msg =>\n            PlayNommDAppFailure.internal(s\"Fail to recover error: $msg\")\n\n      case ua: Transaction.AccountTx.UpdateAccount =>\n        val program = for\n          accountDataOption <- PlayNommDAppAccount.getAccountInfo(\n            ua.account,\n          )\n          accountData <- fromOption(\n            accountDataOption,\n            s\"${ua.account} does not exists\",\n          )\n//          _ <- checkExternal(\n//            sig.account == ua.account ||\n//              Some(sig.account) == accountData.guardian,\n//            s\"Signer ${sig.account} is neither ${ua.account} nor its guardian\",\n//          )\n          accountData1 = accountData.copy(\n            guardian = ua.guardian,\n            externalChainAddresses = ua.ethAddress.fold(Map.empty):\n              ethAddress =>\n                Map(\n                  ExternalChain.ETH -> ExternalChainAddress(ethAddress.utf8),\n                )\n            ,\n            lastChecked = ua.createdAt,\n            memo = None,\n          )\n          _ <- PlayNommState[F].account.name\n            .put(ua.account, accountData1)\n            .mapK:\n              PlayNommDAppFailure.mapInternal:\n                s\"Fail to put account ${ua.account}\"\n          _ <- ua.ethAddress.fold(unit): ethAddress =>\n            PlayNommState[F].account.externalChainAddresses\n              .put(\n                (ExternalChain.ETH, ExternalChainAddress(ethAddress.utf8)),\n                ua.account,\n              )\n              .mapK:\n                PlayNommDAppFailure.mapInternal:\n                  s\"Fail to update eth address ${ua.ethAddress}\"\n        yield TransactionWithResult(Signed(sig, ua))(None)\n\n        program\n          .run(ms)\n          .map: (ms, txWithResult) =>\n            (ms, Option(txWithResult))\n          .leftMap: msg =>\n            PlayNommDAppFailure.internal(s\"Fail to recover error: $msg\")\n\n//      case ap: Transaction.AccountTx.AddPublicKeySummaries =>\n//        val program = for\n//          account <- PlayNommState[F].account.name\n//            .get(ap.account)\n//            .map: account =>\n//              scribe.info(s\"Account Data: $account\")\n//              account\n//          _ <- StateT.liftF:\n//            EitherT.leftT[F, (MerkleTrieState, TransactionWithResult)]:\n//              s\"Not recovered yet: ${e.msg}\"\n//        yield TransactionWithResult(signedTx)(None)\n//\n//        program\n//          .run(ms)\n//          .map: (ms, txWithResult) =>\n//            (ms, Option(txWithResult))\n//          .leftMap: msg =>\n//            PlayNommDAppFailure.internal(s\"Fail to recover error: $msg\")\n\n      case tf: Transaction.TokenTx.TransferFungibleToken =>\n        val sig = signedTx.sig\n//        val tx  = signedTx.value\n        val program = for\n//          _ <- PlayNommDAppAccount.verifySignature(sig, tx)\n          tokenDef <- PlayNommDAppToken.getTokenDefinition(\n            tf.tokenDefinitionId,\n          )\n          inputAmount <- PlayNommDAppToken.getFungibleBalanceTotalAmounts(\n            tf.inputs.map(_.toResultHashValue),\n            sig.account,\n          )\n          outputAmount = tf.outputs.values.foldLeft(BigNat.Zero)(BigNat.add)\n          diffBigInt   = inputAmount.toBigInt - outputAmount.toBigInt\n          _ <- StateT.liftF:\n            if diffBigInt < 0 then\n//              scribe.info(s\"DiffBigInt: $diffBigInt\")\n              EitherT.right:\n                InvalidTxLogger[F].log:\n                  InvalidTx(\n                    signer = sig.account,\n                    reason = InvalidReason.OutputMoreThanInput,\n                    amountToBurn = BigNat.unsafeFromBigInt(diffBigInt.abs),\n                    tx = tf,\n                    createdAt = tf.createdAt,\n                  )\n            else EitherT.pure(())\n          txWithResult = TransactionWithResult(Signed(sig, tf))(None)\n          txHash       = txWithResult.toHash\n          invalidInputs <- removeInputUtxos(\n            sig.account,\n            tf.inputs.map(_.toResultHashValue),\n            tf.tokenDefinitionId,\n          )\n          _ <- StateT.liftF:\n            if invalidInputs.isEmpty then EitherT.pure(())\n            else\n              invalidInputs\n                .traverse(TransactionRepository[F].get)\n                .leftMap(e =>\n                  PlayNommDAppFailure.internal(s\"Fail to get tx: $e\"),\n                )\n                .semiflatMap: txOptions =>\n                  val sum = txOptions\n                    .map: txOption =>\n                      txOption.fold(BigNat.Zero)(\n                        PlayNommDAppToken.tokenBalanceAmount(sig.account),\n                      )\n                    .foldLeft(BigNat.Zero)(BigNat.add)\n//                  scribe.info(s\"Sum of Invalid Tx Inputs: $sum\")\n                  InvalidTxLogger[F].log:\n                    InvalidTx(\n                      signer = sig.account,\n                      reason = InvalidReason.InputAlreadyUsed,\n                      amountToBurn = sum,\n                      tx = tf,\n                      createdAt = tf.createdAt,\n                    )\n          _ <- tf.inputs.toList.traverse: inputTxHash =>\n            PlayNommDAppToken\n              .removeFungibleSnapshot(\n                sig.account,\n                tf.tokenDefinitionId,\n                inputTxHash.toResultHashValue,\n              )\n              .mapK:\n                PlayNommDAppFailure.mapInternal:\n                  s\"Fail to remove fungible snapshot of $inputTxHash\"\n          _ <- tf.outputs.toSeq.traverse:\n            case (account, _) =>\n              PlayNommDAppToken.putBalance(\n                account,\n                tf.tokenDefinitionId,\n                txHash,\n              ) *>\n                PlayNommDAppToken\n                  .addFungibleSnapshot(\n                    account,\n                    tf.tokenDefinitionId,\n                    txHash,\n                    outputAmount,\n                  )\n                  .mapK:\n                    PlayNommDAppFailure.mapInternal:\n                      s\"Fail to add fungible snapshot of $account\"\n\n          totalAmount <- fromEitherInternal:\n            val either =\n              BigNat.fromBigInt(tokenDef.totalAmount.toBigInt - diffBigInt)\n//            if either.isLeft then\n//              scribe.info(s\"Total Amount: \\t${tokenDef.totalAmount}\")\n//              scribe.info(s\"DiffBigInt: \\t$diffBigInt\")\n//              scribe.info(s\"Input Amount: \\t$inputAmount\")\n//              scribe.info(s\"Output Amount: \\t$outputAmount\")\n//              scribe.info(s\"Diff: \\t$diffBigInt\")\n//              scribe.info(s\"Either: \\t$either\")\n            either\n          _ <- PlayNommDAppToken.putTokenDefinition(\n            tf.tokenDefinitionId,\n            tokenDef.copy(totalAmount = totalAmount),\n          )\n          diffEither = BigNat.fromBigInt(diffBigInt)\n          _ <- diffEither match\n            case Right(diff) =>\n              PlayNommDAppToken\n                .removeTotalSupplySnapshot(tf.tokenDefinitionId, diff)\n                .mapK:\n                  PlayNommDAppFailure.mapInternal:\n                    s\"Fail to remove total supply snapshot of ${tf.tokenDefinitionId}\"\n            case Left(_) =>\n              PlayNommDAppToken\n                .addTotalSupplySnapshot(\n                  tf.tokenDefinitionId,\n                  BigNat.unsafeFromBigInt(diffBigInt.abs),\n                )\n                .mapK:\n                  PlayNommDAppFailure.mapInternal:\n                    s\"Fail to add total supply snapshot of ${tf.tokenDefinitionId}\"\n        yield txWithResult\n\n        program\n          .run(ms)\n          .map: (ms, txWithResult) =>\n            (ms, Option(txWithResult))\n          .leftMap: msg =>\n            PlayNommDAppFailure.internal(s\"Fail to recover error: $msg\")\n\n      case mn: Transaction.TokenTx.MintNFT =>\n        val program = for\n          tokenDefOption <- PlayNommState[F].token.definition\n            .get(mn.tokenDefinitionId)\n            .mapK:\n              PlayNommDAppFailure.mapExternal:\n                s\"No token definition of ${mn.tokenDefinitionId}\"\n          nftStateOption <- PlayNommState[F].token.nftState\n            .get(mn.tokenId)\n            .mapK:\n              PlayNommDAppFailure.mapInternal:\n                s\"Fail to get nft state of ${mn.tokenId}\"\n//          _ <- checkExternal(\n//            nftStateOption.isEmpty,\n//            s\"NFT ${mn.tokenId} is already minted\",\n//          )\n          txWithResult = TransactionWithResult(Signed(sig, mn))(None)\n          txHash       = txWithResult.toHash\n          _ <- PlayNommState[F].token.nftBalance\n            .put((mn.output, mn.tokenId, txHash), ())\n            .mapK:\n              PlayNommDAppFailure.mapInternal:\n                s\"Fail to put token balance (${mn.output}, ${mn.tokenId}, $txHash)\"\n          _ <- PlayNommDAppToken\n            .addNftSnapshot(mn.output, mn.tokenDefinitionId, mn.tokenId)\n            .mapK:\n              PlayNommDAppFailure.mapInternal:\n                s\"Fail to add nft snapshot of ${mn.tokenId}\"\n          rarity: Map[Rarity, BigNat] =\n            val rarityMapOption: Option[Map[Rarity, BigNat]] = for\n              tokenDef <- tokenDefOption\n              nftInfo  <- tokenDef.nftInfo\n            yield nftInfo.rarity\n\n            rarityMapOption.getOrElse:\n              Map(\n                Rarity(Utf8.unsafeFrom(\"LGDY\")) -> BigNat\n                  .unsafeFromLong(16),\n                Rarity(Utf8.unsafeFrom(\"UNIQ\")) -> BigNat\n                  .unsafeFromLong(12),\n                Rarity(Utf8.unsafeFrom(\"EPIC\")) -> BigNat.unsafeFromLong(8),\n                Rarity(Utf8.unsafeFrom(\"RARE\")) -> BigNat.unsafeFromLong(4),\n              )\n          weight = rarity.getOrElse(mn.rarity, BigNat.unsafeFromLong(2L))\n          nftState = NftState(\n            tokenId = mn.tokenId,\n            tokenDefinitionId = mn.tokenDefinitionId,\n            rarity = mn.rarity,\n            weight = weight,\n            currentOwner = mn.output,\n            memo = None,\n            lastUpdateTx = txHash,\n            previousState = None,\n          )\n          _ <- PlayNommState[F].token.nftState\n            .put(mn.tokenId, nftState)\n            .mapK:\n              PlayNommDAppFailure.mapInternal:\n                s\"Fail to put nft state of ${mn.tokenId}\"\n          _ <- PlayNommState[F].token.nftHistory\n            .put(txHash, nftState)\n            .mapK:\n              PlayNommDAppFailure.mapInternal:\n                s\"Fail to put nft history of ${mn.tokenId} of $txHash\"\n          _ <- PlayNommState[F].token.rarityState\n            .put((mn.tokenDefinitionId, mn.rarity, mn.tokenId), ())\n            .mapK:\n              PlayNommDAppFailure.mapInternal:\n                s\"Fail to put rarity state of ${mn.tokenDefinitionId}, ${mn.rarity}, ${mn.tokenId}\"\n        yield txWithResult\n\n        program\n          .run(ms)\n          .map: (ms, txWithResult) =>\n            (ms, Option(txWithResult))\n          .leftMap: msg =>\n            PlayNommDAppFailure.internal(s\"Fail to recover error: $msg\")\n\n      case tn: Transaction.TokenTx.TransferNFT =>\n        val sig          = signedTx.sig\n        val txWithResult = TransactionWithResult(Signed(sig, tn))(None)\n        val txHash       = txWithResult.toHash\n        val utxoKey      = (sig.account, tn.tokenId, tn.input.toResultHashValue)\n\n        val program = for\n          isRemoveSuccessful <- PlayNommState[F].token.nftBalance\n            .remove(utxoKey)\n            .mapK:\n              PlayNommDAppFailure.mapInternal:\n                s\"Fail to remove nft balance of $utxoKey\"\n          _ <-\n            if isRemoveSuccessful then StateT.liftF(EitherT.pure(()))\n            else if sig.account === Account(Utf8.unsafeFrom(\"playnomm\"))\n            then removePreviousNftBalance(tn.tokenId, signedTx)\n            else\n              StateT.liftF:\n                EitherT.right:\n                  InvalidTxLogger[F].log:\n                    InvalidTx(\n                      signer = sig.account,\n                      reason = InvalidReason.BalanceNotExist,\n                      amountToBurn = BigNat.Zero,\n                      tx = tn,\n                      wrongNftInput = Some(tn.tokenId),\n                      createdAt = tn.createdAt,\n                    )\n          newUtxoKey = (tn.output, tn.tokenId, txHash)\n          _ <- PlayNommState[F].token.nftBalance\n            .put(newUtxoKey, ())\n            .mapK:\n              PlayNommDAppFailure.mapInternal:\n                s\"Fail to put nft balance of $newUtxoKey\"\n          nftStateOption <- PlayNommState[F].token.nftState\n            .get(tn.tokenId)\n            .mapK:\n              PlayNommDAppFailure.mapInternal:\n                s\"Fail to get nft state of ${tn.tokenId}\"\n          nftState <- fromOption(\n            nftStateOption,\n            s\"Empty NFT State: ${tn.tokenId}\",\n          )\n          nftState1 = nftState.copy(currentOwner = tn.output)\n          _ <- PlayNommState[F].token.nftState\n            .put(tn.tokenId, nftState1)\n            .mapK:\n              PlayNommDAppFailure.mapInternal:\n                s\"Fail to put nft state of ${tn.tokenId}\"\n        yield txWithResult\n\n        program\n          .run(ms)\n          .map: (ms, txWithResult) =>\n            (ms, Option(txWithResult))\n          .leftMap: msg =>\n            PlayNommDAppFailure.internal(s\"Fail to recover error: $msg\")\n\n      case bf: Transaction.TokenTx.BurnFungibleToken =>\n        val program = for\n//          _ <- PlayNommDAppAccount.verifySignature(sig, tx)\n          tokenDef <- PlayNommDAppToken.getTokenDefinition(bf.definitionId)\n          inputAmount <- PlayNommDAppToken.getFungibleBalanceTotalAmounts(\n            bf.inputs.map(_.toResultHashValue),\n            sig.account,\n          )\n          outputAmount <- fromEitherExternal:\n            BigNat.tryToSubtract(inputAmount, bf.amount)\n          result = Transaction.TokenTx.BurnFungibleTokenResult(outputAmount)\n          txWithResult = TransactionWithResult(Signed(sig, bf))(Some(result))\n          txHash       = txWithResult.toHash\n          _ <- removeInputUtxos(\n            sig.account,\n            bf.inputs.map(_.toResultHashValue),\n            bf.definitionId,\n          )\n          _ <- bf.inputs.toList.traverse: inputTxHash =>\n            PlayNommDAppToken\n              .removeFungibleSnapshot(\n                sig.account,\n                bf.definitionId,\n                inputTxHash.toResultHashValue,\n              )\n              .mapK:\n                PlayNommDAppFailure.mapInternal:\n                  s\"Fail to remove fungible snapshot of $inputTxHash\"\n          _ <-\n            if outputAmount === BigNat.Zero then unit\n            else\n              PlayNommDAppToken.putBalance(\n                sig.account,\n                bf.definitionId,\n                txWithResult.toHash,\n              ) *>\n                PlayNommDAppToken\n                  .addFungibleSnapshot(\n                    sig.account,\n                    bf.definitionId,\n                    txWithResult.toHash,\n                    outputAmount,\n                  )\n                  .mapK:\n                    PlayNommDAppFailure.mapInternal:\n                      s\"Fail to add fungible snapshot of ${sig.account}\"\n          totalAmount <- fromEitherInternal:\n            BigNat.tryToSubtract(tokenDef.totalAmount, bf.amount)\n          _ <- PlayNommDAppToken.putTokenDefinition(\n            bf.definitionId,\n            tokenDef.copy(totalAmount = totalAmount),\n          )\n          _ <- PlayNommDAppToken\n            .removeTotalSupplySnapshot(bf.definitionId, bf.amount)\n            .mapK:\n              PlayNommDAppFailure.mapInternal:\n                s\"Fail to remove total supply snapshot of ${bf.definitionId}\"\n        yield txWithResult\n\n        program\n          .run(ms)\n          .map: (ms, txWithResult) =>\n            (ms, Option(txWithResult))\n          .leftMap: msg =>\n            PlayNommDAppFailure.internal(s\"Fail to recover error: $msg\")\n\n      case bn: Transaction.TokenTx.BurnNFT =>\n        val program = for\n//          _       <- PlayNommDAppAccount.verifySignature(sig, tx)\n          tokenId <- getNftTokenId(bn.input.toResultHashValue)\n          txWithResult = TransactionWithResult(Signed(sig, bn))(None)\n          txHash       = txWithResult.toHash\n          utxoKey      = (sig.account, tokenId, bn.input.toResultHashValue)\n          isRemoveSuccessful <- PlayNommState[F].token.nftBalance\n            .remove(utxoKey)\n            .mapK:\n              PlayNommDAppFailure.mapInternal:\n                s\"Fail to remove nft balance of $utxoKey\"\n          _ <-\n            if isRemoveSuccessful then StateT.liftF(EitherT.pure(()))\n            else\n              StateT.liftF:\n                EitherT.right:\n                  InvalidTxLogger[F].log:\n                    InvalidTx(\n                      signer = sig.account,\n                      reason = InvalidReason.BalanceNotExist,\n                      amountToBurn = BigNat.Zero,\n                      tx = bn,\n                      wrongNftInput = Some(tokenId),\n                      createdAt = bn.createdAt,\n                    )\n          nftStateOption <- PlayNommState[F].token.nftState\n            .get(tokenId)\n            .mapK:\n              PlayNommDAppFailure.mapInternal:\n                s\"Fail to get nft state of ${tokenId}\"\n          _ <- nftStateOption.traverse: nftState =>\n            for\n              _ <- PlayNommState[F].token.nftState\n                .remove(tokenId)\n                .mapK:\n                  PlayNommDAppFailure.mapInternal:\n                    s\"Fail to remove nft state of ${tokenId}\"\n              _ <- PlayNommState[F].token.rarityState\n                .remove((bn.definitionId, nftState.rarity, tokenId))\n                .mapK:\n                  PlayNommDAppFailure.mapInternal:\n                    s\"Fail to remove rarity state of ${tokenId}\"\n            yield ()\n          _ <- PlayNommDAppToken\n            .removeNftSnapshot[F](sig.account, bn.definitionId, tokenId)\n            .mapK:\n              PlayNommDAppFailure.mapInternal:\n                s\"Fail to remove nft snapshot of ${tokenId}\"\n        yield txWithResult\n\n        program\n          .run(ms)\n          .map: (ms, txWithResult) =>\n            (ms, Option(txWithResult))\n          .leftMap: msg =>\n            PlayNommDAppFailure.internal(s\"Fail to recover error: $msg\")\n\n      case ef: Transaction.TokenTx.EntrustFungibleToken =>\n        val sig = signedTx.sig\n//        val tx  = signedTx.value\n        val program = for\n//          _        <- PlayNommDAppAccount.verifySignature(sig, tx)\n          tokenDef <- PlayNommDAppToken.getTokenDefinition(ef.definitionId)\n          inputAmount <- PlayNommDAppToken.getFungibleBalanceTotalAmounts(\n            ef.inputs.map(_.toResultHashValue),\n            sig.account,\n          )\n          diffBigInt = inputAmount.toBigInt - ef.amount.toBigInt\n          diff <- StateT.liftF:\n            if diffBigInt < 0 then\n              EitherT.right:\n                InvalidTxLogger[F]\n                  .log:\n                    InvalidTx(\n                      signer = sig.account,\n                      reason = InvalidReason.OutputMoreThanInput,\n                      amountToBurn = BigNat.unsafeFromBigInt(diffBigInt.abs),\n                      tx = ef,\n                      createdAt = ef.createdAt,\n                    )\n                  .as(BigNat.Zero)\n            else\n              EitherT\n                .fromEither(BigNat.fromBigInt(diffBigInt))\n                .leftMap: msg =>\n                  PlayNommDAppFailure.internal(\n                    s\"Fail to convert diff to BigNat: $msg\",\n                  )\n          result = Transaction.TokenTx.EntrustFungibleTokenResult(diff)\n          txWithResult = TransactionWithResult(Signed(sig, ef))(\n            Some(result),\n          )\n          txHash = txWithResult.toHash\n          invalidInputs <- removeInputUtxos(\n            sig.account,\n            ef.inputs.map(_.toResultHashValue),\n            ef.definitionId,\n          )\n          _ <- StateT.liftF:\n            if invalidInputs.isEmpty then EitherT.pure(())\n            else\n              invalidInputs\n                .traverse(TransactionRepository[F].get)\n                .leftMap: e =>\n                  PlayNommDAppFailure.internal(s\"Fail to get tx: $e\")\n                .semiflatMap: txOptions =>\n                  val sum = txOptions\n                    .map: txOption =>\n                      txOption.fold(BigNat.Zero):\n                        PlayNommDAppToken.tokenBalanceAmount(sig.account)\n                    .foldLeft(BigNat.Zero)(BigNat.add)\n                  InvalidTxLogger[F].log:\n                    InvalidTx(\n                      signer = sig.account,\n                      reason = InvalidReason.InputAlreadyUsed,\n                      amountToBurn = sum,\n                      tx = ef,\n                      createdAt = ef.createdAt,\n                    )\n          _ <- PlayNommState[F].token.entrustFungibleBalance\n            .put((sig.account, ef.to, ef.definitionId, txHash), ())\n            .mapK:\n              PlayNommDAppFailure.mapInternal:\n                s\"Fail to put entrust fungible balance of (${sig.account}, ${ef.to}, ${ef.definitionId}, ${txHash})\"\n          _ <- PlayNommDAppToken.putBalance(\n            sig.account,\n            ef.definitionId,\n            txHash,\n          )\n        yield txWithResult\n\n        program\n          .run(ms)\n          .map: (ms, txWithResult) =>\n            (ms, Option(txWithResult))\n          .leftMap: msg =>\n            PlayNommDAppFailure.internal(s\"Fail to recover error: $msg\")\n\n      case ef: Transaction.TokenTx.EntrustNFT =>\n        val txWithResult = TransactionWithResult(Signed(sig, ef))(None)\n        val txHash       = txWithResult.toHash\n        val program = for\n//          _ <- PlayNommDAppAccount.verifySignature(sig, tx)\n          inputTxHashes <- PlayNommState[F].token.nftBalance\n            .streamWithPrefix((sig.account, ef.tokenId).toBytes)\n            .flatMapF: stream =>\n              stream.map(_._1._3).compile.toList\n            .mapK:\n              PlayNommDAppFailure.mapInternal:\n                s\"Fail to get nft balance stream of (${sig.account}, ${ef.tokenId})\"\n          txWithResultOption <- inputTxHashes.headOption match\n            case None =>\n//              scribe.info(s\"No input tx hash found for ${sig.account} and ${ef.tokenId}\")\n              StateT.liftF:\n                EitherT.right:\n                  InvalidTxLogger[F]\n                    .log:\n                      InvalidTx(\n                        signer = sig.account,\n                        reason = InvalidReason.BalanceNotExist,\n                        amountToBurn = BigNat.Zero,\n                        tx = ef,\n                        wrongNftInput = Some(ef.tokenId),\n                        createdAt = ef.createdAt,\n                      )\n                    .map(_ => None)\n            case Some(inputTxHash) =>\n//              scribe.info(s\"Input tx hash found for ${sig.account} and ${ef.tokenId}: $inputTxHash\")\n              val utxoKey = (sig.account, ef.tokenId, inputTxHash)\n              for\n                isRemoveSuccessful <- PlayNommState[F].token.nftBalance\n                  .remove(utxoKey)\n                  .mapK:\n                    PlayNommDAppFailure.mapInternal:\n                      s\"Fail to remove nft balance of $utxoKey\"\n                _ <- StateT.liftF:\n//                  scribe.info(s\"Is remove successful: $isRemoveSuccessful\")\n                  if !isRemoveSuccessful then\n                    EitherT.right:\n                      InvalidTxLogger[F].log:\n                        InvalidTx(\n                          signer = sig.account,\n                          reason = InvalidReason.BalanceNotExist,\n                          amountToBurn = BigNat.Zero,\n                          tx = ef,\n                          wrongNftInput = Some(ef.tokenId),\n                          createdAt = ef.createdAt,\n                        )\n                  else EitherT.pure(())\n                newUtxoKey = (sig.account, ef.to, ef.tokenId, txHash)\n                _ <- PlayNommState[F].token.entrustNftBalance\n                  .put(newUtxoKey, ())\n                  .mapK:\n                    PlayNommDAppFailure.mapInternal:\n                      s\"Fail to put entrust nft balance of $newUtxoKey\"\n              yield\n              //                scribe.info(s\"Succeed to recover EntrustNFT: $txWithResult\")\n              Some(txWithResult)\n        yield txWithResultOption\n\n        program\n          .run(ms)\n          .leftMap: msg =>\n            PlayNommDAppFailure.internal(s\"Fail to recover error: $msg\")\n\n      case de: Transaction.TokenTx.DisposeEntrustedFungibleToken =>\n        val program = for\n//          _        <- PlayNommDAppAccount.verifySignature(sig, tx)\n          tokenDef <- PlayNommDAppToken.getTokenDefinition(de.definitionId)\n          inputHashList = de.inputs.map(_.toResultHashValue)\n          inputMap <- PlayNommDAppToken.getEntrustedInputs(\n            inputHashList,\n            sig.account,\n          )\n          inputAmount  = inputMap.values.foldLeft(BigNat.Zero)(BigNat.add)\n          outputAmount = de.outputs.values.foldLeft(BigNat.Zero)(BigNat.add)\n//          _ <- checkExternal(\n//            inputAmount === outputAmount,\n//            s\"Output amount is not equal to input amount $inputAmount\",\n//          )\n          txWithResult = TransactionWithResult(Signed(sig, de))(None)\n          txHash       = txWithResult.toHash\n          _ <- inputMap.toList\n            .map(_._1)\n            .traverse: (account, txHash) =>\n              PlayNommState[F].token.entrustFungibleBalance\n                .remove((account, sig.account, de.definitionId, txHash))\n                .mapK:\n                  PlayNommDAppFailure.mapInternal:\n                    s\"Fail to remove entrust fungible balance of (${account}, ${sig.account}, ${de.definitionId}, ${txHash})\"\n                *> PlayNommDAppToken\n                  .removeFungibleSnapshot[F](account, de.definitionId, txHash)\n                  .mapK:\n                    PlayNommDAppFailure.mapInternal:\n                      s\"Fail to remove fungible snapshot of $txHash\"\n          _ <- de.outputs.toList.traverse: (account, amount) =>\n            PlayNommState[F].token.fungibleBalance\n              .put((account, de.definitionId, txHash), ())\n              .mapK:\n                PlayNommDAppFailure.mapInternal:\n                  s\"Fail to put fungible balance of (${account}, ${de.definitionId}, ${txHash})\"\n              *> PlayNommDAppToken\n                .addFungibleSnapshot[F](\n                  account,\n                  de.definitionId,\n                  txHash,\n                  amount,\n                )\n                .mapK:\n                  PlayNommDAppFailure.mapInternal:\n                    s\"Fail to put fungible snapshot of $txHash\"\n        yield txWithResult\n\n        program\n          .run(ms)\n          .map: (ms, txWithResult) =>\n            (ms, Option(txWithResult))\n          .leftMap: msg =>\n            PlayNommDAppFailure.internal(s\"Fail to recover error: $msg\")\n\n      case de: Transaction.TokenTx.DisposeEntrustedNFT =>\n        val txWithResult = TransactionWithResult(Signed(sig, de))(None)\n        val txHash       = txWithResult.toHash\n        val program = for\n          inputOption <- StateT.liftF:\n            TransactionRepository[F]\n              .get(de.input.toResultHashValue)\n              .leftMap: e =>\n                PlayNommDAppFailure.internal:\n                  s\"Fail to get tx ${de.input}: ${e.msg}\"\n              .map: txOption =>\n                txOption.flatMap: txWithResult =>\n                  txWithResult.signedTx.value match\n                    case ef: Transaction.TokenTx.EntrustNFT\n                        if ef.to === sig.account =>\n                      Some(\n                        (\n                          txWithResult,\n                          ef.tokenId,\n                          txWithResult.signedTx.sig.account,\n                        ),\n                      )\n                    case _ =>\n                      None\n          txWithResultOption <- inputOption match\n            case Some((inputTx, tokenId, from)) =>\n              val utxoKey = (\n                from,\n                sig.account,\n                de.tokenId,\n                de.input.toResultHashValue,\n              )\n              for\n                isRemoveSuccessful <- PlayNommState[\n                  F,\n                ].token.entrustNftBalance\n                  .remove(utxoKey)\n                  .mapK:\n                    PlayNommDAppFailure.mapInternal:\n                      s\"Fail to remove entrust nft balance of $utxoKey\"\n                _ <-\n                  if isRemoveSuccessful then\n                    PlayNommDAppToken\n                      .removeNftSnapshot[F](from, de.definitionId, de.tokenId)\n                      .mapK:\n                        PlayNommDAppFailure.mapInternal:\n                          s\"Fail to remove nft snapshot of ${de.tokenId}\"\n                  else\n                    for\n                      _ <- removePreviousNftBalance[F](de.tokenId, signedTx)\n                      _ <- StateT\n                        .liftF:\n                          EitherT.right:\n                            InvalidTxLogger[F].log:\n                              InvalidTx(\n                                signer = sig.account,\n                                reason = InvalidReason.BalanceNotExist,\n                                amountToBurn = BigNat.Zero,\n                                tx = de,\n                                wrongNftInput = Some(de.tokenId),\n                                createdAt = de.createdAt,\n                              )\n                      _ <- PlayNommDAppToken\n                        .removeNftSnapshot[F](from, de.definitionId, de.tokenId)\n                        .mapK:\n                          PlayNommDAppFailure.mapInternal:\n                            s\"Fail to remove nft snapshot of ${de.tokenId}\"\n                    yield ()\n                toAccount  = de.output.getOrElse(from)\n                newUtxoKey = (toAccount, de.tokenId, txHash)\n                _ <- PlayNommState[F].token.nftBalance\n                  .put(newUtxoKey, ())\n                  .mapK:\n                    PlayNommDAppFailure.mapInternal:\n                      s\"Fail to put nft balance of $newUtxoKey\"\n                _ <- PlayNommDAppToken\n                  .addNftSnapshot[F](\n                    de.output.getOrElse(from),\n                    de.definitionId,\n                    de.tokenId,\n                  )\n                  .mapK:\n                    PlayNommDAppFailure.mapInternal:\n                      s\"Fail to add nft snapshot of ${de.tokenId}\"\n                nftStateOption <- PlayNommState[F].token.nftState\n                  .get(de.tokenId)\n                  .mapK:\n                    PlayNommDAppFailure.mapInternal:\n                      s\"Fail to get nft state of ${de.tokenId}\"\n                nftState <- fromOption(\n                  nftStateOption,\n                  s\"Empty NFT State: ${de.tokenId}\",\n                )\n                nftState1 = nftState.copy(currentOwner = toAccount)\n                _ <- PlayNommState[F].token.nftState\n                  .put(de.tokenId, nftState1)\n                  .mapK:\n                    PlayNommDAppFailure.mapInternal:\n                      s\"Fail to put nft state of ${de.tokenId}\"\n              yield Option(txWithResult)\n            case None =>\n              for\n                to <- de.output.traverse: output =>\n                  PlayNommState[F].token.nftBalance\n                    .streamWithPrefix(output.toBytes)\n                    .mapK:\n                      PlayNommDAppFailure.mapInternal:\n                        s\"Fail to get nft balance of ${output}\"\n                    .flatMapF: stream =>\n                      stream\n                        .filter:\n                          case ((account, tokenId, txHash), _) =>\n                            tokenId == de.tokenId\n                        .head\n                        .compile\n                        .toList\n                        .map: list =>\n                          list.headOption.map(_._1._1)\n                        .leftMap: msg =>\n                          PlayNommDAppFailure.internal:\n                            s\"Fail to get nft balance stream: $msg\"\n                txWithResultOption <- to.flatten match\n                  case Some(account) =>\n                    unit[F].map: _ =>\n                      None\n                  case None =>\n                    for\n                      stateOption <- PlayNommState[F].token.nftState\n                        .get(de.tokenId)\n                        .mapK:\n                          PlayNommDAppFailure.mapInternal:\n                            s\"Fail to get nft state of ${de.tokenId}\"\n                      currentOwnerOption = stateOption.map(_.currentOwner)\n                    yield Some(txWithResult)\n              yield txWithResultOption\n        yield txWithResultOption\n\n        program\n          .run(ms)\n          .leftMap: msg =>\n            PlayNommDAppFailure.internal(s\"Fail to recover error: $msg\")\n\n      case tf: Transaction.RewardTx.OfferReward =>\n        val sig = signedTx.sig\n//        val tx  = signedTx.value\n        val program = for\n//          _ <- PlayNommDAppAccount.verifySignature(sig, tx)\n          tokenDef <- PlayNommDAppToken.getTokenDefinition(\n            tf.tokenDefinitionId,\n          )\n          inputAmount <- PlayNommDAppToken.getFungibleBalanceTotalAmounts(\n            tf.inputs.map(_.toResultHashValue),\n            sig.account,\n          )\n          outputAmount = tf.outputs.values.foldLeft(BigNat.Zero)(BigNat.add)\n          diffBigInt   = inputAmount.toBigInt - outputAmount.toBigInt\n          _ <- StateT.liftF:\n            if diffBigInt < 0 then\n              EitherT.right:\n                InvalidTxLogger[F].log:\n                  InvalidTx(\n                    signer = sig.account,\n                    reason = InvalidReason.OutputMoreThanInput,\n                    amountToBurn = BigNat.unsafeFromBigInt(diffBigInt.abs),\n                    tx = tf,\n                    createdAt = tf.createdAt,\n                  )\n            else EitherT.pure(())\n          txWithResult = TransactionWithResult(Signed(sig, tf))(None)\n          txHash       = txWithResult.toHash\n          invalidInputs <- removeInputUtxos(\n            sig.account,\n            tf.inputs.map(_.toResultHashValue),\n            tf.tokenDefinitionId,\n          )\n          _ <- StateT.liftF:\n            if invalidInputs.isEmpty then EitherT.pure(())\n            else\n              invalidInputs\n                .traverse(TransactionRepository[F].get)\n                .leftMap(e =>\n                  PlayNommDAppFailure.internal(s\"Fail to get tx: $e\"),\n                )\n                .semiflatMap: txOptions =>\n                  val sum = txOptions\n                    .map: txOption =>\n                      txOption.fold(BigNat.Zero)(\n                        PlayNommDAppToken.tokenBalanceAmount(sig.account),\n                      )\n                    .foldLeft(BigNat.Zero)(BigNat.add)\n                  InvalidTxLogger[F].log:\n                    InvalidTx(\n                      signer = sig.account,\n                      reason = InvalidReason.InputAlreadyUsed,\n                      amountToBurn = sum,\n                      tx = tf,\n                      createdAt = tf.createdAt,\n                    )\n          _ <- tf.inputs.toList.traverse: inputTxHash =>\n            PlayNommDAppToken\n              .removeFungibleSnapshot(\n                sig.account,\n                tf.tokenDefinitionId,\n                inputTxHash.toResultHashValue,\n              )\n              .mapK:\n                PlayNommDAppFailure.mapInternal:\n                  s\"Fail to remove fungible snapshot of $inputTxHash\"\n          _ <- tf.outputs.toSeq.traverse:\n            case (account, _) =>\n              PlayNommDAppToken.putBalance(\n                account,\n                tf.tokenDefinitionId,\n                txHash,\n              ) *>\n                PlayNommDAppToken\n                  .addFungibleSnapshot(\n                    account,\n                    tf.tokenDefinitionId,\n                    txHash,\n                    outputAmount,\n                  )\n                  .mapK:\n                    PlayNommDAppFailure.mapInternal:\n                      s\"Fail to add fungible snapshot of $account\"\n          totalAmount <- fromEitherInternal:\n            BigNat.fromBigInt(tokenDef.totalAmount.toBigInt - diffBigInt)\n          _ <- PlayNommDAppToken.putTokenDefinition(\n            tf.tokenDefinitionId,\n            tokenDef.copy(totalAmount = totalAmount),\n          )\n          diffEither = BigNat.fromBigInt(diffBigInt)\n          _ <- diffEither match\n            case Right(diff) =>\n              PlayNommDAppToken\n                .removeTotalSupplySnapshot(tf.tokenDefinitionId, diff)\n                .mapK:\n                  PlayNommDAppFailure.mapInternal:\n                    s\"Fail to remove total supply snapshot of ${tf.tokenDefinitionId}\"\n            case Left(_) =>\n              PlayNommDAppToken\n                .addTotalSupplySnapshot(\n                  tf.tokenDefinitionId,\n                  BigNat.unsafeFromBigInt(diffBigInt.abs),\n                )\n                .mapK:\n                  PlayNommDAppFailure.mapInternal:\n                    s\"Fail to add total supply snapshot of ${tf.tokenDefinitionId}\"\n        yield txWithResult\n\n        program\n          .run(ms)\n          .map: (ms, txWithResult) =>\n            (ms, Option(txWithResult))\n          .leftMap: msg =>\n            PlayNommDAppFailure.internal(s\"Fail to recover error: $msg\")\n\n//      case ss: Transaction.AgendaTx.SuggestSimpleAgenda =>\n//        EitherT.pure((ms, Some(TransactionWithResult(Signed(sig, ss))(None))))\n//\n//      case vs: Transaction.AgendaTx.VoteSimpleAgenda =>\n//        EitherT.pure((ms, Some(TransactionWithResult(Signed(sig, vs))(None))))\n\n      case _ =>\n        PlayNommDApp[F](signedTx)\n          .run(ms)\n          .map: (ms, txWithResult) =>\n            (ms, Option(txWithResult))\n\n  def removePreviousNftBalance[F[_]\n    : Async: PlayNommState: TransactionRepository: InvalidTxLogger](\n      tokenId: TokenId,\n      signedTx: Signed.Tx,\n  ): StateT[EitherT[F, PlayNommDAppFailure, *], MerkleTrieState, Unit] =\n    val removePreviousNftBalanceOptionT = for\n      nftState <- OptionT:\n        PlayNommState[F].token.nftState\n          .get(tokenId)\n          .mapK:\n            PlayNommDAppFailure.mapInternal:\n              s\"Fail to get nft state of ${tokenId}\"\n      currentOwner = nftState.currentOwner\n      nftBalanceStream <- OptionT.liftF:\n        PlayNommState[F].token.nftBalance\n          .streamWithPrefix((currentOwner, tokenId).toBytes)\n          .mapK:\n            PlayNommDAppFailure.mapInternal:\n              s\"Fail to get nft balance of $currentOwner\"\n      currentBalance <- OptionT:\n        StateT.liftF:\n          nftBalanceStream.head.compile.toList\n            .leftMap: msg =>\n              PlayNommDAppFailure.internal(\n                s\"Fail to get nft balance of $currentOwner: $msg\",\n              )\n            .map(_.headOption)\n      currentUtxoHash = currentBalance._1._3\n      _ <- OptionT.liftF:\n        for\n          _ <- PlayNommState[F].token.nftBalance\n            .remove((currentOwner, tokenId, currentUtxoHash))\n            .mapK:\n              PlayNommDAppFailure.mapInternal:\n                s\"Fail to remove nft balance of $currentOwner\"\n          _ <- StateT.liftF:\n            EitherT.right:\n              InvalidTxLogger[F].log:\n                InvalidTx(\n                  signer = signedTx.sig.account,\n                  reason = InvalidReason.CanceledBalance,\n                  amountToBurn = BigNat.Zero,\n                  tx = signedTx.value,\n                  wrongNftInput = Some(tokenId),\n                  createdAt = signedTx.value.createdAt,\n                )\n        yield ()\n    yield ()\n\n    removePreviousNftBalanceOptionT.value.map(_ => ())\n\n  def getNftTokenId[F[_]: Monad: TransactionRepository](\n      utxoHash: Hash.Value[TransactionWithResult],\n  ): StateT[EitherT[F, PlayNommDAppFailure, *], MerkleTrieState, TokenId] =\n    StateT.liftF:\n      TransactionRepository[F]\n        .get(utxoHash)\n        .leftMap(e => PlayNommDAppFailure.internal(s\"Fail to get tx: ${e.msg}\"))\n        .subflatMap: txOption =>\n          Either.fromOption(\n            txOption,\n            PlayNommDAppFailure.internal(s\"There is no tx of $utxoHash\"),\n          )\n        .flatMap: txWithResult =>\n          txWithResult.signedTx.value match\n            case nb: Transaction.NftBalance =>\n              EitherT.pure:\n                nb match\n                  case mn: Transaction.TokenTx.MintNFT     => mn.tokenId\n                  case tn: Transaction.TokenTx.TransferNFT => tn.tokenId\n                  case den: Transaction.TokenTx.DisposeEntrustedNFT =>\n                    den.tokenId\n                  case mnm: Transaction.TokenTx.MintNFTWithMemo => mnm.tokenId\n            case _ =>\n              EitherT.leftT:\n                PlayNommDAppFailure.external:\n                  s\"Tx $txWithResult is not a nft balance\"\n\n  def removeInputUtxos[F[_]: Monad: PlayNommState](\n      account: Account,\n      inputs: Set[Hash.Value[TransactionWithResult]],\n      definitionId: TokenDefinitionId,\n  ): StateT[EitherT[F, PlayNommDAppFailure, *], MerkleTrieState, List[\n    Hash.Value[TransactionWithResult],\n  ]] =\n    val inputList = inputs.toList\n    for\n      removeResults <- inputList.traverse: txHash =>\n        PlayNommState[F].token.fungibleBalance\n          .remove((account, definitionId, txHash))\n          .mapK:\n            PlayNommDAppFailure.mapInternal:\n              s\"Fail to remove fingible balance ($account, $definitionId, $txHash)\"\n      invalidUtxos = inputList.zip(removeResults).filterNot(_._2).map(_._1)\n    yield invalidUtxos\n"
  },
  {
    "path": "modules/eth-gateway/src/main/scala/io/leisuremeta/chain/gateway/eth/EthGatewayMain.scala",
    "content": ""
  },
  {
    "path": "modules/eth-gateway-common/src/main/scala/io/leisuremeta/chain/gateway/eth/common/GatewayApi.scala",
    "content": "package io.leisuremeta.chain.gateway.eth.common\n\nimport io.circe.generic.auto.*\nimport sttp.model.StatusCode\nimport sttp.tapir.*\nimport sttp.tapir.json.circe.*\nimport sttp.tapir.generic.auto.given\n\nobject GatewayApi:\n\n  final case class GatewayRequest(\n    key: String,\n    doublyEncryptedFrontPartBase64: String,\n  )\n\n  final case class GatewayResponse(\n    singlyEncryptedBase64: String,\n  )\n\n  sealed trait GatewayApiError\n\n  final case class ServerError(msg: String) extends GatewayApiError\n\n  sealed trait UserError extends GatewayApiError:\n    def msg: String\n  final case class Unauthorized(msg: String) extends UserError\n  final case class NotFound(msg: String)     extends UserError\n  final case class BadRequest(msg: String)   extends UserError\n\n  val postDecryptEndpoint: PublicEndpoint[GatewayRequest, GatewayApiError, GatewayResponse, Any] =\n    endpoint.post\n      .in(jsonBody[GatewayRequest])\n      .out(jsonBody[GatewayResponse])\n      .errorOut:\n        oneOf[GatewayApiError](\n          oneOfVariant(statusCode(StatusCode.BadRequest).and(jsonBody[BadRequest])),\n          oneOfVariant(statusCode(StatusCode.Unauthorized).and(jsonBody[Unauthorized])),\n          oneOfVariant(statusCode(StatusCode.NotFound).and(jsonBody[NotFound])),\n          oneOfVariant(statusCode(StatusCode.InternalServerError).and(jsonBody[ServerError])),\n        )\n  "
  },
  {
    "path": "modules/eth-gateway-common/src/main/scala/io/leisuremeta/chain/gateway/eth/common/GatewayConf.scala",
    "content": "package io.leisuremeta.chain.gateway.eth.common\n\nimport pureconfig.*\nimport pureconfig.generic.derivation.default.*\n\nfinal case class GatewayConf(\n    ethChainId: Int,\n    ethContractAddress: String,\n    gatewayEthAddress: String,\n    gatewayEndpoint: String,\n    localServerPort: Int,\n    lmEndpoint: String,\n    kmsAlias: String,\n    encryptedEthEndpoint: String,\n    encryptedDatabaseEndpoint: String,\n    databaseTableName: String,\n    databaseValueColumn: String,\n    targetGateway: String,\n) derives ConfigReader\n\nobject GatewayConf:\n  def loadOrThrow(): GatewayConf =\n    ConfigSource.default.loadOrThrow[GatewayConf]\n"
  },
  {
    "path": "modules/eth-gateway-common/src/main/scala/io/leisuremeta/chain/gateway/eth/common/GatewayDecryptService.scala",
    "content": "package io.leisuremeta.chain.gateway.eth.common\n\nimport cats.data.EitherT\nimport cats.effect.{Async, Resource}\n\nimport scodec.bits.ByteVector\nimport client.*\n\nobject GatewayDecryptService:\n  def getPlainTextResource[F[_]\n    : Async: GatewayApiClient: GatewayDatabaseClient: GatewayKmsClient](\n      key: String,\n  ): EitherT[F, String, Resource[F, Array[Byte]]] =\n    for\n      doublyEncryptedFrontOption <- GatewayDatabaseClient[F].select(key)\n      doublyEncryptedFront <- EitherT.fromOption(\n        doublyEncryptedFrontOption,\n        s\"Value not found for key: ${key}\",\n      )\n      singlyEncryptedBase64 <- GatewayApiClient[F].get(\n        key,\n        doublyEncryptedFront,\n      )\n      bytes <- EitherT.fromEither:\n        ByteVector.fromBase64Descriptive(singlyEncryptedBase64)\n      plaintextResource <- GatewayKmsClient[F].decrypt(bytes.toArray)\n    yield plaintextResource\n\n  def getEth[F[_]\n    : Async: GatewayApiClient: GatewayDatabaseClient: GatewayKmsClient]\n      : EitherT[F, String, Resource[F, Array[Byte]]] =\n    getPlainTextResource[F](\"ETH\")\n\n  def getLm[F[_]\n    : Async: GatewayApiClient: GatewayDatabaseClient: GatewayKmsClient]\n      : EitherT[F, String, Resource[F, Array[Byte]]] =\n    getPlainTextResource[F](\"LM\")\n\n  def getLmD[F[_]\n    : Async: GatewayApiClient: GatewayDatabaseClient: GatewayKmsClient]\n      : EitherT[F, String, Resource[F, Array[Byte]]] =\n    getPlainTextResource[F](\"LM-D\")\n  \n  def getSimplifiedPlainTextResource[F[_]: Async: GatewayKmsClient](\n      encryptedBase64: String,\n  ): EitherT[F, String, Resource[F, Array[Byte]]] = for\n    bytes <- EitherT.fromEither:\n      ByteVector.fromBase64Descriptive(encryptedBase64)\n    plaintextResource <- GatewayKmsClient[F].decrypt(bytes.toArrayUnsafe)\n  yield plaintextResource\n"
  },
  {
    "path": "modules/eth-gateway-common/src/main/scala/io/leisuremeta/chain/gateway/eth/common/GatewayResource.scala",
    "content": "package io.leisuremeta.chain.gateway.eth.common\n\nimport cats.data.EitherT\nimport cats.effect.{Async, Resource}\n\nimport org.web3j.protocol.Web3j\nimport scodec.bits.ByteVector\nimport sttp.client3.*\nimport sttp.client3.armeria.cats.ArmeriaCatsBackend\n\nimport client.*\nimport cats.effect.std.Dispatcher\n\nobject GatewayResource:\n  def getAllResource[F[_]: Async](conf: GatewayConf): Resource[\n    EitherT[F, String, *],\n    (GatewayKmsClient[F], Web3j, GatewayDatabaseClient[F], SttpBackend[F, Any]),\n  ] = for\n    kms <- GatewayKmsClient\n      .make[F](conf.kmsAlias)\n      .mapK(EitherT.liftK[F, String])\n    web3jBytes <- Resource.eval:\n      EitherT.fromEither:\n        ByteVector.fromBase64Descriptive(conf.encryptedEthEndpoint)\n    web3jPlaintextResource <- Resource.eval(kms.decrypt(web3jBytes.toArray))\n    web3jPlaintext <- web3jPlaintextResource.mapK(EitherT.liftK[F, String])\n    web3jEndpoint = String(web3jPlaintext, \"UTF-8\")\n    web3j <- GatewayWeb3Service\n      .web3Resource[F](web3jEndpoint)\n      .mapK(EitherT.liftK[F, String])\n    dbBytes <- Resource.eval:\n      EitherT.fromEither:\n        ByteVector.fromBase64Descriptive(conf.encryptedDatabaseEndpoint)\n    dbPlaintextResource <- Resource.eval(kms.decrypt(dbBytes.toArray))\n    dbPlaintext         <- dbPlaintextResource.mapK(EitherT.liftK[F, String])\n    dbEndpoint = String(dbPlaintext, \"UTF-8\")\n    db <- GatewayDatabaseClient\n      .make[F](dbEndpoint, conf.databaseTableName, conf.databaseValueColumn)\n      .mapK(EitherT.liftK[F, String])\n    sttp <- ArmeriaCatsBackend.resource[F]().mapK(EitherT.liftK[F, String])\n    dispatcher <- Dispatcher.parallel[F].mapK(EitherT.liftK[F, String])\n    server <- GatewayServer\n      .make[F](dispatcher, conf.localServerPort, db, kms)\n      .mapK(EitherT.liftK[F, String])\n  yield (kms, web3j, db, sttp)\n\n  def getSimpleResource[F[_]: Async](\n      conf: GatewaySimpleConf,\n  ): Resource[EitherT[F, String, *], (GatewayKmsClient[F], Web3j, SttpBackend[F, Any])] = for\n    kms <- GatewayKmsClient\n      .make[F](conf.kmsAlias)\n      .mapK(EitherT.liftK[F, String])\n    web3jBytes <- Resource.eval:\n      EitherT.fromEither:\n        ByteVector.fromBase64Descriptive(conf.encryptedEthEndpoint)\n    web3jPlaintextResource <- Resource.eval(kms.decrypt(web3jBytes.toArray))\n    web3jPlaintext <- web3jPlaintextResource.mapK(EitherT.liftK[F, String])\n    web3jEndpoint = String(web3jPlaintext, \"UTF-8\")\n    web3j <- GatewayWeb3Service\n      .web3Resource[F](web3jEndpoint)\n      .mapK(EitherT.liftK[F, String])\n    sttp <- ArmeriaCatsBackend.resource[F]().mapK(EitherT.liftK[F, String])\n  yield (kms, web3j, sttp)\n"
  },
  {
    "path": "modules/eth-gateway-common/src/main/scala/io/leisuremeta/chain/gateway/eth/common/GatewayServer.scala",
    "content": "package io.leisuremeta.chain.gateway.eth.common\n\nimport cats.data.EitherT\nimport cats.effect.{Async, Resource}\nimport cats.effect.std.Dispatcher\nimport cats.syntax.flatMap.*\n\nimport com.linecorp.armeria.server.Server\nimport scodec.bits.ByteVector\nimport sttp.capabilities.fs2.Fs2Streams\nimport sttp.tapir.server.ServerEndpoint\nimport sttp.tapir.server.armeria.cats.{\n  ArmeriaCatsServerInterpreter,\n  ArmeriaCatsServerOptions,\n}\nimport sttp.tapir.server.interceptor.log.DefaultServerLog\n\nimport client.{GatewayDatabaseClient, GatewayKmsClient}\n\nobject GatewayServer:\n  def gatewayServerEndpoint[F[_]: Async](\n      dbClient: GatewayDatabaseClient[F],\n      kmsClient: GatewayKmsClient[F],\n  ): ServerEndpoint.Full[\n    Unit,\n    Unit,\n    GatewayApi.GatewayRequest,\n    GatewayApi.GatewayApiError,\n    GatewayApi.GatewayResponse,\n    Any,\n    F,\n  ] =\n    def decrypt(\n        key: String,\n        doublyEncryptedFrontPartBase64: String,\n    ): EitherT[F, String, Resource[F, Array[Byte]]] =\n      for\n        frontBytes <- EitherT.fromEither[F]:\n          ByteVector.fromBase64Descriptive(doublyEncryptedFrontPartBase64)\n        doublyEncryptedBackPartBase64Option <- dbClient.select(key)\n        backPartBase64 <- EitherT.fromOption(\n          doublyEncryptedBackPartBase64Option,\n          s\"Value not found for key: ${key}\",\n        )\n        backBytes <- EitherT.fromEither[F]:\n          ByteVector.fromBase64Descriptive(backPartBase64)\n        plaintextResource <- kmsClient.decrypt:\n          (frontBytes ++ backBytes).toArrayUnsafe\n      yield plaintextResource\n\n    GatewayApi.postDecryptEndpoint.serverLogic: request =>\n      decrypt(\n        request.key,\n        request.doublyEncryptedFrontPartBase64,\n      ).value\n        .flatMap:\n          case Left(errorMsg) =>\n            Async[F].delay:\n              Left(GatewayApi.ServerError(errorMsg))\n          case Right(resource) =>\n            resource.use: plaintext =>\n              Async[F].delay:\n                Right(\n                  GatewayApi.GatewayResponse(\n                    ByteVector.view(plaintext).toBase64,\n                  ),\n                )\n\n  def make[F[_]: Async](\n      dispatcher: Dispatcher[F],\n      port: Int,\n      dbClient: GatewayDatabaseClient[F],\n      kmsClient: GatewayKmsClient[F],\n  ): Resource[F, Server] =\n    val serverEndpoint = gatewayServerEndpoint[F](dbClient, kmsClient)\n    Resource.fromAutoCloseable(getServer(dispatcher, port, serverEndpoint))\n\n  @SuppressWarnings(Array(\"org.wartremover.warts.Null\"))\n  def getServer[F[_]: Async](\n      dispatcher: Dispatcher[F],\n      port: Int,\n      serverEndpoint: ServerEndpoint[Fs2Streams[F], F],\n  ): F[Server] = Async[F].fromCompletableFuture:\n\n    def log[F[_]: Async](\n        level: scribe.Level,\n    )(msg: String, exOpt: Option[Throwable])(using\n        mdc: scribe.mdc.MDC,\n    ): F[Unit] = Async[F].delay:\n      exOpt match\n        case None     => scribe.log(level, mdc, msg)\n        case Some(ex) => scribe.log(level, mdc, msg, ex)\n\n    val serverLog = DefaultServerLog(\n      doLogWhenReceived = log(scribe.Level.Info)(_, None),\n      doLogWhenHandled = log(scribe.Level.Info),\n      doLogAllDecodeFailures = log(scribe.Level.Info),\n      doLogExceptions =\n        (msg: String, ex: Throwable) => Async[F].delay(scribe.warn(msg, ex)),\n      noLog = Async[F].pure(()),\n    )\n\n    val serverOptions = ArmeriaCatsServerOptions\n      .customiseInterceptors[F](dispatcher)\n      .serverLog(serverLog)\n      .options\n\n    val tapirService = ArmeriaCatsServerInterpreter[F](serverOptions)\n      .toService(serverEndpoint)\n\n    val server = Server.builder\n      .maxRequestLength(128 * 1024 * 1024)\n      .requestTimeout(java.time.Duration.ofMinutes(10))\n      .http(port)\n      .service(tapirService)\n      .build\n    Async[F].delay:\n      server.start().thenApply(_ => server)\n"
  },
  {
    "path": "modules/eth-gateway-common/src/main/scala/io/leisuremeta/chain/gateway/eth/common/GatewaySimpleConf.scala",
    "content": "package io.leisuremeta.chain.gateway.eth.common\n\nimport pureconfig.*\nimport pureconfig.generic.derivation.default.*\n\nfinal case class GatewaySimpleConf(\n    ethChainId: Int,\n    ethLmContractAddress: String,\n    ethMultisigContractAddress: String,\n    gatewayEthAddress: String,\n    depositExempts: Seq[String],\n    lmEndpoint: String,\n    kmsAlias: String,\n    encryptedEthEndpoint: String,\n    encryptedEthPrivate: String,\n    encryptedLmPrivate: String,\n    targetGateway: String,\n) derives ConfigReader\n\nobject GatewaySimpleConf:\n  def loadOrThrow(): GatewaySimpleConf =\n    ConfigSource.default.loadOrThrow[GatewaySimpleConf]\n"
  },
  {
    "path": "modules/eth-gateway-common/src/main/scala/io/leisuremeta/chain/gateway/eth/common/GatewayWeb3Service.scala",
    "content": "package io.leisuremeta.chain.gateway.eth.common\n\nimport cats.effect.{Async, Resource}\n\nimport okhttp3.OkHttpClient\nimport okhttp3.logging.HttpLoggingInterceptor\nimport org.web3j.protocol.Web3j\nimport org.web3j.protocol.http.HttpService\n\nobject GatewayWeb3Service:\n  def web3Resource[F[_]: Async](url: String): Resource[F, Web3j] = Resource.make {\n\n    val interceptor = HttpLoggingInterceptor()\n    interceptor.setLevel(HttpLoggingInterceptor.Level.BASIC)\n\n    val client = OkHttpClient\n      .Builder()\n      .addInterceptor(interceptor)\n      .build()\n\n    Async[F].delay(Web3j.build(new HttpService(url, client)))\n  }(web3j => Async[F].delay(web3j.shutdown()))\n"
  },
  {
    "path": "modules/eth-gateway-common/src/main/scala/io/leisuremeta/chain/gateway/eth/common/client/GatewayApiClient.scala",
    "content": "package io.leisuremeta.chain.gateway.eth.common\npackage client\n\nimport cats.data.EitherT\nimport cats.effect.Async\nimport cats.syntax.bifunctor.*\nimport cats.syntax.functor.*\n\nimport sttp.client3.SttpBackend\nimport sttp.model.Uri\nimport sttp.tapir.DecodeResult\nimport sttp.tapir.client.sttp.SttpClientInterpreter\n\ntrait GatewayApiClient[F[_]]:\n  def get(\n      key: String,\n      doublyEncryptedFrontPartBase64: String,\n  ): EitherT[F, String, String]\n\nobject GatewayApiClient:\n\n  def apply[F[_]: GatewayApiClient]: GatewayApiClient[F] = summon\n\n  def make[F[_]: Async](\n    backend: SttpBackend[F, Any],\n    uri: Uri\n  ): GatewayApiClient[F] =\n    val sttpClient = SttpClientInterpreter().toClient(\n      GatewayApi.postDecryptEndpoint,\n      Some(uri),\n      backend,\n    )\n    @SuppressWarnings(Array(\"org.wartremover.warts.ToString\"))\n    def sanitize[A, B](\n        result: DecodeResult[Either[A, B]],\n    ): Either[String, B] =\n      result match\n        case DecodeResult.Value(v)   => v.leftMap(_.toString)\n        case f: DecodeResult.Failure => Left(s\"Fail: ${f.toString()}\")\n\n    new GatewayApiClient[F]:\n      override def get(\n          key: String,\n          doublyEncryptedFrontPartBase64: String,\n      ): EitherT[F, String, String] =\n        EitherT\n          .apply:\n            sttpClient(\n              GatewayApi.GatewayRequest(key, doublyEncryptedFrontPartBase64),\n            ).map(sanitize)\n          .map(_.singlyEncryptedBase64)\n"
  },
  {
    "path": "modules/eth-gateway-common/src/main/scala/io/leisuremeta/chain/gateway/eth/common/client/GatewayDatabaseClient.scala",
    "content": "package io.leisuremeta.chain.gateway.eth.common.client\n\nimport scala.jdk.CollectionConverters.*\n\nimport cats.data.EitherT\nimport cats.effect.{Async, Resource}\nimport cats.syntax.functor.*\n\nimport com.github.jasync.sql.db.QueryResult\nimport com.github.jasync.sql.db.mysql.MySQLConnectionBuilder\nimport com.github.jasync.sql.db.ConnectionPoolConfigurationBuilder\n\ntrait GatewayDatabaseClient[F[_]]:\n  def select(\n      key: String,\n  ): EitherT[F, String, Option[String]]\n\nobject GatewayDatabaseClient:\n\n  def apply[F[_]: GatewayDatabaseClient]: GatewayDatabaseClient[F] = summon\n\n  @SuppressWarnings(Array(\"org.wartremover.warts.OptionPartial\"))\n  def make[F[_]: Async](\n      dbEndpoint: String,\n      tableName: String,\n      valueColumn: String,\n  ): Resource[F, GatewayDatabaseClient[F]] =\n    Resource\n      .make:\n        Async[F].blocking:\n          val reg = \"\"\"^jdbc:mysql://(.*):(\\d{4,5})/(.*)\\?user=(.*)&password=(.*)$\"\"\".r\n          val builder = for \n            res <- reg.findFirstMatchIn(dbEndpoint)\n            config = ConnectionPoolConfigurationBuilder()\n            _ = config.setHost(res.group(1))\n            _ = config.setPort(res.group(2).toInt)\n            _ = config.setDatabase(res.group(3))\n            _ = config.setUsername(res.group(4))\n            _ = config.setPassword(res.group(5))\n          yield config\n          MySQLConnectionBuilder.createConnectionPool(builder.get)\n      .apply: connection =>\n        Async[F]\n          .fromCompletableFuture:\n            Async[F].delay(connection.disconnect())\n          .map(_ => ())\n      .map: connection =>\n        new GatewayDatabaseClient[F]:\n          override def select(key: String): EitherT[F, String, Option[String]] =\n            Async[F]\n              .attemptT:\n                Async[F]\n                  .fromCompletableFuture:\n                    Async[F].delay:\n                      connection.sendPreparedStatement(\n                        s\"SELECT GTWY_SE_CODE, ${valueColumn} FROM ${tableName} WHERE GTWY_SE_CODE = ?\",\n                        List(key).asJava,\n                      )\n                  .map: (queryResult: QueryResult) =>\n                    scribe.info(s\"Query result: ${queryResult}\")\n                    queryResult.getRows.asScala.headOption.map(\n                      _.getString(valueColumn),\n                    )\n              .leftMap(_.getMessage())\n"
  },
  {
    "path": "modules/eth-gateway-common/src/main/scala/io/leisuremeta/chain/gateway/eth/common/client/GatewayKmsClient.scala",
    "content": "package io.leisuremeta.chain.gateway.eth.common.client\n\nimport cats.data.EitherT\nimport cats.effect.{Async, Resource}\nimport cats.syntax.functor.*\n\nimport software.amazon.awssdk.core.SdkBytes\nimport software.amazon.awssdk.services.kms.KmsAsyncClient\nimport software.amazon.awssdk.services.kms.model.DecryptRequest\n\ntrait GatewayKmsClient[F[_]]:\n  def decrypt(\n      cipherText: Array[Byte],\n  ): EitherT[F, String, Resource[F, Array[Byte]]]\n\nobject GatewayKmsClient:\n\n  def apply[F[_]: GatewayKmsClient]: GatewayKmsClient[F] = summon\n\n  def make[F[_]: Async](alias: String): Resource[F, GatewayKmsClient[F]] =\n    Resource\n      .fromAutoCloseable(Async[F].delay(KmsAsyncClient.create()))\n      .map: kmsAsyncClient =>\n        new GatewayKmsClient[F]:\n          override def decrypt(\n              cipherText: Array[Byte],\n          ): EitherT[F, String, Resource[F, Array[Byte]]] = Async[F]\n            .attemptT:\n              Async[F]\n                .fromCompletableFuture:\n                  Async[F].delay:\n                    kmsAsyncClient.decrypt:\n                      DecryptRequest\n                        .builder()\n                        .keyId(s\"alias/${alias}\")\n                        .ciphertextBlob(SdkBytes.fromByteArray(cipherText))\n                        .build()\n                .map: decryptResponse =>\n                  Resource\n                    .make:\n                      Async[F].delay:\n                        decryptResponse.plaintext().asByteArrayUnsafe()\n                    .apply: byteArray =>\n                      Async[F].delay:\n                        val emptyArray =\n                          Array.fill[Byte](byteArray.length)(0x00)\n                        System.arraycopy(\n                          emptyArray,\n                          0,\n                          byteArray,\n                          0,\n                          byteArray.length,\n                        )\n            .leftMap(_.getMessage())\n"
  },
  {
    "path": "modules/eth-gateway-common/src/test/scala/io/leisuremeta/chain/gateway/eth/common/GatewayServerTest.scala",
    "content": "package io.leisuremeta.chain.gateway.eth.common\n\nimport cats.effect.IO\n\nclass GatewayServerTest extends munit.CatsEffectSuite:\n\n  test(\"should start and stop server\"):\n    IO(42).assertEquals(42)\n"
  },
  {
    "path": "modules/eth-gateway-deposit/src/main/resources/application.conf.sample",
    "content": "eth-chain-id = 11155111\neth-lm-contract-address = \"0x02886136510172E313932EA66FE7bDC4d4C2fcc4\"\neth-multisig-contract-address = \"0x02886136510172E313932EA66FE7bDC4d4C2fcc4\"\ngateway-eth-address = \"0x3E984778B16b5a0fE489Aac0b716078202400b6A\"\ndeposit-exempts = [\n  \"0x3E984778B16b5a0fE489Aac0b716078202400b6A\",\n]\nlm-endpoint = \"http://127.0.0.1:8080\"\nkms-alias = \"gateway_withdraw_key\"\nencrypted-eth-endpoint = \"...\"\nencrypted-eth-private = \"...\"\nencrypted-lm-private = \"...\"\ntarget-gateway = \"eth-gateway\"\n"
  },
  {
    "path": "modules/eth-gateway-deposit/src/main/scala/io/leisuremeta/chain/gateway/eth/EthGatewayDepositMain.scala",
    "content": "package io.leisuremeta.chain\npackage gateway.eth\n\n//import java.math.BigInteger\nimport java.nio.charset.StandardCharsets \nimport java.nio.file.{Files, Paths, StandardOpenOption}\nimport java.util.{Arrays, Locale}\nimport java.time.Instant\n//import java.time.temporal.ChronoUnit\nimport scala.jdk.CollectionConverters.*\n//import scala.jdk.FutureConverters.*\nimport scala.concurrent.duration.*\nimport scala.util.Try\n\nimport cats.data.EitherT\nimport cats.effect.{Async, Clock, ExitCode, IO, IOApp, Resource}\nimport cats.syntax.applicativeError.*\nimport cats.syntax.apply.*\n//import cats.syntax.bifunctor.*\nimport cats.syntax.eq.*\nimport cats.syntax.flatMap.*\nimport cats.syntax.functor.*\nimport cats.syntax.traverse.*\n\n//import com.github.jasync.sql.db.{Connection, QueryResult}\n//import com.github.jasync.sql.db.mysql.MySQLConnectionBuilder\n//import com.typesafe.config.{Config, ConfigFactory}\nimport io.circe.Encoder\nimport io.circe.syntax.given\nimport io.circe.generic.auto.*\nimport io.circe.parser.decode\nimport org.web3j.abi.{\n  EventEncoder,\n//  FunctionEncoder,\n  FunctionReturnDecoder,\n  TypeReference,\n}\nimport org.web3j.abi.datatypes.{Address, Event, Type}\nimport org.web3j.abi.datatypes.generated.Uint256\n//import org.web3j.crypto.{Credentials, MnemonicUtils}\nimport org.web3j.protocol.Web3j\nimport org.web3j.protocol.core.{\n  DefaultBlockParameter,\n//  DefaultBlockParameterName,\n}\nimport org.web3j.protocol.core.methods.request.{EthFilter}\nimport org.web3j.protocol.core.methods.response.EthLog.{LogResult, LogObject}\n//import org.web3j.protocol.core.methods.response.TransactionReceipt\n//import org.web3j.protocol.http.HttpService\n//import org.web3j.tx.RawTransactionManager\n//import org.web3j.tx.response.PollingTransactionReceiptProcessor\nimport sttp.client3.*\nimport sttp.model.{MediaType, StatusCode}\n\nimport lib.crypto.CryptoOps//, KeyPair}\n//import lib.crypto.Hash.ops.*\nimport lib.crypto.Sign.ops.*\nimport lib.datatype.*\nimport api.model.*\n//import api.model.TransactionWithResult.ops.*\nimport api.model.api_model.{AccountInfo, BalanceInfo}\nimport api.model.token.*\nimport common.*\nimport common.client.*\n\nobject EthGatewayDepositMain extends IOApp:\n\n  case class TransferTokenEvent(\n      blockNumber: BigInt,\n      txHash: String,\n      from: String,\n      to: String,\n      value: BigInt,\n  )\n\n  extension [A](logResult: LogResult[A])\n    def toTransferTokenEvent: TransferTokenEvent =\n\n      val log = logResult.get.asInstanceOf[LogObject].get\n\n      val typeRefAddress: TypeReference[Address] =\n        new TypeReference[Address]() {}\n      val typeRefUint256: TypeReference[Type[?]] =\n        (new TypeReference[Uint256]() {}).asInstanceOf[TypeReference[Type[?]]]\n\n      val topics = log.getTopics.asScala.toVector\n      val from: Address = FunctionReturnDecoder\n        .decodeIndexedValue[Address](topics(1), typeRefAddress)\n        .asInstanceOf[Address]\n      val to: Address = FunctionReturnDecoder\n        .decodeIndexedValue[Address](topics(2), typeRefAddress)\n        .asInstanceOf[Address]\n      val amount = FunctionReturnDecoder\n        .decode(log.getData, List(typeRefUint256).asJava)\n        .asScala\n        .headOption.map: amount =>\n          amount\n            .asInstanceOf[Uint256]\n            .getValue\n        .fold(BigInt(0))(BigInt(_))\n\n      TransferTokenEvent(\n        blockNumber = BigInt(log.getBlockNumber),\n        txHash = log.getTransactionHash,\n        from = from.getValue,\n        to = to.getValue,\n        value = amount,\n      )\n\n  def writeUnsentDeposits[F[_]: Async](\n      deposits: Seq[TransferTokenEvent],\n  ): F[Unit] = Async[F].blocking:\n    val path = Paths.get(\"unsent-deposits.json\")\n    val json = deposits.asJson.spaces2\n    val _ = Files.write(\n      path,\n      json.getBytes(StandardCharsets.UTF_8),\n      StandardOpenOption.CREATE,\n      StandardOpenOption.WRITE,\n      StandardOpenOption.TRUNCATE_EXISTING,\n    )\n\n  def readUnsentDeposits[F[_]: Async](): F[Seq[TransferTokenEvent]] =\n    Async[F].blocking:\n      val path = Paths.get(\"unsent-deposits.json\")\n      val seqEither = for\n        json <- Try(Files.readAllLines(path).asScala.mkString(\"\\n\")).toEither\n        seq  <- decode[Seq[TransferTokenEvent]](json)\n      yield seq\n      seqEither match\n        case Right(seq) => seq\n        case Left(e) =>\n          e.printStackTrace()\n          scribe.error(s\"Error reading unsent deposits: ${e.getMessage}\")\n          Seq.empty\n\n  def logSentDeposits[F[_]: Async](\n      deposits: Seq[(Account, TransferTokenEvent)],\n  ): F[Unit] =\n    if deposits.isEmpty then Async[F].unit\n    else\n      Async[F].blocking:\n        val path = Paths.get(\"sent-deposits.logs\")\n        val jsons =\n          deposits.map(_.asJson.noSpaces).mkString(\"\", \"\\n\", \"\\n\")\n        val _ = Files.write(\n          path,\n          jsons.getBytes(StandardCharsets.UTF_8),\n          StandardOpenOption.CREATE,\n          StandardOpenOption.WRITE,\n          StandardOpenOption.APPEND,\n        )\n\n  def writeLastBlockRead[F[_]: Async](blockNumber: BigInt): F[Unit] =\n    Async[F].blocking:\n      val path = Paths.get(\"last-block-read.json\")\n      val json = blockNumber.asJson.spaces2\n      val _ = Files.write(\n        path,\n        json.getBytes(StandardCharsets.UTF_8),\n        StandardOpenOption.CREATE,\n        StandardOpenOption.WRITE,\n        StandardOpenOption.TRUNCATE_EXISTING,\n      )\n\n  def readLastBlockRead[F[_]: Async](): F[BigInt] = Async[F].blocking:\n    val path = Paths.get(\"last-block-read.json\")\n    val blockNumberEither = for\n      json <- Try(Files.readAllLines(path).asScala.mkString(\"\\n\")).toEither\n      blockNumber <- decode[BigInt](json)\n    yield blockNumber\n\n    blockNumberEither match\n      case Right(blockNumber) => blockNumber\n      case Left(e) =>\n        scribe.error(s\"Error reading last block read: ${e.getMessage}\")\n        BigInt(0)\n\n  def getTransferTokenEvents[F[_]: Async](\n      web3j: Web3j,\n      contractAddress: String,\n      fromBlock: DefaultBlockParameter,\n      toBlock: DefaultBlockParameter,\n  ): F[Seq[TransferTokenEvent]] =\n    val TransferEvent = new Event(\n      \"Transfer\",\n      Arrays.asList(\n        new TypeReference[Address]() {},\n        new TypeReference[Address]() {},\n        new TypeReference[Uint256]() {},\n      ),\n    )\n\n    val filter = new EthFilter(fromBlock, toBlock, contractAddress)\n    filter.addSingleTopic(EventEncoder.encode(TransferEvent))\n\n    Async[F]\n      .fromCompletableFuture:\n        Async[F].delay(web3j.ethGetLogs(filter).sendAsync())\n      .map: ethLog =>\n        ethLog.getLogs.asScala.map(_.toTransferTokenEvent).toSeq\n\n  def submitTx[F[_]: Async: GatewayKmsClient](\n      sttp: SttpBackend[F, Any],\n      lmEndpoint: String,\n      encryptedLmPrivate: String,\n      account: Account,\n      tx: Transaction,\n  ): F[Unit] = GatewayDecryptService\n    .getSimplifiedPlainTextResource[F](encryptedLmPrivate)\n    .value\n    .flatMap:\n      case Left(msg) =>\n        scribe.error(s\"Failed to get LM private: $msg\")\n        Async[F].sleep(10.seconds) *> submitTx[F](sttp, lmEndpoint, encryptedLmPrivate, account, tx)\n      case Right(lmPrivateResource) =>\n        lmPrivateResource.use: lmPrivateArray =>\n          val keyPair    = CryptoOps.fromPrivate(BigInt(lmPrivateArray))\n          val Right(sig) = keyPair.sign(tx): @unchecked\n          val signedTxs  = Seq(Signed(AccountSignature(sig, account), tx))\n\n          scribe.info(s\"Sending signed transactions: $signedTxs\")\n\n          given bodyJsonSerializer[A: Encoder]: BodySerializer[A] =\n            (a: A) =>\n              val serialized = a.asJson.noSpaces\n              StringBody(serialized, \"UTF-8\", MediaType.ApplicationJson)\n\n          basicRequest\n            .response(asStringAlways)\n            .post(uri\"http://$lmEndpoint/tx\")\n            .body(signedTxs)\n            .send(sttp)\n            .map: response =>\n              scribe.info(s\"Response: $response\")\n\n  def mintLM[F[_]: Async: Clock: GatewayKmsClient](\n      sttp: SttpBackend[F, Any],\n      lmEndpoint: String,\n      encryptedLmPrivate: String,\n      toAccount: Account,\n      amount: BigInt,\n      targetGateway: String,\n  ): F[Unit] =\n\n    val networkId = NetworkId(BigNat.unsafeFromLong(1000L))\n    val lmDef     = TokenDefinitionId(Utf8.unsafeFrom(\"LM\"))\n    val account   = Account(Utf8.unsafeFrom(targetGateway))\n\n    Clock[F].realTimeInstant.flatMap: now =>\n\n      val mintFungibleToken = Transaction.TokenTx.MintFungibleToken(\n        networkId = networkId,\n        createdAt = now,\n        definitionId = lmDef,\n        outputs = Map(toAccount -> BigNat.unsafeFromBigInt(amount)),\n      )\n\n      submitTx[F](sttp, lmEndpoint, encryptedLmPrivate, account, mintFungibleToken)\n\n  def findAccountByEthAddress[F[_]: Async](\n      sttp: SttpBackend[F, Any],\n      lmEndpoint: String,\n      ethAddress: String,\n  ): F[Option[Account]] =\n\n    def findAccountByEthAddress0(\n        ethAddress: String,\n    ): F[Option[Account]] =\n      scribe.info(s\"requesting eth address $ethAddress 's LM account\")\n      Async[F]\n        .attempt:\n          basicRequest\n            .response(asStringAlways)\n            .get(uri\"http://$lmEndpoint/eth/$ethAddress\")\n            .send(sttp)\n            .map: response =>\n              scribe.info(s\"eth address $ethAddress response: $response\")\n\n              if response.code.isSuccess then\n                Some(\n                  Account(Utf8.unsafeFrom(response.body.drop(1).dropRight(1))),\n                )\n              else\n                scribe.info(s\"Account $ethAddress not found: ${response.body}\")\n                None\n        .map(_.toOption.flatten)\n\n    findAccountByEthAddress0(ethAddress).flatMap: accountOption =>\n      if accountOption.isEmpty && ethAddress.startsWith(\"0x\") then\n        findAccountByEthAddress0(ethAddress.drop(2))\n      else Async[F].pure(accountOption)\n\n  def checkDepositAndMint[F[_]: Async: GatewayKmsClient](\n      sttp: SttpBackend[F, Any],\n      web3j: Web3j,\n      lmEndpoint: String,\n      encryptedLmPrivate: String,\n      ethContract: String,\n      multisigContractAddress: String,\n      exemptAddressSet: Set[String],\n      startBlockNumber: BigInt,\n      endBlockNumber: BigInt,\n      targetGateway: String,\n  ): F[Unit] =\n    for\n      events <- getTransferTokenEvents[F](\n        web3j,\n        ethContract.toLowerCase(Locale.ENGLISH),\n        DefaultBlockParameter.valueOf(startBlockNumber.bigInteger),\n        DefaultBlockParameter.valueOf(endBlockNumber.bigInteger),\n      )\n      _ <- Async[F].delay(scribe.info(s\"events: $events\"))\n      depositEvents = events.filter: e =>\n        e.to.toLowerCase(Locale.ENGLISH) === multisigContractAddress.toLowerCase(Locale.ENGLISH)\n          && !exemptAddressSet.contains(e.to.toLowerCase(Locale.ENGLISH))\n      _ <- Async[F].delay:\n        scribe.info(s\"current deposit events: $depositEvents\")\n      oldEvents <- readUnsentDeposits[F]()\n      _ <- Async[F].delay(scribe.info(s\"old deposit events: $oldEvents\"))\n      allEvents = depositEvents ++ oldEvents\n      _ <- Async[F].delay(scribe.info(s\"all deposit events: $allEvents\"))\n      eventAndAccountOptions <- allEvents.toList.traverse: event =>\n//        scribe.info(s\"current event: $event\")\n//        val amount    = event.value\n//        val toAccount = Account(Utf8.unsafeFrom(event.to))\n        findAccountByEthAddress(sttp, lmEndpoint, event.from).map:\n          (accountOption: Option[Account]) =>\n            scribe.info:\n              s\"eth address ${event.from}'s LM account: $accountOption\"\n            (event, accountOption)\n      (known, unknown) = eventAndAccountOptions.partition(_._2.nonEmpty)\n      toMints = known.flatMap:\n        case (event, Some(account)) => List((account, event))\n        case (event, None) =>\n          scribe.error(s\"Internal error: Account ${event.from} not found\")\n          Nil\n      _ <- Async[F].delay(scribe.info(s\"toMints: $toMints\"))\n      _ <- toMints.traverse: (account, event) =>\n        mintLM[F](sttp, lmEndpoint, encryptedLmPrivate, account, event.value, targetGateway)\n      _ <- logSentDeposits[F](toMints.map(_._1).zip(known.map(_._1)))\n      unsent = unknown.map(_._1)\n      _ <- Async[F].delay(scribe.info(s\"unsent: $unsent\"))\n      _ <- writeUnsentDeposits[F](unsent)\n    yield ()\n\n  def checkLoop[F[_]: Async: GatewayKmsClient](\n      sttp: SttpBackend[F, Any],\n      web3j: Web3j,\n      conf: GatewaySimpleConf,\n  ): F[Unit] =\n    def run: F[Unit] = for\n      _ <- Async[F].delay:\n        scribe.info(s\"Checking for deposit / withdrawal events\")\n      lastBlockNumber <- readLastBlockRead[F]()\n      startBlockNumber = lastBlockNumber + 1\n      blockNumber <- Async[F]\n        .fromCompletableFuture(Async[F].delay(web3j.ethBlockNumber.sendAsync()))\n        .map(_.getBlockNumber)\n        .map(BigInt(_))\n      _ <- Async[F].delay(scribe.info(s\"blockNumber: $blockNumber\"))\n      endBlockNumber = (startBlockNumber + 10000) min (blockNumber - 6)\n      _ <- Async[F].delay:\n        scribe.info(s\"startBlockNumber: $startBlockNumber\")\n        scribe.info:\n          s\"blockNumber: $blockNumber, endBlockNumber: $endBlockNumber\"\n      _ <- checkDepositAndMint[F](\n        sttp = sttp,\n        web3j = web3j,\n        lmEndpoint = conf.lmEndpoint,\n        encryptedLmPrivate = conf.encryptedLmPrivate,\n        ethContract = conf.ethLmContractAddress,\n        multisigContractAddress = conf.ethMultisigContractAddress,\n        exemptAddressSet = conf.depositExempts.map(_.toLowerCase(Locale.ENGLISH)).toSet,\n        startBlockNumber = startBlockNumber,\n        endBlockNumber = endBlockNumber,\n        targetGateway = conf.targetGateway,\n      )\n      _ <- writeLastBlockRead[F](endBlockNumber)\n      _ <- Async[F].delay(scribe.info(s\"Deposit check finished.\"))\n    yield ()\n\n    def loop: F[Unit] = for\n      _ <- run.orElse(Async[F].unit)\n      _ <- Async[F].sleep(10000.millis)\n      _ <- loop\n    yield ()\n\n    loop\n\n  def getGasPrice[F[_]: Async](web3j: Web3j): F[BigInt] =\n    Async[F]\n      .fromCompletableFuture:\n        Async[F].delay(web3j.ethGasPrice.sendAsync())\n      .map: ethGasPrice =>\n        BigInt(ethGasPrice.getGasPrice)\n\n  def getBalance[F[_]: Async](\n      sttp: SttpBackend[F, Any],\n      lmEndpoint: String,\n      targetGateway: String,\n  ): F[Option[BalanceInfo]] =\n    val lmDef = TokenDefinitionId(Utf8.unsafeFrom(\"LM\"))\n    basicRequest\n      .response(asStringAlways)\n      .get(uri\"http://$lmEndpoint/balance/$targetGateway?movable=all\")\n      .send(sttp)\n      .map: response =>\n        if response.code.isSuccess then\n          decode[Map[TokenDefinitionId, BalanceInfo]](response.body) match\n            case Right(balanceInfoMap) => balanceInfoMap.get(lmDef)\n            case Left(error) =>\n              scribe.error(s\"Error decoding balance info: $error\")\n              scribe.error(s\"response: ${response.body}\")\n              None\n        else if response.code.code === StatusCode.NotFound.code then\n          scribe.info:\n            s\"balance of account $targetGateway not found: ${response.body}\"\n          None\n        else\n          scribe.error(s\"Error getting balance: ${response.body}\")\n          None\n\n  def getAccountInfo[F[_]: Async](\n      sttp: SttpBackend[F, Any],\n      lmAddress: String,\n      account: Account,\n  ): F[Option[AccountInfo]] =\n    basicRequest\n      .response(asStringAlways)\n      .get(uri\"http://$lmAddress/account/${account.utf8.value}\")\n      .send(sttp)\n      .map: response =>\n        if response.code.isSuccess then\n          decode[AccountInfo](response.body) match\n            case Right(accountInfo) => Some(accountInfo)\n            case Left(error) =>\n              scribe.error(s\"Error decoding account info: $error\")\n              None\n        else if response.code.code === StatusCode.NotFound.code then\n          scribe.info(s\"account info not found: ${response.body}\")\n          None\n        else\n          scribe.error(s\"Error getting account info: ${response.body}\")\n          None\n\n  def run(args: List[String]): IO[ExitCode] =\n    val conf = GatewaySimpleConf.loadOrThrow()\n    GatewayResource\n      .getSimpleResource[IO](conf)\n      .use: (kms, web3j, sttp) =>\n        given GatewayKmsClient[IO]      = kms\n        EitherT.liftF:\n          checkLoop[IO](\n            sttp = sttp,\n            web3j = web3j,\n            conf = conf,\n          )\n      .value\n      .map:\n        case Left(error) => scribe.error(s\"Error: $error\")\n        case Right(result) => scribe.info(s\"Result: $result\")\n      .as(ExitCode.Success)\n"
  },
  {
    "path": "modules/eth-gateway-setup/src/main/resources/application.conf.sample",
    "content": "// eth-private = \"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef\"\n// lm-private = \"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef\"\n// deposit-db {\n//   host = \"some.url1\"\n//   port = 3306\n//   db = \"some-db\"\n//   table = \"SOME_TABLE\"\n//   value-column = \"PRIVKY\"\n//   user = \"user\"\n//   password = \"password\"\n// }\n// withdraw-db {\n//   host = \"some.url2\"\n//   port = 3306\n//   db = \"some-db\"\n//   table = \"SOME_TABLE\"\n//   value-column = \"PRIVKY\"\n//   user = \"user\"\n//   password = \"password\"\n// }\n// db-write-account {\n//   user = \"writeuser\"\n//   password = \"writepassword\"\"\n// }\n// deposit-kms-alias = \"deposit_kms\"\n// withdraw-kms-alias = \"withdraw_kms\"\n// eth-endpoint = \"https://goerli.infura.io/v3/1234567890abcdef1234567890abcdef\"\n\neth-private = \"0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef\"\nlm-private = \"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef\"\nkms-alias = \"gateway_withdraw_key\"\neth-endpoint = \"https://sepolia.infura.io/v3/1234567890abcdef1234567890abcdef\"\n"
  },
  {
    "path": "modules/eth-gateway-setup/src/main/scala/io/leisuremeta/chain/gateway/eth/setup/EthGatewaySetupConfig.scala",
    "content": "package io.leisuremeta.chain.gateway.eth.setup\n\nimport pureconfig.*\nimport pureconfig.generic.derivation.default.*\n\nimport EthGatewaySetupConfig.*\n\nfinal case class EthGatewaySetupConfig(\n    ethPrivate: String,\n    lmPrivate: String,\n    depositDb: DbConfig,\n    withdrawDb: DbConfig,\n    dbWriteAccount: DbWriteAccountConfig,\n    depositKmsAlias: String,\n    withdrawKmsAlias: String,\n    ethEndpoint: String,\n) derives ConfigReader\n\nobject EthGatewaySetupConfig:\n\n  def apply(): EthGatewaySetupConfig =\n    ConfigSource.default.loadOrThrow[EthGatewaySetupConfig]\n\n  final case class DbConfig(\n      host: String,\n      port: Int,\n      db: String,\n      table: String,\n      valueColumn: String,\n      user: String,\n      password: String,\n  )\n\n  final case class DbWriteAccountConfig(\n      user: String,\n      password: String,\n  )\n"
  },
  {
    "path": "modules/eth-gateway-setup/src/main/scala/io/leisuremeta/chain/gateway/eth/setup/EthGatewaySetupMain.scala",
    "content": "package io.leisuremeta.chain.gateway.eth.setup\n\nimport scala.jdk.CollectionConverters.*\n//import scala.jdk.FutureConverters.*\n\nimport cats.effect.{Async, ExitCode, IO, IOApp, Resource}\nimport cats.syntax.flatMap.*\nimport cats.syntax.functor.*\n\nimport com.github.jasync.sql.db.{Connection, QueryResult}\nimport com.github.jasync.sql.db.mysql.MySQLConnectionBuilder\n\nimport scodec.bits.ByteVector\n\n//import software.amazon.awssdk.auth.credentials.{\n//  AwsCredentials,\n//  StaticCredentialsProvider,\n//}\nimport software.amazon.awssdk.core.SdkBytes\n//import software.amazon.awssdk.regions.Region\nimport software.amazon.awssdk.services.kms.KmsAsyncClient\nimport software.amazon.awssdk.services.kms.model.{\n  DecryptRequest,\n//  GenerateDataKeyRequest,\n}\n//import software.amazon.awssdk.services.kms.model.DataKeySpec\nimport software.amazon.awssdk.services.kms.model.EncryptRequest\n//import software.amazon.awssdk.services.kms.model.EncryptionAlgorithmSpec\n\nobject EthGatewaySetupMain extends IOApp:\n\n  def connectDatabase[F[_]: Async](\n      host: String,\n      db: String,\n      table: String,\n      user: String,\n      password: String,\n  ): Resource[F, Connection] =\n    Resource\n      .make:\n        Async[F].blocking:\n          MySQLConnectionBuilder.createConnectionPool:\n            s\"jdbc:mysql://${host}:3306/${db}?user=${user}&password=${password}\"\n      .apply: connection =>\n        Async[F]\n          .fromCompletableFuture:\n            Async[F].delay(connection.disconnect())\n          .map(_ => ())\n\n  def connectKms[F[_]: Async]: Resource[F, KmsAsyncClient] =\n    Resource.fromAutoCloseable(Async[F].delay(KmsAsyncClient.create()))\n\n  def encrypt[F[_]: Async](kmsClient: KmsAsyncClient, alias: String)(\n      data: Array[Byte],\n  ): F[Array[Byte]] =\n    Async[F]\n      .fromCompletableFuture:\n        Async[F].delay:\n          kmsClient.encrypt:\n            EncryptRequest\n              .builder()\n              .keyId(s\"alias/${alias}\")\n              .plaintext(SdkBytes.fromByteArray(data))\n              .build()\n      .map(_.ciphertextBlob().asByteArrayUnsafe())\n\n  def decrypt[F[_]: Async](kmsClient: KmsAsyncClient, alias: String)(\n      data: Array[Byte],\n  ): F[Array[Byte]] =\n    Async[F]\n      .fromCompletableFuture:\n        Async[F].delay:\n          kmsClient.decrypt:\n            DecryptRequest\n              .builder()\n              .keyId(s\"alias/${alias}\")\n              .ciphertextBlob(SdkBytes.fromByteArray(data))\n              .build()\n      .map(_.plaintext().asByteArrayUnsafe())\n\n  def encryptDepositSide[F[_]: Async](\n      kmsClient: KmsAsyncClient,\n      config: EthGatewaySetupConfig,\n  )(data: Array[Byte]): F[Array[Byte]] =\n    for\n      cipherText1 <- encrypt[F](kmsClient, config.depositKmsAlias)(data)\n      cipherText2 <- encrypt[F](kmsClient, config.withdrawKmsAlias)(cipherText1)\n    yield cipherText2\n\n  def encryptWithdrawSide[F[_]: Async](\n      kmsClient: KmsAsyncClient,\n      config: EthGatewaySetupConfig,\n  )(data: Array[Byte]): F[Array[Byte]] =\n    for\n      cipherText1 <- encrypt[F](kmsClient, config.withdrawKmsAlias)(data)\n      cipherText2 <- encrypt[F](kmsClient, config.depositKmsAlias)(cipherText1)\n    yield cipherText2\n\n  def divide(cipherText: Array[Byte]): (String, String) =\n    val bytes         = ByteVector(cipherText)\n    val (front, back) = bytes.splitAt(bytes.size / 2)\n    (front.toBase64, back.toBase64)\n\n  def saveToDatabase[F[_]: Async](conn: Connection)(\n      tableName: String,\n      valueColumn: String,\n      key: String,\n      value: String,\n  ): F[Unit] = Async[F]\n    .fromCompletableFuture:\n      Async[F].delay:\n        conn.sendPreparedStatement(\n          s\"INSERT INTO ${tableName} (GTWY_SE_CODE, ${valueColumn}) VALUES (?, ?) ON DUPLICATE KEY UPDATE GTWY_SE_CODE = ?\",\n          List(key, value, value).asJava,\n        )\n    .map: (queryResult: QueryResult) =>\n      scribe.info(s\"Query result: ${queryResult}\")\n      ()\n\n  def encryptAndDivide[F[_]: Async](encrypt: Array[Byte] => F[Array[Byte]])(\n      plainText: Array[Byte],\n  ): F[(String, String)] =\n    for\n      cipherText <- encrypt(plainText)\n      (front, back) = divide(cipherText)\n    yield (front, back)\n\n  def saveFrontAndBack[F[_]: Async](\n    frontDb: Connection,\n    frontTable: String,\n    frontValueColumn: String,\n    backDb: Connection,\n    backTable: String,\n    backValueColumn: String,\n    frontAndBack: Map[String, (String, String)],\n    key: String,\n  ): F[Unit] =\n    for\n      _ <- saveToDatabase[F](frontDb)(frontTable, key, frontValueColumn, frontAndBack(key)._1)\n      _ <- saveToDatabase[F](backDb)(backTable, key, backValueColumn, frontAndBack(key)._2)\n    yield ()\n\n  def encryptAndSave[F[_]: Async](encrypt: Array[Byte] => F[Array[Byte]])(\n    frontDb: Connection,\n    frontTable: String,\n    frontValueColumn: String,\n    backDb: Connection,\n    backTable: String,\n    backValueColumn: String,\n    key: String,\n    plainText: Array[Byte],\n  ): F[Unit] =\n    for\n      cipherText <- encrypt(plainText)\n      (front, back) = divide(cipherText)\n      _ <- saveToDatabase[F](frontDb)(frontTable, key, frontValueColumn, front)\n      _ <- saveToDatabase[F](backDb)(backTable, key, backValueColumn, back)\n    yield ()\n\n  def hexToByteArray(hex: String): Array[Byte] =\n    ByteVector.fromValidHex(hex).toArrayUnsafe\n\n  def allEncryptAndDivide[F[_]: Async](\n    config: EthGatewaySetupConfig,\n    kmsClient: KmsAsyncClient,\n  ): F[Map[String, (String, String)]] = for\n    lmd <- encryptAndDivide[F](encryptDepositSide[F](kmsClient, config))(\n      hexToByteArray(config.lmPrivate),\n    )\n    lm <- encryptAndDivide[F](encryptWithdrawSide[F](kmsClient, config))(\n      hexToByteArray(config.lmPrivate),\n    )\n    eth <- encryptAndDivide[F](encryptWithdrawSide[F](kmsClient, config))(\n      hexToByteArray(config.ethPrivate),\n    )\n  yield Map(\"LM-D\" -> lmd, \"LM\" -> lm, \"ETH\" -> eth)\n\n  def allSaveFrontAndBack[F[_]: Async](\n    config: EthGatewaySetupConfig,\n    depositDb: Connection,\n    withdrawDb: Connection,\n    frontAndBack: Map[String, (String, String)],\n  ): F[Unit] = for\n    _ <- saveFrontAndBack[F](\n      depositDb,\n      config.depositDb.table,\n      config.depositDb.valueColumn,\n      withdrawDb,\n      config.withdrawDb.table,\n      config.withdrawDb.valueColumn,\n      frontAndBack,\n      \"LM-D\",\n    )\n    _ <- saveFrontAndBack[F](\n      withdrawDb,\n      config.withdrawDb.table,\n      config.withdrawDb.valueColumn,\n      depositDb,\n      config.depositDb.table,\n      config.depositDb.valueColumn,\n      frontAndBack,\n      \"LM\",\n    )\n    _ <- saveFrontAndBack[F](\n      withdrawDb,\n      config.withdrawDb.table,\n      config.withdrawDb.valueColumn,\n      depositDb,\n      config.depositDb.table,\n      config.depositDb.valueColumn,\n      frontAndBack,\n      \"ETH\",\n    )\n  yield ()\n\n  def run(args: List[String]): IO[ExitCode] =\n\n//    val config = EthGatewaySetupConfig()\n//\n//    val resources = for\n//      kmsClient <- connectKms[IO]\n//      depositDb <- connectDatabase[IO](\n//        config.depositDb.host,\n//        config.depositDb.db,\n//        config.depositDb.table,\n//        config.dbWriteAccount.user,\n//        config.dbWriteAccount.password,\n//      )\n//      withdrawDb <- connectDatabase[IO](\n//        config.withdrawDb.host,\n//        config.withdrawDb.db,\n//        config.withdrawDb.table,\n//        config.dbWriteAccount.user,\n//        config.dbWriteAccount.password,\n//      )\n//    yield (kmsClient, depositDb, withdrawDb)\n//\n//    connectKms[IO].use: kmsClient =>\n//      def dbEndpoint(conf: EthGatewaySetupConfig.DbConfig): Array[Byte] =\n//        s\"jdbc:mysql://${conf.host}:${conf.port}/${conf.db}?user=${conf.user}&password=${conf.password}\".getBytes(\"UTF-8\")\n//\n//      val depositEndpoint = dbEndpoint(config.depositDb)\n//      val withdrawEndpoint = dbEndpoint(config.withdrawDb)\n//\n//      def toBase64(bytes: Array[Byte]): String =\n//        ByteVector.view(bytes).toBase64\n//\n//      for\n//        encryptedDepositDb <- encrypt[IO](kmsClient, config.depositKmsAlias)(depositEndpoint)\n//        decryptedDepositDb <- decrypt[IO](kmsClient, config.depositKmsAlias)(encryptedDepositDb)\n//        encryptedWithdrawDb <- encrypt[IO](kmsClient, config.withdrawKmsAlias)(withdrawEndpoint)\n//        decryptedWithdrawDb <- decrypt[IO](kmsClient, config.withdrawKmsAlias)(encryptedWithdrawDb)\n//        encryptedEthEndpointWithDepositKey <- encrypt[IO](kmsClient, config.depositKmsAlias):\n//          config.ethEndpoint.getBytes(\"UTF-8\")\n//        encryptedEthEndpointWithWithdrawKey <- encrypt[IO](kmsClient, config.withdrawKmsAlias):\n//          config.ethEndpoint.getBytes(\"UTF-8\")\n//        decryptedEthEndpointWithDepositKey <- decrypt[IO](kmsClient, config.depositKmsAlias):\n//          encryptedEthEndpointWithDepositKey\n//        decryptedEthEndpointWithWithdrawKey <- decrypt[IO](kmsClient, config.withdrawKmsAlias):\n//          encryptedEthEndpointWithWithdrawKey\n//      yield\n//        println(s\"Deposit DB: ${toBase64(encryptedDepositDb)}\")\n//        println(s\"Decrypted Deposit DB: ${String(decryptedDepositDb, \"UTF-8\")}\")\n//        println(s\"Withdraw DB: ${toBase64(encryptedWithdrawDb)}\")\n//        println(s\"Decrypted Withdraw DB: ${String(decryptedWithdrawDb, \"UTF-8\")}\")\n//        println(s\"ETH Endpoint with Deposit Key: ${toBase64(encryptedEthEndpointWithDepositKey)}\")\n//        println(s\"ETH Endpoint with Withdraw Key: ${toBase64(encryptedEthEndpointWithWithdrawKey)}\")\n//        println(s\"Decrypted ETH Endpoint with Deposit Key: ${String(decryptedEthEndpointWithDepositKey, \"UTF-8\")}\")\n//        println(s\"Decrypted ETH Endpoint with Withdraw Key: ${String(decryptedEthEndpointWithWithdrawKey, \"UTF-8\")}\")\n//        ExitCode.Success\n\n//    resources.use: (kmsClient, depositDb, withdrawDb) =>\n//      allEncryptAndDivide[IO](config, kmsClient)\n//        .map: (keys: Map[String, (String, String)]) =>\n//          keys.foreach:\n//            case (key, (front, back)) =>\n//              println(s\"\"\"\"${key}\" -> (\"${front}\", \"${back}\")\"\"\")\n//              ()\n//        .as(ExitCode.Success)\n\n//      val keys = Map(\n//        \"LM-D\" -> (\"\", \"\"),\n//        \"LM\" -> (\"\", \"\"),\n//        \"ETH\" -> (\"\", \"\"),\n//      )\n//\n//      allSaveFrontAndBack[IO](config, depositDb, withdrawDb, keys)\n//        .as(ExitCode.Success)\n\n    val config = EthGatewaySetupSimpleConfig()\n    extension (ba: Array[Byte])\n      def toHex: String = ByteVector.view(ba).toHex\n      def toBase64: String = ByteVector.view(ba).toBase64\n\n    connectKms[IO].use: kmsClient =>\n      val Right(ethPrivate) = ByteVector.fromHexDescriptive(config.ethPrivate): @unchecked\n      val Right(lmPrivate) = ByteVector.fromHexDescriptive(config.lmPrivate): @unchecked\n      for\n        encryptedEthEndpoint <- encrypt[IO](kmsClient, config.kmsAlias)(config.ethEndpoint.getBytes(\"UTF-8\"))\n        decryptedEthEndpoint <- decrypt[IO](kmsClient, config.kmsAlias)(encryptedEthEndpoint)\n        encryptedEthPrivate <- encrypt[IO](kmsClient, config.kmsAlias)(ethPrivate.toArrayUnsafe)\n        decryptedEthPrivate <- decrypt[IO](kmsClient, config.kmsAlias)(encryptedEthPrivate)\n        encryptedLmPrivate <- encrypt[IO](kmsClient, config.kmsAlias)(lmPrivate.toArrayUnsafe)\n        decryptedLmPrivate <- decrypt[IO](kmsClient, config.kmsAlias)(encryptedLmPrivate)\n      yield\n        println(s\"Encrypted Eth Endpoint: ${encryptedEthEndpoint.toBase64}\")\n        println(s\"Decrypted Eth Endpoint: ${new String(decryptedEthEndpoint, \"UTF-8\")}\")\n        println(s\"Encrypted Eth Private: ${encryptedEthPrivate.toBase64}\")\n        println(s\"Decrypted Eth Private: ${decryptedEthPrivate.toHex}\")\n        println(s\"Encrypted LM Private: ${encryptedLmPrivate.toBase64}\")\n        println(s\"Decrypted LM Private: ${decryptedLmPrivate.toHex}\")\n        ExitCode.Success\n"
  },
  {
    "path": "modules/eth-gateway-setup/src/main/scala/io/leisuremeta/chain/gateway/eth/setup/EthGatewaySetupSimpleConfig.scala",
    "content": "package io.leisuremeta.chain.gateway.eth.setup\n\nimport pureconfig.*\nimport pureconfig.generic.derivation.default.*\n\nimport EthGatewaySetupConfig.*\n\nfinal case class EthGatewaySetupSimpleConfig(\n    ethPrivate: String,\n    lmPrivate: String,\n    kmsAlias: String,\n    ethEndpoint: String,\n) derives ConfigReader\n\nobject EthGatewaySetupSimpleConfig:\n\n  def apply(): EthGatewaySetupSimpleConfig =\n    ConfigSource.default.loadOrThrow[EthGatewaySetupSimpleConfig]\n\n  final case class DbConfig(\n      host: String,\n      port: Int,\n      db: String,\n      table: String,\n      valueColumn: String,\n      user: String,\n      password: String,\n  )\n\n  final case class DbWriteAccountConfig(\n      user: String,\n      password: String,\n  )\n"
  },
  {
    "path": "modules/eth-gateway-withdraw/src/main/resources/application.conf.sample",
    "content": "//eth-chain-id = 5777\n//eth-contract-address = \"0x02886136510172E313932EA66FE7bDC4d4C2fcc4\"\n//gateway-eth-address = \"0x3E984778B16b5a0fE489Aac0b716078202400b6A\"\n//gateway-endpoint = \"http://127.0.0.1:8081\"\n//local-server-port = 8082\n//lm-endpoint = \"http://127.0.0.1:8080\"\n//kms-alias = \"gateway_withdraw_key\"\n//encrypted-eth-endpoint = \"...\"\n//encrypted-database-endpoint = \"...\"\n//database-table-name = \"...\"\n//database-value-column = \"...\"\n\neth-chain-id = 11155111\neth-lm-contract-address = \"0x02886136510172E313932EA66FE7bDC4d4C2fcc4\"\neth-multisig-contract-address = \"0x02886136510172E313932EA66FE7bDC4d4C2fcc4\"\ngateway-eth-address = \"0x3E984778B16b5a0fE489Aac0b716078202400b6A\"\ndeposit-exempts = [\n  \"0x3E984778B16b5a0fE489Aac0b716078202400b6A\",\n]\nlm-endpoint = \"http://127.0.0.1:8080\"\nkms-alias = \"gateway_withdraw_key\"\nencrypted-eth-endpoint = \"...\"\nencrypted-eth-private = \"...\"\nencrypted-lm-private = \"...\"\ntarget-gateway = \"eth-gateway\"\n"
  },
  {
    "path": "modules/eth-gateway-withdraw/src/main/scala/io/leisuremeta/chain/gateway/eth/EthGatewayWithdrawMain.scala",
    "content": "package io.leisuremeta.chain\npackage gateway.eth\n\nimport java.math.{BigInteger, MathContext}\nimport java.nio.file.{Files, Paths, StandardOpenOption}\nimport java.time.Instant\nimport java.util.{ArrayList, Collections, Locale}\n//import java.util.concurrent.CompletableFuture\n\nimport scala.collection.mutable.ArrayBuffer\nimport scala.concurrent.duration.*\nimport scala.jdk.CollectionConverters.*\n//import scala.jdk.OptionConverters.*\nimport scala.util.Try\n\nimport cats.data.EitherT\nimport cats.effect.{Async, Clock, ExitCode, IO, IOApp, Resource}\nimport cats.syntax.apply.*\nimport cats.syntax.applicativeError.*\nimport cats.syntax.either.*\nimport cats.syntax.eq.*\nimport cats.syntax.flatMap.*\nimport cats.syntax.functor.*\nimport cats.syntax.traverse.*\n\n//import com.github.jasync.sql.db.mysql.MySQLConnectionBuilder\n//import com.typesafe.config.{Config, ConfigFactory}\nimport io.circe.Encoder\nimport io.circe.generic.auto.*\nimport io.circe.parser.decode\nimport io.circe.syntax.given\n//import okhttp3.OkHttpClient\n//import okhttp3.logging.HttpLoggingInterceptor\nimport org.web3j.abi.{FunctionEncoder, TypeReference}\nimport org.web3j.abi.datatypes.{Address, Function, Type}\nimport org.web3j.abi.datatypes.generated.Uint256\nimport org.web3j.crypto.{Credentials, RawTransaction}\nimport org.web3j.protocol.Web3j\nimport org.web3j.protocol.core.{\n  DefaultBlockParameter,\n  DefaultBlockParameterName,\n  Request as Web3jRequest,\n  Response as Web3jResponse,\n}\n//import org.web3j.protocol.core.methods.response.EthFeeHistory.FeeHistory\nimport org.web3j.protocol.core.methods.response.TransactionReceipt\nimport org.web3j.protocol.exceptions.TransactionException\nimport org.web3j.tx.RawTransactionManager\nimport org.web3j.tx.response.PollingTransactionReceiptProcessor\nimport scodec.bits.ByteVector\n//import software.amazon.awssdk.auth.credentials.{\n//  AwsCredentials,\n//  StaticCredentialsProvider,\n//}\n//import software.amazon.awssdk.core.SdkBytes\n//import software.amazon.awssdk.regions.Region\n//import software.amazon.awssdk.services.kms.KmsAsyncClient\n//import software.amazon.awssdk.services.kms.model.DecryptRequest\nimport sttp.client3.*\nimport sttp.model.{MediaType, StatusCode}\n\nimport lib.crypto.{CryptoOps, Hash}\nimport lib.crypto.Hash.ops.*\nimport lib.crypto.Sign.ops.*\nimport lib.datatype.*\nimport api.model.*\nimport api.model.TransactionWithResult.ops.*\nimport api.model.api_model.{AccountInfo, BalanceInfo, NftBalanceInfo}\nimport api.model.token.*\n\nimport common.*\nimport common.client.*\nimport org.web3j.abi.FunctionReturnDecoder\nimport java.nio.charset.StandardCharsets\n\nobject EthGatewayWithdrawMain extends IOApp:\n\n  def submitTx[F[_]: Async: GatewayKmsClient](\n      sttp: SttpBackend[F, Any],\n      lmAddress: String,\n      account: Account,\n      tx: Transaction,\n      encryptedLmPrivateBase64: String,\n  ): F[Unit] = GatewayDecryptService\n    .getSimplifiedPlainTextResource[F](encryptedLmPrivateBase64)\n    .value\n    .flatMap:\n      case Left(msg) =>\n        scribe.error(s\"Failed to get LM private: $msg\")\n        Async[F].sleep(10.seconds) *> submitTx[F](\n          sttp,\n          lmAddress,\n          account,\n          tx,\n          encryptedLmPrivateBase64,\n        )\n      case Right(lmPrivateResource) =>\n        lmPrivateResource.use: lmPrivateArray =>\n          val keyPair    = CryptoOps.fromPrivate(BigInt(lmPrivateArray))\n          val Right(sig) = keyPair.sign(tx): @unchecked\n          val signedTxs  = Seq(Signed(AccountSignature(sig, account), tx))\n\n          scribe.info(s\"Sending signed transactions: $signedTxs\")\n\n          given bodyJsonSerializer[A: Encoder]: BodySerializer[A] =\n            (a: A) =>\n              val serialized = a.asJson.noSpaces\n              StringBody(serialized, \"UTF-8\", MediaType.ApplicationJson)\n\n          basicRequest\n            .response(asStringAlways)\n            .post(uri\"http://$lmAddress/tx\")\n            .body(signedTxs)\n            .send(sttp)\n            .map: response =>\n              scribe.info(s\"Response: $response\")\n\n  def initialLmSecretCheck[F[_]: Async: GatewayKmsClient](\n      sttp: SttpBackend[F, Any],\n      web3j: Web3j,\n      conf: GatewaySimpleConf,\n  ): EitherT[F, String, Unit] = for\n    gatewayInfoResponse <- EitherT.liftF:\n      basicRequest\n        .response(asStringAlways)\n        .get(uri\"http://${conf.lmEndpoint}/account/${conf.targetGateway}\")\n        .send(sttp)\n    gatewayAccountInfo <- EitherT.fromEither[F]:\n      decode[AccountInfo](gatewayInfoResponse.body).leftMap(_.getMessage())\n    lmSecretResource <- GatewayDecryptService\n      .getSimplifiedPlainTextResource[F](conf.encryptedLmPrivate)\n    pks <- EitherT.liftF:\n      lmSecretResource.use: lmSecretArray =>\n        Async[F].delay:\n          val keyPair = CryptoOps.fromPrivate(BigInt(1, lmSecretArray))\n          PublicKeySummary.fromPublicKeyHash(keyPair.publicKey.toHash)\n    _ <- EitherT.cond[F](\n      gatewayAccountInfo.publicKeySummaries.contains(pks),\n      (),\n      s\"Fail to check lm secret. ${conf.targetGateway} does not have pks ${pks}\",\n    )\n  yield ()\n\n  def checkLoop[F[_]: Async: GatewayKmsClient](\n      sttp: SttpBackend[F, Any],\n      web3j: Web3j,\n      conf: GatewaySimpleConf,\n  ): F[Unit] =\n    def run: F[Unit] = for\n      _ <- Async[F].delay(scribe.info(s\"Withdrawal check started\"))\n      _ <- checkLmWithdrawal[F](\n        sttp,\n        web3j,\n        conf,\n      )\n      _ <- Async[F].delay(scribe.info(s\"Withdrawal check finished\"))\n    yield ()\n\n    def loop: F[Unit] = for\n      _ <- run.orElse(Async[F].unit)\n      _ <- Async[F].sleep(10000.millis)\n      _ <- loop\n    yield ()\n\n    loop\n\n  def getFungibleBalance[F[_]: Async](\n      sttp: SttpBackend[F, Any],\n      lmEndpoint: String,\n      targetGateway: String,\n  ): F[Map[TokenDefinitionId, BalanceInfo]] = basicRequest\n    .response(asStringAlways)\n    .get(uri\"http://$lmEndpoint/balance/$targetGateway?movable=free\")\n    .send(sttp)\n    .map: response =>\n      if response.code.isSuccess then\n        decode[Map[TokenDefinitionId, BalanceInfo]](response.body) match\n          case Right(balanceInfoMap) => balanceInfoMap\n          case Left(error) =>\n            scribe.error(s\"Error decoding balance info: $error\")\n            scribe.error(s\"response: ${response.body}\")\n            Map.empty\n      else if response.code.code === StatusCode.NotFound.code then\n        scribe.info(\n          s\"balance of account $targetGateway not found: ${response.body}\",\n        )\n        Map.empty\n      else\n        scribe.error(s\"Error getting balance: ${response.body}\")\n        Map.empty\n\n  def getNftBalance[F[_]: Async](\n      sttp: SttpBackend[F, Any],\n      lmEndpoint: String,\n      targetGateway: String,\n  ): F[Map[TokenId, NftBalanceInfo]] = basicRequest\n    .response(asStringAlways)\n    .get(uri\"http://$lmEndpoint/nft-balance/$targetGateway?movable=free\")\n    .send(sttp)\n    .map: response =>\n      if response.code.isSuccess then\n        decode[Map[TokenId, NftBalanceInfo]](response.body) match\n          case Right(balanceInfoMap) => balanceInfoMap\n          case Left(error) =>\n            scribe.error(s\"Error decoding nft-balance info: $error\")\n            scribe.error(s\"response: ${response.body}\")\n            Map.empty\n      else if response.code.code === StatusCode.NotFound.code then\n        scribe.info(\n          s\"nft-balance of account $targetGateway not found: ${response.body}\",\n        )\n        Map.empty\n      else\n        scribe.error(s\"Error getting nft-balance: ${response.body}\")\n        Map.empty\n\n  def getAccountInfo[F[_]: Async](\n      sttp: SttpBackend[F, Any],\n      lmEndpoint: String,\n      account: Account,\n  ): F[Option[AccountInfo]] = basicRequest\n    .response(asStringAlways)\n    .get(uri\"http://$lmEndpoint/account/${account.utf8.value}\")\n    .send(sttp)\n    .map: response =>\n      if response.code.isSuccess then\n        decode[AccountInfo](response.body) match\n          case Right(accountInfo) => Some(accountInfo)\n          case Left(error) =>\n            scribe.error(s\"Error decoding account info: $error\")\n            None\n      else if response.code.code === StatusCode.NotFound.code then\n        scribe.info(s\"account info not found: ${response.body}\")\n        None\n      else\n        scribe.error(s\"Error getting account info: ${response.body}\")\n        None\n\n  def checkLmWithdrawal[F[_]: Async: Clock: GatewayKmsClient](\n      sttp: SttpBackend[F, Any],\n      web3j: Web3j,\n      conf: GatewaySimpleConf,\n  ): F[Unit] = getFungibleBalance(sttp, conf.lmEndpoint, conf.targetGateway)\n    .flatMap { (balanceMap: Map[TokenDefinitionId, BalanceInfo]) =>\n\n      val gatewayAccount = Account(Utf8.unsafeFrom(conf.targetGateway))\n      val LM             = TokenDefinitionId(Utf8.unsafeFrom(\"LM\"))\n\n      balanceMap\n        .get(LM)\n        .toList\n        .flatMap(_.unused.toSeq)\n        .filterNot(_._2.signedTx.sig.account === gatewayAccount)\n        .traverse { case (txHash, txWithResult) =>\n          txWithResult.signedTx.value match\n            case tx: Transaction.TokenTx.TransferFungibleToken =>\n              {\n                scribe.info:\n                  s\"Try to handle ${txWithResult.signedTx.sig.account}'s tx: $tx\"\n                for\n                  amount <- EitherT.fromOption[F](\n                    tx.outputs.get(gatewayAccount),\n                    s\"No output amount to send to gateway\",\n                  )\n                  accountInfo <- EitherT.fromOptionF(\n                    getAccountInfo(\n                      sttp,\n                      conf.lmEndpoint,\n                      txWithResult.signedTx.sig.account,\n                    ),\n                    s\"No account info of ${txWithResult.signedTx.sig.account}\",\n                  )\n                  ethAddress <- EitherT.fromOption[F](\n                    accountInfo.ethAddress,\n                    s\"No eth address of ${txWithResult.signedTx.sig.account}\",\n                  )\n                  _ <- EitherT.liftF:\n                    requestEthLmMultisigTransfer(\n                      web3j = web3j,\n                      ethChainId = conf.ethChainId,\n                      multiSigContractAddress = conf.ethMultisigContractAddress,\n                      encryptedEthPrivate = conf.encryptedEthPrivate,\n                      gatewayEthAddress = conf.gatewayEthAddress,\n                      txId = txHash,\n                      receiverEthAddress = ethAddress.utf8.value,\n                      amount = amount,\n                    )\n                  now <- EitherT.liftF(Clock[F].realTimeInstant)\n                  tx1 = Transaction.TokenTx.TransferFungibleToken(\n                    networkId = NetworkId(BigNat.unsafeFromLong(1000L)),\n                    createdAt = now,\n                    tokenDefinitionId = LM,\n                    inputs = Set(txHash.toSignedTxHash),\n                    outputs = Map(gatewayAccount -> amount),\n                    memo = Some(Utf8.unsafeFrom {\n                      s\"After withdrawing of ${txWithResult.signedTx.sig.account}'s $amount\"\n                    }),\n                  )\n                  _ <- EitherT.liftF:\n                    submitTx(\n                      sttp,\n                      conf.lmEndpoint,\n                      gatewayAccount,\n                      tx1,\n                      conf.encryptedLmPrivate,\n                    )\n                yield ()\n              }.leftMap { msg =>\n                scribe.error(msg)\n                msg\n              }.value\n            case _ => Async[F].delay(().asRight[String])\n        }\n    }\n    .as(())\n\n//  def checkNftWithdrawal[F[_]\n//    : Async: Clock: GatewayApiClient: GatewayDatabaseClient: GatewayKmsClient](\n//      sttp: SttpBackend[F, Any],\n//      web3j: Web3j,\n//      conf: GatewayConf,\n//  ): F[Unit] = getNftBalance(sttp, conf.lmEndpoint, conf.targetGateway)\n//    .flatMap { (balanceMap: Map[TokenId, NftBalanceInfo]) =>\n//\n//      val gatewayAccount = Account(Utf8.unsafeFrom(conf.targetGateway))\n//\n//      balanceMap.toSeq.traverse { case (tokenId, balanceInfo) =>\n//        balanceInfo.tx.signedTx.value match\n//          case tx: Transaction.TokenTx.TransferNFT\n//              if balanceInfo.tx.signedTx.sig.account =!= gatewayAccount =>\n//            {\n//              for\n//                info <- OptionT:\n//                  getAccountInfo(\n//                    sttp,\n//                    conf.lmEndpoint,\n//                    balanceInfo.tx.signedTx.sig.account,\n//                  )\n//                ethAddress <- OptionT.fromOption[F](info.ethAddress)\n//                _ <- OptionT.liftF:\n//                  mintEthNft[F](\n//                    web3j = web3j,\n//                    ethChainId = conf.ethChainId,\n//                    ethNftContract = conf.ethContractAddress,\n//                    gatewayEthAddress = conf.gatewayEthAddress,\n//                    receiverEthAddress = ethAddress.utf8.value,\n//                    tokenId = tokenId,\n//                  )\n//                now <- OptionT.liftF(Clock[F].realTimeInstant)\n//                tx1 = tx.copy(\n//                  createdAt = now,\n//                  input = balanceInfo.tx.signedTx.toHash,\n//                  output = gatewayAccount,\n//                  memo =\n//                    Some(Utf8.unsafeFrom(\"gateway balance after withdrawal\")),\n//                )\n//                _ <- OptionT.liftF:\n//                  submitTx[F](sttp, conf.lmEndpoint, gatewayAccount, tx1)\n//              yield ()\n//            }.value\n//          case _ => Async[F].delay(None)\n//      }\n//    }\n//    .as(())\n\n//  def transferEthLM[F[_]\n//    : Async: GatewayApiClient: GatewayDatabaseClient: GatewayKmsClient](\n//      web3j: Web3j,\n//      ethChainId: Int,\n//      ethLmContract: String,\n//      gatewayEthAddress: String,\n//      receiverEthAddress: String,\n//      amount: BigNat,\n//  ): F[Unit] =\n//\n//    scribe.info(s\"Transfer eth LM to ${receiverEthAddress}\")\n//\n//    val mintParams = new ArrayList[Type[?]]()\n//    mintParams.add(new Address(receiverEthAddress))\n//    mintParams.add(new Uint256(amount.toBigInt.bigInteger))\n//\n//    val returnTypes = Collections.emptyList[TypeReference[?]]()\n//\n//    val transferTxData = FunctionEncoder.encode:\n//      new Function(\"transfer\", mintParams, returnTypes)\n//\n//    sendEthTransaction[F](\n//      web3j = web3j,\n//      ethChainId = ethChainId,\n//      contractAddress = ethLmContract,\n//      txData = transferTxData,\n//      gatewayEthAddress = gatewayEthAddress,\n//    ).as(())\n\n  def requestEthLmMultisigTransfer[F[_]: Async: GatewayKmsClient](\n      web3j: Web3j,\n      ethChainId: Int,\n      gatewayEthAddress: String,\n      multiSigContractAddress: String,\n      encryptedEthPrivate: String,\n      txId: Hash.Value[TransactionWithResult],\n      receiverEthAddress: String,\n      amount: BigNat,\n  ): F[Unit] =\n\n    scribe.info(s\"Transfer eth LM to ${receiverEthAddress}\")\n\n    val mintParams = new ArrayList[Type[?]]()\n    mintParams.add(new Uint256(txId.toUInt256Bytes.toBigInt.bigInteger))\n    mintParams.add(new Address(receiverEthAddress))\n    mintParams.add(new Uint256(amount.toBigInt.bigInteger))\n\n    val returnTypes = Collections.emptyList[TypeReference[?]]()\n\n    val transferTxData = FunctionEncoder.encode:\n      new Function(\"addTransaction\", mintParams, returnTypes)\n\n    sendEthTransaction[F](\n      web3j = web3j,\n      ethChainId = ethChainId,\n      contractAddress = multiSigContractAddress,\n      txData = transferTxData,\n      gatewayEthAddress = gatewayEthAddress,\n      encryptedEthPrivate = encryptedEthPrivate,\n    ).as(())\n\n//  def mintEthNft[F[_]\n//    : Async: GatewayApiClient: GatewayDatabaseClient: GatewayKmsClient](\n//      web3j: Web3j,\n//      ethChainId: Int,\n//      ethNftContract: String,\n//      gatewayEthAddress: String,\n//      receiverEthAddress: String,\n//      tokenId: TokenId,\n//  ): F[Unit] =\n//\n//    val tokenIdBigInt = BigInt(tokenId.utf8.value)\n//\n//    val mintParams = new ArrayList[Type[?]]()\n//    mintParams.add(new Address(receiverEthAddress))\n//    mintParams.add(new Uint256(tokenIdBigInt.bigInteger))\n//\n//    val returnTypes = Collections.emptyList[TypeReference[?]]()\n//\n//    val mintTxData = FunctionEncoder.encode {\n//      new Function(\"safeMint\", mintParams, returnTypes)\n//    }\n//\n//    sendEthTransaction[F](\n//      web3j = web3j,\n//      ethChainId = ethChainId,\n//      contractAddress = ethNftContract,\n//      txData = mintTxData,\n//      gatewayEthAddress = gatewayEthAddress,\n//    ).as(())\n\n  def requestToF[F[_]: Async, A, B, C <: Web3jResponse[B], D](\n      request: Web3jRequest[A, C],\n  )(map: C => D): F[D] =\n    Async[F]\n      .recoverWith:\n        Async[F]\n          .fromCompletableFuture(Async[F].delay(request.sendAsync()))\n          .map(map)\n      .apply:\n        case t: Throwable =>\n          scribe.error(t)\n          Async[F].sleep(10.seconds) *> requestToF(request)(map)\n\n  def sendEthTransaction[F[_]: Async: GatewayKmsClient](\n      web3j: Web3j,\n      ethChainId: Int,\n      contractAddress: String,\n      txData: String,\n      gatewayEthAddress: String,\n      encryptedEthPrivate: String,\n  ): F[Unit] = GatewayDecryptService\n    .getSimplifiedPlainTextResource[F](encryptedEthPrivate)\n    .value\n    .flatMap:\n      case Left(msg) =>\n        scribe.error(s\"Fail to get eth private key: $msg\")\n        Async[F].sleep(10.seconds) *> sendEthTransaction[F](\n          web3j,\n          ethChainId,\n          contractAddress,\n          txData,\n          gatewayEthAddress,\n          encryptedEthPrivate,\n        )\n      case Right(ethResource) =>\n        ethResource.use: ethPrivateByteArray =>\n          val ethPrivate = ByteVector.view(ethPrivateByteArray).toHex\n          val credential = Credentials.create(ethPrivate)\n          assert(\n            credential.getAddress() === gatewayEthAddress\n              .toLowerCase(Locale.ENGLISH),\n            s\"invalid gateway eth address: ${credential.getAddress} vs $gatewayEthAddress\",\n          )\n          val TX_END_CHECK_DURATION = 20000\n          val TX_END_CHECK_RETRY    = 9\n          val receiptProcessor = new PollingTransactionReceiptProcessor(\n            web3j,\n            TX_END_CHECK_DURATION,\n            TX_END_CHECK_RETRY,\n          )\n          val manager =\n            new RawTransactionManager(\n              web3j,\n              credential,\n              ethChainId,\n              receiptProcessor,\n            )\n\n          val GAS_LIMIT = 600_000\n\n          def loop(\n              lastTrial: Option[(BigInteger, BigInteger, String)],\n          ): F[Unit] =\n\n            def getMaxPriorityFeePerGas(): F[BigInteger] =\n              requestToF(web3j.ethMaxPriorityFeePerGas()):\n                _.getMaxPriorityFeePerGas()\n\n            def getBaseFee(): F[BigInteger] =\n              val blockCount: String = BigInt(9).toString(16)\n              val newestBlock: DefaultBlockParameter =\n                DefaultBlockParameterName.LATEST\n              val rewardPercentiles: java.util.List[java.lang.Double] =\n                ArrayBuffer[java.lang.Double](0, 0.5, 1, 1.5, 3, 80).asJava\n\n              requestToF {\n                web3j.ethFeeHistory(blockCount, newestBlock, rewardPercentiles)\n              }.apply: response =>\n                val history = response.getFeeHistory()\n\n                val baseFees = history.getBaseFeePerGas().asScala.toList\n\n                val mean = BigDecimal(baseFees.map(BigInt(_)).sum) / 10\n                val std = baseFees\n                  .map(x =>\n                    BigDecimal(\n                      (BigDecimal(x) - mean)\n                        .pow(2)\n                        .bigDecimal\n                        .sqrt(MathContext.DECIMAL32),\n                    ),\n                  )\n                  .sum / 10\n                val targetBaseFees = (mean + std + 0.5).toBigInt\n\n                targetBaseFees.bigInteger\n\n            def getNonce(): F[BigInteger] = requestToF {\n              web3j.ethGetTransactionCount(\n                gatewayEthAddress,\n                DefaultBlockParameterName.LATEST,\n              )\n            }(_.getTransactionCount())\n\n            def sendNewTx(\n                baseFee: BigInteger,\n                maxPriorityFeePerGas: BigInteger,\n            ): F[Option[String]] = for\n              nonce <- getNonce()\n              _     <- Async[F].delay { scribe.info(s\"Nonce: $nonce\") }\n              tx = RawTransaction.createTransaction(\n                ethChainId,\n                nonce,\n                BigInteger.valueOf(GAS_LIMIT),\n                contractAddress,\n                BigInteger.ZERO,\n                txData,\n                maxPriorityFeePerGas,\n                baseFee `add` maxPriorityFeePerGas,\n              )\n              txResponseOption <- Async[F]\n                .blocking { manager.signAndSend(tx) }\n                .map: resp =>\n                  if resp.hasError() then\n                    val e = resp.getError()\n                    scribe.info:\n                      s\"Error in sending tx: #(${e.getCode()}) ${e.getMessage()}\"\n                    writeFailLog(txData)\n                  else scribe.info(s\"Sending Eth Tx: ${resp.getResult()}\")\n                  Option(resp.getResult)\n            yield txResponseOption\n\n            def getReceipt(\n                txResponse: String,\n            ): F[Either[Throwable, TransactionReceipt]] =\n              Async[F].blocking:\n                Try(\n                  receiptProcessor.waitForTransactionReceipt(txResponse),\n                ).toEither\n\n            for\n              _ <- Async[F].delay { scribe.info(s\"Last trial: ${lastTrial}\") }\n              baseFee <- getBaseFee()\n              _ <- Async[F].delay { scribe.info(s\"New base fee: ${baseFee}\") }\n              maxPriorityFeePerGas <- getMaxPriorityFeePerGas()\n              _ <- Async[F].delay:\n                scribe.info(s\"Max Priority Fee Per Gas: $maxPriorityFeePerGas\")\n              txIdOption <- lastTrial match\n                case Some((oldBaseFee, oldPriorityFee, txId))\n                    if (oldBaseFee `add` oldPriorityFee).compareTo(\n                      baseFee `add` maxPriorityFeePerGas,\n                    ) >= 0 =>\n                  scribe.info(s\"New base fee is less than old one\")\n                  Async[F].pure(Some(txId))\n                case _ =>\n                  sendNewTx(baseFee, maxPriorityFeePerGas).map:\n                    _.orElse(lastTrial.map(_._3))\n              _ <- txIdOption match\n                case Some(txId) =>\n                  for\n                    receiptEither <- getReceipt(txId)\n                    _ <- receiptEither match\n                      case Left(e) =>\n                        e match\n                          case te: TransactionException =>\n                            scribe.info(s\"Timeout: ${te.getMessage()}\")\n                            Async[F].delay(())\n//                            loop(Some(baseFee, maxPriorityFeePerGas, txId))\n                          case _ =>\n                            scribe.error:\n                              s\"Fail to send transaction: ${e.getMessage()}\"\n                            Async[F].delay(())\n//                            loop(Some(baseFee, maxPriorityFeePerGas, txId))\n                      case Right(receipt) =>\n                        if receipt.isStatusOK() then\n                          Async[F].delay:\n                            scribe.info:\n                              s\"transaction ${receipt.getTransactionHash()} saved to block #${receipt.getBlockNumber()}\"\n                            // BigInt(receipt.getBlockNumber())\n                            ()\n                        else\n                          scribe.error:\n                            s\"transaction ${receipt.getTransactionHash()} failed with receipt:${receipt}\"\n                          Async[F].delay(())\n//                          loop(None)\n                  yield ()\n                case None =>\n//                  Async[F].sleep(1.minute) *> loop(None)\n                  Async[F].delay(())\n            yield ()\n\n          loop(None)\n\n  @SuppressWarnings(Array(\"org.wartremover.warts.MutableDataStructures\"))\n  def writeFailLog[F[_]: Async](txData: String): F[Unit] = Async[F].blocking:\n    val typeRefAddress: TypeReference[Type[?]] = (new TypeReference[Address]() {}).asInstanceOf[TypeReference[Type[?]]]\n    val typeRefUint256: TypeReference[Type[?]] = (new TypeReference[Uint256]() {}).asInstanceOf[TypeReference[Type[?]]]\n    val parseTx = FunctionReturnDecoder.decode(txData.drop(10), List(typeRefUint256, typeRefAddress, typeRefUint256).asJava).asScala\n    val path = Paths.get(\"failed_tx\")\n    val res = parseTx.map(e => e.getValue).mkString(\"\", \" \", \"\\n\")\n    val _ = Files.write(\n      path,\n      res.getBytes(StandardCharsets.UTF_8),\n      StandardOpenOption.CREATE,\n      StandardOpenOption.WRITE,\n      StandardOpenOption.APPEND,\n    )\n\n  def run(args: List[String]): IO[ExitCode] =\n    val conf = GatewaySimpleConf.loadOrThrow()\n    GatewayResource\n      .getSimpleResource[IO](conf)\n      .use: (kms, web3j, sttp) =>\n        given GatewayKmsClient[IO] = kms\n        initialLmSecretCheck[IO](sttp, web3j, conf) *> EitherT.liftF:\n          checkLoop[IO](\n            sttp = sttp,\n            web3j = web3j,\n            conf = conf,\n          )\n      .value\n      .map:\n        case Left(error)   => scribe.error(s\"Error: $error\")\n        case Right(result) => scribe.info(s\"Result: $result\")\n      .as(ExitCode.Success)\n"
  },
  {
    "path": "modules/jvm-client/src/main/scala/io/leisuremeta/chain/jvmclient/JvmClientMain.scala",
    "content": "package io.leisuremeta.chain\npackage jvmclient\n\nimport cats.effect.{ExitCode, IO, IOApp}\nimport cats.syntax.traverse.*\nimport com.typesafe.config.ConfigFactory\nimport io.circe.syntax.*\nimport scodec.bits.hex\nimport sttp.client3.*\nimport sttp.client3.armeria.cats.ArmeriaCatsBackend\n//import sttp.tapir.client.sttp.SttpClientInterpreter\n\n//import api.LeisureMetaChainApi\nimport api.model.*\nimport api.model.account.*\n//import api.model.reward.*\nimport api.model.token.*\n//import api.model.TransactionWithResult.ops.*\nimport lib.datatype.*\nimport lib.crypto.*\nimport lib.crypto.Hash.ops.*\nimport lib.crypto.Sign.ops.*\nimport node.NodeConfig\nimport scodec.bits.ByteVector\n\nobject JvmClientMain extends IOApp:\n\n  java.security.Security.addProvider(\n    new org.bouncycastle.jce.provider.BouncyCastleProvider(),\n  )\n\n  val aliceKey = CryptoOps.fromPrivate(\n    BigInt(\n      \"b229e76b742616db3ac2c5c2418f44063fcc5fcc52a08e05d4285bdb31acba06\",\n      16,\n    ),\n  )\n  val alicePKS = PublicKeySummary.fromPublicKeyHash(aliceKey.publicKey.toHash)\n  val alice    = Account(Utf8.unsafeFrom(\"alice\"))\n  val bob      = Account(Utf8.unsafeFrom(\"bob\"))\n  val carol    = Account(Utf8.unsafeFrom(\"carol\"))\n\n  def sign(account: Account, key: KeyPair)(tx: Transaction): Signed.Tx =\n    key.sign(tx).map { sig =>\n      Signed(AccountSignature(sig, account), tx)\n    } match\n      case Right(signedTx) => signedTx\n      case Left(msg)       => throw Exception(msg)\n\n  def signAlice = sign(alice, aliceKey)\n\n  val txs: IndexedSeq[Transaction] = IndexedSeq(\n    Transaction.AccountTx.CreateAccount(\n      networkId = NetworkId(BigNat.unsafeFromLong(2021L)),\n      createdAt = java.time.Instant.parse(\"2023-01-11T19:01:00.00Z\"),\n      account = alice,\n      ethAddress = None,\n      guardian = None,\n//      memo = None,\n    ),\n    Transaction.AccountTx.CreateAccountWithExternalChainAddresses(\n      networkId = NetworkId(BigNat.unsafeFromLong(2021L)),\n      createdAt = java.time.Instant.parse(\"2023-01-11T19:01:30.00Z\"),\n      account = bob,\n      externalChainAddresses = Map(\n        ExternalChain.ETH -> ExternalChainAddress(\n          Utf8.unsafeFrom(alicePKS.toBytes.toHex),\n        ),\n      ),\n      guardian = Some(alice),\n      memo = None,\n    ),\n    Transaction.AccountTx.UpdateAccountWithExternalChainAddresses(\n      networkId = NetworkId(BigNat.unsafeFromLong(2021L)),\n      createdAt = java.time.Instant.parse(\"2023-01-11T19:01:40.00Z\"),\n      account = bob,\n      externalChainAddresses = Map(\n        ExternalChain.ETH -> ExternalChainAddress(\n          Utf8.unsafeFrom(alicePKS.toBytes.toHex),\n        ),\n      ),\n      guardian = Some(alice),\n      memo = Some(Utf8.unsafeFrom(\"bob updated\")),\n    ),\n    Transaction.GroupTx.CreateGroup(\n      networkId = NetworkId(BigNat.unsafeFromLong(2021L)),\n      createdAt = java.time.Instant.parse(\"2023-01-11T19:02:00.00Z\"),\n      groupId = GroupId(Utf8.unsafeFrom(\"mint-group\")),\n      name = Utf8.unsafeFrom(\"Mint Group\"),\n      coordinator = alice,\n//      memo = None,\n    ),\n    Transaction.GroupTx.AddAccounts(\n      networkId = NetworkId(BigNat.unsafeFromLong(2021L)),\n      createdAt = java.time.Instant.parse(\"2023-01-11T19:03:00.00Z\"),\n      groupId = GroupId(Utf8.unsafeFrom(\"mint-group\")),\n      accounts = Set(alice),\n//      memo = None,\n    ),\n    Transaction.TokenTx.DefineToken(\n      networkId = NetworkId(BigNat.unsafeFromLong(2021L)),\n      createdAt = java.time.Instant.parse(\"2023-01-11T19:04:00.00Z\"),\n      definitionId = TokenDefinitionId(Utf8.unsafeFrom(\"LM\")),\n      name = Utf8.unsafeFrom(\"LeisureMeta\"),\n      symbol = Some(Utf8.unsafeFrom(\"LM\")),\n      minterGroup = Some(GroupId(Utf8.unsafeFrom(\"mint-group\"))),\n      nftInfo = None,\n    ),\n    Transaction.TokenTx.DefineTokenWithPrecision(\n      networkId = NetworkId(BigNat.unsafeFromLong(2021L)),\n      createdAt = java.time.Instant.parse(\"2023-01-11T19:05:00.00Z\"),\n      definitionId = TokenDefinitionId(Utf8.unsafeFrom(\"nft-with-precision\")),\n      name = Utf8.unsafeFrom(\"NFT with precision\"),\n      symbol = Some(Utf8.unsafeFrom(\"NFTWP\")),\n      minterGroup = Some(GroupId(Utf8.unsafeFrom(\"mint-group\"))),\n      nftInfo = Some(\n        NftInfoWithPrecision(\n          minter = alice,\n          rarity = Map(\n            Rarity(Utf8.unsafeFrom(\"LGDY\")) -> BigNat.unsafeFromLong(100),\n            Rarity(Utf8.unsafeFrom(\"UNIQ\")) -> BigNat.unsafeFromLong(66),\n            Rarity(Utf8.unsafeFrom(\"EPIC\")) -> BigNat.unsafeFromLong(33),\n            Rarity(Utf8.unsafeFrom(\"RARE\")) -> BigNat.unsafeFromLong(10),\n          ),\n          precision = BigNat.unsafeFromLong(2),\n          dataUrl = Utf8.unsafeFrom(\n            \"https://www.playnomm.com/data/nft-with-precision.json\",\n          ),\n          contentHash = UInt256\n            .from(\n              hex\"2475a387f22c248c5a3f09cea0ef624484431c1eaf8ffbbf98a4a27f43fabc84\",\n            )\n            .toOption\n            .get,\n        ),\n      ),\n//      memo = None,\n    ),\n    Transaction.TokenTx.MintNFT(\n      networkId = NetworkId(BigNat.unsafeFromLong(2021L)),\n      createdAt = java.time.Instant.parse(\"2023-01-11T19:06:00.00Z\"),\n      tokenDefinitionId =\n        TokenDefinitionId(Utf8.unsafeFrom(\"nft-with-precision\")),\n      tokenId = TokenId(Utf8.unsafeFrom(\"2022061710000513118\")),\n      rarity = Rarity(Utf8.unsafeFrom(\"EPIC\")),\n      dataUrl = Utf8.unsafeFrom(\n        \"https://d3j8b1jkcxmuqq.cloudfront.net/temp/collections/TEST_NOMM4/NFT_ITEM/F7A92FB1-B29F-4E6F-BEF1-47C6A1376D68.jpg\",\n      ),\n      contentHash = UInt256\n        .from(\n          hex\"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef\",\n        )\n        .toOption\n        .get,\n      output = alice,\n//      memo = None,\n    ),\n    Transaction.TokenTx.MintNFTWithMemo(\n      networkId = NetworkId(BigNat.unsafeFromLong(2021L)),\n      createdAt = java.time.Instant.parse(\"2023-01-11T19:07:00.00Z\"),\n      tokenDefinitionId =\n        TokenDefinitionId(Utf8.unsafeFrom(\"nft-with-precision\")),\n      tokenId = TokenId(Utf8.unsafeFrom(\"2022061710000513118\")),\n      rarity = Rarity(Utf8.unsafeFrom(\"EPIC\")),\n      dataUrl = Utf8.unsafeFrom(\n        \"https://d3j8b1jkcxmuqq.cloudfront.net/temp/collections/TEST_NOMM4/NFT_ITEM/F7A92FB1-B29F-4E6F-BEF1-47C6A1376D68.jpg\",\n      ),\n      contentHash = UInt256\n        .from(\n          hex\"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef\",\n        )\n        .toOption\n        .get,\n      output = alice,\n      memo = Some(Utf8.unsafeFrom(\"Test Minting NFT #2022061710000513118\")),\n    ),\n    Transaction.TokenTx.UpdateNFT(\n      networkId = NetworkId(BigNat.unsafeFromLong(2021L)),\n      createdAt = java.time.Instant.parse(\"2023-01-11T19:08:00.00Z\"),\n      tokenDefinitionId =\n        TokenDefinitionId(Utf8.unsafeFrom(\"nft-with-precision\")),\n      tokenId = TokenId(Utf8.unsafeFrom(\"2022061710000513118\")),\n      rarity = Rarity(Utf8.unsafeFrom(\"EPIC\")),\n      dataUrl = Utf8.unsafeFrom(\n        \"https://d3j8b1jkcxmuqq.cloudfront.net/temp/collections/TEST_NOMM4/NFT_ITEM/F7A92FB1-B29F-4E6F-BEF1-47C6A1376D68.jpg\",\n      ),\n      contentHash = UInt256\n        .from(\n          hex\"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef\",\n        )\n        .toOption\n        .get,\n      output = alice,\n      memo = Some(Utf8.unsafeFrom(\"Test Updating NFT #2022061710000513118\")),\n    ),\n    Transaction.TokenTx.CreateSnapshots(\n      networkId = NetworkId(BigNat.unsafeFromLong(2021L)),\n      createdAt = java.time.Instant.parse(\"2023-01-11T19:09:00.00Z\"),\n      definitionIds = Set(\n        TokenDefinitionId(Utf8.unsafeFrom(\"LM\")),\n        TokenDefinitionId(Utf8.unsafeFrom(\"nft-with-precision\")),\n      ),\n      memo = Some(Utf8.unsafeFrom(\"Snapshot for NFT\")),\n    ),\n  )\n\n  override def run(args: List[String]): IO[ExitCode] =\n    ArmeriaCatsBackend.resource[IO]().use { backend =>\n\n      val loadConfig = IO.blocking(ConfigFactory.load)\n\n      NodeConfig.load[IO](loadConfig).value.flatMap {\n        case Right(config) =>\n//          val baseUri = uri\"http://localhost:${config.local.port}\"\n//          val postTxClient = SttpClientInterpreter().toClient(\n//            LeisureMetaChainApi.postTxEndpoint,\n//            Some(baseUri),\n//            backend,\n//          )\n\n          txs.toList\n            .traverse: tx =>\n              val signedTx = signAlice(tx)\n              val json     = Seq(signedTx).asJson.spaces2\n              println(json)\n              println(Seq(tx.toHash).asJson.noSpaces)\n              IO.unit\n\n//              for response <-\n//                postTxClient(Seq(signedTx))\n//              yield\n//                println(response)\n//                ExitCode.Success\n            .as(ExitCode.Success)\n\n        case Left(err) =>\n          IO.println(err).as(ExitCode.Error)\n      }\n    }\n"
  },
  {
    "path": "modules/lib/js/src/main/scala/io/leisuremeta/chain/lib/crypto/CryptoOps.scala",
    "content": "package io.leisuremeta.chain.lib\npackage crypto\n\nimport scala.scalajs.js.JSConverters.*\nimport scala.scalajs.js.typedarray.Uint8Array\n\nimport io.github.iltotore.iron.*\nimport scodec.bits.ByteVector\nimport typings.bnJs.bnJsStrings.hex\nimport typings.elliptic.mod.{ec as EC}\nimport typings.elliptic.mod.ec.{KeyPair as JsKeyPair}\nimport typings.jsSha3.mod.{keccak256 as jsKeccak256}\n\nimport datatype.UInt256\n\nobject CryptoOps:\n\n  val ec: EC = new EC(\"secp256k1\")\n\n  def generate(): KeyPair = ec.genKeyPair().toScala\n\n  def fromPrivate(privateKey: BigInt): KeyPair =\n    ec.keyFromPrivate(privateKey.toByteArray.toUint8Array).toScala\n\n  @SuppressWarnings(Array(\"org.wartremover.warts.Throw\"))\n  def keccak256(input: Array[Byte]): Array[Byte] =\n\n    val hexString: String = jsKeccak256.hex(input.toUint8Array)\n\n    ByteVector\n      .fromHexDescriptive(hexString)\n      .fold(e => throw new Exception(e), _.toArray)\n\n  def sign(\n      keyPair: KeyPair,\n      transactionHash: Array[Byte],\n  ): Either[String, Signature] =\n    val jsSig = keyPair.toJs.sign(transactionHash.toUint8Array)\n    val recoveryParamEither:Either[String, Int] = (jsSig.recoveryParam: Any) match\n      case d: Double => Right[String, Int](d.toInt)\n      case other     => Left[String, Int](s\"Expected double but received: $other\")\n    for\n      recoveryParam <- recoveryParamEither\n      v             <- (27 + recoveryParam).refineEither[Signature.HeaderRange]\n      r <- UInt256.from(BigInt(jsSig.r.toString_hex(hex), 16)).left.map(_.msg)\n      s <- UInt256.from(BigInt(jsSig.s.toString_hex(hex), 16)).left.map(_.msg)\n    yield Signature(v, r, s)\n\n  @SuppressWarnings(Array(\"org.wartremover.warts.TripleQuestionMark\"))\n  def recover(\n      signature: Signature,\n      hashArray: Array[Byte],\n  ): Either[String, PublicKey] = ???\n\n  extension(jsKeyPair: JsKeyPair)\n    @SuppressWarnings(Array(\"org.wartremover.warts.Throw\"))\n    def toScala: KeyPair =\n      val privHex = jsKeyPair.getPrivate().toString_hex(hex)\n      val pubKey  = jsKeyPair.getPublic()\n      val xHex    = pubKey.getX().toString_hex(hex)\n      val yHex    = pubKey.getY().toString_hex(hex)\n      val xBigInt = BigInt(xHex, 16)\n      val yBigInt = BigInt(yHex, 16)\n      val pBigInt = BigInt(privHex, 16)\n      (for\n        p <- UInt256.from(pBigInt)\n        x <- UInt256.from(xBigInt)\n        y <- UInt256.from(yBigInt)\n      yield KeyPair(p, PublicKey(x, y))).getOrElse(\n        throw new Exception(s\"Wrong keyPair: $privHex, $xHex, $yHex\"),\n      )\n\n  extension(keyPair: KeyPair)\n    def toJs: JsKeyPair =\n      ec.keyFromPrivate(keyPair.privateKey.toByteArray.toUint8Array)\n\n  extension(byteArray: Array[Byte])\n    def toUint8Array: Uint8Array =\n      Uint8Array.from[Byte](byteArray.toJSArray, _.toShort)\n"
  },
  {
    "path": "modules/lib/jvm/src/main/scala/io/leisuremeta/chain/lib/crypto/CryptoOps.scala",
    "content": "package io.leisuremeta.chain.lib\npackage crypto\n\nimport java.math.BigInteger\nimport java.security.{KeyPairGenerator, SecureRandom, Security}\nimport java.security.spec.ECGenParameterSpec\nimport java.util.Arrays\n\nimport cats.Eq\nimport cats.syntax.either.given\nimport cats.syntax.eq.given\n\nimport io.github.iltotore.iron.*\nimport org.bouncycastle.asn1.x9.{X9ECParameters, X9IntegerConverter}\nimport org.bouncycastle.crypto.digests.SHA256Digest\nimport org.bouncycastle.crypto.ec.CustomNamedCurves\nimport org.bouncycastle.crypto.params.{\n  ECDomainParameters,\n  ECPrivateKeyParameters,\n}\nimport org.bouncycastle.crypto.signers.{ECDSASigner, HMacDSAKCalculator}\nimport org.bouncycastle.jcajce.provider.asymmetric.ec.{\n  BCECPrivateKey,\n  BCECPublicKey,\n}\nimport org.bouncycastle.jcajce.provider.digest.Keccak\nimport org.bouncycastle.jce.provider.BouncyCastleProvider\nimport org.bouncycastle.math.ec.{\n  ECAlgorithms,\n  ECPoint,\n  FixedPointCombMultiplier,\n}\nimport org.bouncycastle.math.ec.custom.sec.SecP256K1Curve\nimport shapeless3.typeable.syntax.typeable.cast\n\nimport datatype.{UInt256, UInt256BigInt}\nimport failure.UInt256RefineFailure\n\n@SuppressWarnings(Array(\"org.wartremover.warts.Equals\"))\nobject CryptoOps:\n\n  locally:\n    if Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null then\n      val _ = Security.addProvider(new BouncyCastleProvider())\n\n  val secureRandom: SecureRandom = new SecureRandom()\n\n  @SuppressWarnings(Array(\"org.wartremover.warts.Throw\"))\n  def generate(): KeyPair =\n    val gen  = KeyPairGenerator.getInstance(\"ECDSA\", \"BC\")\n    val spec = new ECGenParameterSpec(\"secp256k1\")\n    gen.initialize(spec, secureRandom)\n    val pair = gen.generateKeyPair\n\n    val keyPairOption = for\n      bcecPrivate <- pair.getPrivate.cast[BCECPrivateKey]\n      bcecPublic  <- pair.getPublic.cast[BCECPublicKey]\n      privateKey  <- UInt256.from(BigInt(bcecPrivate.getD)).toOption\n      publicKey <- PublicKey\n        .fromByteArray(bcecPublic.getQ.getEncoded(false).tail)\n        .toOption\n    yield KeyPair(privateKey, publicKey)\n    \n    keyPairOption.getOrElse:\n      throw new Exception(s\"Wrong keypair result: $pair\")\n\n  @SuppressWarnings(Array(\"org.wartremover.warts.Throw\"))\n  def fromPrivate(privateKey: BigInt): KeyPair =\n    val point: ECPoint = new FixedPointCombMultiplier()\n      .multiply(Curve.getG, privateKey.bigInteger mod Curve.getN)\n    val encoded: Array[Byte] = point.getEncoded(false)\n    val keypairEither: Either[UInt256RefineFailure, KeyPair] = for\n      private256 <- UInt256.from(privateKey)\n      public <- PublicKey.fromByteArray:\n        Arrays.copyOfRange(encoded, 1, encoded.length)\n    yield KeyPair(private256, public)\n\n    keypairEither match\n      case Right(keypair)                  => keypair\n      case Left(UInt256RefineFailure(msg)) => throw new Exception(msg)\n\n  given Eq[Array[Byte]]       = Eq.fromUniversalEquals\n  given Eq[Option[PublicKey]] = Eq.fromUniversalEquals\n\n  def keccak256(input: Array[Byte]): Array[Byte] =\n    val kecc = new Keccak.Digest256()\n    kecc.update(input, 0, input.length)\n    kecc.digest()\n\n  def sign(\n      keyPair: KeyPair,\n      transactionHash: Array[Byte],\n  ): Either[String, Signature] =\n    val signer = new ECDSASigner(new HMacDSAKCalculator(new SHA256Digest()))\n    signer.init(\n      true,\n      new ECPrivateKeyParameters(keyPair.privateKey.bigInteger, Curve),\n    )\n    val Array(r, sValue) = signer.generateSignature(transactionHash)\n    val s =\n      if sValue.compareTo(HalfCurveOrder) > 0 then Curve.getN subtract sValue\n      else sValue\n    for\n      r256 <- UInt256.from(BigInt(r)).leftMap(_.msg)\n      s256 <- UInt256.from(BigInt(s)).leftMap(_.msg)\n      recId <- (0 until 4)\n        .find: id =>\n          recoverFromSignature(id, r256, s256, transactionHash) ===\n            Some(keyPair.publicKey)\n        .toRight:\n          \"Could not construct a recoverable key. The credentials might not be valid.\"\n      v <- (recId + 27).refineEither[Signature.HeaderRange]\n    yield Signature(v, r256, s256)\n\n  def recover(\n      signature: Signature,\n      hashArray: Array[Byte],\n  ): Either[String, PublicKey] =\n    val header = signature.v & 0xff\n    val recId  = header - 27\n    recoverFromSignature(recId, signature.r, signature.s, hashArray)\n      .toRight(\"Could not recover public key from signature\")\n\n  private def recoverFromSignature(\n      recId: Int,\n      r: UInt256BigInt,\n      s: UInt256BigInt,\n      message: Array[Byte],\n  ): Option[PublicKey] =\n\n    val n = Curve.getN\n    val x = r.bigInteger add (n multiply BigInteger.valueOf(recId.toLong / 2))\n    val prime = SecP256K1Curve.q\n    if x.compareTo(prime) >= 0 then None\n    else\n      val R =\n        def decompressKey(xBN: BigInteger, yBit: Boolean): ECPoint =\n          val x9 = new X9IntegerConverter()\n          val compEnc: Array[Byte] =\n            x9.integerToBytes(xBN, 1 + x9.getByteLength(Curve.getCurve()))\n          compEnc(0) = if yBit then 0x03 else 0x02\n          Curve.getCurve().decodePoint(compEnc)\n        decompressKey(x, (recId & 1) === 1)\n      if !R.multiply(n).isInfinity() then None\n      else\n        val e        = new BigInteger(1, message)\n        val eInv     = BigInteger.ZERO subtract e mod n\n        val rInv     = r.bigInteger modInverse n\n        val srInv    = rInv multiply s.bigInteger mod n\n        val eInvrInv = rInv multiply eInv mod n\n        val q: ECPoint =\n          ECAlgorithms.sumOfTwoMultiplies(Curve.getG(), eInvrInv, R, srInv)\n        PublicKey.fromByteArray(q.getEncoded(false).tail).toOption\n\n  val CurveParams: X9ECParameters = CustomNamedCurves.getByName(\"secp256k1\")\n  val Curve: ECDomainParameters = new ECDomainParameters(\n    CurveParams.getCurve,\n    CurveParams.getG,\n    CurveParams.getN,\n    CurveParams.getH,\n  )\n  val HalfCurveOrder: BigInteger = CurveParams.getN.shiftRight(1)\n"
  },
  {
    "path": "modules/lib/shared/src/main/scala/io/leisuremeta/chain/lib/application/DAppState.scala",
    "content": "package io.leisuremeta.chain.lib\npackage application\n\nimport cats.Monad\nimport cats.data.{EitherT, StateT}\nimport cats.syntax.either.catsSyntaxEither\nimport cats.syntax.traverse.toTraverseOps\n\nimport fs2.Stream\nimport scodec.bits.ByteVector\n\nimport codec.byte.ByteCodec\nimport codec.byte.ByteDecoder.ops.*\nimport codec.byte.ByteEncoder.ops.*\nimport merkle.*\nimport merkle.MerkleTrie.NodeStore\nimport io.leisuremeta.chain.lib.merkle.toNibbles\n\ntrait DAppState[F[_], K, V]:\n  type ErrorOrF[A] = EitherT[F, String, A]\n\n  def get(k: K): StateT[ErrorOrF, MerkleTrieState, Option[V]]\n  def put(k: K, v: V): StateT[ErrorOrF, MerkleTrieState, Unit]\n  def remove(k: K): StateT[ErrorOrF, MerkleTrieState, Boolean]\n  def streamWithPrefix(\n      prefixBytes: ByteVector,\n  ): StateT[ErrorOrF, MerkleTrieState, Stream[ErrorOrF, (K, V)]]\n  def streamFrom(\n      keyBytes: ByteVector,\n  ): StateT[ErrorOrF, MerkleTrieState, Stream[ErrorOrF, (K, V)]]\n  def reverseStreamFrom(\n      keyPrefix: ByteVector,\n      keySuffix: Option[ByteVector],\n  ): StateT[ErrorOrF, MerkleTrieState, Stream[ErrorOrF, (K, V)]]\n\nobject DAppState:\n\n  case class WithCommonPrefix(prefix: String):\n\n    /** @param name\n      *   must be alpha-numeric\n      * @return\n      *   DAppState[F, K, V]\n      */\n    def ofName[F[_]: Monad: NodeStore, K: ByteCodec, V: ByteCodec](\n        name: String,\n    ): DAppState[F, K, V] =\n      scribe.info:\n        s\"Building DAppState from WithCommonPrefix $prefix of name $name\"\n      DAppState.ofName[F, K, V](s\"$prefix-$name\")\n\n  def ofName[F[_]: Monad: NodeStore, K: ByteCodec, V: ByteCodec](\n      name: String,\n  ): DAppState[F, K, V] =\n\n    scribe.info(s\"Initializing DAppState with name $name\")\n\n    @SuppressWarnings(Array(\"org.wartremover.warts.Throw\"))\n    val nameBytes: ByteVector = ByteVector.encodeUtf8(name) match\n      case Left(e)      => throw e\n      case Right(bytes) => bytes\n\n    new DAppState[F, K, V]:\n      def get(k: K): StateT[ErrorOrF, MerkleTrieState, Option[V]] =\n        for\n          bytesOption <- MerkleTrie.get[F]((nameBytes ++ k.toBytes).toNibbles)\n          vOption <- StateT.liftF:\n            EitherT.fromEither:\n              bytesOption.traverse(_.to[V].leftMap(_.msg))\n        yield\n//          scribe.info(s\"state $name get($k) result: $vOption\")\n          vOption\n      def put(k: K, v: V): StateT[ErrorOrF, MerkleTrieState, Unit] =\n        MerkleTrie\n          .put[F]((nameBytes ++ k.toBytes).toNibbles, v.toBytes)\n//          .map { _ => scribe.info(s\"state $name put($k, $v)\") }\n      def remove(k: K): StateT[ErrorOrF, MerkleTrieState, Boolean] =\n        MerkleTrie.remove[F]((nameBytes ++ k.toBytes).toNibbles)\n      def streamFrom(\n          keyBytes: ByteVector,\n      ): StateT[ErrorOrF, MerkleTrieState, Stream[ErrorOrF, (K, V)]] =\n        val prefixNibbles = (nameBytes ++ keyBytes).toNibbles\n        MerkleTrie\n          .streamFrom(prefixNibbles)\n          .map: binaryStream =>\n            binaryStream\n              .takeWhile(_._1.value.startsWith(nameBytes.bits))\n              .evalMap: (kNibbles, vBytes) =>\n                EitherT\n                  .fromEither:\n                    for\n                      k <- kNibbles.bytes.drop(nameBytes.size).to[K]\n                      v <- vBytes.to[V]\n                    yield (k, v)\n                  .leftMap(_.msg)\n      def streamWithPrefix(\n          prefixBytes: ByteVector,\n      ): StateT[ErrorOrF, MerkleTrieState, Stream[ErrorOrF, (K, V)]] =\n        val prefixNibbles = (nameBytes ++ prefixBytes).toNibbles\n        MerkleTrie\n          .streamFrom(prefixNibbles)\n          .map: binaryStream =>\n            binaryStream\n              .takeWhile: (kNibbles, _) =>\n                val flag = kNibbles.value.startsWith(prefixNibbles.value)\n//                scribe.info(s\"state $name streamWithPrefix(${prefixBytes.toHex}) ${kNibbles.value.toHex} $flag\")\n                flag\n              .evalMap: (kNibbles, vBytes) =>\n                EitherT\n                  .fromEither:\n                    for\n                      k <- kNibbles.bytes.drop(nameBytes.size).to[K]\n                      v <- vBytes.to[V]\n                    yield\n//                      scribe.info(s\"state $name streamWithPrefix($prefixBytes) $k -> $v\")\n                      (k, v)\n                  .leftMap(_.msg)\n\n      def reverseStreamFrom(\n          keyPrefix: ByteVector,\n          keySuffix: Option[ByteVector],\n      ): StateT[ErrorOrF, MerkleTrieState, Stream[ErrorOrF, (K, V)]] =\n        val prefixNibbles = (nameBytes ++ keyPrefix).toNibbles\n        MerkleTrie\n          .reverseStreamFrom(prefixNibbles, keySuffix.map(_.toNibbles))\n          .map: binaryStream =>\n            binaryStream\n              .evalMap: (kNibbles, vBytes) =>\n                EitherT\n                  .fromEither:\n                    for\n                      k <- kNibbles.bytes.drop(nameBytes.size).to[K]\n                      v <- vBytes.to[V]\n                    yield (k, v)\n                  .leftMap(_.msg)\n"
  },
  {
    "path": "modules/lib/shared/src/main/scala/io/leisuremeta/chain/lib/codec/byte/ByteCodec.scala",
    "content": "package io.leisuremeta.chain.lib\npackage codec.byte\n\nimport scodec.bits.ByteVector\nimport failure.DecodingFailure\n\ntrait ByteCodec[A] extends ByteDecoder[A] with ByteEncoder[A]\n\nobject ByteCodec:\n\n  def apply[A](implicit bc: ByteCodec[A]): ByteCodec[A] = bc\n\n  given [A](using\n      decoder: ByteDecoder[A],\n      encoder: ByteEncoder[A],\n  ): ByteCodec[A] = new ByteCodec[A]:\n    override def decode(\n        bytes: ByteVector,\n    ): Either[DecodingFailure, DecodeResult[A]] = decoder.decode(bytes)\n    override def encode(a: A): ByteVector = encoder.encode(a)\n"
  },
  {
    "path": "modules/lib/shared/src/main/scala/io/leisuremeta/chain/lib/codec/byte/ByteDecoder.scala",
    "content": "package io.leisuremeta.chain.lib\npackage codec.byte\n\nimport java.time.Instant\n\nimport scala.compiletime.{erasedValue, summonInline}\nimport scala.deriving.Mirror\nimport scala.reflect.{ClassTag, classTag}\n\nimport cats.syntax.eq.catsSyntaxEq\nimport cats.syntax.either.*\n\nimport eu.timepit.refined.api.Refined\nimport eu.timepit.refined.auto.autoUnwrap\nimport eu.timepit.refined.numeric.NonNegative\nimport eu.timepit.refined.refineV\nimport scodec.bits.ByteVector\n\nimport failure.DecodingFailure\n\ntrait ByteDecoder[A]:\n  def decode(bytes: ByteVector): Either[DecodingFailure, DecodeResult[A]]\n\n  def map[B](f: A => B): ByteDecoder[B] = bytes =>\n    decode(bytes).map { case DecodeResult(a, remainder) =>\n      DecodeResult(f(a), remainder)\n    }\n\n  def emap[B](f: A => Either[DecodingFailure, B]): ByteDecoder[B] = bytes =>\n    for\n      decoded   <- decode(bytes)\n      converted <- f(decoded.value)\n    yield DecodeResult(converted, decoded.remainder)\n\n  def flatMap[B](f: A => ByteDecoder[B]): ByteDecoder[B] = bytes =>\n    decode(bytes).flatMap { case DecodeResult(a, remainder) =>\n      f(a).decode(remainder)\n    }\n\n  def widen[AA >: A]: ByteDecoder[AA] = map(identity)\n\nfinal case class DecodeResult[+A](value: A, remainder: ByteVector)\n\nobject ByteDecoder:\n  def apply[A: ByteDecoder]: ByteDecoder[A] = summon\n\n  object ops:\n    extension (bytes: ByteVector)\n      def to[A: ByteDecoder]: Either[DecodingFailure, A] = for\n        result <- ByteDecoder[A].decode(bytes)\n        DecodeResult(a, r) = result\n        _ <- Either.cond(\n          r.isEmpty,\n          (),\n          DecodingFailure(s\"non empty remainder: $r\"),\n        )\n      yield a\n\n  private def decoderProduct[A](\n      p: Mirror.ProductOf[A],\n      elems: => List[ByteDecoder[?]],\n  ): ByteDecoder[A] = (bytes: ByteVector) =>\n\n    def reverse(tuple: Tuple): Tuple =\n      @SuppressWarnings(Array(\"org.wartremover.warts.Any\"))\n      @annotation.tailrec\n      def loop(tuple: Tuple, acc: Tuple): Tuple = tuple match\n        case _: EmptyTuple => acc\n        case t *: ts       => loop(ts, t *: acc)\n      loop(tuple, EmptyTuple)\n\n    @annotation.tailrec\n    def loop(\n        elems: List[ByteDecoder[?]],\n        bytes: ByteVector,\n        acc: Tuple,\n    ): Either[DecodingFailure, DecodeResult[A]] = elems match\n      case Nil =>\n        (DecodeResult(p.fromProduct(reverse(acc)), bytes))\n          .asRight[DecodingFailure]\n      case decoder :: rest =>\n        scribe.debug(s\"Decoder: $decoder\")\n        scribe.debug(s\"Bytes to decode: $bytes\")\n        decoder.decode(bytes) match\n          case Left(failure) => failure.asLeft[DecodeResult[A]]\n          case Right(DecodeResult(value, remainder)) =>\n            scribe.debug(s\"Decoded: $value\")\n            loop(rest, remainder, value *: acc)\n    loop(elems, bytes, EmptyTuple)\n\n  @SuppressWarnings(Array(\"org.wartremover.warts.Recursion\"))\n  inline def summonAll[T <: Tuple]: List[ByteDecoder[?]] =\n    inline erasedValue[T] match\n      case _: EmptyTuple => Nil\n      case _: (t *: ts)  => summonInline[ByteDecoder[t]] :: summonAll[ts]\n\n  inline given derived[T](using p: Mirror.ProductOf[T]): ByteDecoder[T] =\n    lazy val elemInstances: List[ByteDecoder[?]] =\n      summonAll[p.MirroredElemTypes]\n    decoderProduct(p, elemInstances)\n\n  given unitByteDecoder: ByteDecoder[Unit] = bytes =>\n    Right[DecodingFailure, DecodeResult[Unit]](\n      DecodeResult((), bytes),\n    )\n\n  type BigNat = BigInt Refined NonNegative\n\n  @SuppressWarnings(Array(\"org.wartremover.warts.Throw\"))\n  def unsafeFromBigInt(n: BigInt): BigNat = refineV[NonNegative](n) match\n    case Right(nat) => nat\n    case Left(e)    => throw new Exception(e)\n\n  def fromFixedSizeBytes[T: ClassTag](\n      size: Long,\n  )(f: ByteVector => T): ByteDecoder[T] = bytes =>\n    Either.cond(\n      bytes.size >= size,\n      bytes splitAt size match\n        case (front, back) => DecodeResult(f(front), back)\n      ,\n      DecodingFailure(\n        s\"Too short bytes to decode ${classTag[T]}; required $size bytes, but receiced ${bytes.size} bytes: $bytes\",\n      ),\n    )\n\n//  @SuppressWarnings(Array(\"org.wartremover.warts.OptionPartial\"))\n//  given ByteDecoder[UInt256BigInt] = fromFixedSizeBytes(32) { bytes =>\n//    UInt256.from(BigInt(1, bytes.toArray)).toOption.get\n//  }\n//\n//  @SuppressWarnings(Array(\"org.wartremover.warts.OptionPartial\"))\n//  given ByteDecoder[UInt256Bytes] = fromFixedSizeBytes(32) {\n//    UInt256.from(_).toOption.get\n//  }\n\n  given byteDecoder: ByteDecoder[Byte] = fromFixedSizeBytes(1)(_.toByte())\n\n  given booleanDecoder: ByteDecoder[Boolean] = fromFixedSizeBytes(1): bytes =>\n    bytes.head =!= 0x00.toByte\n\n  given longDecoder: ByteDecoder[Long] = fromFixedSizeBytes(8)(_.toLong())\n\n  given instantDecoder: ByteDecoder[Instant] =\n    ByteDecoder[Long] map Instant.ofEpochMilli\n\n  given bignatByteDecoder: ByteDecoder[BigNat] = bytes =>\n    Either.cond(bytes.nonEmpty, bytes, DecodingFailure(\"Empty bytes\")).flatMap {\n      nonEmptyBytes =>\n        val head: Int        = nonEmptyBytes.head & 0xff\n        val tail: ByteVector = nonEmptyBytes.tail\n        if head <= 0x80 then\n          Right[DecodingFailure, DecodeResult[BigNat]](\n            DecodeResult(unsafeFromBigInt(BigInt(head)), tail),\n          )\n        else if head <= 0xf8 then\n          val size = head - 0x80\n          if tail.size < size then\n            Left[DecodingFailure, DecodeResult[BigNat]](\n              DecodingFailure(\n                s\"required byte size $size, but $tail\",\n              ),\n            )\n          else\n            val (front, back) = tail.splitAt(size.toLong)\n            Right[DecodingFailure, DecodeResult[BigNat]](\n              DecodeResult(unsafeFromBigInt(BigInt(1, front.toArray)), back),\n            )\n        else\n          val sizeOfNumber = head - 0xf8 + 1\n          if tail.size < sizeOfNumber then\n            Left[DecodingFailure, DecodeResult[BigNat]](\n              DecodingFailure(\n                s\"required byte size $sizeOfNumber, but $tail\",\n              ),\n            )\n          else\n            val (sizeBytes, data) = tail.splitAt(sizeOfNumber.toLong)\n            val size              = BigInt(1, sizeBytes.toArray).toLong\n\n            if data.size < size then\n              Left[DecodingFailure, DecodeResult[BigNat]](\n                DecodingFailure(\n                  s\"required byte size $size, but $data\",\n                ),\n              )\n            else\n              val (front, back) = data.splitAt(size)\n              Right[DecodingFailure, DecodeResult[BigNat]](\n                DecodeResult(unsafeFromBigInt(BigInt(1, front.toArray)), back),\n              )\n    }\n\n  given bigintByteDecoder: ByteDecoder[BigInt] = ByteDecoder[BigNat].map{\n    case x if x % 2 === 0 => x / 2\n    case x => (x - 1) / (-2)\n  }\n\n  def sizedListDecoder[A: ByteDecoder](size: BigNat): ByteDecoder[List[A]] =\n    bytes =>\n      @annotation.tailrec\n      def loop(\n          bytes: ByteVector,\n          count: BigInt,\n          acc: List[A],\n      ): Either[DecodingFailure, DecodeResult[List[A]]] =\n        if count === BigInt(0) then\n          Right[DecodingFailure, DecodeResult[List[A]]](\n            DecodeResult(acc.reverse, bytes),\n          )\n        else\n          ByteDecoder[A].decode(bytes) match\n            case Left(failure) =>\n              Left[DecodingFailure, DecodeResult[List[A]]](failure)\n            case Right(DecodeResult(value, remainder)) =>\n              loop(remainder, count - 1, value :: acc)\n      loop(bytes, size, Nil)\n\n  given mapByteDecoder[K: ByteDecoder, V: ByteDecoder]: ByteDecoder[Map[K, V]] =\n    bignatByteDecoder flatMap sizedListDecoder[(K, V)] map (_.toMap)\n\n  given optionByteDecoder[A: ByteDecoder]: ByteDecoder[Option[A]] =\n    bignatByteDecoder flatMap sizedListDecoder[A] map (_.headOption)\n\n  given setByteDecoder[A: ByteDecoder]: ByteDecoder[Set[A]] =\n    bignatByteDecoder flatMap sizedListDecoder[A] map (_.toSet)\n\n  @SuppressWarnings(Array(\"org.wartremover.warts.AsInstanceOf\"))\n  given seqByteDecoder[A: ByteDecoder]: ByteDecoder[Seq[A]] =\n    bignatByteDecoder flatMap sizedListDecoder[A] map (_.asInstanceOf[Seq[A]])\n    \n"
  },
  {
    "path": "modules/lib/shared/src/main/scala/io/leisuremeta/chain/lib/codec/byte/ByteEncoder.scala",
    "content": "package io.leisuremeta.chain.lib\npackage codec.byte\n\nimport java.time.Instant\n\nimport scala.compiletime.{erasedValue, summonInline}\nimport scala.deriving.Mirror\nimport scala.reflect.ClassTag\n\nimport cats.syntax.eq.catsSyntaxEq\nimport eu.timepit.refined.api.Refined\nimport eu.timepit.refined.auto.autoUnwrap\nimport eu.timepit.refined.numeric.NonNegative\nimport eu.timepit.refined.refineV\nimport scodec.bits.ByteVector\nimport datatype.UInt256\n\ntrait ByteEncoder[A]:\n  def encode(a: A): ByteVector\n\n  def contramap[B](f: B => A): ByteEncoder[B] = b => encode(f(b))\n\nobject ByteEncoder:\n  def apply[A: ByteEncoder]: ByteEncoder[A] = summon\n\n  object ops:\n    extension [A](a: A)\n      def toBytes(implicit be: ByteEncoder[A]): ByteVector = be.encode(a)\n\n  type BigNat = BigInt Refined NonNegative\n\n  @SuppressWarnings(Array(\"org.wartremover.warts.Throw\"))\n  def unsafeFromBigInt(n: BigInt): BigNat = refineV[NonNegative](n) match\n    case Right(nat) => nat\n    case Left(e)    => throw new Exception(e)\n\n  @SuppressWarnings(Array(\"org.wartremover.warts.AsInstanceOf\", \"org.wartremover.warts.Any\"))\n  private def encoderProduct[A](\n//      p: Mirror.ProductOf[A],\n      elems: => List[ByteEncoder[?]],\n  ): ByteEncoder[A] = (a: A) =>\n    a.asInstanceOf[Product].productIterator.zip(elems).map{\n     case (aElem, encoder) => encoder.asInstanceOf[ByteEncoder[Any]].encode(aElem)\n    }.foldLeft(ByteVector.empty)(_ ++ _)\n\n  @SuppressWarnings(Array(\"org.wartremover.warts.Recursion\"))\n  inline def summonAll[T <: Tuple]: List[ByteEncoder[?]] =\n    inline erasedValue[T] match\n      case _: EmptyTuple => Nil\n      case _: (t *: ts)  => summonInline[ByteEncoder[t]] :: summonAll[ts]\n\n  inline given derived[T](using p: Mirror.ProductOf[T]): ByteEncoder[T] =\n    lazy val elemInstances: List[ByteEncoder[?]] =\n      summonAll[p.MirroredElemTypes]\n    encoderProduct(elemInstances)\n\n  given unitByteEncoder: ByteEncoder[Unit] = _ => ByteVector.empty\n\n  given [A: UInt256.Ops]: ByteEncoder[UInt256.Refined[A]] = _.toBytes\n\n  given instantEncoder: ByteEncoder[Instant] =\n    ByteVector fromLong _.toEpochMilli\n\n  given bignatByteEncoder: ByteEncoder[BigNat] = bignat =>\n    val bytes = ByteVector.view(bignat.toByteArray).dropWhile(_ === 0x00.toByte)\n    if bytes.isEmpty then ByteVector(0x00.toByte)\n    else if bignat <= 0x80 then bytes\n    else\n      val size = bytes.size\n      if size < (0xf8 - 0x80) + 1 then\n        ByteVector.fromByte((size + 0x80).toByte) ++ bytes\n      else\n        val sizeBytes = ByteVector.fromLong(size).dropWhile(_ === 0x00.toByte)\n        ByteVector.fromByte(\n          (sizeBytes.size + 0xf8 - 1).toByte,\n        ) ++ sizeBytes ++ bytes\n\n  given booleanEncoder: ByteEncoder[Boolean] =\n    case true => ByteVector(0x01.toByte)\n    case false => ByteVector(0x00.toByte)\n\n  @SuppressWarnings(Array(\"org.wartremover.warts.AsInstanceOf\"))\n  given bigintByteEncoder: ByteEncoder[BigInt] = ByteEncoder[BigNat].contramap {\n    case n if n >= 0 => (n * 2).asInstanceOf[BigNat]\n    case n           => (n * (-2) + 1).asInstanceOf[BigNat]\n  }\n\n  given listByteEncoder[A: ByteEncoder]: ByteEncoder[List[A]] =\n    (list: List[A]) =>\n      list.foldLeft(bignatByteEncoder.encode(unsafeFromBigInt(list.size))) {\n        case (acc, a) => acc ++ ByteEncoder[A].encode(a)\n      }\n\n  given mapByteEncoder[K: ByteEncoder, V: ByteEncoder]: ByteEncoder[Map[K, V]] =\n    listByteEncoder[(K, V)].contramap(_.toList)\n\n  given optionByteEncoder[A: ByteEncoder]: ByteEncoder[Option[A]] =\n    listByteEncoder.contramap(_.toList)\n\n  given setByteEncoder[A: ByteEncoder]: ByteEncoder[Set[A]] = (set: Set[A]) =>\n    set\n      .map(ByteEncoder[A].encode)\n      .toList\n      .sorted\n      .foldLeft {\n        bignatByteEncoder.encode(unsafeFromBigInt(set.size))\n      }(_ ++ _)\n\n  given seqByteEncoder[A: ByteEncoder]: ByteEncoder[Seq[A]] =\n    (seq: Seq[A]) =>\n      seq.foldLeft(bignatByteEncoder.encode(unsafeFromBigInt(seq.size))) {\n        case (acc, a) => acc ++ ByteEncoder[A].encode(a)\n      }\n"
  },
  {
    "path": "modules/lib/shared/src/main/scala/io/leisuremeta/chain/lib/crypto/Hash.scala",
    "content": "package io.leisuremeta.chain.lib\npackage crypto\n\nimport cats.Eq\nimport cats.Contravariant\n\nimport io.circe.{Decoder, Encoder, KeyEncoder, KeyDecoder}\nimport scodec.bits.ByteVector\n\nimport codec.byte.{ByteDecoder, ByteEncoder}\nimport datatype.{UInt256, UInt256Bytes}\n\ntrait Hash[A]:\n  def apply(a: A): Hash.Value[A]\n  def contramap[B](f: B => A): Hash[B] = (b: B) =>\n    Hash.Value[B](apply(f(b)).toUInt256Bytes)\n\nobject Hash:\n  def apply[A: Hash]: Hash[A] = summon\n\n  opaque type Value[A] = UInt256Bytes\n  object Value:\n    def apply[A](uint256: UInt256Bytes): Value[A] = uint256\n\n    given circeValueDecoder[A]: Decoder[Value[A]] =\n      UInt256.uint256bytesCirceDecoder.map(Value[A](_))\n\n    given circeValueEncoder[A]: Encoder[Value[A]] =\n      UInt256.uint256bytesCirceEncoder.contramap[Value[A]](_.toUInt256Bytes)\n\n    given circeKeyDecoder[A]: KeyDecoder[Value[A]] = (str) =>\n      for\n        bytes <- ByteVector.fromHex(str)\n        uint256 <- UInt256.from(bytes).toOption\n      yield Value[A](uint256)\n\n    given circeKeyEncoder[A]: KeyEncoder[Value[A]] =\n      KeyEncoder.encodeKeyString.contramap[Value[A]](_.toUInt256Bytes.toBytes.toHex)\n\n    given byteValueDecoder[A]: ByteDecoder[Value[A]] =\n      UInt256.uint256bytesByteDecoder.map(Value[A](_))\n\n    given byteValueEncoder[A]: ByteEncoder[Value[A]] =\n      UInt256.uint256bytesByteEncoder.contramap[Value[A]](_.toUInt256Bytes)\n\n    given eqValue[A]: Eq[Value[A]] = Eq.fromUniversalEquals\n\n\n  extension [A](value: Value[A]) def toUInt256Bytes: UInt256Bytes = value\n\n  object ops:\n    extension [A](a: A) def toHash(using h: Hash[A]): Value[A] = h(a)\n\n  given contravariant: Contravariant[Hash] = new Contravariant[Hash]:\n    override def contramap[A, B](fa: Hash[A])(f: B => A): Hash[B] =\n      fa.contramap(f)\n\n  @SuppressWarnings(Array(\"org.wartremover.warts.OptionPartial\"))\n  def build[A: ByteEncoder]: Hash[A] = (a: A) =>\n    val bytes = ByteEncoder[A].encode(a)\n    val h     = ByteVector.view(CryptoOps.keccak256(bytes.toArray))\n    Value[A](UInt256.from(h).toOption.get)\n"
  },
  {
    "path": "modules/lib/shared/src/main/scala/io/leisuremeta/chain/lib/crypto/KeyPair.scala",
    "content": "package io.leisuremeta.chain.lib\npackage crypto\n\nimport datatype.UInt256BigInt\n\nfinal case class KeyPair(privateKey: UInt256BigInt, publicKey: PublicKey) {\n  override lazy val toString: String =\n    s\"KeyPair(${privateKey.toBytes.toHex}, $publicKey)\"\n}\n"
  },
  {
    "path": "modules/lib/shared/src/main/scala/io/leisuremeta/chain/lib/crypto/PublicKey.scala",
    "content": "package io.leisuremeta.chain.lib\npackage crypto\n\nimport cats.implicits.given\n\nimport scodec.bits.ByteVector\n\nimport codec.byte.{ByteDecoder, ByteEncoder}\nimport datatype.{UInt256BigInt, UInt256}\nimport failure.UInt256RefineFailure\nimport io.leisuremeta.chain.lib.failure.DecodingFailure\n\nfinal case class PublicKey(x: UInt256BigInt, y: UInt256BigInt):\n  def toBytes: ByteVector = x.toBytes ++ y.toBytes\n  def toBigInt: BigInt    = BigInt(1, toBytes.toArray)\n\n  override def toString: String = s\"PublicKey($toBytes)\"\n\nobject PublicKey:\n  @SuppressWarnings(Array(\"org.wartremover.warts.Nothing\"))\n  def fromByteArray(\n      array: Array[Byte],\n  ): Either[UInt256RefineFailure, PublicKey] =\n    if array.length =!= 64 then\n      Left(\n        UInt256RefineFailure(s\"Public key array size are not 64: $array\"),\n      )\n    else\n      val (xArr, yArr) = array splitAt 32\n      for\n        x <- UInt256.from(BigInt(1, xArr))\n        y <- UInt256.from(BigInt(1, yArr))\n      yield PublicKey(x, y)\n\n  val pubkeyByteEncoder: ByteEncoder[PublicKey] = _.toBytes\n  given pubkeyByteDecoder: ByteDecoder[PublicKey] =\n    ByteDecoder.fromFixedSizeBytes(64)(identity).emap { bytes =>\n      fromByteArray(bytes.toArray).left.map(e => DecodingFailure(e.msg))\n    }\n\n  given Hash[PublicKey] = Hash.build\n"
  },
  {
    "path": "modules/lib/shared/src/main/scala/io/leisuremeta/chain/lib/crypto/Recover.scala",
    "content": "package io.leisuremeta.chain.lib\npackage crypto\n\ntrait Recover[A]:\n  def apply(a: A, signature: Signature)(implicit\n      hash: Hash[A],\n  ): Option[PublicKey] = fromHash(hash(a), signature)\n\n  def fromHash(\n      hashValue: Hash.Value[A],\n      signature: Signature,\n  ): Option[PublicKey]\n\nobject Recover:\n  def apply[A: Recover]: Recover[A] = summon\n\n  def build[A]: Recover[A] = \n    (hashValue: Hash.Value[A], signature: Signature) =>\n      CryptoOps.recover(signature, hashValue.toUInt256Bytes.toArray).toOption\n\n  object ops:\n    extension [A](hashValue: Hash.Value[A])\n      def recover(signature: Signature)(using\n          r: Recover[A],\n      ): Option[PublicKey] = r.fromHash(hashValue, signature)\n  \n"
  },
  {
    "path": "modules/lib/shared/src/main/scala/io/leisuremeta/chain/lib/crypto/Sign.scala",
    "content": "package io.leisuremeta.chain.lib.crypto\n\ntrait Sign[A]:\n  def apply(a: A, keyPair: KeyPair)(implicit\n      hash: Hash[A],\n  ): Either[String, Signature] = byHash(hash(a), keyPair)\n\n  def byHash(\n      hashValue: Hash.Value[A],\n      keyPair: KeyPair,\n  ): Either[String, Signature]\n\nobject Sign:\n  def apply[A: Sign]: Sign[A] = summon\n\n  def build[A]: Sign[A] = (hashValue: Hash.Value[A], keyPair: KeyPair) =>\n    CryptoOps.sign(keyPair, hashValue.toUInt256Bytes.toBytes.toArray)\n  object ops:\n    extension (keyPair: KeyPair)\n      def sign[A: Hash: Sign](a: A): Either[String, Signature] =\n        Sign[A].apply(a, keyPair)\n\n    extension [A](hashValue: Hash.Value[A])\n      def signBy(keyPair: KeyPair)(using\n          sign: Sign[A],\n      ): Either[String, Signature] = sign.byHash(hashValue, keyPair)\n"
  },
  {
    "path": "modules/lib/shared/src/main/scala/io/leisuremeta/chain/lib/crypto/Signature.scala",
    "content": "package io.leisuremeta.chain.lib\npackage crypto\n\nimport cats.syntax.either.*\n\nimport io.circe.{Decoder, Encoder}\nimport io.circe.generic.semiauto.*\nimport io.github.iltotore.iron.*\nimport io.github.iltotore.iron.constraint.numeric.*\nimport io.github.iltotore.iron.circe.given\nimport scodec.bits.ByteVector\n\nimport codec.byte.{ByteDecoder, ByteEncoder, DecodeResult}\nimport datatype.UInt256BigInt\nimport failure.DecodingFailure\n\nfinal case class Signature(\n    v: Signature.Header,\n    r: UInt256BigInt,\n    s: UInt256BigInt,\n):\n  override lazy val toString: String =\n    s\"Signature($v, ${r.toBytes.toHex}, ${s.toBytes.toHex})\"\n\nobject Signature:\n\n  type HeaderRange = Interval.Closed[27, 34]\n\n  type Header = Int :| HeaderRange\n\n  given headerEncoder: ByteEncoder[Header] =\n    ByteVector `fromByte` _.toByte\n\n  given headerDecoder: ByteDecoder[Header] =\n    ByteDecoder[Byte]\n      .decode(_)\n      .flatMap:\n        case DecodeResult(b, remainder) =>\n          b.toInt\n            .refineEither[HeaderRange]\n            .map(DecodeResult(_, remainder))\n            .leftMap(DecodingFailure(_))\n\n  given sigEncoder: ByteEncoder[Signature] = ByteEncoder.derived\n\n  given sigDecoder: ByteDecoder[Signature] = ByteDecoder.derived\n\n  given sigCirceEncoder: Encoder[Signature] = deriveEncoder[Signature]\n\n  given sigCirceDecoder: Decoder[Signature] = deriveDecoder[Signature]\n"
  },
  {
    "path": "modules/lib/shared/src/main/scala/io/leisuremeta/chain/lib/datatype/BigNat.scala",
    "content": "package io.leisuremeta.chain.lib\npackage datatype\n\nimport scala.math.Ordering\n\nimport cats.Eq\nimport cats.implicits.given\n\nimport eu.timepit.refined.api.Refined\nimport eu.timepit.refined.numeric.NonNegative\nimport eu.timepit.refined.refineV\nimport io.circe.{\n  Decoder as CirceDecoder,\n  Encoder as CirceEncoder,\n}\nimport io.circe.refined.*\n\nimport codec.byte.{ByteDecoder, ByteEncoder}\n\nopaque type BigNat = BigInt Refined NonNegative\n\nobject BigNat:\n  @SuppressWarnings(Array(\"org.wartremover.warts.OptionPartial\"))\n  val Zero: BigNat = refineV[NonNegative](BigInt(0)).toOption.get\n  @SuppressWarnings(Array(\"org.wartremover.warts.OptionPartial\"))\n  val One: BigNat = refineV[NonNegative](BigInt(1)).toOption.get\n\n  def fromBigInt(n: BigInt): Either[String, BigNat] = refineV[NonNegative](n)\n\n  extension (bignat: BigNat)\n    @annotation.targetName(\"plus\")\n    def +(that: BigNat): BigNat = BigNat.add(bignat, that)\n\n    @annotation.targetName(\"times\")\n    def *(that: BigNat): BigNat = BigNat.multiply(bignat, that)\n\n    @annotation.targetName(\"diviedBy\")\n    def /(that: BigNat): BigNat = BigNat.divide(bignat, that)\n\n    def toBigInt: BigInt = bignat.value\n\n    def floorAt(e: Int): BigNat =\n      val n = BigInt(10).pow(e)\n      unsafeFromBigInt(bignat.value / n * n)\n\n  @SuppressWarnings(Array(\"org.wartremover.warts.Throw\"))\n  def unsafeFromBigInt(n: BigInt): BigNat = fromBigInt(n) match\n    case Right(nat) => nat\n    case Left(e)    => throw new Exception(e)\n\n  def unsafeFromLong(long: Long): BigNat = unsafeFromBigInt(BigInt(long))\n\n  @SuppressWarnings(Array(\"org.wartremover.warts.Throw\"))\n  def add(x: BigNat, y: BigNat): BigNat =\n    refineV[NonNegative](x.value + y.value) match\n      case Right(nat) => nat\n      case Left(e)    => throw new Exception(e)\n\n  @SuppressWarnings(Array(\"org.wartremover.warts.Throw\"))\n  def multiply(x: BigNat, y: BigNat): BigNat =\n    refineV[NonNegative](x.value * y.value) match\n      case Right(nat) => nat\n      case Left(e)    => throw new Exception(e)\n\n  @SuppressWarnings(Array(\"org.wartremover.warts.Throw\"))\n  def divide(x: BigNat, y: BigNat): BigNat =\n    refineV[NonNegative](x.value / y.value) match\n      case Right(nat) => nat\n      case Left(e)    => throw new Exception(e)\n\n  def tryToSubtract(x: BigNat, y: BigNat): Either[String, BigNat] =\n    fromBigInt(x.toBigInt - y.toBigInt)\n\n  def max(x: BigNat, y: BigNat): BigNat =\n    if x.toBigInt >= y.toBigInt then x else y\n\n  def min(x: BigNat, y: BigNat): BigNat =\n    if x.toBigInt <= y.toBigInt then x else y\n\n  given bignatByteDecoder: ByteDecoder[BigNat] = ByteDecoder.bignatByteDecoder\n\n  given bignatByteEncoder: ByteEncoder[BigNat] = ByteEncoder.bignatByteEncoder\n\n  given bignatCirceDecoder: CirceDecoder[BigNat] =\n    refinedDecoder[BigInt, NonNegative, Refined]\n\n  given bignatCirceEncoder: CirceEncoder[BigNat] =\n    refinedEncoder[BigInt, NonNegative, Refined]\n\n  given bignatOrdering: Ordering[BigNat] = Ordering.by(_.toBigInt)\n\n  given bignatEq: Eq[BigNat] = Eq.by(_.toBigInt)\n"
  },
  {
    "path": "modules/lib/shared/src/main/scala/io/leisuremeta/chain/lib/datatype/UInt256.scala",
    "content": "package io.leisuremeta.chain.lib\npackage datatype\n\nimport scala.util.Try\n\nimport cats.syntax.eq.given\n\nimport io.circe.{Decoder, Encoder}\nimport scodec.bits.ByteVector\n\nimport codec.byte.{ByteDecoder, ByteEncoder}\nimport failure.{DecodingFailure, UInt256RefineFailure}\n\ntype UInt256Bytes  = UInt256.Refined[ByteVector]\ntype UInt256BigInt = UInt256.Refined[BigInt]\n\nobject UInt256:\n  trait Refine[A]\n\n  type Refined[A] = A & Refine[A]\n\n  def from[A: Ops](value: A): Either[UInt256RefineFailure, Refined[A]] =\n    Ops[A].from(value)\n\n  extension [A: Ops](value: Refined[A])\n    def toBytes: ByteVector = Ops[A].toBytes(value)\n    def toBigInt: BigInt    = Ops[A].toBigInt(value)\n\n  trait Ops[A]:\n    def from(value: A): Either[UInt256RefineFailure, Refined[A]]\n    def toBytes(value: A): ByteVector\n    def toBigInt(value: A): BigInt\n\n  object Ops:\n    def apply[A: Ops]: Ops[A] = summon\n\n    given Ops[ByteVector] = new Ops[ByteVector]:\n\n      @SuppressWarnings(Array(\"org.wartremover.warts.AsInstanceOf\"))\n      def from(\n          value: ByteVector,\n      ): Either[UInt256RefineFailure, Refined[ByteVector]] =\n        Either.cond(\n          value.size === 32L,\n          value.asInstanceOf[UInt256Bytes],\n          UInt256RefineFailure(\n            s\"Incorrect sized bytes to be UInt256: $value\",\n          ),\n        )\n      def toBytes(value: ByteVector): ByteVector = value\n      def toBigInt(value: ByteVector): BigInt    = BigInt(1, value.toArray)\n\n    given Ops[BigInt] = new Ops[BigInt]:\n\n      @SuppressWarnings(Array(\"org.wartremover.warts.AsInstanceOf\"))\n      def from(value: BigInt): Either[UInt256RefineFailure, Refined[BigInt]] =\n        Either.cond(\n          value >= 0L && value.bitLength <= 256,\n          value.asInstanceOf[UInt256BigInt],\n          UInt256RefineFailure(\n            s\"Bigint out of range to be UInt256: $value\",\n          ),\n        )\n      def toBytes(value: BigInt): ByteVector =\n        ByteVector.view(value.toByteArray).takeRight(32L).padLeft(32L)\n      def toBigInt(value: BigInt): BigInt = value\n  end Ops\n\n  given uint256bytesCirceEncoder: Encoder[UInt256Bytes] =\n    Encoder[String].contramap[UInt256Bytes](_.toBytes.toHex)\n  given uint256bytesCirceDecoder: Decoder[UInt256Bytes] =\n    Decoder.decodeString.emap((str: String) =>\n      for\n        bytes   <- ByteVector.fromHexDescriptive(str)\n        refined <- UInt256.from(bytes).left.map(_.msg)\n      yield refined,\n    )\n\n  given uint256bigintCirceEncoder: Encoder[UInt256BigInt] =\n    Encoder[String].contramap[UInt256BigInt](_.toBytes.toHex)\n  given uint256bigintCirceDecoder: Decoder[UInt256BigInt] =\n    Decoder.decodeString.emap((str: String) =>\n      for\n        bigint  <- Try(BigInt(str, 16)).toEither.left.map(_.getMessage)\n        refined <- UInt256.from(bigint).left.map(_.msg)\n      yield refined,\n    )\n\n  given uint256bytesByteDecoder: ByteDecoder[UInt256Bytes] =\n    ByteDecoder\n      .fromFixedSizeBytes(32)(identity)\n      .emap(UInt256.from(_).left.map(e => DecodingFailure(e.msg)))\n  given uint256bytesByteEncoder: ByteEncoder[UInt256Bytes] = _.toBytes\n\n  given uint256bigintByteDecoder: ByteDecoder[UInt256BigInt] =\n    ByteDecoder\n      .fromFixedSizeBytes(32)(bytes => BigInt(1, bytes.toArray))\n      .emap(UInt256.from(_).left.map(e => DecodingFailure(e.msg)))\n  given uint256bigintByteEncoder: ByteEncoder[UInt256BigInt] = _.toBytes\n\n\n  @SuppressWarnings(Array(\"org.wartremover.warts.OptionPartial\"))\n  val EmptyBytes: UInt256Bytes = UInt256.from(ByteVector.low(32)).toOption.get\n"
  },
  {
    "path": "modules/lib/shared/src/main/scala/io/leisuremeta/chain/lib/datatype/Utf8.scala",
    "content": "package io.leisuremeta.chain.lib\npackage datatype\n\nimport java.nio.charset.{CharacterCodingException, StandardCharsets}\n\nimport cats.Eq\n\nimport io.circe.{Decoder, Encoder, KeyDecoder, KeyEncoder}\nimport scodec.bits.ByteVector\n\nimport codec.byte.{ByteDecoder, ByteEncoder}\nimport failure.DecodingFailure\n\nopaque type Utf8 = String\nobject Utf8:\n  def from(s: String): Either[CharacterCodingException, Utf8] =\n    ByteVector.encodeUtf8(s).map(_ => s)\n\n  @SuppressWarnings(Array(\"org.wartremover.warts.Throw\"))\n  def unsafeFrom(s: String): Utf8 = from(s).fold(e => throw e, identity)\n\n  extension (u: Utf8)\n    def value: String = u\n    def bytes: ByteVector = ByteVector.view(u.getBytes(StandardCharsets.UTF_8))\n\n  given eq: Eq[Utf8] = Eq.fromUniversalEquals\n\n  given utf8CirceDecoder: Decoder[Utf8] =\n    Decoder.decodeString.emap(from(_).left.map(_.getMessage))\n  given utf8CirceEncoder: Encoder[Utf8] = Encoder.encodeString\n\n  given utf8CirceKeyDecoder: KeyDecoder[Utf8] =\n    KeyDecoder.instance(from(_).toOption)\n  given utf8CirceKeyEncoder: KeyEncoder[Utf8] = KeyEncoder.encodeKeyString\n\n  given utf8ByteDecoder: ByteDecoder[Utf8] = ByteDecoder[BigNat].flatMap { (b: BigNat) =>\n    ByteDecoder\n      .fromFixedSizeBytes[ByteVector](b.toBigInt.toLong)(identity)\n      .emap(_.decodeUtf8.left.map { e => DecodingFailure(e.getMessage) })\n  }\n  @SuppressWarnings(Array(\"org.wartremover.warts.Throw\"))\n  given utf8ByteEncoder: ByteEncoder[Utf8] = (utf8: String) =>\n    val encoded = ByteVector.encodeUtf8(utf8) match\n      case Right(v) => v\n      case Left(e) => throw e\n    ByteEncoder[BigNat].encode(BigNat.unsafeFromLong(encoded.size)) ++ encoded\n"
  },
  {
    "path": "modules/lib/shared/src/main/scala/io/leisuremeta/chain/lib/failure/LmChainFailure.scala",
    "content": "package io.leisuremeta.chain.lib.failure\n\nimport scala.util.control.NoStackTrace\n\nsealed trait LmChainFailure extends NoStackTrace:\n  def msg: String\n\nfinal case class EncodingFailure(msg: String) extends LmChainFailure\nfinal case class DecodingFailure(msg: String) extends LmChainFailure\n\nfinal case class UInt256RefineFailure(msg: String) extends LmChainFailure\n"
  },
  {
    "path": "modules/lib/shared/src/main/scala/io/leisuremeta/chain/lib/merkle/MerkleTrie.scala",
    "content": "package io.leisuremeta.chain.lib\npackage merkle\n\nimport cats.Monad\nimport cats.data.{EitherT, Kleisli, OptionT, StateT}\nimport cats.syntax.eq.given\n\nimport fs2.Stream\nimport io.github.iltotore.iron.*\nimport scodec.bits.{BitVector, ByteVector}\n\nimport crypto.Hash.ops.*\nimport MerkleTrieNode.{Children, MerkleHash}\n\nobject MerkleTrie:\n\n  type NodeStore[F[_]] =\n    Kleisli[EitherT[F, String, *], MerkleHash, Option[MerkleTrieNode]]\n\n  @SuppressWarnings(Array(\"org.wartremover.warts.Recursion\"))\n  def get[F[_]: Monad: NodeStore](\n      key: Nibbles,\n  ): StateT[EitherT[F, String, *], MerkleTrieState, Option[ByteVector]] =\n    StateT.inspectF: (state: MerkleTrieState) =>\n      val optionT = for\n        node <- OptionT(getNode[F](state))\n        stripped <- OptionT.fromOption[EitherT[F, String, *]]:\n          key.stripPrefix(node.prefix)\n        value <- stripped.unCons\n          .fold:\n            OptionT.fromOption[EitherT[F, String, *]](node.getValue)\n          .apply: (head, remainder) =>\n            for\n              children <- OptionT.fromOption[EitherT[F, String, *]]:\n                node.getChildren\n              nextRoot <- OptionT.fromOption[EitherT[F, String, *]]:\n                children(head)\n              value <- OptionT:\n                get[F](remainder.assumeNibbles).runA:\n                  state.copy(root = Some(nextRoot))\n            yield value\n      yield value\n\n      optionT.value\n\n  @SuppressWarnings(Array(\"org.wartremover.warts.Recursion\"))\n  def put[F[_]: Monad: NodeStore](\n      key: Nibbles,\n      value: ByteVector,\n  ): StateT[EitherT[F, String, *], MerkleTrieState, Unit] =\n    StateT.modifyF: (state: MerkleTrieState) =>\n      getNodeAndStateRoot[F](state).flatMap:\n        case None =>\n          val leaf     = MerkleTrieNode.leaf(key, value)\n          val leafHash = leaf.toHash\n          EitherT.rightT[F, String]:\n            state.copy(\n              root = Some(leafHash),\n              diff = state.diff.add(leafHash, leaf),\n            )\n        case Some((node, root)) =>\n          val prefix0: Nibbles = node.prefix\n          val (commonPrefix, remainder0, remainder1) =\n            getCommonPrefixNibbleAndRemainders(prefix0, key)\n\n          def putLeaf(value0: ByteVector): EitherT[F, String, MerkleTrieState] =\n            EitherT.rightT[F, String]:\n              (remainder0.unCons, remainder1.unCons) match\n                case (None, None) =>\n                  if value0 === value then state\n                  else\n                    val leaf1     = MerkleTrieNode.leaf(prefix0, value)\n                    val leaf1Hash = leaf1.toHash\n                    state.copy(\n                      root = Some(leaf1Hash),\n                      diff = state.diff\n                        .remove(root, node)\n                        .add(leaf1Hash, leaf1),\n                    )\n                case (None, Some((index10, prefix10))) =>\n                  val leaf1 = MerkleTrieNode.leaf(prefix10.assumeNibbles, value)\n                  val leaf1Hash = leaf1.toHash\n                  val children: Children = Children.empty\n                    .updateChild(index10, Some(leaf1Hash))\n                  val branch = MerkleTrieNode.branchWithData(\n                    node.prefix,\n                    children,\n                    value0,\n                  )\n                  val branchHash = branch.toHash\n                  state.copy(\n                    root = Some(branchHash),\n                    diff = state.diff\n                      .remove(root, node)\n                      .add(branchHash, branch)\n                      .add(leaf1Hash, leaf1),\n                  )\n                case (Some((index00, prefix00)), None) =>\n                  val leaf0 =\n                    MerkleTrieNode.leaf(prefix00.assumeNibbles, value0)\n                  val leaf0Hash = leaf0.toHash\n                  val children: Children = Children.empty\n                    .updateChild(index00, Some(leaf0Hash))\n                  val branch = MerkleTrieNode.branchWithData(\n                    commonPrefix,\n                    children,\n                    value,\n                  )\n                  val branchHash = branch.toHash\n                  state.copy(\n                    root = Some(branchHash),\n                    diff = state.diff\n                      .remove(root, node)\n                      .add(branchHash, branch)\n                      .add(leaf0Hash, leaf0),\n                  )\n                case (Some((index00, prefix00)), Some((index10, prefix10))) =>\n                  val leaf0 =\n                    MerkleTrieNode.leaf(prefix00.assumeNibbles, value0)\n                  val leaf0Hash = leaf0.toHash\n                  val leaf1 = MerkleTrieNode.leaf(prefix10.assumeNibbles, value)\n                  val leaf1Hash = leaf1.toHash\n                  val children: Children = Children.empty\n                    .updateChild(index00, Some(leaf0Hash))\n                    .updateChild(index10, Some(leaf1Hash))\n                  val branch = MerkleTrieNode.branch(\n                    commonPrefix,\n                    children,\n                  )\n                  val branchHash = branch.toHash\n                  state.copy(\n                    root = Some(branchHash),\n                    diff = state.diff\n                      .remove(root, node)\n                      .add(branchHash, branch)\n                      .add(leaf0Hash, leaf0)\n                      .add(leaf1Hash, leaf1),\n                  )\n          def putNode(\n              children: MerkleTrieNode.Children,\n          ): EitherT[F, String, MerkleTrieState] =\n            (remainder0.unCons, remainder1.unCons) match\n              case (None, None) =>\n                // key is equal to prefix0, so we need to update node value\n                node.getValue match\n                  case Some(nodeValue) if nodeValue === value =>\n                    EitherT.rightT[F, String](state)\n                  case _ =>\n                    val branch1     = node.setValue(value)\n                    val branch1Hash = branch1.toHash\n                    EitherT.rightT[F, String]:\n                      state.copy(\n                        root = Some(branch1Hash),\n                        diff = state.diff\n                          .remove(root, node)\n                          .add(branch1Hash, branch1),\n                      )\n              case (None, Some((index10, prefix10))) =>\n                // key is starting with prefix0, but not equal to it\n                children(index10) match\n                  case None =>\n                    // child is empty, so we need to create a new leaf\n                    val leaf1 =\n                      MerkleTrieNode.leaf(prefix10.assumeNibbles, value)\n                    val leaf1Hash = leaf1.toHash\n                    val children1 =\n                      children.updateChild(index10, Some(leaf1Hash))\n                    val branch1     = node.setChildren(children1)\n                    val branch1Hash = branch1.toHash\n                    EitherT.rightT[F, String]:\n                      state.copy(\n                        root = Some(branch1Hash),\n                        diff = state.diff\n                          .remove(root, node)\n                          .add(branch1Hash, branch1)\n                          .add(leaf1Hash, leaf1),\n                      )\n                  case Some(childHash) =>\n                    // child is not empty, so we need to update the child recursively\n                    put[F](prefix10.assumeNibbles, value)\n                      .runS(state.copy(root = Some(childHash)))\n                      .map: childState =>\n//                      println(s\"======> Child state: $childState\")\n                        val children1 = children\n                          .updateChild(index10, childState.root)\n                        val branch1     = node.setChildren(children1)\n                        val branch1Hash = branch1.toHash\n                        childState.copy(\n                          root = Some(branch1Hash),\n                          diff = childState.diff\n                            .remove(root, node)\n                            .add(branch1Hash, branch1),\n                        )\n              case (Some((index00, prefix00)), None) =>\n                // prefix is larger than key\n                val child0     = node.setPrefix(prefix00.assumeNibbles)\n                val child0Hash = child0.toHash\n                val children1 = MerkleTrieNode.Children.empty\n                  .updateChild(index00, Some(child0Hash))\n                val branch1 = MerkleTrieNode.branchWithData(\n                  commonPrefix,\n                  children1,\n                  value,\n                )\n                val branch1Hash = branch1.toHash\n                EitherT.rightT[F, String]:\n                  state.copy(\n                    root = Some(branch1Hash),\n                    diff = state.diff\n                      .remove(root, node)\n                      .add(branch1Hash, branch1)\n                      .add(child0Hash, child0),\n                  )\n              case (Some((index00, prefix00)), Some((index10, prefix10))) =>\n                val child0     = node.setPrefix(prefix00.assumeNibbles)\n                val child0Hash = child0.toHash\n                val child1 = MerkleTrieNode.leaf(prefix10.assumeNibbles, value)\n                val child1Hash = child1.toHash\n                val children1 = Children.empty\n                  .updateChild(index00, Some(child0Hash))\n                  .updateChild(index10, Some(child1Hash))\n                val branch1 = MerkleTrieNode.branch(\n                  commonPrefix,\n                  children1,\n                )\n                val branch1Hash = branch1.toHash\n                EitherT.rightT[F, String]:\n                  state.copy(\n                    root = Some(branch1Hash),\n                    diff = state.diff\n                      .remove(root, node)\n                      .add(branch1Hash, branch1)\n                      .add(child0Hash, child0)\n                      .add(child1Hash, child1),\n                  )\n\n          node match\n            case MerkleTrieNode.Leaf(_, value0) =>\n              putLeaf(value0)\n            case MerkleTrieNode.Branch(_, children) =>\n              putNode(children)\n            case MerkleTrieNode.BranchWithData(_, children, _) =>\n              putNode(children)\n\n  @SuppressWarnings(Array(\"org.wartremover.warts.Recursion\"))\n  def remove[F[_]: Monad: NodeStore](\n      key: Nibbles,\n  ): StateT[EitherT[F, String, *], MerkleTrieState, Boolean] =\n    type ErrorOrF[A] = EitherT[F, String, A]\n    StateT: (state: MerkleTrieState) =>\n      val optionT = OptionT(getNodeAndStateRoot[F](state)).flatMap:\n        case ((node, root)) =>\n          node match\n            case MerkleTrieNode.Leaf(prefix, _) =>\n              OptionT.when(prefix === key):\n                state.copy(root = None, diff = state.diff.remove(root, node))\n            case MerkleTrieNode.BranchWithData(prefix, children, _)\n                if prefix === key =>\n              val branch1: MerkleTrieNode =\n                MerkleTrieNode.Branch(prefix, children)\n              val branch1Hash = branch1.toHash\n\n              OptionT.pure[ErrorOrF]:\n                state.copy(\n                  root = Some(branch1Hash),\n                  diff = state.diff.remove(root, node).add(branch1Hash, branch1),\n                )\n            case _ =>\n              for\n                stripped <- OptionT.fromOption[ErrorOrF]:\n                  key.stripPrefix(node.prefix)\n                (index1, key1) <- OptionT.fromOption[ErrorOrF]:\n                  stripped.unCons\n                children <- OptionT.fromOption[ErrorOrF]:\n                  node.getChildren\n                childHash <- OptionT.fromOption[ErrorOrF]:\n                  children(index1)\n                childStateAndResult <- OptionT.liftF:\n                  remove[F](key1.assumeNibbles).run:\n                    state.copy(root = Some(childHash))\n                (childState, result) = childStateAndResult\n                state1 <- OptionT.when[ErrorOrF, MerkleTrieState](result):\n                  val needToRemoveSelf = childState.root.isEmpty\n                    && children.count(_.nonEmpty) <= 1\n                    && node.getValue.isEmpty\n                  if needToRemoveSelf then\n                    childState.copy(\n                      root = None,\n                      diff = childState.diff.remove(root, node),\n                    )\n                  else\n                    val children1 =\n                      children.updateChild(index1, childState.root)\n                    val branch     = node.setChildren(children1)\n                    val branchHash = branch.toHash\n                    childState.copy(\n                      root = Some(branchHash),\n                      diff = childState.diff\n                        .remove(root, node)\n                        .add(branchHash, branch),\n                    )\n              yield state1\n      optionT.value.map(_.fold((state, false))((_, true)))\n\n//  @SuppressWarnings(Array(\"org.wartremover.warts.Var\"))\n//  var count = 0L\n\n  @SuppressWarnings(Array(\"org.wartremover.warts.Overloading\"))\n  def streamFrom[F[_]: Monad: NodeStore](\n      key: Nibbles,\n  ): StateT[EitherT[F, String, *], MerkleTrieState, Stream[\n    EitherT[F, String, *],\n    (Nibbles, ByteVector),\n  ]] =\n    streamFrom[F](key, key)\n\n  @SuppressWarnings(Array(\"org.wartremover.warts.Recursion\"))\n  def streamFrom[F[_]: Monad: NodeStore](\n      key: Nibbles,\n      originalKey: Nibbles,\n  ): StateT[EitherT[F, String, *], MerkleTrieState, Stream[\n    EitherT[F, String, *],\n    (Nibbles, ByteVector),\n  ]] =\n    type ErrorOrF[A] = EitherT[F, String, A]\n//    scribe.info(s\"\"\"#${count}\\t streamFrom: \"${key.value.toHex}\", originalKey: \"${originalKey.value.toHex}\"\"\"\")\n//    scribe.info(s\"\"\"#${count}\\t originalKey UTF8: \"${originalKey.value.bytes.decodeUtf8}\"\"\"\")\n//    count += 1L\n    StateT.inspectF: (state: MerkleTrieState) =>\n      scribe.debug(s\"from: $key, $state\")\n\n      def branchStream(\n          prefix: Nibbles,\n          children: MerkleTrieNode.Children,\n          value: Option[ByteVector],\n      ): OptionT[ErrorOrF, Stream[ErrorOrF, (Nibbles, ByteVector)]] =\n        def runFrom(key: Nibbles)(\n            hashWithIndex: (Option[MerkleHash], Int),\n        ): Stream[EitherT[F, String, *], (Nibbles, ByteVector)] =\n          Stream\n            .eval:\n              streamFrom(key, originalKey)\n                .runA(state.copy(root = hashWithIndex._1))\n            .flatten\n            .map: (key, a) =>\n              val indexNibble =\n                BitVector.fromInt(hashWithIndex._2, 4).assumeNibbles\n              val key1 = Nibbles.combine(prefix, indexNibble, key)\n              (key1, a)\n\n//        scribe.info(s\"Branch: $key <= $prefix: ${key <= prefix}\")\n        if key <= prefix then\n          // key is less than or equal to prefix, so all of its value and children should be included\n          val initialValue: Stream[ErrorOrF, (Nibbles, ByteVector)] =\n            value.fold(Stream.empty): bytes =>\n              Stream.eval(EitherT.pure((prefix, bytes)))\n          val tailStream = Stream\n            .emits:\n              children.toList.zipWithIndex.filter(_._1.nonEmpty)\n            .flatMap(runFrom(Nibbles.empty))\n\n          OptionT.liftF:\n            EitherT.rightT[F, String]:\n              initialValue ++ tailStream\n        else\n          for\n            keyRemainder <- OptionT.fromOption[ErrorOrF]:\n              // if key is not starting with prefix (fail to strip) there is no stream to return\n              key.stripPrefix(prefix)\n            (index1, key1) <- OptionT.fromOption[ErrorOrF]:\n              keyRemainder.unCons // keyRemainder is not empty here (key > prefix)\n            targetChildrenWithIndex = children.toList.zipWithIndex.drop(index1)\n            stream <- OptionT.liftF:\n              EitherT.rightT[F, String]:\n                targetChildrenWithIndex match\n                  case Nil => Stream.empty\n                  case x :: xs =>\n                    val head =\n                      Stream.emit(x).flatMap(runFrom(key1.assumeNibbles))\n                    val tail = Stream\n                      .emits(xs.filter(_._1.nonEmpty))\n                      .flatMap(runFrom(Nibbles.empty))\n                    head ++ tail\n          yield stream\n\n      OptionT(getNode[F](state))\n        .flatMap:\n          case MerkleTrieNode.Leaf(prefix, value) =>\n//            scribe.info(s\"Leaf: $key <= $prefix: ${key <= prefix}\")\n//            scribe.info(s\"#$count\\tLeaf: ${prefix.value.toHex}\")\n            OptionT.when(key <= prefix):\n              Stream.emit((prefix, value))\n          case MerkleTrieNode.Branch(prefix, children) =>\n//            scribe.info(s\"#$count\\tBranch: ${prefix.value.toHex}\")\n//            scribe.info(s\"Children Size: ${children.flatten.size}\")\n            branchStream(prefix, children, None)\n          case MerkleTrieNode.BranchWithData(prefix, children, value) =>\n//            scribe.info(s\"#$count\\tBranch: ${prefix.value.toHex}\")\n//            scribe.info(s\"Children Size: ${children.flatten.size}\")\n            branchStream(prefix, children, Some(value))\n        .value\n        .map:\n          _.getOrElse:\n//            scribe.info(s\"#${count}\\tNo node found for key: ${key.value.toHex}\")\n            Stream.empty\n\n  /** @param keyPrefix:\n    *   the key prefix to get the stream from. This prefix must be included.\n    * @param keySuffix:\n    *   optional key suffix. If this suffix is provided, the stream's key\n    *   iterates over values less than keyPrefix + keySuffix.\n    * @return\n    *   the stream of values starting with the given key prefix.\n    */\n  @SuppressWarnings(Array(\"org.wartremover.warts.Recursion\"))\n  def reverseStreamFrom[F[_]: Monad: NodeStore](\n      keyPrefix: Nibbles,\n      keySuffix: Option[Nibbles],\n  ): StateT[EitherT[F, String, *], MerkleTrieState, Stream[\n    EitherT[F, String, *],\n    (Nibbles, ByteVector),\n  ]] =\n    StateT.inspectF: (state: MerkleTrieState) =>\n      scribe.debug(s\"from: ($keyPrefix, $keySuffix): $state\")\n      def reverseBranchStream(\n          prefix: Nibbles,\n          children: MerkleTrieNode.Children,\n          value: Option[ByteVector],\n      ): OptionT[EitherT[F, String, *], Stream[\n        EitherT[F, String, *],\n        (Nibbles, ByteVector),\n      ]] =\n\n        def reverseRunFrom(keyPrefix: Nibbles, keySuffix: Option[Nibbles])(\n            hashWithIndex: (Option[MerkleHash], Int),\n        ): Stream[EitherT[F, String, *], (Nibbles, ByteVector)] =\n//          scribe.info(s\"reverseRunFrom: $key, $hashWithIndex\")\n          Stream\n            .eval:\n              reverseStreamFrom(keyPrefix, keySuffix)\n                .runA(state.copy(root = hashWithIndex._1))\n            .flatten\n            .map: (key, a) =>\n              val indexNibble =\n                BitVector.fromInt(hashWithIndex._2, 4).assumeNibbles\n              val key1 = Nibbles.combine(prefix, indexNibble, key)\n              (key1, a)\n\n        def reverseRunAllOptionT =\n          val lastStream =\n            value.fold(Stream.empty): bytes =>\n              Stream.eval:\n                EitherT.rightT[F, String]:\n                  (prefix, bytes)\n\n          val initStream = Stream\n            .emits(children.toList.zipWithIndex.reverse)\n            .flatMap(reverseRunFrom(Nibbles.empty, None))\n\n          OptionT.liftF:\n            EitherT.rightT[F, String]:\n              initStream ++ lastStream\n\n        def streamFromKeySuffix(\n            keySuffix: Nibbles,\n        ): OptionT[EitherT[F, String, *], Stream[\n          EitherT[F, String, *],\n          (Nibbles, ByteVector),\n        ]] = keySuffix.unCons match\n          case None => reverseRunAllOptionT\n          case Some((index1, key1)) =>\n            val targetChildren = children.toList.zipWithIndex\n              .take(index1 + 1)\n              .reverse\n            targetChildren match\n              case Nil =>\n                OptionT.none\n              case x :: xs =>\n                OptionT.liftF:\n                  EitherT.rightT[F, String]:\n                    val headStream = Stream\n                      .emit(x)\n                      .flatMap:\n                        reverseRunFrom(Nibbles.empty, Some(key1.assumeNibbles))\n                    val tailStream = Stream\n                      .emits(xs.filter(_._1.nonEmpty))\n                      .flatMap:\n                        reverseRunFrom(Nibbles.empty, None)\n                    val lastStream = value.fold(Stream.empty): bytes =>\n                      Stream.emit((prefix, bytes))\n\n                    headStream ++ tailStream ++ lastStream\n\n        keyPrefix.stripPrefix(prefix) match\n\n          // keyPrefix is not starting with prefix\n          case None =>\n            prefix.stripPrefix(keyPrefix) match\n\n              // prefix is not starting with keyPrefix so we don't need to include it\n              case None => OptionT.none\n\n              // prefix is starting with keyPrefix so we need to include it\n              case Some(prefixRemainder) =>\n                keySuffix.fold(reverseRunAllOptionT): keySuffix1 =>\n                  if prefixRemainder <= keySuffix1 then reverseRunAllOptionT\n                  else\n                    // Here, keySuffix1 < prefixRemainder\n                    keySuffix1.stripPrefix(prefixRemainder) match\n                      case None => OptionT.none\n                      case Some(keySuffix2) =>\n                        streamFromKeySuffix(keySuffix2.assumeNibbles)\n          case Some(keyRemainder) =>\n            keyRemainder.unCons match\n              case None =>\n                keySuffix.fold(reverseRunAllOptionT)(streamFromKeySuffix)\n              case Some((index1, key1)) =>\n                children.toList.zipWithIndex.take(index1 + 1).reverse match\n                  case Nil => OptionT.none\n                  case x :: xs =>\n                    OptionT.liftF:\n                      EitherT.rightT[F, String]:\n                        Stream\n                          .emit(x)\n                          .flatMap:\n                            reverseRunFrom(key1.assumeNibbles, keySuffix)\n\n      OptionT(getNode[F](state))\n        .flatMap:\n          case MerkleTrieNode.Leaf(prefix, value) =>\n            prefix.stripPrefix(keyPrefix) match\n              case None => OptionT.none\n              case Some(prefixRemainder) =>\n                keySuffix match\n                  case None =>\n                    OptionT.pure:\n                      Stream.emit((prefix, value))\n                  case Some(suffix) =>\n                    OptionT.when(prefixRemainder <= suffix):\n                      Stream.emit((prefix, value))\n          case MerkleTrieNode.Branch(prefix, children) =>\n            reverseBranchStream(prefix, children, None)\n          case MerkleTrieNode.BranchWithData(prefix, children, value) =>\n            reverseBranchStream(prefix, children, Some(value))\n        .value\n        .map(_.getOrElse(Stream.empty))\n\n  def getNodeAndStateRoot[F[_]: Monad](state: MerkleTrieState)(using\n      ns: NodeStore[F],\n  ): EitherT[F, String, Option[(MerkleTrieNode, MerkleHash)]] =\n    state.root.fold(EitherT.rightT[F, String](None)): root =>\n      state.diff\n        .get(root)\n        .fold(ns.run(root).map(_.map((_, root)))): node =>\n          EitherT.rightT[F, String](Some((node, root)))\n\n  def getNode[F[_]: Monad](state: MerkleTrieState)(using\n      ns: NodeStore[F],\n  ): EitherT[F, String, Option[MerkleTrieNode]] =\n    state.root.fold(EitherT.rightT[F, String](None)): root =>\n      state.diff\n        .get(root)\n        .fold(ns.run(root)): node =>\n          EitherT.rightT[F, String](Some(node))\n\n  def getCommonPrefixNibbleAndRemainders(\n      nibbles0: Nibbles,\n      nibbles1: Nibbles,\n  ): (Nibbles, Nibbles, Nibbles) =\n    val commonPrefixNibbleSize: Int = (nibbles0.value ^ nibbles1.value)\n      .grouped(4L)\n      .takeWhile(_ === BitVector.low(4L))\n      .size\n    val nextPrefixBitSize = commonPrefixNibbleSize.toLong * 4L\n    val remainder0        = nibbles0.value drop nextPrefixBitSize\n    val (commonPrefix, remainder1) =\n      nibbles1.value splitAt nextPrefixBitSize\n    (\n      commonPrefix.assumeNibbles,\n      remainder0.assumeNibbles,\n      remainder1.assumeNibbles,\n    )\n\nend MerkleTrie\n"
  },
  {
    "path": "modules/lib/shared/src/main/scala/io/leisuremeta/chain/lib/merkle/MerkleTrieNode.scala",
    "content": "package io.leisuremeta.chain.lib\npackage merkle\n\nimport scala.compiletime.constValue\n\nimport cats.Eq\n\nimport io.github.iltotore.iron.*\nimport io.github.iltotore.iron.constraint.collection.*\nimport scodec.bits.{BitVector, ByteVector}\n\nimport codec.byte.{ByteDecoder, ByteEncoder, DecodeResult}\nimport datatype.{BigNat, UInt256}\nimport crypto.Hash\nimport failure.DecodingFailure\n\nsealed trait MerkleTrieNode:\n\n  def prefix: Nibbles\n\n  def getChildren: Option[MerkleTrieNode.Children] = this match\n    case MerkleTrieNode.Leaf(_, _)                     => None\n    case MerkleTrieNode.Branch(_, children)            => Some(children)\n    case MerkleTrieNode.BranchWithData(_, children, _) => Some(children)\n\n  def getValue: Option[ByteVector] = this match\n    case MerkleTrieNode.Leaf(_, value)              => Some(value)\n    case MerkleTrieNode.Branch(_, _)                => None\n    case MerkleTrieNode.BranchWithData(_, _, value) => Some(value)\n\n  def setPrefix(prefix: Nibbles): MerkleTrieNode =\n    this match\n      case MerkleTrieNode.Leaf(_, value) => MerkleTrieNode.Leaf(prefix, value)\n      case MerkleTrieNode.Branch(_, children) =>\n        MerkleTrieNode.Branch(prefix, children)\n      case MerkleTrieNode.BranchWithData(_, key, value) =>\n        MerkleTrieNode.BranchWithData(prefix, key, value)\n\n  def setChildren(\n      children: MerkleTrieNode.Children,\n  ): MerkleTrieNode = this match\n    case MerkleTrieNode.Leaf(prefix, value) =>\n      MerkleTrieNode.BranchWithData(prefix, children, value)\n    case MerkleTrieNode.Branch(prefix, _) =>\n      MerkleTrieNode.Branch(prefix, children)\n    case MerkleTrieNode.BranchWithData(prefix, _, value) =>\n      MerkleTrieNode.BranchWithData(prefix, children, value)\n\n  def setValue(value: ByteVector): MerkleTrieNode = this match\n    case MerkleTrieNode.Leaf(prefix, _) => MerkleTrieNode.Leaf(prefix, value)\n    case MerkleTrieNode.Branch(prefix, children) =>\n      MerkleTrieNode.BranchWithData(prefix, children, value)\n    case MerkleTrieNode.BranchWithData(prefix, children, _) =>\n      MerkleTrieNode.BranchWithData(prefix, children, value)\n\n  override def toString: String =\n    @SuppressWarnings(Array(\"org.wartremover.warts.Any\"))\n    val childrenString = getChildren.fold(\"[]\"): (childrenRefined) =>\n      (0 until 16)\n        .map: i =>\n          f\"${i}%x: ${childrenRefined(i)}\"\n        .mkString(\"[\", \",\", \"]\")\n    s\"MerkleTrieNode(${prefix.value.toHex}, $childrenString, $getValue)\"\n\nobject MerkleTrieNode:\n\n  final case class Leaf(prefix: Nibbles, value: ByteVector)\n      extends MerkleTrieNode\n  final case class Branch(prefix: Nibbles, children: Children)\n      extends MerkleTrieNode\n  final case class BranchWithData(\n      prefix: Nibbles,\n      children: Children,\n      value: ByteVector,\n  ) extends MerkleTrieNode\n\n  def leaf(prefix: Nibbles, value: ByteVector): MerkleTrieNode =\n    Leaf(prefix, value)\n\n  def branch(\n      prefix: Nibbles,\n      children: Children,\n  ): MerkleTrieNode = Branch(prefix, children)\n\n  def branchWithData(\n      prefix: Nibbles,\n      children: Children,\n      value: ByteVector,\n  ): MerkleTrieNode = BranchWithData(prefix, children, value)\n\n  type MerkleHash = Hash.Value[MerkleTrieNode]\n  type MerkleRoot = MerkleHash\n\n  type Children = Vector[Option[MerkleHash]] :| ChildrenCondition\n\n  type ChildrenCondition = Length[StrictEqual[16]]\n\n  extension (c: Children)\n    def updateChild(i: Int, v: Option[MerkleHash]): Children = c.updated(i, v).assume\n  object Children:\n    inline def empty: Children = Vector.fill(16)(Option.empty[MerkleHash]).assume\n\n  given Eq[MerkleTrieNode] = Eq.fromUniversalEquals\n\n  given merkleTrieNodeEncoder: ByteEncoder[MerkleTrieNode] = node =>\n    val encodePrefix: ByteVector =\n      val prefixNibbleSize: Long = node.prefix.value.size / 4\n      ByteEncoder[BigNat].encode(\n        BigNat.unsafeFromBigInt(BigInt(prefixNibbleSize)),\n      ) ++ node.prefix.bytes\n\n    def encodeChildren(children: MerkleTrieNode.Children): ByteVector =\n      val existenceBytes = BitVector.bits(children.map(_.nonEmpty)).bytes\n      children\n        .flatMap(_.toList)\n        .foldLeft(existenceBytes)(_ ++ _.toUInt256Bytes)\n\n    def encodeValue(value: ByteVector): ByteVector =\n      ByteEncoder[BigNat].encode(\n        BigNat.unsafeFromBigInt(BigInt(value.size)),\n      ) ++ value\n    val encoded = node match\n      case Leaf(_, value) =>\n        ByteVector.fromByte(1) ++ encodePrefix ++ encodeValue(value)\n      case Branch(_, children) =>\n        ByteVector.fromByte(2) ++ encodePrefix ++ encodeChildren(children)\n      case BranchWithData(_, children, value) =>\n        ByteVector.fromByte(3) ++ encodePrefix ++ encodeChildren(\n          children,\n        ) ++ encodeValue(value)\n    encoded\n\n  given merkleTrieNodeDecoder: ByteDecoder[MerkleTrieNode] =\n    val childrenDecoder: ByteDecoder[MerkleTrieNode.Children] =\n      ByteDecoder\n        .fromFixedSizeBytes(2)(_.bits)\n        .flatMap { (existenceBits) => (bytes) =>\n          type LoopType = Either[DecodingFailure, DecodeResult[\n            Vector[Option[MerkleHash]],\n          ]]\n          @annotation.tailrec\n          @SuppressWarnings(Array(\"org.wartremover.warts.Nothing\"))\n          def loop(\n              bits: BitVector,\n              bytes: ByteVector,\n              acc: List[Option[MerkleHash]],\n          ): LoopType = bits.headOption match\n            case None =>\n              Right(DecodeResult(acc.reverse.toVector, bytes))\n            case Some(false) =>\n              loop(bits.tail, bytes, None :: acc)\n            case Some(true) =>\n              val (hashBytes, rest) = bytes.splitAt(32)\n              UInt256.from(hashBytes) match\n                case Left(err) => Left(DecodingFailure(err.msg))\n                case Right(hash) =>\n                  loop(\n                    bits.tail,\n                    rest,\n                    Some(Hash.Value[MerkleTrieNode](hash)) :: acc,\n                  )\n          end loop\n          loop(existenceBits, bytes, Nil)\n        }\n        .map(_.assume)\n\n    val valueDecoder: ByteDecoder[ByteVector] = ByteDecoder[BigNat].flatMap {\n      (size) =>\n        ByteDecoder.fromFixedSizeBytes(size.toBigInt.toLong)(identity)\n    }\n\n    ByteDecoder.byteDecoder\n      .emap: b =>\n        Either.cond(1 <= b && b <= 3, b, DecodingFailure(s\"wrong range: $b\"))\n      .flatMap:\n        case 1 =>\n          for\n            prefix <- nibblesByteDecoder\n            value  <- valueDecoder\n          yield Leaf(prefix, value)\n        case 2 =>\n          for\n            prefix   <- nibblesByteDecoder\n            children <- childrenDecoder\n          yield Branch(prefix, children)\n        case 3 =>\n          for\n            prefix   <- nibblesByteDecoder\n            children <- childrenDecoder\n            value    <- valueDecoder\n          yield BranchWithData(prefix, children, value)\n\n  given merkleTrieNodeHash: Hash[MerkleTrieNode] = Hash.build\n"
  },
  {
    "path": "modules/lib/shared/src/main/scala/io/leisuremeta/chain/lib/merkle/MerkleTrieState.scala",
    "content": "package io.leisuremeta.chain.lib.merkle\n\nimport cats.syntax.eq.*\n\nimport MerkleTrieNode.MerkleRoot\n\nfinal case class MerkleTrieState(\n    root: Option[MerkleRoot],\n    base: Option[MerkleRoot],\n    diff: MerkleTrieStateDiff,\n):\n  def rebase(that: MerkleTrieState): Either[String, MerkleTrieState] =\n    def right: MerkleTrieState =\n      val map1 = this.diff.toMap\n        .map:\n          case (k, (v, count)) =>\n            val thatCount = that.diff.toMap.get(k).fold(0)(_._2)\n            (k, (v, count + thatCount))\n        .toMap\n\n      this.copy(\n        base = that.root,\n        diff = MerkleTrieStateDiff(map1),\n      )\n    end right\n\n    Either.cond(\n      this.base === that.base,\n      right,\n      s\"Different base\",\n    )\n\nobject MerkleTrieState:\n  def empty: MerkleTrieState = MerkleTrieState(\n    None,\n    None,\n    MerkleTrieStateDiff.empty,\n  )\n  def fromRoot(root: MerkleRoot): MerkleTrieState = MerkleTrieState(\n    root = Some(root),\n    base = Some(root),\n    diff = MerkleTrieStateDiff.empty,\n  )\n  def fromRootOption(root: Option[MerkleRoot]): MerkleTrieState =\n    MerkleTrieState(\n      root = root,\n      base = root,\n      diff = MerkleTrieStateDiff.empty,\n    )\n"
  },
  {
    "path": "modules/lib/shared/src/main/scala/io/leisuremeta/chain/lib/merkle/MerkleTrieStateDiff.scala",
    "content": "package io.leisuremeta.chain.lib.merkle\n\nimport MerkleTrieNode.MerkleHash\n\nopaque type MerkleTrieStateDiff = Map[MerkleHash, (MerkleTrieNode, Int)]\n\nobject MerkleTrieStateDiff:\n\n  def apply(\n      map: Map[MerkleHash, (MerkleTrieNode, Int)],\n  ): MerkleTrieStateDiff = map\n\n  def empty: MerkleTrieStateDiff = Map.empty\n\n  extension (diff: MerkleTrieStateDiff)\n    def get(hash: MerkleHash): Option[MerkleTrieNode] =\n      diff.get(hash).flatMap {\n        case (node, count) if count > 0 => Some(node)\n        case _                          => None\n      }\n\n    def foreach(f: (MerkleHash, MerkleTrieNode) => Unit): Unit =\n      for\n        diffItem <- diff\n        (hash, (node, count)) = diffItem if count > 0\n      yield f(hash, node)\n      ()\n\n    def add(\n        hash: MerkleHash,\n        node: MerkleTrieNode,\n    ): MerkleTrieStateDiff =\n      diff.get(hash).fold(diff + (hash -> (node, 1))) {\n        case (node, -1)    => diff - hash\n        case (node, count) => diff + (hash -> (node, count + 1))\n      }\n\n    def remove(\n        hash: MerkleHash,\n        node: MerkleTrieNode,\n    ): MerkleTrieStateDiff =\n      diff.get(hash).fold(diff + (hash -> (node, -1))) {\n        case (node, 1)     => diff - hash\n        case (node, count) => diff + (hash -> (node, count - 1))\n      }\n\n    def toList: List[(MerkleHash, (MerkleTrieNode, Int))] = diff.toList\n\n    def toMap: Map[MerkleHash, (MerkleTrieNode, Int)] = diff\n  end extension\n"
  },
  {
    "path": "modules/lib/shared/src/main/scala/io/leisuremeta/chain/lib/merkle/package.scala",
    "content": "package io.leisuremeta.chain.lib\npackage merkle\n\nimport scala.compiletime.{summonInline}\n\nimport cats.Eq\nimport cats.syntax.either.*\nimport cats.syntax.eq.given\n\nimport io.github.iltotore.iron.*\nimport io.github.iltotore.iron.constraint.collection.*\nimport io.github.iltotore.iron.constraint.numeric.*\n\nimport scodec.bits.{BitVector, ByteVector}\n\nimport codec.byte.{ByteDecoder, ByteEncoder}\nimport codec.byte.ByteEncoder.ops.*\nimport datatype.BigNat\nimport failure.DecodingFailure\nimport util.iron.given\n\nopaque type Nibbles = BitVector :| Nibbles.NibbleCond\n\nobject Nibbles:\n  type NibbleCond = Length[Multiple[4L]]\n  val empty: Nibbles = BitVector.empty.assumeNibbles\n  def combine(nibbles: Nibbles*): Nibbles =\n    nibbles.foldLeft(BitVector.empty)(_ ++ _.value).assumeNibbles\n\nextension (nibbles: Nibbles)\n  def value: BitVector  = nibbles\n  def bytes: ByteVector = nibbles.bytes\n  def nibbleSize: Long  = nibbles.size / 4L\n  def unCons: Option[(Int, Nibbles)] =\n    if nibbles.isEmpty then None\n    else\n      val head = nibbles.value.take(4).toInt(signed = false)\n      val tail = nibbles.value.drop(4).assumeNibbles\n      Some((head, tail))\n  def stripPrefix(prefix: Nibbles): Option[Nibbles] =\n    if nibbles.startsWith(prefix) then\n      Some(nibbles.drop(prefix.size).assumeNibbles)\n    else None\n\n  def compareTo(that: Nibbles): Int =\n    val thisBytes = nibbles.bytes\n    val thatBytes = that.bytes\n    val minSize   = thisBytes.size min thatBytes.size\n\n    (0L `until` minSize)\n      .find: i =>\n        thisBytes.get(i) =!= thatBytes.get(i)\n      .fold(thisBytes.size compareTo thatBytes.size): i =>\n        (thisBytes.get(i) & 0xff) compare (thatBytes.get(i) & 0xff)\n\n  def <=(that: Nibbles): Boolean = compareTo(that) <= 0\n  def <(that: Nibbles): Boolean  = compareTo(that) < 0\n  def >=(that: Nibbles): Boolean = compareTo(that) >= 0\n  def >(that: Nibbles): Boolean  = compareTo(that) > 0\n\nextension (bitVector: BitVector)\n  def refineToNibble: Either[String, Nibbles] =\n    bitVector.refineEither[Length[Multiple[4L]]]\n  def assumeNibbles: Nibbles = bitVector.assume[Nibbles.NibbleCond]\n\nextension (byteVector: ByteVector)\n  def toNibbles: Nibbles = byteVector.bits.refineUnsafe[Length[Multiple[4L]]]\n\ngiven nibblesByteEncoder: ByteEncoder[Nibbles] = (nibbles: Nibbles) =>\n  BigNat.unsafeFromLong(nibbles.size / 4).toBytes ++ nibbles.bytes\n\ngiven nibblesByteDecoder: ByteDecoder[Nibbles] =\n  ByteDecoder[BigNat].flatMap: nibbleSize =>\n    val nibbleSizeLong = nibbleSize.toBigInt.toLong\n    ByteDecoder\n      .fromFixedSizeBytes((nibbleSizeLong + 1) / 2): nibbleBytes =>\n        val bitsSize = nibbleSizeLong * 4\n        val padSize  = bitsSize - nibbleBytes.size * 8\n        val nibbleBits =\n          if padSize > 0 then nibbleBytes.bits.padLeft(padSize)\n          else nibbleBytes.bits\n        nibbleBits.take(bitsSize)\n      .emap(_.refineToNibble.leftMap(DecodingFailure(_)))\n\ngiven nibblesEq: Eq[Nibbles] = Eq.fromUniversalEquals\n"
  },
  {
    "path": "modules/lib/shared/src/main/scala/io/leisuremeta/chain/lib/util/iron/package.scala",
    "content": "package io.leisuremeta.chain.lib.util.iron\n\nimport scala.compiletime.{summonInline}\n\nimport io.github.iltotore.iron.*\nimport io.github.iltotore.iron.constraint.collection.*\n\nimport scodec.bits.{BitVector, ByteVector}\n\n@SuppressWarnings(Array(\"org.wartremover.warts.ImplicitParameter\"))\nclass LengthBitVector[C, Impl <: Constraint[Long, C]](using Impl)\n    extends Constraint[BitVector, Length[C]]:\n  override inline def test(value: BitVector): Boolean =\n    summonInline[Impl].test(value.size)\n  override inline def message: String =\n    s\"Length: (${summonInline[Impl].message})\"\n\n@SuppressWarnings(Array(\"org.wartremover.warts.ImplicitParameter\"))\ninline given [C, Impl <: Constraint[Long, C]](using\n    inline impl: Impl,\n): LengthBitVector[C, Impl] = new LengthBitVector[C, Impl]\n\n@SuppressWarnings(Array(\"org.wartremover.warts.ImplicitParameter\"))\nclass LengthByteVector[C, Impl <: Constraint[Long, C]](using Impl)\n    extends Constraint[ByteVector, Length[C]]:\n  override inline def test(value: ByteVector): Boolean =\n    summonInline[Impl].test(value.size)\n  override inline def message: String =\n    s\"Length: (${summonInline[Impl].message})\"\n  \n@SuppressWarnings(Array(\"org.wartremover.warts.ImplicitParameter\"))\ninline given [C, Impl <: Constraint[Long, C]](using\n    inline impl: Impl,\n): LengthByteVector[C, Impl] = new LengthByteVector[C, Impl]\n"
  },
  {
    "path": "modules/lib/shared/src/main/scala/io/leisuremeta/chain/lib/util/refined/bitVector.scala",
    "content": "package io.leisuremeta.chain.lib.util.refined\n\nimport eu.timepit.refined.api.Validate\nimport eu.timepit.refined.collection.Size\nimport eu.timepit.refined.internal.Resources\nimport scodec.bits.BitVector\n\nobject bitVector extends BitVectorValidate\n\nprivate[refined] trait BitVectorValidate:\n  given bitVectorSizeValidate[P, RP](using\n      v: Validate.Aux[Long, P, RP],\n  ): Validate.Aux[BitVector, Size[P], Size[v.Res]] =\n    new Validate[BitVector, Size[P]]:\n\n      override type R = Size[v.Res]\n\n      override def validate(t: BitVector): Res =\n        val r = v.validate(t.size)\n        r.as(Size(r))\n\n      override def showExpr(t: BitVector): String = v.showExpr(t.size)\n\n      override def showResult(t: BitVector, r: Res): String =\n        val size   = t.size\n        val nested = v.showResult(size, r.detail.p)\n        Resources.predicateTakingResultDetail(s\"size($t) = $size\", r, nested)\n"
  },
  {
    "path": "modules/lib/shared/src/test/scala/io/leisuremeta/chain/lib/codec/ByteCodecTest.scala",
    "content": "package io.leisuremeta.chain.lib\npackage codec\n\nimport hedgehog.munit.HedgehogSuite\nimport hedgehog.*\n\nimport codec.byte.{ByteDecoder, ByteEncoder, DecodeResult}\n\nclass ByteCodecTest extends HedgehogSuite:\n\n  property(\"roundtrip of bigint byte codec\") {\n    for\n      bignat <- Gen\n        .bytes(Range.linear(1, 64))\n        .map(BigInt(_))\n        .forAll\n    yield\n      val encoded = ByteEncoder[BigInt].encode(bignat)\n\n      ByteDecoder[BigInt].decode(encoded) match\n        case Right(DecodeResult(decoded, remainder)) =>\n          Result.all(\n            List(\n              decoded ==== bignat,\n              Result.assert(remainder.isEmpty),\n            ),\n          )\n        case _ => Result.failure\n  }\n"
  },
  {
    "path": "modules/lib/shared/src/test/scala/io/leisuremeta/chain/lib/crypto/CryptoOpsTest.scala",
    "content": "package io.leisuremeta.chain.lib\npackage crypto\n\nimport hedgehog.munit.HedgehogSuite\nimport hedgehog.*\n\nimport scodec.bits.*\n\nclass CryptoOpsTest extends HedgehogSuite:\n\n  test(\"keccak256 #1\") {\n    withMunitAssertions { assertions =>\n      val expected =\n        hex\"c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470\"\n      val result = ByteVector.view(CryptoOps.keccak256(Array.empty))\n\n      assertions.assertEquals(result, expected)\n    }\n  }\n\n  test(\"keccak256 #2\") {\n    withMunitAssertions { assertions =>\n      val expected =\n        hex\"4d741b6f1eb29cb2a9b9911c82f56fa8d73b04959d3d9d222895df6c0b28aa15\"\n      val result = ByteVector.view(\n        CryptoOps.keccak256(\n          \"The quick brown fox jumps over the lazy dog\".getBytes(),\n        ),\n      )\n      assertions.assertEquals(result, expected)\n    }\n  }\n\n  test(\"keccak256 #3\") {\n    withMunitAssertions { assertions =>\n      val expected =\n        hex\"578951e24efd62a3d63a86f7cd19aaa53c898fe287d2552133220370240b572d\"\n      val result = ByteVector.view(\n        CryptoOps.keccak256(\n          \"The quick brown fox jumps over the lazy dog.\".getBytes(),\n        ),\n      )\n      assertions.assertEquals(result, expected)\n    }\n  }\n\n  test(\"keypair\") {\n    withMunitAssertions { assertions =>\n      val keyPair = CryptoOps.fromPrivate(\n        BigInt(\n          \"10e93a6c964aa6bc089f84e4fe3fb37583f3e1162891a689dd99bb629520f3df\",\n          16,\n        ),\n      )\n      val expected =\n        hex\"e72699136b12ffd11549616ff047cd5ec93665cd6f13b859030a3c99d14842abc27a7442bc05143db53c41407a7059c85def28f6749b86b3123c48be3085e459\"\n\n      assertions.assertEquals(keyPair.publicKey.toBytes, expected)\n    }\n  }\n"
  },
  {
    "path": "modules/lib/shared/src/test/scala/io/leisuremeta/chain/lib/datatype/BigNatTest.scala",
    "content": "package io.leisuremeta.chain.lib\npackage datatype\n\n//import eu.timepit.refined.auto.autoUnwrap\nimport io.circe.Decoder\n//import io.circe.generic.auto.given\n//import io.circe.refined.given\nimport io.circe.syntax.given\n\nimport hedgehog.munit.HedgehogSuite\nimport hedgehog.*\n\nimport codec.byte.{ByteDecoder, ByteEncoder, DecodeResult}\n\nclass BigNatTest extends HedgehogSuite:\n\n  property(\"roundtrip of bignat byte codec\") {\n    for\n      bignat <- Gen\n        .bytes(Range.linear(0, 64))\n        .map(BigInt(1, _))\n        .map(BigNat.unsafeFromBigInt)\n        .forAll\n    yield\n      val encoded = ByteEncoder[BigNat].encode(bignat)\n\n      ByteDecoder[BigNat].decode(encoded) match\n        case Right(DecodeResult(decoded, remainder)) =>\n          Result.all(\n            List(\n              decoded ==== bignat,\n              Result.assert(remainder.isEmpty),\n            ),\n          )\n        case _ => Result.failure\n  }\n\n  property(\"roundtrip of bignat circe codec\") {\n    for\n      bignat <- Gen\n        .bytes(Range.linear(0, 64))\n        .map(BigInt(1, _))\n        .map(BigNat.unsafeFromBigInt)\n        .forAll\n    yield\n      val encoded = bignat.asJson\n\n      Decoder[BigNat].decodeJson(encoded) match\n        case Right(decoded) => decoded ==== bignat\n        case _ => Result.failure\n  }\n"
  },
  {
    "path": "modules/lib/shared/src/test/scala/io/leisuremeta/chain/lib/datatype/UInt256Test.scala",
    "content": "package io.leisuremeta.chain.lib.datatype\n\nimport hedgehog.munit.HedgehogSuite\nimport hedgehog.*\nimport scodec.bits.ByteVector\n\nclass UInt256Test extends HedgehogSuite:\n\n  property(\"roundtrip of uint256bytes\") {\n    for bytes <- Gen.bytes(Range.singleton(32)).map(ByteVector.view).forAll\n    yield\n      val roundTrip =\n        for uint256bytes <- UInt256.from(bytes)\n        yield uint256bytes.toBytes\n\n      roundTrip ==== Right(bytes)\n  }\n\n  property(\"roundtrip of uint256bigint\") {\n    for bigint <- Gen.bytes(Range.singleton(32)).map(BigInt(1, _)).forAll\n    yield\n      val roundTrip =\n        for uint256bigint <- UInt256.from(bigint)\n        yield uint256bigint.toBigInt\n\n      roundTrip ==== Right(bigint)\n  }\n\n  property(\"roundtrip of uint256bytes -> uint256bigint -> uint256bytes\") {\n    for bytes <- Gen.bytes(Range.singleton(32)).map(ByteVector.view).forAll\n    yield\n      val roundTrip =\n        for\n          uint256bytes <- UInt256.from(bytes)\n          bigint = uint256bytes.toBigInt\n          uint256bigint <- UInt256.from(bigint)\n        yield uint256bigint.toBytes\n\n      roundTrip ==== Right(bytes)\n  }\n\n  property(\"roundtrip of uint256bigint -> uint256bytes -> uint256bigint\") {\n    for bigint <- Gen.bytes(Range.singleton(32)).map(BigInt(1, _)).forAll\n    yield\n      val roundTrip =\n        for\n          uint256bigint <- UInt256.from(bigint)\n          bytes = uint256bigint.toBytes\n          uint256bytes <- UInt256.from(bytes)\n        yield uint256bytes.toBigInt\n\n      roundTrip ==== Right(bigint)\n  }\n"
  },
  {
    "path": "modules/lib/shared/src/test/scala/io/leisuremeta/chain/lib/merkle/MerkleTrieNodeTest.scala",
    "content": "package io.leisuremeta.chain.lib\npackage merkle\n\nimport io.github.iltotore.iron.assume\nimport scodec.bits.ByteVector\n\nimport hedgehog.*\nimport hedgehog.munit.HedgehogSuite\n\nimport codec.byte.{ByteDecoder, ByteEncoder, DecodeResult}\nimport crypto.Hash\nimport datatype.UInt256\n\nclass MerkleTrieNodeTest extends HedgehogSuite:\n\n  val genPrefix: Gen[Nibbles] = for\n    bytes <- Gen.bytes(Range.linear(0, 64))\n    byteVector = ByteVector.view(bytes)\n    bits <- Gen.element1(\n      byteVector.bits,\n      byteVector.bits.drop(4),\n    )\n  yield bits.assumeNibbles\n\n  def genChildren: Gen[MerkleTrieNode.Children] = Gen\n    .list[Option[MerkleTrieNode.MerkleHash]](\n      Gen.frequency1(\n        1 -> Gen.constant(None),\n        9 -> Gen.bytes(Range.singleton(32)).map { (byteArray) =>\n          UInt256.from(ByteVector.view(byteArray)).toOption.map {\n            Hash.Value[MerkleTrieNode](_)\n          }\n        },\n      ),\n      Range.singleton(16),\n    ) \n    .map: (list) =>\n      list.toVector.assume[MerkleTrieNode.ChildrenCondition]\n\n  def genValue: Gen[ByteVector] =\n    Gen.sized(size => Gen.bytes(Range.linear(0, size.value.abs)),\n    ) map ByteVector.view\n\n  def genLeaf: Gen[MerkleTrieNode] = for\n    prefix <- genPrefix\n    value  <- genValue\n  yield MerkleTrieNode.Leaf(prefix, value)\n\n  def genBranch: Gen[MerkleTrieNode] = for\n    prefix   <- genPrefix\n    children <- genChildren\n  yield MerkleTrieNode.Branch(prefix, children)\n\n  def genBranchWithData: Gen[MerkleTrieNode] = for\n    prefix   <- genPrefix\n    children <- genChildren\n    value    <- genValue\n  yield MerkleTrieNode.BranchWithData(prefix, children, value)\n\n  def genMerkleTrieNode: Gen[MerkleTrieNode] = for\n    node <- Gen.choice1(\n      genLeaf,\n      genBranch,\n      genBranchWithData,\n    )\n  yield node\n\n  property(\"roundtrip of MerkleTrieNode byte codec\"):\n    for node <- genMerkleTrieNode.forAll\n    yield\n      val encoded =\n        ByteEncoder[MerkleTrieNode].encode(node)\n\n      ByteDecoder[MerkleTrieNode].decode(encoded) match\n        case Right(DecodeResult(decoded, remainder)) =>\n          Result.all(\n            List(\n              decoded ==== node,\n              Result.assert(remainder.isEmpty),\n            ),\n          )\n        case Left(error) =>\n          println(s\"=== error: ${error.msg}\")\n          println(s\"=== encoded: $encoded\")\n          Result.failure\n"
  },
  {
    "path": "modules/lib/shared/src/test/scala/io/leisuremeta/chain/lib/merkle/MerkleTrieTest.scala",
    "content": "package io.leisuremeta.chain.lib\npackage merkle\n\nimport hedgehog.munit.HedgehogSuite\nimport hedgehog.*\nimport hedgehog.state.*\n\nimport scala.collection.immutable.SortedMap\n\nimport cats.Id\nimport cats.arrow.FunctionK\nimport cats.data.{EitherT, Kleisli}\nimport cats.effect.SyncIO\nimport cats.syntax.all.*\n\nimport fs2.Stream\nimport scodec.bits.ByteVector\nimport scodec.bits.hex\n\nimport codec.byte.{ByteDecoder, ByteEncoder}\nimport datatype.BigNat\nimport MerkleTrie.NodeStore\nimport MerkleTrieNode.{MerkleHash, MerkleRoot}\n\nclass MerkleTrieTest extends HedgehogSuite:\n\n  given ByteEncoder[ByteVector] = (bytes: ByteVector) =>\n    import ByteEncoder.ops.*\n    BigNat.unsafeFromBigInt(bytes.size).toBytes ++ bytes\n\n  given ByteDecoder[ByteVector] =\n    ByteDecoder[BigNat].flatMap { size =>\n      ByteDecoder.fromFixedSizeBytes(size.toBigInt.toLong)(identity)\n    }\n\n  case class State(\n      current: SortedMap[ByteVector, ByteVector],\n      hashLog: Map[SortedMap[ByteVector, ByteVector], Option[MerkleRoot]],\n  )\n  object State:\n    def empty: State = State(SortedMap.empty, Map.empty)\n\n  case class Get(key: Nibbles)\n  case class Put(key: Nibbles, value: ByteVector)\n  case class Remove(key: Nibbles)\n  case class StreamFrom(key: Nibbles)\n  case class ReverseStreamFrom(keyPrefix: Nibbles, keySuffix: Option[Nibbles])\n\n  given emptyIdNodeStore: NodeStore[Id] = Kleisli: (_: MerkleHash) =>\n    EitherT.rightT[Id, String](None)\n\n  given emptyIoNodeStore: NodeStore[SyncIO] = Kleisli: (_: MerkleHash) =>\n    EitherT.rightT[SyncIO, String](None)\n\n  val initialState = MerkleTrieState.empty\n\n  var merkleTrieState: MerkleTrieState = initialState\n\n  val genByteVector = Gen.bytes(Range.linear(0, 64)).map(ByteVector.view)\n  def commandGet: CommandIO[State] =\n    new Command[State, Get, Option[ByteVector]]:\n\n      override def gen(s: State): Option[Gen[Get]] = Some(\n        (s.current.keys.toList match\n          case Nil => genByteVector\n          case h :: t =>\n            Gen.frequency1(\n              80 -> Gen.element(h, t),\n              20 -> genByteVector,\n            )\n        ).map(bytes => Get(bytes.toNibbles)),\n      )\n      override def execute(\n          env: Environment,\n          i: Get,\n      ): Either[String, Option[ByteVector]] =\n        val program = MerkleTrie.get[Id](i.key)\n        program.runA(merkleTrieState).value\n\n      override def update(s: State, i: Get, o: Var[Option[ByteVector]]): State =\n        s\n\n      override def ensure(\n          env: Environment,\n          s0: State,\n          s: State,\n          i: Get,\n          o: Option[ByteVector],\n      ): Result = s.current.get(i.key.bytes) ==== o\n\n  def commandPut: CommandIO[State] = new Command[State, Put, Unit]:\n\n    override def gen(s: State): Option[Gen[Put]] =\n      Some(for\n        key   <- genByteVector\n        value <- genByteVector\n      yield Put(key.toNibbles, value))\n\n    override def execute(env: Environment, i: Put): Either[String, Unit] =\n      //    println(s\"===> execute: $i\")\n\n      val program = MerkleTrie.put[Id](i.key, i.value)\n      program.runS(merkleTrieState).value.map { (newState: MerkleTrieState) =>\n        merkleTrieState = newState\n      }\n\n    override def update(s: State, i: Put, o: Var[Unit]): State =\n\n//      println(s\"===> Command Put (${i}) update: ${s.current}\")\n      val current1  = s.current + ((i.key.bytes -> i.value))\n      val stateRoot = merkleTrieState.root\n      val hashLog1  = s.hashLog + ((current1    -> stateRoot))\n\n//      println(s\"===> After Command Put(${i}) update: ${current1}\")\n      State(current1, hashLog1)\n\n    override def ensure(\n        env: Environment,\n        s0: State,\n        s: State,\n        i: Put,\n        o: Unit,\n    ): Result = Result.all(\n      List(\n        //      s0.hashLog.get(s.current).fold(Result.success) {\n        //        (rootOption: Option[MerkleRoot[K, V]]) =>\n        //          if s.hashLog.get(s.current) != Some(rootOption) then\n        //            println(s\"===> current: ${s.current}\")\n        //          s.hashLog.get(s.current) ==== Some(rootOption)\n        //      },\n        merkleTrieState.root.fold(Result.success) { (root: MerkleRoot) =>\n          val result = merkleTrieState.diff.get(root).nonEmpty\n          if result == false then\n            println(s\"====> failed: $i with state ${s0.current}\")\n          Result.assert(result)\n        },\n        s.current.get(i.key.bytes) ==== Some(i.value),\n      ),\n    )\n\n  def commandRemove: CommandIO[State] = new Command[State, Remove, Boolean]:\n\n    override def gen(s: State): Option[Gen[Remove]] = Some(\n      (s.current.keys.toList match\n        case Nil => genByteVector\n        case h :: t =>\n          Gen.frequency1(\n            80 -> Gen.element(h, t),\n            20 -> genByteVector,\n          )\n      ).map(bytes => Remove(bytes.toNibbles)),\n    )\n    override def execute(env: Environment, i: Remove): Either[String, Boolean] =\n      val program = MerkleTrie.remove[Id](i.key)\n      program.run(merkleTrieState).value.map { case (state1, result) =>\n        merkleTrieState = state1\n        result\n      }\n\n    override def update(s: State, i: Remove, o: Var[Boolean]): State =\n      val current1  = s.current - i.key.bytes\n      val stateRoot = merkleTrieState.root\n      val hashLog1  = s.hashLog + ((current1 -> stateRoot))\n      State(current1, hashLog1)\n\n    override def ensure(\n        env: Environment,\n        s0: State,\n        s: State,\n        i: Remove,\n        o: Boolean,\n    ): Result = Result.all(\n      List(\n        s0.current.contains(i.key.bytes) ==== o,\n        s.current.get(i.key.bytes) ==== None,\n      ),\n    )\n\n  type S = Stream[EitherT[Id, String, *], (Nibbles, ByteVector)]\n\n  def commandStreamFrom: CommandIO[State] = new Command[State, StreamFrom, S]:\n\n    override def gen(s: State): Option[Gen[StreamFrom]] = Some(\n      (s.current.keys.toList match\n        case Nil => genByteVector\n        case h :: t =>\n          Gen.frequency1(\n            80 -> Gen.element(h, t),\n            20 -> genByteVector,\n          )\n      ).map(bytes => StreamFrom(bytes.toNibbles)),\n    )\n    override def execute(env: Environment, i: StreamFrom): Either[String, S] =\n      val program = MerkleTrie.streamFrom[Id](i.key)\n      program.runA(merkleTrieState).value\n\n    override def update(s: State, i: StreamFrom, o: Var[S]): State = s\n\n    override def ensure(\n        env: Environment,\n        s0: State,\n        s: State,\n        i: StreamFrom,\n        o: S,\n    ): Result =\n      val toId = new FunctionK[EitherT[Id, String, *], Id]:\n        override def apply[A](fa: EitherT[Id, String, A]): Id[A] =\n          fa.value.toOption.get\n\n      val expected =\n        s.current.iteratorFrom(i.key.bytes).take(10).toList.map { (k, v) =>\n          (k.bits, v)\n        }\n\n      expected ==== o\n        .take(10)\n        .translate[EitherT[Id, String, *], Id](toId)\n        .compile\n        .toList\n\n  def commandReverseStreamFrom: CommandIO[State] =\n    new Command[State, ReverseStreamFrom, S]:\n      override def gen(s: State): Option[Gen[ReverseStreamFrom]] =\n        val keyGen = s.current.keys.toList match\n          case Nil => genByteVector\n          case h :: t =>\n            Gen.frequency1(\n              80 -> Gen.element(h, t),\n              20 -> genByteVector,\n            )\n        Some:\n          for\n            bytes <- keyGen\n            suffixSize <- Gen.frequency1(\n              80 -> Gen.int(Range.linear(0, bytes.size.toInt)),\n              20 -> Gen.constant(0),\n            )\n          yield\n            val (prefix, suffix) = bytes.splitAt(bytes.size - suffixSize)\n            val suffixOption: Option[Nibbles] = Option.when(suffixSize > 0)(suffix.toNibbles)\n            ReverseStreamFrom(prefix.toNibbles, suffixOption)\n\n      override def execute(\n          env: Environment,\n          i: ReverseStreamFrom,\n      ): Either[String, S] =\n        val program = MerkleTrie.reverseStreamFrom[Id](i.keyPrefix, i.keySuffix)\n        program.runA(merkleTrieState).value\n\n      override def update(s: State, i: ReverseStreamFrom, o: Var[S]): State = s\n\n      override def ensure(\n          env: Environment,\n          s0: State,\n          s: State,\n          i: ReverseStreamFrom,\n          o: S,\n      ): Result =\n        val toId = new FunctionK[EitherT[Id, String, *], Id]:\n          override def apply[A](fa: EitherT[Id, String, A]): Id[A] =\n            fa.value.toOption.get\n\n        val withPrefix = s.current.filter(_._1.startsWith(i.keyPrefix.bytes))\n\n        val expected = i.keySuffix\n          .fold(withPrefix): suffix =>\n            withPrefix.filter(_._1 <= i.keyPrefix.bytes ++ suffix.bytes) \n          .takeRight(10)\n          .toList\n          .reverse\n          .map: (k, v) =>\n            (k.bits, v)\n\n        val result = o\n          .take(10)\n          .translate[EitherT[Id, String, *], Id](toId)\n          .compile\n          .toList\n\n//        if expected != result then\n//          println(s\"===> ReverseStreamFrom: (${i.keyPrefix.bytes}, ${i.keySuffix.map(_.bytes)})\")\n//          println(s\"===> result: ${result}\")\n//          println(s\"===> expected: $expected\")\n\n        expected ==== result\n\n  test(\"put same key value twice expect not to change state\") {\n    withMunitAssertions { assertions =>\n      val initialState = MerkleTrieState.empty\n      val program =\n        MerkleTrie.put[Id](ByteVector.empty.toNibbles, ByteVector.empty)\n\n      val resultEitherT = for\n        state1 <- program.runS(initialState)\n        state2 <- program.runS(state1)\n      yield assertions.assertEquals(state1, state2)\n\n      resultEitherT.value\n    }\n  }\n\n  test(\"put 10 -> put empty with empty -> put 10\") {\n    withMunitAssertions { assertions =>\n      val initialState = MerkleTrieState.empty\n      val put10 =\n        MerkleTrie.put[Id](hex\"10\".toNibbles, ByteVector.empty)\n      val putEmptyWithEmpty =\n        MerkleTrie.put[Id](ByteVector.empty.toNibbles, ByteVector.empty)\n\n//    val forPrint = for\n//      state1 <- put10.runS(initialState)\n//      state2 <- putEmptyWithEmpty.runS(state1)\n//      state3 <- put10.runS(state2)\n//    yield\n//      Seq(state1, state2, state3).zipWithIndex.foreach{ (s, i) =>\n//        println(s\"====== State #${i + 1} ======\")\n//        println(s\"root: ${s.root}\")\n//        s.diff.foreach{ (hash, node) => println(s\" $hash: $node\") }\n//      }\n\n      val initialProgram = for\n        _ <- put10\n        _ <- putEmptyWithEmpty\n      yield ()\n\n      val resultEitherT = for\n        state1 <- initialProgram.runS(initialState)\n        state2 <- put10.runS(state1)\n      yield assertions.assertEquals(state1, state2)\n\n      resultEitherT.value\n    }\n  }\n\n  test(\n    \"put (10, empty) -> put (empty, empty) -> put (10, 00) -> put (10, empty)\",\n  ) {\n    withMunitAssertions { assertions =>\n\n      val initialState = MerkleTrieState.empty\n      val put10withEmpty =\n        MerkleTrie.put[Id](hex\"10\".toNibbles, ByteVector.empty)\n      val putEmptyWithEmpty =\n        MerkleTrie.put[Id](ByteVector.empty.toNibbles, ByteVector.empty)\n      val put10with10 =\n        MerkleTrie.put[Id](hex\"10\".toNibbles, hex\"10\")\n\n//    val forPrint = for\n//      state1 <- put10withEmpty.runS(initialState)\n//      _ <- EitherT.pure[Id, String](println(s\"===> state1: ${state1}\"))\n//      state2 <- putEmptyWithEmpty.runS(state1)\n//      _ <- EitherT.pure[Id, String](println(s\"===> state2: ${state2}\"))\n//      state3 <- put10with10.runS(state2)\n//      _ <- EitherT.pure[Id, String](println(s\"===> state3: ${state3}\"))\n//      state4 <- put10withEmpty.runS(state3)\n//      _ <- EitherT.pure[Id, String](println(s\"===> state4: ${state4}\"))\n//    yield\n//      Seq(state1, state2, state3, state4).zipWithIndex.foreach{ (s, i) =>\n//        println(s\"====== State #${i + 1} ======\")\n//        println(s\"root: ${s.root}\")\n//        s.diff.foreach{ (hash, node) => println(s\" $hash: $node\") }\n//      }\n\n      val program = for\n        _ <- put10withEmpty\n        _ <- putEmptyWithEmpty\n        _ <- put10with10\n        _ <- put10withEmpty\n      yield ()\n\n      program.runS(initialState).value match\n        case Right(state) =>\n          assertions.assert(state.diff.get(state.root.get).nonEmpty)\n        case Left(error) =>\n          assertions.fail(error)\n    }\n  }\n\n  test(\"put (empty, empty) -> put (00, 00) -> get (empty)\") {\n    withMunitAssertions { assertions =>\n      val initialState = MerkleTrieState.empty\n      val putEmptyWithEmpty =\n        MerkleTrie.put[Id](ByteVector.empty.toNibbles, ByteVector.empty)\n      val put00_00 =\n        MerkleTrie.put[Id](hex\"00\".toNibbles, hex\"00\")\n      val getEmpty =\n        MerkleTrie.get[Id](ByteVector.empty.toNibbles)\n\n      val program = for\n        _     <- putEmptyWithEmpty\n        _     <- put00_00\n        value <- getEmpty\n      yield assertions.assertEquals(value, Some(ByteVector.empty))\n\n//      for\n//        state1 <- putEmptyWithEmpty.runS(initialState)\n//        state2 <- put00_00.runS(state1)\n//        state3 <- getEmpty.runS(state2)\n//      yield\n//        Seq(state1, state2, state3).zipWithIndex.foreach: (s, i) =>\n//          println(s\"====== State #${i + 1} ======\")\n//          println(s\"root: ${s.root}\")\n//          s.diff.foreach{ (hash, node) => println(s\" $hash: $node\") }\n\n      program.runA(initialState).value\n    }\n  }\n\n  test(\"put 00 -> put 0000 -> put empty -> get empty\") {\n    withMunitAssertions { assertions =>\n      val initialState = MerkleTrieState.empty\n      val put00        = MerkleTrie.put[Id](hex\"00\".toNibbles, ByteVector.empty)\n      val put0000 = MerkleTrie.put[Id](hex\"0000\".toNibbles, ByteVector.empty)\n      val putEmpty =\n        MerkleTrie.put[Id](ByteVector.empty.toNibbles, ByteVector.empty)\n      val getEmpty = MerkleTrie.get[Id](ByteVector.empty.toNibbles)\n\n      val program = for\n        _     <- put00\n        _     <- put0000\n        _     <- putEmpty\n        value <- getEmpty\n      yield assertions.assertEquals(value, Some(ByteVector.empty))\n\n//    val forPrint = for\n//      state1 <- put00.runS(initialState)\n//      state2 <- put0000.runS(state1)\n//      state3 <- putEmpty.runS(state2)\n//    yield\n//      Seq(state1, state2, state3).zipWithIndex.foreach{ (s, i) =>\n//        println(s\"====== State #${i + 1} ======\")\n//        println(s\"root: ${s.root}\")\n//        s.diff.foreach{ (hash, node) => println(s\" $hash: $node\") }\n//      }\n\n      program.runA(initialState).value\n    }\n  }\n\n  test(\"put 0700 -> put 07 -> put 10 -> get empty\") {\n    withMunitAssertions { assertions =>\n      val initialState = MerkleTrieState.empty\n      val put0700  = MerkleTrie.put[Id](hex\"0700\".toNibbles, ByteVector.empty)\n      val put07    = MerkleTrie.put[Id](hex\"07\".toNibbles, ByteVector.empty)\n      val put10    = MerkleTrie.put[Id](hex\"10\".toNibbles, ByteVector.empty)\n      val getEmpty = MerkleTrie.get[Id](ByteVector.empty.toNibbles)\n\n      val program = for\n        _     <- put0700\n        _     <- put07\n        _     <- put10\n        value <- getEmpty\n      yield assertions.assertEquals(value, None)\n\n//    val forPrint = for\n//      state1 <- put0700.runS(initialState)\n//      state2 <- put07.runS(state1)\n//      state3 <- put10.runS(state2)\n//    yield\n//      Seq(state1, state2, state3).zipWithIndex.foreach{ (s, i) =>\n//        println(s\"====== State #${i + 1} ======\")\n//        println(s\"root: ${s.root}\")\n//        s.diff.foreach{ (hash, node) => println(s\" $hash: $node\") }\n//      }\n\n      program.runA(initialState).value\n    }\n  }\n\n  test(\"put 00 -> put 01 -> get 00\") {\n    withMunitAssertions { assertions =>\n      val initialState = MerkleTrieState.empty\n      val put00        = MerkleTrie.put[Id](hex\"00\".toNibbles, ByteVector.empty)\n      val put01        = MerkleTrie.put[Id](hex\"01\".toNibbles, ByteVector.empty)\n      val get00        = MerkleTrie.get[Id](hex\"00\".toNibbles)\n\n      val program = for\n        _     <- put00\n        _     <- put01\n        value <- get00\n      yield assertions.assertEquals(value, Some(ByteVector.empty))\n\n//    val forPrint = for\n//      state1 <- put00.runS(initialState)\n//      state2 <- put01.runS(state1)\n//    yield\n//      Seq(state1, state2).zipWithIndex.foreach{ (s, i) =>\n//        println(s\"====== State #${i + 1} ======\")\n//        println(s\"root: ${s.root}\")\n//        s.diff.foreach{ (hash, node) => println(s\" $hash: $node\") }\n//      }\n\n      program.runA(initialState).value\n    }\n  }\n\n  test(\"put(00, empty) -> put(01, empty) -> put(00, 00) -> get 01\") {\n    withMunitAssertions { assertions =>\n      val initialState = MerkleTrieState.empty\n      val put00        = MerkleTrie.put[Id](hex\"00\".toNibbles, ByteVector.empty)\n      val put01        = MerkleTrie.put[Id](hex\"01\".toNibbles, ByteVector.empty)\n      val put00_00     = MerkleTrie.put[Id](hex\"00\".toNibbles, hex\"00\")\n      val get01        = MerkleTrie.get[Id](hex\"01\".toNibbles)\n\n      val program = for\n        _     <- put00\n        _     <- put01\n        _     <- put00_00\n        value <- get01\n      yield value\n\n//    val forPrint = for\n//      state1 <- put00.runS(initialState)\n//      state2 <- put01.runS(state1)\n//      state3 <- put00_00.runS(state2)\n//    yield\n//      Seq(state1, state2, state3).zipWithIndex.foreach{ (s, i) =>\n//        println(s\"====== State #${i + 1} ======\")\n//        println(s\"root: ${s.root}\")\n//        s.diff.foreach{ (hash, node) => println(s\" $hash: $node\") }\n//      }\n\n      val result = program.runA(initialState).value\n      assertions.assertEquals(result, Right(Some(ByteVector.empty)))\n    }\n  }\n\n  test(\"put 50 -> put 5000 -> remove 00\") {\n    withMunitAssertions { assertions =>\n      val initialState = MerkleTrieState.empty\n\n      def put(key: ByteVector) =\n        MerkleTrie.put[Id](key.toNibbles, ByteVector.empty)\n      def remove(key: ByteVector) = MerkleTrie.remove[Id](key.toNibbles)\n\n      val program = for\n        _      <- put(hex\"50\")\n        _      <- put(hex\"5000\")\n        result <- remove(hex\"00\")\n      yield result\n\n//    val forPrint = for\n//      state1 <- put(hex\"50\").runS(initialState)\n//      state2 <- put(hex\"5000\").runS(state1)\n//      result <- remove(hex\"00\").run(state2)\n//    yield\n//      Seq(state1, state2, result._1).zipWithIndex.foreach{ (s, i) =>\n//        println(s\"====== State #${i + 1} ======\")\n//        println(s\"root: ${s.root}\")\n//        s.diff.foreach{ (hash, node) => println(s\" $hash: $node\") }\n//      }\n//      println(s\"result: ${result._2}\")\n\n      val result = program.runA(initialState).value\n      assertions.assertEquals(result, Right(false))\n    }\n  }\n\n  test(\"put d0 -> put d000 -> put empty -> put 000000 -> remove d000\") {\n    withMunitAssertions { assertions =>\n      val initialState = MerkleTrieState.empty\n\n      def put(key: ByteVector) =\n        MerkleTrie.put[Id](key.toNibbles, ByteVector.empty)\n      def remove(key: ByteVector) = MerkleTrie.remove[Id](key.toNibbles)\n\n      val program = for\n        _ <- put(hex\"d0\")\n        _ <- put(hex\"d000\")\n        _ <- put(hex\"\")\n        _ <- put(hex\"000000\")\n        _ <- remove(hex\"d000\")\n      yield ()\n\n//    val forPrint = for\n//      state1 <- put(hex\"d0\").runS(initialState)\n//      state2 <- put(hex\"d000\").runS(state1)\n//      state3 <- put(hex\"\").runS(state2)\n//      state4 <- put(hex\"000000\").runS(state3)\n//      state5 <- remove(hex\"d000\").runS(state4)\n//    yield\n//      Seq(state1, state2, state3, state4).zipWithIndex.foreach{ (s, i) =>\n//        println(s\"====== State #${i + 1} ======\")\n//        println(s\"root: ${s.root}\")\n//        s.diff.foreach{ (hash, node) => println(s\" $hash: $node\") }\n//      }\n\n      val result = program.runA(initialState).value\n      assertions.assertEquals(result, Right(()))\n    }\n  }\n\n  test(\"put 80 -> streamFrom 00\"):\n    withMunitAssertions: assertions =>\n\n      def put(key: ByteVector) = MerkleTrie.put[SyncIO](key.toNibbles, ByteVector.empty)\n      def streamFrom(key: ByteVector) = MerkleTrie.streamFrom[SyncIO](key.toNibbles)\n\n      val program = for\n        _ <- put(hex\"80\")\n        value <- streamFrom(hex\"00\")\n      yield value\n\n      val resultIO = program\n        .runA(initialState)\n        .flatMap: stream =>\n          stream.compile.toList\n        .value\n\n      val result = resultIO.unsafeRunSync()\n\n      val expected: List[(Nibbles, ByteVector)] = List((hex\"80\".toNibbles, ByteVector.empty))\n\n      assertions.assertEquals(result, expected.asRight[String])\n\n  test(\"put 80 -> put empty -> streamFrom 00\"):\n    withMunitAssertions: assertions =>\n\n      def put(key: ByteVector) = MerkleTrie.put[SyncIO](key.toNibbles, ByteVector.empty)\n      def streamFrom(key: ByteVector) = MerkleTrie.streamFrom[SyncIO](key.toNibbles)\n\n      val program = for\n        _ <- put(hex\"80\")\n        _ <- put(ByteVector.empty)\n        value <- streamFrom(hex\"01\")\n      yield value\n\n//      val forPrint = for\n//        (state1, _)     <- put(hex\"80\").run(initialState)\n//        (state2, _)     <- put(ByteVector.empty).run(state1)\n//        (state3, value) <- streamFrom(hex\"01\").run(state2)\n//        resultList      <- value.compile.toList\n//      yield\n//        Seq(state1, state2, state3).zipWithIndex.foreach: (s, i) =>\n//          println(s\"====== State #${i + 1} ======\")\n//          println(s\"root: ${s.root}\")\n//          s.diff.foreach { (hash, node) => println(s\" $hash: $node\") }\n//        println(s\"========\")\n//        println(s\"result: ${resultList}\")\n//\n//        value\n//\n//      val result = forPrint\n//        .flatMap(_.compile.toList)\n//        .value\n//        .unsafeRunSync()\n\n      val resultIO = program\n        .runA(initialState)\n        .flatMap: stream =>\n          stream.compile.toList\n        .value\n\n      val result = resultIO.unsafeRunSync()\n\n      val expected: List[(Nibbles, ByteVector)] = List((hex\"80\".toNibbles, ByteVector.empty))\n\n      assertions.assertEquals(result, expected.asRight[String])\n\n  test(\"put empty -> reverseStreamFrom (00, None)\"):\n    withMunitAssertions: assertions =>\n\n      def put(key: ByteVector) = MerkleTrie.put[SyncIO](key.toNibbles, ByteVector.empty)\n      def reverseStreamFrom(keyPrefix: ByteVector, keySuffix: Option[Nibbles]) =\n        MerkleTrie.reverseStreamFrom[SyncIO](keyPrefix.toNibbles, keySuffix)\n\n      val program = for\n        _     <- put(ByteVector.empty)\n        value <- reverseStreamFrom(hex\"00\", None)\n      yield value\n\n//      val forPrint = for\n//        (state1, _)     <- put(ByteVector.empty).run(initialState)\n//        (state2, value) <- reverseStreamFrom(hex\"00\", None).run(state1)\n//        resultList      <- value.compile.toList\n//      yield\n//        Seq(state1, state2).zipWithIndex.foreach: (s, i) =>\n//          println(s\"====== State #${i + 1} ======\")\n//          println(s\"root: ${s.root}\")\n//          s.diff.foreach { (hash, node) => println(s\" $hash: $node\") }\n//        println(s\"========\")\n//        println(s\"result: ${resultList}\")\n//\n//        value\n//\n//      forPrint\n//        .flatMap(_.compile.toList)\n//        .value\n//        .unsafeRunSync()\n\n      val resultIO = program\n        .runA(initialState)\n        .flatMap: stream =>\n          stream.compile.toList\n        .value\n\n      resultIO.unsafeRunSync()\n      val result = resultIO.unsafeRunSync()\n      val expected: List[(Nibbles, ByteVector)] = List.empty\n\n      assertions.assertEquals(result, expected.asRight[String])\n\n  test(\"put 00 -> reverseStreamFrom (empty, None)\"):\n    withMunitAssertions: assertions =>\n\n      def put(key: ByteVector) = MerkleTrie.put[SyncIO](key.toNibbles, ByteVector.empty)\n      def reverseStreamFrom(keyPrefix: ByteVector, keySuffix: Option[Nibbles]) =\n        MerkleTrie.reverseStreamFrom[SyncIO](keyPrefix.toNibbles, keySuffix)\n\n      val program = for\n        _     <- put(hex\"00\")\n        value <- reverseStreamFrom(ByteVector.empty, None)\n      yield value\n\n//      val forPrint = for\n//        (state1, _)     <- put(hex\"00\").run(initialState)\n//        (state2, value) <- reverseStreamFrom(ByteVector.empty, None).run(state1)\n//        resultList      <- value.compile.toList\n//      yield\n//        Seq(state1, state2).zipWithIndex.foreach: (s, i) =>\n//          println(s\"====== State #${i + 1} ======\")\n//          println(s\"root: ${s.root}\")\n//          s.diff.foreach { (hash, node) => println(s\" $hash: $node\") }\n//        println(s\"========\")\n//        println(s\"result: ${resultList}\")\n//\n//        value\n//\n//      forPrint\n//        .flatMap(_.compile.toList)\n//        .value\n//        .unsafeRunSync()\n\n      val resultIO = program\n        .runA(initialState)\n        .flatMap: stream =>\n          stream.compile.toList\n        .value\n\n      resultIO.unsafeRunSync()\n      val result = resultIO.unsafeRunSync()\n\n      val expected: List[(Nibbles, ByteVector)] = List((hex\"00\".toNibbles, ByteVector.empty))\n\n      assertions.assertEquals(result, expected.asRight[String])\n\n\n  test(\"put empty -> put 00 -> reverseStreamFrom (00, None)\"):\n    withMunitAssertions: assertions =>\n\n      def put(key: ByteVector) = MerkleTrie.put[SyncIO](key.toNibbles, ByteVector.empty)\n      def reverseStreamFrom(keyPrefix: ByteVector, keySuffix: Option[Nibbles]) =\n        MerkleTrie.reverseStreamFrom[SyncIO](keyPrefix.toNibbles, keySuffix)\n\n      val program = for\n        _     <- put(ByteVector.empty)\n        _     <- put(hex\"00\")\n        value <- reverseStreamFrom(hex\"00\", None)\n      yield value\n\n//      val forPrint = for\n//        (state1, _)     <- put(ByteVector.empty).run(initialState)\n//        (state2, _)     <- put(hex\"00\").run(state1)\n//        (state3, value) <- reverseStreamFrom(hex\"00\", None).run(state2)\n//        resultList      <- value.compile.toList\n//      yield\n//        Seq(state1, state2, state3).zipWithIndex.foreach: (s, i) =>\n//          println(s\"====== State #${i + 1} ======\")\n//          println(s\"root: ${s.root}\")\n//          s.diff.foreach { (hash, node) => println(s\" $hash: $node\") }\n//        println(s\"========\")\n//        println(s\"result: ${resultList}\")\n//        value\n//\n//      forPrint\n//        .flatMap(_.compile.toList)\n//        .value\n//        .unsafeRunSync()\n\n      val resultIO = program\n        .runA(initialState)\n        .flatMap: stream =>\n          stream.compile.toList\n        .value\n\n      resultIO.unsafeRunSync()\n      val result = resultIO.unsafeRunSync()\n\n      val expected: List[(Nibbles, ByteVector)] = List(\n        (hex\"00\".toNibbles, ByteVector.empty),\n      )\n\n      assertions.assertEquals(result, expected.asRight[String])\n\n  test(\"put 00 -> put 0000 -> reverseStreamFrom (empty, None)\"):\n    withMunitAssertions: assertions =>\n\n      def put(key: ByteVector) = MerkleTrie.put[SyncIO](key.toNibbles, ByteVector.empty)\n      def reverseStreamFrom(keyPrefix: ByteVector, keySuffix: Option[Nibbles]) =\n        MerkleTrie.reverseStreamFrom[SyncIO](keyPrefix.toNibbles, keySuffix)\n\n      val program = for\n        _     <- put(hex\"00\")\n        _     <- put(hex\"0000\")\n        value <- reverseStreamFrom(ByteVector.empty, None)\n      yield value\n\n      val resultIO = program\n        .runA(initialState)\n        .flatMap: stream =>\n          stream.compile.toList\n        .value\n\n      resultIO.unsafeRunSync()\n      val result = resultIO.unsafeRunSync()\n\n      val expected: List[(Nibbles, ByteVector)] = List(\n        (hex\"0000\".toNibbles, ByteVector.empty),\n        (hex\"00\".toNibbles, ByteVector.empty),\n      )\n\n      assertions.assertEquals(result, expected.asRight[String])\n\n\n  test(\"put 0000 -> put 10 -> reverseStreamFrom (10, None)\"):\n    withMunitAssertions: assertions =>\n\n      def put(key: ByteVector) = MerkleTrie.put[SyncIO](key.toNibbles, ByteVector.empty)\n      def reverseStreamFrom(keyPrefix: ByteVector, keySuffix: Option[Nibbles]) =\n        MerkleTrie.reverseStreamFrom[SyncIO](keyPrefix.toNibbles, keySuffix)\n\n      val program = for\n        _     <- put(hex\"0000\")\n        _     <- put(hex\"10\")\n        value <- reverseStreamFrom(hex\"10\", None)\n      yield value\n\n      val resultIO = program\n        .runA(initialState)\n        .flatMap: stream =>\n          stream.compile.toList\n        .value\n\n      resultIO.unsafeRunSync()\n      val result = resultIO.unsafeRunSync()\n\n      val expected: List[(Nibbles, ByteVector)] = List(\n        (hex\"10\".toNibbles, ByteVector.empty),\n      )\n\n      assertions.assertEquals(result, expected.asRight[String])\n\n  property(\"test merkle trie\"):\n    sequential(\n      range = Range.linear(1, 100),\n      initial = State.empty,\n      commands = List(\n        commandGet,\n        commandPut,\n        commandRemove,\n        commandStreamFrom,\n        commandReverseStreamFrom,\n      ),\n      cleanup = () => merkleTrieState = MerkleTrieState.empty,\n    )\n"
  },
  {
    "path": "modules/lib/shared/src/test/scala/io/leisuremeta/chain/lib/merkle/NibblesTest.scala",
    "content": "package io.leisuremeta.chain.lib\npackage merkle\n\nimport scodec.bits.ByteVector\n\nimport codec.byte.{ByteDecoder, ByteEncoder, DecodeResult}\nimport codec.byte.ByteEncoder.ops.*\nimport failure.DecodingFailure\n\nimport hedgehog.*\nimport hedgehog.munit.HedgehogSuite\n\n\nclass NibblesTest extends HedgehogSuite:\n\n  property(\"roundtrip of nibbles byte codec\"):\n    val nibblesGen = for\n      bytes <- Gen.bytes(Range.linear(0, 64))\n      byteVector = ByteVector.view(bytes)\n      bits <- Gen.element1(\n        byteVector.bits,\n        byteVector.bits.drop(4),\n      )\n    yield bits.assumeNibbles\n\n    nibblesGen.forAll.map: nibbles =>      \n      val encoded = nibbles.toBytes\n      val decodedEither = ByteDecoder[Nibbles].decode(encoded)\n\n      decodedEither match\n        case Right(DecodeResult(decoded, remainder)) =>\n          Result.all(\n            List(\n              decoded ==== nibbles,\n              Result.assert(remainder.isEmpty),\n            ),\n          )\n        case Left(DecodingFailure(msg)) =>\n          println(s\"Encoded: ${encoded.toHex}\")\n          println(s\"Decoding Failure: ${msg}\")\n\n          Result.failure\n"
  },
  {
    "path": "modules/lmscan-agent/src/main/resources/application.conf.sample",
    "content": "scan = \"\"\nbase = \"\"\nmarket {\n  key = \"\",\n  token = 1,\n}\nes {\n  key = \"\",\n  lm = \"\",\n  addrs = [\n    \"\"\n  ],\n}\nremote {\n  driver = \"org.postgresql.Driver\",\n  url = \"jdbc:postgresql://URL\",\n}\nlocal {\n  driver = \"org.sqlite.JDBC\",\n  url = \"jdbc:sqlite:sample.db\",\n}\n"
  },
  {
    "path": "modules/lmscan-agent/src/main/scala/io/leisuremeta/chain/lmscan/agent/ScanAgentApp.scala",
    "content": "package io.leisuremeta.chain\npackage lmscan.agent\n\nimport cats.effect.*\nimport cats.implicits.*\nimport cats.data.*\nimport cats.effect.kernel.instances.all.*\nimport sttp.client3.SttpBackend\nimport sttp.client3.httpclient.fs2.HttpClientFs2Backend\nimport doobie.util.transactor.Transactor\nimport com.zaxxer.hikari.HikariConfig\nimport doobie.hikari.*\nimport io.leisuremeta.chain.lmscan.agent.service.*\nimport io.leisuremeta.chain.lmscan.agent.service.RequestServiceApp\nimport io.leisuremeta.chain.lmscan.agent.apps.DataStoreApp\nimport io.leisuremeta.chain.lmscan.agent.apps.BalanceApp\nimport io.leisuremeta.chain.lmscan.agent.apps.NftApp\nimport cats.effect.std.Queue\nimport io.leisuremeta.chain.api.model.Signed.TxHash\nimport io.leisuremeta.chain.api.model.TransactionWithResult\nimport io.leisuremeta.chain.api.model.Transaction.TokenTx\nimport io.leisuremeta.chain.api.model.Account\n\nobject ScanAgentResource:\n  def transactorBuilder[F[_]: Async](conf: DBConfig) =\n    for\n      conf <- Resource.pure:\n        val config = new HikariConfig()\n        config.setDriverClassName(conf.driver)\n        config.setJdbcUrl(conf.url)\n        config\n      xa <- HikariTransactor.fromHikariConfig[F](conf)\n    yield xa\n\n  def build[F[_]: Async](conf: ScanAgentConfig): Resource[\n    EitherT[F, String, *],\n    (\n        Transactor[F],\n        Transactor[F],\n        SttpBackend[F, Any],\n    ),\n  ] =\n    for\n      postgres <- transactorBuilder[F](conf.remote).mapK(\n        EitherT.liftK[F, String],\n      )\n      sqlite <- transactorBuilder[F](conf.local).mapK(EitherT.liftK[F, String])\n      sttp <- HttpClientFs2Backend.resource[F]().mapK(EitherT.liftK[F, String])\n    yield (postgres, sqlite, sttp)\n\nobject LoopCheckerApp:\n  def build[F[_]: Async](\n      remote: RemoteStoreApp[F],\n      local: LocalStoreApp[F],\n      client: RequestServiceApp[F],\n      base: String,\n  ): F[(DataStoreApp[F], BalanceApp[F], NftApp[F])] =\n    val nftQ = Queue.bounded[F, (TxHash, TokenTx, Account)](1000)\n    val balQ = Queue.bounded[F, (TxHash, TransactionWithResult)](1000)\n    for\n      qa <- nftQ\n      qb <- balQ\n      storeApp <- Async[F].pure:\n        DataStoreApp.build(qa, qb)(remote, client, base)\n      balApp <- Async[F].pure:\n        BalanceApp.build(qb)(remote, local)\n      nftApp <- Async[F].pure:\n        NftApp.build(qa)(remote, client)\n    yield (storeApp, balApp, nftApp)\n  def run[F[_]: Async](\n      remote: RemoteStoreApp[F],\n      local: LocalStoreApp[F],\n      client: RequestServiceApp[F],\n      base: String,\n  ): F[Unit] =\n    for\n      (a, b, c) <- build[F](remote, local, client, base)\n      _         <- a.run &> b.run &> c.run\n    yield ()\n"
  },
  {
    "path": "modules/lmscan-agent/src/main/scala/io/leisuremeta/chain/lmscan/agent/ScanAgentConfig.scala",
    "content": "package io.leisuremeta.chain.lmscan.agent\n\nimport pureconfig.*\nimport pureconfig.generic.derivation.default.*\n\nfinal case class ScanAgentConfig(\n    base: String,\n    remote: DBConfig,\n    local: DBConfig,\n    scan: String,\n    market: MarketConfig,\n    es: ESConfig,\n) derives ConfigReader\n\nfinal case class MarketConfig(\n    key: String,\n    token: Int,\n)\nfinal case class ESConfig(\n    key: String,\n    lm: String,\n    addrs: List[String],\n)\n\nfinal case class DBConfig(\n    driver: String,\n    url: String,\n    user: Option[String],\n    password: Option[String],\n)\n\nobject ScanAgentConfig:\n  def load: ScanAgentConfig = ConfigSource.default.loadOrThrow[ScanAgentConfig]\n"
  },
  {
    "path": "modules/lmscan-agent/src/main/scala/io/leisuremeta/chain/lmscan/agent/ScanAgentMain.scala",
    "content": "package io.leisuremeta.chain\npackage lmscan.agent\n\nimport cats.effect.*\nimport service.RequestService\nimport cats.data.EitherT\nimport io.leisuremeta.chain.lmscan.agent.service.StoreService\nimport scribe.file.*\nimport scribe.format.*\nimport io.leisuremeta.chain.lmscan.agent.apps.SummaryStoreApp\n\nobject ScanAgentMain extends IOApp:\n  scribe.Logger.root\n    .withHandler(\n      writer = FileWriter(\n        \"logs\" / (\"agent-\" % year % \"-\" % month % \"-\" % day % \".log\"),\n      ),\n      formatter =\n        formatter\"[$threadName] $positionAbbreviated - $messages$newLine\",\n      minimumLevel = Some(scribe.Level.Error),\n    )\n    .replace()\n  def run(args: List[String]): IO[ExitCode] =\n    val conf = ScanAgentConfig.load\n    ScanAgentResource\n      .build[IO](conf)\n      .use: (post, sqlite, server) =>\n        val client = RequestService.build[IO](server)\n        val remote = StoreService.buildRemote(post)\n        val local  = StoreService.buildLocal(sqlite)\n        val summary = SummaryStoreApp.build[IO](conf.market, conf.es)(\n          remote,\n          client,\n        )\n        EitherT.liftF:\n          summary.run &> LoopCheckerApp.run(remote, local, client, conf.base)\n      .value\n      .as(ExitCode.Success)\n"
  },
  {
    "path": "modules/lmscan-agent/src/main/scala/io/leisuremeta/chain/lmscan/agent/apps/BalanceStoreApp.scala",
    "content": "package io.leisuremeta.chain.lmscan.agent\npackage apps\n\nimport cats.effect.*\nimport cats.effect.kernel.instances.all.*\nimport cats.implicits.*\nimport io.leisuremeta.chain.lmscan.agent.service.*\nimport io.leisuremeta.chain.lib.crypto.Hash\nimport cats.effect.std.Queue\nimport io.leisuremeta.chain.api.model.Transaction\nimport io.leisuremeta.chain.api.model.Transaction.*\nimport io.leisuremeta.chain.api.model.Transaction.TokenTx.*\nimport io.leisuremeta.chain.api.model.Transaction.RewardTx.*\nimport io.leisuremeta.chain.api.model.Signed.TxHash\nimport io.leisuremeta.chain.api.model.TransactionWithResult\nimport scala.concurrent.duration.DurationInt\nimport cats.data.NonEmptyList\n\ntrait BalanceApp[F[_]]:\n  def run: F[Unit]\n\ncase class Balance(\n    address: String,\n    free: BigDecimal,\n)\ncase class Ledger(\n    hash: String,\n    address: String,\n    free: Option[String],\n    used: Option[String],\n)\nenum LedgerType:\n  case InputLedger(\n      hash: String,\n      address: String,\n      free: BigInt,\n  )\n  case SpendLedger(\n      hash: String,\n      address: String,\n      used: String,\n  )\n  case LockLedger(\n      hash: String,\n      address: String,\n      locked: BigInt,\n  )\n  case SpendLockLedger(\n      hash: String,\n      used: String,\n  )\nobject BalanceApp:\n  def build[F[_]: Async](\n      balQ: Queue[F, (TxHash, TransactionWithResult)],\n  )(remote: RemoteStoreApp[F], local: LocalStoreApp[F]): BalanceApp[F] =\n    new BalanceApp[F]:\n      def run =\n        for\n          _ <- init\n          _ <- loop\n        yield ()\n      def init: F[Unit] =\n        for\n          _ <- local.balRepo.createLedgerTable\n          _ <- local.balRepo.createLockLedgerTable\n        yield ()\n      def loop: F[Unit] =\n        for\n          x      <- balQ.tryTakeN(Some(100))\n          balOps <- x.flatMap(toBalanceOp).pure[F]\n          (a, b, c, d) = balOps.foldLeft(\n            (\n              List.empty[LedgerType.InputLedger],\n              List.empty[LedgerType.SpendLedger],\n              List.empty[LedgerType.LockLedger],\n              List.empty[LedgerType.SpendLockLedger],\n            ),\n          )((acc, v) =>\n            v match\n              case i: LedgerType.InputLedger =>\n                (i :: acc._1, acc._2, acc._3, acc._4)\n              case s: LedgerType.SpendLedger =>\n                (acc._1, s :: acc._2, acc._3, acc._4)\n              case l: LedgerType.LockLedger =>\n                (acc._1, acc._2, l :: acc._3, acc._4)\n              case r: LedgerType.SpendLockLedger =>\n                (acc._1, acc._2, acc._3, r :: acc._4),\n          )\n          _ <- local.balRepo.addInputLedger(a)\n          _ <- local.balRepo.addSpendLedger(b)\n          _ <- local.balRepo.addLockLedger(c)\n          _ <- local.balRepo.addSpendLockLedger(d)\n          updates = balOps.foldLeft(\n            Set.empty[String],\n          )((acc, v) =>\n            v match\n              case LedgerType.InputLedger(_, address, free) =>\n                acc + address\n              case LedgerType.SpendLedger(_, address, _) =>\n                acc + address\n              case _ => acc,\n          )\n          freeArr <-\n            if updates.isEmpty then Async[F].pure(Right(Nil))\n            else\n              local.balRepo\n                .getLedger(\n                  NonEmptyList.fromListUnsafe(updates.toList),\n                )\n          balMap = freeArr match\n            case Left(e) => Map.empty[String, BigDecimal]\n            case Right(v) =>\n              v\n                .foldLeft(Map.empty[String, BigDecimal])((acc, b) =>\n                  acc.get(b._1) match\n                    case Some(v) =>\n                      acc + (b._1 -> (v + BigDecimal(b._2)))\n                    case None =>\n                      acc + (b._1 -> BigDecimal(b._2)),\n                )\n          _ <-\n            if balMap.isEmpty then Async[F].pure(Right(0))\n            else remote.balRepo.updateBalance(balMap.toList)\n          _ <- Async[F].delay:\n            scribe.info(\"balance updated: \" + balMap.size)\n          _ <- Async[F].sleep(20.seconds)\n          r <- run\n        yield r\n\n      def toBalanceOp(\n          hash: TxHash,\n          tx: TransactionWithResult,\n      ) =\n        val signer = tx.signedTx.sig.account\n        tx.signedTx.value match\n          case t: MintFungibleToken =>\n            t.outputs\n              .map((acc, nat) =>\n                LedgerType.InputLedger(\n                  hash.toUInt256Bytes.toHex,\n                  acc.toString,\n                  nat.toBigInt,\n                ),\n              )\n              .toList\n          case t: BurnFungibleToken =>\n            val inputs = t.inputs\n              .map(h =>\n                LedgerType.SpendLedger(\n                  h.toUInt256Bytes.toHex,\n                  signer.toString,\n                  hash.toUInt256Bytes.toHex,\n                ),\n              )\n              .toList\n            tx.result match\n              case Some(BurnFungibleTokenResult(v)) =>\n                LedgerType.InputLedger(\n                  hash.toUInt256Bytes.toHex,\n                  signer.toString,\n                  v.toBigInt,\n                ) ::\n                  inputs\n              case _ => inputs\n          case t: TransferFungibleToken =>\n            t.outputs\n              .map((acc, nat) =>\n                LedgerType.InputLedger(\n                  hash.toUInt256Bytes.toHex,\n                  acc.toString,\n                  nat.toBigInt,\n                ),\n              )\n              .toList :::\n              t.inputs\n                .map(h =>\n                  LedgerType.SpendLedger(\n                    h.toUInt256Bytes.toHex,\n                    signer.toString,\n                    hash.toUInt256Bytes.toHex,\n                  ),\n                )\n                .toList\n          case t: OfferReward =>\n            t.outputs\n              .map((acc, nat) =>\n                LedgerType.InputLedger(\n                  hash.toUInt256Bytes.toHex,\n                  acc.toString,\n                  nat.toBigInt,\n                ),\n              )\n              .toList :::\n              t.inputs\n                .map(h =>\n                  LedgerType.SpendLedger(\n                    h.toUInt256Bytes.toHex,\n                    signer.toString,\n                    hash.toUInt256Bytes.toHex,\n                  ),\n                )\n                .toList\n          case t: EntrustFungibleToken =>\n            val l = LedgerType.LockLedger(\n              hash.toUInt256Bytes.toHex,\n              t.to.toString,\n              t.amount.toBigInt,\n            ) ::\n              t.inputs\n                .map(h =>\n                  LedgerType.SpendLedger(\n                    h.toUInt256Bytes.toHex,\n                    signer.toString,\n                    hash.toUInt256Bytes.toHex,\n                  ),\n                )\n                .toList\n            tx.result match\n              case Some(EntrustFungibleTokenResult(v)) =>\n                LedgerType.InputLedger(\n                  hash.toUInt256Bytes.toHex,\n                  signer.toString,\n                  v.toBigInt,\n                ) :: l\n              case _ => l\n          case t: DisposeEntrustedFungibleToken =>\n            t.inputs\n              .map(h =>\n                LedgerType.SpendLockLedger(\n                  h.toUInt256Bytes.toHex,\n                  hash.toUInt256Bytes.toHex,\n                ),\n              )\n              .toList :::\n              t.outputs\n                .map((acc, nat) =>\n                  LedgerType.InputLedger(\n                    hash.toUInt256Bytes.toHex,\n                    acc.toString,\n                    nat.toBigInt,\n                  ),\n                )\n                .toList\n          case _ => Nil\n"
  },
  {
    "path": "modules/lmscan-agent/src/main/scala/io/leisuremeta/chain/lmscan/agent/apps/NftStoreApp.scala",
    "content": "package io.leisuremeta.chain.lmscan.agent\npackage apps\n\nimport cats.effect.*\nimport cats.implicits.*\nimport io.leisuremeta.chain.lmscan.agent.service.*\nimport cats.effect.std.Queue\nimport io.leisuremeta.chain.api.model.Transaction\nimport io.leisuremeta.chain.api.model.Transaction.*\nimport io.leisuremeta.chain.api.model.Transaction.TokenTx.*\nimport scala.concurrent.duration.DurationInt\nimport io.leisuremeta.chain.api.model.Account\nimport io.circe.Decoder\nimport io.circe.generic.semiauto.*\nimport cats.data.EitherT\nimport io.leisuremeta.chain.api.model.Signed.TxHash\nimport cats.Monad\n\ntrait NftApp[F[_]]:\n  def run: F[Unit]\n\nobject NftApp:\n  case class Nft(\n      txHash: String,\n      tokenId: String,\n      action: String,\n      fromAddr: String,\n      toAddr: String,\n      eventTime: Long,\n  )\n  case class NftFile(\n      tokenId: String,\n      tokenDefId: String,\n      collectionName: String,\n      nftName: String,\n      nftUri: String,\n      creatorDescription: String,\n      dataUrl: String,\n      rarity: String,\n      creator: String,\n      eventTime: Long,\n  )\n  case class NftOwner(\n      tokenId: String,\n      ownerAddr: String,\n      eventTime: Long,\n  )\n  case class NftMetaInfo(\n      Creator_description: String,\n      Collection_description: String,\n      Rarity: String,\n      NFT_checksum: String,\n      Collection_name: String,\n      Creator: String,\n      NFT_name: String,\n      NFT_URI: String,\n  )\n  given Decoder[NftMetaInfo] = deriveDecoder[NftMetaInfo]\n  def build[F[_]: Async: Monad](nftQ: Queue[F, (TxHash, TokenTx, Account)])(\n      remote: RemoteStoreApp[F],\n      client: RequestServiceApp[F],\n  ): NftApp[F] = new NftApp[F]:\n    def run: F[Unit] =\n      for\n        q <- nftQ.tryTakeN(Some(1000))\n        _ = scribe.info(s\"nftQ size: ${q.size}\")\n        files <- q\n          .traverse((_, tx, _) => getNftFileReq(tx).value)\n        fs <- remote.nftRepo.putNftFileList(files.mapFilter(_.toOption))\n        (nfts, owners) <- Async[F].pure:\n          q.mapFilter(parseTxToRaw).separate\n        ns <- remote.nftRepo.putNftList(nfts)\n        os <- remote.nftRepo.putNftOwnerList(owners)\n        _  <- Async[F].sleep(30.seconds)\n        r  <- run\n      yield r\n    def getNftFileReq(tx: TokenTx) = tx match\n      case tx: MintNFT =>\n        for\n          info <- client.getResult[NftMetaInfo](tx.dataUrl.toString)\n          res = NftFile(\n            tx.tokenId.toString,\n            tx.tokenDefinitionId.toString,\n            info.Collection_name,\n            info.NFT_name,\n            info.NFT_URI,\n            info.Creator_description,\n            tx.dataUrl.toString,\n            info.Rarity,\n            info.Creator,\n            tx.createdAt.getEpochSecond,\n          )\n        yield res\n      case tx: MintNFTWithMemo =>\n        for\n          info <- client.getResult[NftMetaInfo](tx.dataUrl.toString)\n          res = NftFile(\n            tx.tokenId.toString,\n            tx.tokenDefinitionId.toString,\n            info.Collection_name,\n            info.NFT_name,\n            info.NFT_URI,\n            info.Creator_description,\n            tx.dataUrl.toString,\n            info.Rarity,\n            info.Creator,\n            tx.createdAt.getEpochSecond,\n          )\n        yield res\n      case _ => EitherT.leftT[F, NftFile](\"Not MintNFT\")\n    def parseTxToRaw(hash: TxHash, tx: TokenTx, from: Account) = tx match\n      case tx: MintNFT =>\n        Some(\n          Nft(\n            hash.toUInt256Bytes.toHex,\n            tx.tokenId.toString,\n            \"MintNft\",\n            from.toString,\n            tx.output.toString,\n            tx.createdAt.getEpochSecond,\n          ),\n          NftOwner(\n            tx.tokenId.toString,\n            tx.output.toString,\n            tx.createdAt.getEpochSecond,\n          ),\n        )\n      case tx: MintNFTWithMemo =>\n        Some(\n          Nft(\n            hash.toUInt256Bytes.toHex,\n            tx.tokenId.toString,\n            \"MintNft\",\n            from.toString,\n            tx.output.toString,\n            tx.createdAt.getEpochSecond,\n          ),\n          NftOwner(\n            tx.tokenId.toString,\n            tx.output.toString,\n            tx.createdAt.getEpochSecond,\n          ),\n        )\n      case tx: EntrustNFT =>\n        Some(\n          Nft(\n            hash.toUInt256Bytes.toHex,\n            tx.tokenId.toString,\n            \"EntrustNft\",\n            tx.input.toUInt256Bytes.toHex,\n            tx.to.toString,\n            tx.createdAt.getEpochSecond,\n          ),\n          NftOwner(\n            tx.tokenId.toString,\n            tx.to.toString,\n            tx.createdAt.getEpochSecond,\n          ),\n        )\n      case tx: TransferNFT =>\n        Some(\n          Nft(\n            hash.toUInt256Bytes.toHex,\n            tx.tokenId.toString,\n            \"TransferNft\",\n            tx.input.toUInt256Bytes.toHex,\n            tx.output.toString,\n            tx.createdAt.getEpochSecond,\n          ),\n          NftOwner(\n            tx.tokenId.toString,\n            tx.output.toString,\n            tx.createdAt.getEpochSecond,\n          ),\n        )\n      case tx: DisposeEntrustedNFT =>\n        Some(\n          Nft(\n            hash.toUInt256Bytes.toHex,\n            tx.tokenId.toString,\n            \"DisposeEntrustedNft\",\n            tx.input.toUInt256Bytes.toHex,\n            tx.output.map(_.toString).getOrElse(\"\"),\n            tx.createdAt.getEpochSecond,\n          ),\n          NftOwner(\n            tx.tokenId.toString,\n            tx.output.map(_.toString).getOrElse(\"\"),\n            tx.createdAt.getEpochSecond,\n          ),\n        )\n      case _ => None\n"
  },
  {
    "path": "modules/lmscan-agent/src/main/scala/io/leisuremeta/chain/lmscan/agent/apps/NodeDataStoreApp.scala",
    "content": "package io.leisuremeta.chain.lmscan.agent\npackage apps\n\nimport cats.effect.*\nimport cats.effect.kernel.instances.all.*\nimport cats.implicits.*\nimport cats.data.*\nimport io.leisuremeta.chain.lmscan.agent.service.*\nimport io.leisuremeta.chain.lmscan.agent.service.RequestServiceApp\nimport io.leisuremeta.chain.api.model.NodeStatus\nimport io.leisuremeta.chain.api.model.Block.BlockHash\nimport io.circe.*, io.circe.generic.semiauto.*\nimport io.leisuremeta.chain.lmscan.backend.entity.Block\nimport io.leisuremeta.chain.lib.crypto.Hash\nimport io.leisuremeta.chain.lib.datatype.UInt256\nimport scodec.bits.ByteVector\nimport cats.effect.std.Queue\nimport io.leisuremeta.chain.api.model.Transaction\nimport io.leisuremeta.chain.api.model.Transaction.*\nimport io.leisuremeta.chain.api.model.Transaction.AccountTx.*\nimport io.leisuremeta.chain.api.model.Transaction.GroupTx.*\nimport io.leisuremeta.chain.api.model.Transaction.TokenTx.*\nimport io.leisuremeta.chain.api.model.Transaction.RewardTx.*\nimport io.leisuremeta.chain.api.model.Transaction.AgendaTx.*\nimport io.leisuremeta.chain.api.model.Transaction.VotingTx.*\nimport io.leisuremeta.chain.api.model.Transaction.CreatorDaoTx.*\nimport io.leisuremeta.chain.api.model.Block as NodeBlock\nimport io.leisuremeta.chain.api.model.Signed.TxHash\nimport io.leisuremeta.chain.api.model.TransactionWithResult\nimport java.time.Instant\nimport scala.concurrent.duration.DurationInt\nimport io.leisuremeta.chain.api.model.Account\n\ncase class Tx(\n    hash: String,\n    signer: String,\n    txType: Option[String] = None,\n    blockHash: String,\n    eventTime: Long,\n    tokenType: String = \"LM\",\n    blockNumber: Long,\n    subType: Option[String],\n)\n\ntrait DataStoreApp[F[_]]:\n  def run: F[Unit]\n\nobject DataStoreApp:\n  given Decoder[NodeStatus]            = deriveDecoder[NodeStatus]\n  given Decoder[NodeBlock]             = deriveDecoder[NodeBlock]\n  given Decoder[TransactionWithResult] = deriveDecoder[TransactionWithResult]\n  extension (b: Block)\n    def toHash =\n      Hash.Value[NodeBlock](\n        UInt256\n          .from(ByteVector.fromHex(b.hash).get)\n          .getOrElse(UInt256.EmptyBytes),\n      )\n    def getParent =\n      Hash.Value[NodeBlock](\n        UInt256\n          .from(ByteVector.fromHex(b.parentHash).get)\n          .getOrElse(UInt256.EmptyBytes),\n      )\n  def checkHashRange(\n      from: BlockHash,\n      to: BlockHash,\n  ): Either[String, (String, BlockHash)] =\n    if from != to then Right(from.toUInt256Bytes.toHex, to)\n    else Left(\"Store block is latest\")\n  def build[F[_]: Async](\n      nftQ: Queue[F, (TxHash, TokenTx, Account)],\n      balQ: Queue[F, (TxHash, TransactionWithResult)],\n  )(\n      db: RemoteStoreApp[F],\n      client: RequestServiceApp[F],\n      base: String,\n  ): DataStoreApp[F] =\n    new DataStoreApp[F]:\n      def run: F[Unit] =\n        toGen &> toBest\n      def toGen: F[Unit] =\n        for\n          lowest <- db.blcRepo.getLowestBlock\n          x = lowest\n            .leftMap(_.getMessage)\n            .flatMap:\n              case Some(b) =>\n                Right(b)\n              case None =>\n                Left(\"block not found\")\n          sEither <- client.getResult[NodeStatus](s\"$base/status\").value\n          _ <- (x, sEither) match\n            case (Right(blc), Right(status)) =>\n              if blc.number != 0 then\n                storeBlockLoop(blc.getParent, status.genesisHash)\n              else Async[F].unit\n            case (Left(s), Right(status)) =>\n              storeBlockLoop(status.bestHash, status.genesisHash)\n            case _ =>\n              Async[F].unit\n        yield ()\n      def toBest: F[Unit] =\n        for\n          latest <- db.blcRepo.getLatestBlock\n          x = latest\n            .leftMap(_.getMessage)\n            .flatMap:\n              case Some(b) =>\n                Right(b)\n              case None =>\n                Left(\"block not found\")\n          sEither <- client.getResult[NodeStatus](s\"$base/status\").value\n          _ <- (x, sEither) match\n            case (Right(blc), Right(status)) =>\n              scribe.info(\n                s\"storeHashRange: ${status.bestHash.toUInt256Bytes.toHex}, ${blc.hash}\",\n              )\n              storeBlockLoop(status.bestHash, blc.toHash)\n            case _ =>\n              Async[F].unit\n          _ <- Async[F].sleep(10.seconds)\n          r <- toBest\n        yield r\n      def storeBlockLoop(from: BlockHash, to: BlockHash): F[Unit] =\n        for\n          next <- Async[F].delay:\n            checkHashRange(from, to)\n          res <- next match\n            case Right((f, _)) =>\n              for\n                blc <- client.getResult[NodeBlock](s\"$base/block/$f\").value\n                r <- blc match\n                  case Right(b) =>\n                    for\n                      _ <- db.blcRepo.putBlock(\n                        f,\n                        b.header.number.toBigInt.toLong,\n                        b.header.parentHash.toUInt256Bytes.toHex,\n                        b.transactionHashes.size,\n                        b.header.timestamp.getEpochSecond,\n                      )\n                      _ <- storeTxLoop(f, b, b.transactionHashes.toList)\n                      r <- storeBlockLoop(b.header.parentHash, to)\n                    yield r\n                  case Left(_) =>\n                    Async[F].delay:\n                      scribe.error(s\"block not found: $f\")\n              yield r\n            case Left(_) =>\n              Async[F].unit\n        yield res\n      def storeTxLoop(\n          bHash: String,\n          blc: NodeBlock,\n          txs: List[TxHash],\n      ): F[Unit] =\n        txs match\n          case Nil => Async[F].unit\n          case x :: xs =>\n            for\n              resE <- client\n                .getResultWithJsonString[TransactionWithResult](\n                  s\"$base/tx/${x.toUInt256Bytes.toHex}\",\n                )\n                .value\n              res <- resE match\n                case Right((tx, json)) =>\n                  val hash                 = x.toUInt256Bytes.toHex\n                  val (txEntity, accounts) = parseTxr(hash, bHash, tx, blc)\n                  for\n                    _ <- db.txRepo.putTx(txEntity)\n                    _ <- db.txRepo.putTxState(\n                      hash,\n                      blc.header.parentHash.toUInt256Bytes.toHex,\n                      tx,\n                      json,\n                    )\n                    _ <- db.accRepo.putAccountMapper(hash, accounts)\n                    _ <- addNftQueue(x, tx)\n                    _ <- addBalanceQueue(x, tx)\n                    r <- storeTxLoop(bHash, blc, xs)\n                  yield r\n                case Left(_) =>\n                  Async[F].delay:\n                    scribe.error(s\"tx not found: ${x.toUInt256Bytes.toHex}\")\n            yield res\n      def parseTxr(\n          hash: String,\n          bHash: String,\n          txr: TransactionWithResult,\n          blc: NodeBlock,\n      ) =\n        val t =\n          Tx(\n            hash,\n            txr.signedTx.sig.account.toString,\n            None,\n            bHash,\n            txr.signedTx.value.createdAt.getEpochSecond,\n            \"LM\",\n            blc.header.number.toBigInt.toLong,\n            None,\n          )\n        txr.signedTx.value match\n          case tx: AccountTx =>\n            parseAccountTx(t, tx)\n          case tx: GroupTx =>\n            parseGroupTx(t, tx, txr.signedTx.sig.account)\n          case tT: TokenTx =>\n            parseTokenTx(t, tT, txr.signedTx.sig.account)\n          case tx: RewardTx =>\n            parseRewardTx(t, tx, txr.signedTx.sig.account)\n          case tx: AgendaTx =>\n            parseAgendaTx(t, tx, txr.signedTx.sig.account)\n          case tx: VotingTx =>\n            parseVotingTx(t, tx, txr.signedTx.sig.account)\n          case tx: CreatorDaoTx =>\n            parseCreatorDaoTx(t, tx, txr.signedTx.sig.account)\n      def parseAccountTx(c: Tx, a: AccountTx) =\n        val t = c.copy(txType = Some(\"Account\"))\n        a match\n          case tx: CreateAccount =>\n            (\n              t.copy(\n                subType = Some(\"CreateAccount\"),\n              ),\n              Set(tx.account),\n            )\n          case tx: CreateAccountWithExternalChainAddresses =>\n            (\n              t.copy(\n                subType = Some(\"CreateAccountWithExternalChainAddresses\"),\n              ),\n              Set.empty,\n            )\n          case tx: UpdateAccount =>\n            (\n              t.copy(\n                subType = Some(\"UpdateAccount\"),\n              ),\n              Set.empty,\n            )\n          case tx: UpdateAccountWithExternalChainAddresses =>\n            (\n              t.copy(\n                subType = Some(\"UpdateAccountWithExternalChainAddresses\"),\n              ),\n              Set.empty,\n            )\n          case tx: AddPublicKeySummaries =>\n            (\n              t.copy(\n                subType = Some(\"AddPublicKeySummaries\"),\n              ),\n              Set.empty,\n            )\n      def parseGroupTx(c: Tx, g: GroupTx, signer: Account) =\n        val t = c.copy(txType = Some(\"Group\"))\n        g match\n          case tx: CreateGroup =>\n            (\n              t.copy(\n                subType = Some(\"CreateGroup\"),\n              ),\n              Set(tx.coordinator),\n            )\n          case tx: AddAccounts =>\n            (\n              t.copy(\n                subType = Some(\"AddAccounts\"),\n              ),\n              tx.accounts + signer,\n            )\n      def parseTokenTx(c: Tx, tt: TokenTx, signer: Account) =\n        val t = c.copy(txType = Some(\"Token\"))\n        tt match\n          case tx: DefineToken =>\n            (\n              t.copy(\n                tokenType = tx.definitionId.toString,\n                subType = Some(\"DefineToken\"),\n              ),\n              Set(signer),\n            )\n          case tx: DefineTokenWithPrecision =>\n            (\n              t.copy(\n                tokenType = tx.definitionId.toString,\n                subType = Some(\"DefineToken\"),\n              ),\n              Set(signer),\n            )\n          case tx: MintFungibleToken =>\n            (\n              t.copy(\n                subType = Some(\"MintFungibleToken\"),\n              ),\n              tx.outputs.keySet + signer,\n            )\n          case _: MintNFT =>\n            (\n              t.copy(\n                subType = Some(\"MintNFT\"),\n              ),\n              Set(signer),\n            )\n          case _: MintNFTWithMemo =>\n            (\n              t.copy(\n                subType = Some(\"MintNFT\"),\n              ),\n              Set(signer),\n            )\n          case _: BurnFungibleToken =>\n            (\n              t.copy(\n                subType = Some(\"BurnFungibleToken\"),\n              ),\n              Set(signer),\n            )\n          case _: BurnNFT =>\n            (\n              t.copy(\n                subType = Some(\"BurnNFT\"),\n              ),\n              Set(signer),\n            )\n          case _: UpdateNFT =>\n            (\n              t.copy(\n                subType = Some(\"UpdateNFT\"),\n              ),\n              Set(signer),\n            )\n          case tx: TransferFungibleToken =>\n            (\n              t.copy(\n                subType = Some(\"TransferFungibleToken\"),\n              ),\n              tx.outputs.keySet + signer,\n            )\n          case tx: TransferNFT =>\n            (\n              t.copy(\n                subType = Some(\"TransferNFT\"),\n              ),\n              Set(signer, tx.output),\n            )\n          case tx: EntrustFungibleToken =>\n            (\n              t.copy(\n                subType = Some(\"EntrustFungibleToken\"),\n              ),\n              Set(signer, tx.to),\n            )\n          case tx: EntrustNFT =>\n            (\n              t.copy(\n                subType = Some(\"EntrustNFT\"),\n              ),\n              Set(signer, tx.to),\n            )\n          case tx: DisposeEntrustedFungibleToken =>\n            (\n              t.copy(\n                subType = Some(\"DisposeEntrustedFungibleToken\"),\n              ),\n              tx.outputs.keySet + signer,\n            )\n          case tx: DisposeEntrustedNFT =>\n            (\n              t.copy(\n                subType = Some(\"DisposeEntrustedNFT\"),\n              ),\n              Set(signer, tx.output.getOrElse(signer)),\n            )\n          case _: CreateSnapshots =>\n            (\n              t.copy(\n                subType = Some(\"CreateSnapshots\"),\n              ),\n              Set(signer),\n            )\n      def parseRewardTx(c: Tx, r: RewardTx, signer: Account) =\n        val t = c.copy(txType = Some(\"Reward\"))\n        r match\n          case tx: RegisterDao =>\n            (\n              t.copy(\n                subType = Some(\"RegisterDao\"),\n              ),\n              tx.moderators + tx.daoAccountName + signer,\n            )\n          case tx: UpdateDao =>\n            (\n              t.copy(\n                subType = Some(\"UpdateDao\"),\n              ),\n              tx.moderators + signer,\n            )\n          case tx: RecordActivity =>\n            (\n              t.copy(\n                subType = Some(\"RecordActivity\"),\n              ),\n              tx.userActivity.keySet + signer,\n            )\n          case tx: OfferReward =>\n            (\n              t.copy(\n                subType = Some(\"OfferReward\"),\n              ),\n              tx.outputs.keySet + signer,\n            )\n          case tx: BuildSnapshot =>\n            (\n              t.copy(\n                subType = Some(\"BuildSnapshot\"),\n              ),\n              Set(signer),\n            )\n          case tx: ExecuteReward =>\n            (\n              t.copy(\n                subType = Some(\"ExecuteReward\"),\n              ),\n              Set(signer, tx.daoAccount.getOrElse(signer)),\n            )\n          case tx: ExecuteOwnershipReward =>\n            (\n              t.copy(\n                subType = Some(\"ExecuteOwnershipReward\"),\n              ),\n              Set(signer),\n            )\n      def parseAgendaTx(c: Tx, a: AgendaTx, signer: Account) =\n        val t = c.copy(txType = Some(\"Agenda\"))\n        a match\n          case _: SuggestSimpleAgenda =>\n            (\n              t.copy(\n                subType = Some(\"SuggestSimpleAgenda\"),\n              ),\n              Set(signer),\n            )\n          case _: VoteSimpleAgenda =>\n            (\n              t.copy(\n                subType = Some(\"VoteSimpleAgenda\"),\n              ),\n              Set(signer),\n            )\n      def parseVotingTx(c: Tx, v: VotingTx, signer: Account) =\n        val t = c.copy(txType = Some(\"Voting\"))\n        v match\n          case tx: CreateVoteProposal =>\n            (\n              t.copy(\n                subType = Some(\"CreateVoteProposal\"),\n              ),\n              Set(signer),\n            )\n          case tx: CastVote =>\n            (\n              t.copy(\n                subType = Some(\"CastVote\"),\n              ),\n              Set(signer),\n            )\n          case tx: TallyVotes =>\n            (\n              t.copy(\n                subType = Some(\"TallyVotes\"),\n              ),\n              Set(signer),\n            )\n      def parseCreatorDaoTx(c: Tx, cd: CreatorDaoTx, signer: Account) =\n        val t = c.copy(txType = Some(\"CreatorDao\"))\n        cd match\n          case tx: CreateCreatorDao =>\n            (\n              t.copy(\n                subType = Some(\"CreateCreatorDao\"),\n              ),\n              Set(signer),\n            )\n          case tx: UpdateCreatorDao =>\n            (\n              t.copy(\n                subType = Some(\"UpdateCreatorDao\"),\n              ),\n              Set(signer),\n            )\n          case tx: DisbandCreatorDao =>\n            (\n              t.copy(\n                subType = Some(\"DisbandCreatorDao\"),\n              ),\n              Set(signer),\n            )\n          case tx: ReplaceCoordinator =>\n            (\n              t.copy(\n                subType = Some(\"ReplaceCoordinator\"),\n              ),\n              Set(signer, tx.newCoordinator),\n            )\n          case tx: AddMembers =>\n            (\n              t.copy(\n                subType = Some(\"AddMembers\"),\n              ),\n              tx.members + signer,\n            )\n          case tx: RemoveMembers =>\n            (\n              t.copy(\n                subType = Some(\"RemoveMembers\"),\n              ),\n              tx.members + signer,\n            )\n          case tx: PromoteModerators =>\n            (\n              t.copy(\n                subType = Some(\"PromoteModerators\"),\n              ),\n              tx.members + signer,\n            )\n          case tx: DemoteModerators =>\n            (\n              t.copy(\n                subType = Some(\"DemoteModerators\"),\n              ),\n              tx.members + signer,\n            )\n\n      def addNftQueue(hash: TxHash, tx: TransactionWithResult): F[Unit] =\n        tx.signedTx.value match\n          case v: MintNFT =>\n            nftQ.offer((hash, v, tx.signedTx.sig.account))\n          case v: MintNFTWithMemo =>\n            nftQ.offer((hash, v, tx.signedTx.sig.account))\n          case v: TransferNFT =>\n            nftQ.offer((hash, v, tx.signedTx.sig.account))\n          case v: EntrustNFT =>\n            nftQ.offer((hash, v, tx.signedTx.sig.account))\n          case v: DisposeEntrustedNFT =>\n            nftQ.offer((hash, v, tx.signedTx.sig.account))\n          case _ => Async[F].unit\n\n      def addBalanceQueue(hash: TxHash, tx: TransactionWithResult): F[Unit] =\n        tx.signedTx.value match\n          case _: MintFungibleToken             => balQ.offer(hash -> tx)\n          case _: BurnFungibleToken             => balQ.offer(hash -> tx)\n          case _: TransferFungibleToken         => balQ.offer(hash -> tx)\n          case _: EntrustFungibleToken          => balQ.offer(hash -> tx)\n          case _: DisposeEntrustedFungibleToken => balQ.offer(hash -> tx)\n          case _: OfferReward                   => balQ.offer(hash -> tx)\n          case _                                => Async[F].unit\n"
  },
  {
    "path": "modules/lmscan-agent/src/main/scala/io/leisuremeta/chain/lmscan/agent/apps/SummaryStoreApp.scala",
    "content": "package io.leisuremeta.chain.lmscan.agent.apps\n\nimport cats.effect.*\nimport io.leisuremeta.chain.lmscan.agent.service.RemoteStoreApp\nimport io.leisuremeta.chain.lmscan.agent.service.RequestServiceApp\nimport io.circe.Decoder\nimport io.circe.generic.semiauto.*\nimport cats.implicits.*\nimport scala.concurrent.duration.*\nimport io.leisuremeta.chain.lmscan.agent.ESConfig\nimport io.leisuremeta.chain.lmscan.agent.MarketConfig\nimport cats.data.EitherT\n\ntrait SummaryStoreApp[F[_]]:\n  def run: F[Unit]\n\nobject SummaryStoreApp:\n  case class TokenBalance(status: Int, message: String, result: BigDecimal)\n  case class LmPrice(status: MarketStatus, data: TokenMap)\n  case class MarketStatus(error_code: Int, error_message: Option[String])\n  case class TokenMap(`20315`: MarketData)\n  case class MarketData(\n      id: Int,\n      name: String,\n      symbol: String,\n      last_updated: String,\n      quote: Currency,\n      circulating_supply: BigDecimal,\n  )\n  case class Currency(USD: USDCurrency)\n  case class USDCurrency(\n      price: BigDecimal,\n      last_updated: String,\n      market_cap: BigDecimal,\n  )\n  given Decoder[LmPrice]      = deriveDecoder[LmPrice]\n  given Decoder[TokenBalance] = deriveDecoder[TokenBalance]\n\n  def build[F[_]: Async](market: MarketConfig, es: ESConfig)(\n      remote: RemoteStoreApp[F],\n      client: RequestServiceApp[F],\n  ): SummaryStoreApp[F] = new SummaryStoreApp[F]:\n    def run =\n      val loop = for\n        _ <- EitherT.liftF:\n          Async[F].delay:\n            scribe.info(\"start summary loop\")\n        balance <- getTotalBalance\n        lmprice <- getLmPriceAndSupply\n        data   = lmprice.data.`20315`\n        supply = data.circulating_supply\n        cap    = data.quote.USD.market_cap\n        price  = data.quote.USD.price\n        _ <- EitherT(\n          remote.summary\n            .updateSummary(\n              balance,\n              cap,\n              supply,\n              price,\n            ),\n        ).leftMap(_.getMessage)\n        _ <- EitherT(\n          remote.summary.updateValidatorInfo,\n        ).leftMap(_.getMessage)\n      yield ()\n\n      for\n        _ <- Async[F].sleep(10.minutes)\n        _ <- loop.value\n        r <- run\n      yield r\n\n    def getLmPriceAndSupply =\n      client\n        .getResultFromKeyApi[LmPrice](\n          s\"https://pro-api.coinmarketcap.com/v2/cryptocurrency/quotes/latest?id=${market.token}\",\n          Map(\"X-CMC_PRO_API_KEY\" -> market.key),\n        )\n\n    def getTotalBalance =\n      def url(addr: String) =\n        s\"https://api.etherscan.io/v2/api?chainid=1&module=account&action=tokenbalance&contractaddress=${es.lm}&address=${addr}&tag=latest&apikey=${es.key}\"\n      es.addrs\n        .map(addr => client.getResult[TokenBalance](url(addr)))\n        .parSequence\n        .map(\n          _.map(_.result)\n            .fold(BigDecimal(0))((acc, x) => acc + x),\n        )\n"
  },
  {
    "path": "modules/lmscan-agent/src/main/scala/io/leisuremeta/chain/lmscan/agent/service/RequestService.scala",
    "content": "package io.leisuremeta.chain.lmscan.agent.service\n\nimport sttp.client3.*\nimport cats.effect.kernel.Async\nimport io.circe.Decoder\nimport io.circe.parser.decode\nimport cats.implicits.toFunctorOps\nimport cats.data.EitherT\n\ntrait RequestServiceApp[F[_]: Async]:\n  def getResult[A](url: String)(using Decoder[A]): EitherT[F, String, A]\n  def getResultWithJsonString[A](url: String)(using\n      Decoder[A],\n  ): EitherT[F, String, (A, String)]\n  def getResultFromKeyApi[A](url: String, hs: Map[String, String])(using\n      Decoder[A],\n  ): EitherT[F, String, A]\n\nobject RequestService:\n  def parseRes(\n      res: Response[Either[String, String]],\n  ): Either[String, String] = res.body\n\n  def parseJson[A](json: String)(using Decoder[A]): Either[String, A] =\n    decode(json) match\n      case Left(v)  => Left(v.toString)\n      case Right(v) => Right(v)\n\n  def build[F[_]: Async](backend: SttpBackend[F, Any]) =\n    new RequestServiceApp[F]:\n      def getResult[A](url: String)(using Decoder[A]) =\n        getResultWithJsonString(url).fmap: v =>\n          v._1\n\n      def getResultWithJsonString[A](url: String)(using Decoder[A]) =\n        EitherT.apply:\n          basicRequest\n            .get(uri\"$url\")\n            .send(backend)\n            .map: res =>\n              for\n                body <- parseRes(res)\n                json <- parseJson[A](body)\n              yield (json, body)\n\n      def getResultFromKeyApi[A](url: String, hs: Map[String, String])(using\n          Decoder[A],\n      ) =\n        EitherT.apply:\n          basicRequest\n            .get(uri\"$url\")\n            .headers(hs)\n            .send(backend)\n            .map: res =>\n              for\n                body <- parseRes(res)\n                json <- parseJson[A](body)\n              yield json\n"
  },
  {
    "path": "modules/lmscan-agent/src/main/scala/io/leisuremeta/chain/lmscan/agent/service/StoreService.scala",
    "content": "package io.leisuremeta.chain.lmscan.agent\npackage service\n\nimport doobie.util.transactor.Transactor\nimport doobie.*\nimport cats.*\nimport cats.effect.*\n\ntrait RemoteStoreApp[F[_]: MonadCancelThrow]:\n  val blcRepo: BlockRepository[F]\n  val txRepo: TxRepository[F]\n  val summary: SummaryRepository[F]\n  val nftRepo: NftRepository[F]\n  val balRepo: BalanceRepository[F]\n  val accRepo: AccountRepository[F]\ntrait LocalStoreApp[F[_]: MonadCancelThrow]:\n  val balRepo: LocalBalanceRepository[F]\n\nobject StoreService:\n  def buildRemote[F[_]: MonadCancelThrow](xa: Transactor[F]) =\n    new RemoteStoreApp[F]:\n      val blcRepo: BlockRepository[F]   = BlockRepository.build(xa)\n      val txRepo: TxRepository[F]       = TxRepository.build(xa)\n      val summary: SummaryRepository[F] = SummaryRepository.build(xa)\n      val nftRepo: NftRepository[F]     = NftRepository.build(xa)\n      val balRepo: BalanceRepository[F] = BalanceRepository.build(xa)\n      val accRepo: AccountRepository[F] = AccountRepository.build(xa)\n\n  def buildLocal[F[_]: MonadCancelThrow](xa: Transactor[F]) =\n    new LocalStoreApp[F]:\n      val balRepo: LocalBalanceRepository[F] = LocalBalanceRepository.build(xa)\n"
  },
  {
    "path": "modules/lmscan-agent/src/main/scala/io/leisuremeta/chain/lmscan/agent/service/store/AccountStore.scala",
    "content": "package io.leisuremeta.chain.lmscan.agent\npackage service\n\nimport doobie.util.transactor.Transactor\nimport doobie.*\nimport doobie.implicits.*\nimport cats.*\nimport cats.effect.*\nimport io.leisuremeta.chain.api.model.Account\n\ncase class AccountRepository[F[_]: MonadCancelThrow](xa: Transactor[F]):\n  def putAccountMapper(\n      hash: String,\n      accounts: Set[Account],\n  ) =\n    Update[(String, String)](s\"\"\"insert into account_mapper (hash, address) \n    values(?, ?) on conflict (hash, address) do nothing\"\"\")\n      .updateMany(accounts.toList.map(a => (hash, a.toString)))\n      .transact(xa)\n      .attemptSql\n\nobject AccountRepository:\n  def build[F[_]: MonadCancelThrow](xa: Transactor[F]) = AccountRepository(xa)\n"
  },
  {
    "path": "modules/lmscan-agent/src/main/scala/io/leisuremeta/chain/lmscan/agent/service/store/BalanceStore.scala",
    "content": "package io.leisuremeta.chain.lmscan.agent\npackage service\n\nimport doobie.util.transactor.Transactor\nimport doobie.*\nimport doobie.implicits.*\nimport cats.*\nimport cats.effect.*\nimport io.leisuremeta.chain.lmscan.agent.apps.*\nimport cats.data.NonEmptyList\n\ngiven inputWrite: Write[LedgerType.InputLedger] =\n  Write[(String, String, String)].contramap: v =>\n    (v.hash, v.address, v.free.toString)\ngiven ledgerWrite: Write[LedgerType.SpendLedger] =\n  Write[(String, String, String)].contramap: v =>\n    (v.hash, v.address, v.used)\ngiven lockWrite: Write[LedgerType.LockLedger] =\n  Write[(String, String, String)].contramap: v =>\n    (v.hash, v.address, v.locked.toString)\ngiven returnWrite: Write[LedgerType.SpendLockLedger] =\n  Write[(String, String)].contramap: v =>\n    (v.hash, v.used)\n\ncase class BalanceRepository[F[_]: MonadCancelThrow](xa: Transactor[F]):\n  def updateBalance(txs: List[(String, BigDecimal)]) =\n    Update[(String, BigDecimal)](\n      \"insert into balance (address ,free, updated_at) values(?, ?, EXTRACT(epoch FROM now())) on conflict (address) do update set free = excluded.free, updated_at = EXTRACT(epoch FROM now())\",\n    ).updateMany(txs).transact(xa).attemptSql\n\ncase class LocalBalanceRepository[F[_]: MonadCancelThrow](xa: Transactor[F]):\n  def createLedgerTable =\n    sql\"\"\"create table if not exists ledger (\n          hash text,\n          address text,\n          free text,\n          used text,\n          primary key (hash, address)\n        );\"\"\".update.run.transact(xa)\n\n  def createLockLedgerTable =\n    sql\"\"\"create table if not exists lock_ledger (\n          hash text,\n          address text,\n          locked text,\n          used text,\n          primary key (hash)\n        );\"\"\".update.run.transact(xa)\n\n  def addInputLedger(txs: List[LedgerType.InputLedger]) =\n    Update[LedgerType.InputLedger](\n      \"insert into ledger (hash, address, free) values(?, ?, ?) on conflict (hash, address) do update set free = excluded.free\",\n    ).updateMany(txs).transact(xa).attemptSql\n\n  def addSpendLedger(txs: List[LedgerType.SpendLedger]) =\n    Update[LedgerType.SpendLedger](\n      \"insert into ledger (hash, address, used) values(?, ?, ?) on conflict (hash, address) do update set used = excluded.used\",\n    ).updateMany(txs).transact(xa).attemptSql\n\n  def addLockLedger(txs: List[LedgerType.LockLedger]) =\n    Update[LedgerType.LockLedger](\n      \"insert into lock_ledger (hash, address, locked) values(?, ?, ?) on conflict (hash) do update set address = excluded.address, locked = excluded.locked\",\n    ).updateMany(txs).transact(xa).attemptSql\n\n  def addSpendLockLedger(txs: List[LedgerType.SpendLockLedger]) =\n    Update[LedgerType.SpendLockLedger](\n      \"insert into lock_ledger (hash, used) values(?, ?) on conflict (hash) do update set used = excluded.used\",\n    ).updateMany(txs).transact(xa).attemptSql\n\n  def getLedger(\n      qs: NonEmptyList[String],\n  ) =\n    val q =\n      fr\"\"\"select address, free from ledger where used is null and free is not null and \"\"\"\n        ++ Fragments\n          .in(fr\"address\", qs)\n\n    q.query[(String, String)]\n      .to[List]\n      .transact(xa)\n      .attemptSql\n\nobject BalanceRepository:\n  def build[F[_]: MonadCancelThrow](xa: Transactor[F]) =\n    BalanceRepository(xa)\n\nobject LocalBalanceRepository:\n  def build[F[_]: MonadCancelThrow](xa: Transactor[F]) =\n    LocalBalanceRepository(xa)\n"
  },
  {
    "path": "modules/lmscan-agent/src/main/scala/io/leisuremeta/chain/lmscan/agent/service/store/BlockStore.scala",
    "content": "package io.leisuremeta.chain.lmscan.agent\npackage service\n\nimport doobie.util.transactor.Transactor\nimport doobie.*\nimport doobie.implicits.*\nimport cats.*\nimport cats.effect.*\nimport io.leisuremeta.chain.lmscan.backend.entity.Block\n\ncase class BlockRepository[F[_]: MonadCancelThrow](xa: Transactor[F]):\n  def getLatestBlock =\n    sql\"select number, hash, parent_hash, tx_count, event_time, created_at, proposer from block order by number desc limit 1\"\n      .query[Block]\n      .option\n      .transact(xa)\n      .attemptSql\n\n  def getLowestBlock =\n    sql\"select number, hash, parent_hash, tx_count, event_time, created_at, proposer from block order by number limit 1\"\n      .query[Block]\n      .option\n      .transact(xa)\n      .attemptSql\n\n  def putBlock(hash: String, num: Long, pHash: String, txC: Int, et: Long) =\n    sql\"\"\"insert into block (hash, number, parent_hash, tx_count, event_time, proposer) values(\n        $hash, $num, $pHash, $txC, $et, (select address from validator_info where ${et % 4} = id))\"\"\".update.run\n      .transact(xa)\n\nobject BlockRepository:\n  def build[F[_]: MonadCancelThrow](xa: Transactor[F]) =\n    BlockRepository(xa)\n"
  },
  {
    "path": "modules/lmscan-agent/src/main/scala/io/leisuremeta/chain/lmscan/agent/service/store/NftStore.scala",
    "content": "package io.leisuremeta.chain.lmscan.agent\npackage service\n\nimport doobie.util.transactor.Transactor\nimport doobie.*\nimport doobie.implicits.*\nimport cats.*\nimport cats.effect.*\nimport io.leisuremeta.chain.lmscan.agent.apps.*\nimport io.leisuremeta.chain.lmscan.agent.apps.NftApp.{Nft, NftFile, NftOwner}\n\ngiven nftWrite: Write[Nft] =\n  Write[(String, String, String, String, String, Long)].contramap(nft =>\n    (\n      nft.txHash,\n      nft.tokenId,\n      nft.action,\n      nft.fromAddr,\n      nft.toAddr,\n      nft.eventTime,\n    ),\n  )\ngiven nftFileWrite: Write[NftFile] = Write[\n  (\n      String,\n      String,\n      String,\n      String,\n      String,\n      String,\n      String,\n      String,\n      String,\n      Long,\n  ),\n].contramap(nft =>\n  (\n    nft.tokenId,\n    nft.tokenDefId,\n    nft.collectionName,\n    nft.nftName,\n    nft.nftUri,\n    nft.creatorDescription,\n    nft.dataUrl,\n    nft.rarity,\n    nft.creator,\n    nft.eventTime,\n  ),\n)\ngiven nftOwnerWrite: Write[NftOwner] =\n  Write[(String, String, Long)].contramap(nft =>\n    (nft.tokenId, nft.ownerAddr, nft.eventTime),\n  )\n\ncase class NftRepository[F[_]: MonadCancelThrow](xa: Transactor[F]):\n  def putNftList(nft: List[Nft]) =\n    Update[Nft](\n      \"\"\"insert into nft (tx_hash, token_id, action, from_addr, to_addr, event_time) \n        values(?, ?, ?, ?, ?, ?) on conflict (tx_hash) do nothing\"\"\",\n    ).updateMany(nft).transact(xa).attemptSql\n\n  def putNftFileList(nft: List[NftFile]) =\n    Update[NftFile](\n      \"insert into nft_file (token_id, token_def_id, collection_name, nft_name, nft_uri, creator_description, data_url, rarity, creator, event_time) values(?, ?, ?, ?, ?, ?, ?, ?, ?, ?) on conflict (token_id) do nothing\",\n    ).updateMany(nft).transact(xa).attemptSql\n\n  def putNftOwnerList(nft: List[NftOwner]) =\n    Update[NftOwner](\n      \"insert into nft_owner (token_id, owner, event_time) values(?,?,?) on conflict (token_id) do update set owner = excluded.owner where excluded.event_time > nft_owner.event_time\",\n    ).updateMany(nft).transact(xa).attemptSql\n\nobject NftRepository:\n  def build[F[_]: MonadCancelThrow](xa: Transactor[F]) = NftRepository(xa)\n"
  },
  {
    "path": "modules/lmscan-agent/src/main/scala/io/leisuremeta/chain/lmscan/agent/service/store/SummaryStore.scala",
    "content": "package io.leisuremeta.chain.lmscan.agent\npackage service\n\nimport doobie.util.transactor.Transactor\nimport doobie.*\nimport doobie.implicits.*\nimport cats.*\nimport cats.effect.*\n\ncase class SummaryRepository[F[_]: MonadCancelThrow](xa: Transactor[F]):\n  def updateValidatorInfo =\n    sql\"\"\"update validator_info set cnt = v.c from (select proposer p, count(1) c from block group by p) v\n        where validator_info.address = v.p\"\"\".update.run\n      .transact(xa)\n      .attemptSql\n\n  def updateSummary(\n      balance: BigDecimal,\n      cap: BigDecimal,\n      supply: BigDecimal,\n      price: BigDecimal,\n  ) =\n    sql\"\"\"\n        INSERT INTO summary (lm_price, total_balance, market_cap, cir_supply, block_number, total_accounts, total_tx_size, total_nft)\n        VALUES($price, $balance, $cap, $supply, \n          (SELECT number FROM block ORDER BY number DESC LIMIT 1),\n          (SELECT count(1) FROM tx WHERE sub_type = 'CreateAccount' or sub_type = 'CreateAccountWithExternalChainAddresses'),\n          (SELECT count(1) FROM tx),\n          (SELECT count(1) FROM nft)\n        )\"\"\".update.run\n      .transact(xa)\n      .attemptSql\n\nobject SummaryRepository:\n  def build[F[_]: MonadCancelThrow](xa: Transactor[F]) =\n    SummaryRepository(xa)\n"
  },
  {
    "path": "modules/lmscan-agent/src/main/scala/io/leisuremeta/chain/lmscan/agent/service/store/TxStore.scala",
    "content": "package io.leisuremeta.chain.lmscan.agent\npackage service\n\nimport doobie.util.transactor.Transactor\nimport doobie.*\nimport doobie.implicits.*\nimport cats.*\nimport cats.effect.*\nimport io.leisuremeta.chain.api.model.TransactionWithResult\nimport io.leisuremeta.chain.lmscan.agent.apps.Tx\n\ncase class TxRepository[F[_]: MonadCancelThrow](xa: Transactor[F]):\n  def putTxState(\n      hash: String,\n      bHash: String,\n      tx: TransactionWithResult,\n      json: String,\n  ) =\n    sql\"\"\"insert into tx_state (hash, block_hash, json, event_time) values(\n        $hash, $bHash, $json, ${tx.signedTx.value.createdAt.getEpochSecond})\"\"\".update.run\n      .transact(xa)\n      .attemptSql\n  def putTx(\n      tx: Tx,\n  ) =\n    sql\"\"\"insert into tx (hash, signer, token_type, tx_type, sub_type, block_hash, block_number, event_time) \n    values(${tx.hash}, ${tx.signer}, ${tx.tokenType}, ${tx.txType}, ${tx.subType}, ${tx.blockHash}, ${tx.blockNumber}, ${tx.eventTime})\"\"\".update.run\n      .transact(xa)\n      .attemptSql\n\nobject TxRepository:\n  def build[F[_]: MonadCancelThrow](xa: Transactor[F]) = TxRepository(xa)\n"
  },
  {
    "path": "modules/lmscan-backend/docs/flyway.md",
    "content": "<flyway command with sbt>\n1. sbt flywayBaseline -> baselineVersion까지의 migration 제외한 현재 db의 baseline 지정 \n2. sbt flywayClean -> 해당 schema 모든 내용 삭제\n3. sbt flywayMigrate -> baselineVersion 이후 migration version file update\n4. sbt flywayInfo -> migration status info 출력\n5. sbt flywayRepair -> flyway_schema_history table 수정 (DDL 트랜젝션 없이 db에 실패한 migration 삭제 및 잘못된 checksum 수정)\n\n\n<use case>\n1. 처음 빈 스키마의 경우 -> sbt flywayMigrate\n2. 처음 빈 스키마가 아닌 경우\n    (1) 스키마를 모두 비우고 해당 migration file로 다시 시작하는 경우 -> sbt flywayClean && sbt flywayMigrate\n    (2) sbt flywayBaseline으로 flyway_schema_history 생성 후 sbt flywayMigrate 통해 특정 버전 이후부터 관리 및 적용 가능\n3. 이후부터는 sbt flywayMigrate 명령어를 통해 변경된 migration이 있을경우마다 반영가능\n\n\n<refs>\n1. https://davidmweber.github.io/flyway-sbt-docs/repair.html\n2. https://flywaydb.org/documentation/command/migrate\n\n"
  },
  {
    "path": "modules/lmscan-backend/src/main/resources/application.sample.properties",
    "content": "ctx.dataSourceClassName=org.postgresql.ds.PGSimpleDataSource\nctx.url=postgresql://...\nctx.connectTimeout=30000s\nctx.testTimeout=10s\nctx.queryTimeout=10s\nctx.getBalanceapi_url = \"\"\n"
  },
  {
    "path": "modules/lmscan-backend/src/main/resources/db/\bdist/V20230116164800__Alter_ts_pgdefault.sql",
    "content": "-- Tablespace: pg_default\n\n-- DROP TABLESPACE IF EXISTS pg_default;\n\nALTER TABLESPACE pg_default OWNER TO rdsadmin;\n"
  },
  {
    "path": "modules/lmscan-backend/src/main/resources/db/\bdist/V20230116164800__Alter_ts_pgglobal.sql",
    "content": "-- Tablespace: pg_global\n\n-- DROP TABLESPACE IF EXISTS pg_global;\n\nALTER TABLESPACE pg_global OWNER TO rdsadmin;\n"
  },
  {
    "path": "modules/lmscan-backend/src/main/resources/db/\bdist/V20230116164801__Create_r_playnomm.sql",
    "content": "-- Role: playnomm\n-- DROP ROLE IF EXISTS playnomm;\n\nCREATE ROLE playnomm WITH\n  LOGIN\n  NOSUPERUSER\n  INHERIT\n  CREATEDB\n  CREATEROLE\n  NOREPLICATION\n  VALID UNTIL 'infinity';\n  \nGRANT rds_superuser TO playnomm;\n"
  },
  {
    "path": "modules/lmscan-backend/src/main/resources/db/common/V20230116164802__Create_t_account.sql",
    "content": "-- Table: public.account\n\n-- DROP TABLE IF EXISTS public.account;\n\nCREATE TABLE IF NOT EXISTS public.account\n(\n    id bigint NOT NULL,\n    address character varying COLLATE pg_catalog.\"default\",\n    balance numeric(28,18),\n    amount numeric(11,2),\n    type character varying COLLATE pg_catalog.\"default\",\n    created_at bigint\n)\nTABLESPACE pg_default;\nALTER TABLE IF EXISTS public.account OWNER to playnomm;\n"
  },
  {
    "path": "modules/lmscan-backend/src/main/resources/db/common/V20230116164803__Create_t_block.sql",
    "content": "-- Table: public.block\n\n-- DROP TABLE IF EXISTS public.block;\n\nCREATE TABLE IF NOT EXISTS public.block\n(\n    id bigint NOT NULL,\n    \"number\" bigint NOT NULL,\n    hash character varying COLLATE pg_catalog.\"default\" NOT NULL,\n    parent_hash character varying COLLATE pg_catalog.\"default\",\n    tx_count bigint NOT NULL,\n    event_time bigint NOT NULL,\n    created_at bigint NOT NULL,\n    CONSTRAINT block_pkey PRIMARY KEY (id)\n)\nTABLESPACE pg_default;\nALTER TABLE IF EXISTS public.block OWNER to playnomm;\nCOMMENT ON COLUMN public.block.event_time IS '블록 발생시간';\n"
  },
  {
    "path": "modules/lmscan-backend/src/main/resources/db/common/V20230116164805__Create_t_nft.sql",
    "content": "-- SEQUENCE: public.nft_id_seq\n-- DROP SEQUENCE IF EXISTS public.nft_id_seq;\nCREATE SEQUENCE IF NOT EXISTS public.nft_id_seq\n    INCREMENT 1\n    START 1\n    MINVALUE 1\n    MAXVALUE 9223372036854775807\n    CACHE 1;\nALTER SEQUENCE public.nft_id_seq OWNER TO playnomm;\n\n-- Table: public.nft\n-- DROP TABLE IF EXISTS public.nft;\nCREATE TABLE IF NOT EXISTS public.nft\n(\n    id bigint NOT NULL DEFAULT nextval('nft_id_seq'::regclass),\n    token_id bigint,\n    tx_hash character varying(64) COLLATE pg_catalog.\"default\" NOT NULL,\n    rarity character varying(32) COLLATE pg_catalog.\"default\",\n    owner character varying(40) COLLATE pg_catalog.\"default\",\n    action character varying(32) COLLATE pg_catalog.\"default\",\n    \"from\" character varying(40) COLLATE pg_catalog.\"default\",\n    \"to\" character varying(40) COLLATE pg_catalog.\"default\",\n    event_time bigint,\n    created_at bigint,\n    CONSTRAINT nft_pkey PRIMARY KEY (tx_hash)\n)\nTABLESPACE pg_default;\nALTER TABLE IF EXISTS public.nft OWNER to playnomm;\nALTER SEQUENCE IF EXISTS public.nft_id_seq OWNED BY nft.id\n"
  },
  {
    "path": "modules/lmscan-backend/src/main/resources/db/common/V20230116164809__Create_t_transaction.sql",
    "content": "-- Table: public.transaction\n\n-- DROP TABLE IF EXISTS public.transaction;\n\nCREATE TABLE IF NOT EXISTS public.transaction\n(\n    id bigint NOT NULL GENERATED ALWAYS AS IDENTITY ( INCREMENT 1 START 1 MINVALUE 1 MAXVALUE 9223372036854775807 CACHE 1 ),\n    hash character varying(64) COLLATE pg_catalog.\"default\" NOT NULL,\n    type character varying(32) COLLATE pg_catalog.\"default\" NOT NULL,\n    \"from\" character varying(64) COLLATE pg_catalog.\"default\" NOT NULL,\n    \"to\" character varying[] COLLATE pg_catalog.\"default\" NOT NULL,\n    value numeric(28,18) NOT NULL,\n    block_hash character varying(64) COLLATE pg_catalog.\"default\" NOT NULL,\n    event_time bigint NOT NULL,\n    created_at bigint NOT NULL,\n    CONSTRAINT transaction_pkey PRIMARY KEY (id)\n)\nTABLESPACE pg_default;\nALTER TABLE IF EXISTS public.transaction OWNER to playnomm;\n"
  },
  {
    "path": "modules/lmscan-backend/src/main/resources/db/seed/R__001_Seed_account.sql",
    "content": "INSERT INTO account(id, balance, amount, created_at) \nVALUES  (1, 1.1, 1.1, 20230116), \n        (2, 2.2, 2.2, 20230116), \n        (3, 3.3, 3.3, 20230116);\n-- ON CONFLICT(id) DO NOTHING;\n"
  },
  {
    "path": "modules/lmscan-backend/src/main/resources/db/test/V20230116164801__Create_r_playnomm.sql",
    "content": "-- Role: playnomm\n-- DROP ROLE IF EXISTS playnomm;\nDROP ROLE IF EXISTS playnomm;\nCREATE ROLE playnomm WITH\n  LOGIN\n  NOSUPERUSER\n  INHERIT\n  CREATEDB\n  CREATEROLE\n  NOREPLICATION\n  VALID UNTIL 'infinity';\n  \n-- GRANT rds_superuser TO playnomm;\n"
  },
  {
    "path": "modules/lmscan-backend/src/main/scala/io/leisuremeta/chain/lmscan/backend/LmscanBackendMain.scala",
    "content": "package io.leisuremeta.chain.lmscan\npackage backend\n\nimport cats.effect.{ExitCode, IO, IOApp, Resource}\nimport cats.effect.std.Dispatcher\nimport com.linecorp.armeria.server.Server\nimport sttp.capabilities.fs2.Fs2Streams\nimport sttp.tapir.server.ServerEndpoint\nimport sttp.tapir.server.armeria.cats.ArmeriaCatsServerInterpreter\nimport sttp.tapir.*\nimport common.ExploreApi\nimport common.model.PageNavigation\nimport io.leisuremeta.chain.lmscan.backend.service.*\nimport cats.effect.Async\nimport com.linecorp.armeria.server.HttpService;\nimport sttp.tapir.server.armeria.cats.ArmeriaCatsServerOptions\nimport sttp.tapir.server.interceptor.cors.CORSInterceptor\nimport sttp.tapir.server.interceptor.cors.CORSConfig\nimport sttp.tapir.server.interceptor.log.DefaultServerLog\n\nobject BackendMain extends IOApp:\n\n  def txPaging[F[_]: Async]: ServerEndpoint[Fs2Streams[F], F] =\n    ExploreApi.getTxPageEndPoint.serverLogic {\n      (\n          pageInfo,\n      ) =>\n        TransactionService\n          .getPage[F](pageInfo)\n          .leftMap:\n            case Right(msg) => Right(ExploreApi.BadRequest(msg))\n            case Left(msg) => Left(ExploreApi.ServerError(msg))\n        .value\n    }\n\n  def txDetail[F[_]: Async]: ServerEndpoint[Fs2Streams[F], F] =\n    ExploreApi.getTxDetailEndPoint.serverLogic { (hash: String) =>\n      TransactionService\n        .getDetail(hash)\n        .leftMap:\n          case Right(msg) => Right(ExploreApi.BadRequest(msg))\n          case Left(msg) => Left(ExploreApi.ServerError(msg))\n        .value\n    }\n  def blockPaging[F[_]: Async]: ServerEndpoint[Fs2Streams[F], F] =\n    ExploreApi.getBlockPageEndPoint.serverLogic { (pageInfo: PageNavigation) =>\n      BlockService\n        .getPage[F](pageInfo)\n        .leftMap:\n          case Right(msg) => Right(ExploreApi.BadRequest(msg))\n          case Left(msg) => Left(ExploreApi.ServerError(msg))\n        .value\n    }\n\n  def blockDetail[F[_]: Async]: ServerEndpoint[Fs2Streams[F], F] =\n    ExploreApi.getBlockDetailEndPoint.serverLogic { (hash: String, p: Option[Int]) =>\n      BlockService\n        .getDetail(hash, p.getOrElse(1))\n        .leftMap:\n          case Right(msg) => Right(ExploreApi.BadRequest(msg))\n          case Left(msg) => Left(ExploreApi.ServerError(msg))\n        .value\n    }\n\n  def accountPaging[F[_]: Async]: ServerEndpoint[Fs2Streams[F], F] =\n    ExploreApi.getAccountPageEndPoint.serverLogic { (pageInfo: PageNavigation) =>\n      AccountService\n        .getPage[F](pageInfo)\n        .leftMap:\n          case Right(msg) => Right(ExploreApi.BadRequest(msg))\n          case Left(msg) => Left(ExploreApi.ServerError(msg))\n        .value\n    }\n\n  def accountDetail[F[_]: Async]: ServerEndpoint[Fs2Streams[F], F] =\n    ExploreApi.getAccountDetailEndPoint.serverLogic { (address: String, p: Option[Int]) =>\n      AccountService\n        .get(address, p.getOrElse(1))\n        .leftMap:\n          case Right(msg) => Right(ExploreApi.BadRequest(msg))\n          case Left(msg) => Left(ExploreApi.ServerError(msg))\n        .value\n    }\n\n  def nftDetail[F[_]: Async]: ServerEndpoint[Fs2Streams[F], F] =\n    ExploreApi.getNftDetailEndPoint.serverLogic { (tokenId: String) =>\n      NftService\n        .getNftDetail(tokenId)\n        .leftMap:\n          case Right(msg) => Right(ExploreApi.BadRequest(msg))\n          case Left(msg) => Left(ExploreApi.ServerError(msg))\n        .value\n    }\n\n  def nftSeasonPaging[F[_]: Async]: ServerEndpoint[Fs2Streams[F], F] =\n    ExploreApi.getNftSeasonEndPoint.serverLogic { (season: String, pageInfo: PageNavigation) =>\n      NftService\n        .getSeasonPage[F](pageInfo, season)\n        .leftMap:\n          case Right(msg) => Right(ExploreApi.BadRequest(msg))\n          case Left(msg) => Left(ExploreApi.ServerError(msg))\n        .value\n    }\n  def nftPaging[F[_]: Async]: ServerEndpoint[Fs2Streams[F], F] =\n    ExploreApi.getNftPageEndPoint.serverLogic { (pageInfo: PageNavigation) =>\n      NftService\n        .getPage[F](pageInfo)\n        .leftMap:\n          case Right(msg) => Right(ExploreApi.BadRequest(msg))\n          case Left(msg) => Left(ExploreApi.ServerError(msg))\n        .value\n    }\n\n  def nftOwnerInfo[F[_]: Async]: ServerEndpoint[Fs2Streams[F], F] =\n    ExploreApi.getNftOwnerInfoEndPoint.serverLogic { (tokenId: String) =>\n      NftService\n        .getNftOwnerInfo(tokenId)\n        .leftMap:\n          case Right(msg) => Right(ExploreApi.BadRequest(msg))\n          case Left(msg) => Left(ExploreApi.ServerError(msg))\n        .value\n    }\n\n\n  def summaryMain[F[_]: Async]: ServerEndpoint[Fs2Streams[F], F] =\n    ExploreApi.getSummaryMainEndPoint.serverLogic { Unit =>\n      SummaryService.getBoard\n        .leftMap:\n          case Right(msg) => Right(ExploreApi.BadRequest(msg))\n          case Left(msg) => Left(ExploreApi.ServerError(msg))\n        .value\n    }\n\n  def summaryChart[F[_]: Async]: ServerEndpoint[Fs2Streams[F], F] =\n    ExploreApi.getSummaryChartEndPoint.serverLogic { (chartType: String) =>\n      val list = chartType match\n        case \"balance\" => SummaryService.getList\n        case _ => SummaryService.get5List\n      \n      list.leftMap:\n          case Right(msg) => Right(ExploreApi.BadRequest(msg))\n          case Left(msg) => Left(ExploreApi.ServerError(msg))\n        .value\n    }\n\n  def keywordSearch[F[_]: Async]: ServerEndpoint[Fs2Streams[F], F] =\n    ExploreApi.getKeywordSearchResult.serverLogic:\n      (keyword: String) =>\n        SearchService\n          .getKeywordSearch(keyword)\n          .leftMap:\n            case Right(msg) => Right(ExploreApi.BadRequest(msg))\n            case Left(msg) => Left(ExploreApi.ServerError(msg))\n          .value\n\n  def valdatorPage[F[_]: Async]: ServerEndpoint[Fs2Streams[F], F] =\n    ExploreApi.getValidators.serverLogic:\n      _ =>\n        ValidatorService\n          .getPage()\n          .leftMap:\n            case Right(msg) => Right(ExploreApi.BadRequest(msg))\n            case Left(msg) => Left(ExploreApi.ServerError(msg))\n          .value\n\n  def valdatorDetail[F[_]: Async]: ServerEndpoint[Fs2Streams[F], F] =\n    ExploreApi.getValidator.serverLogic:\n      (address, p) =>\n        ValidatorService\n          .get(address, p.getOrElse(1))\n          .leftMap:\n            case Right(msg) => Right(ExploreApi.BadRequest(msg))\n            case Left(msg) => Left(ExploreApi.ServerError(msg))\n          .value\n\n  def explorerEndpoints[F[_]: Async]: List[ServerEndpoint[Fs2Streams[F], F]] =\n    List(\n      txPaging[F],\n      txDetail[F],\n      blockPaging[F],\n      blockDetail[F],\n      accountPaging[F],\n      accountDetail[F],\n      nftPaging[F],\n      nftSeasonPaging[F],\n      nftDetail[F],\n      nftOwnerInfo[F],\n      summaryMain[F],\n      summaryChart[F],\n      keywordSearch[F],\n      valdatorPage[F],\n      valdatorDetail[F],\n    )\n\n  def getServerResource[F[_]: Async]: Resource[F, Server] =\n    for\n      dispatcher <- Dispatcher.parallel[F]\n      server <- Resource.fromAutoCloseable:\n        def log[F[_]: Async](\n            level: scribe.Level,\n          )(msg: String, exOpt: Option[Throwable])(using\n            mdc: scribe.mdc.MDC,\n          ): F[Unit] =\n            Async[F].delay(exOpt match\n              case None     => scribe.log(level, mdc, msg)\n              case Some(ex) => scribe.log(level, mdc, msg, ex),\n          )\n        val serverLog = DefaultServerLog(\n          doLogWhenReceived = log(scribe.Level.Info)(_, None),\n          doLogWhenHandled  = log(scribe.Level.Info),\n          doLogAllDecodeFailures = log(scribe.Level.Error),\n          doLogExceptions = (msg: String, ex: Throwable) => Async[F].delay(scribe.error(msg, ex)),\n          noLog = Async[F].pure(()),\n        )\n        Async[F].fromCompletableFuture:\n          val options = ArmeriaCatsServerOptions\n            .customiseInterceptors(dispatcher)\n            .corsInterceptor(Some {\n              CORSInterceptor\n                .customOrThrow[F](CORSConfig.default)\n            })\n            .serverLog(serverLog)\n            .options\n          \n          val tapirService = ArmeriaCatsServerInterpreter[F](options)\n            .toService(explorerEndpoints[F])\n          val server = Server.builder\n            .annotatedService(tapirService)\n            .http(8081)\n            .maxRequestLength(128 * 1024 * 1024)\n            .requestTimeout(java.time.Duration.ofMinutes(2))\n            .service(tapirService)\n            .build\n          Async[F].delay:\n            scribe.info(\"server start / port: 8081\")\n            server.start().thenApply(_ => server)\n    yield server\n\n  override def run(args: List[String]): IO[ExitCode] =\n    val program: Resource[IO, Server] =\n      for server <- getServerResource[IO]\n      yield server\n\n    program.useForever.as(ExitCode.Success)\n"
  },
  {
    "path": "modules/lmscan-backend/src/main/scala/io/leisuremeta/chain/lmscan/backend/docs/Lmscan_API.md",
    "content": "# Lmscan API\n\n`GET` **/tx/list** 트랜잭션(Tx) 목록 페이지 조회\n\n> `param` pageNo: 페이지 번호\n> `param` sizePerRequest: 페이지 당 출력할 레코드 갯수\n> `param` _(optional)_ accountAddr: 사용자 지갑 주소\n> `param` _(optional)_ blockHash: 블록 해쉬 값\n> (단, accountAddr / blockHash 모두 입력시 에러)\n\n- Response: PageResponse[TxInfo]\n\n* totalCount: 트랜잭션 레코드의 총 갯수\n* totalPages: 'sizePerRequest' 파라미터 값에 따른 총 페이지 번호\n* payload: 요청 페이지의 트랜잭션 목록\n  - hash: 트랜잭션 해쉬 값\n  - blockNumber: 블록 번호\n  - txType: 트랜잭션 형태에 따른 구분 (account / group / token / reward)\n  - tokenType: 토큰 타입 구분 ( LM / NFT )\n  - createdAt: 트랜잭션 생성 시간\n\n- Example (pageNo `0`, sizePerRequest: `3` 으로 요청한 예시)\n  - http://localhost:8081/tx/list?pageNo=0&sizePerRequest=3\n\n```json\n{\n  \"totalCount\": 21,\n  \"totalPages\": 7,\n  \"payload\": [\n    {\n      \"hash\": \"7913b313f68610159bca2cfcc0758a726494c442d8116200e1ec2f459642f2dc\",\n      \"blockNumber\": 14,\n      \"createdAt\": 1673939878,\n      \"txType\": \"account\",\n      \"tokenType\": \"LM\",\n      \"signer\": \"26A463A0ED56A4A97D673A47C254728409C7B002\",\n      \"value\": \"123456789.12345678912345678\"\n    },\n    {\n      \"hash\": \"6913b313f68610159bca2cfcc0758a726494c442d8116200e1ec2f459642f2db\",\n      \"blockNumber\": 12,\n      \"createdAt\": 1673853478,\n      \"txType\": \"account\",\n      \"tokenType\": \"LM\",\n      \"signer\": \"26A463A0ED56A4A97D673A47C254728409C7B002\",\n      \"value\": \"123456789.12345678912345678\"\n    },\n    {\n      \"hash\": \"5913b313f68610159bca2cfcc0758a726494c442d8116200e1ec2f459642f2da\",\n      \"blockNumber\": 15,\n      \"createdAt\": 1673767078,\n      \"txType\": \"account\",\n      \"tokenType\": \"LM\",\n      \"signer\": \"26A463A0ED56A4A97D673A47C254728409C7B002\",\n      \"value\": \"123456789.12345678912345678\"\n    }\n  ]\n}\n```\n\n`GET` **/tx/{transactionHash}/detail** 특정 트랜잭션 상세정보 조회\n\n> `param` transactionHash: 트랜잭션 해쉬 값\n\n- Response: Option[TxDetail]\n\n  - hash: 트랜잭션 해쉬 값\n  - txType: 트랜잭션 형태에 따른 구분 (account / group / token / reward)\n  - signer: 해당 TX의 서명인(발신자)\n  - amount: Tx에 의해 전송되는 LM\n  - createdAt: Tx 가 Lmscan Db에 저장된 시간\n  - eventTime: 트랜잭션 생성 시간\n  - inputHashs: 인풋 트랜잭션 해쉬 목록\n  - transferHist:\n  - json: 트랜잭션의 raw json\n\n  - Example (transactionHash `1513b313f68610159bca2cfcc0758a726494c442d8116200e1ec2f459642f2da` 으로 요청한 예시)\n    - http://localhost:8081/tx/1513b313f68610159bca2cfcc0758a726494c442d8116200e1ec2f459642f2da/detail\n\n```json\n{\n  \"hash\": \"1513b313f68610159bca2cfcc0758a726494c442d8116200e1ec2f459642f2da\",\n  \"createdAt\": 1673767078,\n  \"signer\": \"26A463A0ED56A4A97D673A47C254728409C7B002\",\n  \"txType\": \"account\",\n  \"tokenType\": \"LM\",\n  \"inputHashs\": [\n    \"4913b313f68610159bca2cfcc0758a726494c442d8116200e1ec2f459642f2da\"\n  ],\n  \"transferHist\": [\n    {\n      \"toAddress\": \"b775871c85faae7eb5f6bcebfd28b1e1b412235c\",\n      \"value\": \"123456789.12345678912345678\"\n    },\n    {\n      \"toAddress\": \"b775871c85faae7eb5f6bcebfd28b1e1b412235c\",\n      \"value\": \"123456789.12345678912345678\"\n    }\n  ],\n  \"json\": \"test\"\n}\n```\n\n`GET` **/block/list** 블록 목록 페이지 조회\n\n> `param` pageNo: 페이지 번호\n> `param` sizePerRequest: 페이지 당 출력할 레코드 갯수\n\n- Response: PageResponse[TxInfo]\n\n* totalCount: 트랜잭션 레코드의 총 갯수\n* totalPages: 'sizePerRequest' 파라미터 값에 따른 총 페이지 번호\n* payload: 요청 페이지의 트랜잭션 목록\n  - number: 블록 번호\n  - hash: 블록 해쉬 값\n  - txCount: 트랜잭션 갯수\n  - createdAt: 블록 생성 시간\n\n- Example (pageNo `0`, sizePerRequest: `3` 으로 요청한 예시)\n  - http://localhost:8081/block/list?pageNo=0&sizePerRequest=3\n\n```json\n{\n  \"totalCount\": 2,\n  \"totalPages\": 1,\n  \"payload\": [\n    {\n      \"number\": 123456789,\n      \"hash\": \"6913b313f68610159bca2cfcc0758a726494c442d8116200e1ec2f459642f2da\",\n      \"txCount\": 1234,\n      \"createdAt\": 1675068555\n    },\n    {\n      \"number\": 123456790,\n      \"hash\": \"2913b313f68610159bca2cfcc0758a726494c442d8116200e1ec2f459642f2dc\",\n      \"txCount\": 1234,\n      \"createdAt\": 1675068555\n    }\n  ]\n}\n```\n\n`GET` **/block/{blockHash}/detail** 특정 블록 상세정보 조회\n\n> `param` blockHash: 블록 해쉬 값\n\n- Response: Option[BlockDetail]\n\n  - hash: 블록 해쉬 값\n  - parentHash: 이전 블록 해쉬 값\n  - number: 블록 번호\n  - txCount: 트랜잭션 갯수\n  - createdAt: 블록 생성 시간\n  - txs: 해당 블록의 트랜잭션 목록 ()\n\n  - Example (blockHash `6913b313f68610159bca2cfcc0758a726494c442d8116200e1ec2f459642f2da` 으로 요청한 예시)\n    - http://localhost:8081/block/6913b313f68610159bca2cfcc0758a726494c442d8116200e1ec2f459642f2da/detail\n\n```json\n{\n  \"hash\": \"6913b313f68610159bca2cfcc0758a726494c442d8116200e1ec2f459642f2da\",\n  \"parentHash\": \"7913b313f68610159bca2cfcc0758a726494c442d8116200e1ec2f459642f2db\",\n  \"number\": 123456789,\n  \"timestamp\": 1675068000,\n  \"txCount\": 1234,\n  \"txs\": [\n    {\n      \"hash\": \"7913b313f68610159bca2cfcc0758a726494c442d8116200e1ec2f459642f2dc\",\n      \"blockNumber\": 14,\n      \"createdAt\": 1673939878,\n      \"txType\": \"account\",\n      \"tokenType\": \"LM\",\n      \"signer\": \"26A463A0ED56A4A97D673A47C254728409C7B002\",\n      \"value\": \"123456789.12345678912345678\"\n    },\n    {\n      \"hash\": \"6913b313f68610159bca2cfcc0758a726494c442d8116200e1ec2f459642f2db\",\n      \"blockNumber\": 12,\n      \"createdAt\": 1673853478,\n      \"txType\": \"account\",\n      \"tokenType\": \"LM\",\n      \"signer\": \"26A463A0ED56A4A97D673A47C254728409C7B002\",\n      \"value\": \"123456789.12345678912345678\"\n    },\n    {\n      \"hash\": \"5913b313f68610159bca2cfcc0758a726494c442d8116200e1ec2f459642f2da\",\n      \"blockNumber\": 15,\n      \"createdAt\": 1673767078,\n      \"txType\": \"account\",\n      \"tokenType\": \"LM\",\n      \"signer\": \"26A463A0ED56A4A97D673A47C254728409C7B002\",\n      \"value\": \"123456789.12345678912345678\"\n    }\n  ]\n}\n```\n\n`GET` **/account/{accountAddr}/detail** 특정 어카운트 상세정보 조회\n\n> `param` accountAddr: 어카운트 해쉬 값\n\n- Response: Option[AccountDetail]\n\n  - address: 어카운트 주소\n  - balance: 보유 LM 토큰 수량\n  - value: 해당 토큰 수량의 달러화 환산 가치\n  - txHistory: 해당 어카운트의 트랜잭션 히스토리\n\n  - Example (pageNo `0`, sizePerRequest: `3` 으로 요청한 예시)\n    - http://localhost:8081/account/26A463A0ED56A4A97D673A47C254728409C7B002/detail\n\n```json\n{\n  \"address\": \"26A463A0ED56A4A97D673A47C254728409C7B002\",\n  \"balance\": 100.2222,\n  \"value\": 12.32,\n  \"txHistory\": [\n    {\n      \"hash\": \"7913b313f68610159bca2cfcc0758a726494c442d8116200e1ec2f459642f2dc\",\n      \"blockNumber\": 14,\n      \"createdAt\": 1673939878,\n      \"txType\": \"account\",\n      \"tokenType\": \"LM\",\n      \"signer\": \"26A463A0ED56A4A97D673A47C254728409C7B002\",\n      \"value\": \"123456789.12345678912345678\"\n    },\n    {\n      \"hash\": \"6913b313f68610159bca2cfcc0758a726494c442d8116200e1ec2f459642f2db\",\n      \"blockNumber\": 12,\n      \"createdAt\": 1673853478,\n      \"txType\": \"account\",\n      \"tokenType\": \"LM\",\n      \"signer\": \"26A463A0ED56A4A97D673A47C254728409C7B002\",\n      \"value\": \"123456789.12345678912345678\"\n    },\n    {\n      \"hash\": \"5913b313f68610159bca2cfcc0758a726494c442d8116200e1ec2f459642f2da\",\n      \"blockNumber\": 15,\n      \"createdAt\": 1673767078,\n      \"txType\": \"account\",\n      \"tokenType\": \"LM\",\n      \"signer\": \"26A463A0ED56A4A97D673A47C254728409C7B002\",\n      \"value\": \"123456789.12345678912345678\"\n    }\n  ]\n}\n```\n\n`GET` **/nft/{tokenId}/detail** 특정 NFT 상세정보 조회\n\n> `param` tokenId: nft 토큰 아이디\n\n- Response: Option[NftDetail]\n\n  - nftFile: Nft 파일 정보\n  - activities: 보유 LM 토큰 수량\n\n  - Example (pageNo `0`, sizePerRequest: `3` 으로 요청한 예시)\n    - http://localhost:8081/nft/2022122110000930000002558/detail\n\n```json\n{\n  \"nftFile\": {\n    \"tokenId\": \"2022122110000930000002558\",\n    \"tokenDefId\": \"test-token\",\n    \"collectionName\": \"BPS-JinKei\",\n    \"nftName\": \"#2558\",\n    \"nftUri\": \"https://d2t5puzz68k49j.cloudfront.net/release/collections/BPS_JinKei/NFT_ITEM/CE298DB9-66E4-4258-9A73-A00E09899698.mp4\",\n    \"creatorDescription\": \"It is an Act to Earn NFT based on the artwork of Jin Kei [Block Artist] and Younghoon Shin [Sumukhwa (Ink Wash Painting) Artist].\",\n    \"dataUrl\": \"https://d2t5puzz68k49j.cloudfront.net/release/collections/BPS_JinKei/NFT_ITEM_META/CE298DB9-66E4-4258-9A73-A00E09899698.json\",\n    \"rarity\": \"UNIQ\",\n    \"creator\": \"JinKei\",\n    \"eventTime\": 1675069161,\n    \"createdAt\": 1675069161,\n    \"owner\": \"b775871c85faae7eb5f6bcebfd28b1e1b412235c\"\n  },\n  \"activities\": [\n    {\n      \"txHash\": \"6913b313f68610159bca2cfcc0758a726494c442d8116200e1ec2f459642f2da\",\n      \"action\": \"MintNFT\",\n      \"fromAddr\": \"b775871c85faae7eb5f6bcebfd28b1e1b412235c\",\n      \"toAddr\": \"b775871c85faae7eb5f6bcebfd28b1e1b412235c\",\n      \"createdAt\": 1675068858\n    }\n  ]\n}\n```\n\n`GET` **/summary/main** 조회시점 기준 가장 최근 24시간 이내 통계 데이터 조회\n\n- Response: Option[Summary]\n\n  - Example \n    - http://localhost:8081/summary/main\n\n```json\n{\n  \"id\": 1,\n  \"lmPrice\": 0.394,\n  \"blockNumber\": 123456789,\n  \"txCountInLatest24h\": 123456789,\n  \"totalAccounts\": 123456789,\n  \"createdAt\": 1675612055\n}\n```\n"
  },
  {
    "path": "modules/lmscan-backend/src/main/scala/io/leisuremeta/chain/lmscan/backend/entity/Account.scala",
    "content": "package io.leisuremeta.chain.lmscan.backend.entity\n\nfinal case class Account(\n    address: String,\n    createdAt: Long,\n    eventTime: Long,\n)\n"
  },
  {
    "path": "modules/lmscan-backend/src/main/scala/io/leisuremeta/chain/lmscan/backend/entity/AccountMapper.scala",
    "content": "package io.leisuremeta.chain.lmscan.backend.entity\n\nfinal case class AccountMapper(\n    address: String,\n    hash: String,\n    eventTime: Long,\n)\n"
  },
  {
    "path": "modules/lmscan-backend/src/main/scala/io/leisuremeta/chain/lmscan/backend/entity/Balance.scala",
    "content": "package io.leisuremeta.chain.lmscan.backend.entity\n\nfinal case class Balance(\n    address: String,\n    free: BigDecimal = BigDecimal(0),\n    locked: BigDecimal = BigDecimal(0),\n    updatedAt: Long,\n    createdAt: Long,\n)\n"
  },
  {
    "path": "modules/lmscan-backend/src/main/scala/io/leisuremeta/chain/lmscan/backend/entity/Block.scala",
    "content": "package io.leisuremeta.chain.lmscan.backend.entity\n\nimport io.leisuremeta.chain.lmscan.common.model.BlockInfo\n\nfinal case class Block(\n    number: Long,\n    hash: String,\n    parentHash: String,\n    txCount: Long,\n    eventTime: Long,\n    createdAt: Long,\n    proposer: String\n):\n    def toModel = BlockInfo(\n        Some(number),\n        Some(hash),\n        Some(txCount),\n        Some(createdAt),\n        Some(proposer),\n    )\n"
  },
  {
    "path": "modules/lmscan-backend/src/main/scala/io/leisuremeta/chain/lmscan/backend/entity/CollectionInfo.scala",
    "content": "package io.leisuremeta.chain.lmscan.backend.entity\n\nimport java.util.Date\n\nfinal case class CollectionInfo(\n    tokenDefId: String,\n    season: String,\n    collectionName: String,\n    collectionSn: Int,\n    totalSupply: Option[Int],\n    startDate: Option[Date],\n    endDate: Option[Date],\n    infoProg: Option[Int],\n    thumbUrl: Option[String],\n)\n"
  },
  {
    "path": "modules/lmscan-backend/src/main/scala/io/leisuremeta/chain/lmscan/backend/entity/Nft.scala",
    "content": "package io.leisuremeta.chain.lmscan\npackage backend\npackage entity\n\nfinal case class Nft(\n    txHash: String,\n    action: String,\n    fromAddr: String,\n    toAddr: String,\n    eventTime: Long,\n    createdAt: Long,\n    tokenId: String,\n)\n"
  },
  {
    "path": "modules/lmscan-backend/src/main/scala/io/leisuremeta/chain/lmscan/backend/entity/NftFile.scala",
    "content": "package io.leisuremeta.chain.lmscan.backend.entity\n\nfinal case class NftFile(\n    tokenId: String,\n    tokenDefId: String,\n    collectionName: String,\n    nftName: String,\n    nftUri: String,\n    creatorDescription: String,\n    dataUrl: String,\n    rarity: String,\n    creator: String,\n    eventTime: Long,\n    createdAt: Long,\n    // owner: String,\n)\n"
  },
  {
    "path": "modules/lmscan-backend/src/main/scala/io/leisuremeta/chain/lmscan/backend/entity/NftInfo.scala",
    "content": "package io.leisuremeta.chain.lmscan.backend.entity\n\nimport java.util.Date\n\nfinal case class NftInfo(\n    season: String,\n    seasonName: String,\n    totalSupply: Option[Int],\n    startDate: Option[Date],\n    endDate: Option[Date],\n    thumbUrl: Option[String],\n    sort: Int,\n)\n"
  },
  {
    "path": "modules/lmscan-backend/src/main/scala/io/leisuremeta/chain/lmscan/backend/entity/NftOwner.scala",
    "content": "package io.leisuremeta.chain.lmscan.backend.entity\n\nfinal case class NftOwner(\n    tokenId: String = \"\",\n    owner: String = \"\",\n    createdAt: Long = 0,\n    eventTime: Long = 0,\n)\n"
  },
  {
    "path": "modules/lmscan-backend/src/main/scala/io/leisuremeta/chain/lmscan/backend/entity/NftSeason.scala",
    "content": "package io.leisuremeta.chain.lmscan.backend.entity\n\nimport io.leisuremeta.chain.lmscan.common.model.NftSeasonModel\n\nfinal case class NftSeason(\n    nftName: String,\n    tokenId: String,\n    tokenDefId: String,\n    creator: String,\n    rarity: String,\n    dataUrl: String,\n    collection: String,\n):\n  def toModel: NftSeasonModel =\n    NftSeasonModel(\n      Some(nftName),\n      Some(tokenId),\n      Some(tokenDefId),\n      Some(creator),\n      Some(rarity),\n      Some(dataUrl),\n      Some(collection),\n    )\n"
  },
  {
    "path": "modules/lmscan-backend/src/main/scala/io/leisuremeta/chain/lmscan/backend/entity/Summary.scala",
    "content": "package io.leisuremeta.chain.lmscan.backend.entity\n\nfinal case class Summary(\n    lmPrice: Double,\n    blockNumber: Long,\n    totalAccounts: Long,\n    createdAt: Long,\n    totalTxSize: BigDecimal,\n    totalBalance: BigDecimal,\n    marketCap: Option[BigDecimal],\n    cirSupply: Option[BigDecimal],\n    totalNft: Option[Long],\n)\n"
  },
  {
    "path": "modules/lmscan-backend/src/main/scala/io/leisuremeta/chain/lmscan/backend/entity/Tx.scala",
    "content": "package io.leisuremeta.chain.lmscan.backend.entity\n\nfinal case class Tx(\n    hash: String,\n    signer: String,\n    txType: String, // col_name : type\n    blockHash: String,\n    eventTime: Long,\n    createdAt: Long,\n    tokenType: String,\n    blockNumber: Long,\n    subType: String,\n)\n"
  },
  {
    "path": "modules/lmscan-backend/src/main/scala/io/leisuremeta/chain/lmscan/backend/entity/TxState.scala",
    "content": "package io.leisuremeta.chain.lmscan.backend.entity\n\nfinal case class TxState(\n    hash: String,\n    blockHash: String,\n    json: String,\n    eventTime: Long,\n    createdAt: Long,\n)\n"
  },
  {
    "path": "modules/lmscan-backend/src/main/scala/io/leisuremeta/chain/lmscan/backend/entity/Validator.scala",
    "content": "package io.leisuremeta.chain.lmscan.backend.entity\n\nimport io.leisuremeta.chain.lmscan.common.model.NodeValidator\n\nfinal case class ValidatorInfo(\n    address: String,\n    power: Option[Double],\n    cnt: Option[Long],\n    name: Option[String],\n):\n    def toModel = NodeValidator.Validator(\n        Some(address),\n        power,\n        cnt,\n        name,\n    )\n"
  },
  {
    "path": "modules/lmscan-backend/src/main/scala/io/leisuremeta/chain/lmscan/backend/repository/AccountRepository.scala",
    "content": "package io.leisuremeta.chain.lmscan.backend.repository\n\nimport cats.effect.kernel.Async\nimport cats.data.EitherT\nimport io.getquill.*\nimport io.leisuremeta.chain.lmscan.backend.entity._\nimport io.getquill.autoQuote\nimport io.leisuremeta.chain.lmscan.common.model._\n\nobject AccountRepository extends CommonQuery:\n  import ctx.*\n\n  def get[F[_]: Async](\n      addr: String,\n  ): EitherT[F, String, Option[Balance]] =\n    inline def detailQuery =\n      quote { (addr: String) =>\n        query[Balance]\n          .filter(_.address == addr)\n          .take(1)\n      }\n    optionQuery(detailQuery(lift(addr)))\n\n  def getPage[F[_]: Async](\n      pageNavInfo: PageNavigation,\n  ): EitherT[F, String, PageResponse[Balance]] =\n    val cntQuery = quote {\n      query[Balance]\n        .filter(_.address != \"eth-gateway\") // filter eth-gateway\n    }\n\n    def pagedQuery =\n      quote { (pageNavInfo: PageNavigation) =>\n        val offset         = sizePerRequest * pageNavInfo.pageNo\n        val sizePerRequest = pageNavInfo.sizePerRequest\n\n        query[Balance]\n          .filter(_.address != \"eth-gateway\")\n          .sortBy(a => a.free)(Ord.desc)\n          .drop(offset)\n          .take(sizePerRequest)\n      }\n\n    val res = for\n      a <- countQuery(cntQuery)\n      b <- seqQuery(pagedQuery(lift(pageNavInfo)))\n    yield (a, b)\n\n    res.map { (totalCnt, r) =>\n      val totalPages = calTotalPage(totalCnt, pageNavInfo.sizePerRequest)\n      new PageResponse(totalCnt, totalPages, r)\n    }\n"
  },
  {
    "path": "modules/lmscan-backend/src/main/scala/io/leisuremeta/chain/lmscan/backend/repository/BlockRepository.scala",
    "content": "package io.leisuremeta.chain.lmscan.backend.repository\n\nimport io.leisuremeta.chain.lmscan.common.model.PageNavigation\nimport io.leisuremeta.chain.lmscan.backend.entity.Block\nimport cats.data.EitherT\nimport cats.effect.Async\nimport cats.implicits.*\nimport io.getquill.*\nimport java.sql.SQLException\nobject BlockRepository extends CommonQuery:\n\n  import ctx.*\n  def getPage[F[_]: Async](\n      pageNavInfo: PageNavigation,\n      cnt: Long,\n  ): EitherT[F, String, Seq[Block]] =\n    def pagedQuery =\n      quote { (offset: Long, sizePerRequest: Int) =>\n\n        query[Block]\n          .filter(t => t.number <= offset)\n          .sortBy(t => t.number)(Ord.desc)\n          .take(sizePerRequest)\n      }\n\n    val sizePerRequest = pageNavInfo.sizePerRequest\n    val offset         = sizePerRequest * pageNavInfo.pageNo\n    seqQuery(pagedQuery(lift(cnt - offset), lift(sizePerRequest)))\n\n  def getPageByProposer[F[_]: Async](\n      pageNavInfo: PageNavigation,\n      cnt: Long,\n      addr: String,\n  ): EitherT[F, String, Seq[Block]] =\n    def pagedQuery =\n      quote { (offset: Long, sizePerRequest: Int, s: String) =>\n\n        query[Block]\n          .filter(t => t.proposer == s)\n          .filter(t => t.number <= offset)\n          .sortBy(t => t.number)(Ord.desc)\n          .take(sizePerRequest)\n      }\n\n    val sizePerRequest = pageNavInfo.sizePerRequest\n    seqQuery(pagedQuery(lift(cnt), lift(sizePerRequest), lift(addr)))\n\n  def getLast[F[_]: Async](): EitherT[F, String, Option[Block]] =\n    inline def q =\n      quote { () =>\n        query[Block]\n          .sortBy(t => t.number)(Ord.desc)\n          .take(1)\n      }\n    optionQuery(q())\n\n  def get[F[_]: Async](\n      hash: String,\n  ): EitherT[F, String, Option[Block]] =\n    inline def detailQuery =\n      quote { (hash: String) =>\n        query[Block].filter(b => b.hash == hash).take(1)\n      }\n    optionQuery(detailQuery(lift(hash)))\n\n  def getByNumber[F[_]: Async](\n      number: Long\n  ): EitherT[F, String, Option[Block]] =\n    inline def detailQuery =\n      quote { (number: Long) =>\n        query[Block].filter(b => b.number == number).take(1)\n      }\n    optionQuery(detailQuery(lift(number)))\n"
  },
  {
    "path": "modules/lmscan-backend/src/main/scala/io/leisuremeta/chain/lmscan/backend/repository/CommonQuery.scala",
    "content": "package io.leisuremeta.chain.lmscan.backend.repository\n\nimport cats.data.EitherT\nimport cats.effect.kernel.Async\nimport io.getquill.Query\nimport java.sql.SQLException\nimport cats.implicits.*\nimport io.getquill.PostgresJAsyncContext\nimport io.getquill.SnakeCase\nimport io.getquill.*\n\nimport scala.concurrent.ExecutionContext\n\ntrait CommonQuery:\n  val ctx = new PostgresJAsyncContext(SnakeCase, \"ctx\")\n  inline def seqQuery[F[_]: Async, T](\n      inline query: Query[T],\n  ): EitherT[F, String, Seq[T]] =\n    EitherT {\n      Async[F].recover {\n        for\n          given ExecutionContext <- Async[F].executionContext\n          result <- Async[F]\n            .fromFuture(Async[F].delay {\n              ctx.run(query)\n            })\n            .map(Either.right(_))\n        yield result\n      } {\n        case e: SQLException =>\n          Left(s\"sql exception occured: \" + e.getMessage())\n        case e: Exception => Left(e.getMessage())\n      }\n    }\n\n  inline def countQuery[F[_]: Async, T](\n      inline query: Query[T],\n  ): EitherT[F, String, Long] =\n    EitherT {\n      Async[F].recover {\n        for\n          given ExecutionContext <- Async[F].executionContext\n          result <- Async[F]\n            .fromFuture(Async[F].delay {\n              ctx.run(query.size)\n            })\n            .map(Either.right(_))\n        yield\n          result\n      } {\n        case e: SQLException =>\n          Left(s\"sql exception occured: \" + e.getMessage())\n        case e: Exception => Left(e.getMessage())\n      }\n    }\n\n  inline def optionSeqQuery[F[_]: Async, T](\n      inline query: Query[T],\n  ): EitherT[F, String, Option[Seq[T]]] =\n    EitherT {\n      Async[F].recover {\n        for\n          given ExecutionContext <- Async[F].executionContext\n          detail <- Async[F]\n            .fromFuture(Async[F].delay {\n              ctx.run(query)\n            })\n          res = if detail.isEmpty then Left(\"Can't found match data\") else Right(Some(detail))\n        yield res\n      } {\n        case e: SQLException =>\n          Left(s\"sql exception occured: \" + e.getMessage())\n        case e: Exception => Left(e.getMessage())\n      }\n    }\n  inline def optionQuery[F[_]: Async, T](\n      inline query: Query[T],\n  ): EitherT[F, String, Option[T]] =\n    EitherT {\n      Async[F].recover {\n        for\n          given ExecutionContext <- Async[F].executionContext\n          detail <- Async[F]\n            .fromFuture(Async[F].delay {\n              ctx.run(query)\n            })\n          res = if detail.isEmpty then Left(\"Can't found match data\") else Right(detail.headOption)\n        yield res\n      } {\n        case e: SQLException =>\n          Left(s\"sql exception occured: \" + e.getMessage())\n        case e: Exception => Left(e.getMessage())\n      }\n    }\n\n  def calTotalPage(totalCnt: Long, sizePerRequest: Integer): Integer =\n    Math.ceil(totalCnt.toDouble / sizePerRequest).toInt;\n"
  },
  {
    "path": "modules/lmscan-backend/src/main/scala/io/leisuremeta/chain/lmscan/backend/repository/NftFileRepository.scala",
    "content": "package io.leisuremeta.chain.lmscan.backend.repository\n\nimport io.leisuremeta.chain.lmscan.backend.entity.NftFile\nimport cats.effect.kernel.Async\nimport cats.data.EitherT\nimport io.getquill.*\n\nobject NftFileRepository extends CommonQuery:\n  import ctx.*\n\n  def get[F[_]: Async](\n      tokenId: String,\n  ): EitherT[F, String, Option[NftFile]] =\n    inline def detailQuery =\n      quote { (tokenId: String) =>\n        query[NftFile].filter(f => f.tokenId == tokenId).take(1)\n      }\n    optionQuery(detailQuery(lift(tokenId)))\n"
  },
  {
    "path": "modules/lmscan-backend/src/main/scala/io/leisuremeta/chain/lmscan/backend/repository/NftInfoRepository.scala",
    "content": "package io.leisuremeta.chain.lmscan\npackage backend\npackage repository\n\nimport entity._\nimport common.model._\nimport cats.effect.kernel.Async\nimport cats.data.EitherT\nimport io.getquill.*\nimport io.leisuremeta.chain.lmscan.backend.entity.NftSeason\nimport entity.NftInfo\nimport java.net.URLDecoder\n\nobject NftInfoRepository extends CommonQuery:\n  import ctx.*\n\n  def getSeasonPage[F[_]: Async](\n      pageNavInfo: PageNavigation,\n      seasonEnc: String,\n  ): EitherT[F, String, PageResponse[NftSeason]] =\n    val season = URLDecoder.decode(seasonEnc, \"UTF-8\")\n    val cntQuery = quote:\n      (season: String) => query[NftFile]\n        .join(query[CollectionInfo].filter(s => s.season == season))\n        .on((n, c) => n.tokenDefId == c.tokenDefId)\n\n    def pagedQuery =\n      quote: (pageNavInfo: PageNavigation, season: String) =>\n        val offset         = sizePerRequest * pageNavInfo.pageNo\n        val sizePerRequest = pageNavInfo.sizePerRequest\n\n        query[NftFile]\n          .join(query[CollectionInfo].filter(s => s.season == season))\n          .on((n, c) => n.tokenDefId == c.tokenDefId)\n          .map((n, c) => \n            NftSeason(\n              n.nftName,\n              n.tokenId,\n              n.tokenDefId,\n              n.creator,\n              n.rarity,\n              n.dataUrl,\n              c.collectionName,\n            ) \n          )\n          .sortBy(s => s.tokenId)(Ord.asc)\n          .drop(offset)\n          .take(sizePerRequest)\n\n    val res = for\n      a <- countQuery(cntQuery(lift(season)))\n      b <- seqQuery(pagedQuery(lift(pageNavInfo), lift(season)))\n    yield (a, b)\n\n    res.map: (totalCnt, r) =>\n      val totalPages = calTotalPage(totalCnt, pageNavInfo.sizePerRequest)\n      new PageResponse(totalCnt, totalPages, r)\n\n  def getPage[F[_]: Async](\n      pageNavInfo: PageNavigation,\n  ): EitherT[F, String, PageResponse[NftInfo]] =\n    val cntQuery = quote: \n      query[NftInfo]\n\n    def pagedQuery =\n      quote: (pageNavInfo: PageNavigation) =>\n        val offset         = sizePerRequest * pageNavInfo.pageNo\n        val sizePerRequest = pageNavInfo.sizePerRequest\n\n        query[NftInfo]\n          .sortBy(t => t.sort)(Ord.asc)\n          .drop(offset)\n          .take(sizePerRequest)\n\n    val res = for\n      a <- countQuery(cntQuery)\n      b <- seqQuery(pagedQuery(lift(pageNavInfo)))\n    yield (a, b)\n\n    res.map: (totalCnt, r) =>\n      val totalPages = calTotalPage(totalCnt, pageNavInfo.sizePerRequest)\n      new PageResponse(totalCnt, totalPages, r)\n\n  def get[F[_]: Async](\n      tokenId: String,\n  ): EitherT[F, String, Option[CollectionInfo]] =\n    inline def detailQuery =\n      quote: (tokenId: String) => \n        query[CollectionInfo].filter(f => f.tokenDefId == tokenId).take(1)\n    optionQuery(detailQuery(lift(tokenId)))\n"
  },
  {
    "path": "modules/lmscan-backend/src/main/scala/io/leisuremeta/chain/lmscan/backend/repository/NftOwnerRepository.scala",
    "content": "package io.leisuremeta.chain.lmscan.backend.repository\n\nimport cats.effect.kernel.Async\nimport cats.data.EitherT\nimport io.getquill.*\nimport io.leisuremeta.chain.lmscan.backend.entity.NftOwner\n\nobject NftOwnerRepository extends CommonQuery:\n  import ctx.*\n\n  def get[F[_]: Async](\n      tokenId: String,\n  ): EitherT[F, String, Option[NftOwner]] =\n    inline def detailQuery =\n      quote { (tokenId: String) =>\n        query[NftOwner].filter(f => f.tokenId == tokenId).take(1)\n      }\n    optionQuery(detailQuery(lift(tokenId)))\n"
  },
  {
    "path": "modules/lmscan-backend/src/main/scala/io/leisuremeta/chain/lmscan/backend/repository/NftRepository.scala",
    "content": "package io.leisuremeta.chain.lmscan.backend.repository\n\nimport io.leisuremeta.chain.lmscan.backend.entity.Nft\nimport io.leisuremeta.chain.lmscan.common.model.PageNavigation\nimport io.leisuremeta.chain.lmscan.common.model.PageResponse\nimport cats.effect.kernel.Async\nimport cats.data.EitherT\nimport io.getquill.*\n\nobject NftRepository extends CommonQuery:\n  import ctx.*\n\n  def getPageByTokenId[F[_]: Async](\n      tokenId: String,\n      pageNavInfo: PageNavigation,\n  ): EitherT[F, String, PageResponse[Nft]] =\n    val cntQuery = quote {\n      query[Nft]\n    }\n\n    def pagedQuery =\n      quote { (pageNavInfo: PageNavigation) =>\n        val sizePerRequest = pageNavInfo.sizePerRequest\n        val offset         = sizePerRequest * pageNavInfo.pageNo\n\n        query[Nft]\n          .filter(t => t.tokenId == lift(tokenId))\n          .sortBy(t => t.eventTime)(Ord.desc)\n          .drop(offset)\n          .take(sizePerRequest)\n      }\n\n    val res = for\n      a <- countQuery(cntQuery)\n      b <- seqQuery(pagedQuery(lift(pageNavInfo)))\n    yield (a, b)\n\n    res.map { (totalCnt, r) =>\n      val totalPages = calTotalPage(totalCnt, pageNavInfo.sizePerRequest)\n      new PageResponse(totalCnt, totalPages, r)\n    }\n"
  },
  {
    "path": "modules/lmscan-backend/src/main/scala/io/leisuremeta/chain/lmscan/backend/repository/SummaryRepository.scala",
    "content": "package io.leisuremeta.chain.lmscan.backend.repository\n\nimport io.leisuremeta.chain.lmscan.backend.entity.Summary\nimport cats.effect.kernel.Async\nimport cats.data.EitherT\nimport io.getquill.*\n\nobject SummaryRepository extends CommonQuery:\n  import ctx.*\n\n  def get[F[_]: Async](n: Int = 0, l: Int = 1): EitherT[F, String, Option[Seq[Summary]]] =\n    inline def detailQuery =\n      quote {\n        query[Summary].sortBy(t => t.createdAt)(Ord.desc).drop(lift(n)).take(lift(l))\n      }\n    optionSeqQuery(detailQuery)\n"
  },
  {
    "path": "modules/lmscan-backend/src/main/scala/io/leisuremeta/chain/lmscan/backend/repository/TransactionRepository.scala",
    "content": "package io.leisuremeta.chain.lmscan.backend.repository\n\nimport io.leisuremeta.chain.lmscan.common.model.PageNavigation\nimport io.leisuremeta.chain.lmscan.common.model.PageResponse\nimport io.leisuremeta.chain.lmscan.backend.entity.Tx\nimport io.leisuremeta.chain.lmscan.backend.entity.TxState\nimport io.leisuremeta.chain.lmscan.backend.entity.AccountMapper\nimport cats.data.EitherT\nimport cats.implicits.*\nimport io.getquill.PostgresJAsyncContext\nimport io.getquill.*\nimport cats.effect.Async\nimport scala.concurrent.Future\n\ntrait TransactionRepository[F[_]]:\n  def getPage(\n      pageNavInfo: PageNavigation,\n  ): EitherT[F, String, Seq[Tx]]\n\nobject TransactionRepository extends CommonQuery:\n  import ctx.*\n\n  def apply[F[_]: TransactionRepository]: TransactionRepository[F] =\n    summon\n\n  def getPage[F[_]: Async](\n      pageNavInfo: PageNavigation,\n  ): EitherT[F, String, Seq[Tx]] =\n\n    inline def pagedQuery =\n      quote { (offset: Int, sizePerRequest: Int) =>\n        query[Tx]\n          .sortBy(t => (t.blockNumber, t.eventTime))(Ord(Ord.desc, Ord.desc))\n          .drop(offset)\n          .take(sizePerRequest)\n      }\n\n    val sizePerRequest = pageNavInfo.sizePerRequest\n    val offset         = sizePerRequest * pageNavInfo.pageNo\n    seqQuery(pagedQuery(lift(offset), lift(sizePerRequest)))\n\n  def get[F[_]: Async](\n      hash: String,\n  ): EitherT[F, String, Option[TxState]] =\n    inline def detailQuery =\n      quote { (hash: String) =>\n        query[TxState].filter(tx => tx.hash == hash).take(1)\n      }\n\n    optionQuery(detailQuery(lift(hash)))\n\n  def getPageByAccount[F[_]: Async](\n      addr: String,\n      pageNavInfo: PageNavigation,\n  ): EitherT[F, String, PageResponse[Tx]] =\n    val cntQuery = quote {\n      query[AccountMapper].filter(t => t.address == lift(addr))\n    }\n\n    inline def pagedQuery =\n      quote { (pageNavInfo: PageNavigation) =>\n        val sizePerRequest = pageNavInfo.sizePerRequest\n        val offset         = sizePerRequest * pageNavInfo.pageNo\n\n        query[Tx]\n          .join(\n            query[AccountMapper]\n              .filter(t => t.address == lift(addr))\n              .sortBy(_.eventTime)(Ord.desc)\n              .drop(offset)\n              .take(sizePerRequest)\n          )\n          .on((tx, mapper) => tx.hash == mapper.hash)\n          .map((tx, _) => tx)\n      }\n\n    val res = for\n      totalCnt <- countQuery(cntQuery)\n      payload  <- seqQuery(pagedQuery(lift(pageNavInfo)))\n    yield (totalCnt, payload)\n\n    res.map { (totalCnt, payload) =>\n      val totalPages = calTotalPage(totalCnt, pageNavInfo.sizePerRequest)\n      new PageResponse(totalCnt, totalPages, payload)\n    }\n\n  def getTxPageByBlock[F[_]: Async](\n      hash: String,\n      pageNavInfo: PageNavigation,\n  ): EitherT[F, String, PageResponse[Tx]] =\n    val cntQuery = quote {\n      query[Tx].filter(t => t.blockHash == lift(hash))\n    }\n\n    inline def pagedQuery =\n      quote { (pageNavInfo: PageNavigation) =>\n        val sizePerRequest = pageNavInfo.sizePerRequest\n        val offset         = sizePerRequest * pageNavInfo.pageNo\n\n        query[Tx]\n          .filter(t => t.blockHash == lift(hash))\n          .drop(offset)\n          .take(sizePerRequest)\n          .sortBy(t => t.eventTime)(Ord.desc)\n      }\n\n    val res = for\n      totalCnt <- countQuery(cntQuery)\n      payload  <- seqQuery(pagedQuery(lift(pageNavInfo)))\n    yield (totalCnt, payload)\n\n    res.map { (totalCnt, payload) =>\n      val totalPages = calTotalPage(totalCnt, pageNavInfo.sizePerRequest)\n      new PageResponse(totalCnt, totalPages, payload)\n    }\n"
  },
  {
    "path": "modules/lmscan-backend/src/main/scala/io/leisuremeta/chain/lmscan/backend/repository/ValidatorRepository.scala",
    "content": "package io.leisuremeta.chain.lmscan.backend.repository\n\nimport cats.effect.kernel.Async\nimport cats.data.EitherT\nimport io.getquill.*\nimport io.leisuremeta.chain.lmscan.backend.entity._\n\nobject ValidatorRepository extends CommonQuery:\n  import ctx.*\n\n  def get[F[_]: Async](\n      addr: String,\n  ): EitherT[F, String, Option[ValidatorInfo]] =\n    inline def detailQuery =\n      quote { (addr: String) =>\n        query[ValidatorInfo]\n          .filter(_.address == addr)\n          .take(1)\n      }\n    optionQuery(detailQuery(lift(addr)))\n\n  def getPage[F[_]: Async](): EitherT[F, String, Seq[ValidatorInfo]] =\n    inline def q = quote:\n      query[ValidatorInfo]\n        \n\n    for\n      res <- seqQuery(q)\n    yield res\n"
  },
  {
    "path": "modules/lmscan-backend/src/main/scala/io/leisuremeta/chain/lmscan/backend/service/AccountService.scala",
    "content": "package io.leisuremeta.chain.lmscan.backend.service\n\nimport cats.effect.kernel.Async\nimport cats.data.EitherT\n\nimport io.leisuremeta.chain.lmscan.backend.repository.AccountRepository\nimport io.leisuremeta.chain.lmscan.common.model.{\n  PageNavigation,\n  PageResponse,\n  AccountDetail,\n}\nimport io.leisuremeta.chain.lmscan.common.model.AccountInfo\nimport java.time.Instant\nimport io.leisuremeta.chain.lmscan.backend.repository.SummaryRepository\n\nobject AccountService:\n  def get[F[_]: Async](\n      address: String,\n      p: Int,\n  ): EitherT[F, Either[String, String], Option[AccountDetail]] =\n    val balRes = AccountRepository.get(address)\n      .leftFlatMap: _ =>\n        EitherT.pure[F, Either[String, String]](None)\n    val txPageRes = TransactionService.getPageByAccount(\n        address,\n        PageNavigation(p - 1, 20),\n      )\n    for\n      account <- balRes\n      txPage <- txPageRes\n      summary <- SummaryRepository.get().leftMap(Left(_))\n      price = summary match\n        case Some(s) => BigDecimal(s.head.lmPrice)\n        case None => BigDecimal(0)\n      res <- account match\n        case Some(x) => \n          EitherT.rightT[F, Either[String, String]](\n            Some(AccountDetail(\n              Some(x.address),\n              Some(x.free),\n              Some(x.free / BigDecimal(\"1E+18\") * price),\n              txPage.totalCount,\n              txPage.totalPages,\n              txPage.payload,\n            )\n          ))\n        case None =>\n          EitherT.rightT[F, Either[String, String]](\n            Some(AccountDetail(\n              Some(address),\n              None,\n              None,\n              txPage.totalCount,\n              txPage.totalPages,\n              txPage.payload,\n            )\n          ))\n    yield res\n\n  def getPage[F[_]: Async](\n      pageNavInfo: PageNavigation,\n  ): EitherT[F, Either[String, String], PageResponse[AccountInfo]] =\n    for \n      page <- AccountRepository.getPage(pageNavInfo).leftMap(Left(_))\n      summary <- SummaryRepository.get().leftMap(Left(_))\n      price = summary match\n        case Some(s) => s.headOption match\n          case Some(h) => BigDecimal(h.lmPrice)\n          case None => BigDecimal(0)\n        case None => BigDecimal(0)\n      accInfos = page.payload.map((b) =>\n        val balance = b.free\n        AccountInfo(\n          Some(b.address),\n          Some(balance),\n          Some(Instant.ofEpochSecond(b.updatedAt)),\n          Some(balance / BigDecimal(\"1E+18\") * price),\n        )\n      )\n    yield PageResponse(page.totalCount, page.totalPages, accInfos)\n"
  },
  {
    "path": "modules/lmscan-backend/src/main/scala/io/leisuremeta/chain/lmscan/backend/service/BlockService.scala",
    "content": "package io.leisuremeta.chain.lmscan.backend.service\n\nimport cats.effect.kernel.Async\nimport cats.data.EitherT\n\nimport io.leisuremeta.chain.lmscan.common.model.PageNavigation\nimport io.leisuremeta.chain.lmscan.common.model.PageResponse\nimport io.leisuremeta.chain.lmscan.common.model.{BlockDetail, BlockInfo}\nimport io.leisuremeta.chain.lmscan.backend.entity.Block\nimport io.leisuremeta.chain.lmscan.backend.repository.BlockRepository\n\nobject BlockService:\n  def getPage[F[_]: Async](\n      pageNavInfo: PageNavigation,\n  ): EitherT[F, Either[String, String], PageResponse[BlockInfo]] =\n    for \n      latestBlcOpt <- BlockRepository.getLast().leftMap:\n        e => Left(e)\n      cnt = latestBlcOpt.get.number\n      page <- BlockRepository.getPage(pageNavInfo, cnt).leftMap:\n        e => Left(e)\n      blockInfos = page.map { block =>\n        BlockInfo(\n          Some(block.number),\n          Some(block.hash),\n          Some(block.txCount),\n          Some(block.eventTime),\n        )\n      }\n    yield PageResponse.from(cnt, pageNavInfo.sizePerRequest, blockInfos)\n\n  def get[F[_]: Async](\n      hash: String,\n  ): EitherT[F, Either[String, String], Option[Block]] =\n    BlockRepository.get(hash).leftMap(Left(_))\n  \n  def getByNumber[F[_]: Async](\n      number: Long,\n      p: Int = 1,\n  ): EitherT[F, Either[String, String], Option[BlockDetail]] =\n    for\n      block <- BlockRepository.getByNumber(number).leftMap(Left(_))\n      txPage <- TransactionService.getPageByBlock(\n        block.map(_.hash).getOrElse(\"\"),\n        PageNavigation(p - 1, 20),\n      )\n      blockInfo = block.map: bl =>\n        BlockDetail(\n          Some(bl.hash),\n          Some(bl.parentHash),\n          Some(bl.number),\n          Some(bl.eventTime),\n          Some(bl.txCount),\n          txPage.totalCount,\n          txPage.totalPages,\n          txPage.payload,\n        )\n    yield blockInfo\n\n  def getDetail[F[_]: Async](\n      hash: String,\n      p: Int,\n  ): EitherT[F, Either[String, String], Option[BlockDetail]] =\n    for\n      block <- get(hash)\n      txPage <- TransactionService.getPageByBlock(\n        hash,\n        PageNavigation(p - 1, 20),\n      )\n      blockInfo = block.map: bl =>\n        BlockDetail(\n          Some(bl.hash),\n          Some(bl.parentHash),\n          Some(bl.number),\n          Some(bl.eventTime),\n          Some(bl.txCount),\n          txPage.totalCount,\n          txPage.totalPages,\n          txPage.payload,\n        )\n    yield blockInfo\n"
  },
  {
    "path": "modules/lmscan-backend/src/main/scala/io/leisuremeta/chain/lmscan/backend/service/NftService.scala",
    "content": "package io.leisuremeta.chain.lmscan.backend.service\n\nimport cats.effect.kernel.Async\nimport cats.data.EitherT\nimport io.leisuremeta.chain.lmscan.backend.repository._\nimport io.leisuremeta.chain.lmscan.common.model._\nimport io.leisuremeta.chain.lmscan.backend.repository.NftOwnerRepository\nimport io.leisuremeta.chain.lmscan.backend.entity._\nobject NftService:\n  def getNftDetail[F[_]: Async](\n      tokenId: String, // tokenId\n  ): EitherT[F, Either[String, String], Option[NftDetail]] =\n    for\n      page <- NftRepository.getPageByTokenId(\n        tokenId,\n        new PageNavigation(0, 10),\n      ).leftMap(Left(_))\n      activities = page.payload.map(nft =>\n        NftActivity(\n          Some(nft.txHash),\n          Some(nft.action),\n          Some(nft.fromAddr),\n          Some(nft.toAddr),\n          Some(nft.eventTime),\n        ),\n      )\n      nftOwner <- NftOwnerRepository.get(tokenId).leftMap(Left(_))\n      nft <- NftFileRepository.get(tokenId).leftMap(Left(_))\n      nftFile = nft.map(nftFile =>\n        NftFileModel(\n          Some(nftFile.tokenId),\n          Some(nftFile.tokenDefId),\n          Some(nftFile.collectionName),\n          Some(nftFile.nftName),\n          Some(nftFile.nftUri),\n          Some(nftFile.creatorDescription),\n          Some(nftFile.dataUrl),\n          Some(nftFile.rarity),\n          Some(nftFile.creator),\n          Some(nftFile.eventTime),\n          Some(nftFile.createdAt),\n          Some(nftOwner.getOrElse(new NftOwner).owner),\n        ),\n      )\n    yield Some(NftDetail(nftFile, Some(activities)))\n\n  def getPage[F[_]: Async](\n      pageNavInfo: PageNavigation,\n  ): EitherT[F, Either[String, String], PageResponse[NftInfoModel]] =\n    for \n      page <- NftInfoRepository.getPage(pageNavInfo).leftMap(Left(_))\n      nftInfos = page.payload.map { info =>\n        NftInfoModel(\n          season = Some(info.season),\n          seasonName = Some(info.seasonName),\n          totalSupply = info.totalSupply,\n          startDate = info.startDate.map(_.toInstant()),\n          endDate = info.endDate.map(_.toInstant()),\n          thumbUrl = info.thumbUrl,\n        )\n      }\n    yield PageResponse(page.totalCount, page.totalPages, nftInfos)\n  def getSeasonPage[F[_]: Async](\n      pageNavInfo: PageNavigation,\n      season: String,\n  ): EitherT[F, Either[String, String], PageResponse[NftSeasonModel]] =\n    for \n      page <- NftInfoRepository.getSeasonPage(pageNavInfo, season).leftMap(Left(_))\n      seasons = page.payload.map(_.toModel)\n    yield PageResponse(page.totalCount, page.totalPages, seasons)\n\n  def getNftOwnerInfo[F[_]: Async](\n      tokenId: String, // tokenId\n  ): EitherT[F, Either[String, String], Option[NftOwnerInfo]] =\n    for\n      nftOwner <- NftOwnerRepository.get(tokenId).leftMap(Left(_))\n      nft <- NftFileRepository.get(tokenId).leftMap(Left(_))\n      ownerInfo = NftOwnerInfo(\n          nftOwner.map(_.owner),\n          nft.map(_.dataUrl),\n        )\n    yield Some(ownerInfo)\n"
  },
  {
    "path": "modules/lmscan-backend/src/main/scala/io/leisuremeta/chain/lmscan/backend/service/SearchService.scala",
    "content": "package io.leisuremeta.chain.lmscan.backend.service\n\nimport io.leisuremeta.chain.lmscan.common.model._\nimport cats.data.EitherT\nimport cats.effect.Async\n\nobject SearchService:\n  def getKeywordSearch[F[_]: Async](keyword: String): EitherT[F, Either[String, String], SearchResult] =\n    keyword.toLongOption match\n      case Some(n) => numSearch(n)\n      case None => hashSearch(keyword)\n\n  def numSearch[F[_]: Async](n: Long): EitherT[F, Either[String, String], SearchResult] =\n    NftService.getNftDetail(n.toString)\n      .flatMap: d =>\n        EitherT.pure[F, Either[String, String]](SearchResult.nft(d.get))\n      .leftFlatMap: _ =>\n        for\n          blc <- BlockService.getByNumber(n)\n        yield SearchResult.blc(blc.get)\n      .leftFlatMap: _ => \n        EitherT.pure[F, Either[String, String]](SearchResult.empty)\n\n  def hashSearch[F[_]: Async](keyword: String): EitherT[F, Either[String, String], SearchResult] =\n    TransactionService.getDetail(keyword)\n      .flatMap: d =>\n        EitherT.pure[F, Either[String, String]](SearchResult.tx(d.get))\n      .leftFlatMap: _ =>\n        for\n          blc <- BlockService.getDetail(keyword, 1)\n        yield SearchResult.blc(blc.get)\n      .leftFlatMap: _ =>\n        for\n          acc <- AccountService.get(keyword, 1)\n        yield  SearchResult.acc(acc.get)\n      .leftFlatMap: _ => \n        EitherT.pure[F, Either[String, String]](SearchResult.empty)\n"
  },
  {
    "path": "modules/lmscan-backend/src/main/scala/io/leisuremeta/chain/lmscan/backend/service/SummaryService.scala",
    "content": "package io.leisuremeta.chain.lmscan.backend.service\n\nimport io.leisuremeta.chain.lmscan.common.model.SummaryModel\nimport io.leisuremeta.chain.lmscan.backend.repository.SummaryRepository\nimport cats.effect.kernel.Async\nimport cats.data.EitherT\nimport io.leisuremeta.chain.lmscan.common.model.SummaryChart\nimport io.leisuremeta.chain.lmscan.common.model.SummaryBoard\nimport io.leisuremeta.chain.lmscan.backend.entity.Summary\n\nobject SummaryService:\n  extension (s: Summary)\n    def toM: SummaryModel = \n      SummaryModel(\n          Some(s.lmPrice),\n          Some(s.blockNumber),\n          Some(s.totalAccounts),\n          Some(s.createdAt),\n          Some(s.totalTxSize.toLong),\n          Some(s.totalBalance),\n          s.marketCap,\n          s.cirSupply,\n          s.totalNft,\n        )\n  extension (opt: Option[Summary])\n    def toM: SummaryModel = opt match\n      case Some(s) => s.toM\n      case None => SummaryModel()\n    \n  def get[F[_]: Async](n: Int): EitherT[F, Either[String, String], Option[SummaryModel]] =\n    SummaryRepository.get(n, 1)\n      .map: res =>\n        res.map(_.headOption.toM)\n      .leftFlatMap: _ =>\n        EitherT.pure[F, Either[String ,String]](Some(SummaryModel()))\n\n  def getBoard[F[_]: Async]: EitherT[F, Either[String, String], Option[SummaryBoard]] =\n    for \n      todayOpt <- get(0)\n      yesterdayOpt <- get(143)\n      model = todayOpt.zip(yesterdayOpt).map((today, yesterday) => SummaryBoard(today, yesterday))\n    yield model\n\n  def get5List[F[_]: Async]: EitherT[F, Either[String, String], SummaryChart] =\n    for\n      summary <- SummaryRepository.get(0, 144 * 5 + 1).leftMap(Left(_))\n      model = summary.map(\n          _.grouped(144).map(_.head.toM).toSeq\n        )\n      chart = SummaryChart(model.get)\n    yield chart\n\n  def getList[F[_]: Async]: EitherT[F, Either[String, String], SummaryChart] =\n    for\n      summary <- SummaryRepository.get(0, 144 * 5).leftMap(Left(_))\n      model = summary.map(_.map(_.toM\n      ))\n      chart = SummaryChart(model.getOrElse(Seq()))\n    yield chart\n"
  },
  {
    "path": "modules/lmscan-backend/src/main/scala/io/leisuremeta/chain/lmscan/backend/service/TransactionService.scala",
    "content": "package io.leisuremeta.chain.lmscan.backend.service\n\nimport io.leisuremeta.chain.lmscan.backend.entity.Tx\nimport io.leisuremeta.chain.lmscan.backend.entity.TxState\nimport io.leisuremeta.chain.lmscan.common.model._\nimport io.leisuremeta.chain.lmscan.backend.repository._\nimport cats.data.EitherT\nimport cats.effect.Async\n\nobject TransactionService:\n\n  def get[F[_]: Async](\n      hash: String,\n  ): EitherT[F, String, Option[TxState]] =\n    TransactionRepository.get(hash)\n\n  def getDetail[F[_]: Async](\n      hash: String,\n  ): EitherT[F, Either[String, String], Option[TxDetail]] =\n    for\n      trx <- TransactionRepository.get(hash).leftMap(Left(_))\n      detail = trx.map { tx =>\n        TxDetail(\n          Some(tx.hash),\n          Some(tx.eventTime),\n          Some(tx.json),\n        )\n      }\n    yield detail\n\n  def getPage[F[_]: Async](\n      pageNavInfo: PageNavigation,\n  ): EitherT[F, Either[String, String], PageResponse[TxInfo]] =\n    for\n      summaryOpt <- SummaryService.get(0)\n      page <- TransactionRepository.getPage(pageNavInfo).leftMap(Left(_))\n      summary = summaryOpt.getOrElse(SummaryModel())\n      cnt = Math.min(summary.totalTxSize.getOrElse(0L), 100000L)\n      txInfo = convertToInfo(page)\n    yield PageResponse.from(cnt, pageNavInfo.sizePerRequest, txInfo)\n\n  def getPageByAccount[F[_]: Async](\n      address: String,\n      pageNavInfo: PageNavigation,\n  ): EitherT[F, Either[String, String], PageResponse[TxInfo]] =\n    for\n      page <- TransactionRepository.getPageByAccount(address, pageNavInfo).leftMap(Left(_))\n    yield page.copy(payload = convertToInfoForAccount(page.payload, address))\n\n  def getPageByBlock[F[_]: Async](\n      blockHash: String,\n      pageNavInfo: PageNavigation,\n  ): EitherT[F, Either[String, String], PageResponse[TxInfo]] =\n    for\n      page <- TransactionRepository.getTxPageByBlock(blockHash, pageNavInfo).leftMap(Left(_))\n    yield page.copy(payload = convertToInfo(page.payload))\n\n  def getPageByFilter[F[_]: Async](\n      pageInfo: PageNavigation,\n      accountAddr: Option[String],\n      blockHash: Option[String],\n      subType: Option[String],\n  ): EitherT[F, Either[String, String], PageResponse[TxInfo]] =\n    (accountAddr, blockHash) match\n      case (None, None) =>\n        getPage[F](pageInfo)\n      case (_, _) =>\n        EitherT.left(Async[F].delay(Left(\"검색 파라미터를 하나만 입력해주세요.\")))\n\n  def convertToInfo(txs: Seq[Tx]): Seq[TxInfo] =\n    txs.map { tx =>\n      TxInfo(\n        Some(tx.hash),\n        Some(tx.blockNumber),\n        Some(tx.eventTime),\n        Some(tx.txType),\n        Some(tx.tokenType),\n        Some(tx.signer),\n        Some(tx.subType),\n        None,\n        None,\n      )\n    }\n\n  def convertToInfoForAccount(txs: Seq[Tx], address: String): Seq[TxInfo] =\n    txs.map { tx =>\n      TxInfo(\n        Some(tx.hash),\n        Some(tx.blockNumber),\n        Some(tx.eventTime),\n        Some(tx.txType),\n        Some(tx.tokenType),\n        Some(tx.signer),\n        Some(tx.subType),\n        Some(if tx.signer == address then \"Out\" else \"In\"),\n        None,\n      )\n    }\n"
  },
  {
    "path": "modules/lmscan-backend/src/main/scala/io/leisuremeta/chain/lmscan/backend/service/ValidatorService.scala",
    "content": "package io.leisuremeta.chain.lmscan.backend.service\n\nimport cats.effect.kernel.Async\nimport cats.data.EitherT\n\nimport io.leisuremeta.chain.lmscan.common.model.{\n  PageNavigation,\n  PageResponse,\n}\nimport io.leisuremeta.chain.lmscan.backend.repository.ValidatorRepository\nimport io.leisuremeta.chain.lmscan.common.model.NodeValidator\nimport io.leisuremeta.chain.lmscan.backend.repository.BlockRepository\nimport io.leisuremeta.chain.lmscan.backend.entity.Block\nimport com.typesafe.config.ConfigFactory\n\nobject ValidatorService:\n  val conf = ConfigFactory.load()\n  val vdcnt = conf.getInt(\"vdcnt\")\n  def get[F[_]: Async](\n      address: String,\n      p: Int,\n  ): EitherT[F, Either[String, String], Option[NodeValidator.ValidatorDetail]] =\n    for\n      latestBlcOpt: Option[Block] <- BlockRepository.getLast().leftMap:\n        e => Left(e)\n      validator <- ValidatorRepository.get(address).leftMap(Left(_))\n      cnt = latestBlcOpt.get.number\n      start = cnt - ((p - 1) * vdcnt) * 20\n      blcs <- BlockRepository.getPageByProposer(\n        PageNavigation(p - 1, 20),\n        start,\n        address,\n      ).leftMap(Left(_))\n      res = validator.map(v => NodeValidator.ValidatorDetail(v.toModel, PageResponse.from(\n          cnt / vdcnt, 20, blcs.map(_.toModel)\n        )))\n    yield res\n\n  def getPage[F[_]: Async](): EitherT[F, Either[String, String], Seq[NodeValidator.Validator]] =\n    for \n      page <- ValidatorRepository.getPage().leftMap(Left(_))\n      res = page.map(_.toModel)\n    yield res\n"
  },
  {
    "path": "modules/lmscan-backend/src/test/scala/EmbeddedPostgreFlywayTest.scala",
    "content": "import com.opentable.db.postgres.junit.EmbeddedPostgresRules\nimport com.opentable.db.postgres.embedded.FlywayPreparer\nimport org.junit.rules.TestRule\nimport org.junit.runners.model.Statement\nimport org.junit.runner.Description\n\nclass EmbeddedPostgreFlywayTest extends munit.FunSuite {\n    def withRule[T <: TestRule](rule: T)(testCode: T => Any): Unit = {\n        rule(\n            new Statement() {\n                override def evaluate(): Unit =\n                    val _ = testCode(rule)\n            },\n            Description.createSuiteDescription(\"JUnit rule wrapper\")\n        ).evaluate()\n    }\n\n    test(\"test\") {\n        withRule(EmbeddedPostgresRules.preparedDatabase(FlywayPreparer.forClasspathLocation(\"db/test\", \"db/common\", \"db/seed\"))) { \n            preparedDbRule =>\n                val c = preparedDbRule.getTestDatabase().getConnection()\n                val s = c.createStatement()\n                println(\"////////////////////////////////////////////////////// test of printing created table //////////////////////////////////////////////////////\")\n                val rs_t = s.executeQuery(\"SELECT table_name FROM information_schema.tables WHERE table_schema='public' AND table_type='BASE TABLE'\")\n                while (rs_t.next) {\n                    println(rs_t.getString(\"table_name\"))\n                }   \n                println(\"////////////////////////////////////////////////////// test of printing seed data //////////////////////////////////////////////////////\")\n                val rs_s = s.executeQuery(\"SELECT * FROM public.account\")\n                while (rs_s.next) {\n                    println(\n                        \"id = %s, address = %s, balance = %s, amount = %s, type = %s, created_at = %s\"\n                        .format(rs_s.getString(\"id\"), rs_s.getString(\"address\"), rs_s.getString(\"balance\"), rs_s.getString(\"amount\"), rs_s.getString(\"type\"), rs_s.getString(\"created_at\"))\n                    )\n                }   \n        }\n    }\n}\n"
  },
  {
    "path": "modules/lmscan-common/.js/package.json",
    "content": "{\n  \"dependencies\": {\n    \"typescript\": \"^4.9.4\"\n  }\n}\n"
  },
  {
    "path": "modules/lmscan-common/js/package.json",
    "content": "{\n  \"dependencies\": {\n    \"typescript\": \"^4.9.4\"\n  }\n}\n"
  },
  {
    "path": "modules/lmscan-common/shared/src/main/scala/io/leisuremeta/ExplorerAPI.scala",
    "content": "package io.leisuremeta.chain.lmscan.common\n\nimport sttp.model.StatusCode\nimport sttp.tapir.*\nimport sttp.tapir.generic.auto.*\nimport sttp.tapir.json.circe.*\nimport io.circe.generic.auto.*\nimport io.leisuremeta.chain.lmscan.common.model._\nimport io.circe.*\n\nobject ExploreApi:\n  opaque type Utf8 = String\n  object Utf8:\n    def apply(s: String): Utf8 = s\n\n  extension (u: Utf8) def asString: String = u\n\n  final case class ServerError(msg: String)\n\n  sealed trait UserError:\n    def msg: String\n  final case class Unauthorized(msg: String) extends UserError\n  final case class NotFound(msg: String)     extends UserError\n  final case class BadRequest(msg: String)   extends UserError\n\n  val baseEndpoint = endpoint.errorOut(\n    oneOf[Either[ServerError, UserError]](\n      oneOfVariantFromMatchType(\n        StatusCode.Unauthorized,\n        jsonBody[Right[ServerError, Unauthorized]]\n          .description(\"invalid signature\"),\n      ),\n      oneOfVariantFromMatchType(\n        StatusCode.NotFound,\n        jsonBody[Right[ServerError, NotFound]]\n          .description(\"not found\"),\n      ),\n      oneOfVariantFromMatchType(\n        StatusCode.BadRequest,\n        jsonBody[Right[ServerError, BadRequest]]\n          .description(\"bad request\"),\n      ),\n      oneOfVariantFromMatchType(\n        StatusCode.InternalServerError,\n        jsonBody[Left[ServerError, UserError]]\n          .description(\"internal server error\"),\n      ),\n    ),\n  )\n\n  val getTxPageEndPoint = baseEndpoint.get\n    .in(\"tx\" / \"list\")\n    .in(\n      sttp.tapir.EndpointInput.derived[PageNavigation],\n    )\n    .out(jsonBody[PageResponse[TxInfo]])\n\n  val getTxDetailEndPoint = baseEndpoint.get\n    .in(\"tx\")\n    .in(path[String]) // tx_hash\n    .in(\"detail\")\n    .out(jsonBody[Option[TxDetail]])\n\n  val getBlockPageEndPoint = baseEndpoint.get\n    .in(\"block\" / \"list\")\n    .in(\n      sttp.tapir.EndpointInput.derived[PageNavigation],\n    )\n    .out(jsonBody[PageResponse[BlockInfo]])\n\n  val getBlockDetailEndPoint = baseEndpoint.get\n    .in(\"block\")\n    .in(path[String]) // block_hash\n    .in(\"detail\")\n    .in(query[Option[Int]](\"p\"))\n    .out(jsonBody[Option[BlockDetail]])\n\n  val getAccountPageEndPoint = baseEndpoint.get\n    .in(\"account\" / \"list\")\n    .in(\n      sttp.tapir.EndpointInput.derived[PageNavigation],\n    )\n    .out(jsonBody[PageResponse[AccountInfo]])\n\n  val getAccountDetailEndPoint = baseEndpoint.get\n    .in(\"account\")\n    .in(path[String](\"accountAddr\"))\n    .in(\"detail\")\n    .in(query[Option[Int]](\"p\"))\n    .out(jsonBody[Option[AccountDetail]])\n\n  val getNftPageEndPoint = baseEndpoint.get\n    .in(\"nft\" / \"list\")\n    .in(\n      sttp.tapir.EndpointInput.derived[PageNavigation],\n    )\n    .out(jsonBody[PageResponse[NftInfoModel]])\n  \n  val getNftSeasonEndPoint = baseEndpoint.get\n    .in(\"nft\")\n    .in(path[String](\"season\"))\n    .in(\n      sttp.tapir.EndpointInput.derived[PageNavigation],\n    )\n    .out(jsonBody[PageResponse[NftSeasonModel]])\n\n  val getNftDetailEndPoint = baseEndpoint.get\n    .in(\"nft\")\n    .in(path[String](\"tokenId\"))\n    .in(\"detail\")\n    .out(jsonBody[Option[NftDetail]])\n\n  val getNftOwnerInfoEndPoint = baseEndpoint.get\n    .in(\"nft\")\n    .in(path[String](\"tokenId\"))\n    .in(\"info\")\n    .out(jsonBody[Option[NftOwnerInfo]])\n\n  val getSummaryMainEndPoint = baseEndpoint.get\n    .in(\"summary\")\n    .in(\"main\")\n    .out(jsonBody[Option[SummaryBoard]])\n\n  val getSummaryChartEndPoint = baseEndpoint.get\n    .in(\"summary\")\n    .in(\"chart\")\n    .in(path[String](\"chartType\"))\n    .out(jsonBody[SummaryChart])\n\n  val getTotalBalance = baseEndpoint.get\n    .in(\"total\")\n    .in(\"balance\")\n    .out(jsonBody[Option[String]])\n\n  val getValanceFromChainProd = baseEndpoint.get\n    .in(\"prod\")\n    .in(\"chain\")\n    .out(jsonBody[Option[String]])\n\n  val getKeywordSearchResult = baseEndpoint.get\n    .in(\"search\")\n    .in(path[String](\"keyword\"))\n    .out(jsonBody[SearchResult])\n\n  val getValidators = baseEndpoint.get\n    .in(\"vds\")\n    .out(jsonBody[Seq[NodeValidator.Validator]])\n\n  val getValidator = baseEndpoint.get\n    .in(\"vd\")\n    .in(path[String])\n    .in(query[Option[Int]](\"p\"))\n    .out(jsonBody[Option[NodeValidator.ValidatorDetail]])\n"
  },
  {
    "path": "modules/lmscan-common/shared/src/main/scala/io/leisuremeta/model/AccountDetail.scala",
    "content": "package io.leisuremeta.chain.lmscan.common.model\n\nimport io.circe.Decoder\nimport io.circe.generic.semiauto.deriveDecoder\n\nfinal case class AccountDetail(\n  address: Option[String] = None,\n  balance: Option[BigDecimal] = None,\n  value: Option[BigDecimal] = None,\n  totalCount: Long = 0L,\n  totalPages: Int = 0,\n  payload: Seq[TxInfo] = Seq(),\n) extends ApiModel\n\nobject AccountDetail:\n  given Decoder[AccountDetail] = deriveDecoder[AccountDetail]\n"
  },
  {
    "path": "modules/lmscan-common/shared/src/main/scala/io/leisuremeta/model/AccountInfo.scala",
    "content": "package io.leisuremeta.chain.lmscan.common.model\n\nimport java.time.Instant\n\nfinal case class AccountInfo(\n  address: Option[String],\n  balance: Option[BigDecimal],\n  updated: Option[Instant],\n  value: Option[BigDecimal]\n) extends ApiModel\n"
  },
  {
    "path": "modules/lmscan-common/shared/src/main/scala/io/leisuremeta/model/ApiModel.scala",
    "content": "package io.leisuremeta.chain.lmscan.common.model\n\ntrait ApiModel\n"
  },
  {
    "path": "modules/lmscan-common/shared/src/main/scala/io/leisuremeta/model/BlockDetail.scala",
    "content": "package io.leisuremeta.chain.lmscan.common.model\n\nimport io.circe.Decoder\nimport io.circe.generic.semiauto.deriveDecoder\n\nfinal case class BlockDetail(\n  hash: Option[String] = None,\n  parentHash: Option[String] = None,\n  number: Option[Long] = None,\n  timestamp: Option[Long] = None,\n  txCount: Option[Long] = None,\n  totalCount: Long = 0L,\n  totalPages: Int = 0,\n  payload: Seq[TxInfo] = Seq(),\n) extends ApiModel\n\nobject BlockDetail:\n  given Decoder[BlockDetail] = deriveDecoder[BlockDetail]\n"
  },
  {
    "path": "modules/lmscan-common/shared/src/main/scala/io/leisuremeta/model/BlockInfo.scala",
    "content": "package io.leisuremeta.chain.lmscan.common.model\n\nimport io.circe.Decoder\nimport io.circe.generic.semiauto.deriveDecoder\n\nfinal case class BlockInfo(\n    number: Option[Long] = None,\n    hash: Option[String] = None,\n    txCount: Option[Long] = None,\n    createdAt: Option[Long] = None,\n    proposer: Option[String] = None,\n) extends ApiModel\n\nobject BlockInfo:\n  given Decoder[BlockInfo] = deriveDecoder[BlockInfo]\n"
  },
  {
    "path": "modules/lmscan-common/shared/src/main/scala/io/leisuremeta/model/NftActivity.scala",
    "content": "package io.leisuremeta.chain.lmscan.common.model\n\nimport io.circe.Decoder\nimport io.circe.generic.semiauto.deriveDecoder\n\nfinal case class NftActivity(\n    txHash: Option[String] = None,\n    action: Option[String] = None,\n    fromAddr: Option[String] = None,\n    toAddr: Option[String] = None,\n    createdAt: Option[Long] = None,\n) extends ApiModel \n\nobject NftActivity:\n  given Decoder[NftActivity] = deriveDecoder[NftActivity]\n"
  },
  {
    "path": "modules/lmscan-common/shared/src/main/scala/io/leisuremeta/model/NftDetail.scala",
    "content": "package io.leisuremeta.chain.lmscan.common.model\n\nimport io.circe.Decoder\nimport io.circe.generic.semiauto.deriveDecoder\n\nfinal case class NftDetail(\n    nftFile: Option[NftFileModel] = None,\n    activities: Option[Seq[NftActivity]] = None,\n) extends ApiModel \n\nobject NftDetail:\n  given Decoder[NftDetail] = deriveDecoder[NftDetail]\n"
  },
  {
    "path": "modules/lmscan-common/shared/src/main/scala/io/leisuremeta/model/NftFileModel.scala",
    "content": "package io.leisuremeta.chain.lmscan.common.model\n\nimport io.circe.Decoder\nimport io.circe.generic.semiauto.deriveDecoder\n\nfinal case class NftFileModel(\n    tokenId: Option[String] = None,\n    tokenDefId: Option[String] = None,\n    collectionName: Option[String] = None,\n    nftName: Option[String] = None,\n    nftUri: Option[String] = None,\n    creatorDescription: Option[String] = None,\n    dataUrl: Option[String] = None,\n    rarity: Option[String] = None,\n    creator: Option[String] = None,\n    eventTime: Option[Long] = None,\n    createdAt: Option[Long] = None,\n    owner: Option[String] = None,\n) extends ApiModel \n\nobject NftFileModel:\n  given Decoder[NftFileModel] = deriveDecoder[NftFileModel]\n"
  },
  {
    "path": "modules/lmscan-common/shared/src/main/scala/io/leisuremeta/model/NftInfo.scala",
    "content": "package io.leisuremeta.chain.lmscan.common.model\n\nimport java.time.Instant\n\nfinal case class NftInfoModel(\n    season: Option[String] = None,\n    seasonName: Option[String] = None,\n    totalSupply: Option[Int] = None,\n    startDate: Option[Instant] = None,\n    endDate: Option[Instant] = None,\n    thumbUrl: Option[String] = None,\n) extends ApiModel \n"
  },
  {
    "path": "modules/lmscan-common/shared/src/main/scala/io/leisuremeta/model/NftOwnerInfo.scala",
    "content": "package io.leisuremeta.chain.lmscan.common.model\n\nfinal case class NftOwnerInfo(\n    owner: Option[String] = None,\n    nftUrl: Option[String] = None,\n)\n"
  },
  {
    "path": "modules/lmscan-common/shared/src/main/scala/io/leisuremeta/model/NftSeasonModel.scala",
    "content": "package io.leisuremeta.chain.lmscan.common.model\n\nfinal case class NftSeasonModel(\n    nftName: Option[String] = None,\n    tokenId: Option[String] = None,\n    tokenDefId: Option[String] = None,\n    creator: Option[String] = None,\n    rarity: Option[String] = None,\n    thumbUrl: Option[String] = None,\n    collection: Option[String] = None,\n) extends ApiModel\n"
  },
  {
    "path": "modules/lmscan-common/shared/src/main/scala/io/leisuremeta/model/PageNavigation.scala",
    "content": "package io.leisuremeta.chain.lmscan.common.model\n\nimport sttp.tapir.EndpointIO.annotations.query\nimport io.getquill.Ord\nimport io.getquill.ast.Asc\nimport io.getquill.ast.Desc\n\ncase class PageNavigation(\n    @query\n    pageNo: Int,\n    @query\n    sizePerRequest: Int,\n    // @query\n    // orderByProperty: Option[String], // ex) id:desc\n) extends ApiModel \n// ):\n//   def orderBy(): OrderBy =\n//     val items = this.orderByProperty.get.split(\":\")\n//     val x = items(1) match\n//       case \"asc\"  => Ord(Asc)\n//       case \"desc\" => Ord(Desc)\n//     val direction = OrderBy.toDirection(items(1))\n//     new OrderBy(items(0), direction)\n\ncase class OrderBy(\n    property: String,\n    direction: Ord[Any],\n)\n\nobject OrderBy:\n  def toDirection(order: String): Ord[Any] =\n    order match\n      case \"asc\"  => Ord(Asc)\n      case \"desc\" => Ord(Desc)\n"
  },
  {
    "path": "modules/lmscan-common/shared/src/main/scala/io/leisuremeta/model/PageResponse.scala",
    "content": "package io.leisuremeta.chain.lmscan.common.model\n\nfinal case class PageResponse[T](\n    totalCount: Long = 0L,\n    totalPages: Int = 0,\n    payload: Seq[T] = Seq(),\n) extends ApiModel\n\nobject PageResponse:\n    def from[T](total: Long, cnt: Int, seq: Seq[T]) =\n        val totalPage = Math.ceil(total.toDouble / cnt).toInt;\n        PageResponse(total, totalPage, seq)\n"
  },
  {
    "path": "modules/lmscan-common/shared/src/main/scala/io/leisuremeta/model/SearchResult.scala",
    "content": "package io.leisuremeta.chain.lmscan.common.model\n\nimport io.circe.HCursor\nimport io.circe.Decoder\n\nenum SearchResult:\n  case acc(item: AccountDetail)\n  case tx(item: TxDetail)\n  case blc(item: BlockDetail)\n  case nft(item: NftDetail)\n  case empty\n\nobject SearchResult:\n  given Decoder[SearchResult] = (c: HCursor) =>\n    c.keys match\n      case Some(k) if k.head == \"acc\" => \n        c.downField(\"acc\").get[AccountDetail](\"item\").map(item => SearchResult.acc(item))\n      case Some(k) if k.head == \"tx\" => \n        c.downField(\"tx\").get[TxDetail](\"item\").map(item => SearchResult.tx(item))\n      case Some(k) if k.head == \"blc\" => \n        c.downField(\"blc\").get[BlockDetail](\"item\").map(item => SearchResult.blc(item))\n      case Some(k) if k.head == \"nft\" => \n        c.downField(\"nft\").get[NftDetail](\"item\").map(item => SearchResult.nft(item))\n      case _ => Decoder.resultInstance.pure(SearchResult.empty)\n      // res = acc.downField(\"item\").as[AccountDetail]\n    // SearchResult.acc(res.get.)\n"
  },
  {
    "path": "modules/lmscan-common/shared/src/main/scala/io/leisuremeta/model/SummaryModel.scala",
    "content": "package io.leisuremeta.chain.lmscan.common.model\n\nfinal case class SummaryModel(\n    lmPrice: Option[Double] = None,\n    blockNumber: Option[Long] = None,\n    totalAccounts: Option[Long] = None,\n    createdAt: Option[Long] = None,\n    totalTxSize: Option[Long] = None,\n    totalBalance: Option[BigDecimal] = None,\n    marketCap: Option[BigDecimal] = None,\n    cirSupply: Option[BigDecimal] = None,\n    totalNft: Option[Long] = None,\n) extends ApiModel \n\nfinal case class SummaryBoard(\n    today: SummaryModel = SummaryModel(),\n    yesterday: SummaryModel = SummaryModel(),\n) extends ApiModel\n\nfinal case class SummaryChart(\n    list: Seq[SummaryModel] = Seq()\n) extends ApiModel\n"
  },
  {
    "path": "modules/lmscan-common/shared/src/main/scala/io/leisuremeta/model/TxDetail.scala",
    "content": "package io.leisuremeta.chain.lmscan.common.model\n\nimport io.circe.Decoder\nimport io.circe.generic.semiauto.deriveDecoder\n\nfinal case class TxDetail(\n    hash: Option[String] = None,\n    createdAt: Option[Long] = None,\n    json: Option[String] = None,\n) extends ApiModel \n\nobject TxDetail:\n  given Decoder[TxDetail] = deriveDecoder[TxDetail]\n"
  },
  {
    "path": "modules/lmscan-common/shared/src/main/scala/io/leisuremeta/model/TxInfo.scala",
    "content": "package io.leisuremeta.chain.lmscan.common.model\n\nimport io.circe.Decoder\nimport io.circe.generic.semiauto.deriveDecoder\n\nfinal case class TxInfo(\n    hash: Option[String] = None,\n    blockNumber: Option[Long] = None,\n    createdAt: Option[Long] = None,\n    txType: Option[String] = None,\n    tokenType: Option[String] = None,\n    signer: Option[String] = None,\n    subType: Option[String] = None,\n    inOut: Option[String] = None,\n    value: Option[String] = None,\n) extends ApiModel \n\nobject TxInfo:\n  given Decoder[TxInfo] = deriveDecoder[TxInfo]\n"
  },
  {
    "path": "modules/lmscan-common/shared/src/main/scala/io/leisuremeta/model/Validator.scala",
    "content": "package io.leisuremeta.chain.lmscan.common.model\n\nimport io.circe.Decoder\nimport io.circe.generic.semiauto.deriveDecoder\n\nobject NodeValidator:\n  case class Validator(\n      address: Option[String] = None,\n      power: Option[Double] = None,\n      cnt: Option[Long] = None,\n      name: Option[String] = None,\n  ) extends ApiModel \n  case class ValidatorList(\n    payload: List[Validator] = Nil,\n  ) extends ApiModel\n  case class ValidatorDetail(\n    validator: Validator = Validator(),\n    page: PageResponse[BlockInfo] = PageResponse(),\n  ) extends ApiModel\n  given Decoder[Validator] = deriveDecoder[Validator]\n  given Decoder[ValidatorList] = deriveDecoder[ValidatorList]\n  given Decoder[ValidatorDetail] = deriveDecoder[ValidatorDetail]\n"
  },
  {
    "path": "modules/lmscan-frontend/.parcelrc",
    "content": "{\n  \"extends\": \"@parcel/config-default\",\n  \"compressors\": {\n    \"*.{html,css,js,svg,map}\": [\n      \"...\",\n      \"@parcel/compressor-gzip\",\n      \"@parcel/compressor-brotli\"\n    ]\n  }\n}\n"
  },
  {
    "path": "modules/lmscan-frontend/assets/css/desktop.css",
    "content": "@charset \"utf-8\";\r\n@import url(./tooltip.css);\r\n\r\n.con-wrap {\r\n  min-height: calc(100vh - 350px);\r\n}\r\n\r\n.con-wrap > * {\r\n  gap: var(--pad);\r\n}\r\n\r\n.board-comp {\r\n  grid-column: span 3;\r\n  grid-template-columns: repeat(2, 50%);\r\n  gap: 8px;\r\n  > * {\r\n    grid-column: span 1;\r\n  }\r\n  > :first-child {\r\n    grid-row: 1;\r\n    grid-column: 1 / 3;\r\n    font-weight: var(--bold);\r\n  }\r\n  > :nth-child(2) {\r\n    grid-row: 2;\r\n    grid-column: 1 / 3;\r\n    font-size: var(--t-text);\r\n    font-weight: var(--bold);\r\n    color: var(--b1);\r\n  }\r\n  > :nth-child(3) {\r\n    grid-row: 3;\r\n    display: flex;\r\n    gap: 11px;\r\n    align-items: center;\r\n    > :first-child {\r\n      padding: 4px 6px;\r\n      font-size: 10px;\r\n      background-color: var(--w3);\r\n      border-radius: var(--br);\r\n    }\r\n    > :last-child {\r\n      font-size: 12px;\r\n      font-weight: var(--bold);\r\n      &.pos {\r\n        color: var(--darken-pri);\r\n        &::before {\r\n          content: \"+\";\r\n        }\r\n      }\r\n      &.neg {\r\n        color: var(--darken-sec);\r\n      }\r\n    }\r\n  }\r\n  svg {\r\n    grid-row: 3 / 4;\r\n    fill: transparent;\r\n    aspect-ratio: 4;\r\n    justify-self: end;\r\n    align-self: self-end;\r\n    width: 100%;\r\n    path {\r\n      stroke: var(--lighten-pri);\r\n      stroke-width: 1;\r\n      stroke-linejoin: round;\r\n    }\r\n    polyline {\r\n      fill: var(--lightest-pri);\r\n    }\r\n  }\r\n  @media screen and (max-width: 1200px) {\r\n    grid-column: span 6;   \r\n  }\r\n}\r\n\r\nnav {\r\n  display: flex;\r\n  gap: calc(2 * var(--pad));\r\n  padding-top: var(--pad);\r\n  padding-bottom: 12px;\r\n  align-items: baseline;\r\n  * {\r\n    font-size: var(--t-text);\r\n    color: var(--b1);\r\n    font-weight: var(--bold);\r\n  }\r\n  > :first-child {\r\n    font-size: 32px;\r\n    cursor: pointer;\r\n  }\r\n}\r\n\r\n.con-wrap > .table-container:not(:last-child) {\r\n  margin-bottom: calc(var(--pad) - 8px);\r\n}\r\n.table-container {\r\n  grid-column: span 6;\r\n  background-color: var(--w0);\r\n  border-radius: var(--br);\r\n  display: flex;\r\n  flex-direction: column;\r\n  row-gap: calc(var(--pad) / 2);\r\n  border: 1px solid var(--lightest-pri);\r\n  padding: var(--pad);\r\n  position: relative;\r\n}\r\n.detail.table-container {\r\n  row-gap: var(--pad);\r\n}\r\n.main-table .table-container {\r\n  row-gap: var(--br);\r\n}\r\n.table-head {\r\n  font-weight: var(--bold);\r\n}\r\n.table-container .row {\r\n  display: grid;\r\n  padding-bottom: calc(var(--pad) / 2);\r\n  border-bottom: 1px solid var(--lightest-pri);\r\n  gap: var(--pad);\r\n}\r\n.table-container .row > * {\r\n  overflow: hidden;\r\n  text-overflow: ellipsis;\r\n}\r\n.table-container.blc .row {\r\n  grid-template-columns: 2fr 2fr 7fr 1fr;\r\n}\r\n.table-container.tx-m .row {\r\n  grid-template-columns: 5fr 2fr 5fr;\r\n}\r\n.table-container.tx .row {\r\n  grid-template-columns: 4fr 1fr 1fr 4fr 2fr;\r\n}\r\n.table-container.accs .row {\r\n  grid-template-columns: 5fr 3fr 2fr 2fr;\r\n}\r\n.table-container.nfts .row {\r\n  grid-template-columns: 1fr 5fr 2fr 2fr 2fr;\r\n}\r\n.table-container.nft-token .row {\r\n  grid-template-columns: 1fr 3fr 4fr 3fr 1fr;\r\n}\r\n.table-container.nft .row {\r\n  grid-template-columns: 3fr 1fr 2fr 3fr 3fr;\r\n}\r\n.table-container.vds .row {\r\n  grid-template-columns: 5fr 3fr 4fr;\r\n}\r\n.table-title {\r\n  display: flex;\r\n  justify-content: space-between;\r\n  padding-bottom: calc(2 * var(--br));\r\n\r\n  > :first-child {\r\n    font-size: var(--t-text);\r\n    color: var(--b1);\r\n    font-weight: var(--bold);\r\n  }\r\n  > :last-child {\r\n    color: var(--w0);\r\n    font-size: 10px;\r\n    background-color: var(--primary);\r\n    border-radius: var(--br);\r\n    padding: 4px 16px;\r\n  }\r\n}\r\n\r\n.page-title {\r\n  display: block;\r\n  font-size: var(--t-text);\r\n  font-weight: var(--bold);\r\n  color: var(--b1);\r\n}\r\n\r\n.detail .row {\r\n  grid-template-columns: repeat(12, 1fr);\r\n  padding-bottom: 0;\r\n  border: none;\r\n  :first-child {\r\n    grid-column: span 4;\r\n  }\r\n  :last-child {\r\n    grid-column: span 8;\r\n  }\r\n  &:last-child {\r\n    padding-bottom: 0;\r\n  }\r\n}\r\n.detail .row.tri {\r\n  :first-child {\r\n    font-weight: var(--bold);\r\n    grid-column: span 2;\r\n  }\r\n  :nth-child(2) {\r\n    grid-column: span 5;\r\n  }\r\n  :last-child {\r\n    grid-column: span 5;\r\n  }\r\n}\r\n\r\n.nft-detail {\r\n  gap: 24px;\r\n}\r\n\r\n.nft-detail > img, .nft-detail > video {\r\n  grid-column: span 4;\r\n  width: 100%;\r\n}\r\n.nft-detail > .nft-title {\r\n  grid-column: span 8;\r\n}\r\n.nft-detail > .table-container {\r\n  grid-column: span 8;\r\n}\r\n.table-search{\r\n  display: flex;\r\n  gap: var(--br);\r\n  width: 100%;\r\n  padding-top: var(--pad);\r\n  align-items: center;\r\n  justify-content: center;\r\n  font-family: JSDongkang,Roboto,sans-serif;\r\n}\r\n.table-search a, .table-search p {\r\n  border-radius: var(--br);\r\n  padding: 5px 16px;\r\n  background-color: var(--lightest-pri);\r\n  &.dis {\r\n    pointer-events: none;\r\n    cursor: default;\r\n    color: var(--g);\r\n  }\r\n}\r\n.table-search p {\r\n  cursor: pointer;\r\n}\r\n\r\n.type-search {\r\n  border-radius: 5px;\r\n  background-position: 50% 50%;\r\n  padding: 3px 16px;\r\n  width: 100px;\r\n  background-color: var(--lightest-pri);\r\n}\r\n\r\n.blc-num, .tx-hash, .blc-hash, .acc-hash, .token-id {\r\n  color: var(--darken-pri);\r\n  cursor: pointer;\r\n}\r\n\r\n.search-container {\r\n  display: grid;\r\n  height: 3em;\r\n  grid-template-columns: repeat(12, 1fr);\r\n}\r\n.search-container > * {\r\n  font-size: 16px;\r\n}\r\n.search-container > :first-child {\r\n  padding-left: var(--pad);\r\n  grid-column: span 10;\r\n  border-radius: var(--br) 0 0 var(--br);\r\n  ::placeholder {\r\n    color: var(--lightest-pri)\r\n  }\r\n}\r\n.search-container > :last-child {\r\n  display: flex;\r\n  align-items: center;\r\n  justify-content: center;\r\n  cursor: pointer;\r\n  border: 1px solid var(--darken-pri);\r\n  border-radius: 0 var(--br) var(--br) 0;\r\n  background-color: var(--darken-pri);\r\n  color: var(--w0);\r\n  grid-column: span 2;\r\n}\r\n\r\ninput {\r\n  border: 1px solid var(--lightest-pri);\r\n  &:focus {\r\n    outline: none;\r\n    border-color: var(--primary);\r\n    color: var(--primary);\r\n  }\r\n}\r\n"
  },
  {
    "path": "modules/lmscan-frontend/assets/css/footer.css",
    "content": "footer {\r\n  background-color: var(--w0);\r\n}\r\nfooter * {\r\n  color: #303972;\r\n  font-size: 16px;\r\n}\r\nfooter p > *, footer dt {\r\n  color: var(--b1);\r\n}\r\nfooter > div { \r\n  display: flex;\r\n  flex-direction: row; \r\n  justify-content: space-between;\r\n  gap: var(--pad);\r\n}\r\nfooter .footer-left {\r\n  flex-basis: 100%;\r\n  display: flex;\r\n  flex-wrap: wrap;\r\n  flex-direction: column;\r\n  gap: var(--pad);\r\n  > :first-child {\r\n    display: flex;\r\n    align-items: end;\r\n    gap: .5em;\r\n    strong {\r\n      font-size: var(--t-text);\r\n    }\r\n  }\r\n  strong {\r\n    font-size: 16px;\r\n    font-weight: var(--bold);\r\n  }\r\n}\r\n\r\nfooter .footer-right{\r\n  flex-basis: 100%;\r\n  display: flex;\r\n  justify-content: center;\r\n  dl {\r\n    flex: 0 0 40%;\r\n  }\r\n  dt {\r\n    font-size: 14px;\r\n    font-weight: var(--bold);\r\n  }\r\n  dd{\r\n    padding-top: var(--br);\r\n    display: flex;\r\n    flex-wrap: wrap;\r\n    flex-direction: column;\r\n    gap: 12px;\r\n  }\r\n  a{\r\n    display: inline-flex;\r\n    align-items: center;\r\n    font-size: 10px;\r\n    text-decoration: none;\r\n    height: 20px;\r\n    padding-left:35px;\r\n    background-position: left top;\r\n    background-size: auto 20px;\r\n    background-repeat: no-repeat;\r\n    &:hover{opacity: 0.8;}\r\n  }\r\n}\r\n.icon-The-Moon-Labs{background-image: url(../img/footer/TMLS_303972.png);}\r\n.icon-leisuremetaverse{background-image: url(../img/footer/LM.png);}\r\n.icon-playnomm{background-image: url(../img/footer/playnomm.png);}\r\n.icon-lm-nova{background-image: url(../img/footer/ilike.png);}\r\n.icon-tme{background-image: url(../img/footer/tme.png);}\r\n.icon-github{background-image: url(../img/footer/Github.png);}\r\n.icon-telegram{background-image: url(../img/footer/icon_telegram.svg);}\r\n.icon-twitter{background-image: url(../img/footer/Tweeter.png);}\r\n.icon-discord{background-image: url(../img/footer/Discord.png);}\r\n\r\n@media screen and (max-width: 800px) {\r\n  footer  > div {\r\n    flex-wrap: wrap;\r\n  }\r\n}\r\n"
  },
  {
    "path": "modules/lmscan-frontend/assets/css/index.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"UTF-8\" />\n    <title>LeisureMeta Chain Block Explorer</title>\n    <link rel=\"stylesheet\" type=\"text/css\" href=\"css/reset.css\">\n    <link rel=\"stylesheet\" type=\"text/css\" href=\"css/style.css\" media=\"all\"/>\n    <!-- <link rel=\"stylesheet\" type=\"text/css\" href=\"css/desktop.css\" media=\"screen and (min-width: 1211px)\"/> -->\n    <!-- <link href=\"css/mid.css\" rel=\"stylesheet\" type=\"text/css\" media=\"screen and (max-width: 1210px)\" /> -->\n    <link href=\"css/mobile.css\" rel=\"stylesheet\" type=\"text/css\" media=\"screen and (max-width: 800px)\" />\n    <link rel=\"stylesheet\" type=\"text/css\" href=\"css/tooltip.css\">\n    <link rel=\"stylesheet\" type=\"text/css\" href=\"css/loading.css\">\n    <link rel=\"stylesheet\" type=\"text/css\" href=\"css/footer.css\" media=\"all\"/>\n    <link rel=\"shortcut icon\" href=\"img/favicon.ico\">\n    <link rel=\"stylesheet\" href=\"https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200\">\n    <link rel=\"stylesheet\" href=\"https://fonts.googleapis.com/css?family=Readex Pro\">\n    <link rel=\"stylesheet\" href='https://fonts.googleapis.com/css?family=Roboto'  type='text/css'>\n  </head>\n\n  <body>\n    <div id=\"app-container\"></div>\n    <script type=\"module\" src=\"./load-main.js\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "modules/lmscan-frontend/assets/css/loading.css",
    "content": "div.loader-case {\n  display: flex;\n  padding: 24px 0;\n  background: rgba(15, 14, 14, 0.4);\n  grid-column: span 12;\n  border-radius: var(--br);\n}\n\n.loader {\n  position: relative;\n  left: calc(50% - 24px);\n  top: calc(50% - 24px);\n  -webkit-animation: spin 2s linear infinite; /* Safari */\n  width: 48px;\n  height: 48px;\n  border-radius: 50%;\n  border: 10px solid;\n  border-color: rgba(255, 255, 255, 0.25) rgba(255, 255, 255, 0.45) rgba(255, 255, 255, 0.55) rgba(255, 255, 255, 0.95);\n  animation: rotation 1s linear infinite;\n}\n\n/* Safari */\n@-webkit-keyframes spin {\n  0% { -webkit-transform: rotate(0deg); }\n  100% { -webkit-transform: rotate(360deg); }\n}\n\n@keyframes rotation {\n  0% {\n    transform: rotate(0deg);\n  }\n  100% {\n    transform: rotate(360deg);\n  }\n} \n"
  },
  {
    "path": "modules/lmscan-frontend/assets/css/mobile.css",
    "content": "@charset \"utf-8\";\r\nhtml {\r\n  --pad: 20px;\r\n  font-size: 10px;\r\n  --t-text: 14px;\r\n  --b-c: 2px solid var(--lightest-pri);\r\n}\r\n\r\n.con-wrap {\r\n  min-height: calc(100vh - 350px);\r\n}\r\n\r\n.con-wrap > * {\r\n  gap: var(--br);\r\n}\r\n\r\n.board-area {\r\n  gap: 0;\r\n  height: 190px;\r\n  grid-template-rows: repeat(3, 1fr);\r\n  border: 1px solid var(--lightest-pri);\r\n  border-radius: var(--br);\r\n  padding: var(--br) var(--pad);\r\n  background-color: var(--w0);\r\n  >:nth-child(2),\r\n  >:nth-child(3),\r\n  >:nth-child(6),\r\n  >:nth-child(7),\r\n  >:nth-child(8) {\r\n    display: none;\r\n  }\r\n  >.board-comp:nth-child(4) {\r\n    border-top: var(--b-c);\r\n    border-bottom: var(--b-c);\r\n  }\r\n}\r\n\r\n.board-comp, .board-comp.chart {\r\n  grid-column: span 12;\r\n  gap: var(--br) 0;\r\n  padding: var(--br) 0;\r\n  grid-template-columns: repeat(6, 1fr);\r\n  border: none;\r\n  border-radius: 0;\r\n  > :first-child {\r\n    grid-column: span 6;\r\n  }\r\n  > :nth-child(2) {\r\n    grid-column: span 2;\r\n  }\r\n  > :nth-child(3) {\r\n    grid-column: span 4;\r\n    flex-direction: row-reverse;\r\n    > :first-child {\r\n      background-color: transparent;\r\n      &::before {\r\n        content: 'Last ';\r\n      }\r\n    }\r\n  }\r\n  svg {\r\n    display: none;\r\n  }\r\n}\r\n\r\nnav {\r\n  display: flex;\r\n  gap: calc(2 * var(--pad));\r\n  padding-top: var(--pad);\r\n  padding-bottom: 12px;\r\n  align-items: baseline;\r\n  > :first-child {\r\n    font-size: 32px;\r\n  }\r\n}\r\n\r\n.table-container {\r\n  grid-column: span 12;\r\n  padding: var(--br) var(--pad);\r\n  .row {\r\n    grid-column: span 12;\r\n    height: 23px;\r\n  }\r\n}\r\n.table-container.blc .row {\r\n  grid-template-columns: 2fr 2fr 6fr 2fr;\r\n}\r\n.table-container.tx-m .row {\r\n  grid-template-columns: 5fr 2fr 5fr;\r\n}\r\n.table-container.tx .row {\r\n  grid-template-columns: 5fr 2fr 5fr;\r\n  >:nth-child(2),\r\n  >:nth-child(5) {\r\n    display: none;\r\n  }\r\n}\r\n.table-container.accs .row {\r\n  grid-template-columns: 9fr 3fr;\r\n  >:nth-child(3),\r\n  >:nth-child(4) {\r\n    display: none;\r\n  }\r\n}\r\n.table-container.nfts .row {\r\n  grid-template-columns: 2fr 9fr 3fr;\r\n  >:nth-child(4),\r\n  >:nth-child(5) {\r\n    display: none;\r\n  }\r\n}\r\n.table-container.nft-token .row {\r\n  grid-template-columns: 2fr 9fr 3fr;\r\n  >:nth-child(3),\r\n  >:nth-child(4) {\r\n    display: none;\r\n  }\r\n}\r\n.table-container.nft .row {\r\n  grid-template-columns: 3fr 1fr 2fr 3fr 3fr;\r\n}\r\n.main-table {\r\n  position: relative;\r\n  height: 710px;\r\n  background-color: var(--w0);\r\n  border-radius: var(--br);\r\n  border: 1px solid var(--lightest-pri);\r\n\r\n  .table-container {\r\n    padding-top: 0;\r\n    position: absolute;\r\n    bottom: 0;\r\n    border: none;\r\n  }\r\n\r\n  .table-title {\r\n    height: 0;\r\n    > :first-child {\r\n      width: calc(50vw - 2 * var(--pad));\r\n      height: 25px;\r\n      font-size: 14px;\r\n      font-weight: normal;\r\n      border-bottom: var(--b-c);\r\n    }\r\n    > :last-child {\r\n      display: none;\r\n    }\r\n  }\r\n  .blc .table-title > :first-child {\r\n    transform: translate(0, -30px);\r\n    z-index: 2;\r\n  }\r\n  .tx-m .table-title > :first-child {\r\n    text-align: right;\r\n    transform: translate(100%, -30px);\r\n    z-index: 2;\r\n  }\r\n  .table-title > :first-child:has(:checked) {\r\n    border-color: var(--darken-pri);\r\n  }\r\n  .table-container:has(input:checked) {\r\n    z-index: 2;\r\n  }\r\n}\r\n\r\n.page-title {\r\n  display: block;\r\n  font-size: var(--t-text);\r\n  font-weight: var(--bold);\r\n}\r\n\r\n.con-wrap:has(.detail.table-container) {\r\n  gap: 0;\r\n}\r\n\r\n.detail.table-container:has(+ .page-title) {\r\n  margin: var(--br) 0;\r\n}\r\n\r\n.detail.table-container + .page-title {\r\n  margin-bottom: var(--br);\r\n}\r\n\r\n.detail.table-container:has(+ .detail.table-container) {\r\n  border-bottom-left-radius: 0;\r\n  border-bottom-right-radius: 0;\r\n  border-bottom: 0;\r\n  > :last-child {\r\n    padding-bottom: var(--br);\r\n    border-bottom: var(--b-c);\r\n  }\r\n}\r\n.detail.table-container + .detail.table-container {\r\n  border-top-left-radius: 0;\r\n  border-top-right-radius: 0;\r\n  border-top: 0;\r\n}\r\n\r\n.detail .row {\r\n  border-bottom: none;\r\n  height: auto;\r\n  * {\r\n    word-wrap: break-word;\r\n    white-space: unset;\r\n  }\r\n  :first-child {\r\n    grid-column: span 12;\r\n  }\r\n  :last-child {\r\n    grid-column: span 12;\r\n  }\r\n  &:last-child {\r\n    padding-bottom: 0;\r\n  }\r\n}\r\n.detail .row.tri {\r\n  :first-child {\r\n    font-weight: var(--bold);\r\n    grid-column: span 2;\r\n  }\r\n  :nth-child(2) {\r\n    grid-column: span 5;\r\n  }\r\n  :last-child {\r\n    grid-column: span 5;\r\n  }\r\n}\r\n\r\n.nft-detail {\r\n  grid-template-columns: 1fr;\r\n  grid-template-rows: repeat(3, auto);\r\n  gap: var(--br);\r\n}\r\n\r\n.nft-detail > img {\r\n  grid-column: 1 / 2;\r\n  grid-row: 2 / 3;\r\n  border-radius: var(--br);\r\n}\r\n.nft-detail > .nft-title {\r\n  grid-column: 1 / 2;\r\n}\r\n.nft-detail > .table-container {\r\n  grid-column: 1 / 2;\r\n}\r\n\r\n.table-search{\r\n  padding-top: 0;\r\n  > :first-child { display: none; }\r\n  > :last-child { display: none; }\r\n}\r\n.table-search a, .table-search p {\r\n  padding: 5px 16px;\r\n}\r\n\r\n.blc-num, .tx-hash, .blc-hash, .acc-hash, .token-id {\r\n  color: var(--darken-pri);\r\n  cursor: pointer;\r\n}\r\n\r\n.main > header {\r\n  padding: 0;\r\n}\r\n\r\n.search-area {\r\n  width: 100vw;\r\n  padding: var(--pad);\r\n  padding-bottom: 0;\r\n  background-color: #f6f7fe; \r\n}\r\n.search-container > * {\r\n  font-size: 16px;\r\n}\r\n.search-container > :first-child {\r\n  grid-column: span 12;\r\n  border-radius: var(--br);\r\n  ::placeholder {\r\n    color: var(--lightest-pri)\r\n  }\r\n}\r\n.search-container > :last-child {\r\n  display: none;\r\n}\r\n\r\n.err-wrap {\r\n  display: flex;\r\n  flex-direction: column;\r\n  align-items: center;\r\n  > :first-child {\r\n    font-size: 32px;\r\n  }\r\n  > :nth-child(2) {\r\n    font-size: var(--t-text);\r\n  }\r\n}\r\n\r\nnav {\r\n  flex-wrap: wrap;\r\n}\r\n"
  },
  {
    "path": "modules/lmscan-frontend/assets/css/reset.css",
    "content": "/* http://meyerweb.com/eric/tools/css/reset/ \r\n   v2.0 | 20110126\r\n   License: none (public domain)\r\n*/\r\n\r\nhtml, body, div, span, applet, object, iframe,\r\nh1, h2, h3, h4, h5, h6, p, blockquote, pre,\r\na, abbr, acronym, address, big, cite, code,\r\ndel, dfn, em, img, ins, kbd, q, s, samp,\r\nsmall, strike, strong, sub, sup, tt, var,\r\nb, u, i, center,\r\ndl, dt, dd, ol, ul, li,\r\nfieldset, form, label, legend,\r\ntable, caption, tbody, tfoot, thead, tr, th, td,\r\narticle, aside, canvas, details, embed, \r\nfigure, figcaption, footer, header, hgroup, \r\nmenu, nav, output, ruby, section, summary,\r\ntime, mark, audio, video {\r\n\tmargin: 0;\r\n\tpadding: 0;\r\n\tborder: 0;\r\n\tfont-size: 100%;\r\n\tfont: inherit;\r\n\tvertical-align: baseline;\r\n}\r\n/* HTML5 display-role reset for older browsers */\r\narticle, aside, details, figcaption, figure, \r\nfooter, header, hgroup, menu, nav, section {\r\n\tdisplay: block;\r\n}\r\nbody {\r\n\tline-height: 1;\r\n}\r\nol, ul {\r\n\tlist-style: none;\r\n}\r\nblockquote, q {\r\n\tquotes: none;\r\n}\r\nblockquote:before, blockquote:after,\r\nq:before, q:after {\r\n\tcontent: '';\r\n\tcontent: none;\r\n}\r\ntable {\r\n\tborder-collapse: collapse;\r\n\tborder-spacing: 0;\r\n}\r\n"
  },
  {
    "path": "modules/lmscan-frontend/assets/css/style.css",
    "content": "@charset \"utf-8\";\r\n@import url(./footer.css);\r\n@import url(./loading.css);\r\n\r\n* {\r\n  box-sizing: border-box;\r\n  color: var(--b3);\r\n}\r\nhtml {\r\n  font-family: 'Roboto', sans-serif;\r\n  --primary: #3d5afe;\r\n  --secondary: #ff7e40;\r\n  --lightest-pri: #dadff7;\r\n  --lighten-pri: #536dfe;\r\n  --darken-pri: #304ffe;\r\n  --lightest-sec: #ff9e80;\r\n  --darken-sec: #ff3d00;\r\n  --w0: #fff;\r\n  --w1: #f5f5f5;\r\n  --w2: #eee;\r\n  --w3: #e0e0e0;\r\n  --g: #9e9e9e;\r\n  --b3: #757575;\r\n  --b2: #616161;\r\n  --b1: #424242;\r\n  --b0: #000;\r\n  --pad: 24px;\r\n  --br: 8px;\r\n  --t-text: 20px;\r\n  font-size: 14px;\r\n  --bold: 600;\r\n}\r\na {\r\n  cursor: pointer;\r\n}\r\n.main {\r\n  background-color: #f6f7fe;\r\n}\r\n.main > * {\r\n  display: flex; \r\n  flex-flow: column;\r\n  align-items: center;\r\n  padding: var(--pad) 0;\r\n}\r\n\r\n.main > * > * {\r\n  max-width: 1200px;\r\n  width: calc(100vw - 2 * var(--pad));\r\n}\r\n\r\nheader {\r\n  background-color: var(--w0);\r\n}\r\n\r\n.con-wrap {\r\n  gap: 8px var(--pad);\r\n}\r\n\r\n.con-wrap > * {\r\n  display: grid;\r\n  grid-template-columns: repeat(12, 1fr);\r\n}\r\n\r\n.board-area {\r\n  gap: var(--pad);\r\n  margin-bottom: calc(var(--pad) - 8px);\r\n}\r\n\r\n.board-comp {\r\n  background-color: var(--w0);\r\n  border-radius: var(--br);\r\n  border: 1px solid var(--lightest-pri);\r\n  padding: 10px 16px;\r\n  display: grid;\r\n  > :first-child {\r\n    font-weight: var(--bold);\r\n  }\r\n  > :nth-child(2) {\r\n    font-size: var(--t-text);\r\n    font-weight: var(--bold);\r\n    color: var(--b1);\r\n  }\r\n  > :nth-child(3) {\r\n    display: flex;\r\n    align-items: center;\r\n    gap: 11px;\r\n    > :first-child {\r\n      background-color: var(--w3);\r\n      border-radius: var(--br);\r\n      padding: 3px 5px;\r\n    }\r\n    > :last-child {\r\n      font-weight: var(--bold);\r\n      &.pos {\r\n        color: var(--darken-pri);\r\n        &::before {\r\n          content: \"+\";\r\n        }\r\n      }\r\n      &.neg {\r\n        color: var(--darken-sec);\r\n      }\r\n    }\r\n  }\r\n}\r\n\r\nnav {\r\n  display: flex;\r\n  gap: calc(2 * var(--pad));\r\n  padding-top: var(--pad);\r\n  padding-bottom: 12px;\r\n  align-items: baseline;\r\n  * {\r\n    font-size: var(--t-text);\r\n    color: var(--b1);\r\n    font-weight: var(--bold);\r\n  }\r\n  > :first-child {\r\n    font-size: 32px;\r\n    cursor: pointer;\r\n  }\r\n}\r\n\r\n.table-container {\r\n  grid-column: span 6;\r\n  background-color: var(--w0);\r\n  border-radius: var(--br);\r\n  display: flex;\r\n  flex-direction: column;\r\n  row-gap: 8px;\r\n  border: 1px solid var(--lightest-pri);\r\n  padding: var(--pad);\r\n  position: relative;\r\n}\r\n.table-head {\r\n  font-weight: var(--bold);\r\n}\r\n.table-container .row {\r\n  display: grid;\r\n  padding-bottom: 8px;\r\n  border-bottom: 1px solid var(--lightest-pri);\r\n  gap: var(--br);\r\n}\r\n.table-container .row > * {\r\n  overflow: hidden;\r\n  text-overflow: ellipsis;\r\n  white-space: nowrap;\r\n}\r\n.table-container.nfts .row,\r\n.table-container.nft-token .row {\r\n  &.table-body {\r\n    align-items: center;\r\n    padding-top: 8px;\r\n  }\r\n  img {\r\n    width: 48px;\r\n    height: 48px;\r\n    border-radius: var(--br);\r\n  }\r\n}\r\n.table-container.nft .row {\r\n  grid-template-columns: 3fr 1fr 2fr 3fr 3fr;\r\n}\r\n.table-title {\r\n  display: flex;\r\n  justify-content: space-between;\r\n\r\n  > :first-child {\r\n    font-size: var(--t-text);\r\n    color: var(--b1);\r\n    font-weight: var(--bold);\r\n  }\r\n  input { display: none; }\r\n  > :last-child {\r\n    color: var(--w0);\r\n    background-color: var(--primary);\r\n    border-radius: var(--br);\r\n    padding: 4px 16px;\r\n  }\r\n}\r\n\r\n.page-title {\r\n  display: block;\r\n  font-size: var(--t-text);\r\n  font-weight: var(--bold);\r\n}\r\n\r\n.detail .row {\r\n  grid-template-columns: repeat(12, 1fr);\r\n  border: none;\r\n  > :first-child {\r\n    font-weight: var(--bold);\r\n    grid-column: span 4;\r\n  }\r\n  :last-child {\r\n    grid-column: span 8;\r\n  }\r\n  &:last-child {\r\n    padding-bottom: 0;\r\n  }\r\n}\r\n.detail .row.tri {\r\n  :first-child {\r\n    font-weight: var(--bold);\r\n    grid-column: span 2;\r\n  }\r\n  :nth-child(2) {\r\n    grid-column: span 5;\r\n  }\r\n  :last-child {\r\n    grid-column: span 5;\r\n  }\r\n}\r\n\r\n.inner {\r\n  display: flex;\r\n  flex-flow: column;\r\n  gap: var(--pad);\r\n}\r\n\r\n.nft-detail > img {\r\n  border: 1px solid var(--lightest-pri);\r\n  border-radius: var(--br);\r\n  grid-column: span 4;\r\n  width: 100%;\r\n}\r\n.nft-detail > .nft-title {\r\n  grid-column: span 8;\r\n}\r\n.nft-detail > .table-container {\r\n  grid-column: span 8;\r\n}\r\n.table-search{\r\n  display: flex;\r\n  gap: var(--br);\r\n  width: 100%;\r\n  padding-top: var(--pad);\r\n  align-items: center;\r\n  justify-content: center;\r\n  font-family: JSDongkang,Roboto,sans-serif;\r\n}\r\n.table-search a, .table-search p {\r\n  border-radius: var(--br);\r\n  padding: 5px 16px;\r\n  background-color: var(--lightest-pri);\r\n  color: var(--darken-pri);\r\n  &.dis {\r\n    pointer-events: none;\r\n    cursor: default;\r\n    color: var(--g);\r\n  }\r\n}\r\n.table-search p {\r\n  cursor: pointer;\r\n}\r\n\r\n.type-search {\r\n  border-radius: 5px;\r\n  background-position: 50% 50%;\r\n  padding: 3px 16px;\r\n  width: 100px;\r\n  background-color: var(--lightest-pri);\r\n}\r\n\r\n.blc-num, .tx-hash, .blc-hash, .acc-hash, .token-id {\r\n  color: var(--darken-pri);\r\n  cursor: pointer;\r\n}\r\n\r\n.search-container {\r\n  display: grid;\r\n  height: 3em;\r\n  grid-template-columns: repeat(12, 1fr);\r\n}\r\n.search-container > * {\r\n  font-size: 16px;\r\n}\r\n.search-container > :first-child {\r\n  padding-left: var(--pad);\r\n  grid-column: span 10;\r\n  border-radius: var(--br) 0 0 var(--br);\r\n  ::placeholder {\r\n    color: var(--lightest-pri)\r\n  }\r\n}\r\n.search-container > :last-child {\r\n  display: flex;\r\n  align-items: center;\r\n  justify-content: center;\r\n  cursor: pointer;\r\n  border: 1px solid var(--darken-pri);\r\n  border-radius: 0 var(--br) var(--br) 0;\r\n  background-color: var(--darken-pri);\r\n  color: var(--w0);\r\n  grid-column: span 2;\r\n}\r\n\r\n.err-wrap {\r\n  display: flex;\r\n  flex-direction: column;\r\n  align-items: center;\r\n  justify-content: center;\r\n  flex-grow: 1;\r\n  * {\r\n    color: var(--b1);\r\n  }\r\n  span {\r\n    color: var(--darken-pri);\r\n    cursor: pointer;\r\n  }\r\n  > :first-child {\r\n    font-size: 32px;\r\n  }\r\n  > :nth-child(2) {\r\n    font-size: var(--t-text);\r\n  }\r\n}\r\n\r\ninput {\r\n  border: 1px solid var(--lightest-pri);\r\n  &:focus {\r\n    outline: none;\r\n    border-color: var(--primary);\r\n    color: var(--primary);\r\n  }\r\n}\r\n"
  },
  {
    "path": "modules/lmscan-frontend/assets/css/tooltip.css",
    "content": ".row .cell[data-tooltip-text]:hover,\n[data-tooltip-text]:hover {\n\tposition: relative;\n\toverflow: visible;\n}\n\n[data-tooltip-text]:after {\n\t-webkit-transition: bottom .3s ease-in-out, opacity .3s ease-in-out;\n\t-moz-transition: bottom .3s ease-in-out, opacity .3s ease-in-out;\n\ttransition: bottom .3s ease-in-out, opacity .3s ease-in-out;\n\n\tbackground-color: rgba(0, 0, 0, 0.8);\n\n    -webkit-box-shadow: 0px 0px 3px 1px rgba(50, 50, 50, 0.4);\n\t-moz-box-shadow: 0px 0px 3px 1px rgba(50, 50, 50, 0.4);\n\tbox-shadow: 0px 0px 3px 1px rgba(50, 50, 50, 0.4);\n\t\n    -webkit-border-radius: 5px;\n\t-moz-border-radius: 5px;\n\tborder-radius: 5px;\n\t\n    color: #FFFFFF;\n\tfont-size: 14px;\n\tmargin-bottom: 10px;\n\tpadding: 7px 12px;\n\tposition: absolute;\n\twidth: auto;\n\t/* min-width: 50px;\n\tmax-width: 300px; */\n    white-space: nowrap;\n\tword-wrap: break-word;\n\n\tz-index: 9999;\n\n\topacity: 0;\n\tleft: -9999px;\n    top: 90%;\n\t\n\tcontent: attr(data-tooltip-text);\n}\n\n[data-tooltip-text]:hover:after {\n\ttop: 130%;\n\tleft: 0;\n\topacity: 1;\n}\n"
  },
  {
    "path": "modules/lmscan-frontend/assets/index.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"UTF-8\" />\n    <title>LeisureMeta Chain Block Explorer</title>\n    <link rel=\"stylesheet\" type=\"text/css\" href=\"css/reset.css\">\n    <link rel=\"stylesheet\" type=\"text/css\" href=\"css/style.css\" media=\"all\"/>\n    <link rel=\"stylesheet\" type=\"text/css\" href=\"css/desktop.css\" media=\"screen and (min-width: 801px)\"/>\n    <link href=\"css/mobile.css\" rel=\"stylesheet\" type=\"text/css\" media=\"screen and (max-width: 800px)\" />\n    <link rel=\"shortcut icon\" href=\"img/favicon.ico\">\n    <link rel=\"stylesheet\" href=\"https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200\">\n    <link rel=\"stylesheet\" href=\"https://fonts.googleapis.com/css?family=Readex Pro\">\n    <link rel=\"stylesheet\" href='https://fonts.googleapis.com/css?family=Roboto'  type='text/css'>\n  </head>\n\n  <body>\n    <div id=\"app-container\"></div>\n    <script type=\"module\" src=\"./load-main.js\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "modules/lmscan-frontend/assets/load-main.js",
    "content": "(async () => {\n  let m\n  if (process.env.NODE_ENV === \"production\") {\n    m = await import(\"../target/scala-3.4.1/leisuremeta-chain-lmscan-frontend-opt/main.js\")\n  } else {\n    m = await import(\"../target/scala-3.4.1/leisuremeta-chain-lmscan-frontend-fastopt/main.js\")\n  }\n  m.LmScan.launch(\"app-container\")\n})()\n"
  },
  {
    "path": "modules/lmscan-frontend/package.json",
    "content": "{\n  \"name\": \"lmscan\",\n  \"source\": \"assets/index.html\",\n  \"browserslist\": \"> 0.5%, last 2 versions, not dead\",\n  \"scripts\": {\n    \"start\": \"parcel\",\n    \"build\": \"parcel build\"\n  },\n  \"devDependencies\": {\n    \"@parcel/compressor-brotli\": \"^2.12.0\",\n    \"@parcel/compressor-gzip\": \"^2.12.0\",\n    \"@parcel/reporter-bundle-analyzer\": \"^2.12.0\",\n    \"parcel\": \"^2.12.0\",\n    \"process\": \"^0.11.10\",\n    \"typescript\": \"^4.9.5\"\n  }\n}\n"
  },
  {
    "path": "modules/lmscan-frontend/project/build.properties",
    "content": "sbt.version=1.8.0\n"
  },
  {
    "path": "modules/lmscan-frontend/readme.md",
    "content": "#### To start\n\n```md\n# sbt 실행\n\ncmd) sbt\nsbt) project lmscanFrontend\nsbt:leisuremeta-chain-lmscan-fronte) ~fastLinkJS\n\n# yarn 실행\n\ncmd) cd modules/lmscan-frontend\ncmd) yarn start\n```\n\n# 배포 방법\n\n```md\n# 루트폴더\n\n- fullLinkJS\n\n# lmscan-frontend 폴더\n\n- yarn build\n```\n"
  },
  {
    "path": "modules/lmscan-frontend/src/main/scala/io/leisuremeta/chain/lmscan/frontend/LmscanFrontendApp.scala",
    "content": "package io.leisuremeta.chain.lmscan.frontend\n\nimport cats.effect.IO\nimport tyrian.*\nimport scala.scalajs.js.annotation.*\nimport io.leisuremeta.chain.lmscan.common.model._\nimport concurrent.duration.DurationInt\n\n@JSExportTopLevel(\"LmScan\")\nobject LmscanFrontendApp extends TyrianIOApp[Msg, Model]:\n  def init(flags: Map[String, String]): (Model, Cmd[IO, Msg]) = (BaseModel(), Cmd.None)\n\n  def update(model: Model): Msg => (Model, Cmd[IO, Msg]) = model.update\n  def view(model: Model): Html[Msg] = model.view\n\n  def subscriptions(model: Model): Sub[IO, Msg] =\n    SearchView.detectSearch ++ Pagination.detectSearch\n    ++ Sub.Batch(\n      Sub.every[IO](1.second, \"clock-ticks\").map(UpdateTime.apply),\n      Sub.every[IO](60.second, \"refresh-ticks\").map(_ => RefreshData),\n    )\n\n  def router: Location => Msg =\n    case loc: Location.Internal =>\n      loc.pathName match\n        case s\"/blcs/$page\" => ToPage(BlcModel(page = page.toInt))\n        case s\"/txs/$page\" => ToPage(TxModel(page = page.toInt))\n        case s\"/nfts/$page\" => ToPage(NftModel(page = page.toInt))\n        case s\"/accs/$page\" => ToPage(AccModel(page = page.toInt))\n        case s\"/tx/$hash\" => ToPage(TxDetailModel(txDetail = TxDetail(hash = Some(hash))))\n        case s\"/nft/$id/$page\" => ToPage(NftTokenModel(page = page.toInt, id = id))\n        case s\"/nft/$id\" => ToPage(NftDetailModel(nftDetail = NftDetail(nftFile = Some(NftFileModel(tokenId = Some(id))))))\n        case s\"/blc/$hash\" => \n          val p = loc.search.getOrElse(\"\").split(\"=\").last\n          ToPage(BlcDetailModel(blcDetail = BlockDetail(hash = Some(hash)), page = p.toInt))\n        case s\"/acc/$address\" => \n          val p = loc.search.getOrElse(\"\").split(\"=\").last\n          ToPage(AccDetailModel(accDetail = AccountDetail(address = Some(address)), page = p.toInt))\n        case s\"/vds\" => ToPage(VdModel())\n        case s\"/vds/$address\" => \n          val p = loc.search.getOrElse(\"\").split(\"=\").last\n          ToPage(VdDetailModel(address = address, page = p.toInt))\n        case \"/\" => ToPage(BaseModel())\n        case \"\" => ToPage(BaseModel())\n        case _   => ToPage(ErrorModel(error = \"\"))\n    case loc: Location.External =>\n      NavigateToUrl(loc.href)\n"
  },
  {
    "path": "modules/lmscan-frontend/src/main/scala/io/leisuremeta/chain/lmscan/frontend/components/BoardView.scala",
    "content": "package io.leisuremeta.chain.lmscan.frontend\n\nimport tyrian.Html._\nimport tyrian.SVG._\nimport tyrian.*\nimport io.leisuremeta.chain.lmscan.common.model._\n\nobject BoardView:\n  def f(v: Long) = f\"$v%,d\" \n  def f(v: BigDecimal) = f\"$v%,.0f\" \n  val x = 400\n  val y = 100\n\n  def pricePan(summary: SummaryBoard, v: SummaryChart) = \n    div(cls := \"board-comp chart\")(\n      span(\"Price\"),\n      summary.today.lmPrice match\n        case None => LoaderView.view\n        case Some(v) => span(\"$\" + v.toString.take(6))\n      , \n      (summary.today.lmPrice, summary.yesterday.lmPrice) match\n        case (Some(v), Some(w)) => \n          val diff = (v - w) / w * 100\n          div(\n            span(\"24H\"),\n            span(cls := s\"${if diff >= 0 then \"pos\" else \"neg\"}\")(f\"$diff%3.2f %%\")\n          )\n        case (_, _) => div()\n      ,\n      makeChart(v.list.map(vv => vv.lmPrice.getOrElse(0.0)).reverse.take(144).toList, x, y),\n    )\n\n  def txPan(summary: SummaryBoard, v: SummaryChart) =\n    div(cls := \"board-comp chart\")(\n      span(\"Total Transaction\"),\n      span(f(summary.today.totalTxSize.getOrElse(0L))),\n      drawDiff(summary.today.totalTxSize, summary.yesterday.totalTxSize, v => v.toString),\n      makeChart(v.list.map(vv => vv.totalTxSize.map(_.toDouble).getOrElse(0.0)).reverse.toList, x, y),\n    )\n\n  def balPan(summary: SummaryBoard, v: SummaryChart) =\n    def f(v: BigDecimal) = f\"${v / Math.pow(10, 18)}%,.0f\"\n    div(cls := \"board-comp\")(\n      span(\"Total Balance in LMC\"),\n      span(f(summary.today.totalBalance.getOrElse(BigDecimal(0)))) ,\n      drawDiff(summary.today.totalBalance, summary.yesterday.totalBalance, f),\n      makeChart(v.list.map(vv => vv.totalBalance.getOrElse(BigDecimal(0)).toDouble).reverse.take(144).toList, x, y),\n    )\n  \n  def drawDiff[T](oT: Option[T], oY: Option[T], f: T => String = (v: T) => v.toString)(using nu: Numeric[T]) = (oT, oY) match \n    case (Some(t), Some(y)) => \n      val diff = nu.minus(t, y)\n      div(\n        span(\"24H\"),\n        span(cls := s\"${if nu.gteq(t, y) then \"pos\" else \"neg\"}\")(f(diff))\n      )\n    case (Some(v), _) => div(span(\"24H\"), span(f(v)))\n    case (_, Some(v)) => div(span(\"24H\"), span(f(v)))\n    case _ => div(span(\"24H\"))\n\n  def accPan(summary: SummaryBoard, v: SummaryChart) =\n    div(cls := \"board-comp\")(\n      span(\"Total Accounts\"),\n      span(f(summary.today.totalAccounts.getOrElse(0L))),\n      drawDiff(summary.today.totalAccounts, summary.yesterday.totalAccounts),\n      makeChart(v.list.map(vv => vv.totalAccounts.getOrElse(0L).toDouble).reverse.take(144).toList, x, y),\n    )\n\n  def normalize(xs: List[Double]): List[Double] =\n    if xs.isEmpty then return xs\n    val max = xs.max\n    val min = xs.min\n    val d = max - min\n    if d == 0 then xs.map(_ => 0.5)\n    else xs.map(x => (x - min) / d)\n\n  def makeLine(ys: List[Double], x: Long, h: Long): String =\n    val dx = x / (ys.size - 1).toDouble\n    normalize(ys).zipWithIndex\n    .map:\n      case (y, i) if i == 0 => s\"M0,${h - y * h} L${i * dx},${h - y * h}\"\n      case (y, i) => s\"L${i * dx},${h - y * h}\"\n    .mkString(\"\")\n\n  def makeArea(ys: List[Double], x: Long, h: Long): String =\n    val dx = x / (ys.size - 1).toDouble\n    normalize(ys).zipWithIndex\n    .map:\n      case (y, i) if i == 0 => s\"0,${h} 0,${h - y * h} ${i * dx},${h - y * h}\"\n      case (y, i) => s\" ${i * dx},${h - y * h}\"\n    .mkString(\"\", \"\", s\" ${x},${h}\")\n\n  def makeChart(ys: List[Double], x: Long, h: Long): Html[Msg] =\n    svg(viewBox := s\"0, 0, ${x}, ${h}\")(\n      path(d := makeLine(ys, x, h)),\n      polyline(points := makeArea(ys, x, h))\n    )\n\n  def marketCap(summary: SummaryBoard, v: SummaryChart) =\n    div(cls := \"board-comp\")(\n      span(\"Market cap\"),\n      span(\"$\" + f(summary.today.marketCap.getOrElse(BigDecimal(0)))),\n      drawDiff(summary.today.marketCap, summary.yesterday.marketCap, f),\n      makeChart(v.list.map(vv => vv.marketCap.getOrElse(BigDecimal(0)).toDouble).reverse.take(144).toList, x, y),\n    )\n  def cirSupply(summary: SummaryBoard, v: SummaryChart) =\n    def f(v: BigDecimal) = f\"$v%,.0f\"\n    div(cls := \"board-comp\")(\n      span(\"Circulation Supply\"),\n      span(f(summary.today.cirSupply.getOrElse(BigDecimal(0))) + \" LM\"), \n      drawDiff(summary.today.cirSupply, summary.yesterday.cirSupply, f),\n      makeChart(v.list.map(vv => vv.cirSupply.getOrElse(BigDecimal(0)).toDouble).reverse.take(144).toList, x, y),\n    )\n  def totalBlock(summary: SummaryBoard, v: SummaryChart) =\n    div(cls := \"board-comp\")(\n      span(\"Total Blocks\"),\n      span(f(summary.today.blockNumber.getOrElse(0L))),\n      drawDiff(summary.today.blockNumber, summary.yesterday.blockNumber, f),\n      makeChart(v.list.map(vv => vv.blockNumber.getOrElse(0L).toDouble).reverse.take(144).toList, x, y),\n    )\n  def totalNft(summary: SummaryBoard, v: SummaryChart) =\n    div(cls := \"board-comp\")(\n      span(\"Total NFTs\"),\n      span(f(summary.today.totalNft.getOrElse(0L))), \n      drawDiff(summary.today.totalNft, summary.yesterday.totalNft, f),\n      makeChart(v.list.map(vv => vv.totalNft.getOrElse(0L).toDouble).reverse.take(144).toList, x, y),\n    )\n\n  def view(model: BaseModel): Html[Msg] =\n    (model.summary, model.chartData) match\n      case (Some(summary), Some(v)) =>\n        div(cls := \"board-area\")(\n          pricePan(summary, v),\n          marketCap(summary, v),\n          cirSupply(summary, v),\n          balPan(summary, v), \n          txPan(summary, v),\n          accPan(summary, v), \n          totalBlock(summary, v),\n          totalNft(summary, v),\n        )\n      case _ => div(cls := \"board-area\")(LoaderView.view)\n"
  },
  {
    "path": "modules/lmscan-frontend/src/main/scala/io/leisuremeta/chain/lmscan/frontend/components/Footer.scala",
    "content": "package io.leisuremeta.chain.lmscan.frontend\nimport tyrian.Html.*\nimport tyrian.*\n\nobject Footer:\n  def view(): Html[Msg] =\n    div(\n      div(cls := \"footer-left\")(\n        p(\n          strong(\"LMSCAN\"),\n          span(\"for LeisureMeta Chain\"),\n        ),\n        strong(\"Copyright © LM LLC All Rights Reserved\"),\n      ),\n      div(cls := \"footer-right\")(\n        dl(\n          dt(\"Company Family\"),\n          dd(\n            a(\n              href   := \"https://themoonlabs.net/\",\n              target := \"_blank\",\n              cls    := \"icon-The-Moon-Labs\",\n            )(\"The Moon Labs\"),\n            a(\n              href   := \"https://leisuremeta.io/\",\n              target := \"_blank\",\n              cls    := \"icon-leisuremetaverse\",\n            )(\"LeisureMetaverse\"),\n            a(\n              href   := \"https://www.ilikelm.com/\",\n              target := \"_blank\",\n              cls    := \"icon-lm-nova\",\n            )(\"ILIKELM\"),\n            a(\n              href   := \"https://www.themoonent.com/\",\n              target := \"_blank\",\n              cls    := \"icon-tme\",\n            )(\"THE MOON ENTERTAINMENT\"),\n          ),\n        ),\n        dl(\n          dt(\"Social\"),\n          dd(\n            a(\n              href   := \"https://github.com/leisuremeta/leisuremeta-chain\",\n              target := \"_blank\",\n              cls    := \"icon-github\",\n            )(\"Github\"),\n            a(\n              href   := \"https://t.me/LeisureMeta_Official\",\n              target := \"_blank\",\n              cls    := \"icon-telegram\",\n            )(\"Telegram\"),\n            a(\n              href   := \"https://twitter.com/LeisureMeta_LM\",\n              target := \"_blank\",\n              cls    := \"icon-twitter\",\n            )(\"Twitter\"),\n            a(\n              href   := \"https://discord.gg/leisuremetaofficial\",\n              target := \"_blank\",\n              cls    := \"icon-discord\",\n            )(\"Discord\"),\n          ),\n        ),\n      ),\n    )\n"
  },
  {
    "path": "modules/lmscan-frontend/src/main/scala/io/leisuremeta/chain/lmscan/frontend/components/Loader.scala",
    "content": "package io.leisuremeta.chain.lmscan.frontend\n\nimport tyrian.Html.*\nimport tyrian.*\n\nobject LoaderView:\n  def view: Html[Msg] =\n    div(cls := \"loader-case\")(\n      div(cls := \"loader\")(),\n    )\n"
  },
  {
    "path": "modules/lmscan-frontend/src/main/scala/io/leisuremeta/chain/lmscan/frontend/components/NavBar.scala",
    "content": "package io.leisuremeta.chain.lmscan.frontend\n\nimport tyrian.Html.*\nimport tyrian.*\n\nobject NavBar:\n  val main = \"Dashboard\"\n  val blc = \"Blocks\"\n  val tx = \"Transactions\"\n  val acc = \"Accounts\"\n  val nft = \"NFTs\"\n  val vd = \"Validators\"\n  def view(model: Model): Html[Msg] =\n    nav()(\n      p(onClick(ToPage(BaseModel())))(\"LM SCAN\")\n      ::\n      List(\n        (main, ToPage(BaseModel())),\n        (blc, ToPage(BlcModel(page = 1))),\n        (tx, ToPage(TxModel(page = 1))),\n        (acc, ToPage(AccModel(page = 1))),\n        (nft, ToPage(NftModel(page = 1))),\n        (vd, ToPage(VdModel())),\n      ).map((name, msg) =>\n        a(\n          cls := isActive(name, model),\n          onClick(msg),\n        )(span(name))\n      ),\n    )\n\n  def isActive(name: String, model: Model) = model match\n    case m: BaseModel => if name == main then \"active\" else \"\"\n    case _: BlcModel if name == blc => \"active\"\n    case _: BlcDetailModel if name == blc => \"active\"\n    case _: TxModel if name == tx => \"active\"\n    case _: TxDetailModel if name == tx => \"active\"\n    case _: AccModel if name == acc => \"active\"\n    case _: AccDetailModel if name == acc => \"active\"\n    case _: NftModel if name == nft => \"active\"\n    case _: NftTokenModel if name == nft => \"active\"\n    case _: NftDetailModel if name == nft => \"active\"\n    case _: VdModel if name == vd => \"active\"\n    case _ => \"\"\n    \n"
  },
  {
    "path": "modules/lmscan-frontend/src/main/scala/io/leisuremeta/chain/lmscan/frontend/components/SearchView.scala",
    "content": "package io.leisuremeta.chain.lmscan.frontend\n\nimport tyrian.Html.*\nimport tyrian.*\nimport org.scalajs.dom._\nimport cats.effect.IO\n\nobject SearchView:\n  def view(model: Model): Html[Msg] =\n    div(cls := \"search-area\")(\n      div(cls := \"search-container\")(\n        input(\n          id := \"global-search\",\n          onInput(s => GlobalInput(s)),\n          value   := s\"${model.global.searchValue}\",\n          cls := \"search-text\",\n          `placeholder` := (\n            \"Search by address, transaction, NFT, block\",\n          ),\n        ),\n        div(\n          onClick(GlobalSearch),\n          cls := \"search-icon\",\n        )(\n          \"Search >>\"\n        ),\n      ),\n    )\n  \n  def detectSearch = \n    Sub.Batch(\n      Option(document.getElementById(\"global-search\")) match\n        case None => Sub.None\n        case Some(el) =>\n          Sub.fromEvent[IO, KeyboardEvent, Msg](\"keyup\", el): e =>\n            e.key match\n              case \"Enter\" => Some(GlobalSearch)\n              case _ => None\n      ,\n    )\n"
  },
  {
    "path": "modules/lmscan-frontend/src/main/scala/io/leisuremeta/chain/lmscan/frontend/components/common/Body.scala",
    "content": "package io.leisuremeta.chain.lmscan.frontend\n\nimport tyrian.Html.*\nimport tyrian.*\nimport io.leisuremeta.chain.lmscan.common.model._\n\nobject Body:\n  def blocks(payload: List[BlockInfo], g: GlobalModel) =\n    payload.map(v =>\n      div(cls := \"row table-body\")(\n        gen.cell(\n          Cell.BLOCK_NUMBER(v.hash, v.number),\n          Cell.AGE(v.createdAt, g.current),\n          Cell.BLOCK_HASH(v.hash),\n          Cell.PlainLong(v.txCount),\n        ),\n      ),\n    )\n  def accs(payload: List[AccountInfo]) =\n    payload.map(v =>\n      div(cls := \"row table-body\")(\n        gen.cell(\n          Cell.ACCOUNT_HASH(v.address),\n          Cell.Balance(v.balance),\n          Cell.PriceS(v.value),\n          Cell.DateS(v.updated),\n        ),\n      ),\n    )\n  def txRow = (payload: List[TxInfo], g: GlobalModel) =>\n    payload.map(v =>\n      div(cls := \"row table-body\")(\n        gen.cell(\n          Cell.TX_HASH(v.hash),\n          Cell.PlainLong(v.blockNumber),\n          Cell.AGE(v.createdAt, g.current),\n          Cell.ACCOUNT_HASH(v.signer),\n          Cell.PlainStr(v.subType),\n        ),\n      ),\n    )\n  def boardTxRow = (payload: List[TxInfo], g: GlobalModel) =>\n    payload\n      .map(v =>\n        div(cls := \"row table-body\")(\n          gen.cell(\n            Cell.TX_HASH(v.hash),\n            Cell.AGE(v.createdAt, g.current),\n            Cell.ACCOUNT_HASH(v.signer),\n          ),\n        ),\n      )\n\n  def nfts = (payload: List[NftInfoModel]) =>\n    payload\n      .map(v =>\n        div(cls := \"row table-body\")(\n          gen.cell(\n            Cell.ImageS(v.thumbUrl),\n            Cell.NftToken(v),\n            Cell.PlainStr(v.totalSupply),\n            Cell.DateS(v.startDate),\n            Cell.DateS(v.endDate),\n          ),\n        ),\n      )\n\n\n  def nftToken = (payload: List[NftSeasonModel]) =>\n    payload\n      .map(v =>\n        div(cls := \"row table-body\")(\n          gen.cell(\n            Cell.NftDetail(v, v.nftName),\n            Cell.PlainStr(v.collection),\n            Cell.NftDetail(v, v.tokenId),\n            Cell.PlainStr(v.creator),\n            Cell.PlainStr(v.rarity),\n          ),\n        ),\n      )\n\n  def nft = (payload: List[NftActivity], g: GlobalModel) =>\n    payload\n      .map(v =>\n        div(cls := \"row table-body\")(\n          gen.cell(\n            Cell.TX_HASH(v.txHash),\n            Cell.AGE(v.createdAt, g.current),\n            Cell.PlainStr(v.action),\n            Cell.ACCOUNT_HASH(v.fromAddr),\n            Cell.ACCOUNT_HASH(v.toAddr),\n          ),\n        ),\n      )\n\n\n  def vds = (payload: List[NodeValidator.Validator]) =>\n    payload\n      .map(v =>\n        div(cls := \"row table-body\")(\n          span(\n            cls := s\"cell acc-hash\",\n            onClick(ToPage(VdDetailModel(address = v.address.getOrElse(\"\")))),\n          )(\n            v.address.getOrElse(\"\"),\n          ),\n          span(v.power.getOrElse(0.0).toString + \"%\"),\n          span(f\"${v.cnt.getOrElse(0L)}%,d\"),\n        ),\n      )\n"
  },
  {
    "path": "modules/lmscan-frontend/src/main/scala/io/leisuremeta/chain/lmscan/frontend/components/common/Head.scala",
    "content": "package io.leisuremeta.chain.lmscan.frontend\n\nimport tyrian.Html.*\n\nobject Head:\n  def template(hs: List[String], cs: String = \"\") = div(cls := s\"row table-head $cs\")(hs.map(span(_)))\n  val block = template(List(\"Block\", \"Age\", \"Block hash\", \"TxN\")) \n  val accs = template(List(\"Address\", \"Balance\", \"Value\", \"Last Seen\")) \n  val nfts = template(List(\"\", \"Season\", \"Total Supply\", \"Sale Started\", \"Sale Ended\")) \n  val nftToken = template(List(\"NFT\", \"Collection\", \"Token ID\", \"Creator\", \"Rarity\"))\n  val nft = template(List(\"Tx Hash\", \"Timestamp\", \"Action\", \"From\", \"To\"))\n  val tx = template(List(\"Tx Hash\", \"Block\", \"Age\", \"Signer\", \"Subtype\"))\n  val tx2 = template(List(\"Tx Hash\", \"Block\", \"Age\", \"Signer\", \"Subtype\", \"Value\"))\n  val tx_dashBoard = template(List(\"Tx Hash\", \"Age\", \"Signer\"))\n  val vds = template(List(\"Address\", \"Voting Power\", \"Total Block Proposed\"))\n"
  },
  {
    "path": "modules/lmscan-frontend/src/main/scala/io/leisuremeta/chain/lmscan/frontend/components/common/Pagination.scala",
    "content": "package io.leisuremeta.chain.lmscan.frontend\n\nimport tyrian.Html.*\nimport tyrian.*\nimport io.leisuremeta.chain.lmscan.common.model.*\nimport org.scalajs.dom._\nimport cats.effect.IO\n\nobject Pagination:\n  def toInt(s: String) =\n    try Some(Integer.parseInt(s))\n    catch case _ => None\n  def checkAndMake(s: String, last: Int, cur: Int) =\n    val v = s.toIntOption.getOrElse(cur)\n    if v < 1 then 1\n    else if v > last then last\n    else v\n  def view[T](model: PageModel) =\n    val curPage = model.page\n    val totalPage = model.data match\n      case None    => 0\n      case Some(v) => v match\n        case PageResponse(totalCount, totalPages, payload) => totalPages.toInt\n\n    def goTo(v: Int) = model match\n      case _: BlcModel => ToPage(BlcModel(page = v))\n      case _: TxModel => ToPage(TxModel(page = v))\n      case _: AccModel => ToPage(AccModel(page = v))\n      case _: NftModel => ToPage(NftModel(page = v))\n      case n: BlcDetailModel=> ToPage(n.copy(page = v))\n      case n: AccDetailModel=> ToPage(n.copy(page = v))\n      case n: NftTokenModel => ToPage(n.copy(page = v))\n      case n: VdDetailModel => ToPage(n.copy(page = v))\n    def isDis(condition: Boolean) = if condition then \"dis\" else \"\"\n    def toggleInput = TogglePageInput(!model.pageToggle)\n\n    div(cls := s\"table-search\")(\n      a(\n        cls := s\"${isDis(1 == curPage)}\",\n        onClick(goTo(1)),\n      )(\"First\"),\n      a(\n        cls := s\"${isDis(curPage <= 1)}\",\n        onClick(goTo(curPage - 1)),\n      )(\"<\"),\n      model.pageToggle match\n        case true => \n          input(\n            id := \"list-search\",\n            onInput(s => UpdateSearch(checkAndMake(s, totalPage, curPage))),\n            value := s\"${curPage}\",\n            cls := \"type-search\",\n          )\n        case false => p(onClick(toggleInput))(s\"${curPage} of ${totalPage}\")\n      ,\n      a(\n        cls := s\"${isDis(curPage >= totalPage)}\",\n        onClick(goTo(curPage + 1)),\n      )(\">\"),\n      a(\n        cls := s\"${isDis(curPage == totalPage)}\",\n        onClick(goTo(totalPage)),\n      )(\"Last\"),\n    )\n  \n  def detectSearch = \n    Sub.Batch(\n      Option(document.getElementById(\"list-search\")) match\n        case None => Sub.None\n        case Some(el) =>\n          Sub.fromEvent[IO, KeyboardEvent, Msg](\"keyup\", el): e =>\n            e.key match\n              case \"Enter\" => Some(ListSearch)\n              case _ => None\n      ,\n    )\n"
  },
  {
    "path": "modules/lmscan-frontend/src/main/scala/io/leisuremeta/chain/lmscan/frontend/components/common/Table.scala",
    "content": "package io.leisuremeta.chain.lmscan\npackage frontend\n\nimport tyrian.Html.*\nimport tyrian.*\nimport common.model.*\n\nobject Table:\n  def mainView(model: BaseModel): Html[Msg] =\n    div(cls := \"table-area main-table\")(\n      div(\n        cls := \"blc table-container\",\n      )(\n        div(cls := \"table-title\")(\n          label(text(\"Latest Blocks\"), input(name := \"toggle-main\", typ := \"radio\", checked)),\n          a(onClick(ToPage(BlcModel(page = 1))))(\"More\"),\n        ) ::\n        (model.blcs match\n          case None => List(LoaderView.view)\n          case Some(v) => Head.block :: Body.blocks(v.payload.toList, model.global))\n      ),\n      div(\n        cls := \"tx-m table-container\",\n      )(\n        div(cls := \"table-title\")(\n          label(text(\"Latest transactions\"), input(name := \"toggle-main\", typ := \"radio\")),\n          a(onClick(ToPage(TxModel(page = 1))))(\"More\"),\n        ) ::\n        (model.txs match\n          case None => List(LoaderView.view)\n          case Some(v) => Head.tx_dashBoard :: Body.boardTxRow(v.payload.toList, model.global))\n      ),\n    )\n\n  def view(model: BlcModel) =\n    div(cls := \"table-container blc\")(\n      Head.block ::\n      (model.data match\n        case Some(v) =>  Body.blocks(v.payload.toList, model.global).appended(Pagination.view(model))\n        case None    => List(LoaderView.view)),\n    )\n  def view(model: AccModel) =\n    div(cls := \"table-container accs\")(\n      model.data match\n        case None    => List(LoaderView.view)\n        case Some(data) => \n          Head.accs :: Body.accs(data.payload.toList).appended(Pagination.view(model))\n    )\n  def view(model: NftModel) =\n    div(cls := \"table-container nfts\")(\n      nft(model.data),\n      Pagination.view(model),\n      model.data match\n        case None    => LoaderView.view\n        case Some(_) => div(),\n    )\n  def view(model: NftTokenModel) =\n    div(cls := \"table-container nft-token\")(\n      nftToken(model.data),\n      Pagination.view(model),\n      model.data match\n        case None    => LoaderView.view\n        case Some(_) => div(),\n    )\n\n  def view(model: TxModel): Html[Msg] =\n    div(cls := \"table-container tx\")(\n      Head.tx :: \n      (model.data match\n        case None => List(LoaderView.view)\n        case Some(v) =>\n          Body.txRow(v.payload.toList, model.global).appended(Pagination.view(model))\n      ),\n    )\n  def view(model: BlcDetailModel): Html[Msg] =\n    div(cls := \"table-container tx\")(\n      Head.tx :: Body.txRow(model.blcDetail.payload.toList, model.global).appended(Pagination.view(model))\n    )\n  def view(model: AccDetailModel): Html[Msg] =\n    div(cls := \"table-container tx\")(\n      Head.tx :: Body.txRow(model.accDetail.payload.toList, model.global).appended(Pagination.view(model))\n    )\n  def view(model: NftDetailModel): Html[Msg] =\n    div(cls := \"table-container nft\")(\n      model.nftDetail.activities match\n        case None    => List(Head.nft, LoaderView.view)\n        case Some(v) => Head.nft :: Body.nft(v.toList, model.global),\n    )\n  def view(model: VdModel): Html[Msg] =\n    div(cls := \"table-container vds\")(\n      Head.vds :: Body.vds(model.payload)\n    )\n  def view(model: VdDetailModel): Html[Msg] =\n    div(cls := \"table-container blc\")(\n      Head.block ::\n      Body.blocks(model.blcs.payload.toList, model.global).appended(Pagination.view(model))\n    )\n\n  def nft(list: Option[PageResponse[NftInfoModel]]) =\n    div(\n      list match\n        case Some(v) => Head.nfts :: Body.nfts(v.payload.toList)\n        case None    => List(Head.nfts),\n    )\n  def nftToken(list: Option[PageResponse[NftSeasonModel]]) =\n    div(\n      list match\n        case Some(v) => Head.nftToken :: Body.nftToken(v.payload.toList)\n        case None    => List(Head.nftToken),\n    )\n"
  },
  {
    "path": "modules/lmscan-frontend/src/main/scala/io/leisuremeta/chain/lmscan/frontend/components/detail/AccountDetailTable.scala",
    "content": "package io.leisuremeta.chain.lmscan.frontend\n\nimport tyrian.Html.*\nimport tyrian.*\nimport io.leisuremeta.chain.lmscan.common.model.*\n\nobject AccountDetailTable:\n  def view(data: AccountDetail) =\n    div(\n      cls := \"detail table-container\",\n    )(\n      div(cls := \"row\")(\n        gen.cell(\n          Cell.Head(\"Account\", \"cell type-detail-head\"),\n          Cell.PlainStr(data.address, \"cell type-detail-body\"),\n        ),\n      ),\n      div(cls := \"row\")(\n        gen.cell(\n          Cell.Head(\"Balance\", \"cell type-detail-head\"),\n          Cell.Balance(\n            data.balance,\n            \"cell type-detail-body\",\n          ),\n        ),\n      ),\n      div(cls := \"row\")(\n        gen.cell(\n          Cell.Head(\"Value\", \"cell type-detail-head\"),\n          Cell.PriceS(\n            data.value,\n            \"cell type-detail-body\",\n          ),\n        ),\n      ),\n    )\n"
  },
  {
    "path": "modules/lmscan-frontend/src/main/scala/io/leisuremeta/chain/lmscan/frontend/components/detail/blockDetailTable.scala",
    "content": "package io.leisuremeta.chain.lmscan.frontend\n\nimport tyrian.Html.*\nimport tyrian.*\nimport io.leisuremeta.chain.lmscan.common.model.BlockDetail\nobject BlockDetailTable:\n  def view(data: BlockDetail) =\n    div(\n      cls := \"detail table-container\",\n    )(\n      div(cls := \"row\")(\n        gen.cell(\n          Cell.Head(\"Block Number\", \"cell type-detail-head\"),\n          Cell.PlainStr(data.number, \"cell type-detail-body\"),\n        ),\n      ),\n      div(cls := \"row\")(\n        gen.cell(\n          Cell.Head(\"Timestamp\", \"cell type-detail-head\"),\n          Cell.DATE(data.timestamp, \"cell type-detail-body\"),\n        ),\n      ),\n      div(cls := \"row\")(\n        gen.cell(\n          Cell.Head(\"Block hash\", \"cell type-detail-head\"),\n          Cell.PlainStr(data.hash, \"cell type-detail-body\"),\n        ),\n      ),\n      div(cls := \"row\")(\n        gen.cell(\n          Cell.Head(\"Parent hash\", \"cell type-detail-head\"),\n          Cell.BLOCK_HASH(data.parentHash),\n        ),\n      ),\n      div(cls := \"row\")(\n        gen.cell(\n          Cell.Head(\"Transaction count\", \"cell type-detail-head\"),\n          Cell.PlainStr(data.txCount, \"cell type-detail-body\"),\n        ),\n      ),\n    )\n"
  },
  {
    "path": "modules/lmscan-frontend/src/main/scala/io/leisuremeta/chain/lmscan/frontend/components/detail/nftDetailTable.scala",
    "content": "package io.leisuremeta.chain.lmscan.frontend\n\nimport tyrian.Html.*\nimport tyrian.*\nimport io.leisuremeta.chain.lmscan.common.model.*\n\nobject NftDetailTable:\n  def view(data: NftDetail) =\n    val nftFile = data.nftFile.getOrElse(NftFileModel())\n\n    div(cls := \"nft-detail\")(\n      gen.cell(Cell.Image(nftFile.nftUri))(0),\n      div(cls := \"detail table-container\")(\n        div(cls := \"row\")(\n          span(\"NFT Name\"),\n          span(nftFile.nftName.getOrElse(\"-\")),\n        ),\n        div(cls := \"row\")(\n          span(\"Collection Name\"),\n          span(nftFile.collectionName.getOrElse(\"-\")),\n        ),\n        div(cls := \"row\")(\n          span(\"Token ID\"),\n          span(nftFile.tokenId.getOrElse(\"-\")),\n        ),\n        div(cls := \"row\")(\n          span(\"Definition ID\"),\n          span(nftFile.tokenDefId.getOrElse(\"-\")),\n        ),\n        div(cls := \"row\")(\n          span(\"Rarity\"),\n          span(nftFile.rarity.getOrElse(\"-\")),\n        ),\n        div(cls := \"row\")(\n          span(\"Creator\"),\n          span(nftFile.creator.getOrElse(\"-\")),\n        ),\n        div(cls := \"row\")(\n          span(\"Owner\"),\n          ParseHtml.fromAccHash(nftFile.owner),\n        ),\n        div(cls := \"row\")(\n          span(\"Issue Date\"),\n          ParseHtml.fromDate(nftFile.createdAt),\n        ),\n      ),\n    )\n"
  },
  {
    "path": "modules/lmscan-frontend/src/main/scala/io/leisuremeta/chain/lmscan/frontend/components/detail/txDetailTableCommon.scala",
    "content": "package io.leisuremeta.chain\npackage lmscan.frontend\n\nimport tyrian.Html.*\nimport tyrian.*\nimport lib.datatype.BigNat\nimport api.model._\nimport api.model.token._\nimport api.model.Signed.TxHash\nimport api.model.Transaction._\nimport api.model.Transaction.AccountTx._\nimport api.model.Transaction.TokenTx._\nimport api.model.Transaction.GroupTx._\nimport api.model.Transaction.RewardTx._\nimport api.model.Transaction.AgendaTx._\nimport api.model.Transaction.CreatorDaoTx._\nimport api.model.Transaction.VotingTx._\n\nobject TxDetailTableCommon:\n  def row(head: String, value: String) = div(cls := \"row\")(span(head), span(value))\n\n  def rowCustom(head: String, custom: Html[Msg]) = \n    div(cls := \"row\")(span(head), custom)\n\n  def rowInputHead = row(\"Input\", \"Transaction Hash\")\n\n  def rowTriOutput(s: String) = div(cls := \"row tri\")(span(\"Output\"), span(\"To\"), span(s))\n\n  def rowInputBody(input: TxHash, i: Int = 0) = \n    div(cls := \"row\")(span(s\"${i + 1}\"), ParseHtml.fromTxHash(input.toUInt256Bytes.toHex))\n\n  def rowAccToken(output: Account, v: TokenId) =\n    div(cls := \"row tri\")(span(\"1\"), ParseHtml.fromAccHash(Some(output.toString)), ParseHtml.fromTokenId(v.toString))\n\n  def rowAccBal(outputs: (Account, BigNat), i: Int) =\n    div(cls := \"row tri\")(\n      span(s\"${i + 1}\"),\n      ParseHtml.fromAccHash(Some(outputs._1.toString())),\n      ParseHtml.fromBal(Some(BigDecimal(outputs._2.toString))),\n    )\n  \n  def getNFromId(id: String) =\n    id.drop(16).dropWhile(c => !c.isDigit || c == '0').prepended('#')\n    \n  def view(data: Option[TransactionWithResult]) =\n    val result = for\n      tx <- data\n      res = tx.signedTx.value match\n        case t: Transaction.TokenTx => tokenView(t)\n        case t: Transaction.AccountTx => accountView(t)\n        case t: Transaction.GroupTx => groupView(t)\n        case t: Transaction.RewardTx => rewardView(t)\n        case t: Transaction.AgendaTx => agendaView(t, tx.result)\n        case t: Transaction.VotingTx => votingView(t)\n        case t: Transaction.CreatorDaoTx => creatorDaoView(t)\n    yield res\n    result.getOrElse(List(div(\"\"))).prepended(div(cls := \"page-title\")(\"Transaction Values\"))\n\n  def tokenView(tx: TokenTx) = tx match\n    case nft: DefineToken => \n      div(cls := \"detail table-container\")(\n        row(\"Definition ID\", nft.definitionId.toString),\n        row(\"Name\", nft.name.toString()),\n      ) :: List()\n    case nft: DefineTokenWithPrecision => \n      div(cls := \"detail table-container\")(\n        row(\"Definition ID\", nft.definitionId.toString),\n        row(\"Name\", nft.name.toString()),\n      ) :: List()\n    case nft: MintNFT => \n      div(cls := \"detail table-container\")(\n        rowCustom(\"Token ID\", ParseHtml.fromTokenId(nft.tokenId.toString)),\n        // row(\"Collection Name\", nft.collectionName),\n        row(\"No\", getNFromId(nft.tokenId.toString)),\n        rowCustom(\"Data URI\", a(href := nft.dataUrl.toString, target := \"_blank\")(nft.dataUrl.toString)),\n        row(\"Content Hash\", nft.contentHash.toHex),\n        row(\"Rarity\", nft.rarity.toString),\n      ) :: List()\n    case nft: MintNFTWithMemo => \n      div(cls := \"detail table-container\")(\n        rowCustom(\"Token ID\", ParseHtml.fromTokenId(nft.tokenId.toString)),\n        // row(\"Collection Name\", nft.collectionName),\n        row(\"No\", getNFromId(nft.tokenId.toString)),\n        rowCustom(\"Data URI\", a(href := nft.dataUrl.toString, target := \"_blank\")(nft.dataUrl.toString)),\n        row(\"Content Hash\", nft.contentHash.toHex),\n        row(\"Rarity\", nft.rarity.toString),\n      ) :: List()\n    case nft: UpdateNFT => \n      div(cls := \"detail table-container\")(\n        rowTriOutput(\"Token ID\"),\n        rowAccToken(nft.output, nft.tokenId),\n      ) :: List()\n    case nft: TransferNFT => \n      div(cls := \"detail table-container\")(\n        rowInputHead,\n        rowInputBody(nft.input),\n      ) ::\n      div(cls := \"detail table-container\")(\n        rowTriOutput(\"Token ID\"),\n        rowAccToken(nft.output, nft.tokenId),\n      ) :: List()\n    case nft: BurnNFT => \n      div(cls := \"detail table-container\")(\n        row(\"Definition ID\", nft.definitionId.toString),\n      ) :: List()\n    case nft: EntrustNFT => \n      div(cls := \"detail table-container\")(\n        rowInputHead,\n        rowInputBody(nft.input),\n      ) ::\n      div(cls := \"detail table-container\")(\n        rowTriOutput(\"Token ID\"),\n        rowAccToken(nft.to, nft.tokenId),\n      ) :: List()\n    case nft: DisposeEntrustedNFT => \n      div(cls := \"detail table-container\")(\n        rowInputHead,\n        rowInputBody(nft.input),\n      ) ::\n      div(cls := \"detail table-container\")(\n        rowTriOutput(\"Token ID\"),\n        rowAccToken(nft.output.get, nft.tokenId),\n      ) :: List()\n    case tx: MintFungibleToken => \n      div(cls := \"detail table-container\")(\n        rowTriOutput(\"Value\") ::\n        tx.outputs.zipWithIndex\n          .map((d, i) => rowAccBal(d, i))\n          .toList,\n      ) :: List()\n    case tx: TransferFungibleToken => \n      div(cls := \"detail table-container\")(\n        row(\"Definition ID\", tx.tokenDefinitionId.toString),\n      ) ::\n      div(cls := \"detail table-container\")(\n        rowInputHead ::\n        tx.inputs\n          .zipWithIndex\n          .toList\n          .sortBy(_._2)\n          .map((a, i) => rowInputBody(a, i))\n      ) ::\n      div(cls := \"detail table-container\")(\n        rowTriOutput(\"Value\") ::\n        tx.outputs.zipWithIndex\n          .map((d, i) => rowAccBal(d, i))\n          .toList\n      ) :: List()\n    case tx: EntrustFungibleToken => \n      div(cls := \"detail table-container\")(\n        rowInputHead ::\n        tx.inputs.zipWithIndex\n          .map((a, i) => rowInputBody(a, i))\n          .toList\n      ) ::\n      div(cls := \"detail table-container\")(\n        rowTriOutput(\"Value\"),\n        rowAccBal((tx.to, tx.amount), 0),\n      ) :: List()\n    case tx: DisposeEntrustedFungibleToken => \n      div(cls := \"detail table-container\")(\n        rowInputHead ::\n        tx.inputs.zipWithIndex\n          .map((a, i) => rowInputBody(a, i))\n          .toList\n      ) ::\n      div(cls := \"detail table-container\")(\n        rowTriOutput(\"Value\") ::\n        tx.outputs.zipWithIndex\n          .map((d, i) => rowAccBal(d, i))\n          .toList\n      ) :: List()\n    case tx: BurnFungibleToken =>\n      div(cls := \"detail table-container\")(\n        row(\"Ammount\", tx.amount.toString),\n      ) :: List()\n    case tx: CreateSnapshots =>\n      div(cls := \"detail table-container\")(\n        rowCustom(\"Definition ID\", div(cls := \"inner\")(tx.definitionIds.map(di => span(di.toString)).toList)),\n      ) :: List()\n\n  def accountView(tx: AccountTx) = tx match\n    case tx: CreateAccount =>\n      div(cls := \"detail table-container\")(\n        rowCustom(\n          \"Account\",\n          ParseHtml.fromAccHash(Some(tx.account.toString)),\n        ),\n      ) :: List()\n    case tx: UpdateAccount =>\n      div(cls := \"detail table-container\")(\n        rowCustom(\n          \"Account\",\n          ParseHtml.fromAccHash(Some(tx.account.toString)),\n        ),\n        row(\"Ethereum Address\", tx.ethAddress.get.toString),\n        rowCustom(\n          \"Guardian\",\n          ParseHtml.fromAccHash(tx.guardian.map(_.toString), false),\n        ),\n      ) :: List()\n    case tx: AddPublicKeySummaries =>\n      div(cls := \"detail table-container\")(\n        rowCustom(\n          \"Account\",\n          ParseHtml.fromAccHash(Some(tx.account.toString)),\n        ),\n        row(\"PublicKey Summary\", tx.summaries.keys.head.toBytes.toHex),\n      ) :: List()\n    case _ => List()\n\n  def groupView(tx: GroupTx) = tx match\n    case tx: CreateGroup =>\n      div(cls := \"detail table-container\")(\n        row(\"Group ID\", tx.groupId.toString),\n        row(\"Coordinator Account\", tx.coordinator.toString),\n      ) :: List()\n    case tx: AddAccounts =>\n      div(cls := \"detail table-container\")(\n        row(\"Group ID\", tx.groupId.toString),\n        row(\"Account\", tx.accounts.toList(0).toString()),\n      ) :: List()\n\n  def rewardView(tx: RewardTx) = tx match\n    case tx: OfferReward =>\n      div(cls := \"detail table-container\")(\n        row(\"Definition ID\", tx.tokenDefinitionId.toString),\n      ) ::\n      div(cls := \"detail table-container\")(\n        rowInputHead ::\n        tx.inputs.zipWithIndex\n          .map((a, i) => rowInputBody(a, i))\n          .toList\n      ) :: List()\n      div(cls := \"detail table-container\")(\n        rowTriOutput(\"Value\") ::\n        tx.outputs.zipWithIndex\n          .map((d, i) => rowAccBal(d, i))\n          .toList,\n      ) :: List()\n    case tx: RegisterDao =>\n      div(cls := \"detail table-container\")(\n        row(\"Group ID\", tx.groupId.toString),\n        row(\"DAO Account Name\", tx.daoAccountName.toString),\n        if tx.moderators.size > 0 then row(\"Moderators\", tx.moderators.mkString(\",\")) else Empty,\n      ) :: List()\n    case tx: UpdateDao =>\n      div(cls := \"detail table-container\")(\n        row(\"Group ID\", tx.groupId.toString),\n      ) :: List()\n    case _ => \n      div(cls := \"detail table-container\")(\n        \"\"\n      ) :: List()\n\n  def agendaView(tx: AgendaTx, result: Option[TransactionResult]) = tx match\n    case tx: SuggestSimpleAgenda =>\n      div(cls := \"detail table-container\")(\n        row(\"Title\", tx.title.toString),\n        row(\"Voting Token\", tx.votingToken.toString),\n        rowCustom(\"Vote Start\", ParseHtml.fromDate(Some(tx.voteStart.getEpochSecond()))),\n        rowCustom(\"Vote End\", ParseHtml.fromDate(Some(tx.voteEnd.getEpochSecond()))),\n        rowCustom(\"Vote End\", p(tx.voteEnd.toString())),\n      ) ::\n      div(cls := \"detail table-container\")(\n        row(\"vote option\", \"selected\") ::\n        tx.voteOptions\n          .map(a => row(a._1.toString, a._2.toString))\n          .toList\n      ) :: List()\n    case tx: VoteSimpleAgenda =>\n      div(cls := \"detail table-container\")(\n        rowCustom(\n          \"Agenda Tx Hash\",\n          ParseHtml.fromTxHash(tx.agendaTxHash.toUInt256Bytes.toHex),\n        ),\n        row(\"Selected Option\", tx.selectedOption.toString),\n      ) :: List()\n\n  def votingView(tx: VotingTx) = tx match\n    case tx: CreateVoteProposal =>\n      div(cls := \"detail table-container\")(\n        row(\"Proposal ID\", tx.proposalId.value.toString),\n        row(\"Title\", tx.title.value),\n        row(\"Description\", tx.description.value),\n        rowCustom(\"Vote Start\", ParseHtml.fromDate(Some(tx.voteStart.getEpochSecond()))),\n        rowCustom(\"Vote End\", ParseHtml.fromDate(Some(tx.voteEnd.getEpochSecond()))),\n        rowCustom(\"Vote Type\", p(tx.voteType.name)),\n        rowCustom(\"Voting Power\", div(cls := \"inner\")(tx.votingPower.keys.map(k => p(k.toString)).toList)),\n      ) ::\n      div(cls := \"detail table-container\")(\n        rowCustom(\"Voting Option\", div(cls := \"inner\")(tx.voteOptions.map(a => row(a._1.toString, a._2.toString)).toList))\n      ) :: List()\n    case tx: CastVote =>\n      div(cls := \"detail table-container\")(\n        row(\"Proposal Id\", tx.proposalId.value.toString),\n        row(\"Selected Option\", tx.selectedOption.toString),\n      ) :: List()\n    case tx: TallyVotes =>\n      div(cls := \"detail table-container\")(\n        row(\"Proposal Id\", tx.proposalId.value.toString),\n      ) :: List()\n\n  def creatorDaoView(tx: CreatorDaoTx) = tx match\n    case tx: CreateCreatorDao =>\n      div(cls := \"detail table-container\")(\n        row(\"ID\", tx.id.toString),\n        row(\"Name\", tx.name.toString),\n        row(\"Description\", tx.description.toString),\n        row(\"Founder\", tx.founder.toString),\n        row(\"Coordinator\", tx.coordinator.toString),\n      ) :: List()\n    case tx: UpdateCreatorDao =>\n      div(cls := \"detail table-container\")(\n        row(\"ID\", tx.id.toString),\n        row(\"Name\", tx.name.toString),\n        row(\"Description\", tx.description.toString),\n      ) :: List()\n    case tx: DisbandCreatorDao =>\n      div(cls := \"detail table-container\")(\n        row(\"ID\", tx.id.toString),\n      ) :: List()\n    case tx: ReplaceCoordinator =>\n      div(cls := \"detail table-container\")(\n        row(\"ID\", tx.id.toString),\n        row(\"New Coordinator\", tx.newCoordinator.utf8.value),\n      ) :: List()\n    case tx: AddMembers =>\n      div(cls := \"detail table-container\")(\n        row(\"ID\", tx.id.toString),\n        rowCustom(\"Members\", div(cls := \"inner\")(tx.members.map(a => div(a.toString)).toList))\n      ) :: List()\n    case tx: RemoveMembers =>\n      div(cls := \"detail table-container\")(\n        row(\"ID\", tx.id.toString),\n        rowCustom(\"Members\", div(cls := \"inner\")(tx.members.map(a => div(a.toString)).toList))\n      ) :: List()\n    case tx: PromoteModerators =>\n      div(cls := \"detail table-container\")(\n        row(\"ID\", tx.id.toString),\n        rowCustom(\"Members\", div(cls := \"inner\")(tx.members.map(a => div(a.toString)).toList))\n      ) :: List()\n    case tx: DemoteModerators =>\n      div(cls := \"detail table-container\")(\n        row(\"ID\", tx.id.toString),\n        rowCustom(\"Members\", div(cls := \"inner\")(tx.members.map(a => div(a.toString)).toList))\n      ) :: List()\n"
  },
  {
    "path": "modules/lmscan-frontend/src/main/scala/io/leisuremeta/chain/lmscan/frontend/controllers/Model.scala",
    "content": "package io.leisuremeta.chain.lmscan\npackage frontend\n\nimport common.model.*\nimport tyrian._\nimport cats.effect.IO\nimport scalajs.js\nimport io.circe.Decoder\n\nfinal case class GlobalModel(\n    popup: Boolean = false,\n    searchValue: String = \"\",\n    current: js.Date = new js.Date(),\n):\n  def update(msg: GlobalMsg): GlobalModel = msg match \n    case GlobalInput(s) => this.copy(searchValue = s)\n    case UpdateTime(t) => this.copy(current = t)\n\ntrait Model:\n  val global: GlobalModel\n  def view: Html[Msg]\n  def url: String\n  def update: Msg => (Model, Cmd[IO, Msg])\n  def toEmptyModel: EmptyModel = EmptyModel(global)\n\ntrait PageModel extends Model with ApiModel:\n  val page: Int\n  val size: Int = 20\n  val searchPage: Int\n  val data: Option[ApiModel]\n  val pageToggle: Boolean\n\ncase class EmptyModel(\n  global: GlobalModel = GlobalModel(),\n) extends Model:\n  def view = DefaultLayout.view(\n      this,\n      LoaderView.view\n    )\n  def url = \"\"\n  def update: Msg => (Model, Cmd[IO, Msg]) =\n    case ToPage(model) => model.update(Init)\n    case NavigateToUrl(url) => (this, Nav.loadUrl(url))\n    case ErrorMsg => (ErrorModel(error = \"\"), Cmd.None)\n    case GlobalSearch => (this, DataProcess.globalSearch(global.searchValue.toLowerCase))\n    case GlobalSearchResult(v) => (v, Nav.pushUrl(v.url))\n    case _ => (this, Cmd.None)\n\ncase class IssueInfo(date: String, n: Int)\ngiven Decoder[IssueInfo] =\n  Decoder.forProduct2(\n    \"Issue_date\",\n    \"Issuance\"\n  )(IssueInfo.apply)\n\ncase class NftJson(\n  creatorDesc: String,\n  collectionDesc: String,\n  rarity: String,\n  checksum: String,\n  issue: IssueInfo,\n  collection: String,\n  creator: String,\n  name: String,\n  uri: String,\n)\ngiven Decoder[NftJson] =\n  Decoder.forProduct9(\n    \"Creator_description\",\n    \"Collection_description\",\n    \"Rarity\",\n    \"NFT_checksum\",\n    \"Issuance_info\",\n    \"Collection_name\",\n    \"Creator\",\n    \"NFT_name\",\n    \"NFT_URI\"\n  )(NftJson.apply)\n"
  },
  {
    "path": "modules/lmscan-frontend/src/main/scala/io/leisuremeta/chain/lmscan/frontend/controllers/Msg.scala",
    "content": "package io.leisuremeta.chain.lmscan\npackage frontend\n\nimport common.model._\nimport scalajs.js\n\nsealed trait Msg\nsealed trait GlobalMsg extends Msg\n\ncase object ErrorMsg extends Msg\n\ncase object RefreshData extends Msg\ncase object NoneMsg extends Msg\ncase class GlobalInput(s: String) extends GlobalMsg\ncase class UpdateTime(t: js.Date) extends GlobalMsg\ncase object GlobalSearch extends Msg\ncase class GlobalSearchResult(v: Model) extends Msg\ncase object ListSearch extends Msg\ncase object Init extends Msg\n\ncase class UpdateDetailPage(param: ApiModel) extends Msg\n\ncase object DrawChart extends Msg\ncase class UpdateChart(param: SummaryChart) extends Msg\n\ncase class UpdateSearch(v: Int) extends Msg\n\ncase class UpdateModel(model: ApiModel) extends Msg\ncase class UpdateListModel[T](model: PageResponse[T]) extends Msg\ncase class UpdateBlcs(model: PageResponse[BlockInfo]) extends Msg\ncase class UpdateTxs(model: PageResponse[TxInfo]) extends Msg\ncase class UpdateSample[T](model: PageResponse[T]) extends Msg\n\ncase class GetDataFromApi(key: String) extends Msg\ncase class SetLocal(key: String, d: String) extends Msg\n\ncase class TogglePageInput(t: Boolean) extends Msg\n\ntrait RouterMsg extends Msg\ncase class ToPage(model: Model) extends RouterMsg\ncase class NavigateToUrl(url: String) extends RouterMsg\ncase object EmptyRoute extends RouterMsg\n"
  },
  {
    "path": "modules/lmscan-frontend/src/main/scala/io/leisuremeta/chain/lmscan/frontend/layouts/DefaultLayout.scala",
    "content": "package io.leisuremeta.chain.lmscan.frontend\nimport tyrian.*\nimport tyrian.Html.*\n\nobject DefaultLayout:\n  def view(model: Model, contents: List[Html[Msg]]): Html[Msg] =\n    div(cls := \"main\")(\n      header(\n        NavBar.view(model),\n        SearchView.view(model),\n      ),\n      section(cls := \"con-wrap\")(contents),\n      footer(Footer.view()),\n    )\n  def view(model: Model, contents: Html[Msg]): Html[Msg] =\n    this.view(model, List(contents))\n"
  },
  {
    "path": "modules/lmscan-frontend/src/main/scala/io/leisuremeta/chain/lmscan/frontend/pages/AccountDetailPage.scala",
    "content": "package io.leisuremeta.chain.lmscan\npackage frontend\n\nimport tyrian.*\nimport cats.effect.IO\nimport tyrian.Html.*\nimport common.model._\n\nobject AccountDetailPage:\n  def update(model: AccDetailModel): Msg => (Model, Cmd[IO, Msg]) =\n    case Init => (model, DataProcess.getData(model))\n    case RefreshData => (model, Cmd.None)\n    case UpdateDetailPage(d: AccountDetail) => (model, DataProcess.getData(model.copy(accDetail = d)))\n    case UpdateModel(v: AccountDetail) => (model.set(v), Nav.pushUrl(model.url))\n    case UpdateSearch(v) => (model.copy(searchPage = v), Cmd.None)\n    case ListSearch => (model.copy(page = model.searchPage, pageToggle = false), Cmd.emit(Init))\n    case TogglePageInput(t) => (model.copy(pageToggle = t), Cmd.None)\n    case msg: GlobalMsg => (model.copy(global = model.global.update(msg)), Cmd.None)\n    case msg => (model.toEmptyModel, Cmd.emit(msg))\n\n  def view(model: AccDetailModel): Html[Msg] =\n    DefaultLayout.view(\n      model,\n      List(\n        div(cls := \"page-title\")(\"Account\"),\n        model.data match\n          case None => LoaderView.view\n          case Some(_) => AccountDetailTable.view(model.accDetail)\n        ,\n        div(cls := \"page-title\")(\"Transaction History\"),\n        model.data match\n          case None => LoaderView.view\n          case Some(_) => Table.view(model)\n      ),\n    )\n\nfinal case class AccDetailModel(\n    global: GlobalModel = GlobalModel(),\n    accDetail: AccountDetail = AccountDetail(),\n    page: Int = 1,\n    searchPage: Int = 1,\n    data: Option[PageResponse[TxInfo]] = None,\n    pageToggle: Boolean = false,\n) extends PageModel:\n    def set(v: AccountDetail) =\n      val data = if v.payload.length == 0 then Some(PageResponse()) else Some(PageResponse(v.totalCount, v.totalPages, v.payload))\n      this.copy(\n        accDetail = v,\n        data = data,\n      )\n\n    def view: Html[Msg] = AccountDetailPage.view(this)\n    def url = s\"/acc/${accDetail.address.get}?p=${page}\"\n    def update: Msg => (Model, Cmd[IO, Msg]) = AccountDetailPage.update(this)\n"
  },
  {
    "path": "modules/lmscan-frontend/src/main/scala/io/leisuremeta/chain/lmscan/frontend/pages/AccountPage.scala",
    "content": "package io.leisuremeta.chain.lmscan\npackage frontend\n\nimport tyrian.*\nimport cats.effect.IO\nimport tyrian.Html.*\nimport common.model._\n\nobject AccountPage:\n  def update(model: AccModel): Msg => (Model, Cmd[IO, Msg]) =\n    case Init => (model, DataProcess.getData(model))\n    case RefreshData => (model, Cmd.None)\n    case UpdateListModel(v: PageResponse[AccountInfo]) => (model.copy(data = Some(v)), Nav.pushUrl(model.url))\n    case UpdateSearch(v) => (model.copy(searchPage = v), Cmd.None)\n    case ListSearch => (AccModel(page = model.searchPage), Cmd.emit(Init))\n    case TogglePageInput(t) => (model.copy(pageToggle = t), Cmd.None)\n    case msg: GlobalMsg => (model.copy(global = model.global.update(msg)), Cmd.None)\n    case msg => (model.toEmptyModel, Cmd.emit(msg))\n\n  def view(model: AccModel): Html[Msg] =\n    DefaultLayout.view(\n      model,\n      List(\n        div(cls := \"page-title\")(\"Accounts\"),\n        Table.view(model),\n      ),\n    )\n\nfinal case class AccModel(\n    global: GlobalModel = GlobalModel(),\n    page: Int = 1,\n    searchPage: Int = 1,\n    data: Option[PageResponse[AccountInfo]] = None,\n    pageToggle: Boolean = false,\n) extends PageModel:\n    def view: Html[Msg] = AccountPage.view(this)\n    def url = s\"/accs/$page\"\n    def update: Msg => (Model, Cmd[IO, Msg]) = AccountPage.update(this)\n"
  },
  {
    "path": "modules/lmscan-frontend/src/main/scala/io/leisuremeta/chain/lmscan/frontend/pages/BlockDetailPage.scala",
    "content": "package io.leisuremeta.chain.lmscan\npackage frontend\n\nimport tyrian.*\nimport cats.effect.IO\nimport tyrian.Html.*\nimport common.model._\n\nobject BlockDetailPage:\n  def update(model: BlcDetailModel): Msg => (Model, Cmd[IO, Msg]) =\n    case Init => (model, DataProcess.getData(model))\n    case RefreshData => (model, Cmd.None)\n    case UpdateDetailPage(d: BlockDetail) => (model, DataProcess.getData(model.copy(blcDetail = d)))\n    case UpdateModel(v: BlockDetail) => (model.set(v), Nav.pushUrl(model.url))\n    case UpdateSearch(v) => (model.copy(searchPage = v), Cmd.None)\n    case ListSearch => (model.copy(page = model.searchPage, pageToggle = false), Cmd.emit(Init))\n    case TogglePageInput(t) => (model.copy(pageToggle = t), Cmd.None)\n    case msg: GlobalMsg => (model.copy(global = model.global.update(msg)), Cmd.None)\n    case msg => (model.toEmptyModel, Cmd.emit(msg))\n\n  def view(model: BlcDetailModel): Html[Msg] =\n    DefaultLayout.view(\n      model,\n      List(\n        div(cls := \"page-title\")(\"Block Details\"),\n        model.data match\n          case None => LoaderView.view\n          case Some(_) => BlockDetailTable.view(model.blcDetail)\n        ,\n        div(cls := \"page-title\")(\"Transaction List\"),\n        model.data match\n          case None => LoaderView.view\n          case Some(_) => Table.view(model)\n      ),\n    )\n\nfinal case class BlcDetailModel(\n    global: GlobalModel = GlobalModel(),\n    blcDetail: BlockDetail = BlockDetail(),\n    page: Int = 1,\n    searchPage: Int = 1,\n    data: Option[PageResponse[TxInfo]] = None,\n    pageToggle: Boolean = false,\n) extends PageModel:\n    def set(v: BlockDetail) =\n      val data = if v.payload.length == 0 then Some(PageResponse()) else Some(PageResponse(v.totalCount, v.totalPages, v.payload))\n      this.copy(\n        blcDetail = v,\n        data = data,\n      )\n    def view: Html[Msg] = BlockDetailPage.view(this)\n    def url = s\"/blc/${blcDetail.hash.get}?p=${page}\"\n    def update: Msg => (Model, Cmd[IO, Msg]) = BlockDetailPage.update(this)\n"
  },
  {
    "path": "modules/lmscan-frontend/src/main/scala/io/leisuremeta/chain/lmscan/frontend/pages/BlockPage.scala",
    "content": "package io.leisuremeta.chain.lmscan\npackage frontend\n\nimport tyrian.*\nimport cats.effect.IO\nimport tyrian.Html.*\nimport common.model._\n\nobject BlockPage:\n  def update(model: BlcModel): Msg => (Model, Cmd[IO, Msg]) =\n    case Init => (model, DataProcess.getData(model))\n    case RefreshData => (model, DataProcess.getData(model))\n    case UpdateBlcs(v) => (model.copy(data = Some(v)), Nav.pushUrl(model.url))\n    case UpdateSearch(v) => (model.copy(searchPage = v), Cmd.None)\n    case ListSearch => (BlcModel(page = model.searchPage), Cmd.emit(Init))\n    case TogglePageInput(t) => (model.copy(pageToggle = t), Cmd.None)\n    case msg: GlobalMsg => (model.copy(global = model.global.update(msg)), Cmd.None)\n    case msg => (model.toEmptyModel, Cmd.emit(msg))\n\n  def view(model: BlcModel): Html[Msg] =\n    DefaultLayout.view(\n      model,\n      List(\n        div(cls := \"page-title\")(\"Blocks\"),\n        Table.view(model),\n      ),\n    )\n\nfinal case class BlcModel(\n    global: GlobalModel = GlobalModel(),\n    page: Int = 1,\n    searchPage: Int = 1,\n    data: Option[PageResponse[BlockInfo]] = None,\n    pageToggle: Boolean = false,\n) extends PageModel:\n    def view: Html[Msg] = BlockPage.view(this)\n    def url = s\"/blcs/$page\"\n    def update: Msg => (Model, Cmd[IO, Msg]) = BlockPage.update(this)\n"
  },
  {
    "path": "modules/lmscan-frontend/src/main/scala/io/leisuremeta/chain/lmscan/frontend/pages/ErrorPage.scala",
    "content": "package io.leisuremeta.chain.lmscan.frontend\nimport tyrian.*\nimport cats.effect.IO\nimport tyrian.Html.*\n\nobject ErrorPage:\n  def update(model: ErrorModel): Msg => (Model, Cmd[IO, Msg]) =\n    case Init => (model, Cmd.None)\n    case RefreshData => (model, Cmd.None)\n    case msg: GlobalMsg => (model.copy(global = model.global.update(msg)), Cmd.None)\n    case msg => (model.toEmptyModel, Cmd.emit(msg))\n\n  def view(model: ErrorModel): Html[Msg] =\n    DefaultLayout.view(\n      model,\n      model.error match\n        case \"timeout\" => timeout\n        case _ => \n          div(cls := \"err-wrap\")(\n            p(\"THE PAGE YOU WERE LOOKING FOR DOESN’T EXIST.\"),\n            p(\"You may have mistyped the information. Please check before searching.\"),\n            div(cls := \"cell type-button\")(\n              span(\n                cls := \"font-20px\",\n                onClick(\n                  ToPage(BaseModel()),\n                ),\n              )(\n                \"Back to Previous Page\",\n              ),\n            ),\n          ),\n    )\n\n  val timeout = \n    div(cls := \"err-wrap\")(\n      p(\"TIME OUT! TRY AGAIN LATER.\")\n    )\n\nfinal case class ErrorModel(\n    global: GlobalModel = GlobalModel(),\n    error: String\n) extends Model:\n    def view: Html[Msg] = ErrorPage.view(this)\n    def url = \"/error\"\n    def update: Msg => (Model, Cmd[IO, Msg]) = ErrorPage.update(this)\n"
  },
  {
    "path": "modules/lmscan-frontend/src/main/scala/io/leisuremeta/chain/lmscan/frontend/pages/MainPage.scala",
    "content": "package io.leisuremeta.chain.lmscan\npackage frontend\n\nimport tyrian.*\nimport cats.effect.IO\nimport tyrian.Html.*\nimport common.model._\n\nobject MainPage:\n  def update(model: BaseModel): Msg => (Model, Cmd[IO, Msg]) =\n    case Init => \n      (model, Cmd.Batch(\n        DataProcess.getData(BlcModel()),\n        DataProcess.getData(TxModel()),\n        DataProcess.getLocal(\"board\"),\n        DataProcess.getLocal(\"chart\"),\n        Nav.pushUrl(model.url),\n      ))\n    case RefreshData =>\n      (model, Cmd.Batch(\n        DataProcess.getData(BlcModel()),\n        DataProcess.getData(TxModel()),\n        DataProcess.getLocal(\"board\"),\n        DataProcess.getLocal(\"chart\"),\n      ))\n    case UpdateModel(v: SummaryBoard) => (model.copy(summary = Some(v)), Cmd.None)\n    case UpdateChart(v: SummaryChart) => (model.copy(chartData = Some(v)), Cmd.None)\n    case UpdateBlcs(v) => (model.copy(blcs = Some(v)), Cmd.None)\n    case UpdateTxs(v) => (model.copy(txs = Some(v)), Cmd.None)\n    case GetDataFromApi(key) => (model, DataProcess.getDataAll(key))\n    case SetLocal(key, d) => (model, DataProcess.setLocal(key, d))\n    case msg: GlobalMsg => (model.copy(global = model.global.update(msg)), Cmd.None)\n    case msg => (model.toEmptyModel, Cmd.emit(msg))\n\n  def view(model: BaseModel): Html[Msg] =\n    DefaultLayout.view(\n      model,\n      List( \n        BoardView.view(model),\n        Table.mainView(model),\n      )\n    )\n\nfinal case class BaseModel(\n    global: GlobalModel = GlobalModel(),\n    summary: Option[SummaryBoard] = None,\n    chartData: Option[SummaryChart] = None,\n    blcs: Option[PageResponse[BlockInfo]] = None,\n    txs: Option[PageResponse[TxInfo]] = None,\n) extends Model:\n    def view: Html[Msg] = MainPage.view(this)\n    def url = \"/\"\n    def update: Msg => (Model, Cmd[IO, Msg]) = MainPage.update(this)\n"
  },
  {
    "path": "modules/lmscan-frontend/src/main/scala/io/leisuremeta/chain/lmscan/frontend/pages/NftPage.scala",
    "content": "package io.leisuremeta.chain.lmscan\npackage frontend\n\nimport tyrian.*\nimport cats.effect.IO\nimport tyrian.Html.*\nimport common.model._\n\nobject NftPage:\n  def update(model: NftModel): Msg => (Model, Cmd[IO, Msg]) =\n    case Init => (model, DataProcess.getData(model))\n    case RefreshData => (model, Cmd.None)\n    case UpdateListModel(v: PageResponse[NftInfoModel]) => (model.copy(data = Some(v)), Nav.pushUrl(model.url))\n    case UpdateSearch(v) => (model.copy(searchPage = v), Cmd.None)\n    case ListSearch => (NftModel(page = model.searchPage), Cmd.emit(Init))\n    case TogglePageInput(t) => (model.copy(pageToggle = t), Cmd.None)\n    case msg: GlobalMsg => (model.copy(global = model.global.update(msg)), Cmd.None)\n    case msg => (model.toEmptyModel, Cmd.emit(msg))\n\n  def view(model: NftModel): Html[Msg] =\n    DefaultLayout.view(\n      model,\n      List(\n        div(cls := \"page-title\")(\"NFTs\"),\n        Table.view(model),\n      ),\n    )\n\nfinal case class NftModel(\n    global: GlobalModel = GlobalModel(),\n    page: Int = 1,\n    searchPage: Int = 1,\n    data: Option[PageResponse[NftInfoModel]] = None,\n    pageToggle: Boolean = false,\n) extends PageModel:\n    def view: Html[Msg] = NftPage.view(this)\n    def url = s\"/nfts/$page\"\n    def update: Msg => (Model, Cmd[IO, Msg]) = NftPage.update(this)\n"
  },
  {
    "path": "modules/lmscan-frontend/src/main/scala/io/leisuremeta/chain/lmscan/frontend/pages/NftTokenPage.scala",
    "content": "package io.leisuremeta.chain.lmscan\npackage frontend\n\nimport tyrian.*\nimport cats.effect.IO\nimport tyrian.Html.*\nimport common.model._\n\nobject NftTokenPage:\n  def update(model: NftTokenModel): Msg => (Model, Cmd[IO, Msg]) =\n    case Init => (model, DataProcess.getData(model))\n    case RefreshData => (model, Cmd.None)\n    case UpdateListModel(v: PageResponse[NftSeasonModel]) => (model.copy(data = Some(v)), Nav.pushUrl(model.url))\n    case UpdateSearch(v) => (model.copy(searchPage = v), Cmd.None)\n    case ListSearch => (NftTokenModel(page = model.searchPage, id = model.id), Cmd.emit(Init))\n    case TogglePageInput(t) => (model.copy(pageToggle = t), Cmd.None)\n    case msg: GlobalMsg => (model.copy(global = model.global.update(msg)), Cmd.None)\n    case msg => (model.toEmptyModel, Cmd.emit(msg))\n\n  def view(model: NftTokenModel): Html[Msg] =\n    DefaultLayout.view(\n      model,\n      List(\n        div(cls := \"page-title\")(\"NFTs Token\"),\n        Table.view(model),\n      ),\n    )\n\nfinal case class NftTokenModel(\n    global: GlobalModel = GlobalModel(),\n    page: Int = 1,\n    searchPage: Int = 1,\n    id: String,\n    data: Option[PageResponse[NftSeasonModel]] = None,\n    pageToggle: Boolean = false,\n) extends PageModel:\n    def view: Html[Msg] = NftTokenPage.view(this)\n    def url = s\"/nft/$id/$page\"\n    def update: Msg => (Model, Cmd[IO, Msg]) = NftTokenPage.update(this)\n"
  },
  {
    "path": "modules/lmscan-frontend/src/main/scala/io/leisuremeta/chain/lmscan/frontend/pages/NtfDetailPage.scala",
    "content": "package io.leisuremeta.chain.lmscan\npackage frontend\n\nimport tyrian.*\nimport cats.effect.IO\nimport tyrian.Html.*\nimport common.model._\n\nobject NftDetailPage:\n  def update(model: NftDetailModel): Msg => (Model, Cmd[IO, Msg]) =\n    case Init => (model, DataProcess.getData(model.nftDetail.nftFile.get))\n    case RefreshData => (model, Cmd.None)\n    case UpdateDetailPage(d: NftDetail) => (model, DataProcess.getData(d.nftFile.get))\n    case UpdateModel(v: NftDetail) => (NftDetailModel(nftDetail = v), Nav.pushUrl(model.url))\n    case msg: GlobalMsg => (model.copy(global = model.global.update(msg)), Cmd.None)\n    case msg => (model.toEmptyModel, Cmd.emit(msg))\n\n  def view(model: NftDetailModel): Html[Msg] =\n    DefaultLayout.view(\n      model,\n      List(\n        div(cls := \"page-title\")(\"NFT Details\"),\n        NftDetailTable.view(model.nftDetail),\n        div(cls := \"page-title\")(\"History\"),\n        Table.view(model)\n      ),\n    )\n\nfinal case class NftDetailModel(\n    global: GlobalModel = GlobalModel(),\n    nftDetail: NftDetail = NftDetail(),\n) extends Model:\n    def view: Html[Msg] = NftDetailPage.view(this)\n    def url = s\"/nft/${nftDetail.nftFile.get.tokenId.get}\"\n    def update: Msg => (Model, Cmd[IO, Msg]) = NftDetailPage.update(this)\n"
  },
  {
    "path": "modules/lmscan-frontend/src/main/scala/io/leisuremeta/chain/lmscan/frontend/pages/TxDetailPage.scala",
    "content": "package io.leisuremeta.chain\npackage lmscan\npackage frontend\n\nimport tyrian.*\nimport cats.effect.IO\nimport tyrian.Html.*\nimport common.model._\nimport io.circe.*\nimport io.circe.parser.*\nimport io.circe.generic.auto.*\nimport api.model._\nimport api.model.Transaction.AccountTx\nimport api.model.Transaction.TokenTx\nimport api.model.Transaction.GroupTx\nimport api.model.Transaction.RewardTx\nimport api.model.Transaction.AgendaTx\n\nobject TxDetailPage:\n  def update(model: TxDetailModel): Msg => (Model, Cmd[IO, Msg]) =\n    case Init => (model, DataProcess.getData(model.txDetail))\n    case RefreshData => (model, Cmd.None)\n    case UpdateDetailPage(d: TxDetail) => (model, DataProcess.getData(d))\n    case UpdateModel(v: TxDetail) => (TxDetailModel(txDetail = v), Nav.pushUrl(model.url))\n    case msg: GlobalMsg => (model.copy(global = model.global.update(msg)), Cmd.None)\n    case msg => (model.toEmptyModel, Cmd.emit(msg))\n\n  def view(model: TxDetailModel): Html[Msg] =\n    DefaultLayout.view(\n      model,\n      div(cls := \"page-title\")(\"Transaction details\") :: \n      commonView(model.txDetail, model.getInfo) ::\n      TxDetailTableCommon.view(model.getTxr),\n    )\n\n  def commonView(data: TxDetail, info: (String, String, String)) =\n    div(cls := \"detail table-container\")(\n      div(cls := \"row\")(\n        span(\"Transaction Hash\"),\n        span(data.hash.getOrElse(\"-\")),\n      ),\n      div(cls := \"row\")(\n        span(\"Created At\"),\n        ParseHtml.fromDate(data.createdAt),\n      ),\n      div(cls := \"row\")(\n        span(\"Signer\"),\n        ParseHtml.fromAccHash(Some(info._1)),\n      ),\n      div(cls := \"row\")(\n        span(\"Type\"),\n        span(info._2),\n      ),\n      div(cls := \"row\")(\n        span(\"SubType\"),\n        span(info._3),\n      ),\n    )\n\nfinal case class TxDetailModel(\n    global: GlobalModel = GlobalModel(),\n    txDetail: TxDetail = TxDetail(),\n) extends Model:\n    def view: Html[Msg] = TxDetailPage.view(this)\n    def url = s\"/tx/${txDetail.hash.get}\"\n    def update: Msg => (Model, Cmd[IO, Msg]) = TxDetailPage.update(this)\n    def getTxr: Option[TransactionWithResult] =\n      for\n        json <- txDetail.json\n        tx <- decode[TransactionWithResult](json).toOption\n      yield tx\n    def getInfo: (String, String, String) =\n      extension (a: Transaction)\n        def splitTx = a.toString.split(\"\\\\(\").head\n      this.getTxr match\n        case Some(x) =>\n          val acc = x.signedTx.sig.account.toString\n          val (tt, st) = x.signedTx.value match\n            case tx: Transaction.TokenTx => (\"TokenTx\", tx.splitTx)\n            case tx: Transaction.AccountTx => (\"AccountTx\", tx.splitTx)\n            case tx: Transaction.GroupTx => (\"GroupTx\", tx.splitTx)\n            case tx: Transaction.RewardTx => (\"RewardTx\", tx.splitTx)\n            case tx: Transaction.AgendaTx => (\"AgendaTx\", tx.splitTx)\n            case tx: Transaction.VotingTx => (\"VotingTx\", tx.splitTx)\n            case tx: Transaction.CreatorDaoTx => (\"CreatorDaoTx\", tx.splitTx)\n          (acc, tt, st)\n        case None => (\"\", \"\", \"\")\n"
  },
  {
    "path": "modules/lmscan-frontend/src/main/scala/io/leisuremeta/chain/lmscan/frontend/pages/TxPage.scala",
    "content": "package io.leisuremeta.chain.lmscan\npackage frontend\n\nimport tyrian.*\nimport cats.effect.IO\nimport tyrian.Html.*\nimport common.model._\n\nobject TxPage:\n  def update(model: TxModel): Msg => (Model, Cmd[IO, Msg]) =\n    case Init => (model, DataProcess.getData(model))\n    case RefreshData => \n      if (model.page == 1) then (model, DataProcess.getData(model))\n      else (model, Cmd.None)\n    case UpdateTxs(v) => (model.copy(data = Some(v)), Nav.pushUrl(model.url))\n    case UpdateSearch(v) => (model.copy(searchPage = v), Cmd.None)\n    case ListSearch => (TxModel(page = model.searchPage), Cmd.emit(Init))\n    case TogglePageInput(t) => (model.copy(pageToggle = t), Cmd.None)\n    case msg: GlobalMsg => (model.copy(global = model.global.update(msg)), Cmd.None)\n    case msg => (model.toEmptyModel, Cmd.emit(msg))\n\n  def view(model: TxModel): Html[Msg] =\n    DefaultLayout.view(\n      model,\n      List(\n        div(cls := \"page-title\")(\"Transactions\"),\n        Table.view(model),\n      ),\n    )\n\nfinal case class TxModel(\n    global: GlobalModel = GlobalModel(),\n    page: Int = 1,\n    searchPage: Int = 1,\n    data: Option[PageResponse[TxInfo]] = None,\n    pageToggle: Boolean = false,\n) extends PageModel:\n    def view: Html[Msg] = TxPage.view(this)\n    def url = s\"/txs/$page\"\n    def update: Msg => (Model, Cmd[IO, Msg]) = TxPage.update(this)\n    "
  },
  {
    "path": "modules/lmscan-frontend/src/main/scala/io/leisuremeta/chain/lmscan/frontend/pages/VdDetailPage.scala",
    "content": "package io.leisuremeta.chain.lmscan\npackage frontend\n\nimport tyrian.*\nimport cats.effect.IO\nimport tyrian.Html.*\nimport common.model._\n\nobject VdDetailPage:\n  def update(model: VdDetailModel): Msg => (Model, Cmd[IO, Msg]) =\n    case Init => (model, DataProcess.getData(model))\n    case RefreshData => (model, DataProcess.getData(model))\n    case UpdateModel(v: NodeValidator.ValidatorDetail) => \n      (model.copy(payload = v), Nav.pushUrl(model.url))\n    case msg: GlobalMsg => (model.copy(global = model.global.update(msg)), Cmd.None)\n    case msg => (model.toEmptyModel, Cmd.emit(msg))\n\n  def view(model: VdDetailModel): Html[Msg] =\n    DefaultLayout.view(\n      model,\n      List(\n        div(cls := \"page-title\")(\"Validator Detail\"),\n        infoView(model.validator),\n        div(cls := \"page-title\")(\"Proposed Blocks\"),\n        Table.view(model),\n      ),\n    )\n  \n  def infoView(data: NodeValidator.Validator) =\n      div(cls := \"detail table-container\")(\n        div(cls := \"row\")(\n          span(\"address\"),\n          span(data.address.getOrElse(\"\")),\n        ),\n        div(cls := \"row\")(\n          span(\"Total Proposed Blocks\"),\n          span(f\"${data.cnt.getOrElse(0L)}%,d\"),\n        ),\n        div(cls := \"row\")(\n          span(\"Voting Power\"),\n          span(data.power.getOrElse(0.0).toString + \"%\"),\n        ),\n      )\n\nfinal case class VdDetailModel(\n    global: GlobalModel = GlobalModel(),\n    address: String,\n    page: Int = 1,\n    searchPage: Int = 1,\n    pageToggle: Boolean = false,\n    payload: NodeValidator.ValidatorDetail = NodeValidator.ValidatorDetail()\n) extends PageModel:\n    def view: Html[Msg] = VdDetailPage.view(this)\n    def url = s\"/vds/$address?p=$page\"\n    def update: Msg => (Model, Cmd[IO, Msg]) = VdDetailPage.update(this)\n    def validator = payload.validator\n    def blcs = payload.page\n    val data = Some(payload.page)\n    "
  },
  {
    "path": "modules/lmscan-frontend/src/main/scala/io/leisuremeta/chain/lmscan/frontend/pages/VdPage.scala",
    "content": "package io.leisuremeta.chain.lmscan\npackage frontend\n\nimport tyrian.*\nimport cats.effect.IO\nimport tyrian.Html.*\nimport common.model._\n\nobject VdPage:\n  def update(model: VdModel): Msg => (Model, Cmd[IO, Msg]) =\n    case Init => (model, DataProcess.getData(model))\n    case RefreshData => (model, DataProcess.getData(model))\n    case UpdateModel(v: NodeValidator.ValidatorList) => (model.copy(payload = v.payload.toList), Nav.pushUrl(model.url))\n    case msg: GlobalMsg => (model.copy(global = model.global.update(msg)), Cmd.None)\n    case msg => (model.toEmptyModel, Cmd.emit(msg))\n\n  def view(model: VdModel): Html[Msg] =\n    DefaultLayout.view(\n      model,\n      List(\n        div(cls := \"page-title\")(\"Validators\"),\n        Table.view(model),\n      ),\n    )\n\nfinal case class VdModel(\n    global: GlobalModel = GlobalModel(),\n    payload: List[NodeValidator.Validator] = List()\n) extends Model:\n    def view: Html[Msg] = VdPage.view(this)\n    def url = s\"/vds\"\n    def update: Msg => (Model, Cmd[IO, Msg]) = VdPage.update(this)\n    "
  },
  {
    "path": "modules/lmscan-frontend/src/main/scala/io/leisuremeta/chain/lmscan/frontend/utils/Cell.scala",
    "content": "package io.leisuremeta.chain.lmscan.frontend\nimport tyrian.Html.*\nimport tyrian.*\nimport V.*\nimport io.leisuremeta.chain.lmscan.common.model.*\nimport java.text.DecimalFormat\nimport java.time.format.DateTimeFormatter\nimport java.time._\nimport scalajs.js\n\ndef toDT(t: Long, cur: js.Date): String = \n  val offset = -cur.getTimezoneOffset().toInt / 60\n  LocalDateTime\n  .ofEpochSecond(t, 0, ZoneOffset.ofHours(offset))\n  .toString\n  .replace(\"T\", \" \") + s\" (UTC${if offset < 0 then \"-\" else \"+\"}${offset.toString})\"\n\ndef timeAgo(t: Long, cur: js.Date): String =\n  val now = LocalDateTime.now(ZoneId.ofOffset(\"UTC\", ZoneOffset.ofHours(0))).toEpochSecond(ZoneOffset.UTC)\n  val timeGap = now - t\n  List(\n      ((timeGap / 31536000).toInt, \" year ago\"),\n      ((timeGap / 2592000).toInt, \" month ago\"),\n      ((timeGap / 86400).toInt, \" day ago\"),\n      ((timeGap / 3600).toInt, \" hour ago\"),\n      ((timeGap / 60).toInt, \" min ago\"),\n      ((timeGap / 1).toInt, \"s ago\"),\n    )\n    .find((time, _) => time > 0)\n    .map:\n      case (time, msg) if time > 1 => time.toString + msg.replace(\" a\", \"s a\").replace(\"ss\", \"s\")\n      case (time, msg) => time.toString + msg\n    .getOrElse(\"-\")\n\nobject ParseHtml:\n  def fromDate(data: Option[Long]) =\n    div(\n        data match\n          case Some(v) => toDT(v, new js.Date)\n          case _ => \"-\"\n    )\n  def fromAccHash(data: Option[String], filter: Boolean = true) =\n    div(\n      cls := s\"acc-hash\",\n      onClick(ToPage(AccDetailModel(accDetail = AccountDetail(address = data)))),\n    )(if filter then accountHash(data) else data.getOrElse(\"\"))\n  \n  def fromTxHash(data: String) =\n    span(\n      cls := \"tx-hash\",\n      onClick(\n        ToPage(TxDetailModel(txDetail = TxDetail(hash = Some(data))))\n      ),\n    )(data)\n\n  def fromTokenId(value: String) = \n    span(\n      cls := \"token-id\",\n      onClick(\n        ToPage(NftDetailModel(nftDetail = NftDetail(nftFile = Some(NftFileModel(tokenId = Some(value))))))\n      )\n    )(value)\n\n  def fromBal(data: Option[BigDecimal]) = \n    div(\n      span(\n        data match\n          case None => \"- LM\"\n          case Some(v) =>\n            val a = v / BigDecimal(\"1E+18\")\n            DecimalFormat(\"#,###.####\").format(a) + \" LM\",\n      ),\n    )\n\nenum Cell:\n  case ImageS(data: Option[String])              extends Cell\n  case Image(data: Option[String])              extends Cell\n  case Head(data: String, css: String = \"cell\") extends Cell\n  case Any(data: String, css: String = \"cell\")  extends Cell\n  case PriceS(\n      price: Option[BigDecimal],\n      css: String = \"cell\",\n  )                                                             extends Cell\n  case Price(\n      price: Option[Double],\n      balance: Option[BigDecimal],\n      css: String = \"cell\",\n  )                                                             extends Cell\n  case Balance(data: Option[BigDecimal], css: String = \"cell\")  extends Cell\n  case AGE(data: Option[Long], cur: js.Date)                                  extends Cell\n  case DateS(data: Option[Instant], css: String = \"cell\")           extends Cell\n  case DATE(data: Option[Long], css: String = \"cell\")           extends Cell\n  case BLOCK_NUMBER(data: (Option[String], Option[Long]))       extends Cell\n  case NftToken(data: NftInfoModel)                         extends Cell\n  case NftDetail(data: NftSeasonModel, s: Option[String])                         extends Cell\n  case BLOCK_HASH(data: Option[String])                         extends Cell\n  case ACCOUNT_HASH(data: Option[String], css: String = \"cell\") extends Cell\n  case TX_HASH(data: Option[String])                            extends Cell\n  case Tx_VALUE(data: (Option[String], Option[String]))         extends Cell\n  case PlainInt(data: Option[Int])                              extends Cell\n  case PlainLong(data: Option[Long])                            extends Cell\n  case PlainStr(\n      data: Option[String] | Option[Int] | Option[Long] | Option[Double],\n      css: String = \"cell\",\n  )                      extends Cell\n  case AAA(data: String) extends Cell\n\nobject gen:\n  def cell(cells: Cell*) = cells\n    .map(_ match\n      case Cell.ImageS(nftUri) =>\n        img(\n          cls := \"thumb-img\",\n          src     := s\"${getOptionValue(nftUri, \"-\").toString}\",\n        )\n      case Cell.Image(nftUri) =>\n        List(\"mp3\", \"mp4\")\n          .find(data => plainStr(nftUri).contains(data)) match\n          case Some(_) => // 비디오 포맷\n            video(\n              cls := \"nft-image p-10px\",\n              autoPlay,\n              loop,\n              name := \"media\",\n            )(\n              source(\n                src    := s\"${getOptionValue(nftUri, \"-\").toString}\",\n                `type` := \"video/mp4\",\n              ),\n            )\n          case None => // 이미지 포맷\n            img(\n              cls := \"nft-image p-10px\",\n              src     := s\"${getOptionValue(nftUri, \"-\").toString}\",\n            )\n\n      case Cell.Head(data, css) => div(cls := s\"$css\")(span()(data))\n      case Cell.Any(data, css)  => div(cls := s\"$css\")(span()(data))\n      case Cell.PriceS(price, css) =>\n        div(cls := s\"$css\")(\n          span(\n            price match\n              case Some(p) =>\n                \"$ \" + DecimalFormat(\"#,###.0000\").format(p)\n              case _ => \"$ 0\",\n          ),\n        )\n      case Cell.Price(price, data, css) =>\n        div(cls := s\"$css\")(\n          span(\n            (price, data) match\n              case (Some(p), Some(v)) =>\n                val a = v / BigDecimal(\"1E+18\") * BigDecimal(p)\n                \"$ \" + DecimalFormat(\"#,###.0000\").format(a)\n              case _ => \"$ 0\",\n          ),\n        )\n      case Cell.Balance(data, css) =>\n        div(cls := s\"$css\")(\n          span(\n            data match\n              case None => \"- LM\"\n              case Some(v) =>\n                val a = v / BigDecimal(\"1E+18\")\n                DecimalFormat(\"#,###.00\").format(a) + \" LM\",\n          ),\n        )\n      case Cell.PlainStr(data, css) =>\n        div(cls := s\"$css\")(span()(plainStr(data)))\n      case Cell.PlainInt(data) =>\n        div(cls := \"cell\")(\n          span(\n          )(plainStr(data)),\n        )\n      case Cell.PlainLong(data) =>\n        div(cls := \"cell\")(\n          span(\n          )(plainStr(data)),\n        )\n      case Cell.Tx_VALUE(subType, value) =>\n        div(\n          cls := s\"cell ${plainStr(subType).contains(\"Nft\") match\n              case true => \"type-3\"\n              case _    => \"\"\n            }\",\n        )(\n          span(\n            plainStr(subType).contains(\"Nft\") match\n              case true =>\n                onClick(\n                  ToPage(NftDetailModel(nftDetail = NftDetail(nftFile = Some(NftFileModel(tokenId = value)))))\n                )\n              case _ => EmptyAttribute,\n          )(\n            plainStr(subType).contains(\"Nft\") match\n              case true => plainStr(value)\n              case _ =>\n                value\n                  .map(s => s.forall(Character.isDigit))\n                  .getOrElse(false) match\n                  case true =>\n                    txValue(value)\n                  case false => plainStr(value),\n          ),\n        )\n      case Cell.ACCOUNT_HASH(hash, css) =>\n        div(\n          cls := s\"cell acc-hash $css\",\n          onClick(ToPage(AccDetailModel(accDetail = AccountDetail(address = hash)))),\n        )(\n          accountHash(hash),\n        )\n      case Cell.AGE(data, cur) =>\n        div(cls := \"cell\",\n            dataAttr(\n              \"tooltip-text\",\n              toDT(data.getOrElse(0L), cur),\n            ),\n          )(\n            data match\n              case Some(v) => timeAgo(v, cur)\n              case _ => \"-\"\n          )\n      case Cell.DateS(data, css) =>\n        data match\n          case None => div()\n          case Some(v) => \n            div(\n              cls := s\"$css\",\n              dataAttr(\n                \"tooltip-text\",\n                v.toString\n              )\n            )(\n              DateTimeFormatter.ofPattern(\"yyyy-MM-dd HH:mm (O)\")\n                .withZone(ZoneId.of(\"+09:00\")) \n                .format(v)\n            )\n      case Cell.DATE(data, css) =>\n        div(cls := s\"$css\")(\n            {\n              val date = toDT(\n                plainStr(data).toInt, new js.Date\n              ) + \" +UTC\"\n              date match\n                case x if x.contains(\"1970\") => \"-\"\n                case _                       => date\n\n            },\n        )\n\n      case Cell.BLOCK_NUMBER((hash, number)) =>\n        div(cls := \"cell blc-num\",\n            onClick(\n              ToPage(BlcDetailModel(blcDetail = BlockDetail(hash = hash))),\n            ),\n          )(plainStr(number))\n\n      case Cell.NftToken(nftInfo) =>\n        a(\n          cls := \"token-id\",\n          onClick(\n            ToPage(NftTokenModel(id = nftInfo.season.get)),\n          ),\n        )(\n          s\"${plainSeason(nftInfo.season)}${plainStr(nftInfo.seasonName)}\",\n        )\n      case Cell.NftDetail(nftInfo, s) =>\n        div(\n          cls := \"token-id\",\n          onClick(\n            ToPage(NftDetailModel(nftDetail = NftDetail(nftFile = Some(NftFileModel(tokenId = nftInfo.tokenId)))))\n          ),\n        )(plainStr(s))\n      case Cell.BLOCK_HASH(hash) =>\n        div(\n          cls := \"cell blc-hash\",\n          onClick(\n            ToPage(BlcDetailModel(blcDetail = BlockDetail(hash = hash))),\n          ),\n        )(plainStr(hash))\n      case Cell.TX_HASH(hash) =>\n        div(\n          cls := \"cell tx-hash\",\n          onClick(\n            ToPage(TxDetailModel(txDetail = TxDetail(hash = hash)))\n          ),\n        )(\n          plainStr(hash),\n        )\n\n      case _ => div(cls := \"cell\")(span()),\n    )\n    .toList\n"
  },
  {
    "path": "modules/lmscan-frontend/src/main/scala/io/leisuremeta/chain/lmscan/frontend/utils/DataProcess.scala",
    "content": "package io.leisuremeta.chain.lmscan\npackage frontend\n\nimport cats.effect.IO\nimport io.circe.parser.*\nimport tyrian.*\nimport tyrian.http.*\nimport scala.scalajs.js\nimport scala.concurrent.duration.*\nimport common.model._\nimport tyrian.cmds.LocalStorage\n\nobject Parse:\n  import io.circe.*, io.circe.generic.semiauto.*\n  given Decoder[SummaryBoard] = deriveDecoder[SummaryBoard]\n  given Decoder[SummaryModel] = deriveDecoder[SummaryModel]\n  given Decoder[SummaryChart] = deriveDecoder[SummaryChart]\n  given Decoder[NftInfoModel] = deriveDecoder[NftInfoModel]\n  given Decoder[BlockInfo] = deriveDecoder[BlockInfo]\n  given Decoder[AccountInfo] = deriveDecoder[AccountInfo]\n  given Decoder[NftSeasonModel] = deriveDecoder[NftSeasonModel]\n  given a: Decoder[PageResponse[AccountInfo]] = deriveDecoder[PageResponse[AccountInfo]]\n  given b: Decoder[PageResponse[BlockInfo]] = deriveDecoder[PageResponse[BlockInfo]]\n  given c: Decoder[PageResponse[TxInfo]] = deriveDecoder[PageResponse[TxInfo]]\n  given d: Decoder[PageResponse[NftInfoModel]] = deriveDecoder[PageResponse[NftInfoModel]]\n  given e: Decoder[PageResponse[NftSeasonModel]] = deriveDecoder[PageResponse[NftSeasonModel]]\n    \n  def searchResponse(): Response => Msg = response =>\n    response.status match\n      case Status(400, _) => ErrorMsg\n      case Status(500, _) => ErrorMsg\n      case _ => searchResult(response)\n\n  def searchResult(response: Response): Msg = decode[SearchResult](response.body) match\n    case Right(SearchResult.acc(x)) => GlobalSearchResult(AccDetailModel().set(x))\n    case Right(SearchResult.tx(x)) => GlobalSearchResult(TxDetailModel(txDetail = x))\n    case Right(SearchResult.blc(x)) => GlobalSearchResult(BlcDetailModel().set(x))\n    case Right(SearchResult.nft(x)) => GlobalSearchResult(NftDetailModel(nftDetail = x))\n    case _ => ErrorMsg\n\n  def onResponse(model: ApiModel): Response => Msg = response =>\n    response.status match\n      case Status(400, _) => ErrorMsg\n      case Status(500, _) => ErrorMsg\n      case _ => result(model, response)\n\n  def result(model: ApiModel, response: Response): Msg = (model, response.body) match\n      case (_: BlcModel, str) => UpdateBlcs(decode[PageResponse[BlockInfo]](str).getOrElse(PageResponse()))\n      case (_: TxModel, str) => UpdateTxs(decode[PageResponse[TxInfo]](str).getOrElse(PageResponse()))\n      case (_: AccModel, str) => UpdateListModel[AccountInfo](decode[PageResponse[AccountInfo]](str).getOrElse(PageResponse()))\n      case (_: NftModel, str) => UpdateListModel(decode[PageResponse[NftInfoModel]](str).getOrElse(PageResponse()))\n      case (_: NftTokenModel, str) => UpdateListModel(decode[PageResponse[NftSeasonModel]](str).getOrElse(PageResponse()))\n      case (_: TxDetail, str) => UpdateModel(decode[TxDetail](str).getOrElse(TxDetail()))\n      case (_: BlockDetail, str) => UpdateModel(decode[BlockDetail](str).getOrElse(BlockDetail()))\n      case (_: AccountDetail, str) => UpdateModel(decode[AccountDetail](str).getOrElse(AccountDetail()))\n      case (_: NftDetail, str) => UpdateModel(decode[NftDetail](str).getOrElse(NftDetail()))\n      case (_: SummaryBoard, str) => SetLocal(\"board\", str)\n      case (_: SummaryChart, str) => SetLocal(\"chart\", str)\n      case (_: NodeValidator.ValidatorList, str) => \n        val res = decode[List[NodeValidator.Validator]](str).map(NodeValidator.ValidatorList(_))\n        UpdateModel(res.getOrElse(NodeValidator.ValidatorList()))\n      case (_: NodeValidator.ValidatorDetail, str) => \n        UpdateModel(decode[NodeValidator.ValidatorDetail](str).getOrElse(NodeValidator.ValidatorDetail()))\n      case (_, str) => ErrorMsg\n      case (_, Left(json)) => ErrorMsg\n\n  def parseFromString(k: String, s: String): Msg = k match\n    case \"board\" => UpdateModel(decode[SummaryBoard](s).getOrElse(SummaryBoard()))\n    case \"chart\" => UpdateChart(decode[SummaryChart](s).getOrElse(SummaryChart()))\n\nobject DataProcess:\n  val base = js.Dynamic.global.process.env.BASE_API_URL\n  def onError(e: HttpError): Msg = ErrorMsg\n  def getData(model: PageModel): Cmd[IO, Msg] =\n    val url = model match\n      case _: BlcModel => s\"${base}block/list?pageNo=${model.page - 1}&sizePerRequest=${model.size}\"\n      case _: TxModel => s\"${base}tx/list?pageNo=${model.page - 1}&sizePerRequest=${model.size}\"\n      case _: NftModel => s\"${base}nft/list?pageNo=${model.page - 1}&sizePerRequest=${model.size}\"\n      case m: NftTokenModel => s\"${base}nft/${m.id}?pageNo=${model.page - 1}&sizePerRequest=${model.size}\"\n      case _: AccModel => s\"${base}account/list?pageNo=${model.page - 1}&sizePerRequest=${model.size}\"\n    Http.send(\n      Request.get(url).withTimeout(20.seconds),\n      Decoder[Msg](Parse.onResponse(model), onError)\n    )\n  def getData(detail: TxDetail): Cmd[IO, Msg] =\n    Http.send(\n      Request.get(s\"${base}tx/${detail.hash.getOrElse(\"\")}/detail\").withTimeout(10.seconds),\n      Decoder[Msg](Parse.onResponse(detail), onError)\n    )\n  def getData(model: BlcDetailModel): Cmd[IO, Msg] =\n    Http.send(\n      Request.get(s\"${base}block/${model.blcDetail.hash.getOrElse(\"\")}/detail?p=${model.page}\").withTimeout(10.seconds),\n      Decoder[Msg](Parse.onResponse(model.blcDetail), onError)\n    )\n  def getData(model: AccDetailModel): Cmd[IO, Msg] =\n    Http.send(\n      Request.get(s\"${base}account/${model.accDetail.address.getOrElse(\"\")}/detail?p=${model.page}\").withTimeout(10.seconds),\n      Decoder[Msg](Parse.onResponse(model.accDetail), onError)\n    )\n  def getData(model: NftFileModel): Cmd[IO, Msg] =\n    Http.send(\n      Request.get(s\"${base}nft/${model.tokenId.getOrElse(\"\")}/detail\").withTimeout(10.seconds),\n      Decoder[Msg](Parse.onResponse(NftDetail()), onError)\n    )\n  def getData(model: VdModel): Cmd[IO, Msg] =\n    Http.send(\n      Request.get(s\"${base}vds\").withTimeout(10.seconds),\n      Decoder[Msg](Parse.onResponse(NodeValidator.ValidatorList()), onError)\n    )\n  def getData(model: VdDetailModel): Cmd[IO, Msg] =\n    Http.send(\n      Request.get(s\"${base}vd/${model.address}?p=${model.page}\").withTimeout(10.seconds),\n      Decoder[Msg](Parse.onResponse(NodeValidator.ValidatorDetail()), onError)\n    )\n  def getDataAll(key: String): Cmd[IO, Msg] = key match\n    case \"chart\" =>\n      Http.send(\n        Request.get(s\"${base}summary/chart/balance\"),\n        Decoder[Msg](Parse.onResponse(SummaryChart()), onError)\n      )\n    case \"board\" =>\n      Http.send(\n        Request.get(s\"${base}summary/main\"),\n        Decoder[Msg](Parse.onResponse(SummaryBoard()), onError)\n      )\n  def globalSearch(v: String): Cmd[IO, Msg] = \n    Http.send(\n      Request.get(s\"${base}search/${v}\"),\n      Decoder[Msg](Parse.searchResponse(), onError)\n    )\n\n  def getLocal(key: String): Cmd[IO, Msg] =\n    LocalStorage.getItem(key):\n      case Right(LocalStorage.Result.Found(s)) => \n        val (t, d) = s.splitAt(13)\n        if  js.Date.now() - t.toDouble < 60 * 10 * 1000 then Parse.parseFromString(key, d) \n        else GetDataFromApi(key)\n      case Left(LocalStorage.Result.NotFound(_)) => GetDataFromApi(key)\n\n  def setLocal(k: String, d: String): Cmd[IO, Msg] =\n    val now = js.Date.now().toString\n    LocalStorage.setItem(k, now + d):\n      case LocalStorage.Result.Success => Parse.parseFromString(k, d)\n      case _ => Parse.parseFromString(k, d) \n"
  },
  {
    "path": "modules/lmscan-frontend/src/main/scala/io/leisuremeta/chain/lmscan/frontend/utils/ValidData.scala",
    "content": "package io.leisuremeta.chain.lmscan.frontend\nobject V:\n  def validNull = (value: Option[String]) =>\n    value match\n      case Some(\"\") => None\n      case _        => value\n\n  def commaNumber = (value: String) =>\n    String.format(\n      \"%,d\",\n      value.replace(\"-\", \"0\"),\n    )\n  def getOptionValue[T] = (field: Option[T], default: T) =>\n    field match\n      case Some(value) => value\n      case None        => default\n\n  def plainStr(\n      data: Option[String] | Option[Int] | Option[Double] | Option[Long],\n  ) = \n    data match\n      case Some(v) => v.toString\n      case None => \"-\"\n      \n  def plainSeason(data: Option[String]) =\n    data match\n      case Some(v) => if(\"\"\"\\d+\\.?\\d*\"\"\".r.matches(v)) s\"SEASON $v:\" else s\"$v:\"\n      case _ => \"\"\n\n  def rarity(data: Option[String]) =\n    getOptionValue(data, \"-\") match\n      case \"NRML\" => \"Normal\"\n      case \"LGDY\" => \"Legendary\"\n      case \"UNIQ\" => \"Unique\"\n      case \"EPIC\" => \"Epic\"\n      case \"RARE\" => \"Rare\"\n      case _ =>\n        getOptionValue(data, \"-\").toString()\n\n  def txValue(data: Option[String]) =\n    val res = String\n      .format(\n        \"%.4f\",\n        (getOptionValue(data, \"0.0\")\n          .asInstanceOf[String]\n          .toDouble / Math.pow(10, 18).toDouble),\n      )\n    val sosu         = res.takeRight(5)\n    val decimal      = res.replace(sosu, \"\")\n    val commaDecimal = String.format(\"%,d\", decimal.toDouble)\n\n    res == \"0.0000\" match\n      case true =>\n        \"-\"\n      case false => commaDecimal + sosu\n\n  def accountHash(data: Option[String]) =\n    data match\n      case Some(str) => accountMatcher(str)\n      case None => \"\"\n    \n  def accountMatcher(data: String) =\n    data match\n      case \"playnomm\" => \"010cd45939f064fd82403754bada713e5a9563a1\"\n      case \"reward-posting\" => \"d2c442e460e06d652f1d7c8706fd649306a5b9ce\"\n      case \"reward-activity\" => \"43a57958149a577cd7528f6d79adbc5ba728c9f3\"\n      case \"DAO-M\" => \"37cd3566cb27e40efdbdb8bf3e8264e7bd1ffffa\"\n      case \"DAO-RWD\" => \"8010b03a46dd4519965796c011b36d37f841157d\"\n      case \"DAO-REWARD\" => \"293b1e3cbb57ac8c354456d79a1e9675781650ed\"\n      case \"creator-reward-posting\" => \"dad9764447ebe3e363cb383cb114aeedc442447c\"\n      case \"creator-reward-activity\" => \"dca74dec332357ce717ba7702b8421edab2eaeee\"\n      case \"reward-nft\" => \"2bc3ac647d09f47d1c733d28b4d151313d62864b\"\n      case \"creator-rewar-fixqty\" => \"f7b90eed2a28d2d41a7ded5e66427b035be0fe9b\"\n      case \"moonlabs\" => \"ec09ba30ac1038c91fa1ae587fbdf859557cbed1\"\n      case \"eth-gateway\" => \"ca79f6fb199218fa681b8f441fefaac2e9a3ead3\"\n      case _ => data\n"
  },
  {
    "path": "modules/node/src/main/resources/application.conf.sample",
    "content": "local {\n  network-id = 102\n  port = 8081\n  private = \"\", // local private key (Hex)\n}\nwire {\n  time-window-millis = 1000,\n  port = 11111,\n  peers: [\n    {\n      dest: \"localhost: 8081\",    // \"address:port\"\n      public-key-summary: \"\", // Peer Public Key Summary (Hex)\n    },\n  ],\n}\ngenesis {\n  timestamp: \"2020-05-20T09:00:00.00Z\",\n}\nredis {\n  host = \"localhost\"\n  port = 6379\n}\n"
  },
  {
    "path": "modules/node/src/main/scala/io/leisuremeta/chain/node/NodeApp.scala",
    "content": "package io.leisuremeta.chain\npackage node\n\nimport cats.effect.Async\nimport cats.effect.kernel.Resource\nimport cats.effect.std.{Dispatcher, Semaphore}\nimport cats.syntax.flatMap.given\nimport cats.syntax.functor.given\n\nimport com.linecorp.armeria.server.Server\nimport sttp.capabilities.fs2.Fs2Streams\nimport sttp.tapir.server.ServerEndpoint\nimport sttp.tapir.server.armeria.cats.{\n  ArmeriaCatsServerInterpreter,\n  ArmeriaCatsServerOptions,\n}\nimport sttp.tapir.server.interceptor.log.DefaultServerLog\n\nimport api.{LeisureMetaChainApi as Api}\nimport api.model.{\n  Account,\n  GroupId,\n  PublicKeySummary,\n  Transaction,\n  TransactionWithResult,\n}\nimport api.model.account.EthAddress\nimport api.model.creator_dao.CreatorDaoId\nimport api.model.token.{SnapshotState, TokenDefinitionId, TokenId}\nimport api.model.voting.ProposalId\nimport dapp.{PlayNommDAppFailure, PlayNommState}\nimport lib.crypto.{CryptoOps, KeyPair}\nimport lib.crypto.Hash.ops.*\nimport repository.{BlockRepository, StateRepository, TransactionRepository}\nimport service.{\n  BlockService,\n  LocalStatusService,\n  NodeInitializationService,\n  StateReadService,\n  TransactionService,\n}\n\nfinal case class NodeApp[F[_]\n  : Async: BlockRepository: StateRepository: TransactionRepository: PlayNommState](\n    config: NodeConfig,\n):\n\n  /** ****************************************************************************\n    * Setup Endpoints\n    * ****************************************************************************\n    */\n\n//  import java.time.Instant\n  import api.model.{Block, Signed} // , StateRoot}\n  import lib.crypto.Hash\n//  import lib.datatype.{BigNat, UInt256}\n\n  val nodeAddresses: IndexedSeq[PublicKeySummary] = config.wire.peers.map {\n    peer =>\n      PublicKeySummary\n        .fromHex(peer.publicKeySummary)\n        .getOrElse(\n          throw new IllegalArgumentException(\n            s\"invalid pub key summary: ${peer.publicKeySummary}\",\n          ),\n        )\n  }\n  val localKeyPair: KeyPair =\n    val privateKey = scala.sys.env\n      .get(\"LMNODE_PRIVATE_KEY\")\n      .map(BigInt(_, 16))\n      .orElse(config.local.`private`)\n      .get\n    CryptoOps.fromPrivate(privateKey)\n\n  def getAccountServerEndpoint = Api.getAccountEndpoint.serverLogic {\n    (a: Account) =>\n      StateReadService.getAccountInfo(a).map {\n        case Some(info) => Right(info)\n        case None       => Left(Right(Api.NotFound(s\"account not found: $a\")))\n      }\n  }\n\n  def getEthServerEndpoint = Api.getEthEndpoint.serverLogic {\n    (ethAddress: EthAddress) =>\n      StateReadService.getEthAccount(ethAddress).map {\n        case Some(account) => Right(account)\n        case None =>\n          Left(Right(Api.NotFound(s\"account not found: $ethAddress\")))\n      }\n  }\n\n  def getGroupServerEndpoint = Api.getGroupEndpoint.serverLogic {\n    (g: GroupId) =>\n      StateReadService.getGroupInfo(g).map {\n        case Some(info) => Right(info)\n        case None       => Left(Right(Api.NotFound(s\"group not found: $g\")))\n      }\n  }\n\n  def getBlockListServerEndpoint = Api.getBlockListEndpoint.serverLogic {\n    (fromOption, limitOption) =>\n      BlockService\n        .index(fromOption, limitOption)\n        .leftMap { (errorMsg: String) =>\n          Left(Api.ServerError(errorMsg))\n        }\n        .value\n  }\n\n  def getBlockServerEndpoint = Api.getBlockEndpoint.serverLogic {\n    (blockHash: Block.BlockHash) =>\n      val result = BlockService.get(blockHash).value\n\n      result.map {\n        case Right(Some(block)) => Right(block)\n        case Right(None) =>\n          Left(Right(Api.NotFound(s\"block not found: $blockHash\")))\n        case Left(err) => Left(Left(Api.ServerError(err)))\n      }\n  }\n\n  def getStatusServerEndpoint = Api.getStatusEndpoint.serverLogicSuccess { _ =>\n    LocalStatusService\n      .status[F](\n        networkId = config.local.networkId,\n        genesisTimestamp = config.genesis.timestamp,\n      )\n  }\n\n  def getTokenDefServerEndpoint = Api.getTokenDefinitionEndpoint.serverLogic {\n    (tokenDefinitionId: TokenDefinitionId) =>\n      StateReadService.getTokenDef(tokenDefinitionId).map {\n        case Some(tokenDef) => Right(tokenDef)\n        case None =>\n          Left(\n            Right(\n              Api.NotFound(s\"token definition not found: $tokenDefinitionId\"),\n            ),\n          )\n      }\n  }\n\n  def getBalanceServerEndpoint = Api.getBalanceEndpoint.serverLogic {\n    (account, movable) =>\n      StateReadService.getBalance(account, movable).map { balanceMap =>\n        Either.cond(\n          balanceMap.nonEmpty,\n          balanceMap,\n          Right(Api.NotFound(s\"balance not found: $account\")),\n        )\n      }\n  }\n\n  def getNftBalanceServerEndpoint = Api.getNftBalanceEndpoint.serverLogic {\n    (account, movable) =>\n      StateReadService.getNftBalance(account, movable).map { nftBalanceMap =>\n        Either.cond(\n          nftBalanceMap.nonEmpty,\n          nftBalanceMap,\n          Right(Api.NotFound(s\"nft balance not found: $account\")),\n        )\n      }\n  }\n\n  def getTokenServerEndpoint = Api.getTokenEndpoint.serverLogic {\n    (tokenId: TokenId) =>\n      StateReadService.getToken(tokenId).value.map {\n        case Right(Some(nftState)) => Right(nftState)\n        case Right(None) =>\n          Left(Right(Api.NotFound(s\"token not found: $tokenId\")))\n        case Left(err) => Left(Left(Api.ServerError(err)))\n      }\n  }\n\n  def getTokenHistoryServerEndpoint = Api.getTokenHistoryEndpoint.serverLogic {\n    (txHash: Hash.Value[TransactionWithResult]) =>\n      StateReadService.getTokenHistory(txHash).value.map {\n        case Right(Some(nftState)) => Right(nftState)\n        case Right(None) =>\n          Left(Right(Api.NotFound(s\"token history not found: $txHash\")))\n        case Left(err) => Left(Left(Api.ServerError(err)))\n      }\n  }\n\n  def getOwnersServerEndpoint = Api.getOwnersEndpoint.serverLogic {\n    (tokenDefinitionId: TokenDefinitionId) =>\n      StateReadService\n        .getOwners(tokenDefinitionId)\n        .leftMap { (errMsg) =>\n          Left(Api.ServerError(errMsg))\n        }\n        .value\n  }\n\n  def getAccountActivityServerEndpoint =\n    Api.getAccountActivityEndpoint.serverLogic { (account: Account) =>\n      StateReadService\n        .getAccountActivity(account)\n        .leftMap {\n          case Right(msg) => Right(Api.BadRequest(msg))\n          case Left(msg)  => Left(Api.ServerError(msg))\n        }\n        .value\n    }\n\n  def getTokenActivityServerEndpoint =\n    Api.getTokenActivityEndpoint.serverLogic { (tokenId: TokenId) =>\n      StateReadService\n        .getTokenActivity(tokenId)\n        .leftMap {\n          case Right(msg) => Right(Api.BadRequest(msg))\n          case Left(msg)  => Left(Api.ServerError(msg))\n        }\n        .value\n    }\n\n  def getAccountSnapshotServerEndpoint =\n    Api.getAccountSnapshotEndpoint.serverLogic { (account: Account) =>\n      StateReadService\n        .getAccountSnapshot(account)\n        .leftMap {\n          case Right(msg) => Right(Api.BadRequest(msg))\n          case Left(msg)  => Left(Api.ServerError(msg))\n        }\n        .subflatMap {\n          case Some(snapshot) => Right(snapshot)\n          case None =>\n            Left(Right(Api.NotFound(s\"No snapshot of account $account\")))\n        }\n        .value\n    }\n\n  def getTokenSnapshotServerEndpoint =\n    Api.getTokenSnapshotEndpoint.serverLogic { (tokenId: TokenId) =>\n      StateReadService\n        .getTokenSnapshot(tokenId)\n        .leftMap {\n          case Right(msg) => Right(Api.BadRequest(msg))\n          case Left(msg)  => Left(Api.ServerError(msg))\n        }\n        .subflatMap {\n          case Some(snapshot) => Right(snapshot)\n          case None =>\n            Left(Right(Api.NotFound(s\"No snapshot of token $tokenId\")))\n        }\n        .value\n    }\n\n  def getOwnershipSnapshotServerEndpoint =\n    Api.getOwnershipSnapshotEndpoint.serverLogic { (tokenId: TokenId) =>\n      StateReadService\n        .getOwnershipSnapshot(tokenId)\n        .leftMap {\n          case Right(msg) => Right(Api.BadRequest(msg))\n          case Left(msg)  => Left(Api.ServerError(msg))\n        }\n        .subflatMap {\n          case Some(snapshot) => Right(snapshot)\n          case None =>\n            Left(Right(Api.NotFound(s\"No snapshot of token $tokenId\")))\n        }\n        .value\n    }\n\n  def getOwnershipSnapshotMapServerEndpoint =\n    Api.getOwnershipSnapshotMapEndpoint.serverLogic {\n      (from: Option[TokenId], limit: Option[Int]) =>\n        StateReadService\n          .getOwnershipSnapshotMap(from, limit.getOrElse(100))\n          .leftMap {\n            case Right(msg) => Right(Api.BadRequest(msg))\n            case Left(msg)  => Left(Api.ServerError(msg))\n          }\n          .value\n    }\n\n  def getOwnershipRewardedServerEndpoint =\n    Api.getOwnershipRewardedEndpoint.serverLogic { (tokenId: TokenId) =>\n      StateReadService\n        .getOwnershipRewarded(tokenId)\n        .leftMap {\n          case Right(msg) => Right(Api.BadRequest(msg))\n          case Left(msg)  => Left(Api.ServerError(msg))\n        }\n        .subflatMap {\n          case Some(log) => Right(log)\n          case None =>\n            Left(Right(Api.NotFound(s\"No rewarded log of token $tokenId\")))\n        }\n        .value\n    }\n\n  def getDaoServerEndpoint =\n    Api.getDaoEndpoint.serverLogic: (groupId: GroupId) =>\n      StateReadService\n        .getDaoInfo(groupId)\n        .leftMap:\n          case Right(msg) => Right(Api.BadRequest(msg))\n          case Left(msg)  => Left(Api.ServerError(msg))\n        .subflatMap:\n          case Some(daoInfo) => Right(daoInfo)\n          case None =>\n            Left(Right(Api.NotFound(s\"No DAO information of group $groupId\")))\n        .value\n\n  def getSnapshotStateServerEndpoint =\n    Api.getSnapshotStateEndpoint.serverLogic: (defId: TokenDefinitionId) =>\n      StateReadService\n        .getSnapshotState(defId)\n        .leftMap:\n          case Right(msg) => Right(Api.BadRequest(msg))\n          case Left(msg)  => Left(Api.ServerError(msg))\n        .subflatMap:\n          case Some(snapshotState) => Right(snapshotState)\n          case None =>\n            Left(Right(Api.NotFound(s\"No snapshot state of token $defId\")))\n        .value\n\n  def getFungibleSnapshotBalanceServerEndpoint =\n    Api.getFungibleSnapshotBalanceEndpoint.serverLogic:\n      (\n          account: Account,\n          defId: TokenDefinitionId,\n          snapshotId: SnapshotState.SnapshotId,\n      ) =>\n        StateReadService\n          .getFungibleSnapshotBalance(account, defId, snapshotId)\n          .leftMap:\n            case Right(msg) => Right(Api.BadRequest(msg))\n            case Left(msg)  => Left(Api.ServerError(msg))\n          .value\n  def getNftSnapshotBalanceServerEndpoint =\n    Api.getNftSnapshotBalanceEndpoint.serverLogic:\n      (\n          account: Account,\n          defId: TokenDefinitionId,\n          snapshotId: SnapshotState.SnapshotId,\n      ) =>\n        StateReadService\n          .getNftSnapshotBalance(account, defId, snapshotId)\n          .leftMap:\n            case Right(msg) => Right(Api.BadRequest(msg))\n            case Left(msg)  => Left(Api.ServerError(msg))\n          .value\n\n  def getVoteProposalServerEndpoint =\n    Api.getVoteProposalEndpoint.serverLogic: (proposalId: ProposalId) =>\n      StateReadService\n        .getVoteProposal(proposalId)\n        .leftMap:\n          case Right(msg) => Right(Api.BadRequest(msg))\n          case Left(msg)  => Left(Api.ServerError(msg))\n        .subflatMap:\n          case Some(proposal) => Right(proposal)\n          case None =>\n            Left(Right(Api.NotFound(s\"No proposal of vote $proposalId\")))\n        .value\n\n  def getAccountVotesServerEndpoint =\n    Api.getAccountVotesEndpoint.serverLogic:\n      (proposalId: ProposalId, account: Account) =>\n        StateReadService\n          .getAccountVotes(proposalId, account)\n          .leftMap:\n            case Right(msg) => Right(Api.BadRequest(msg))\n            case Left(msg)  => Left(Api.ServerError(msg))\n          .subflatMap:\n            case Some(proposal) => Right(proposal)\n            case None =>\n              Left(Right(Api.NotFound(s\"No vote of $proposalId by $account\")))\n          .value\n\n  def getVoteCountServerEndpoint =\n    Api.getVoteCountEndpoint.serverLogic: (proposalId: ProposalId) =>\n      StateReadService\n        .getVoteCount(proposalId)\n        .leftMap:\n          case Right(msg) => Right(Api.BadRequest(msg))\n          case Left(msg)  => Left(Api.ServerError(msg))\n        .value\n\n  def getCreatorDaoInfoServerEndpoint =\n    Api.getCreatorDaoInfoEndpoint.serverLogic: (daoId: CreatorDaoId) =>\n      StateReadService\n        .getCreatorDaoInfo(daoId)\n        .leftMap:\n          case Right(msg) => Right(Api.BadRequest(msg))\n          case Left(msg)  => Left(Api.ServerError(msg))\n        .subflatMap:\n          case Some(daoInfo) => Right(daoInfo)\n          case None =>\n            Left(Right(Api.NotFound(s\"No creator DAO information of $daoId\")))\n        .value\n\n  def getCreatorDaoMemberServerEndpoint =\n    Api.getCreatorDaoMemberEndpoint.serverLogic:\n      (daoId: CreatorDaoId, from: Option[Account], limit: Option[Int]) =>\n        StateReadService\n          .getCreatorDaoMember(daoId, from, limit.getOrElse(100))\n          .leftMap:\n            case Right(msg) => Right(Api.BadRequest(msg))\n            case Left(msg)  => Left(Api.ServerError(msg))\n          .value\n\n  def postTxServerEndpoint(semaphore: Semaphore[F]) =\n    Api.postTxEndpoint.serverLogic { (txs: Seq[Signed.Tx]) =>\n      scribe.info(s\"received postTx request: $txs\")\n      val result =\n        TransactionService.submit[F](semaphore, txs, localKeyPair).value\n      result.map {\n        case Left(PlayNommDAppFailure.External(msg)) =>\n          scribe.info(s\"external error occured in tx $txs: $msg\")\n          Left(Right(Api.BadRequest(msg)))\n        case Left(PlayNommDAppFailure.Internal(msg)) =>\n          scribe.error(s\"internal error occured in tx $txs: $msg\")\n          Left(Left(Api.ServerError(msg)))\n        case Right(txHashes) =>\n          scribe.info(s\"submitted txs: $txHashes\")\n          Right(txHashes)\n      }\n    }\n\n  def getTxSetServerEndpoint = Api.getTxSetEndpoint.serverLogic {\n    (block: Block.BlockHash) =>\n      TransactionService\n        .index(block)\n        .leftMap {\n          case Left(serverErrorMsg) => Left(Api.ServerError(serverErrorMsg))\n          case Right(errorMessage)  => Right(Api.NotFound(errorMessage))\n        }\n        .value\n  }\n\n  def getTxServerEndpoint = Api.getTxEndpoint.serverLogic {\n    (txHash: Signed.TxHash) =>\n      TransactionService.get(txHash).value.map {\n        case Right(Some(tx)) => Right(tx)\n        case Right(None) => Left(Right(Api.NotFound(s\"tx not found: $txHash\")))\n        case Left(err)   => Left(Left(Api.ServerError(err)))\n      }\n  }\n\n  def postTxHashServerEndpoint =\n    Api.postTxHashEndpoint.serverLogicPure[F] { (txs: Seq[Transaction]) =>\n      scribe.info(s\"received postTxHash request: $txs\")\n      Right(txs.map(_.toHash))\n    }\n\n  def leisuremetaEndpoints(\n      semaphore: Semaphore[F],\n  ): List[ServerEndpoint[Fs2Streams[F], F]] = List(\n    getAccountServerEndpoint,\n    getEthServerEndpoint,\n    getBlockListServerEndpoint,\n    getBlockServerEndpoint,\n    getGroupServerEndpoint,\n    getStatusServerEndpoint,\n    getTxServerEndpoint,\n    getTokenDefServerEndpoint,\n    getBalanceServerEndpoint,\n    getNftBalanceServerEndpoint,\n    getTokenServerEndpoint,\n    getTokenHistoryServerEndpoint,\n    getOwnersServerEndpoint,\n    getTxSetServerEndpoint,\n    getAccountActivityServerEndpoint,\n    getTokenActivityServerEndpoint,\n    getAccountSnapshotServerEndpoint,\n    getTokenSnapshotServerEndpoint,\n    getOwnershipSnapshotServerEndpoint,\n    getOwnershipSnapshotMapServerEndpoint,\n    getOwnershipRewardedServerEndpoint,\n    getDaoServerEndpoint,\n    getSnapshotStateServerEndpoint,\n    getFungibleSnapshotBalanceServerEndpoint,\n    getNftSnapshotBalanceServerEndpoint,\n    getVoteProposalServerEndpoint,\n    getAccountVotesServerEndpoint,\n    getCreatorDaoInfoServerEndpoint,\n    getCreatorDaoMemberServerEndpoint,\n    postTxServerEndpoint(semaphore),\n    postTxHashServerEndpoint,\n  )\n\n  val localPublicKeySummary: PublicKeySummary =\n    PublicKeySummary.fromPublicKeyHash(localKeyPair.publicKey.toHash)\n\n  val localNodeIndex: Int =\n    config.wire.peers.map(_.publicKeySummary).indexOf(localPublicKeySummary)\n\n  def getServer(\n      dispatcher: Dispatcher[F],\n  ): F[Server] = for\n    initializeResult <- NodeInitializationService\n      .initialize[F](config.genesis.timestamp)\n      .value\n    bestBlock <- initializeResult match\n      case Left(err)    => Async[F].raiseError(Exception(err))\n      case Right(block) => Async[F].pure(block)\n    semaphore <- Semaphore[F](1)\n    server <- Async[F].fromCompletableFuture:\n      def log[F[_]: Async](\n          level: scribe.Level,\n      )(msg: String, exOpt: Option[Throwable])(using\n          mdc: scribe.mdc.MDC,\n      ): F[Unit] =\n        Async[F].delay(exOpt match\n          case None     => scribe.log(level, mdc, msg)\n          case Some(ex) => scribe.log(level, mdc, msg, ex),\n        )\n      val serverLog = DefaultServerLog(\n        doLogWhenReceived = log(scribe.Level.Info)(_, None),\n        doLogWhenHandled = log(scribe.Level.Info),\n        doLogAllDecodeFailures = log(scribe.Level.Info),\n        doLogExceptions =\n          (msg: String, ex: Throwable) => Async[F].delay(scribe.warn(msg, ex)),\n        noLog = Async[F].pure(()),\n      )\n      val serverOptions = ArmeriaCatsServerOptions\n        .customiseInterceptors[F](dispatcher)\n        .serverLog(serverLog)\n        .options\n      val tapirService = ArmeriaCatsServerInterpreter[F](serverOptions)\n        .toService(leisuremetaEndpoints(semaphore))\n      val server = Server.builder\n        .maxRequestLength(128 * 1024 * 1024)\n        .requestTimeout(java.time.Duration.ofMinutes(10))\n        .http(config.local.port.value)\n        .service(tapirService)\n        .build\n      Async[F].delay:\n        server.start().thenApply(_ => server)\n  yield server\n\n  def resource: Resource[F, Server] =\n    for\n      dispatcher <- Dispatcher.parallel[F]\n      server     <- Resource.fromAutoCloseable(getServer(dispatcher))\n    yield server\n"
  },
  {
    "path": "modules/node/src/main/scala/io/leisuremeta/chain/node/NodeConfig.scala",
    "content": "package io.leisuremeta.chain\npackage node\n\nimport scala.jdk.CollectionConverters.*\nimport scala.util.Try\nimport cats.data.EitherT\nimport cats.effect.Async\nimport cats.syntax.traverse.given\nimport com.typesafe.config.{Config, ConfigException}\nimport eu.timepit.refined.numeric.Interval\nimport eu.timepit.refined.types.net.PortNumber\nimport eu.timepit.refined.refineV\nimport api.model.NetworkId\nimport lib.datatype.{BigNat, UInt256, UInt256BigInt}\nimport NodeConfig.*\nimport java.time.Instant\n\nfinal case class NodeConfig(\n    local: LocalConfig,\n    wire: WireConfig,\n    genesis: GenesisConfig,\n    redis: RedisConfig,\n)\n\nobject NodeConfig:\n  def load[F[_]: Async](getConf: F[Config]): EitherT[F, String, NodeConfig] =\n    for\n      config  <- EitherT.right[String](getConf)\n      local   <- EitherT.fromEither[F](LocalConfig.load(config))\n      wire    <- EitherT.fromEither[F](WireConfig.load(config))\n      genesis <- EitherT.fromEither[F](GenesisConfig.load(config))\n      redis <- EitherT.fromEither[F](RedisConfig.load(config))\n    yield NodeConfig(local, wire, genesis, redis)\n\n  case class LocalConfig(\n      networkId: NetworkId,\n      port: PortNumber,\n      `private`: Option[UInt256BigInt],\n  ):\n    override def toString: String =\n      s\"LocalConfig($networkId, $port, **hidden**)\"\n\n  object LocalConfig:\n    def load(config: Config): Either[String, LocalConfig] = for\n      networkIdLong <- either(config.getLong(\"local.network-id\"))\n      networkId     <- BigNat.fromBigInt(BigInt(networkIdLong))\n      portInt       <- either(config.getInt(\"local.port\"))\n      port          <- refineV[Interval.Closed[0, 65535]](portInt)\n      privStrOption <- eitherOption(config.getString(\"local.private\"))\n      privOption    <- privStrOption match\n        case None => Right(None)\n        case Some(s) => UInt256.from(BigInt(s, 16)).map(Option(_)).left.map(_.msg)\n    yield LocalConfig(NetworkId(networkId), port, privOption)\n\n  case class WireConfig(\n      timeWindowMillis: Long,\n      port: PortNumber,\n      peers: IndexedSeq[PeerConfig],\n  )\n  object WireConfig:\n    def load(config: Config): Either[String, WireConfig] = for\n      timeWindowMillis <- either(config.getLong(\"wire.time-window-millis\"))\n      portInt          <- either(config.getInt(\"wire.port\"))\n      port             <- refineV[Interval.Closed[0, 65535]](portInt)\n      peers            <- either(config.getConfigList(\"wire.peers\"))\n      peerConfigs      <- peers.asScala.toVector.traverse(PeerConfig.load)\n    yield WireConfig(timeWindowMillis, port, peerConfigs)\n\n  case class PeerConfig(dest: String, publicKeySummary: String)\n  object PeerConfig:\n    def load(config: Config): Either[String, PeerConfig] = for\n      dest             <- either(config.getString(\"dest\"))\n      publicKeySummary <- either(config.getString(\"public-key-summary\"))\n    yield PeerConfig(dest, publicKeySummary)\n\n  case class GenesisConfig(timestamp: Instant)\n  object GenesisConfig:\n    def load(config: Config): Either[String, GenesisConfig] = for\n      timestampString <- either(config.getString(\"genesis.timestamp\"))\n      timestamp       <- either(Instant.parse(timestampString))\n    yield GenesisConfig(timestamp)\n  \n  case class RedisConfig(host: String, port: Int)\n  object RedisConfig:\n    def load(config: Config): Either[String, RedisConfig] = for\n      host <- either(config.getString(\"redis.host\"))\n      port <- either(config.getInt(\"redis.port\"))\n    yield RedisConfig(host, port)\n\n  private def either[A](action: => A): Either[String, A] =\n    Try(action).toEither.left.map {\n      case e: ConfigException.Missing   => s\"Missing config: ${e.getMessage}\"\n      case e: ConfigException.WrongType => s\"Wrong type: ${e.getMessage}\"\n      case e: Exception                 => s\"Exception: ${e.getMessage}\"\n    }\n\n  private def eitherOption[A](action: => A): Either[String, Option[A]] =\n    Try(action)\n      .map(Some(_))\n      .recover { case e: ConfigException.Missing => None }\n      .toEither\n      .left\n      .map {\n        case e: ConfigException.WrongType => s\"Wrong type: ${e.getMessage}\"\n        case e: Exception                 => s\"Exception: ${e.getMessage}\"\n      }\n"
  },
  {
    "path": "modules/node/src/main/scala/io/leisuremeta/chain/node/NodeMain.scala",
    "content": "package io.leisuremeta.chain\npackage node\n\n//import java.time.Instant\n\nimport cats.effect.{ExitCode, Resource, IO, IOApp}\nimport com.typesafe.config.{Config, ConfigFactory}\n\nimport api.model.*\nimport dapp.PlayNommState\nimport lib.codec.byte.ByteCodec\nimport lib.crypto.Hash\nimport lib.datatype.{BigNat, UInt256Bytes}\nimport lib.merkle.MerkleTrieNode\nimport lib.merkle.MerkleTrieNode.MerkleHash\nimport repository.{BlockRepository, StateRepository, TransactionRepository}\nimport repository.StateRepository.given\nimport store.*\nimport store.interpreter._\n\nobject NodeMain extends IOApp:\n  def multi[K: ByteCodec, V: ByteCodec](\n      config: NodeConfig,\n      target: InterpreterTarget,\n  ): Resource[IO, KeyValueStore[IO, K, V]] =\n    SwayInterpreter[K, V](target.s)\n//    MultiInterpreter[K, V](config.redis, target)\n\n  def getBlockRepo(config: NodeConfig): Resource[IO, BlockRepository[IO]] = for\n    bestBlockKVStore <- multi[UInt256Bytes, Block.Header](config, InterpreterTarget.BEST_NUM)\n    given SingleValueStore[IO, Block.Header] = SingleValueStore\n      .fromKeyValueStore[IO, Block.Header](using bestBlockKVStore)\n    given KeyValueStore[IO, Block.BlockHash, Block] <- multi[Hash.Value[\n      Block,\n    ], Block](config, InterpreterTarget.BLOCK)\n    given KeyValueStore[IO, BigNat, Block.BlockHash] <- multi[\n      BigNat,\n      Block.BlockHash,\n    ](config, InterpreterTarget.BLOCK_NUM)\n    given KeyValueStore[IO, Signed.TxHash, Block.BlockHash] <- multi[\n      Signed.TxHash,\n      Block.BlockHash,\n    ](config, InterpreterTarget.TX_BLOCK)\n  yield BlockRepository.fromStores[IO]\n\n  def getStateRepo(config: NodeConfig): Resource[IO, StateRepository[IO]] = for given KeyValueStore[\n      IO,\n      MerkleHash,\n      MerkleTrieNode,\n    ] <- multi[MerkleHash, MerkleTrieNode](config, InterpreterTarget.MERKLE_TRIE)\n  yield StateRepository.fromStores[IO]\n\n  def getTransactionRepo(config: NodeConfig): Resource[IO, TransactionRepository[IO]] =\n    for given KeyValueStore[IO, Hash.Value[\n        TransactionWithResult,\n      ], TransactionWithResult] <-\n        multi[Hash.Value[TransactionWithResult], TransactionWithResult](config, InterpreterTarget.TX)\n    yield TransactionRepository.fromStores[IO]\n\n  override def run(args: List[String]): IO[ExitCode] =\n\n    val getConfig: IO[Config] = IO.blocking(ConfigFactory.load)\n\n    NodeConfig.load[IO](getConfig).value.flatMap {\n      case Right(config) =>\n        val program = for\n          given BlockRepository[IO]       <- getBlockRepo(config)\n          given TransactionRepository[IO] <- getTransactionRepo(config)\n          given StateRepository[IO]       <- getStateRepo(config)\n          given PlayNommState[IO] = PlayNommState.build[IO]\n          server <- NodeApp[IO](config).resource\n        yield server\n\n        program.use(_ => IO.never).as(ExitCode.Success)\n      case Left(err) =>\n        IO(println(err)).as(ExitCode.Error)\n    }\n"
  },
  {
    "path": "modules/node/src/main/scala/io/leisuremeta/chain/node/dapp/PlayNommDApp.scala",
    "content": "package io.leisuremeta.chain\npackage node\npackage dapp\n\nimport cats.effect.Concurrent\nimport cats.data.{EitherT, StateT}\n\nimport api.model.{Signed, Transaction, TransactionWithResult}\nimport lib.merkle.MerkleTrieState\nimport repository.TransactionRepository\nimport submodule.*\n\nobject PlayNommDApp:\n  def apply[F[_]: Concurrent: TransactionRepository: PlayNommState](\n      signedTx: Signed.Tx,\n  ): StateT[EitherT[F, PlayNommDAppFailure, *], MerkleTrieState, TransactionWithResult] =\n    signedTx.value match\n      case accountTx: Transaction.AccountTx =>\n        PlayNommDAppAccount(accountTx, signedTx.sig)\n      case groupTx: Transaction.GroupTx =>\n        PlayNommDAppGroup(groupTx, signedTx.sig)\n      case tokenTx: Transaction.TokenTx =>\n        PlayNommDAppToken(tokenTx, signedTx.sig)\n      case rewardTx: Transaction.RewardTx =>\n        PlayNommDAppReward(rewardTx, signedTx.sig)\n      case agendaTx: Transaction.AgendaTx =>\n        PlayNommDAppAgenda(agendaTx, signedTx.sig)\n      case votingTx: Transaction.VotingTx =>\n        PlayNommDAppVoting(votingTx, signedTx.sig)\n      case creatorDaoTx: Transaction.CreatorDaoTx =>\n        PlayNommDAppCreatorDao(creatorDaoTx, signedTx.sig)\n"
  },
  {
    "path": "modules/node/src/main/scala/io/leisuremeta/chain/node/dapp/PlayNommDAppFailure.scala",
    "content": "package io.leisuremeta.chain.node.dapp\n\nimport cats.{~>, Functor}\nimport cats.arrow.FunctionK\nimport cats.data.EitherT\nimport cats.syntax.bifunctor.*\n\nsealed trait PlayNommDAppFailure:\n  def msg: String\nobject PlayNommDAppFailure:\n  final case class External(msg: String) extends PlayNommDAppFailure\n  final case class Internal(msg: String) extends PlayNommDAppFailure\n\n  def external(msg: String): PlayNommDAppFailure = External(msg)\n  def internal(msg: String): PlayNommDAppFailure = Internal(msg)\n\n  def mapExternal[F[_]: Functor](\n      msg: String,\n  ): EitherT[F, String, *] ~> EitherT[F, PlayNommDAppFailure, *] =\n    FunctionK.lift {\n      [A] =>\n        (stringOr: EitherT[F, String, A]) =>\n          stringOr.leftMap(e => PlayNommDAppFailure.external(s\"$msg: $e\"))\n    }\n\n  def mapInternal[F[_]: Functor](\n      msg: String,\n  ): EitherT[F, String, *] ~> EitherT[F, PlayNommDAppFailure, *] =\n    FunctionK.lift {\n      [A] =>\n        (stringOr: EitherT[F, String, A]) =>\n          stringOr.leftMap(e => PlayNommDAppFailure.internal(s\"$msg: $e\"))\n    }\n"
  },
  {
    "path": "modules/node/src/main/scala/io/leisuremeta/chain/node/dapp/PlayNommState.scala",
    "content": "package io.leisuremeta.chain\npackage node\npackage dapp\n\nimport cats.Monad\n\nimport api.model.*\nimport api.model.{Account as AccountM}\nimport api.model.account.*\nimport api.model.token.*\nimport api.model.reward.*\nimport api.model.voting.*\nimport api.model.creator_dao.*\nimport lib.crypto.Hash\nimport lib.datatype.{BigNat, Utf8}\nimport lib.merkle.MerkleTrie.NodeStore\nimport lib.application.DAppState\nimport java.time.Instant\n\ntrait PlayNommState[F[_]]:\n  def account: PlayNommState.Account[F]\n  def group: PlayNommState.Group[F]\n  def token: PlayNommState.Token[F]\n  def reward: PlayNommState.Reward[F]\n  def voting: PlayNommState.Voting[F]\n  def creatorDao: PlayNommState.CreatorDao[F]\n\nobject PlayNommState:\n\n  def apply[F[_]: PlayNommState]: PlayNommState[F] = summon\n\n  case class Account[F[_]](\n      name: DAppState[F, AccountM, AccountData],\n      key: DAppState[F, (AccountM, PublicKeySummary), PublicKeySummary.Info],\n      externalChainAddresses: DAppState[F, (ExternalChain, ExternalChainAddress), AccountM],\n  )\n\n  case class Reward[F[_]](\n      dao: DAppState[F, GroupId, DaoInfo],\n      accountActivity: DAppState[F, (AccountM, Instant), Seq[ActivityLog]],\n      tokenReceived: DAppState[F, (TokenId, Instant), Seq[ActivityLog]],\n      accountSnapshot: DAppState[F, AccountM, ActivitySnapshot],\n      tokenSnapshot: DAppState[F, TokenId, ActivitySnapshot],\n      ownershipSnapshot: DAppState[F, TokenId, OwnershipSnapshot],\n      accountRewarded: DAppState[F, AccountM, ActivityRewardLog],\n      tokenRewarded: DAppState[F, TokenId, ActivityRewardLog],\n      ownershipRewarded: DAppState[F, TokenId, OwnershipRewardLog],\n  )\n\n  case class Group[F[_]](\n      group: DAppState[F, GroupId, GroupData],\n      groupAccount: DAppState[F, (GroupId, AccountM), Unit],\n  )\n\n  type TxHash = Hash.Value[TransactionWithResult]\n\n  type BalanceAmount = BigNat\n\n  case class Token[F[_]](\n      definition: DAppState[F, TokenDefinitionId, TokenDefinition],\n      fungibleBalance: DAppState[\n        F,\n        (AccountM, TokenDefinitionId, TxHash),\n        Unit,\n      ],\n      nftBalance: DAppState[F, (AccountM, TokenId, TxHash), Unit],\n      nftState: DAppState[F, TokenId, NftState],\n      nftHistory: DAppState[F, TxHash, NftState],\n      rarityState: DAppState[F, (TokenDefinitionId, Rarity, TokenId), Unit],\n      entrustFungibleBalance: DAppState[\n        F,\n        (AccountM, AccountM, TokenDefinitionId, TxHash),\n        Unit,\n      ],\n      entrustNftBalance: DAppState[\n        F,\n        (AccountM, AccountM, TokenId, TxHash),\n        Unit,\n      ],\n      snapshotState: DAppState[F, TokenDefinitionId, SnapshotState],\n      fungibleSnapshot: DAppState[\n        F,\n        (AccountM, TokenDefinitionId, SnapshotState.SnapshotId),\n        Map[TxHash, BalanceAmount],\n      ],\n      nftSnapshot: DAppState[\n        F,\n        (AccountM, TokenDefinitionId, SnapshotState.SnapshotId),\n        Set[TokenId],\n      ],\n      totalSupplySnapshot: DAppState[\n        F,\n        (TokenDefinitionId, SnapshotState.SnapshotId),\n        BalanceAmount,\n      ],\n  )\n\n  case class Voting[F[_]](\n      proposal: DAppState[F, ProposalId, Proposal],\n      votes: DAppState[F, (ProposalId, AccountM), (Utf8, BigNat)],\n      counting: DAppState[F, ProposalId, Map[Utf8, BigNat]],\n  )\n\n  case class CreatorDao[F[_]](\n      dao: DAppState[F, CreatorDaoId, CreatorDaoData],\n      daoModerators: DAppState[F, (CreatorDaoId, AccountM), Unit],\n      daoMembers: DAppState[F, (CreatorDaoId, AccountM), Unit],\n  )\n\n  def build[F[_]: Monad: NodeStore]: PlayNommState[F] =\n    scribe.info(s\"Building PlayNommState... \")\n\n    val playNommState = DAppState.WithCommonPrefix(\"playnomm\")\n\n    new PlayNommState:\n      val account: Account[F] = Account[F](\n        name = playNommState.ofName(\"name\"),\n        key = playNommState.ofName(\"key\"),\n        externalChainAddresses = playNommState.ofName(\"pca\"),\n      )\n\n      val group: Group[F] = Group[F](\n        group = playNommState.ofName(\"group\"),\n        groupAccount = playNommState.ofName(\"group-account\"),\n      )\n\n      val token: Token[F] = Token[F](\n        definition = playNommState.ofName(\"token-def\"),\n        fungibleBalance = playNommState.ofName(\"fungible-balance\"),\n        nftBalance = playNommState.ofName(\"nft-balance\"),\n        nftState = playNommState.ofName(\"nft-state\"),\n        nftHistory = playNommState.ofName(\"nft-history\"),\n        rarityState = playNommState.ofName(\"rarity-state\"),\n        entrustFungibleBalance =\n          playNommState.ofName(\"entrust-fungible-balance\"),\n        entrustNftBalance = playNommState.ofName(\"entrust-nft-balance\"),\n        snapshotState = playNommState.ofName(\"snapshot-state\"),\n        fungibleSnapshot = playNommState.ofName(\"fungible-snapshot\"),\n        nftSnapshot = playNommState.ofName(\"nft-snapshot\"),\n        totalSupplySnapshot = playNommState.ofName(\"total-supply-snapshot\"),\n      )\n\n      val reward: Reward[F] = Reward[F](\n        dao = playNommState.ofName(\"dao\"),\n        accountActivity = playNommState.ofName(\"account-activity\"),\n        tokenReceived = playNommState.ofName(\"token-received\"),\n        accountSnapshot = playNommState.ofName(\"account-snapshot\"),\n        tokenSnapshot = playNommState.ofName(\"token-snapshot\"),\n        ownershipSnapshot = playNommState.ofName(\"ownership-snapshot\"),\n        accountRewarded = playNommState.ofName(\"account-rewarded\"),\n        tokenRewarded = playNommState.ofName(\"token-rewarded\"),\n        ownershipRewarded = playNommState.ofName(\"ownership-rewarded\"),\n      )\n\n      val voting: Voting[F] = Voting[F](\n        proposal = playNommState.ofName(\"proposal\"),\n        votes = playNommState.ofName(\"votes\"),\n        counting = playNommState.ofName(\"counting\"),\n      )\n\n      val creatorDao: CreatorDao[F] = CreatorDao[F](\n        dao = playNommState.ofName(\"creator-dao\"),\n        daoModerators = playNommState.ofName(\"dao-moderators\"),\n        daoMembers = playNommState.ofName(\"dao-members\"),\n      )\n"
  },
  {
    "path": "modules/node/src/main/scala/io/leisuremeta/chain/node/dapp/submodule/PlayNommDAppAccount.scala",
    "content": "package io.leisuremeta.chain\npackage node\npackage dapp\npackage submodule\n\nimport java.time.temporal.ChronoUnit\n\nimport cats.Monad\nimport cats.data.{EitherT, StateT}\nimport cats.effect.Concurrent\nimport cats.syntax.eq.*\nimport cats.syntax.traverse.*\n\nimport api.model.{\n  Account,\n  AccountData,\n  AccountSignature,\n  PublicKeySummary,\n  Signed,\n  Transaction,\n  TransactionWithResult,\n}\nimport api.model.account.{ExternalChain, ExternalChainAddress}\nimport lib.crypto.Hash.ops.*\nimport lib.crypto.Recover.ops.*\nimport lib.datatype.Utf8\nimport lib.merkle.MerkleTrieState\n\nobject PlayNommDAppAccount:\n  def apply[F[_]: Concurrent: PlayNommState](\n      tx: Transaction.AccountTx,\n      sig: AccountSignature,\n  ): StateT[\n    EitherT[F, PlayNommDAppFailure, *],\n    MerkleTrieState,\n    TransactionWithResult,\n  ] =\n    tx match\n      case ca: Transaction.AccountTx.CreateAccount =>\n        for\n          accountInfoOption <- getAccountInfo(ca.account)\n          _ <- checkExternal(\n            accountInfoOption.isEmpty,\n            s\"${ca.account} already exists\",\n          )\n          _ <- checkExternal(\n            sig.account == ca.account ||\n              Some(sig.account) == ca.guardian,\n            s\"Signer ${sig.account} is neither ${ca.account} nor its guardian\",\n          )\n          initialPKS <- getPKS(sig, ca)\n          keyInfo = PublicKeySummary.Info(\n            addedAt = ca.createdAt,\n            description =\n              Utf8.unsafeFrom(s\"automatically added at account creation\"),\n            expiresAt = Some(ca.createdAt.plus(40, ChronoUnit.DAYS)),\n          )\n          _ <-\n            if Option(sig.account) === ca.guardian then unit\n            else\n              PlayNommState[F].account.key\n                .put((ca.account, initialPKS), keyInfo)\n                .mapK:\n                  PlayNommDAppFailure.mapInternal:\n                    s\"Fail to put account key ${ca.account}\"\n          accountData = AccountData(\n            guardian = ca.guardian,\n            externalChainAddresses = ca.ethAddress.fold(Map.empty): ethAddress =>\n              Map(ExternalChain.ETH -> ExternalChainAddress(ethAddress.utf8)),\n            lastChecked = ca.createdAt,\n            memo = None,\n          )\n          _ <- PlayNommState[F].account.name\n            .put(ca.account, accountData)\n            .mapK:\n              PlayNommDAppFailure.mapInternal:\n                s\"Fail to put account ${ca.account}\"\n          _ <- ca.ethAddress.fold(unit): ethAddress =>\n            PlayNommState[F].account.externalChainAddresses\n              .put(\n                (ExternalChain.ETH, ExternalChainAddress(ethAddress.utf8)),\n                ca.account,\n              )\n              .mapK:\n                PlayNommDAppFailure.mapInternal:\n                  s\"Fail to update eth address ${ca.ethAddress}\"\n        yield TransactionWithResult(Signed(sig, ca))(None)\n\n      case ca: Transaction.AccountTx.CreateAccountWithExternalChainAddresses =>\n        for\n          accountInfoOption <- getAccountInfo(ca.account)\n          _ <- checkExternal(\n            accountInfoOption.isEmpty,\n            s\"${ca.account} already exists\",\n          )\n          _ <- checkExternal(\n            sig.account == ca.account ||\n              Some(sig.account) == ca.guardian,\n            s\"Signer ${sig.account} is neither ${ca.account} nor its guardian\",\n          )\n          initialPKS <- getPKS(sig, ca)\n          keyInfo = PublicKeySummary.Info(\n            addedAt = ca.createdAt,\n            description =\n              Utf8.unsafeFrom(s\"automatically added at account creation\"),\n            expiresAt = Some(ca.createdAt.plus(40, ChronoUnit.DAYS)),\n          )\n          _ <-\n            if Option(sig.account) === ca.guardian then unit\n            else\n              PlayNommState[F].account.key\n                .put((ca.account, initialPKS), keyInfo)\n                .mapK:\n                  PlayNommDAppFailure.mapInternal:\n                    s\"Fail to put account key ${ca.account}\"\n          accountData = AccountData(\n            guardian = ca.guardian,\n            externalChainAddresses = ca.externalChainAddresses,\n            lastChecked = ca.createdAt,\n            memo = ca.memo,\n          )\n          _ <- PlayNommState[F].account.name\n            .put(ca.account, accountData)\n            .mapK:\n              PlayNommDAppFailure.mapInternal:\n                s\"Fail to put account ${ca.account}\"\n          _ <- ca.externalChainAddresses.toSeq.traverse:\n            (ExternalChain, ExternalChainAddress) =>\n              PlayNommState[F].account.externalChainAddresses\n                .put((ExternalChain, ExternalChainAddress), ca.account)\n                .mapK:\n                  PlayNommDAppFailure.mapInternal:\n                    s\"Fail to put public chain address ${((ExternalChain, ExternalChainAddress), ca.account)}\"\n        yield TransactionWithResult(Signed(sig, ca))(None)\n\n      case ua: Transaction.AccountTx.UpdateAccount =>\n        for\n          _                 <- verifySignature(sig, ua)\n          accountDataOption <- getAccountInfo(ua.account)\n          accountData <- fromOption(\n            accountDataOption,\n            s\"${ua.account} does not exists\",\n          )\n          _ <- checkExternal(\n            sig.account == ua.account ||\n              Some(sig.account) == accountData.guardian,\n            s\"Signer ${sig.account} is neither ${ua.account} nor its guardian\",\n          )\n          accountData1 = accountData.copy(\n            guardian = ua.guardian,\n            externalChainAddresses = ua.ethAddress.fold(Map.empty): ethAddress =>\n              Map(ExternalChain.ETH -> ExternalChainAddress(ethAddress.utf8)),\n            lastChecked = ua.createdAt,\n            memo = None,\n          )\n          _ <- PlayNommState[F].account.name\n            .put(ua.account, accountData1)\n            .mapK:\n              PlayNommDAppFailure.mapInternal:\n                s\"Fail to put account ${ua.account}\"\n          _ <- ua.ethAddress.fold(unit): ethAddress =>\n            PlayNommState[F].account.externalChainAddresses\n              .put(\n                (ExternalChain.ETH, ExternalChainAddress(ethAddress.utf8)),\n                ua.account,\n              )\n              .mapK:\n                PlayNommDAppFailure.mapInternal:\n                  s\"Fail to update eth address ${ua.ethAddress}\"\n        yield TransactionWithResult(Signed(sig, ua))(None)\n\n      case ua: Transaction.AccountTx.UpdateAccountWithExternalChainAddresses =>\n        for\n          _                 <- verifySignature(sig, ua)\n          accountDataOption <- getAccountInfo(ua.account)\n          accountData <- fromOption(\n            accountDataOption,\n            s\"${ua.account} does not exists\",\n          )\n          _ <- checkExternal(\n            sig.account == ua.account ||\n              Some(sig.account) == accountData.guardian,\n            s\"Signer ${sig.account} is neither ${ua.account} nor its guardian\",\n          )\n          accountData1 = accountData.copy(\n            guardian = ua.guardian,\n            externalChainAddresses = ua.externalChainAddresses,\n            lastChecked = ua.createdAt,\n            memo = ua.memo,\n          )\n          _ <- PlayNommState[F].account.name\n            .put(ua.account, accountData1)\n            .mapK:\n              PlayNommDAppFailure.mapInternal:\n                s\"Fail to put account ${ua.account}\"\n          _ <- ua.externalChainAddresses.toSeq.traverse:\n            (ExternalChain, ExternalChainAddress) =>\n              PlayNommState[F].account.externalChainAddresses\n                .put((ExternalChain, ExternalChainAddress), ua.account)\n                .mapK:\n                  PlayNommDAppFailure.mapInternal:\n                    s\"Fail to update public chain address ${((ExternalChain, ExternalChainAddress), ua.account)}\"\n        yield TransactionWithResult(Signed(sig, ua))(None)\n\n      case ap: Transaction.AccountTx.AddPublicKeySummaries =>\n        for\n          _                 <- verifySignature(sig, ap)\n          accountDataOption <- getAccountInfo(ap.account)\n          accountData <- fromOption(\n            accountDataOption,\n            s\"${ap.account} does not exists\",\n          )\n          _ <- checkExternal(\n            sig.account == ap.account ||\n              Some(sig.account) == accountData.guardian,\n            s\"Signer ${sig.account} is neither ${ap.account} nor its guardian ${accountData.guardian}\",\n          )\n          timeToCheck = accountData.lastChecked\n            .plus(10, ChronoUnit.DAYS)\n            .compareTo(ap.createdAt) < 0\n//          toRemove <-\n//            if !timeToCheck then pure(Vector.empty)\n//            else\n//              PlayNommState[F].account.key\n//                .from(ap.account.toBytes)\n//                .flatMapF { stream =>\n//                  stream\n//                    .filter { case (_, info) =>\n//                      info.expiresAt match\n//                        case None => false\n//                        case Some(time) =>\n//                          time\n//                            .plus(40, ChronoUnit.DAYS)\n//                            .compareTo(ap.createdAt) < 0\n//                    }\n//                    .map { case ((account, pks), info) =>\n//                      pks -> info.description\n//                    }\n//                    .compile\n//                    .toVector\n//                }\n//                .mapK(PlayNommDAppFailure.mapInternal {\n//                  s\"Fail to get PKSes of account ${ap.account}\"\n//                })\n//          _ <- toRemove.traverse { case (pks, _) =>\n//            PlayNommState[F].account.key\n//              .remove((ap.account, pks))\n//              .mapK(PlayNommDAppFailure.mapInternal {\n//                s\"Fail to remove old PKS $pks from account ${ap.account}\"\n//              })\n//          }\n          _ <- ap.summaries.toSeq.traverse: (pks, description) =>\n            val expiresAt =\n              if description === Utf8.unsafeFrom(\"permanent\") then None\n              else Some(ap.createdAt.plus(40, ChronoUnit.DAYS))\n            val keyInfo = PublicKeySummary.Info(\n              addedAt = ap.createdAt,\n              description = description,\n              expiresAt = expiresAt,\n            )\n            PlayNommState[F].account.key\n              .put((ap.account, pks), keyInfo)\n              .mapK:\n                PlayNommDAppFailure.mapInternal:\n                  s\"Fail to put PKS $pks of account ${ap.account} with key info $PublicKeySummary.Info\"\n          txResult = Some:\n            Transaction.AccountTx.AddPublicKeySummariesResult(Map.empty)\n        yield TransactionWithResult(Signed(sig, ap))(txResult)\n\n  def verifySignature[F[_]: Concurrent: PlayNommState](\n      sig: AccountSignature,\n      tx: Transaction,\n  ): StateT[EitherT[F, PlayNommDAppFailure, *], MerkleTrieState, Unit] =\n    for\n      pks <- getPKS(sig, tx)\n      keyInfoOption <- PlayNommState[F].account.key\n        .get((sig.account, pks))\n        .mapK:\n          PlayNommDAppFailure.mapInternal:\n            s\"Fail to decode key info of ${(sig.account, pks)}\"\n      //     _ <- PlayNommState[F].account.key\n      //         .from(sig.account.toBytes)\n      //         .flatMapF: stream =>\n      //           stream.compile.toList.map: list =>\n      //             scribe.info(s\"===> PKS: $list\")\n      //         .mapK:\n      //           PlayNommDAppFailure.mapInternal:\n      //             s\"Fail to get stream of PKSes of account ${sig.account}\"\n      keyInfo <- fromOption(\n        keyInfoOption,\n        s\"There is no public key summary $pks from account ${sig.account}\",\n      )\n      newExpiresAt = keyInfo.expiresAt.map: instant =>\n        Seq(instant, tx.createdAt.plus(30, ChronoUnit.DAYS))\n          .maxBy(_.toEpochMilli())\n      _ <-\n        if keyInfo.expiresAt.map(_.toEpochMilli()) ===\n            newExpiresAt.map(_.toEpochMilli())\n        then\n          StateT\n            .pure[EitherT[F, PlayNommDAppFailure, *], MerkleTrieState, Unit](())\n        else\n          PlayNommState[F].account.key\n            .put((sig.account, pks), keyInfo.copy(expiresAt = newExpiresAt))\n            .mapK:\n              PlayNommDAppFailure.mapInternal:\n                s\"Fail to update key info of ${(sig.account, pks)} with $keyInfo\"\n    yield ()\n\n  def getAccountInfo[F[_]: Monad: PlayNommState](\n      account: Account,\n  ): StateT[EitherT[F, PlayNommDAppFailure, *], MerkleTrieState, Option[\n    AccountData,\n  ]] = PlayNommState[F].account.name\n    .get(account)\n    .mapK:\n      PlayNommDAppFailure.mapInternal:\n        s\"Fail to decode account ${account}\"\n\n  def getPKS[F[_]: Monad: PlayNommState](\n      sig: AccountSignature,\n      tx: Transaction,\n  ): StateT[\n    EitherT[F, PlayNommDAppFailure, *],\n    MerkleTrieState,\n    PublicKeySummary,\n  ] = fromOption(\n    tx.toHash.recover(sig.sig),\n    s\"Fail to recover public key from $tx\",\n  ).map: pubKey =>\n    PublicKeySummary.fromPublicKeyHash(pubKey.toHash)\n"
  },
  {
    "path": "modules/node/src/main/scala/io/leisuremeta/chain/node/dapp/submodule/PlayNommDAppAgenda.scala",
    "content": "package io.leisuremeta.chain\npackage node\npackage dapp\npackage submodule\n\nimport cats.data.{EitherT, StateT}\nimport cats.effect.Concurrent\nimport cats.syntax.all.*\n\nimport api.model.{\n  Account,\n  AccountSignature,\n  Signed,\n  Transaction,\n  TransactionWithResult,\n}\nimport lib.codec.byte.ByteEncoder.ops.*\nimport lib.crypto.Hash\nimport lib.datatype.BigNat\nimport lib.merkle.MerkleTrieState\nimport repository.TransactionRepository\n\nobject PlayNommDAppAgenda:\n  def apply[F[_]: Concurrent: PlayNommState: TransactionRepository](\n      tx: Transaction.AgendaTx,\n      sig: AccountSignature,\n  ): StateT[\n    EitherT[F, PlayNommDAppFailure, *],\n    MerkleTrieState,\n    TransactionWithResult,\n  ] = tx match\n    case ssa: Transaction.AgendaTx.SuggestSimpleAgenda =>\n      PlayNommDAppAccount\n        .verifySignature[F](sig, ssa)\n        .map: _ =>\n          TransactionWithResult(Signed(sig, ssa), None)\n\n    case vsa: Transaction.AgendaTx.VoteSimpleAgenda =>\n      for\n        agendaTx <- StateT.liftF(getSuggestSimpleAgendaTx(vsa.agendaTxHash))\n        fungibleBalanceStream <- PlayNommState[F].token.fungibleBalance\n          .streamWithPrefix((sig.account, agendaTx.votingToken).toBytes)\n          .mapK:\n            PlayNommDAppFailure.mapInternal:\n              s\"Fail to get fungible balance stream of ${sig.account} ${agendaTx.votingToken}\"\n        fungibleBalanceTxs <- StateT\n          .liftF:\n            fungibleBalanceStream\n              .map(_._1._3)\n              .compile\n              .toList\n          .mapK:\n            PlayNommDAppFailure.mapInternal:\n              s\"Fail to get fungible balance txs of ${sig.account} ${agendaTx.votingToken}\"\n        freeBalance <- PlayNommDAppToken.getFungibleBalanceTotalAmounts(\n          fungibleBalanceTxs.toSet,\n          sig.account,\n        )\n        entrustBalanceStream <- PlayNommState[F].token.entrustFungibleBalance\n          .streamWithPrefix(sig.account.toBytes)\n          .mapK:\n            PlayNommDAppFailure.mapInternal:\n              s\"Fail to get entrust balance stream of ${sig.account} ${agendaTx.votingToken}\"\n        enturstBalanceAmounts <- StateT\n          .liftF:\n            entrustBalanceStream\n              .filter(_._1._3 === agendaTx.votingToken)\n              .map(_._1._4)\n              .evalMap: txHash =>\n                TransactionRepository[F]\n                  .get(txHash)\n                  .leftMap(_.msg)\n                  .map:\n                    case Some(txWithResult) =>\n                      txWithResult.signedTx.value match\n                        case ef: Transaction.TokenTx.EntrustFungibleToken =>\n                          ef.amount\n                        case _ =>\n                          BigNat.Zero\n                    case None => BigNat.Zero\n              .compile\n              .toList\n          .mapK:\n            PlayNommDAppFailure.mapInternal:\n              s\"Fail to get entrust balance txs of ${sig.account} ${agendaTx.votingToken}\"\n        entrustBalance = enturstBalanceAmounts.foldLeft(BigNat.Zero)(BigNat.add)\n        votingAmount   = BigNat.add(freeBalance, entrustBalance)\n        txResult = Transaction.AgendaTx.VoteSimpleAgendaResult(votingAmount)\n      yield TransactionWithResult(Signed(sig, vsa), Some(txResult))\n\n  def getSuggestSimpleAgendaTx[F[_]: Concurrent: TransactionRepository](\n      txHash: Hash.Value[TransactionWithResult],\n  ): EitherT[F, PlayNommDAppFailure, Transaction.AgendaTx.SuggestSimpleAgenda] =\n    for\n      txWithResultOption <- TransactionRepository[F]\n        .get(txHash)\n        .leftMap: e =>\n          PlayNommDAppFailure.internal(s\"Fail to get tx $txHash: ${e.msg}\")\n      txWithResult <- EitherT.fromOption(\n        txWithResultOption,\n        PlayNommDAppFailure.external(s\"AgendaTx ${txHash} not found\"),\n      )\n      agendaTx <- txWithResult.signedTx.value match\n        case ssa: Transaction.AgendaTx.SuggestSimpleAgenda => EitherT.pure(ssa)\n        case _ =>\n          EitherT.leftT:\n            PlayNommDAppFailure.external:\n              s\"AgendaTx ${txHash} is not SuggestSimpleAgenda\"\n    yield agendaTx\n"
  },
  {
    "path": "modules/node/src/main/scala/io/leisuremeta/chain/node/dapp/submodule/PlayNommDAppCreatorDao.scala",
    "content": "package io.leisuremeta.chain\npackage node\npackage dapp\npackage submodule\n\nimport cats.Functor\nimport cats.data.{EitherT, StateT}\nimport cats.effect.Concurrent\nimport cats.syntax.all.*\n\nimport api.model.{\n  Account,\n  AccountSignature,\n  Signed,\n  Transaction,\n  TransactionWithResult,\n}\nimport api.model.creator_dao.*\nimport lib.merkle.MerkleTrieState\nimport repository.TransactionRepository\n\nobject PlayNommDAppCreatorDao:\n\n  def apply[F[_]: Concurrent: PlayNommState: TransactionRepository](\n      tx: Transaction.CreatorDaoTx,\n      sig: AccountSignature,\n  ): StateT[\n    EitherT[F, PlayNommDAppFailure, *],\n    MerkleTrieState,\n    TransactionWithResult,\n  ] = tx match\n    case cd: Transaction.CreatorDaoTx.CreateCreatorDao =>\n      for\n        _ <- PlayNommDAppAccount.verifySignature(sig, tx)\n        daoDataOption <- PlayNommState[F].creatorDao.dao\n          .get(cd.id)\n          .mapK:\n            PlayNommDAppFailure.mapInternal:\n              s\"Failed to get CreatorDao with id ${cd.id}\"\n        _ <- checkExternal(\n          daoDataOption.isEmpty,\n          s\"CreatorDao with id ${cd.id} already exists\",\n        )\n        daoData = CreatorDaoData(\n          id = cd.id,\n          name = cd.name,\n          description = cd.description,\n          founder = sig.account,\n          coordinator = cd.coordinator,\n        )\n        _ <- PlayNommState[F].creatorDao.dao\n          .put(cd.id, daoData)\n          .mapK:\n            PlayNommDAppFailure.mapInternal:\n              s\"Failed to put CreatorDao with id ${cd.id}\"\n      yield TransactionWithResult(Signed(sig, cd))(None)\n\n    case ud: Transaction.CreatorDaoTx.UpdateCreatorDao =>\n      for\n        _ <- PlayNommDAppAccount.verifySignature(sig, tx)\n        daoDataOption <- PlayNommState[F].creatorDao.dao\n          .get(ud.id)\n          .mapK:\n            PlayNommDAppFailure.mapInternal:\n              s\"Failed to get CreatorDao with id ${ud.id}\"\n        daoData <- fromOption(\n          daoDataOption,\n          s\"CreatorDao with id ${ud.id} does not exist\",\n        )\n        founderOrCoordinator =\n          sig.account === daoData.founder || sig.account === daoData.coordinator\n        hasAuth <-\n          if founderOrCoordinator then pure(true)\n          else isModerator(ud.id, sig.account)\n        _ <- checkExternal(\n          hasAuth,\n          s\"Account ${sig.account} is not authorized to update CreatorDao with id ${ud.id}\",\n        )\n        daoData1 = daoData.copy(\n          name = ud.name,\n          description = ud.description,\n        )\n        _ <- PlayNommState[F].creatorDao.dao\n          .put(ud.id, daoData1)\n          .mapK:\n            PlayNommDAppFailure.mapInternal:\n              s\"Failed to put CreatorDao with id ${ud.id}\"\n      yield TransactionWithResult(Signed(sig, ud))(None)\n\n    case dd: Transaction.CreatorDaoTx.DisbandCreatorDao =>\n      for\n        _ <- PlayNommDAppAccount.verifySignature(sig, tx)\n        daoDataOption <- PlayNommState[F].creatorDao.dao\n          .get(dd.id)\n          .mapK:\n            PlayNommDAppFailure.mapInternal:\n              s\"Failed to get CreatorDao with id ${dd.id}\"\n        daoData <- fromOption(\n          daoDataOption,\n          s\"CreatorDao with id ${dd.id} does not exist\",\n        )\n        founderOrCoordinator =\n          sig.account === daoData.founder || sig.account === daoData.coordinator\n        _ <- checkExternal(\n          founderOrCoordinator,\n          s\"Account ${sig.account} is not authorized to disband DAO ${dd.id}\",\n        )\n        _ <- PlayNommState[F].creatorDao.dao\n          .remove(dd.id)\n          .mapK:\n            PlayNommDAppFailure.mapInternal:\n              s\"Failed to remove CreatorDao with id ${dd.id}\"\n      yield TransactionWithResult(Signed(sig, dd))(None)\n\n    case rc: Transaction.CreatorDaoTx.ReplaceCoordinator =>\n      for\n        _ <- PlayNommDAppAccount.verifySignature(sig, tx)\n        daoDataOption <- PlayNommState[F].creatorDao.dao\n          .get(rc.id)\n          .mapK:\n            PlayNommDAppFailure.mapInternal:\n              s\"Failed to get CreatorDao with id ${rc.id}\"\n        daoData <- fromOption(\n          daoDataOption,\n          s\"CreatorDao with id ${rc.id} does not exist\",\n        )\n        isCurrentCoordinator = sig.account === daoData.coordinator\n        _ <- checkExternal(\n          isCurrentCoordinator,\n          s\"Only the current Coordinator can replace the Coordinator for DAO ${rc.id}\",\n        )\n        updatedDaoData = daoData.copy(coordinator = rc.newCoordinator)\n        _ <- PlayNommState[F].creatorDao.dao\n          .put(rc.id, updatedDaoData)\n          .mapK:\n            PlayNommDAppFailure.mapInternal:\n              s\"Failed to put CreatorDao with id ${rc.id}\"\n      yield TransactionWithResult(Signed(sig, rc))(None)\n\n    case am: Transaction.CreatorDaoTx.AddMembers =>\n      for\n        _ <- PlayNommDAppAccount.verifySignature(sig, tx)\n        daoDataOption <- PlayNommState[F].creatorDao.dao\n          .get(am.id)\n          .mapK:\n            PlayNommDAppFailure.mapInternal:\n              s\"Failed to get CreatorDao with id ${am.id}\"\n        daoData <- fromOption(\n          daoDataOption,\n          s\"CreatorDao with id ${am.id} does not exist\",\n        )\n        founderOrCoordinator =\n          sig.account === daoData.founder || sig.account === daoData.coordinator\n        hasAuth <-\n          if founderOrCoordinator then pure(true)\n          else isModerator(am.id, sig.account)\n        _ <- checkExternal(\n          hasAuth,\n          s\"Account ${sig.account} is not authorized to add members to DAO ${am.id}\",\n        )\n        _ <- am.members.toSeq.traverse: member =>\n          PlayNommState[F].creatorDao.daoMembers\n            .put((am.id, member), ())\n            .mapK:\n              PlayNommDAppFailure.mapInternal:\n                s\"Failed to put CreatorDaoMember with id ${am.id} and member ${member}\"\n      yield TransactionWithResult(Signed(sig, am))(None)\n\n    case rm: Transaction.CreatorDaoTx.RemoveMembers =>\n      for\n        _ <- PlayNommDAppAccount.verifySignature(sig, tx)\n        daoDataOption <- PlayNommState[F].creatorDao.dao\n          .get(rm.id)\n          .mapK:\n            PlayNommDAppFailure.mapInternal:\n              s\"Failed to get CreatorDao with id ${rm.id}\"\n        daoData <- fromOption(\n          daoDataOption,\n          s\"CreatorDao with id ${rm.id} does not exist\",\n        )\n        founderOrCoordinator =\n          sig.account === daoData.founder || sig.account === daoData.coordinator\n        hasAuth <-\n          if founderOrCoordinator then pure(true)\n          else isModerator(rm.id, sig.account)\n        _ <- checkExternal(\n          hasAuth,\n          s\"Account ${sig.account} is not authorized to remove members from DAO ${rm.id}\",\n        )\n        _ <- rm.members.toSeq.traverse: member =>\n          PlayNommState[F].creatorDao.daoMembers\n            .remove((rm.id, member))\n            .mapK:\n              PlayNommDAppFailure.mapInternal:\n                s\"Failed to remove CreatorDaoMember with id ${rm.id} and member ${member}\"\n      yield TransactionWithResult(Signed(sig, rm))(None)\n\n    case pm: Transaction.CreatorDaoTx.PromoteModerators =>\n      for\n        _ <- PlayNommDAppAccount.verifySignature(sig, tx)\n        daoDataOption <- PlayNommState[F].creatorDao.dao\n          .get(pm.id)\n          .mapK:\n            PlayNommDAppFailure.mapInternal:\n              s\"Failed to get CreatorDao with id ${pm.id}\"\n        daoData <- fromOption(\n          daoDataOption,\n          s\"CreatorDao with id ${pm.id} does not exist\",\n        )\n        hasPermission = daoData.founder == sig.account || daoData.coordinator == sig.account\n        _ <- checkExternal(\n          hasPermission,\n          s\"Account ${sig.account} does not have permission to promote moderators in DAO ${pm.id}\",\n        )\n        _ <- pm.members.toSeq.traverse: member =>\n          PlayNommState[F].creatorDao.daoModerators\n            .put((pm.id, member), ())\n            .mapK:\n              PlayNommDAppFailure.mapInternal:\n                s\"Failed to put CreatorDaoModerator with id ${pm.id} and member ${member}\"\n      yield TransactionWithResult(Signed(sig, pm))(None)\n\n    case dm: Transaction.CreatorDaoTx.DemoteModerators =>\n      for\n        _ <- PlayNommDAppAccount.verifySignature(sig, tx)\n        daoDataOption <- PlayNommState[F].creatorDao.dao\n          .get(dm.id)\n          .mapK:\n            PlayNommDAppFailure.mapInternal:\n              s\"Failed to get CreatorDao with id ${dm.id}\"\n        daoData <- fromOption(\n          daoDataOption,\n          s\"CreatorDao with id ${dm.id} does not exist\",\n        )\n        hasPermission = daoData.founder == sig.account || daoData.coordinator == sig.account\n        _ <- checkExternal(\n          hasPermission,\n          s\"Account ${sig.account} does not have permission to demote moderators in DAO ${dm.id}\",\n        )\n        _ <- dm.members.toSeq.traverse: member =>\n          PlayNommState[F].creatorDao.daoModerators\n            .remove((dm.id, member))\n            .mapK:\n              PlayNommDAppFailure.mapInternal:\n                s\"Failed to remove CreatorDaoModerator with id ${dm.id} and member ${member}\"\n      yield TransactionWithResult(Signed(sig, dm))(None)\n\ndef isModerator[F[_]: Functor: PlayNommState](id: CreatorDaoId, account: Account): StateT[\n  EitherT[F, PlayNommDAppFailure, *],\n  MerkleTrieState,\n  Boolean,\n] = PlayNommState[F].creatorDao.daoModerators\n  .get((id, account))\n  .map(_.isDefined)\n  .mapK:\n    PlayNommDAppFailure.mapInternal:\n      s\"Failed to get CreatorDaoModerator with id ${id} and account ${account}\"\n"
  },
  {
    "path": "modules/node/src/main/scala/io/leisuremeta/chain/node/dapp/submodule/PlayNommDAppGroup.scala",
    "content": "package io.leisuremeta.chain\npackage node\npackage dapp\npackage submodule\n\nimport cats.data.{EitherT, StateT}\nimport cats.effect.Concurrent\nimport cats.syntax.all.*\n\nimport api.model.{\n  Account,\n  AccountSignature,\n  GroupData,\n  Signed,\n  Transaction,\n  TransactionWithResult,\n}\nimport lib.merkle.MerkleTrieState\n\nobject PlayNommDAppGroup:\n  def apply[F[_]: Concurrent: PlayNommState](\n      tx: Transaction.GroupTx,\n      sig: AccountSignature,\n  ): StateT[\n    EitherT[F, PlayNommDAppFailure, *],\n    MerkleTrieState,\n    TransactionWithResult,\n  ] =\n    tx match\n      case cg: Transaction.GroupTx.CreateGroup =>\n        for\n          accountData <- PlayNommDAppAccount.verifySignature[F](sig, cg)\n          _ <- checkExternal[F](\n            cg.coordinator === sig.account,\n            s\"Account does not match signature: ${cg.coordinator} vs ${sig.account}\",\n          )\n          _ <- PlayNommState[F].group.group\n            .put(cg.groupId, GroupData(cg.name, cg.coordinator))\n            .mapK:\n              PlayNommDAppFailure\n                .mapInternal[F](s\"Failed to create group: ${cg.groupId}\")\n        yield TransactionWithResult(Signed(sig, cg), None)\n\n      case aa: Transaction.GroupTx.AddAccounts =>\n        for\n          groupDataOption <- PlayNommState[F].group.group\n            .get(aa.groupId)\n            .mapK:\n              PlayNommDAppFailure\n                .mapInternal[F](s\"Failed to get group ${aa.groupId}\")\n          groupData <- fromOption(\n            groupDataOption,\n            s\"Group does not exist: ${aa.groupId}\",\n          )\n          _ <- checkExternal[F](\n            groupData.coordinator === sig.account,\n            s\"Account does not match signature: ${groupData.coordinator} vs ${sig.account}\",\n          )\n          _ <- PlayNommDAppAccount.verifySignature[F](sig, aa)\n          _ <- aa.accounts.toList.traverse: account =>\n            PlayNommState[F].group.groupAccount\n              .put((aa.groupId, account), ())\n              .mapK:\n                PlayNommDAppFailure.mapInternal[F]:\n                  s\"Failed to add an account $account to group: ${aa.groupId}\"\n        yield TransactionWithResult(Signed(sig, aa), None)\n"
  },
  {
    "path": "modules/node/src/main/scala/io/leisuremeta/chain/node/dapp/submodule/PlayNommDAppReward.scala",
    "content": "package io.leisuremeta.chain\npackage node\npackage dapp\npackage submodule\n\nimport cats.data.{EitherT, StateT}\nimport cats.effect.Concurrent\nimport cats.syntax.traverse.*\n\nimport api.model.{\n  AccountSignature,\n  Signed,\n  Transaction,\n  TransactionWithResult,\n}\nimport api.model.reward.{\n  ActivityLog,\n  DaoInfo,\n}\nimport api.model.TransactionWithResult.ops.*\nimport lib.crypto.Hash\nimport lib.crypto.Hash.ops.*\nimport lib.datatype.BigNat\nimport lib.merkle.MerkleTrieState\nimport repository.TransactionRepository\n\nobject PlayNommDAppReward:\n  def apply[F[_]: Concurrent: TransactionRepository: PlayNommState](\n      tx: Transaction.RewardTx,\n      sig: AccountSignature,\n  ): StateT[\n    EitherT[F, PlayNommDAppFailure, *],\n    MerkleTrieState,\n    TransactionWithResult,\n  ] = tx match\n    case rd: Transaction.RewardTx.RegisterDao =>\n      for\n        groupDataOption <- PlayNommState[F].group.group\n          .get(rd.groupId)\n          .mapK:\n            PlayNommDAppFailure.mapInternal(s\"Fail to get group ${rd.groupId}\")\n        _ <- checkExternal(\n          groupDataOption.isDefined,\n          s\"Group ${rd.groupId} not found\",\n        )\n        daoInfoOption <- PlayNommState[F].reward.dao\n          .get(rd.groupId)\n          .mapK:\n            PlayNommDAppFailure.mapInternal(s\"Fail to get group ${rd.groupId}\")\n        _ <- checkExternal(\n          daoInfoOption.isEmpty,\n          s\"Group ${rd.groupId} already has DAO\",\n        )\n        daoInfo = DaoInfo(moderators = rd.moderators)\n        _ <- PlayNommState[F].reward.dao\n          .put(rd.groupId, daoInfo)\n          .mapK:\n            PlayNommDAppFailure.mapInternal(s\"Fail to put DAO ${rd.groupId}\")\n      yield TransactionWithResult(Signed(sig, tx), None)\n\n    case or: Transaction.RewardTx.OfferReward =>\n      for\n        _        <- PlayNommDAppAccount.verifySignature(sig, tx)\n        tokenDef <- PlayNommDAppToken.getTokenDefinition(or.tokenDefinitionId)\n        inputAmount <- PlayNommDAppToken.getFungibleBalanceTotalAmounts(\n          or.inputs.map(_.toResultHashValue),\n          sig.account,\n        )\n        outputAmount = or.outputs.values.foldLeft(BigNat.Zero)(BigNat.add)\n        diff <- fromEitherExternal:\n          BigNat.tryToSubtract(inputAmount, outputAmount)\n        txWithResult = TransactionWithResult(Signed(sig, or))(None)\n        txHash       = txWithResult.toHash\n        _ <- PlayNommDAppToken.removeInputUtxos(\n          sig.account,\n          or.inputs.map(_.toResultHashValue),\n          or.tokenDefinitionId,\n        )\n        _ <- or.inputs.toList.traverse: inputTxHash =>\n          PlayNommDAppToken\n            .removeFungibleSnapshot(\n              sig.account,\n              or.tokenDefinitionId,\n              inputTxHash.toResultHashValue,\n            )\n            .mapK:\n              PlayNommDAppFailure.mapInternal:\n                s\"Fail to remove fungible snapshot of $inputTxHash\"\n        _ <- or.outputs.toSeq.traverse:\n          case (account, outputAmount) =>\n            for\n              _ <- PlayNommDAppToken\n                .putBalance(account, or.tokenDefinitionId, txHash)\n              _ <- PlayNommDAppToken\n                .addFungibleSnapshot(\n                  account,\n                  or.tokenDefinitionId,\n                  txHash,\n                  outputAmount,\n                )\n                .mapK:\n                  PlayNommDAppFailure.mapInternal:\n                    s\"Fail to put fungible snapshot of $txHash\"\n            yield ()               \n        totalAmount <- fromEitherInternal:\n          BigNat.tryToSubtract(tokenDef.totalAmount, diff)\n        _ <- PlayNommDAppToken.putTokenDefinition(\n          or.tokenDefinitionId,\n          tokenDef.copy(totalAmount = totalAmount),\n        )\n        - <- PlayNommDAppToken\n          .removeTotalSupplySnapshot(or.tokenDefinitionId, diff)\n          .mapK:\n            PlayNommDAppFailure.mapInternal:\n              s\"Fail to remove total supply snapshot of ${or.tokenDefinitionId}\"\n      yield txWithResult\n    case tx: Transaction.RewardTx.UpdateDao => ???\n    case tx: Transaction.RewardTx.RecordActivity =>\n      val txResult = TransactionWithResult(Signed(sig, tx), None)\n      val txHash   = txResult.toHash\n      for\n        _ <- tx.userActivity.toList.traverse { case (account, activities) =>\n          val logs = activities.map { a =>\n            ActivityLog(a.point, a.description, txHash)\n          }\n          PlayNommState[F].reward.accountActivity\n            .put((account, tx.timestamp), logs)\n            .mapK {\n              PlayNommDAppFailure.mapInternal {\n                s\"Fail to put account activity in $txHash\"\n              }\n            }\n        }\n        _ <- tx.tokenReceived.toList.traverse { case (account, activities) =>\n          val logs = activities.map { a =>\n            ActivityLog(a.point, a.description, txHash)\n          }\n          PlayNommState[F].reward.tokenReceived\n            .put((account, tx.timestamp), logs)\n            .mapK {\n              PlayNommDAppFailure.mapInternal {\n                s\"Fail to put account activity in $txHash\"\n              }\n            }\n        }\n      yield txResult\n\n    case tx: Transaction.RewardTx.ExecuteReward => ???\n    case tx: Transaction.RewardTx.BuildSnapshot => ???\n//      val getNftStateStream =\n//        StateT\n//          .inspectF { (ms: MerkleState) =>\n//            GenericMerkleTrie\n//              .from[F, TokenId, NftState](BitVector.empty)\n//              .runA(ms.token.nftState)\n//              .map { stream =>\n//                stream.evalMap { case (_, nftState) =>\n//                  GenericMerkleTrie\n//                    .from[\n//                      F,\n//                      (Account, TokenId, Hash.Value[TransactionWithResult]),\n//                      Unit,\n//                    ] {\n//                      (nftState.currentOwner, nftState.tokenId).toBytes.bits\n//                    }\n//                    .runA(ms.token.nftBalanceState)\n//                    .map { stream =>\n//                      stream\n//                        .evalMap { case (keyBits, _) =>\n//                          for\n//                            txHash <- EitherT.fromEither {\n//                              keyBits.bytes\n//                                .to[\n//                                  (\n//                                      Account,\n//                                      TokenId,\n//                                      Hash.Value[TransactionWithResult],\n//                                  ),\n//                                ]\n//                                .map(_._3)\n//                                .leftMap(_.msg)\n//                            }\n//                            txOption <- TransactionRepository[F]\n//                              .get(txHash)\n//                              .leftMap(_.msg)\n//                            tx <- EitherT.fromOption(\n//                              txOption,\n//                              s\"No transaction $txHash found\",\n//                            )\n//                          yield tx\n//                        }\n//                        .filter(\n//                          _.signedTx.value.createdAt.compareTo(tx.timestamp) < 0,\n//                        )\n//                        .as(nftState)\n//                    }\n//                }.flatten\n//              }\n//          }\n//          .mapK(PlayNommDAppFailure.mapInternal(s\"Fail to get NFT states\"))\n//\n//      val getWeightSum = getNftStateStream.flatMapF { stream =>\n//        stream\n//          .map(_.weight)\n//          .fold(BigNat.Zero)(BigNat.add)\n//          .compile\n//          .toList\n//          .flatMap { list => EitherT.fromOption(list.headOption, \"empty list\") }\n//          .leftMap { e =>\n//            PlayNommDAppFailure.internal(s\"Fail to get weight sum: $e\")\n//          }\n//      }\n//\n//      for\n//        nftStream <- getNftStateStream\n//        weightSum <- getWeightSum\n//        _ <- StateT.modifyF[EitherT[F, PlayNommDAppFailure, *], MerkleState] {\n//          (ms: MerkleState) =>\n//            nftStream\n//              .evalScan(ms) { (ms, nftState) =>\n//                val ownershipSnapshot = OwnershipSnapshot(\n//                  account = nftState.currentOwner,\n//                  timestamp = tx.timestamp,\n//                  point = nftState.weight,\n//                  definitionId = nftState.tokenDefinitionId,\n//                  amount = tx.ownershipAmount * nftState.weight / weightSum,\n//                )\n//                PlayNommState[F].reward.ownershipSnapshot\n//                  .put(nftState.tokenId, ownershipSnapshot)\n//                  .transformS[MerkleState](\n//                    _.main,\n//                    (ms, mts) => (ms.copy(main = mts)),\n//                  )\n//                  .runS(ms)\n//              }\n//              .last\n//              .compile\n//              .toList\n//              .flatMap { list =>\n//                EitherT.fromOption(\n//                  list.headOption.flatten,\n//                  \"Fail to build final mekle state\",\n//                )\n//              }\n//              .leftMap(e => PlayNommDAppFailure.internal(e))\n//        }\n//      yield TransactionWithResult(Signed(sig, tx), None)\n\n    case tx: Transaction.RewardTx.ExecuteOwnershipReward => ???\n//      val getInputAmount\n//          : StateT[EitherT[F, PlayNommDAppFailure, *], MerkleState, BigNat] =\n//        StateT.liftF {\n//          tx.inputs.toList\n//            .traverse { txHash =>\n//              for\n//                txOption <- TransactionRepository[F].get(txHash).leftMap { e =>\n//                  PlayNommDAppFailure.internal(s\"fail to get tx $txHash\")\n//                }\n//                txWithResult <- EitherT.fromOption(\n//                  txOption,\n//                  PlayNommDAppFailure.external(s\"Tx input not found: $txHash\"),\n//                )\n//                amount <- txWithResult.signedTx.value match\n//                  case fb: Transaction.FungibleBalance =>\n//                    EitherT.pure {\n//                      fb match\n//                        case mf: Transaction.TokenTx.MintFungibleToken =>\n//                          mf.outputs.get(sig.account).getOrElse(BigNat.Zero)\n//                        case or: Transaction.TokenTx.TransferFungibleToken =>\n//                          or.outputs.get(sig.account).getOrElse(BigNat.Zero)\n//                        case bf: Transaction.TokenTx.BurnFungibleToken =>\n//                          txWithResult.result.fold(BigNat.Zero) {\n//                            case Transaction.TokenTx.BurnFungibleTokenResult(\n//                                  outputAmount,\n//                                ) =>\n//                              outputAmount\n//                            case _ => BigNat.Zero\n//                          }\n//                        case ef: Transaction.TokenTx.EntrustFungibleToken =>\n//                          txWithResult.result.fold(BigNat.Zero) {\n//                            case Transaction.TokenTx.EntrustFungibleTokenResult(\n//                                  remainder,\n//                                ) =>\n//                              remainder\n//                            case _ => BigNat.Zero\n//                          }\n//                        case de: Transaction.TokenTx.DisposeEntrustedFungibleToken =>\n//                          de.outputs.get(sig.account).getOrElse(BigNat.Zero)\n//                        case or: Transaction.RewardTx.OfferReward =>\n//                          or.outputs.get(sig.account).getOrElse(BigNat.Zero)\n//                        case xr: Transaction.RewardTx.ExecuteReward =>\n//                          txWithResult.result.fold(BigNat.Zero) {\n//                            case Transaction.RewardTx.ExecuteRewardResult(\n//                                  outputs,\n//                                ) =>\n//                              outputs.get(sig.account).getOrElse(BigNat.Zero)\n//                            case _ => BigNat.Zero\n//                          }\n//                        case xo: Transaction.RewardTx.ExecuteOwnershipReward =>\n//                          txWithResult.result.fold(BigNat.Zero) {\n//                            case Transaction.RewardTx\n//                                  .ExecuteOwnershipRewardResult(\n//                                    outputs,\n//                                  ) =>\n//                              outputs.get(sig.account).getOrElse(BigNat.Zero)\n//                            case _ => BigNat.Zero\n//                          }\n//                    }\n//                  case _ =>\n//                    EitherT.leftT(PlayNommDAppFailure.external {\n//                      s\"Tx input is not a fungible balance: $txHash\"\n//                    })\n//              yield amount\n//            }\n//            .map(_.foldLeft(BigNat.Zero)(BigNat.add))\n//        }\n//\n//      def updateBalance(\n//          outputs: Map[Account, BigNat],\n//          outputTxHash: Hash.Value[TransactionWithResult],\n//      ): StateT[EitherT[F, PlayNommDAppFailure, *], MerkleState, Unit] =\n//        val program =\n//          for\n//            _ <- tx.inputs.toList.traverse { inputTxHash =>\n//              GenericMerkleTrie\n//                .remove[\n//                  F,\n//                  (\n//                      Account,\n//                      TokenDefinitionId,\n//                      Hash.Value[TransactionWithResult],\n//                  ),\n//                  Unit,\n//                ] {\n//                  (sig.account, tx.definitionId, inputTxHash).toBytes.bits\n//                }\n//            }\n//            _ <- outputs.toList.traverse { (account, amount) =>\n//              GenericMerkleTrie.put[\n//                F,\n//                (Account, TokenDefinitionId, Hash.Value[TransactionWithResult]),\n//                Unit,\n//              ](\n//                (account, tx.definitionId, outputTxHash).toBytes.bits,\n//                (),\n//              )\n//            }\n//          yield ()\n//\n//        program\n//          .transformS[MerkleState](\n//            _.token.fungibleBalanceState,\n//            (ms, fbs) =>\n//              (ms.copy(token = ms.token.copy(fungibleBalanceState = fbs))),\n//          )\n//          .mapK(\n//            PlayNommDAppFailure.mapInternal(\"Fail to update fungible balance\"),\n//          )\n//\n//      def updateRewarded(txHash: Hash.Value[TransactionWithResult])(\n//          ownershipSnapshot: (TokenId, OwnershipSnapshot),\n//      ): StateT[EitherT[F, PlayNommDAppFailure, *], MerkleState, Unit] =\n//        val program =\n//          for _ <- PlayNommState[F].reward.ownershipRewarded.put(\n//              ownershipSnapshot._1,\n//              OwnershipRewardLog(ownershipSnapshot._2, txHash),\n//            )\n//          yield ()\n//\n//        program\n//          .mapK(\n//            PlayNommDAppFailure.mapInternal(\"Fail to update rewarded state\"),\n//          )\n//          .transformS[MerkleState](_.main, (ms, mts) => (ms.copy(main = mts)))\n//\n//      for\n//        snapshots <- tx.targets.toList\n//          .traverse { tokenId =>\n//            for\n//              ownershipSnapshotOption <-\n//                PlayNommState[F].reward.ownershipSnapshot\n//                  .get(tokenId)\n//                  .mapK(\n//                    PlayNommDAppFailure.mapInternal(\n//                      s\"Fail to get snapshot data of token ${tokenId}\",\n//                    ),\n//                  )\n//              ownershipSnapshot <- fromOption(\n//                ownershipSnapshotOption,\n//                s\"No ownership snapshot for token $tokenId\",\n//              )\n//            yield ownershipSnapshot\n//          }\n//          .transformS[MerkleState](_.main, (ms, mts) => (ms.copy(main = mts)))\n//        inputAmount <- getInputAmount\n//        outputSum = snapshots.map(_.amount).foldLeft(BigNat.Zero)(BigNat.add)\n//\n//        remainder <- StateT.liftF {\n//          EitherT\n//            .fromEither {\n//              BigNat.fromBigInt(outputSum.toBigInt - inputAmount.toBigInt)\n//            }\n//            .leftMap(PlayNommDAppFailure.external)\n//        }\n//        outputs = snapshots.map { snapshot =>\n//          snapshot.account -> snapshot.amount\n//        }.toMap + (sig.account -> remainder)\n//        result = Transaction.RewardTx.ExecuteOwnershipRewardResult(outputs)\n//        txWithResult = TransactionWithResult(Signed(sig, tx), Some(result))\n//        txHash       = txWithResult.toHash\n//        _ <- updateBalance(outputs, txHash)\n//        _ <- (tx.targets.toList zip snapshots).traverse(updateRewarded(txHash))\n//      yield txWithResult\n"
  },
  {
    "path": "modules/node/src/main/scala/io/leisuremeta/chain/node/dapp/submodule/PlayNommDAppToken.scala",
    "content": "package io.leisuremeta.chain\npackage node\npackage dapp\npackage submodule\n\nimport cats.Monad\nimport cats.data.{EitherT, StateT}\nimport cats.effect.Concurrent\nimport cats.syntax.all.*\n\nimport api.model.{\n  Account,\n  AccountSignature,\n  Signed,\n  Transaction,\n  TransactionWithResult,\n}\nimport api.model.TransactionWithResult.ops.*\nimport api.model.token.*\nimport api.model.token.SnapshotState.SnapshotId.*\nimport lib.codec.byte.ByteEncoder.ops.*\nimport lib.crypto.Hash\nimport lib.crypto.Hash.ops.*\nimport lib.datatype.BigNat\nimport lib.merkle.MerkleTrieState\nimport repository.TransactionRepository\n\nobject PlayNommDAppToken:\n  def apply[F[_]: Concurrent: PlayNommState: TransactionRepository](\n      tx: Transaction.TokenTx,\n      sig: AccountSignature,\n  ): StateT[\n    EitherT[F, PlayNommDAppFailure, *],\n    MerkleTrieState,\n    TransactionWithResult,\n  ] = tx match\n    case dt: Transaction.TokenTx.DefineToken =>\n      for\n        _              <- PlayNommDAppAccount.verifySignature(sig, tx)\n        tokenDefOption <- getTokenDefinitionOption(dt.definitionId)\n        _ <- checkExternal(\n          tokenDefOption.isEmpty,\n          s\"Token ${dt.definitionId} is already defined\",\n        )\n        tokenDefinition = TokenDefinition(\n          id = dt.definitionId,\n          name = dt.name,\n          symbol = dt.symbol,\n          adminGroup = dt.minterGroup,\n          totalAmount = BigNat.Zero,\n          nftInfo = dt.nftInfo.map(NftInfoWithPrecision.fromNftInfo),\n        )\n        _ <- putTokenDefinition(dt.definitionId, tokenDefinition)\n      yield TransactionWithResult(Signed(sig, dt))(None)\n\n    case dp: Transaction.TokenTx.DefineTokenWithPrecision =>\n      for\n        _              <- PlayNommDAppAccount.verifySignature(sig, tx)\n        tokenDefOption <- getTokenDefinitionOption(dp.definitionId)\n        _ <- checkExternal(\n          tokenDefOption.isEmpty,\n          s\"Token ${dp.definitionId} is already defined\",\n        )\n        tokenDefinition = TokenDefinition(\n          id = dp.definitionId,\n          name = dp.name,\n          symbol = dp.symbol,\n          adminGroup = dp.minterGroup,\n          totalAmount = BigNat.Zero,\n          nftInfo = dp.nftInfo,\n        )\n        _ <- putTokenDefinition(dp.definitionId, tokenDefinition)\n      yield TransactionWithResult(Signed(sig, dp))(None)\n\n    case mf: Transaction.TokenTx.MintFungibleToken =>\n      for\n        _ <- PlayNommDAppAccount.verifySignature(sig, tx)\n        tokenDef <- checkMinterAndGetTokenDefinition(\n          sig.account,\n          mf.definitionId,\n        )\n        txWithResult = TransactionWithResult(Signed(sig, mf))(None)\n        txHash       = txWithResult.toHash\n        _ <- mf.outputs.toSeq.traverse: (account, amount) =>\n          val partProgram = for\n            _ <- PlayNommState[F].token.fungibleBalance\n              .put((account, mf.definitionId, txHash), ())\n            _ <- addFungibleSnapshot(account, mf.definitionId, txHash, amount)\n          yield ()\n          partProgram.mapK:\n            PlayNommDAppFailure.mapInternal:\n              s\"Fail to put token balance ($account, ${mf.definitionId}, $txHash)\"\n        mintAmount  = mf.outputs.values.foldLeft(BigNat.Zero)(BigNat.add)\n        totalAmount = BigNat.add(tokenDef.totalAmount, mintAmount)\n        _ <- putTokenDefinition(\n          mf.definitionId,\n          tokenDef.copy(totalAmount = totalAmount),\n        )\n        _ <- addTotalSupplySnapshot(mf.definitionId, totalAmount)\n          .mapK:\n            PlayNommDAppFailure.mapInternal:\n              s\"Fail to add total supply snapshot of ${mf.definitionId}\"\n      yield txWithResult\n\n    case mn: Transaction.TokenTx.MintNFT =>\n      for\n        _ <- PlayNommDAppAccount.verifySignature(sig, tx)\n        tokenDef <- checkMinterAndGetTokenDefinition(\n          sig.account,\n          mn.tokenDefinitionId,\n        )\n        nftStateOption <- PlayNommState[F].token.nftState\n          .get(mn.tokenId)\n          .mapK:\n            PlayNommDAppFailure.mapInternal:\n              s\"Fail to get nft state of ${mn.tokenId}\"\n        _ <- checkExternal(\n          nftStateOption.isEmpty,\n          s\"NFT ${mn.tokenId} is already minted\",\n        )\n        txWithResult = TransactionWithResult(Signed(sig, mn))(None)\n        txHash       = txWithResult.toHash\n        _ <- PlayNommState[F].token.nftBalance\n          .put((mn.output, mn.tokenId, txHash), ())\n          .mapK:\n            PlayNommDAppFailure.mapInternal:\n              s\"Fail to put token balance (${mn.output}, ${mn.tokenId}, $txHash)\"\n        _ <- addNftSnapshot(mn.output, mn.tokenDefinitionId, mn.tokenId)\n          .mapK:\n            PlayNommDAppFailure.mapInternal:\n              s\"Fail to add nft snapshot of ${mn.tokenId}\"\n        weight = tokenDef.nftInfo.flatMap(_.rarity.get(mn.rarity))\n          .getOrElse(BigNat.unsafeFromLong(2L))\n        nftState = NftState(\n          tokenId = mn.tokenId,\n          tokenDefinitionId = mn.tokenDefinitionId,\n          rarity = mn.rarity,\n          weight = weight,\n          currentOwner = mn.output,\n          memo = None,\n          lastUpdateTx = txHash,\n          previousState = None,\n        )\n        _ <- PlayNommState[F].token.nftState\n          .put(mn.tokenId, nftState)\n          .mapK:\n            PlayNommDAppFailure.mapInternal:\n              s\"Fail to put nft state of ${mn.tokenId}\"\n        _ <- PlayNommState[F].token.nftHistory\n          .put(txHash, nftState)\n          .mapK:\n            PlayNommDAppFailure.mapInternal:\n              s\"Fail to put nft history of ${mn.tokenId} of $txHash\"\n        _ <- PlayNommState[F].token.rarityState\n          .put((mn.tokenDefinitionId, mn.rarity, mn.tokenId), ())\n          .mapK:\n            PlayNommDAppFailure.mapInternal:\n              s\"Fail to put rarity state of ${mn.tokenDefinitionId}, ${mn.rarity}, ${mn.tokenId}\"\n      yield txWithResult\n\n    case mn: Transaction.TokenTx.MintNFTWithMemo =>\n      for\n        _ <- PlayNommDAppAccount.verifySignature(sig, tx)\n        tokenDef <- checkMinterAndGetTokenDefinition(\n          sig.account,\n          mn.tokenDefinitionId,\n        )\n        nftStateOption <- PlayNommState[F].token.nftState\n          .get(mn.tokenId)\n          .mapK:\n            PlayNommDAppFailure.mapInternal:\n              s\"Fail to get nft state of ${mn.tokenId}\"\n        _ <- checkExternal(\n          nftStateOption.isEmpty,\n          s\"NFT ${mn.tokenId} is already minted\",\n        )\n        txWithResult = TransactionWithResult(Signed(sig, mn))(None)\n        txHash       = txWithResult.toHash\n        _ <- PlayNommState[F].token.nftBalance\n          .put((mn.output, mn.tokenId, txHash), ())\n          .mapK:\n            PlayNommDAppFailure.mapInternal:\n              s\"Fail to put token balance (${mn.output}, ${mn.tokenId}, $txHash)\"\n        _ <- addNftSnapshot(mn.output, mn.tokenDefinitionId, mn.tokenId)\n          .mapK:\n            PlayNommDAppFailure.mapInternal:\n              s\"Fail to add nft snapshot of ${mn.tokenId}\"\n        weight = tokenDef.nftInfo.get.rarity\n          .getOrElse(mn.rarity, BigNat.unsafeFromLong(2L))\n        nftState = NftState(\n          tokenId = mn.tokenId,\n          tokenDefinitionId = mn.tokenDefinitionId,\n          rarity = mn.rarity,\n          weight = weight,\n          currentOwner = mn.output,\n          memo = mn.memo,\n          lastUpdateTx = txHash,\n          previousState = None,\n        )\n        _ <- PlayNommState[F].token.nftState\n          .put(mn.tokenId, nftState)\n          .mapK:\n            PlayNommDAppFailure.mapInternal:\n              s\"Fail to put nft state of ${mn.tokenId}\"\n        _ <- PlayNommState[F].token.nftHistory\n          .put(txHash, nftState)\n          .mapK:\n            PlayNommDAppFailure.mapInternal:\n              s\"Fail to put nft history of ${mn.tokenId} of $txHash\"\n        _ <- PlayNommState[F].token.rarityState\n          .put((mn.tokenDefinitionId, mn.rarity, mn.tokenId), ())\n          .mapK:\n            PlayNommDAppFailure.mapInternal:\n              s\"Fail to put rarity state of ${mn.tokenDefinitionId}, ${mn.rarity}, ${mn.tokenId}\"\n      yield txWithResult\n\n    case un: Transaction.TokenTx.UpdateNFT =>\n      for\n        _ <- PlayNommDAppAccount.verifySignature(sig, tx)\n        tokenDef <- checkMinterAndGetTokenDefinition(\n          sig.account,\n          un.tokenDefinitionId,\n        )\n        nftStateOption <- PlayNommState[F].token.nftState\n          .get(un.tokenId)\n          .mapK:\n            PlayNommDAppFailure.mapInternal:\n              s\"Fail to get nft state of ${un.tokenId}\"\n        nftState <- fromOption(\n          nftStateOption,\n          s\"Empty NFT State: ${un.tokenId}\",\n        )\n        txWithResult = TransactionWithResult(Signed(sig, un))(None)\n        txHash       = txWithResult.toHash\n        weight = tokenDef.nftInfo.get.rarity\n          .getOrElse(un.rarity, BigNat.unsafeFromLong(2L))\n        nftState1 = NftState(\n          tokenId = un.tokenId,\n          tokenDefinitionId = un.tokenDefinitionId,\n          rarity = un.rarity,\n          weight = weight,\n          currentOwner = un.output,\n          memo = un.memo,\n          lastUpdateTx = txHash,\n          previousState = Some(nftState.lastUpdateTx),\n        )\n        _ <- PlayNommState[F].token.nftState\n          .put(un.tokenId, nftState1)\n          .mapK:\n            PlayNommDAppFailure.mapInternal:\n              s\"Fail to put nft state of ${un.tokenId}\"\n        _ <- PlayNommState[F].token.nftHistory\n          .put(txHash, nftState1)\n          .mapK:\n            PlayNommDAppFailure.mapInternal:\n              s\"Fail to put nft history of ${un.tokenId} of $txHash\"\n      yield txWithResult\n\n    case tf: Transaction.TokenTx.TransferFungibleToken =>\n      for\n        _        <- PlayNommDAppAccount.verifySignature(sig, tx)\n        tokenDef <- getTokenDefinition(tf.tokenDefinitionId)\n        inputAmount <- getFungibleBalanceTotalAmounts(\n          tf.inputs.map(_.toResultHashValue),\n          sig.account,\n        )\n        outputAmount = tf.outputs.values.foldLeft(BigNat.Zero)(BigNat.add)\n        diff <- fromEitherExternal:\n          BigNat.tryToSubtract(inputAmount, outputAmount)\n        txWithResult = TransactionWithResult(Signed(sig, tf))(None)\n        txHash       = txWithResult.toHash\n        _ <- removeInputUtxos(\n          sig.account,\n          tf.inputs.map(_.toResultHashValue),\n          tf.tokenDefinitionId,\n        )\n        _ <- tf.inputs.toList.traverse: inputTxHash =>\n          removeFungibleSnapshot(\n            sig.account,\n            tf.tokenDefinitionId,\n            inputTxHash.toResultHashValue,\n          )\n            .mapK:\n              PlayNommDAppFailure.mapInternal:\n                s\"Fail to remove fungible snapshot of $inputTxHash\"\n        _ <- tf.outputs.toSeq.traverse:\n          case (account, outputAmount) =>\n            putBalance(account, tf.tokenDefinitionId, txHash) *>\n              addFungibleSnapshot(\n                account,\n                tf.tokenDefinitionId,\n                txHash,\n                outputAmount,\n              )\n                .mapK:\n                  PlayNommDAppFailure.mapInternal:\n                    s\"Fail to add fungible snapshot of $account\"\n        totalAmount <- fromEitherInternal:\n          BigNat.tryToSubtract(tokenDef.totalAmount, diff)\n        _ <- putTokenDefinition(\n          tf.tokenDefinitionId,\n          tokenDef.copy(totalAmount = totalAmount),\n        )\n        _ <- removeTotalSupplySnapshot(tf.tokenDefinitionId, diff)\n          .mapK:\n            PlayNommDAppFailure.mapInternal:\n              s\"Fail to remove total supply snapshot of ${tf.tokenDefinitionId}\"\n      yield txWithResult\n\n    case tn: Transaction.TokenTx.TransferNFT =>\n      for\n        _ <- PlayNommDAppAccount.verifySignature(sig, tx)\n        txWithResult = TransactionWithResult(Signed(sig, tn))(None)\n        txHash       = txWithResult.toHash\n        utxoKey      = (sig.account, tn.tokenId, tn.input.toResultHashValue)\n        isRemoveSuccessful <- PlayNommState[F].token.nftBalance\n          .remove(utxoKey)\n          .mapK:\n            PlayNommDAppFailure.mapInternal:\n              s\"Fail to remove nft balance of $utxoKey\"\n        _ <- checkExternal(isRemoveSuccessful, s\"No NFT Balance: ${utxoKey}\")\n        newUtxoKey = (tn.output, tn.tokenId, txHash)\n        _ <- PlayNommState[F].token.nftBalance\n          .put(newUtxoKey, ())\n          .mapK:\n            PlayNommDAppFailure.mapInternal:\n              s\"Fail to put nft balance of $newUtxoKey\"\n        nftStateOption <- PlayNommState[F].token.nftState\n          .get(tn.tokenId)\n          .mapK:\n            PlayNommDAppFailure.mapInternal:\n              s\"Fail to get nft state of ${tn.tokenId}\"\n        nftState <- fromOption(\n          nftStateOption,\n          s\"Empty NFT State: ${tn.tokenId}\",\n        )\n        nftState1 = nftState.copy(currentOwner = tn.output)\n        _ <- PlayNommState[F].token.nftState\n          .put(tn.tokenId, nftState1)\n          .mapK:\n            PlayNommDAppFailure.mapInternal:\n              s\"Fail to put nft state of ${tn.tokenId}\"\n        _ <- removeNftSnapshot[F](sig.account, tn.definitionId, tn.tokenId)\n          .mapK:\n            PlayNommDAppFailure.mapInternal:\n              s\"Fail to remove nft snapshot of ${tn.tokenId}\"\n        _ <- addNftSnapshot[F](tn.output, tn.definitionId, tn.tokenId)\n          .mapK:\n            PlayNommDAppFailure.mapInternal:\n              s\"Fail to add nft snapshot of ${tn.tokenId}\"\n      yield txWithResult\n\n    case bf: Transaction.TokenTx.BurnFungibleToken =>\n      for\n        _ <- PlayNommDAppAccount.verifySignature(sig, tx)\n        tokenDef <- checkMinterAndGetTokenDefinition(\n          sig.account,\n          bf.definitionId,\n        )\n        inputAmount <- getFungibleBalanceTotalAmounts(\n          bf.inputs.map(_.toResultHashValue),\n          sig.account,\n        )\n        outputAmount <- fromEitherExternal:\n          BigNat.tryToSubtract(inputAmount, bf.amount)\n        result       = Transaction.TokenTx.BurnFungibleTokenResult(outputAmount)\n        txWithResult = TransactionWithResult(Signed(sig, bf))(Some(result))\n        txHash       = txWithResult.toHash\n        _ <- removeInputUtxos(\n          sig.account,\n          bf.inputs.map(_.toResultHashValue),\n          bf.definitionId,\n        )\n        _ <- bf.inputs.toList.traverse: inputTxHash =>\n          removeFungibleSnapshot(\n            sig.account,\n            bf.definitionId,\n            inputTxHash.toResultHashValue,\n          )\n            .mapK:\n              PlayNommDAppFailure.mapInternal:\n                s\"Fail to remove fungible snapshot of $inputTxHash\"\n        _ <-\n          if outputAmount === BigNat.Zero then unit\n          else\n            putBalance(sig.account, bf.definitionId, txWithResult.toHash) *>\n              addFungibleSnapshot(\n                sig.account,\n                bf.definitionId,\n                txWithResult.toHash,\n                outputAmount,\n              )\n                .mapK:\n                  PlayNommDAppFailure.mapInternal:\n                    s\"Fail to add fungible snapshot of ${sig.account}\"\n        totalAmount <- fromEitherInternal:\n          BigNat.tryToSubtract(tokenDef.totalAmount, bf.amount)\n        _ <- putTokenDefinition(\n          bf.definitionId,\n          tokenDef.copy(totalAmount = totalAmount),\n        )\n        _ <- removeTotalSupplySnapshot(bf.definitionId, bf.amount)\n          .mapK:\n            PlayNommDAppFailure.mapInternal:\n              s\"Fail to remove total supply snapshot of ${bf.definitionId}\"\n      yield txWithResult\n\n    case bn: Transaction.TokenTx.BurnNFT =>\n      for\n        _       <- PlayNommDAppAccount.verifySignature(sig, tx)\n        tokenId <- getNftTokenId(bn.input.toResultHashValue)\n        txWithResult = TransactionWithResult(Signed(sig, bn))(None)\n        txHash       = txWithResult.toHash\n        utxoKey      = (sig.account, tokenId, bn.input.toResultHashValue)\n        isRemoveSuccessful <- PlayNommState[F].token.nftBalance\n          .remove(utxoKey)\n          .mapK:\n            PlayNommDAppFailure.mapInternal:\n              s\"Fail to remove nft balance of $utxoKey\"\n        _ <- checkExternal(isRemoveSuccessful, s\"No NFT Balance: ${utxoKey}\")\n        nftStateOption <- PlayNommState[F].token.nftState\n          .get(tokenId)\n          .mapK:\n            PlayNommDAppFailure.mapInternal:\n              s\"Fail to get nft state of ${tokenId}\"\n        _ <- checkInternal(\n          nftStateOption.isDefined,\n          s\"Empty NFT State: ${tokenId}\",\n        )\n        nftState <- fromOption(\n          nftStateOption,\n          s\"Empty NFT State: ${tokenId}\",\n        )\n        _ <- PlayNommState[F].token.nftState\n          .remove(tokenId)\n          .mapK:\n            PlayNommDAppFailure.mapInternal:\n              s\"Fail to remove nft state of ${tokenId}\"\n        _ <- PlayNommState[F].token.rarityState\n          .remove((bn.definitionId, nftState.rarity, tokenId))\n          .mapK:\n            PlayNommDAppFailure.mapInternal:\n              s\"Fail to remove rarity state of ${tokenId}\"\n        _ <- removeNftSnapshot[F](sig.account, bn.definitionId, tokenId)\n          .mapK:\n            PlayNommDAppFailure.mapInternal:\n              s\"Fail to remove nft snapshot of ${tokenId}\"\n      yield txWithResult\n\n    case ef: Transaction.TokenTx.EntrustFungibleToken =>\n      for\n        _        <- PlayNommDAppAccount.verifySignature(sig, tx)\n        tokenDef <- getTokenDefinition(ef.definitionId)\n        inputAmount <- getFungibleBalanceTotalAmounts(\n          ef.inputs.map(_.toResultHashValue),\n          sig.account,\n        )\n        diff <- fromEitherExternal:\n          BigNat.tryToSubtract(inputAmount, ef.amount)\n        result       = Transaction.TokenTx.EntrustFungibleTokenResult(diff)\n        txWithResult = TransactionWithResult(Signed(sig, ef))(Some(result))\n        txHash       = txWithResult.toHash\n        _ <- removeInputUtxos(\n          sig.account,\n          ef.inputs.map(_.toResultHashValue),\n          ef.definitionId,\n        )\n        _ <- PlayNommState[F].token.entrustFungibleBalance\n          .put((sig.account, ef.to, ef.definitionId, txHash), ())\n          .mapK:\n            PlayNommDAppFailure.mapInternal:\n              s\"Fail to put entrust fungible balance of (${sig.account}, ${ef.to}, ${ef.definitionId}, ${txHash})\"\n        _ <- putBalance(sig.account, ef.definitionId, txHash)\n      yield txWithResult\n\n    case de: Transaction.TokenTx.DisposeEntrustedFungibleToken =>\n      for\n        _        <- PlayNommDAppAccount.verifySignature(sig, tx)\n        tokenDef <- getTokenDefinition(de.definitionId)\n        inputHashList = de.inputs.map(_.toResultHashValue)\n        inputMap <- getEntrustedInputs(inputHashList, sig.account)\n        inputAmount  = inputMap.values.foldLeft(BigNat.Zero)(BigNat.add)\n        outputAmount = de.outputs.values.foldLeft(BigNat.Zero)(BigNat.add)\n        _ <- checkExternal(\n          inputAmount === outputAmount,\n          s\"Output amount is not equal to input amount $inputAmount\",\n        )\n        txWithResult = TransactionWithResult(Signed(sig, de))(None)\n        txHash       = txWithResult.toHash\n        _ <- inputMap.toList\n          .map(_._1)\n          .traverse: (account, txHash) =>\n            PlayNommState[F].token.entrustFungibleBalance\n              .remove((account, sig.account, de.definitionId, txHash))\n              .mapK:\n                PlayNommDAppFailure.mapInternal:\n                  s\"Fail to remove entrust fungible balance of (${account}, ${sig.account}, ${de.definitionId}, ${txHash})\"\n              *> removeFungibleSnapshot[F](account, de.definitionId, txHash)\n                .mapK:\n                  PlayNommDAppFailure.mapInternal:\n                    s\"Fail to remove fungible snapshot of $txHash\"\n        _ <- de.outputs.toList.traverse: (account, amount) =>\n          PlayNommState[F].token.fungibleBalance\n            .put((account, de.definitionId, txHash), ())\n            .mapK:\n              PlayNommDAppFailure.mapInternal:\n                s\"Fail to put fungible balance of (${account}, ${de.definitionId}, ${txHash})\"\n            *> addFungibleSnapshot[F](account, de.definitionId, txHash, amount)\n              .mapK:\n                PlayNommDAppFailure.mapInternal:\n                  s\"Fail to put fungible snapshot of $txHash\"\n      yield txWithResult\n\n    case ef: Transaction.TokenTx.EntrustNFT =>\n      for\n        _ <- PlayNommDAppAccount.verifySignature(sig, tx)\n        txWithResult = TransactionWithResult(Signed(sig, ef))(None)\n        txHash       = txWithResult.toHash\n        utxoKey      = (sig.account, ef.tokenId, ef.input.toResultHashValue)\n        isRemoveSuccessful <- PlayNommState[F].token.nftBalance\n          .remove(utxoKey)\n          .mapK:\n            PlayNommDAppFailure.mapInternal:\n              s\"Fail to remove nft balance of $utxoKey\"\n        _ <- checkExternal(isRemoveSuccessful, s\"No NFT Balance: ${utxoKey}\")\n        newUtxoKey = (sig.account, ef.to, ef.tokenId, txHash)\n        _ <- PlayNommState[F].token.entrustNftBalance\n          .put(newUtxoKey, ())\n          .mapK:\n            PlayNommDAppFailure.mapInternal:\n              s\"Fail to put entrust nft balance of $newUtxoKey\"\n      yield txWithResult\n\n    case de: Transaction.TokenTx.DisposeEntrustedNFT =>\n      for\n        _            <- PlayNommDAppAccount.verifySignature(sig, tx)\n        entrustedNFT <- getEntrustedNFT(de.input.toResultHashValue, sig.account)\n        (fromAccount, entrustTx) = entrustedNFT\n        txWithResult             = TransactionWithResult(Signed(sig, de))(None)\n        txHash                   = txWithResult.toHash\n        utxoKey = (\n          fromAccount,\n          sig.account,\n          de.tokenId,\n          de.input.toResultHashValue,\n        )\n        isRemoveSuccessful <- PlayNommState[F].token.entrustNftBalance\n          .remove(utxoKey)\n          .mapK:\n            PlayNommDAppFailure.mapInternal:\n              s\"Fail to remove entrust nft balance of $utxoKey\"\n        _ <- checkExternal(\n          isRemoveSuccessful,\n          s\"No Entrust NFT Balance: ${utxoKey}\",\n        )\n        _ <- removeNftSnapshot[F](fromAccount, de.definitionId, de.tokenId)\n          .mapK:\n            PlayNommDAppFailure.mapInternal:\n              s\"Fail to remove nft snapshot of ${de.tokenId}\"\n        newUtxoKey = (de.output.getOrElse(fromAccount), de.tokenId, txHash)\n        _ <- PlayNommState[F].token.nftBalance\n          .put(newUtxoKey, ())\n          .mapK:\n            PlayNommDAppFailure.mapInternal:\n              s\"Fail to put nft balance of $newUtxoKey\"\n        _ <- addNftSnapshot[F](\n          de.output.getOrElse(fromAccount),\n          de.definitionId,\n          de.tokenId,\n        )\n          .mapK:\n            PlayNommDAppFailure.mapInternal:\n              s\"Fail to add nft snapshot of ${de.tokenId}\"\n        _ <- de.output.fold(unit): toAddress =>\n          for\n            nftStateOption <- PlayNommState[F].token.nftState\n              .get(de.tokenId)\n              .mapK:\n                PlayNommDAppFailure.mapInternal:\n                  s\"Fail to get nft state of ${de.tokenId}\"\n            nftState <- fromOption(\n              nftStateOption,\n              s\"Empty NFT State: ${de.tokenId}\",\n            )\n            nftState1 = nftState.copy(currentOwner = toAddress)\n            _ <- PlayNommState[F].token.nftState\n              .put(de.tokenId, nftState1)\n              .mapK:\n                PlayNommDAppFailure.mapInternal:\n                  s\"Fail to put nft state of ${de.tokenId}\"\n          yield ()\n      yield txWithResult\n\n    case cs: Transaction.TokenTx.CreateSnapshots =>\n      for\n        _ <- PlayNommDAppAccount.verifySignature(sig, tx)\n        txWithResult = TransactionWithResult(Signed(sig, cs))(None)\n        _ <- cs.definitionIds.toList.traverse: definitionId =>\n          for\n            tokenDefOption <- getTokenDefinitionOption(definitionId)\n            tokenDef <- fromOption(\n              tokenDefOption,\n              s\"Token ${definitionId} is not defined\",\n            )\n            adminGroupId <- fromOption(\n              tokenDef.adminGroup,\n              s\"No admin group in token ${definitionId}\",\n            )\n            _ <- PlayNommState[F].group.groupAccount\n              .get((adminGroupId, sig.account))\n              .mapK:\n                PlayNommDAppFailure.mapExternal:\n                  s\"Not in admin group ${adminGroupId} of ${sig.account}\"\n            snapshotStateOption <- PlayNommState[F].token.snapshotState\n              .get(definitionId)\n              .mapK:\n                PlayNommDAppFailure.mapInternal:\n                  s\"Fail to get snapshot state of ${definitionId}\"\n            lastSnapshotId = snapshotStateOption\n              .fold(SnapshotState.SnapshotId.Zero)(_.snapshotId)\n            snapshotState = SnapshotState(\n              snapshotId = lastSnapshotId.increase,\n              createdAt = cs.createdAt,\n              txHash = txWithResult.toHash.toSignedTxHash,\n              memo = cs.memo,\n            )\n            _ <- PlayNommState[F].token.snapshotState\n              .put(definitionId, snapshotState)\n              .mapK:\n                PlayNommDAppFailure.mapInternal:\n                  s\"Fail to put snapshot state of ${definitionId}\"\n          yield ()\n      yield txWithResult\n\n  def getTokenDefinitionOption[F[_]: Monad: PlayNommState](\n      definitionId: TokenDefinitionId,\n  ): StateT[EitherT[F, PlayNommDAppFailure, *], MerkleTrieState, Option[\n    TokenDefinition,\n  ]] =\n    PlayNommState[F].token.definition\n      .get(definitionId)\n      .mapK:\n        PlayNommDAppFailure.mapInternal:\n          s\"Fail to get token definition of ${definitionId}\"\n\n  def getTokenDefinition[F[_]: Monad: PlayNommState](\n      definitionId: TokenDefinitionId,\n  ): StateT[\n    EitherT[F, PlayNommDAppFailure, *],\n    MerkleTrieState,\n    TokenDefinition,\n  ] =\n    for\n      tokenDefOption <- getTokenDefinitionOption(definitionId)\n      tokenDef <- fromOption(\n        tokenDefOption,\n        s\"Token $definitionId is not defined\",\n      )\n    yield tokenDef\n\n  def putTokenDefinition[F[_]: Monad: PlayNommState](\n      definitionId: TokenDefinitionId,\n      definition: TokenDefinition,\n  ): StateT[EitherT[F, PlayNommDAppFailure, *], MerkleTrieState, Unit] =\n    PlayNommState[F].token.definition\n      .put(definitionId, definition)\n      .mapK(PlayNommDAppFailure.mapInternal {\n        s\"Fail to set token definition of $definitionId\"\n      })\n\n  def checkMinterAndGetTokenDefinition[F[_]: Monad: PlayNommState](\n      account: Account,\n      definitionId: TokenDefinitionId,\n  ): StateT[\n    EitherT[F, PlayNommDAppFailure, *],\n    MerkleTrieState,\n    TokenDefinition,\n  ] =\n    for\n      tokenDef <- getTokenDefinition(definitionId)\n      minterGroup <- fromOption(\n        tokenDef.adminGroup,\n        s\"Token $definitionId does not have a minter group\",\n      )\n      groupAccountInfoOption <- PlayNommState[F].group.groupAccount\n        .get((minterGroup, account))\n        .mapK:\n          PlayNommDAppFailure.mapInternal:\n            s\"Fail to get group account for ($minterGroup, $account)\"\n      _ <- checkExternal(\n        groupAccountInfoOption.nonEmpty,\n        s\"Account $account is not a member of minter group $minterGroup\",\n      )\n    yield tokenDef\n\n  def getFungibleBalanceTotalAmounts[F[_]\n    : Monad: TransactionRepository: PlayNommState](\n      inputs: Set[Hash.Value[TransactionWithResult]],\n      account: Account,\n  ): StateT[EitherT[F, PlayNommDAppFailure, *], MerkleTrieState, BigNat] =\n    StateT.liftF:\n      inputs.toSeq\n        .traverse: txHash =>\n          for\n            txOption <- TransactionRepository[F]\n              .get(txHash)\n              .leftMap: e =>\n                PlayNommDAppFailure.internal(\n                  s\"Fail to get tx $txHash: ${e.msg}\",\n                )\n            txWithResult <- EitherT.fromOption(\n              txOption,\n              PlayNommDAppFailure.internal(s\"There is no tx of $txHash\"),\n            )\n          yield\n            val amount = tokenBalanceAmount(account)(txWithResult)\n//            scribe.info(s\"Amount of ${txHash.toUInt256Bytes.toHex}: $amount\")\n            amount\n        .map { _.foldLeft(BigNat.Zero)(BigNat.add) }\n\n  def removeInputUtxos[F[_]: Monad: PlayNommState](\n      account: Account,\n      inputs: Set[Hash.Value[TransactionWithResult]],\n      definitionId: TokenDefinitionId,\n  ): StateT[EitherT[F, PlayNommDAppFailure, *], MerkleTrieState, List[\n    Hash.Value[TransactionWithResult],\n  ]] =\n    val inputList = inputs.toList\n    for\n      removeResults <- inputList.traverse { txHash =>\n        PlayNommState[F].token.fungibleBalance\n          .remove((account, definitionId, txHash))\n          .mapK(PlayNommDAppFailure.mapInternal {\n            s\"Fail to remove fingible balance ($account, $definitionId, $txHash)\"\n          })\n      }\n      invalidUtxos = inputList.zip(removeResults).filterNot(_._2).map(_._1)\n      _ <- checkExternal(\n        invalidUtxos.isEmpty,\n        s\"These utxos are invalid: $invalidUtxos\",\n      )\n    yield invalidUtxos\n\n  def tokenBalanceAmount(account: Account)(\n      txWithResult: TransactionWithResult,\n  ): BigNat = txWithResult.signedTx.value match\n    case tb: Transaction.FungibleBalance =>\n      tb match\n        case mt: Transaction.TokenTx.MintFungibleToken =>\n          mt.outputs.getOrElse(account, BigNat.Zero)\n        case bt: Transaction.TokenTx.BurnFungibleToken =>\n          txWithResult.result.fold(BigNat.Zero):\n            case Transaction.TokenTx.BurnFungibleTokenResult(amount)\n                if txWithResult.signedTx.sig.account === account =>\n              amount\n            case _ =>\n              scribe.error:\n                s\"Fail to get burn token result: $txWithResult\"\n              BigNat.Zero\n        case tt: Transaction.TokenTx.TransferFungibleToken =>\n          tt.outputs.getOrElse(account, BigNat.Zero)\n        case ef: Transaction.TokenTx.EntrustFungibleToken =>\n          txWithResult.result.fold(BigNat.Zero):\n            case Transaction.TokenTx.EntrustFungibleTokenResult(remainder) =>\n              remainder\n            case _ => BigNat.Zero\n        case df: Transaction.TokenTx.DisposeEntrustedFungibleToken =>\n          df.outputs.getOrElse(account, BigNat.Zero)\n        case or: Transaction.RewardTx.OfferReward =>\n          or.outputs.getOrElse(account, BigNat.Zero)\n        case xr: Transaction.RewardTx.ExecuteReward =>\n          txWithResult.result.fold(BigNat.Zero):\n            case Transaction.RewardTx.ExecuteRewardResult(outputs) =>\n              outputs.getOrElse(account, BigNat.Zero)\n            case _ => BigNat.Zero\n        case xo: Transaction.RewardTx.ExecuteOwnershipReward =>\n          txWithResult.result.fold(BigNat.Zero):\n            case Transaction.RewardTx.ExecuteOwnershipRewardResult(outputs) =>\n              outputs.getOrElse(account, BigNat.Zero)\n            case _ => BigNat.Zero\n    case _ =>\n      scribe.error(s\"Not a fungible balance: $txWithResult\")\n      BigNat.Zero\n\n  def putBalance[F[_]: Monad: PlayNommState](\n      account: Account,\n      definitionId: TokenDefinitionId,\n      txHash: Hash.Value[TransactionWithResult],\n  ): StateT[EitherT[F, PlayNommDAppFailure, *], MerkleTrieState, Unit] =\n    PlayNommState[F].token.fungibleBalance\n      .put((account, definitionId, txHash), ())\n      .mapK:\n        PlayNommDAppFailure.mapInternal:\n          s\"Fail to put token balance ($account, $definitionId, $txHash)\"\n\n  def getNftTokenId[F[_]: Monad: TransactionRepository](\n      utxoHash: Hash.Value[TransactionWithResult],\n  ): StateT[EitherT[F, PlayNommDAppFailure, *], MerkleTrieState, TokenId] =\n    StateT.liftF:\n      TransactionRepository[F]\n        .get(utxoHash)\n        .leftMap(e => PlayNommDAppFailure.internal(s\"Fail to get tx: ${e.msg}\"))\n        .subflatMap: txOption =>\n          Either.fromOption(\n            txOption,\n            PlayNommDAppFailure.internal(s\"There is no tx of $utxoHash\"),\n          )\n        .flatMap: txWithResult =>\n          txWithResult.signedTx.value match\n            case nb: Transaction.NftBalance =>\n              EitherT.pure:\n                nb match\n                  case mn: Transaction.TokenTx.MintNFT          => mn.tokenId\n                  case mnm: Transaction.TokenTx.MintNFTWithMemo => mnm.tokenId\n                  case tn: Transaction.TokenTx.TransferNFT      => tn.tokenId\n                  case den: Transaction.TokenTx.DisposeEntrustedNFT =>\n                    den.tokenId\n            case _ =>\n              EitherT.leftT:\n                PlayNommDAppFailure.external:\n                  s\"Tx $txWithResult is not a nft balance\"\n\n  def getEntrustedInputs[F[_]: Monad: TransactionRepository](\n      inputs: Set[Hash.Value[TransactionWithResult]],\n      account: Account,\n  ): StateT[EitherT[F, PlayNommDAppFailure, *], MerkleTrieState, Map[\n    (Account, Hash.Value[TransactionWithResult]),\n    BigNat,\n  ]] =\n    StateT.liftF:\n      inputs.toSeq\n        .traverse: txHash =>\n          for\n            txOption <- TransactionRepository[F]\n              .get(txHash)\n              .leftMap: e =>\n                PlayNommDAppFailure.internal:\n                  s\"Fail to get tx $txHash: ${e.msg}\"\n            txWithResult <- EitherT.fromOption(\n              txOption,\n              PlayNommDAppFailure.internal(s\"There is no tx of $txHash\"),\n            )\n            amount <- txWithResult.signedTx.value match\n              case ef: Transaction.TokenTx.EntrustFungibleToken =>\n                EitherT.cond(\n                  ef.to === account,\n                  ef.amount,\n                  PlayNommDAppFailure.external:\n                    s\"Entrust fungible token tx $txWithResult is not for $account\",\n                )\n              case _ =>\n                EitherT.leftT:\n                  PlayNommDAppFailure.external:\n                    s\"Tx $txWithResult is not an entrust fungible token transaction\"\n          yield ((txWithResult.signedTx.sig.account, txHash), amount)\n        .map(_.toMap)\n\n  def getEntrustedNFT[F[_]: Monad: TransactionRepository](\n      input: Hash.Value[TransactionWithResult],\n      account: Account,\n  ): StateT[\n    EitherT[F, PlayNommDAppFailure, *],\n    MerkleTrieState,\n    (Account, Transaction.TokenTx.EntrustNFT),\n  ] =\n    StateT.liftF:\n      TransactionRepository[F]\n        .get(input)\n        .leftMap: e =>\n          PlayNommDAppFailure.internal:\n            s\"Fail to get tx $input: ${e.msg}\"\n        .subflatMap: txOption =>\n          Either.fromOption(\n            txOption,\n            PlayNommDAppFailure.internal(s\"There is no tx of $input\"),\n          )\n        .subflatMap: txWithResult =>\n          txWithResult.signedTx.value match\n            case ef: Transaction.TokenTx.EntrustNFT if ef.to === account =>\n              Right((txWithResult.signedTx.sig.account, ef))\n            case _ =>\n              Left:\n                PlayNommDAppFailure.external:\n                  s\"Tx $txWithResult is not an entrust nft transaction\"\n\n  def addFungibleSnapshot[F[_]: Concurrent: PlayNommState](\n      account: Account,\n      defId: TokenDefinitionId,\n      input: Hash.Value[TransactionWithResult],\n      amount: BigNat,\n  ): StateT[EitherT[F, String, *], MerkleTrieState, Unit] =\n    for\n      snapshotStateOption <- PlayNommState[F].token.snapshotState.get(defId)\n      snapshotId = snapshotStateOption\n        .fold(SnapshotState.SnapshotId.Zero)(_.snapshotId)\n      stream <- PlayNommState[F].token.fungibleSnapshot\n        .reverseStreamFrom((account, defId).toBytes, None)\n      lastSnapshotOption <- StateT.liftF:\n        stream.head.compile.toList.flatMap: list =>\n          EitherT.pure(list.headOption)\n      lastSnapshot = lastSnapshotOption\n        .fold(Map.empty[Hash.Value[TransactionWithResult], BigNat])(_._2)\n      _ <- PlayNommState[F].token.fungibleSnapshot.put(\n        (account, defId, snapshotId),\n        lastSnapshot + (input -> amount),\n      )\n    yield ()\n\n  def addTotalSupplySnapshot[F[_]: Concurrent: PlayNommState](\n      defId: TokenDefinitionId,\n      amount: BigNat,\n  ): StateT[EitherT[F, String, *], MerkleTrieState, Unit] =\n    for\n      snapshotStateOption <- PlayNommState[F].token.snapshotState.get(defId)\n      snapshotId = snapshotStateOption\n        .fold(SnapshotState.SnapshotId.Zero)(_.snapshotId)\n      stream <- PlayNommState[F].token.totalSupplySnapshot\n        .reverseStreamFrom(defId.toBytes, None)\n      lastSnapshotOption <- StateT.liftF:\n        stream.head.compile.toList.flatMap: list =>\n          EitherT.pure(list.headOption)\n      lastSnapshot = lastSnapshotOption.fold(BigNat.Zero)(_._2)\n      _ <- PlayNommState[F].token.totalSupplySnapshot.put(\n        (defId, snapshotId),\n        BigNat.add(lastSnapshot, amount),\n      )\n    yield ()\n\n  def addNftSnapshot[F[_]: Concurrent: PlayNommState](\n      account: Account,\n      defId: TokenDefinitionId,\n      tokenId: TokenId,\n  ): StateT[EitherT[F, String, *], MerkleTrieState, Unit] =\n    for\n      snapshotStateOption <- PlayNommState[F].token.snapshotState.get(defId)\n      snapshotId = snapshotStateOption\n        .fold(SnapshotState.SnapshotId.Zero)(_.snapshotId)\n      stream <- PlayNommState[F].token.nftSnapshot\n        .reverseStreamFrom((account, defId).toBytes, None)\n      lastSnapshotOption <- StateT.liftF:\n        stream.head.compile.toList.flatMap: list =>\n          EitherT.pure(list.headOption)\n      lastSnapshot = lastSnapshotOption\n        .fold(Set.empty[TokenId])(_._2)\n      _ <- PlayNommState[F].token.nftSnapshot.put(\n        (account, defId, snapshotId),\n        lastSnapshot + tokenId,\n      )\n    yield ()\n\n  def removeFungibleSnapshot[F[_]: Concurrent: PlayNommState](\n      account: Account,\n      defId: TokenDefinitionId,\n      input: Hash.Value[TransactionWithResult],\n  ): StateT[EitherT[F, String, *], MerkleTrieState, Unit] =\n    for\n      snapshotStateOption <- PlayNommState[F].token.snapshotState.get(defId)\n      snapshotId = snapshotStateOption\n        .fold(SnapshotState.SnapshotId.Zero)(_.snapshotId)\n      stream <- PlayNommState[F].token.fungibleSnapshot\n        .reverseStreamFrom((account, defId).toBytes, None)\n      lastSnapshotOption <- StateT.liftF:\n        stream.head.compile.toList.flatMap: list =>\n          EitherT.pure(list.headOption)\n      lastSnapshot = lastSnapshotOption\n        .fold(Map.empty[Hash.Value[TransactionWithResult], BigNat])(_._2)\n      _ <- PlayNommState[F].token.fungibleSnapshot.put(\n        (account, defId, snapshotId),\n        lastSnapshot - input,\n      )\n    yield ()\n\n  def removeTotalSupplySnapshot[F[_]: Concurrent: PlayNommState](\n      defId: TokenDefinitionId,\n      amount: BigNat,\n  ): StateT[EitherT[F, String, *], MerkleTrieState, Unit] =\n    for\n      snapshotStateOption <- PlayNommState[F].token.snapshotState.get(defId)\n      snapshotId = snapshotStateOption\n        .fold(SnapshotState.SnapshotId.Zero)(_.snapshotId)\n      stream <- PlayNommState[F].token.totalSupplySnapshot\n        .reverseStreamFrom(defId.toBytes, None)\n      lastSnapshotOption <- StateT.liftF:\n        stream.head.compile.toList.flatMap: list =>\n          EitherT.pure(list.headOption)\n      lastSnapshot = lastSnapshotOption.fold(BigNat.Zero)(_._2)\n      remainder <- StateT.liftF:\n        EitherT.fromEither:\n          BigNat.tryToSubtract(lastSnapshot, amount)\n      _ <- PlayNommState[F].token.totalSupplySnapshot.put(\n        (defId, snapshotId),\n        remainder,\n      )\n    yield ()\n\n  def removeNftSnapshot[F[_]: Concurrent: PlayNommState](\n      account: Account,\n      defId: TokenDefinitionId,\n      tokenId: TokenId,\n  ): StateT[EitherT[F, String, *], MerkleTrieState, Unit] =\n    for\n      snapshotStateOption <- PlayNommState[F].token.snapshotState.get(defId)\n      snapshotId = snapshotStateOption\n        .fold(SnapshotState.SnapshotId.Zero)(_.snapshotId)\n      stream <- PlayNommState[F].token.nftSnapshot\n        .reverseStreamFrom((account, defId).toBytes, None)\n      lastSnapshotOption <- StateT.liftF:\n        stream.head.compile.toList.flatMap: list =>\n          EitherT.pure(list.headOption)\n      lastSnapshot = lastSnapshotOption\n        .fold(Set.empty[TokenId])(_._2)\n      _ <- PlayNommState[F].token.nftSnapshot.put(\n        (account, defId, snapshotId),\n        lastSnapshot - tokenId,\n      )\n    yield ()\n"
  },
  {
    "path": "modules/node/src/main/scala/io/leisuremeta/chain/node/dapp/submodule/PlayNommDAppVoting.scala",
    "content": "package io.leisuremeta.chain\npackage node\npackage dapp\npackage submodule\n\n//import cats.Monad\nimport cats.data.{EitherT, StateT}\nimport cats.effect.Concurrent\nimport cats.syntax.all.*\n\nimport api.model.{\n//  Account,\n  AccountSignature,\n  Signed,\n  Transaction,\n  TransactionWithResult,\n}\n//import api.model.TransactionWithResult.ops.*\n//import api.model.token.*\nimport api.model.voting.*\n//import api.model.token.SnapshotState.SnapshotId.*\nimport lib.codec.byte.ByteEncoder.ops.*\n//import lib.crypto.Hash\n//import lib.crypto.Hash.ops.*\nimport lib.datatype.BigNat\nimport lib.merkle.MerkleTrieState\nimport repository.TransactionRepository\n\nobject PlayNommDAppVoting:\n\n  def apply[F[_]: Concurrent: PlayNommState: TransactionRepository](\n      tx: Transaction.VotingTx,\n      sig: AccountSignature,\n  ): StateT[\n    EitherT[F, PlayNommDAppFailure, *],\n    MerkleTrieState,\n    TransactionWithResult,\n  ] = tx match\n    case cp: Transaction.VotingTx.CreateVoteProposal =>\n      for\n        _ <- PlayNommDAppAccount.verifySignature(sig, tx)\n        _ <- cp.votingPower.toList.traverse: (defId, _) =>\n          PlayNommDAppToken.checkMinterAndGetTokenDefinition(sig.account, defId)\n        proposal = Proposal(\n          createdAt = cp.createdAt,\n          proposalId = cp.proposalId,\n          title = cp.title,\n          description = cp.description,\n          votingPower = cp.votingPower,\n          voteStart = cp.voteStart,\n          voteEnd = cp.voteEnd,\n          voteType = cp.voteType,\n          voteOptions = cp.voteOptions,\n          quorum = cp.quorum,\n          passThresholdNumer = cp.passThresholdNumer,\n          passThresholdDenom = cp.passThresholdDenom,\n          isActive = true,\n        )\n        _ <- PlayNommState[F].voting.proposal\n          .put(cp.proposalId, proposal)\n          .mapK:\n            PlayNommDAppFailure.mapInternal:\n              s\"Failed to save proposal: ${cp.proposalId}\"\n      yield TransactionWithResult(Signed(sig, cp))(None)\n    case cv: Transaction.VotingTx.CastVote =>\n      for\n        _ <- PlayNommDAppAccount.verifySignature(sig, tx)\n        voteOption <- PlayNommState[F].voting.votes\n          .get((cv.proposalId, sig.account))\n          .mapK:\n            PlayNommDAppFailure.mapInternal:\n              s\"Failed to get vote: ${cv.proposalId} ${sig.account}\"\n        _ <- checkExternal(\n          voteOption.isEmpty,\n          s\"Vote already casted: ${cv.proposalId} ${sig.account}\",\n        )\n        proposalOption <- PlayNommState[F].voting.proposal\n          .get(cv.proposalId)\n          .mapK:\n            PlayNommDAppFailure.mapInternal:\n              s\"Failed to get proposal: ${cv.proposalId}\"\n        proposal <- fromOption(\n          proposalOption,\n          s\"Proposal not found: ${cv.proposalId}\",\n        )\n        _ <- checkExternal(\n          proposal.voteStart.compareTo(cv.createdAt) <= 0 &&\n            cv.createdAt.compareTo(proposal.voteEnd) <= 0,\n          s\"Vote outside of voting period: ${cv.proposalId} ${sig.account}\",\n        )\n        _ <- checkExternal(\n          proposal.isActive,\n          s\"Proposal not active: ${cv.proposalId}\",\n        )\n        amountSeq <- proposal.voteType match\n          case VoteType.ONE_PERSON_ONE_VOTE =>\n            pure(Seq(BigNat.One))\n          case VoteType.TOKEN_WEIGHTED =>\n            proposal.votingPower.toSeq.traverse: (defId, snapshotId) =>\n              for balance <- PlayNommState[F].token.fungibleSnapshot\n                  .reverseStreamFrom(\n                    (sig.account, defId).toBytes,\n                    Some(snapshotId.toBytes),\n                  )\n                  .mapK:\n                    PlayNommDAppFailure.mapInternal:\n                      s\"Failed to get balance stream of: ${sig.account} ${defId}\"\n                  .flatMap: stream =>\n                    StateT.liftF:\n                      stream.head.compile.toList\n                        .map:\n                          case Nil         => Map.empty\n                          case (k, v) :: _ => v\n                        .leftMap: msg =>\n                          PlayNommDAppFailure.internal:\n                            s\"Failed to get balance of: ${sig.account} ${defId} ${snapshotId}: ${msg}\"\n              yield balance.values.foldLeft(BigNat.Zero)(BigNat.add)\n          case VoteType.NFT_BASED =>\n            proposal.votingPower.toSeq.traverse: (defId, snapshotId) =>\n              for count <- PlayNommState[F].token.nftSnapshot\n                  .reverseStreamFrom(\n                    (sig.account, defId).toBytes,\n                    Some(snapshotId.toBytes),\n                  )\n                  .mapK:\n                    PlayNommDAppFailure.mapInternal:\n                      s\"Failed to get balance stream of: ${sig.account} ${defId}\"\n                  .flatMap: stream =>\n                    StateT.liftF:\n                      stream.head.compile.toList\n                        .map: tokenIds =>\n                          tokenIds.size\n                        .leftMap: msg =>\n                          PlayNommDAppFailure.internal:\n                            s\"Failed to get balance of: ${sig.account} ${defId} ${snapshotId}: ${msg}\"\n              yield BigNat.unsafeFromBigInt(BigInt(count))\n        powerSum = amountSeq.foldLeft(BigNat.Zero)(BigNat.add)\n        originalPowerOption <- PlayNommState[F].voting.votes\n          .put((cv.proposalId, sig.account), (cv.selectedOption, powerSum))\n          .mapK:\n            PlayNommDAppFailure.mapInternal:\n              s\"Failed to save vote: ${cv.proposalId} ${sig.account}\"\n        originalMapOption <- PlayNommState[F].voting.counting\n          .get(cv.proposalId)\n          .mapK:\n            PlayNommDAppFailure.mapInternal:\n              s\"Failed to get counting: ${cv.proposalId}\"\n        originalMap = originalMapOption.getOrElse(Map.empty)\n        newMap      = originalMap.updated(cv.selectedOption, powerSum)\n        _ <- PlayNommState[F].voting.counting\n          .put(cv.proposalId, newMap)\n          .mapK:\n            PlayNommDAppFailure.mapInternal:\n              s\"Failed to save counting: ${cv.proposalId}\"\n      yield TransactionWithResult(Signed(sig, cv))(None)\n\n    case tv: Transaction.VotingTx.TallyVotes =>\n      for\n        _ <- PlayNommDAppAccount.verifySignature(sig, tx)\n        proposalOption <- PlayNommState[F].voting.proposal\n          .get(tv.proposalId)\n          .mapK:\n            PlayNommDAppFailure.mapInternal:\n              s\"Failed to get proposal: ${tv.proposalId}\"\n        proposal <- fromOption(\n          proposalOption,\n          s\"Proposal not found: ${tv.proposalId}\",\n        )\n        _ <- proposal.votingPower.toList.traverse: (defId, _) =>\n          PlayNommDAppToken.checkMinterAndGetTokenDefinition(sig.account, defId)\n        newProposal = proposal.copy(isActive = false)\n        _ <- PlayNommState[F].voting.proposal\n          .put(tv.proposalId, newProposal)\n          .mapK:\n            PlayNommDAppFailure.mapInternal:\n              s\"Failed to save proposal: ${tv.proposalId}\"\n      yield TransactionWithResult(Signed(sig, tv))(None)\n"
  },
  {
    "path": "modules/node/src/main/scala/io/leisuremeta/chain/node/dapp/submodule/package.scala",
    "content": "package io.leisuremeta.chain\npackage node\npackage dapp\npackage submodule\n\nimport cats.Monad\nimport cats.data.{EitherT, StateT}\nimport lib.merkle.MerkleTrieState\n\ndef checkExternal[F[_]: Monad](\n    test: Boolean,\n    errorMessage: String,\n): StateT[EitherT[F, PlayNommDAppFailure, *], MerkleTrieState, Unit] =\n  StateT.liftF {\n    EitherT.cond(\n      test,\n      (),\n      PlayNommDAppFailure.external(errorMessage),\n    )\n  }\n\ndef checkInternal[F[_]: Monad](\n    test: Boolean,\n    errorMessage: String,\n): StateT[EitherT[F, PlayNommDAppFailure, *], MerkleTrieState, Unit] =\n  StateT.liftF:\n    EitherT.cond(\n      test,\n      (),\n      PlayNommDAppFailure.internal(errorMessage),\n    )\n\ndef fromOption[F[_]: Monad, A](\n    option: Option[A],\n    errorMessage: String,\n): StateT[EitherT[F, PlayNommDAppFailure, *], MerkleTrieState, A] =\n  StateT.liftF {\n    EitherT.fromOption(option, PlayNommDAppFailure.external(errorMessage))\n  }\n\ndef fromEitherExternal[F[_]: Monad, A](\n  either: Either[String, A]\n): StateT[EitherT[F, PlayNommDAppFailure, *], MerkleTrieState, A] =\n  StateT.liftF {\n    EitherT.fromEither(either).leftMap(PlayNommDAppFailure.external)\n  }\n\ndef fromEitherInternal[F[_]: Monad, A](\n  either: Either[String, A]\n): StateT[EitherT[F, PlayNommDAppFailure, *], MerkleTrieState, A] =\n  StateT.liftF {\n    EitherT.fromEither(either).leftMap(PlayNommDAppFailure.internal)\n  }\n\ndef pure[F[_]: Monad, A](\n    a: A,\n): StateT[EitherT[F, PlayNommDAppFailure, *], MerkleTrieState, A] =\n  StateT.liftF(EitherT.pure(a))\n\ndef unit[F[_]: Monad] = pure[F, Unit](())\n"
  },
  {
    "path": "modules/node/src/main/scala/io/leisuremeta/chain/node/repository/BlockRepository.scala",
    "content": "package io.leisuremeta.chain\npackage node\npackage repository\n\nimport cats.Monad\nimport cats.data.EitherT\nimport cats.implicits._\n\nimport api.model.{Block, Signed}\nimport api.model.Block.BlockHash\nimport lib.crypto.Hash\nimport lib.crypto.Hash.ops._\nimport lib.datatype.BigNat\nimport lib.failure.DecodingFailure\nimport store.{HashStore, KeyValueStore, SingleValueStore}\n\ntrait BlockRepository[F[_]] {\n  def bestHeader: EitherT[F, DecodingFailure, Option[Block.Header]]\n  def get(hash: Hash.Value[Block]): EitherT[F, DecodingFailure, Option[Block]]\n  def put(block: Block): EitherT[F, DecodingFailure, Unit]\n  def findByTransaction(\n      txHash: Signed.TxHash\n  ): EitherT[F, DecodingFailure, Option[BlockHash]]\n}\n\nobject BlockRepository {\n\n  def apply[F[_]: BlockRepository]: BlockRepository[F] = summon\n\n  def fromStores[F[_]: Monad](using\n      bestBlockHeaderStore: SingleValueStore[F, Block.Header],\n      blockHashStore: HashStore[F, Block],\n      blockNumberIndex: KeyValueStore[F, BigNat, BlockHash],\n      txBlockIndex: KeyValueStore[F, Signed.TxHash, BlockHash],\n  ): BlockRepository[F] = new BlockRepository[F] {\n\n    def bestHeader: EitherT[F, DecodingFailure, Option[Block.Header]] =\n      bestBlockHeaderStore.get()\n\n    def get(\n        blockHash: Hash.Value[Block]\n    ): EitherT[F, DecodingFailure, Option[Block]] =\n      blockHashStore.get(blockHash)\n\n    def put(block: Block): EitherT[F, DecodingFailure, Unit] = for {\n      _ <- EitherT.rightT[F, DecodingFailure](\n        scribe.debug(s\"Putting block: $block\")\n      )\n      _                <- EitherT.right[DecodingFailure](blockHashStore.put(block))\n      _                <- EitherT.rightT[F, DecodingFailure](scribe.debug(s\"block is put\"))\n      bestHeaderOption <- bestHeader\n      _ <- EitherT.rightT[F, DecodingFailure](\n        scribe.debug(s\"best header option: $bestHeaderOption\")\n      )\n      _ <- (bestHeaderOption match {\n        case Some(best) if best.number.toBigInt >= block.header.number.toBigInt =>\n          EitherT.pure[F, DecodingFailure](())\n        case _ =>\n          val blockHash = block.toHash\n          EitherT.right[DecodingFailure](for {\n            _ <- Monad[F].pure(scribe.debug(s\"putting best header\"))\n            _ <- bestBlockHeaderStore.put(block.header)\n            _ <- blockNumberIndex.put(block.header.number, blockHash)\n            _ <- block.transactionHashes.toList.traverse { txHash =>\n              txBlockIndex.put(txHash, blockHash)\n            }\n            _ <- Monad[F].pure(scribe.debug(s\"putting best header is completed\"))\n          } yield ())\n      })\n      _ <- EitherT.rightT[F, DecodingFailure](\n        scribe.debug(s\"Putting completed: $block\")\n      )\n    } yield ()\n\n    def findByTransaction(\n        txHash: Signed.TxHash\n    ): EitherT[F, DecodingFailure, Option[BlockHash]] =\n      txBlockIndex.get(txHash)\n  }\n}\n"
  },
  {
    "path": "modules/node/src/main/scala/io/leisuremeta/chain/node/repository/StateRepository.scala",
    "content": "package io.leisuremeta.chain\npackage node\npackage repository\n\nimport cats.{Functor, Monad}\nimport cats.data.{EitherT, Kleisli}\nimport cats.syntax.traverse.*\n\nimport lib.merkle.{MerkleTrie, MerkleTrieNode, MerkleTrieState}\nimport lib.merkle.MerkleTrieNode.{MerkleHash, MerkleRoot}\nimport lib.failure.DecodingFailure\nimport store.KeyValueStore\n\ntrait StateRepository[F[_]]:\n  def get(merkleRoot: MerkleRoot): EitherT[F, DecodingFailure, Option[MerkleTrieNode]]\n  def put(state: MerkleTrieState): EitherT[F, DecodingFailure, Unit]\n\nobject StateRepository:\n  def apply[F[_]: StateRepository]: StateRepository[F] = summon\n\n  given nodeStore[F[_]: Functor: StateRepository]: MerkleTrie.NodeStore[F] =\n    Kleisli(StateRepository[F].get(_).leftMap(_.msg))\n\n  given fromStores[F[_]: Monad](using\n    stateKvStore: KeyValueStore[F, MerkleHash, MerkleTrieNode],\n  ): StateRepository[F] with\n    def get(merkleRoot: MerkleRoot): EitherT[F, DecodingFailure, Option[MerkleTrieNode]] =\n      stateKvStore.get(merkleRoot)\n\n    def put(state: MerkleTrieState): EitherT[F, DecodingFailure, Unit] = for\n//      _ <- EitherT.pure(scribe.info(s\"Putting state: $state\"))\n      _ <- state.diff.toList.traverse { case (hash, (node, count)) =>\n        if count <= 0 then EitherT.pure(()) else stateKvStore.get(hash).flatMap{\n          case None => EitherT.right(stateKvStore.put(hash, node))\n          case _ => EitherT.pure(())\n        }\n      }\n//      _ <- EitherT.pure(scribe.info(s\"Putting completed: $state\"))\n    yield ()\n"
  },
  {
    "path": "modules/node/src/main/scala/io/leisuremeta/chain/node/repository/TransactionRepository.scala",
    "content": "package io.leisuremeta.chain\npackage node\npackage repository\n\nimport cats.data.EitherT\n\nimport api.model.TransactionWithResult\nimport lib.crypto.Hash\nimport lib.failure.DecodingFailure\nimport store.HashStore\n\ntrait TransactionRepository[F[_]]:\n  def get(\n      transactionHash: Hash.Value[TransactionWithResult],\n  ): EitherT[F, DecodingFailure, Option[TransactionWithResult]]\n  def put(transaction: TransactionWithResult): F[Unit]\n\nobject TransactionRepository:\n\n  def apply[F[_]](implicit\n      txRepo: TransactionRepository[F],\n  ): TransactionRepository[F] = txRepo\n\n  def fromStores[F[_]](implicit\n      transctionHashStore: HashStore[F, TransactionWithResult],\n  ): TransactionRepository[F] = new TransactionRepository[F]:\n\n    def get(\n        transactionHash: Hash.Value[TransactionWithResult],\n    ): EitherT[F, DecodingFailure, Option[TransactionWithResult]] =\n      transctionHashStore.get(transactionHash)\n\n    def put(transaction: TransactionWithResult): F[Unit] =\n      transctionHashStore.put(transaction)\n"
  },
  {
    "path": "modules/node/src/main/scala/io/leisuremeta/chain/node/service/BlockService.scala",
    "content": "package io.leisuremeta.chain\npackage node\npackage service\n\nimport cats.{Functor, Monad}\nimport cats.data.EitherT\nimport cats.syntax.traverse.*\n\nimport api.model.{Block, Signed, TransactionWithResult}\nimport api.model.Block.ops.toBlockHash\nimport api.model.api_model.BlockInfo\nimport repository.{BlockRepository, TransactionRepository}\nimport lib.crypto.Hash.ops.*\n\nobject BlockService:\n  def saveBlock[F[_]: Monad: BlockRepository: TransactionRepository](\n      block: Block,\n      txs: Map[Signed.TxHash, TransactionWithResult],\n  ): EitherT[F, String, Block.BlockHash] = for\n    _ <- BlockRepository[F].put(block).leftMap(_.msg)\n    _ <- EitherT.rightT[F, String](scribe.info(s\"Saving txs: $txs\"))\n    _ <- block.transactionHashes.toList.traverse { (txHash: Signed.TxHash) =>\n      for\n        tx <- EitherT\n          .fromOption[F](txs.get(txHash), s\"Missing transaction: $txHash\")\n        _ <- EitherT.right[String](TransactionRepository[F].put(tx))\n      yield ()\n    }\n    _ <- EitherT.rightT[F, String](scribe.info(s\"txs is saved successfully\"))\n  yield block.toHash\n\n  def index[F[_]: Monad: BlockRepository](\n    fromOption: Option[Block.BlockHash],\n    limitOption: Option[Int],\n  ): EitherT[F, String, List[BlockInfo]] =\n\n    def loop(from: Block.BlockHash, limit: Int, acc: List[BlockInfo]): EitherT[F, String, List[BlockInfo]] =\n      if limit <= 0 then EitherT.pure(acc.reverse) else\n        BlockRepository[F].get(from).leftMap(_.msg).flatMap {\n          case None => EitherT.leftT(s\"block not found: $from\")\n          case Some(block) =>\n            val info: BlockInfo = BlockInfo(\n              blockNumber = block.header.number,\n              timestamp = block.header.timestamp,\n              blockHash = from,\n              txCount = block.transactionHashes.size,\n            )\n\n            if block.header.number.toBigInt <= BigInt(0) then\n              EitherT.pure((info :: acc).reverse)\n            else\n              loop(block.header.parentHash, limit - 1, info :: acc)\n        }\n\n    for\n      from <- fromOption match\n        case Some(from) => EitherT.pure(from)\n        case None => BlockRepository[F].bestHeader.leftMap(_.msg).flatMap{\n          case Some(blockHeader) => EitherT.pure(blockHeader.toHash.toBlockHash)\n          case None => EitherT.leftT(s\"Best header not found\")\n        }\n      result <- loop(from, limitOption.getOrElse(50), Nil)\n    yield result\n    \n\n  def get[F[_]: Functor: BlockRepository](\n      blockHash: Block.BlockHash,\n  ): EitherT[F, String, Option[Block]] =\n    BlockRepository[F].get(blockHash).leftMap(_.msg)\n"
  },
  {
    "path": "modules/node/src/main/scala/io/leisuremeta/chain/node/service/LocalStatusService.scala",
    "content": "package io.leisuremeta.chain\npackage node\npackage service\n\nimport cats.MonadError\nimport cats.syntax.flatMap.*\n\nimport api.model.{Block, NetworkId, NodeStatus}\nimport api.model.Block.ops.*\nimport lib.crypto.Hash.ops.*\nimport lib.datatype.BigNat\nimport lib.failure.DecodingFailure\nimport repository.BlockRepository\nimport java.time.Instant\n\nobject LocalStatusService:\n\n  def status[F[_]: BlockRepository](\n      networkId: NetworkId,\n      genesisTimestamp: Instant,\n  )(using me: MonadError[F, Throwable]): F[NodeStatus] =\n    BlockRepository[F].bestHeader.value.flatMap {\n      case Left(err) => me.raiseError(err)\n      case Right(bestBlockHeader) =>\n        val gHash = genesisHash(genesisTimestamp)\n        me.pure {\n          NodeStatus(\n            networkId = networkId,\n            genesisHash = gHash,\n            bestHash = bestBlockHeader.fold(gHash)(_.toHash.toBlockHash),\n            number = bestBlockHeader.fold(BigNat.Zero)(_.number),\n          )\n        }\n    }\n\n  def genesisHash(genesisTimestamp: Instant): Block.BlockHash =\n    NodeInitializationService.genesisBlock(genesisTimestamp).toHash\n"
  },
  {
    "path": "modules/node/src/main/scala/io/leisuremeta/chain/node/service/NodeInitializationService.scala",
    "content": "package io.leisuremeta.chain\npackage node\npackage service\n\nimport java.time.Instant\n\nimport cats.Monad\nimport cats.data.EitherT\n\nimport api.model.{Block, StateRoot}\nimport api.model.Block.ops.*\nimport lib.crypto.Hash\nimport lib.crypto.Hash.ops.*\nimport lib.datatype.{BigNat, UInt256}\nimport repository.BlockRepository\n\nobject NodeInitializationService:\n  def initialize[F[_]: Monad: BlockRepository](\n      timestamp: Instant,\n  ): EitherT[F, String, Block] = for\n    _ <- EitherT.rightT[F, String](scribe.info(s\"Initialize... \"))\n    bestBlockHeaderOption <- BlockRepository[F].bestHeader.leftMap(_.msg)\n    block <- bestBlockHeaderOption match\n      case None =>\n        scribe.info(\n          \"No best block header found. Initializing genesis block.\",\n        )\n        val block = genesisBlock(timestamp)\n        BlockRepository[F].put(genesisBlock(timestamp)).leftMap(_.msg).map(_ => block)\n      case Some(header) =>\n        scribe.info(\"Best block header found. Skipping genesis block.\")\n        val blockHash = header.toHash.toBlockHash\n        for\n          blockOption <- BlockRepository[F].get(blockHash).leftMap(_.msg)\n          block <- EitherT.fromOption[F](\n            blockOption,\n            s\"best block $blockHash not found in the block repository\",\n          )\n        yield block\n  yield block\n\n  def genesisBlock(genesisTimestamp: Instant): Block = Block(\n    header = Block.Header(\n      number = BigNat.Zero,\n      parentHash = Hash.Value[Block](UInt256.EmptyBytes),\n      stateRoot = StateRoot.empty,\n      transactionsRoot = None,\n      timestamp = genesisTimestamp,\n    ),\n    transactionHashes = Set.empty,\n    votes = Set.empty,\n  )\n"
  },
  {
    "path": "modules/node/src/main/scala/io/leisuremeta/chain/node/service/RewardService.scala",
    "content": "package io.leisuremeta.chain\npackage node\npackage service\n\n//import java.time.{DayOfWeek, Instant, ZoneId}//, ZonedDateTime}\n//import java.time.temporal.{ChronoUnit, TemporalAdjusters}\n\n//import cats.{Monad, Monoid}\n//import cats.data.EitherT\n//import cats.effect.Concurrent\n//import cats.syntax.either.catsSyntaxEither\n//import cats.syntax.eq.catsSyntaxEq\n//import cats.syntax.foldable.toFoldableOps\n//import cats.syntax.functor.toFunctorOps\n//import cats.syntax.traverse.toTraverseOps\n\n//import fs2.Stream\n//import scodec.bits.BitVector\n\n//import api.model.{Block,TransactionWithResult}//{Account, Block, GroupId, StateRoot, TransactionWithResult}\n//import api.model.Block.ops.*\n//import api.model.TransactionWithResult.ops.*\n//import api.model.api_model.RewardInfo\n//import api.model.reward.{DaoActivity, DaoInfo}\n//import api.model.token.{\n//  NftState,\n//  Rarity,\n//  TokenDefinition,\n//  TokenDefinitionId,\n//  TokenId,\n//}\n//import dapp.PlayNommState\n//import lib.codec.byte.{ByteDecoder, DecodeResult}\n//import lib.codec.byte.ByteEncoder.ops.*\n//import lib.crypto.Hash\n//import lib.crypto.Hash.ops.*\n//import lib.datatype.{BigNat, Utf8}\n//import lib.merkle.{GenericMerkleTrie, GenericMerkleTrieState}\n//import lib.merkle.GenericMerkleTrie.NodeStore\n//import repository.{BlockRepository, TransactionRepository}\n//import repository.GenericStateRepository.given\n\n//import lib.merkle.{GenericMerkleTrie, GenericMerkleTrieState}\n\nobject RewardService:\n\n//\n//  def getRewardInfoFromBestHeader[F[_]\n//    : Concurrent: BlockRepository: TransactionRepository: GenericStateRepository.RewardState: GenericStateRepository.TokenState](\n//      account: Account,\n//      timestampOption: Option[Instant],\n//      daoAccount: Option[Account],\n//      rewardAmount: Option[BigNat],\n//  ): EitherT[F, String, RewardInfo] =\n//    for\n//      bestHeaderOption <- BlockRepository[F].bestHeader.leftMap(_.msg)\n//      bestHeader <- EitherT.fromOption(\n//        bestHeaderOption,\n//        s\"Best header not found\",\n//      )\n//      stateRoot = GossipDomain.MerkleState.from(bestHeader)\n//      info <- getRewardInfo[F](\n//        account,\n//        timestampOption,\n//        daoAccount,\n//        rewardAmount,\n//        stateRoot.reward,\n//        stateRoot.token,\n//      )\n//    yield info\n//  \n//\n//  def getRewardInfo[F[_]\n//    : Concurrent: BlockRepository: TransactionRepository: GenericStateRepository.RewardState: GenericStateRepository.TokenState](\n//      account: Account,\n//      timestampOption: Option[Instant],\n//      daoAccount: Option[Account],\n//      rewardAmount: Option[BigNat],\n//      rewardMerkleState: GossipDomain.MerkleState.RewardMerkleState,\n//      tokenMerkleState: GossipDomain.MerkleState.TokenMerkleState,\n//  ): EitherT[F, String, RewardInfo] =\n//    val timestamp          = timestampOption.getOrElse(Instant.now())\n//    val canonicalTimestamp = getLatestRewardInstantBefore(timestamp)\n//    val userActivityState  = rewardMerkleState.userActivityState\n//    val tokenReceivedState = rewardMerkleState.tokenReceivedState\n//\n//    for\n//      totalNumberOfDao <- countDao[F](rewardMerkleState.daoState)\n////      _ <- EitherT.pure(scribe.info(s\"Total number of DAO: ${totalNumberOfDao}\"))\n//      userActivities <- getWeeklyUserActivities[F](\n//        timestamp,\n//        account,\n//        userActivityState,\n//      )\n//      userActivityMilliPoint = userActivities.flatten\n//        .map(dailyDaoActivityToMilliPoint)\n//        .foldLeft(BigNat.Zero)(BigNat.add)\n////      _ <- EitherT.pure(scribe.info(s\"userActivities: ${userActivities.flatten}\"))\n//      totalActivityMilliPoint <- getWeeklyTotalActivityPoint[F](\n//        timestamp,\n//        userActivityState,\n//      )\n////      _ <- EitherT.pure(scribe.info(s\"totalActivityMilliPoint: ${totalActivityMilliPoint}\"))\n//      userActivityReward = calculateUserActivityReward(\n//        totalNumberOfDao,\n//        userActivityMilliPoint,\n//        totalActivityMilliPoint,\n//      )\n//      stateRoot <- findStateRootAt(canonicalTimestamp)\n////      _ <- EitherT.pure(scribe.info(s\"stateRoot: ${stateRoot}\"))\n//      tokens <- getNftOwned(account, stateRoot.token.nftBalanceState)\n////      _ <- EitherT.pure(scribe.info(s\"tokens: ${tokens}\"))\n//      tokenReceivedDaoActivity <- getWeeklyTokenReceived(\n//        tokens,\n//        canonicalTimestamp,\n//        tokenReceivedState,\n//      )\n////      _ <- EitherT.pure(scribe.info(s\"tokenReceivedDaoActivity: ${tokenReceivedDaoActivity}\"))\n//      totalReceivedMilliPoint <- getWeeklyTotalReceivedPoint(\n//        timestamp,\n//        tokenReceivedState,\n//      )\n////      _ <- EitherT.pure(scribe.info(s\"totalReceivedMilliPoint: ${totalReceivedMilliPoint}\"))\n//      tokenReceivedReward = calculateTokenReceivedReward(\n//        totalNumberOfDao,\n//        tokenReceivedDaoActivity,\n//        totalReceivedMilliPoint,\n//      )\n//      totalRarityRewardAmount <- getTotalRarityRewardAmount(\n//        rewardAmount,\n//        daoAccount,\n//      )\n//      userRarityRewardItems <- getUserRarityItem(account, tokenMerkleState)\n////      _ <- EitherT.pure(scribe.info(s\"userRarityRewardItems: ${userRarityRewardItems}\"))\n//      userRarityReward = rarityItemsToRewardDetailMap(userRarityRewardItems)\n//      totalRarityRewardValue <- getTotalRarityRewardValue(\n//        account,\n//        tokenMerkleState,\n//      )\n////      _ <- EitherT.pure(\n////        scribe.info(s\"totalRarityRewardValue: ${totalRarityRewardValue}\"),\n////      )\n//      userRarityRewardValue = calculateUserRarityRewardValue(\n//        totalRarityRewardAmount,\n//        totalNumberOfDao,\n//        userRarityRewardItems,\n//        totalRarityRewardValue,\n//      )\n//      isModerator <- isModerator(account, rewardMerkleState.daoState)\n//      bonus =\n//        if isModerator then userActivityReward + userActivityReward\n//        else BigNat.Zero\n//      total =\n//        userActivityReward + tokenReceivedReward + userRarityRewardValue + bonus\n//    yield RewardInfo(\n//      account = account,\n//      reward = RewardInfo.Reward(\n//        total = total,\n//        activity = userActivityReward,\n//        token = tokenReceivedReward,\n//        rarity = userRarityRewardValue,\n//        bonus = bonus,\n//      ),\n//      point = RewardInfo.Point(\n//        activity = userActivities.flatten.combineAll,\n//        token = tokenReceivedDaoActivity,\n//        rarity = userRarityReward,\n//      ),\n//      timestamp = timestamp,\n//      totalNumberOfDao = BigNat.unsafeFromLong(totalNumberOfDao),\n//    )\n//*/\n\n//  /** count dao (max value 100)\n//    *\n//    * @return\n//    *   number of dao (if larger than 100, return 100)\n//    */\n//  def countDao[F[_]: Concurrent: PlayNommState](\n//      daoState: GenericMerkleTrieState[GroupId, DaoInfo],\n//  ): EitherT[F, String, Int] =\n//    for\n//      stream <- GenericMerkleTrie\n//        .from[F, GroupId, DaoInfo](BitVector.empty)\n//        .runA(daoState)\n//      size <- stream.take(100).compile.count\n//    yield size.toInt\n\n//  def calculateUserActivityReward(\n//      numberOfDao: Int,\n//      userActivityMilliPoint: BigNat,\n//      totalActivityMilliPoint: BigNat,\n//  ): BigNat =\n//    val limit = BigInt(120_000L) * 1000 * numberOfDao\n//    val milliPoint: BigNat =\n//      if totalActivityMilliPoint.toBigInt <= limit then userActivityMilliPoint\n//      else\n//        BigNat.unsafeFromBigInt(\n//          limit,\n//        ) * userActivityMilliPoint / totalActivityMilliPoint\n//    milliPoint * BigNat.unsafeFromBigInt(BigInt(10).pow(15))\n//\n//  def getWeeklyUserActivities[F[_]: Concurrent: GenericStateRepository.RewardState](\n//      timestamp: Instant,\n//      user: Account,\n//      root: GenericMerkleTrieState[(Instant, Account), DaoActivity],\n//  ): EitherT[F, String, Seq[Option[DaoActivity]]] =\n//    val refInstants = getWeeklyRefTime(\n//      getLatestRewardInstantBefore(timestamp),\n//    )\n//\n//    refInstants\n//      .traverse { (refInstant) =>\n//        GenericMerkleTrie.get[F, (Instant, Account), DaoActivity](\n//          (refInstant, user).toBytes.bits,\n//        )\n//      }\n//      .runA(root)\n//\n//  def getLatestRewardInstantBefore(timestamp: Instant): Instant =\n//    timestamp\n//      .atZone(ZoneId.of(\"Asia/Seoul\"))\n//      .`with`(TemporalAdjusters.previous(DayOfWeek.MONDAY))\n//      .truncatedTo(ChronoUnit.DAYS)\n//      .toInstant()\n\n//  def getWeeklyRefTime(last: Instant): Seq[Instant] =\n//    Seq.tabulate(7)(i => last.minus(7 - i, ChronoUnit.DAYS))\n//\n//  def dailyDaoActivityToMilliPoint(a: DaoActivity): BigNat =\n//    BigNat\n//      .fromBigInt {\n//        daoActivityToMilliPoint(a).max(4000)\n//      }\n//      .toOption\n//      .getOrElse(BigNat.Zero)\n//\n//  def daoActivityToMilliPoint(a: DaoActivity): BigInt =\n//    val weights = Seq(10, 10, 10, -10)\n//    Seq(a.like, a.comment, a.share, a.report)\n//      .map(_.toBigInt)\n//      .zip(weights)\n//      .map { case (number, weight) => number * weight }\n//      .sum\n//\n//  def getWeeklyTotalActivityPoint[F[_]\n//    : Concurrent: GenericStateRepository.RewardState](\n//      timestamp: Instant,\n//      root: GenericMerkleTrieState[(Instant, Account), DaoActivity],\n//  ): EitherT[F, String, BigNat] =\n//    val to: Instant   = getLatestRewardInstantBefore(timestamp)\n//    val from: Instant = getLatestRewardInstantBefore(to)\n//    val timestampBits = from.toBytes.bits\n//\n//    GenericMerkleTrie\n//      .from[F, (Instant, Account), DaoActivity](timestampBits)\n//      .runA(root)\n//      .flatMap { stream =>\n//        stream\n//          .evalMap { case (keyBits, daoActivity) =>\n//            EitherT.fromEither[F] {\n//              ByteDecoder[(Instant, Account)]\n//                .decode(keyBits.bytes)\n//                .leftMap(_.msg)\n//                .flatMap { case DecodeResult((instant, account), remainder) =>\n//                  if remainder.isEmpty then Right((instant, daoActivity))\n//                  else\n//                    Left(\n//                      s\"Non-empty remainder: $remainder in account: ${keyBits.bytes}\",\n//                    )\n//                }\n//            }\n//          }\n//          .takeWhile(_._1.compareTo(to) <= 0)\n//          .map(_._2)\n//          .map(dailyDaoActivityToMilliPoint)\n//          .fold(BigNat.Zero)(BigNat.add)\n//          .compile\n//          .toList\n//      }\n//      .map(_.head)\n//\n//  def calculateWeeklyUserActivityReward(\n//      weeklyUserActivity: Seq[DaoActivity],\n//  ): BigNat =\n//\n//    val weights = Seq(10, 10, 10, -10)\n//\n//    val sumBigInt = weeklyUserActivity.map { (a: DaoActivity) =>\n//      Seq(a.like, a.comment, a.share, a.report)\n//        .map(_.toBigInt)\n//        .zip(weights)\n//        .map { case (number, weight) => number * weight }\n//        .sum\n//        .max(4000) // Max daily millipoint\n//    }.sum\n//\n//    BigNat.fromBigInt(sumBigInt).toOption.getOrElse(BigNat.Zero)\n//\n//  def getUserActivityTimeWindows(last: Instant): Seq[Instant] =\n//    Seq.tabulate(8)(i => last.minus(7 - i, ChronoUnit.DAYS))\n//\n\n//  def findStateRootAt[F[_]: Monad: BlockRepository: TransactionRepository](\n//      instant: Instant,\n//  ): EitherT[F, String, GossipDomain.MerkleState] =\n//\n//    def loop(\n//        blockHash: Block.BlockHash,\n//    ): EitherT[F, String, GossipDomain.MerkleState] =\n//      for\n//        blockOption <- BlockRepository[F].get(blockHash).leftMap(_.msg)\n//        block <- EitherT.fromOption(blockOption, s\"Block not found: $blockHash\")\n//        txs <- block.transactionHashes.toList.traverse { txHash =>\n//          TransactionRepository[F]\n//            .get(txHash.toResultHashValue)\n//            .leftMap(_.msg)\n//            .flatMap(\n//              EitherT.fromOption(\n//                _,\n//                s\"Transaction $txHash is not found in block $blockHash\",\n//              ),\n//            )\n//        }\n//        ans <-\n//          if txs.exists(_.signedTx.value.createdAt.compareTo(instant) > 0) then\n//            loop(block.header.parentHash)\n//          else EitherT.pure(MerkleState.from(block.header))\n//      yield ans\n//\n//    BlockRepository[F].bestHeader\n//      .leftMap(_.msg)\n//      .map(_.get.toHash.toBlockHash)\n//      .flatMap(loop)\n\n//  def getNftOwned[F[_]: Concurrent: GenericStateRepository.TokenState](\n//      user: Account,\n//      state: GenericMerkleTrieState[\n//        (Account, TokenId, Hash.Value[TransactionWithResult]),\n//        Unit,\n//      ],\n//  ): EitherT[F, String, List[TokenId]] =\n//    for\n//      stream <- GenericMerkleTrie\n//        .from[F, (Account, TokenId, Hash.Value[TransactionWithResult]), Unit](\n//          user.toBytes.bits,\n//        )\n//        .runA(state)\n//      tokenIds <- stream\n//        .evalMap { case (keyBits, ()) =>\n//          EitherT.fromEither[F] {\n//            ByteDecoder[(Account, TokenId, Hash.Value[TransactionWithResult])]\n//              .decode(keyBits.bytes)\n//              .leftMap(_.msg)\n//              .flatMap {\n//                case DecodeResult((account, tokenId, txHash), remainder)\n//                    if remainder.isEmpty =>\n//                  Right((account, tokenId))\n//                case _ =>\n//                  Left(\n//                    s\"fail to decode ${keyBits.bytes} in nft balance of account $user\",\n//                  )\n//              }\n//          }\n//        }\n//        .takeWhile(_._1 === user)\n//        .map(_._2)\n//        .compile\n//        .toList\n//    yield tokenIds\n//\n//  def getWeeklyTokenReceived[F[_]: Monad: GenericStateRepository.RewardState](\n//      tokenList: List[TokenId],\n//      canonicalTimestamp: Instant,\n//      root: GenericMerkleTrieState[(Instant, TokenId), DaoActivity],\n//  ): EitherT[F, String, DaoActivity] =\n//    val refInstants = getWeeklyRefTime(\n//      getLatestRewardInstantBefore(canonicalTimestamp),\n//    )\n//\n//    val keyList = for\n//      refInstant <- refInstants\n//      tokenId    <- tokenList\n//    yield (refInstant, tokenId)\n//\n//    keyList\n//      .traverse { (refInstant, tokenId) =>\n//        GenericMerkleTrie\n//          .get[F, (Instant, TokenId), DaoActivity](\n//            (refInstant, tokenId).toBytes.bits,\n//          )\n//          .runA(root)\n//      }\n//      .map(_.flatten.combineAll)\n//\n//  def getWeeklyTotalReceivedPoint[F[_]\n//    : Concurrent: GenericStateRepository.RewardState](\n//      timestamp: Instant,\n//      root: GenericMerkleTrieState[(Instant, TokenId), DaoActivity],\n//  ): EitherT[F, String, BigNat] =\n//    val to: Instant   = getLatestRewardInstantBefore(timestamp)\n//    val from: Instant = getLatestRewardInstantBefore(to)\n//    val timestampBits = from.toBytes.bits\n//\n//    GenericMerkleTrie\n//      .from[F, (Instant, TokenId), DaoActivity](timestampBits)\n//      .runA(root)\n//      .flatMap { stream =>\n//        stream\n//          .evalMap { case (keyBits, daoActivity) =>\n//            EitherT.fromEither[F] {\n//              ByteDecoder[(Instant, TokenId)]\n//                .decode(keyBits.bytes)\n//                .leftMap(_.msg)\n//                .flatMap { case DecodeResult((instant, tokenId), remainder) =>\n//                  if remainder.isEmpty then Right((instant, daoActivity))\n//                  else\n//                    Left(\n//                      s\"Non-empty remainder: $remainder in token: ${keyBits.bytes}\",\n//                    )\n//                }\n//            }\n//          }\n//          .takeWhile(_._1.compareTo(to) <= 0)\n//          .map(_._2)\n//          .map(daoActivityToMilliPoint)\n//          .foldMonoid\n//          .compile\n//          .toList\n//      }\n//      .map(_.head)\n//      .map(BigNat.fromBigInt(_).toOption.getOrElse(BigNat.Zero))\n//\n//  def calculateTokenReceivedReward(\n//      numberOfDao: Int,\n//      tokenReceivedActivity: DaoActivity,\n//      totalReceivedMilliPoint: BigNat,\n//  ): BigNat =\n//    if numberOfDao <= 0 then BigNat.Zero\n//    else\n//      val limit = BigInt(125_000L) * 1000 * numberOfDao - 50_000L\n//      val tokenReceivedMilliPoint = BigNat\n//        .fromBigInt(daoActivityToMilliPoint(tokenReceivedActivity))\n//        .toOption\n//        .getOrElse(BigNat.Zero)\n//      val milliPoint: BigNat =\n//        if totalReceivedMilliPoint.toBigInt < limit then tokenReceivedMilliPoint\n//        else\n//          BigNat.unsafeFromBigInt(\n//            limit,\n//          ) * tokenReceivedMilliPoint / totalReceivedMilliPoint\n//      milliPoint * BigNat.unsafeFromBigInt(BigInt(10).pow(15))\n//\n//  def getTotalRarityRewardAmount[F[_]\n//    : Concurrent: BlockRepository: TransactionRepository: GenericStateRepository.TokenState](\n//      rewardAmount: Option[BigNat],\n//      daoAccount: Option[Account],\n//  ): EitherT[F, String, BigNat] = rewardAmount match\n//    case Some(amount) => EitherT.pure(amount)\n//    case None =>\n//      val targetAccount =\n//        daoAccount.getOrElse(Account(Utf8.unsafeFrom(\"DAO-M\")))\n//      EitherT.right {\n//        StateReadService.getFreeBalance[F](targetAccount).map { balanceMap =>\n//          balanceMap\n//            .get(TokenDefinitionId(Utf8.unsafeFrom(\"LM\")))\n//            .fold(BigNat.Zero)(_.totalAmount)\n//        }\n//      }\n//\n//  def getUserRarityReward[F[_]\n//    : Concurrent: GenericStateRepository.TokenState: GenericStateRepository.RewardState](\n//      user: Account,\n//      state: GossipDomain.MerkleState.TokenMerkleState,\n//  ): EitherT[F, String, Map[Rarity, BigNat]] =\n//    getUserRarityItem[F](user, state).map(rarityItemsToRewardDetailMap)\n//\n//  def getUserRarityItem[F[_]\n//    : Concurrent: GenericStateRepository.TokenState: GenericStateRepository.RewardState](\n//      user: Account,\n//      state: GossipDomain.MerkleState.TokenMerkleState,\n//  ): EitherT[F, String, List[(Rarity, BigNat)]] =\n//    for\n//      stream <- GenericMerkleTrie\n//        .from[F, (Account, TokenId, Hash.Value[TransactionWithResult]), Unit](\n//          user.toBytes.bits,\n//        )\n//        .runA(state.nftBalanceState)\n//      result <- stream\n//        .takeWhile(_._1.startsWith(user.toBytes.bits))\n//        .evalMap { (keyBits, _) =>\n//          for\n//            decodeResult <- EitherT.fromEither {\n//              ByteDecoder[\n//                (Account, TokenId, Hash.Value[TransactionWithResult]),\n//              ]\n//                .decode(keyBits.bytes)\n//                .leftMap(_.msg)\n//            }\n////            _ <- EitherT.pure{\n////              scribe.info(s\"Decode Result: $decodeResult\")\n////            }\n//            DecodeResult((_, tokenId, _), remainder) = decodeResult\n//            _ <- EitherT.cond[F](\n//              remainder.isEmpty,\n//              (),\n//              s\"Non-empty remainder: $remainder in nft-balance: ${keyBits.bytes}\",\n//            )\n//            nftStateOption <- GenericMerkleTrie\n//              .get[F, TokenId, NftState](tokenId.toBytes.bits)\n//              .runA(state.nftState)\n//            nftState <- EitherT.fromOption(\n//              nftStateOption,\n//              s\"Nft state not found: $tokenId\",\n//            )\n//            /* Set default rarity as 2 */\n//            weight =\n//              if nftState.weight === BigNat.Zero then BigNat.unsafeFromLong(2)\n//              else nftState.weight\n//          yield (nftState.rarity, weight)\n//        }\n//        .compile\n//        .toList\n//    yield result\n//\n//  def rarityItemsToRewardDetailMap(\n//      items: List[(Rarity, BigNat)],\n//  ): Map[Rarity, BigNat] =\n//    items.groupMapReduce(_._1)(_._2)(_ + _)\n//\n//  def getTotalRarityRewardValue[F[_]: Concurrent: GenericStateRepository.TokenState](\n//      user: Account,\n//      state: GossipDomain.MerkleState.TokenMerkleState,\n//  ): EitherT[F, String, BigNat] =\n//    for\n//      keyBitsStream <- GenericMerkleTrie\n//        .from[F, (TokenDefinitionId, Rarity, TokenId), Unit](BitVector.empty)\n//        .runA(state.rarityState)\n//      stream = keyBitsStream\n//        .evalMap { (keyBits, _) =>\n//          EitherT\n//            .fromEither {\n//              ByteDecoder[(TokenDefinitionId, Rarity, TokenId)]\n//                .decode(keyBits.bytes)\n//                .leftMap(_.msg)\n//                .flatMap {\n//                  case DecodeResult((defId, rarity, tokenId), remainder) =>\n//                    if remainder.isEmpty then Right((defId, rarity, tokenId))\n//                    else\n//                      Left(\n//                        s\"Non-empty remainder: $remainder in token: ${keyBits.bytes}\",\n//                      )\n//                }\n//            }\n//        }\n//      ansList <- stream\n//        .evalMapAccumulate((Option.empty[TokenDefinition], BigNat.Zero)) {\n//          case ((Some(tokenDef), acc), (defId, rarity, _))\n//              if tokenDef.id === defId =>\n//            val weight = getWeightfromTokenDef(tokenDef, rarity)\n//            EitherT.rightT[F, String] {\n//              ((Some(tokenDef), weight + acc), ())\n//            }\n//          case ((_, acc), (defId, rarity, _)) =>\n//            for\n//              tokenDefOption <- GenericMerkleTrie\n//                .get[F, TokenDefinitionId, TokenDefinition](defId.toBytes.bits)\n//                .runA(state.tokenDefinitionState)\n//              tokenDef <- EitherT.fromOption(\n//                tokenDefOption,\n//                s\"TokenDefinition not found: $defId\",\n//              )\n//            yield\n//              val weight = getWeightfromTokenDef(tokenDef, rarity)\n//              ((Some(tokenDef), weight + acc), ())\n//        }\n//        .last\n//        .compile\n//        .toList\n//    yield ansList.flatten.headOption.fold(BigNat.Zero)(_._1._2)\n//\n//  def getWeightfromTokenDef(\n//      tokenDef: TokenDefinition,\n//      rarity: Rarity,\n//  ): BigNat = {\n//    for\n//      nftInfo <- tokenDef.nftInfo\n//      weight  <- nftInfo.rarity.get(rarity)\n//    yield weight\n//  }.getOrElse(BigNat.Zero)\n//\n//  def calculateUserRarityRewardValue(\n//      totalRarityRewardAmount: BigNat,\n//      totalNumberOfDao: Int,\n//      userRarityRewardItems: List[(Rarity, BigNat)],\n//      totalRarityRewardValue: BigNat,\n//  ): BigNat =\n//    val e18 = BigInt(10).pow(18)\n//    val limit =\n//      BigNat.unsafeFromBigInt(BigInt(250_000L) * e18 * totalNumberOfDao)\n//    val totalAmount = BigNat.min(totalRarityRewardAmount, limit)\n//    val userRarityReward =\n//      userRarityRewardItems.map(_._2).foldLeft(BigNat.Zero)(BigNat.add)\n//\n//    val ans =\n//      (totalAmount * userRarityReward / totalRarityRewardValue).floorAt(16)\n//\n////    scribe.info(s\"totalRarityRewardAmount: ${totalRarityRewardAmount}\")\n////    scribe.info(s\"limit: ${limit}\")\n////    scribe.info(s\"totalAmount: ${totalAmount}\")\n////    scribe.info(s\"userRarityReward: ${userRarityReward}\")\n////    scribe.info(s\"userRarityRewardValue: ${ans}\")\n//\n//    ans\n//\n//  def isModerator[F[_]: Concurrent: GenericStateRepository.RewardState](\n//      user: Account,\n//      root: GenericMerkleTrieState[GroupId, DaoInfo],\n//  ): EitherT[F, String, Boolean] =\n//    for\n//      stream <- GenericMerkleTrie\n//        .from[F, GroupId, DaoInfo](BitVector.empty)\n//        .runA(root)\n//      ansList <- stream\n//        .exists { case (_, daoInfo) => daoInfo.moderators.contains(user) }\n//        .compile\n//        .toList\n//    yield ansList.head\n\nend RewardService\n"
  },
  {
    "path": "modules/node/src/main/scala/io/leisuremeta/chain/node/service/StateReadService.scala",
    "content": "package io.leisuremeta.chain\npackage node\npackage service\n\nimport cats.Monad\nimport cats.data.{EitherT, StateT}\nimport cats.effect.Concurrent\nimport cats.syntax.eq.given\nimport cats.syntax.either.*\nimport cats.syntax.functor.*\nimport cats.syntax.flatMap.*\nimport cats.syntax.traverse.*\n\nimport fs2.Stream\nimport scodec.bits.ByteVector\n\nimport api.{LeisureMetaChainApi as Api}\nimport api.model.{\n  Account,\n  AccountData,\n  GroupId,\n  GroupData,\n  PublicKeySummary,\n  Transaction,\n  TransactionWithResult,\n}\nimport api.model.account.{EthAddress, ExternalChain, ExternalChainAddress}\nimport api.model.api_model.{\n  AccountInfo,\n  ActivityInfo,\n  BalanceInfo,\n  CreatorDaoInfo,\n  GroupInfo,\n  NftBalanceInfo,\n}\nimport api.model.creator_dao.CreatorDaoId\nimport api.model.reward.{\n  ActivitySnapshot,\n  DaoInfo,\n  OwnershipSnapshot,\n  OwnershipRewardLog,\n}\nimport api.model.token.*\nimport api.model.voting.*\nimport dapp.PlayNommState\nimport lib.codec.byte.ByteEncoder.ops.*\nimport lib.crypto.Hash\nimport lib.datatype.{BigNat, Utf8}\nimport lib.merkle.MerkleTrieState\nimport repository.{BlockRepository, TransactionRepository}\n\nobject StateReadService:\n  def getAccountInfo[F[_]: Concurrent: BlockRepository: PlayNommState](\n      account: Account,\n  ): F[Option[AccountInfo]] = for\n    bestHeaderEither <- BlockRepository[F].bestHeader.value\n    bestHeader <- bestHeaderEither match\n      case Left(err) => Concurrent[F].raiseError(err)\n      case Right(None) =>\n        Concurrent[F].raiseError(new Exception(\"No best header\"))\n      case Right(Some(bestHeader)) => Concurrent[F].pure(bestHeader)\n    merkleState = MerkleTrieState.fromRootOption(bestHeader.stateRoot.main)\n    accountStateEither <- PlayNommState[F].account.name\n      .get(account)\n      .runA(merkleState)\n      .value\n    accountStateOption <- accountStateEither match\n      case Left(err) => Concurrent[F].raiseError(new Exception(err))\n      case Right(accountStateOption) => Concurrent[F].pure(accountStateOption)\n    keyListEither <- PlayNommState[F].account.key\n      .streamWithPrefix(account.toBytes)\n      .runA(merkleState)\n      .flatMap(_.compile.toList)\n      .map(_.map { case ((_, pks), info) => (pks, info) })\n      .value\n    keyList <- keyListEither match\n      case Left(err)      => Concurrent[F].raiseError(new Exception(err))\n      case Right(keyList) => Concurrent[F].pure(keyList)\n  yield accountStateOption.map: accountData =>\n    AccountInfo(\n      externalChainAddresses = accountData.externalChainAddresses,\n      ethAddress = accountData.externalChainAddresses\n        .get(ExternalChain.ETH)\n        .map: address =>\n          EthAddress(address.utf8),\n      guardian = accountData.guardian,\n      memo = accountData.memo,\n      publicKeySummaries = keyList.toMap,\n    )\n\n  def getEthAccount[F[_]: Concurrent: BlockRepository: PlayNommState](\n      ethAddress: EthAddress,\n  ): F[Option[Account]] = for\n    bestHeaderEither <- BlockRepository[F].bestHeader.value\n    bestHeader <- bestHeaderEither match\n      case Left(err) => Concurrent[F].raiseError(err)\n      case Right(None) =>\n        Concurrent[F].raiseError(new Exception(\"No best header\"))\n      case Right(Some(bestHeader)) => Concurrent[F].pure(bestHeader)\n    merkleState = MerkleTrieState.fromRootOption(bestHeader.stateRoot.main)\n    ethStateEither <- PlayNommState[F].account.externalChainAddresses\n      .get((ExternalChain.ETH, ExternalChainAddress(ethAddress.utf8)))\n      .runA(merkleState)\n      .value\n    ethStateOption <- ethStateEither match\n      case Left(err)             => Concurrent[F].raiseError(new Exception(err))\n      case Right(ethStateOption) => Concurrent[F].pure(ethStateOption)\n  yield ethStateOption\n\n  def getGroupInfo[F[_]: Concurrent: BlockRepository: PlayNommState](\n      groupId: GroupId,\n  ): F[Option[GroupInfo]] = for\n    bestHeaderEither <- BlockRepository[F].bestHeader.value\n    bestHeader <- bestHeaderEither match\n      case Left(err) => Concurrent[F].raiseError(err)\n      case Right(None) =>\n        Concurrent[F].raiseError(new Exception(\"No best header\"))\n      case Right(Some(bestHeader)) => Concurrent[F].pure(bestHeader)\n    merkleState = MerkleTrieState.fromRootOption(bestHeader.stateRoot.main)\n    groupDataEither <- PlayNommState[F].group.group\n      .get(groupId)\n      .runA(merkleState)\n      .value\n    groupDataOption <- groupDataEither match\n      case Left(err) => Concurrent[F].raiseError(new Exception(err))\n      case Right(groupDataOption) => Concurrent[F].pure(groupDataOption)\n    accountListEither <- PlayNommState[F].group.groupAccount\n      .streamWithPrefix(groupId.toBytes)\n      .runA(merkleState)\n      .flatMap(_.compile.toList)\n      .map(_.map(_._1._2))\n      .value\n    accountList <- accountListEither match\n      case Left(err)          => Concurrent[F].raiseError(new Exception(err))\n      case Right(accountList) => Concurrent[F].pure(accountList)\n  yield groupDataOption.map: groupData =>\n    GroupInfo(groupData, accountList.toSet)\n\n  def getTokenDef[F[_]: Concurrent: BlockRepository: PlayNommState](\n      tokenDefinitionId: TokenDefinitionId,\n  ): F[Option[TokenDefinition]] = for\n    bestHeaderEither <- BlockRepository[F].bestHeader.value\n    bestHeader <- bestHeaderEither match\n      case Left(err) => Concurrent[F].raiseError(err)\n      case Right(None) =>\n        Concurrent[F].raiseError(new Exception(\"No best header\"))\n      case Right(Some(bestHeader)) => Concurrent[F].pure(bestHeader)\n    merkleState = MerkleTrieState.fromRootOption(bestHeader.stateRoot.main)\n    tokenDefEither <- PlayNommState[F].token.definition\n      .get(tokenDefinitionId)\n      .runA(merkleState)\n      .value\n    tokenDefOption <- tokenDefEither match\n      case Left(err)             => Concurrent[F].raiseError(new Exception(err))\n      case Right(tokenDefOption) => Concurrent[F].pure(tokenDefOption)\n  yield tokenDefOption\n\n  def getBalance[F[_]\n    : Concurrent: BlockRepository: TransactionRepository: PlayNommState](\n      account: Account,\n      movable: Api.Movable,\n  ): F[Map[TokenDefinitionId, BalanceInfo]] = movable match\n    case Api.Movable.Free   => getFreeBalance[F](account)\n    case Api.Movable.Locked => getEntrustBalance[F](account)\n\n  def getFreeBalance[F[_]\n    : Concurrent: BlockRepository: TransactionRepository: PlayNommState](\n      account: Account,\n  ): F[Map[TokenDefinitionId, BalanceInfo]] =\n    for\n      bestHeaderEither <- BlockRepository[F].bestHeader.value\n      bestHeader <- bestHeaderEither match\n        case Left(err) => Concurrent[F].raiseError(err)\n        case Right(None) =>\n          Concurrent[F].raiseError(new Exception(\"No best header\"))\n        case Right(Some(bestHeader)) => Concurrent[F].pure(bestHeader)\n      merkleState = MerkleTrieState.fromRootOption(bestHeader.stateRoot.main)\n//      time0 = System.nanoTime()\n      balanceListEither <- PlayNommState[F].token.fungibleBalance\n        .streamWithPrefix(account.toBytes)\n        .runA(merkleState)\n        .flatMap: stream =>\n          stream\n            .map:\n              case ((account, defId, txHash), _) => (defId, txHash)\n            .compile\n            .toList\n        .value\n//      time1 = System.nanoTime()\n//      _ = scribe.info(s\"balanceList: ${(time1 - time0) / 1e6} ms\")\n      balanceList <- balanceListEither match\n        case Left(err)          => Concurrent[F].raiseError(new Exception(err))\n        case Right(balanceList) => Concurrent[F].pure(balanceList)\n      balanceTxEither <- balanceList\n        .traverse: (defId, txHash) =>\n          TransactionRepository[F]\n            .get(txHash)\n            .map: txWithResultOption =>\n              txWithResultOption.map(txWithResult =>\n                (defId, txHash, txWithResult),\n              )\n        .value\n      balanceTxList <- balanceTxEither match\n        case Left(err) => Concurrent[F].raiseError(new Exception(err.msg))\n        case Right(balanceTxList) => Concurrent[F].pure(balanceTxList.flatten)\n    yield balanceTxList\n      .groupMapReduce(_._1): (_, txHash, txWithResult) =>\n        val info = txWithResult.signedTx.value match\n          case fb: Transaction.FungibleBalance =>\n            fb match\n              case mf: Transaction.TokenTx.MintFungibleToken =>\n                BalanceInfo(\n                  totalAmount = mf.outputs.get(account).getOrElse(BigNat.Zero),\n                  unused = Map(txHash -> txWithResult),\n                )\n              case tf: Transaction.TokenTx.TransferFungibleToken =>\n                BalanceInfo(\n                  totalAmount = tf.outputs.get(account).getOrElse(BigNat.Zero),\n                  unused = Map(txHash -> txWithResult),\n                )\n              case bf: Transaction.TokenTx.BurnFungibleToken =>\n                val amount = txWithResult.result match\n                  case Some(\n                        Transaction.TokenTx.BurnFungibleTokenResult(\n                          outputAmount,\n                        ),\n                      ) =>\n                    outputAmount\n                  case _ => BigNat.Zero\n                BalanceInfo(\n                  totalAmount = amount,\n                  unused = Map(txHash -> txWithResult),\n                )\n              case ef: Transaction.TokenTx.EntrustFungibleToken =>\n                val amount = txWithResult.result.fold(BigNat.Zero) {\n                  case Transaction.TokenTx.EntrustFungibleTokenResult(\n                        remainder,\n                      ) =>\n                    remainder\n                  case _ => BigNat.Zero\n                }\n                BalanceInfo(\n                  totalAmount = amount,\n                  unused = Map(txHash -> txWithResult),\n                )\n              case de: Transaction.TokenTx.DisposeEntrustedFungibleToken =>\n                val amount = de.outputs.get(account).getOrElse(BigNat.Zero)\n                BalanceInfo(\n                  totalAmount = amount,\n                  unused = Map(txHash -> txWithResult),\n                )\n              case or: Transaction.RewardTx.OfferReward =>\n                BalanceInfo(\n                  totalAmount = or.outputs.get(account).getOrElse(BigNat.Zero),\n                  unused = Map(txHash -> txWithResult),\n                )\n              case xr: Transaction.RewardTx.ExecuteReward =>\n                val amount = txWithResult.result.fold(BigNat.Zero) {\n                  case Transaction.RewardTx.ExecuteRewardResult(outputs) =>\n                    outputs.get(account).getOrElse(BigNat.Zero)\n                  case _ => BigNat.Zero\n                }\n                BalanceInfo(\n                  totalAmount = amount,\n                  unused = Map(txHash -> txWithResult),\n                )\n              case xo: Transaction.RewardTx.ExecuteOwnershipReward =>\n                val amount = txWithResult.result.fold(BigNat.Zero) {\n                  case Transaction.RewardTx.ExecuteOwnershipRewardResult(\n                        outputs,\n                      ) =>\n                    outputs.get(account).getOrElse(BigNat.Zero)\n                  case _ => BigNat.Zero\n                }\n                BalanceInfo(\n                  totalAmount = amount,\n                  unused = Map(txHash -> txWithResult),\n                )\n\n          case _ => BalanceInfo(totalAmount = BigNat.Zero, unused = Map.empty)\n//        scribe.info(s\"Amount of ${txHash.toUInt256Bytes.toHex}: ${info.totalAmount}\")\n        info\n      .apply: (a, b) =>\n        BalanceInfo(\n          totalAmount = BigNat.add(a.totalAmount, b.totalAmount),\n          unused = a.unused ++ b.unused,\n        )\n\n  def getEntrustBalance[F[_]\n    : Concurrent: BlockRepository: TransactionRepository: PlayNommState](\n      account: Account,\n  ): F[Map[TokenDefinitionId, BalanceInfo]] =\n    for\n      bestHeaderEither <- BlockRepository[F].bestHeader.value\n      bestHeader <- bestHeaderEither match\n        case Left(err) => Concurrent[F].raiseError(err)\n        case Right(None) =>\n          Concurrent[F].raiseError(new Exception(\"No best header\"))\n        case Right(Some(bestHeader)) => Concurrent[F].pure(bestHeader)\n      merkleState = MerkleTrieState.fromRootOption(bestHeader.stateRoot.main)\n      balanceListEither <- PlayNommState[F].token.entrustFungibleBalance\n        .streamWithPrefix(account.toBytes)\n        .runA(merkleState)\n        .flatMap: stream =>\n          stream\n            .map:\n              case ((account, toAccount, defId, txHash), _) => (defId, txHash)\n            .compile\n            .toList\n        .value\n      balanceList <- balanceListEither match\n        case Left(err)          => Concurrent[F].raiseError(new Exception(err))\n        case Right(balanceList) => Concurrent[F].pure(balanceList)\n      balanceTxEither <- balanceList\n        .traverse: (defId, txHash) =>\n          TransactionRepository[F]\n            .get(txHash)\n            .map: txWithResultOption =>\n              txWithResultOption.map(txWithResult =>\n                (defId, txHash, txWithResult),\n              )\n        .value\n      balanceTxList <- balanceTxEither match\n        case Left(err) => Concurrent[F].raiseError(new Exception(err.msg))\n        case Right(balanceTxList) => Concurrent[F].pure(balanceTxList.flatten)\n    yield balanceTxList\n      .groupMapReduce(_._1): (_, txHash, txWithResult) =>\n        txWithResult.signedTx.value match\n          case ef: Transaction.TokenTx.EntrustFungibleToken =>\n            BalanceInfo(\n              totalAmount = ef.amount,\n              unused = Map(txHash -> txWithResult),\n            )\n          case _ => BalanceInfo(totalAmount = BigNat.Zero, unused = Map.empty)\n      .apply: (a, b) =>\n        BalanceInfo(\n          totalAmount = BigNat.add(a.totalAmount, b.totalAmount),\n          unused = a.unused ++ b.unused,\n        )\n\n  def getNftBalance[F[_]\n    : Concurrent: BlockRepository: TransactionRepository: PlayNommState](\n      account: Account,\n      movableOption: Option[Api.Movable],\n  ): F[Map[TokenId, NftBalanceInfo]] = movableOption match\n    case None                     => getAllNftBalance[F](account)\n    case Some(Api.Movable.Free)   => getFreeNftBalance[F](account)\n    case Some(Api.Movable.Locked) => getEntrustedNftBalance[F](account)\n\n  def getAllNftBalance[F[_]\n    : Concurrent: BlockRepository: TransactionRepository: PlayNommState](\n      account: Account,\n  ): F[Map[TokenId, NftBalanceInfo]] = for\n    free      <- getFreeNftBalance[F](account)\n    entrusted <- getEntrustedNftBalance[F](account)\n  yield free ++ entrusted\n\n  def getFreeNftBalance[F[_]\n    : Concurrent: BlockRepository: TransactionRepository: PlayNommState](\n      account: Account,\n  ): F[Map[TokenId, NftBalanceInfo]] = for\n    bestHeaderEither <- BlockRepository[F].bestHeader.value\n    bestHeader <- bestHeaderEither match\n      case Left(err) => Concurrent[F].raiseError(err)\n      case Right(None) =>\n        Concurrent[F].raiseError(new Exception(\"No best header\"))\n      case Right(Some(bestHeader)) => Concurrent[F].pure(bestHeader)\n    merkleState = MerkleTrieState.fromRootOption(bestHeader.stateRoot.main)\n    nftBalanceMap <- getNftBalanceFromNftBalanceState[F](\n      account,\n      merkleState,\n    )\n  yield nftBalanceMap\n\n  def getEntrustedNftBalance[F[_]\n    : Concurrent: BlockRepository: TransactionRepository: PlayNommState](\n      account: Account,\n  ): F[Map[TokenId, NftBalanceInfo]] = for\n    bestHeaderEither <- BlockRepository[F].bestHeader.value\n    bestHeader <- bestHeaderEither match\n      case Left(err) => Concurrent[F].raiseError(err)\n      case Right(None) =>\n        Concurrent[F].raiseError(new Exception(\"No best header\"))\n      case Right(Some(bestHeader)) => Concurrent[F].pure(bestHeader)\n    merkleState = MerkleTrieState.fromRootOption(bestHeader.stateRoot.main)\n    nftBalanceMap <- getEntrustedNftBalanceFromEntrustedNftBalanceState[F](\n      account,\n      merkleState,\n    )\n  yield nftBalanceMap\n\n  def getNftBalanceFromNftBalanceState[F[_]\n    : Concurrent: BlockRepository: TransactionRepository: PlayNommState](\n      account: Account,\n      mts: MerkleTrieState,\n  ): F[Map[TokenId, NftBalanceInfo]] = for\n    balanceListEither <- PlayNommState[F].token.nftBalance\n      .streamWithPrefix(account.toBytes)\n      .runA(mts)\n      .flatMap: stream =>\n        stream\n          .map:\n            case ((account, tokenId, txHash), _) => (tokenId, txHash)\n          .compile\n          .toList\n      .value\n    balanceList <- balanceListEither match\n      case Left(err)          => Concurrent[F].raiseError(new Exception(err))\n      case Right(balanceList) => Concurrent[F].pure(balanceList)\n    balanceTxEither <- balanceList\n      .traverse: (tokenId, txHash) =>\n        TransactionRepository[F]\n          .get(txHash)\n          .map: txWithResultOption =>\n            txWithResultOption.map: txWithResult =>\n              txWithResult.signedTx.value match\n                case nb: Transaction.NftBalance =>\n                  nb match\n                    case mf: Transaction.TokenTx.MintNFT =>\n                      Map:\n                        tokenId -> NftBalanceInfo(\n                          mf.tokenDefinitionId,\n                          txHash,\n                          txWithResult,\n                          None,\n                        )\n                    case mfm: Transaction.TokenTx.MintNFTWithMemo =>\n                      Map:\n                        tokenId -> NftBalanceInfo(\n                          mfm.tokenDefinitionId,\n                          txHash,\n                          txWithResult,\n                          mfm.memo,\n                        )\n                    case tn: Transaction.TokenTx.TransferNFT =>\n                      Map:\n                        tokenId -> NftBalanceInfo(\n                          tn.definitionId,\n                          txHash,\n                          txWithResult,\n                          tn.memo,\n                        )\n                    case de: Transaction.TokenTx.DisposeEntrustedNFT =>\n                      Map:\n                        tokenId -> NftBalanceInfo(\n                          de.definitionId,\n                          txHash,\n                          txWithResult,\n                          None,\n                        )\n                case _ =>\n                  Map.empty\n      .value\n    balanceTxList <- balanceTxEither match\n      case Left(err) => Concurrent[F].raiseError(new Exception(err.msg))\n      case Right(balanceTxList) => Concurrent[F].pure(balanceTxList.flatten)\n  yield balanceTxList.foldLeft(Map.empty)(_ ++ _)\n\n  def getEntrustedNftBalanceFromEntrustedNftBalanceState[F[_]\n    : Concurrent: BlockRepository: TransactionRepository: PlayNommState](\n      account: Account,\n      mts: MerkleTrieState,\n  ): F[Map[TokenId, NftBalanceInfo]] = for\n    balanceListEither <- PlayNommState[F].token.entrustNftBalance\n      .streamWithPrefix(account.toBytes)\n      .runA(mts)\n      .flatMap: stream =>\n        stream\n          .map:\n            case ((account, toAccount, tokenId, txHash), _) => (tokenId, txHash)\n          .compile\n          .toList\n      .value\n    balanceList <- balanceListEither match\n      case Left(err)          => Concurrent[F].raiseError(new Exception(err))\n      case Right(balanceList) => Concurrent[F].pure(balanceList)\n    balanceTxEither <- balanceList\n      .traverse: (tokenId, txHash) =>\n        TransactionRepository[F]\n          .get(txHash)\n          .map: txWithResultOption =>\n            txWithResultOption.map: txWithResult =>\n              txWithResult.signedTx.value match\n                case en: Transaction.TokenTx.EntrustNFT =>\n                  Map:\n                    tokenId -> NftBalanceInfo(\n                      en.definitionId,\n                      txHash,\n                      txWithResult,\n                      None,\n                    )\n                case _ =>\n                  Map.empty\n      .value\n    balanceTxList <- balanceTxEither match\n      case Left(err) => Concurrent[F].raiseError(new Exception(err.msg))\n      case Right(balanceTxList) => Concurrent[F].pure(balanceTxList.flatten)\n  yield balanceTxList.foldLeft(Map.empty)(_ ++ _)\n\n  def getToken[F[_]: Concurrent: BlockRepository: PlayNommState](\n      tokenId: TokenId,\n  ): EitherT[F, String, Option[NftState]] = for\n    bestHeaderOption <- BlockRepository[F].bestHeader.leftMap(_.msg)\n    bestHeader <- EitherT.fromOption[F](bestHeaderOption, \"No best header\")\n    merkleState = MerkleTrieState.fromRootOption(bestHeader.stateRoot.main)\n    nftStateOption <- PlayNommState[F].token.nftState\n      .get(tokenId)\n      .runA(merkleState)\n  yield nftStateOption\n\n  def getTokenHistory[F[_]: Concurrent: BlockRepository: PlayNommState](\n      txHash: Hash.Value[TransactionWithResult],\n  ): EitherT[F, String, Option[NftState]] = for\n    bestHeaderOption <- BlockRepository[F].bestHeader.leftMap(_.msg)\n    bestHeader <- EitherT.fromOption[F](bestHeaderOption, \"No best header\")\n    merkleState = MerkleTrieState.fromRootOption(bestHeader.stateRoot.main)\n    nftStateOption <- PlayNommState[F].token.nftHistory\n      .get(txHash)\n      .runA(merkleState)\n  yield nftStateOption\n\n  def getOwners[F[_]: Concurrent: BlockRepository: PlayNommState](\n      tokenDefinitionId: TokenDefinitionId,\n  ): EitherT[F, String, Map[TokenId, Account]] = for\n    bestHeaderOption <- BlockRepository[F].bestHeader.leftMap(_.msg)\n    bestHeader <- EitherT.fromOption[F](bestHeaderOption, \"No best header\")\n    merkleState = MerkleTrieState.fromRootOption(bestHeader.stateRoot.main)\n    tokenIdList <- PlayNommState[F].token.rarityState\n      .streamWithPrefix(tokenDefinitionId.toBytes)\n      .runA(merkleState)\n      .flatMap(stream => stream.map(_._1._3).compile.toList)\n    ownerOptionList <- tokenIdList.traverse: (tokenId: TokenId) =>\n      PlayNommState[F].token.nftState\n        .get(tokenId)\n        .runA(merkleState)\n        .map: nftStateOption =>\n          nftStateOption.map(state => (tokenId, state.currentOwner))\n  yield ownerOptionList.flatten.toMap\n\n  def getAccountActivity[F[_]: Concurrent: BlockRepository: PlayNommState](\n      account: Account,\n  ): EitherT[F, Either[String, String], Seq[ActivityInfo]] =\n\n    val program = PlayNommState[F].reward.accountActivity\n      .streamWithPrefix(account.toBytes)\n      .map: stream =>\n        stream\n          .takeWhile(_._1._1 === account)\n          .flatMap:\n            case ((account, instant), logs) =>\n              Stream.emits:\n                logs.map: log =>\n                  ActivityInfo(\n                    timestamp = instant,\n                    point = log.point,\n                    description = log.description,\n                    txHash = log.txHash,\n                  )\n          .compile\n          .toList\n\n    for\n      bestHeaderOption <- BlockRepository[F].bestHeader.leftMap: e =>\n        Left(e.msg)\n      bestHeader <- EitherT\n        .fromOption[F](bestHeaderOption, Left(\"No best header\"))\n      merkleState = MerkleTrieState.fromRootOption(bestHeader.stateRoot.main)\n      infosEitherT <- program.runA(merkleState).leftMap(_.asLeft[String])\n      infos        <- infosEitherT.leftMap(_.asLeft[String])\n    yield infos.toSeq\n\n  def getTokenActivity[F[_]: Concurrent: BlockRepository: PlayNommState](\n      tokenId: TokenId,\n  ): EitherT[F, Either[String, String], Seq[ActivityInfo]] =\n\n    val program = PlayNommState[F].reward.tokenReceived\n      .streamWithPrefix(tokenId.toBytes)\n      .map: stream =>\n        stream\n          .takeWhile(_._1._1 === tokenId)\n          .flatMap:\n            case ((tokenId, instant), logs) =>\n              Stream.emits:\n                logs.map: log =>\n                  ActivityInfo(\n                    timestamp = instant,\n                    point = log.point,\n                    description = log.description,\n                    txHash = log.txHash,\n                  )\n          .compile\n          .toList\n\n    for\n      bestHeaderOption <- BlockRepository[F].bestHeader.leftMap: e =>\n        Left(e.msg)\n      bestHeader <- EitherT\n        .fromOption[F](bestHeaderOption, Left(\"No best header\"))\n      merkleState = MerkleTrieState.fromRootOption(bestHeader.stateRoot.main)\n      infosEitherT <- program.runA(merkleState).leftMap(_.asLeft[String])\n      infos        <- infosEitherT.leftMap(_.asLeft[String])\n    yield infos.toSeq\n\n  def getAccountSnapshot[F[_]: Concurrent: BlockRepository: PlayNommState](\n      account: Account,\n  ): EitherT[F, Either[String, String], Option[ActivitySnapshot]] =\n\n    val program = PlayNommState[F].reward.accountSnapshot.get(account)\n\n    for\n      bestHeaderOption <- BlockRepository[F].bestHeader.leftMap: e =>\n        Left(e.msg)\n      bestHeader <- EitherT\n        .fromOption[F](bestHeaderOption, Left(\"No best header\"))\n      merkleState = MerkleTrieState.fromRootOption(bestHeader.stateRoot.main)\n      snapshotOption <- program.runA(merkleState).leftMap(_.asLeft[String])\n    yield snapshotOption\n\n  def getTokenSnapshot[F[_]: Concurrent: BlockRepository: PlayNommState](\n      tokenId: TokenId,\n  ): EitherT[F, Either[String, String], Option[ActivitySnapshot]] =\n\n    val program = PlayNommState[F].reward.tokenSnapshot.get(tokenId)\n\n    for\n      bestHeaderOption <- BlockRepository[F].bestHeader.leftMap: e =>\n        Left(e.msg)\n      bestHeader <- EitherT\n        .fromOption[F](bestHeaderOption, Left(\"No best header\"))\n      merkleState = MerkleTrieState.fromRootOption(bestHeader.stateRoot.main)\n      snapshotOption <- program.runA(merkleState).leftMap(_.asLeft[String])\n    yield snapshotOption\n\n  def getOwnershipSnapshot[F[_]: Concurrent: BlockRepository: PlayNommState](\n      tokenId: TokenId,\n  ): EitherT[F, Either[String, String], Option[OwnershipSnapshot]] =\n\n    val program = PlayNommState[F].reward.ownershipSnapshot.get(tokenId)\n\n    for\n      bestHeaderOption <- BlockRepository[F].bestHeader.leftMap: e =>\n        Left(e.msg)\n      bestHeader <- EitherT\n        .fromOption[F](bestHeaderOption, Left(\"No best header\"))\n      merkleState = MerkleTrieState.fromRootOption(bestHeader.stateRoot.main)\n      snapshotOption <- program.runA(merkleState).leftMap(_.asLeft[String])\n    yield snapshotOption\n\n  def getOwnershipSnapshotMap[F[_]: Concurrent: BlockRepository: PlayNommState](\n      from: Option[TokenId],\n      limit: Int,\n  ): EitherT[F, Either[String, String], Map[TokenId, OwnershipSnapshot]] =\n\n    val program = PlayNommState[F].reward.ownershipSnapshot\n      .streamWithPrefix(from.fold(ByteVector.empty)(_.toBytes))\n\n    for\n      bestHeaderOption <- BlockRepository[F].bestHeader.leftMap: e =>\n        Left(e.msg)\n      bestHeader <- EitherT\n        .fromOption[F](bestHeaderOption, Left(\"No best header\"))\n      merkleState = MerkleTrieState.fromRootOption(bestHeader.stateRoot.main)\n      snapshotStream <- program.runA(merkleState).leftMap(_.asLeft[String])\n      snapshots <- snapshotStream\n        .take(limit)\n        .compile\n        .toList\n        .leftMap(_.asLeft[String])\n    yield snapshots.toMap\n\n  def getOwnershipRewarded[F[_]: Concurrent: BlockRepository: PlayNommState](\n      tokenId: TokenId,\n  ): EitherT[F, Either[String, String], Option[OwnershipRewardLog]] =\n\n    val program = PlayNommState[F].reward.ownershipRewarded.get(tokenId)\n\n    for\n      bestHeaderOption <- BlockRepository[F].bestHeader.leftMap: e =>\n        Left(e.msg)\n      bestHeader <- EitherT\n        .fromOption[F](bestHeaderOption, Left(\"No best header\"))\n      merkleState = MerkleTrieState.fromRootOption(bestHeader.stateRoot.main)\n      logOption <- program.runA(merkleState).leftMap(_.asLeft[String])\n    yield logOption\n\n  def getDaoInfo[F[_]: Monad: BlockRepository: PlayNommState](\n      groupId: GroupId,\n  ): EitherT[F, Either[String, String], Option[DaoInfo]] =\n    val program = PlayNommState[F].reward.dao.get(groupId)\n\n    for\n      bestHeaderOption <- BlockRepository[F].bestHeader.leftMap: e =>\n        Left(e.msg)\n      bestHeader <- EitherT\n        .fromOption[F](bestHeaderOption, Left(\"No best header\"))\n      merkleState = MerkleTrieState.fromRootOption(bestHeader.stateRoot.main)\n      infoOption <- program.runA(merkleState).leftMap(_.asLeft[String])\n    yield infoOption\n\n  def getSnapshotState[F[_]: Monad: BlockRepository: PlayNommState](\n      defId: TokenDefinitionId,\n  ): EitherT[F, Either[String, String], Option[SnapshotState]] =\n    val program = PlayNommState[F].token.snapshotState.get(defId)\n\n    for\n      bestHeaderOption <- BlockRepository[F].bestHeader.leftMap: e =>\n        Left(e.msg)\n      bestHeader <- EitherT\n        .fromOption[F](bestHeaderOption, Left(\"No best header\"))\n      merkleState = MerkleTrieState.fromRootOption(bestHeader.stateRoot.main)\n      stateOption <- program.runA(merkleState).leftMap(_.asLeft[String])\n    yield stateOption\n\n  def getFungibleSnapshotBalance[F[_]\n    : Concurrent: BlockRepository: PlayNommState](\n      account: Account,\n      defId: TokenDefinitionId,\n      snapshotId: SnapshotState.SnapshotId,\n  ): EitherT[\n    F,\n    Either[String, String],\n    Map[Hash.Value[TransactionWithResult], BigNat],\n  ] =\n    val program = PlayNommState[F].token.fungibleSnapshot\n      .reverseStreamFrom((account, defId).toBytes, Some(snapshotId.toBytes))\n      .flatMap: stream =>\n        StateT.liftF:\n          stream.head.compile.toList.map:\n            case Nil         => Map.empty\n            case (k, v) :: _ => v\n    for\n      bestHeaderOption <- BlockRepository[F].bestHeader.leftMap: e =>\n        Left(e.msg)\n      bestHeader <- EitherT\n        .fromOption[F](bestHeaderOption, Left(\"No best header\"))\n      merkleState = MerkleTrieState.fromRootOption(bestHeader.stateRoot.main)\n      balanceMap <- program.runA(merkleState).leftMap(_.asLeft[String])\n    yield balanceMap\n\n  def getNftSnapshotBalance[F[_]: Concurrent: BlockRepository: PlayNommState](\n      account: Account,\n      defId: TokenDefinitionId,\n      snapshotId: SnapshotState.SnapshotId,\n  ): EitherT[F, Either[String, String], Set[TokenId]] =\n    val program = PlayNommState[F].token.nftSnapshot\n      .reverseStreamFrom((account, defId).toBytes, Some(snapshotId.toBytes))\n      .flatMap: stream =>\n        StateT.liftF:\n          stream.head.compile.toList.map:\n            case Nil              => Set.empty\n            case (_, tokens) :: _ => tokens\n    for\n      bestHeaderOption <- BlockRepository[F].bestHeader.leftMap: e =>\n        Left(e.msg)\n      bestHeader <- EitherT\n        .fromOption[F](bestHeaderOption, Left(\"No best header\"))\n      merkleState = MerkleTrieState.fromRootOption(bestHeader.stateRoot.main)\n      balanceTokens <- program.runA(merkleState).leftMap(_.asLeft[String])\n    yield balanceTokens\n\n  def getVoteProposal[F[_]: Concurrent: BlockRepository: PlayNommState](\n      proposalId: ProposalId,\n  ): EitherT[F, Either[String, String], Option[Proposal]] =\n    val program = PlayNommState[F].voting.proposal.get(proposalId)\n\n    for\n      bestHeaderOption <- BlockRepository[F].bestHeader.leftMap: e =>\n        Left(e.msg)\n      bestHeader <- EitherT\n        .fromOption[F](bestHeaderOption, Left(\"No best header\"))\n      merkleState = MerkleTrieState.fromRootOption(bestHeader.stateRoot.main)\n      proposalOption <- program.runA(merkleState).leftMap(_.asLeft[String])\n    yield proposalOption\n\n  def getAccountVotes[F[_]: Concurrent: BlockRepository: PlayNommState](\n      proposalId: ProposalId,\n      voter: Account,\n  ): EitherT[F, Either[String, String], Option[(Utf8, BigNat)]] =\n    val program = PlayNommState[F].voting.votes.get((proposalId, voter))\n\n    for\n      bestHeaderOption <- BlockRepository[F].bestHeader.leftMap: e =>\n        Left(e.msg)\n      bestHeader <- EitherT\n        .fromOption[F](bestHeaderOption, Left(\"No best header\"))\n      merkleState = MerkleTrieState.fromRootOption(bestHeader.stateRoot.main)\n      voteOption <- program.runA(merkleState).leftMap(_.asLeft[String])\n    yield voteOption\n\n  def getVoteCount[F[_]: Concurrent: BlockRepository: PlayNommState](\n      proposalId: ProposalId,\n  ): EitherT[F, Either[String, String], Map[Utf8, BigNat]] =\n    val program = PlayNommState[F].voting.counting.get(proposalId)\n\n    for\n      bestHeaderOption <- BlockRepository[F].bestHeader.leftMap: e =>\n        Left(e.msg)\n      bestHeader <- EitherT\n        .fromOption[F](bestHeaderOption, Left(\"No best header\"))\n      merkleState = MerkleTrieState.fromRootOption(bestHeader.stateRoot.main)\n      countOption <- program.runA(merkleState).leftMap(_.asLeft[String])\n    yield countOption.getOrElse(Map.empty)\n\n  def getCreatorDaoInfo[F[_]: Concurrent: BlockRepository: PlayNommState](\n      daoId: CreatorDaoId,\n  ): EitherT[F, Either[String, String], Option[CreatorDaoInfo]] =\n    val program = for\n      daoInfoOption <- PlayNommState[F].creatorDao.dao.get(daoId)\n      moderatorList <- PlayNommState[F].creatorDao.daoModerators\n        .streamWithPrefix(daoId.toBytes)\n        .flatMap: stream =>\n          StateT.liftF:\n            stream\n              .map(_._1._2)\n              .compile\n              .toList\n    yield daoInfoOption.map: daoInfo =>\n      CreatorDaoInfo(\n        id = daoId,\n        name = daoInfo.name,\n        description = daoInfo.description,\n        founder = daoInfo.founder,\n        coordinator = daoInfo.coordinator,\n        moderators = moderatorList.toSet,\n      )\n\n    for\n      bestHeaderOption <- BlockRepository[F].bestHeader.leftMap: e =>\n        Left(e.msg)\n      bestHeader <- EitherT\n        .fromOption[F](bestHeaderOption, Left(\"No best header\"))\n      merkleState = MerkleTrieState.fromRootOption(bestHeader.stateRoot.main)\n      daoInfoOption <- program.runA(merkleState).leftMap(_.asLeft[String])\n    yield daoInfoOption\n    \n  def getCreatorDaoMember[F[_]: Concurrent: BlockRepository: PlayNommState](\n      daoId: CreatorDaoId,\n      from: Option[Account],\n      limit: Int,\n  ): EitherT[F, Either[String, String], Seq[Account]] =\n    val program = PlayNommState[F].creatorDao.daoMembers\n      .streamFrom(daoId.toBytes ++ from.fold(ByteVector.empty)(_.toBytes))\n      .flatMap: stream =>\n        StateT.liftF:\n          stream\n            .map(_._1._2)\n            .limit(limit)\n            .compile\n            .toList\n    for\n      bestHeaderOption <- BlockRepository[F].bestHeader.leftMap: e =>\n        Left(e.msg)\n      bestHeader <- EitherT\n        .fromOption[F](bestHeaderOption, Left(\"No best header\"))\n      merkleState = MerkleTrieState.fromRootOption(bestHeader.stateRoot.main)\n      memberList <- program.runA(merkleState).leftMap(_.asLeft[String])\n    yield memberList\n"
  },
  {
    "path": "modules/node/src/main/scala/io/leisuremeta/chain/node/service/TransactionService.scala",
    "content": "package io.leisuremeta.chain\npackage node\npackage service\n\nimport cats.{Functor, Monad}\nimport cats.data.{EitherT, Kleisli}\nimport cats.effect.{Clock, Concurrent, Resource}\nimport cats.effect.std.Semaphore\nimport cats.syntax.functor.*\nimport cats.syntax.traverse.*\n\nimport scodec.bits.ByteVector\n\nimport api.model.{Block, Signed, StateRoot, Transaction, TransactionWithResult}\nimport api.model.Block.ops.*\nimport api.model.TransactionWithResult.ops.*\nimport api.model.api_model.TxInfo\nimport dapp.{PlayNommDApp, PlayNommDAppFailure, PlayNommState}\nimport lib.crypto.{Hash, KeyPair}\nimport lib.crypto.Hash.ops.*\nimport lib.crypto.Sign.ops.*\nimport lib.datatype.BigNat\nimport lib.merkle.*\nimport lib.merkle.MerkleTrie.NodeStore\nimport repository.{BlockRepository, StateRepository, TransactionRepository}\n\nobject TransactionService:\n  def submit[F[_]\n    : Concurrent: Clock: BlockRepository: TransactionRepository: StateRepository: PlayNommState](\n      semaphore: Semaphore[F],\n      txs: Seq[Signed.Tx],\n      localKeyPair: KeyPair,\n  ): EitherT[F, PlayNommDAppFailure, Seq[Hash.Value[TransactionWithResult]]] =\n    Resource\n      .make:\n        EitherT\n          .right[PlayNommDAppFailure](semaphore.acquire)\n          .map: _ =>\n            scribe.info(s\"Lock Acquired: $txs\")\n            ()\n      .apply: _ =>\n        EitherT.right[PlayNommDAppFailure]:\n          semaphore.release.map: _ =>\n            scribe.info(s\"Lock Released: $txs\")\n            ()\n      .use: _ =>\n        submit0[F](txs, localKeyPair)\n\n  private def submit0[F[_]\n    : Concurrent: Clock: BlockRepository: TransactionRepository: StateRepository: PlayNommState](\n      txs: Seq[Signed.Tx],\n      localKeyPair: KeyPair,\n  ): EitherT[F, PlayNommDAppFailure, Seq[Hash.Value[TransactionWithResult]]] =\n    for\n      time0 <- EitherT.liftF(Clock[F].realTime)\n      bestBlockHeaderOption <- BlockRepository[F].bestHeader.leftMap: e =>\n        scribe.error(s\"Best Header Error: $e\")\n        PlayNommDAppFailure.internal(s\"Fail to get best header: ${e.msg}\")\n      bestBlockHeader <- EitherT.fromOption[F](\n        bestBlockHeaderOption,\n        PlayNommDAppFailure.internal(\"No best Header Available\"),\n      )\n      baseState = MerkleTrieState.fromRootOption(bestBlockHeader.stateRoot.main)\n      result <- txs\n        .traverse(PlayNommDApp[F])\n        .run(baseState)\n      (finalState, txWithResults) = result\n      txHashes                    = txWithResults.map(_.toHash)\n      txState = txs\n        .map(_.toHash)\n        .sortBy(_.toUInt256Bytes.toBytes)\n        .foldLeft(MerkleTrieState.empty): (state, txHash) =>\n          given idNodeStore: NodeStore[cats.Id] = Kleisli.pure(None)\n          MerkleTrie\n            .put[cats.Id](\n              txHash.toUInt256Bytes.toBytes.toNibbles,\n              ByteVector.empty,\n            )\n            .runS(state)\n            .value\n            .getOrElse(state)\n      stateRoot1 = StateRoot(finalState.root)\n      now <- EitherT.right(Clock[F].realTimeInstant)\n      header = Block.Header(\n        number = BigNat.add(bestBlockHeader.number, BigNat.One),\n        parentHash = bestBlockHeader.toHash.toBlockHash,\n        stateRoot = stateRoot1,\n        transactionsRoot = txState.root,\n        timestamp = now,\n      )\n      sig <- EitherT\n        .fromEither(header.toHash.signBy(localKeyPair))\n        .leftMap: msg =>\n          scribe.error(s\"Fail to sign header: $msg\")\n          PlayNommDAppFailure.internal(s\"Fail to sign header: $msg\")\n      block = Block(\n        header = header,\n        transactionHashes = txHashes.toSet.map(_.toSignedTxHash),\n        votes = Set(sig),\n      )\n      _ <- BlockRepository[F]\n        .put(block)\n        .leftMap: e =>\n          scribe.error(s\"Fail to put block: $e\")\n          PlayNommDAppFailure.internal(s\"Fail to put block: ${e.msg}\")\n      _ <- StateRepository[F]\n        .put(finalState)\n        .leftMap: e =>\n          scribe.error(s\"Fail to put state: $e\")\n          PlayNommDAppFailure.internal(s\"Fail to put state: ${e.msg}\")\n      _ <- txWithResults.traverse: txWithResult =>\n        EitherT.liftF:\n          TransactionRepository[F].put(txWithResult)\n      time1 <- EitherT.right(Clock[F].realTime)\n    yield\n      scribe.info(s\"total time consumed: ${time1 - time0}\")\n      txHashes\n\n  def index[F[_]: Monad: BlockRepository: TransactionRepository](\n      blockHash: Block.BlockHash,\n  ): EitherT[F, Either[String, String], Set[TxInfo]] = for\n    blockOption <- BlockRepository[F].get(blockHash).leftMap(e => Left(e.msg))\n    block <- EitherT\n      .fromOption[F](blockOption, Right(s\"block not found: $blockHash\"))\n    txInfoSet <- block.transactionHashes.toList\n      .traverse { (txHash) =>\n        for\n          txOption <- TransactionRepository[F]\n            .get(txHash.toResultHashValue)\n            .leftMap(e => Left(e.msg))\n          tx <- EitherT.fromOption[F](\n            txOption,\n            Left(s\"tx not found: $txHash in block $blockHash\"),\n          )\n        yield\n          val txType: String = tx.signedTx.value match\n            case tx: Transaction.AccountTx    => \"Account\"\n            case tx: Transaction.GroupTx      => \"Group\"\n            case tx: Transaction.TokenTx      => \"Token\"\n            case tx: Transaction.RewardTx     => \"Reward\"\n            case tx: Transaction.AgendaTx     => \"Agenda\"\n            case tx: Transaction.VotingTx     => \"Voting\"\n            case tx: Transaction.CreatorDaoTx => \"CreatorDao\"\n\n          TxInfo(\n            txHash = txHash,\n            createdAt = tx.signedTx.value.createdAt,\n            account = tx.signedTx.sig.account,\n            `type` = txType,\n          )\n      }\n      .map(_.toSet)\n  yield txInfoSet\n\n  def get[F[_]: Functor: TransactionRepository](\n      txHash: Signed.TxHash,\n  ): EitherT[F, String, Option[TransactionWithResult]] =\n    TransactionRepository[F].get(txHash.toResultHashValue).leftMap(_.msg)\n"
  },
  {
    "path": "modules/node/src/main/scala/io/leisuremeta/chain/node/store/HashStore.scala",
    "content": "package io.leisuremeta.chain\npackage node.store\n\nimport cats.data.EitherT\n\nimport lib.crypto.Hash\nimport lib.crypto.Hash.ops.*\nimport lib.failure.DecodingFailure\n\ntrait HashStore[F[_], A]:\n  def get(hash: Hash.Value[A]): EitherT[F, DecodingFailure, Option[A]]\n  def put(a: A): F[Unit]\n  def remove(hash: Hash.Value[A]): F[Unit]\n\nobject HashStore:\n  given fromKeyValueStore[F[_], A: Hash](using\n      kvStore: KeyValueStore[F, Hash.Value[A], A],\n  ): HashStore[F, A] = new HashStore[F, A]:\n    override def get(\n        hash: Hash.Value[A],\n    ): EitherT[F, DecodingFailure, Option[A]] = kvStore.get(hash)\n    override def put(a: A): F[Unit]                   = kvStore.put(a.toHash, a)\n    override def remove(hash: Hash.Value[A]): F[Unit] = kvStore.remove(hash)\n"
  },
  {
    "path": "modules/node/src/main/scala/io/leisuremeta/chain/node/store/KeyValueStore.scala",
    "content": "package io.leisuremeta.chain\npackage node.store\n\nimport cats.data.EitherT\nimport lib.failure.DecodingFailure\n\ntrait KeyValueStore[F[_], K, V]:\n  def get(key: K): EitherT[F, DecodingFailure, Option[V]]\n  def put(key: K, value: V): F[Unit]\n  def remove(key: K): F[Unit]\n"
  },
  {
    "path": "modules/node/src/main/scala/io/leisuremeta/chain/node/store/SingleValueStore.scala",
    "content": "package io.leisuremeta.chain\npackage node.store\n\nimport cats.data.EitherT\n\nimport lib.datatype.{UInt256, UInt256Bytes}\nimport lib.failure.DecodingFailure\n\ntrait SingleValueStore[F[_], A]:\n  def get(): EitherT[F, DecodingFailure, Option[A]]\n  def put(a: A): F[Unit]\n\nobject SingleValueStore:\n  def fromKeyValueStore[F[_], A](using\n      kvStore: KeyValueStore[F, UInt256Bytes, A],\n  ): SingleValueStore[F, A] = new SingleValueStore[F, A]:\n    override def get(): EitherT[F, DecodingFailure, Option[A]] =\n      kvStore.get(Key)\n    override def put(a: A): F[Unit] = kvStore.put(Key, a)\n\n  private val Key: UInt256Bytes = UInt256.EmptyBytes\n"
  },
  {
    "path": "modules/node/src/main/scala/io/leisuremeta/chain/node/store/interpreter/Bag.scala",
    "content": "package io.leisuremeta.chain.node.store.interpreter\n\nimport cats.effect.IO\nimport cats.effect.unsafe.IORuntime\nimport swaydb.Bag.Async\nimport swaydb.{IO as SwayIO}\n\nimport scala.concurrent.{ExecutionContext, Future, Promise}\nimport scala.util.Failure\n\nobject Bag:\n  /** Cats-effect 3 async bag implementation\n    */\n  given swaydb.Bag.Async[IO] =\n    new Async[IO]:\n      self =>\n\n      given runtime: IORuntime = cats.effect.unsafe.implicits.global\n      override def executionContext: ExecutionContext =\n        runtime.compute\n\n      override val unit: IO[Unit] =\n        IO.unit\n\n      override def none[A]: IO[Option[A]] =\n        IO.pure(Option.empty)\n\n      override def apply[A](a: => A): IO[A] =\n        IO(a)\n\n      override def map[A, B](a: IO[A])(f: A => B): IO[B] =\n        a.map(f)\n\n      override def transform[A, B](a: IO[A])(f: A => B): IO[B] =\n        a.map(f)\n\n      override def flatMap[A, B](fa: IO[A])(f: A => IO[B]): IO[B] =\n        fa.flatMap(f)\n\n      override def success[A](value: A): IO[A] =\n        IO.pure(value)\n\n      override def failure[A](exception: Throwable): IO[A] =\n        IO.fromTry(Failure(exception))\n\n      override def foreach[A](a: IO[A])(f: A => Unit): Unit =\n        f(a.unsafeRunSync())\n\n      def fromPromise[A](a: Promise[A]): IO[A] =\n        IO.fromFuture(IO(a.future))\n\n      override def complete[A](promise: Promise[A], a: IO[A]): Unit =\n        promise completeWith a.unsafeToFuture()\n\n      override def fromIO[E: SwayIO.ExceptionHandler, A](\n          a: SwayIO[E, A],\n      ): IO[A] =\n        IO.fromTry(a.toTry)\n\n      override def fromFuture[A](a: Future[A]): IO[A] =\n        IO.fromFuture(IO(a))\n\n      override def suspend[B](f: => IO[B]): IO[B] =\n        IO.defer(f)\n\n      override def flatten[A](fa: IO[IO[A]]): IO[A] =\n        fa.flatMap(io => io)\n"
  },
  {
    "path": "modules/node/src/main/scala/io/leisuremeta/chain/node/store/interpreter/MultiInterpreter.scala",
    "content": "package io.leisuremeta.chain\npackage node\npackage store\npackage interpreter\n\nimport java.nio.file.Path\n\nimport cats.data.EitherT\nimport cats.effect.{IO, Resource}\n\nimport lib.codec.byte.ByteCodec\nimport lib.failure.DecodingFailure\nimport io.leisuremeta.chain.node.NodeConfig.RedisConfig\nimport java.nio.file.Paths\n\n@SuppressWarnings(Array(\"org.wartremover.warts.ImplicitParameter\"))\nclass MultiInterpreter[K, V: ByteCodec](\n    redis: RedisInterpreter[K, V],\n    sway: SwayInterpreter[K, V],\n) extends KeyValueStore[IO, K, V]:\n  def get(key: K): EitherT[IO, DecodingFailure, Option[V]] = \n    for \n      rRes <- redis.get(key)\n      res <- rRes match\n        case Some(v) => EitherT.pure[IO, DecodingFailure](Some(v))\n        case _ => sway.get(key)\n    yield res\n\n  def put(key: K, value: V): IO[Unit] = \n    for \n      _ <- redis.put(key, value)\n      _ <- sway.put(key, value)\n    yield ()\n  def remove(key: K): IO[Unit] = \n    for \n      _ <- redis.remove(key)\n      _ <- sway.remove(key)\n    yield ()\n\nobject MultiInterpreter:\n  def apply[K: ByteCodec, V: ByteCodec](config: RedisConfig, dir: InterpreterTarget): Resource[IO, MultiInterpreter[K, V]] = \n    for\n      redis <- RedisInterpreter[K, V](config, dir.r)\n      sway <- SwayInterpreter[K, V](dir.s)\n      res = new MultiInterpreter(redis, sway)\n    yield res\n\ncase class InterpreterTarget(r: RedisPath, s: Path)\nobject InterpreterTarget:\n  val BEST_NUM = InterpreterTarget(RedisPath(\"node:best\", 0), Paths.get(\"sway\", \"block\", \"best\"))\n  val BLOCK = InterpreterTarget(RedisPath(\"node:blc:\", 0), Paths.get(\"sway\", \"block\"))\n  val BLOCK_NUM = InterpreterTarget(RedisPath(\"node:blc_num:\", 0), Paths.get(\"sway\", \"block\", \"number\"))\n  val TX_BLOCK = InterpreterTarget(RedisPath(\"node:tx_blc:\", 0), Paths.get(\"sway\", \"block\", \"tx\"))\n  val MERKLE_TRIE = InterpreterTarget(RedisPath(\"node:trie:\", 0), Paths.get(\"sway\", \"state\"))\n  val TX = InterpreterTarget(RedisPath(\"node:tx:\", 0), Paths.get(\"sway\", \"transaction\"))\n"
  },
  {
    "path": "modules/node/src/main/scala/io/leisuremeta/chain/node/store/interpreter/RedisInterpreter.scala",
    "content": "package io.leisuremeta.chain\npackage node\npackage store\npackage interpreter\n\nimport cats.Monad\nimport cats.data.EitherT\nimport cats.effect.{IO, Resource}\n\nimport scodec.bits.ByteVector\nimport io.lettuce.core._\n\nimport lib.codec.byte.{ByteCodec, DecodeResult}\nimport lib.failure.DecodingFailure\nimport io.lettuce.core.api.async.RedisAsyncCommands\nimport io.lettuce.core.codec.RedisCodec\nimport java.nio.ByteBuffer\nimport scala.util.Try\nimport io.leisuremeta.chain.lib.datatype.Utf8\nimport scala.util.Success\nimport io.leisuremeta.chain.lib.datatype.UInt256\nimport io.leisuremeta.chain.node.NodeConfig.RedisConfig\n\n@SuppressWarnings(Array(\"org.wartremover.warts.ImplicitParameter\"))\nclass RedisInterpreter[K, V: ByteCodec](\n    cmds: RedisAsyncCommands[K, V],\n    dir: RedisPath,\n) extends KeyValueStore[IO, K, V]:\n  def get(key: K): EitherT[IO, DecodingFailure, Option[V]] =\n    for\n      _ <- EitherT.pure[IO, DecodingFailure]:\n        scribe.debug(s\"===> $dir: Geting with key: $key\")\n      v <- Try[V](cmds.get(key).get()) match\n        case Success(value) if (value != null) => EitherT.right(IO.pure(Some(value)))\n        case _ => EitherT.right(IO.pure(None))\n      _ <- EitherT.pure[IO, DecodingFailure]:\n        scribe.debug(s\"===> $dir: Got value: ${v}\")\n    yield v\n\n  def put(key: K, value: V): IO[Unit] = \n    for\n      _ <- Monad[IO].pure(scribe.debug(s\"===> $dir: Putting $key -> $value\"))\n      _ = cmds.set(key, value)\n    yield ()\n\n  def remove(key: K): IO[Unit] = \n    for\n      _ <- IO.unit\n      _ = cmds.del(key)\n    yield ()\n\ncase class RedisPath(path: String, db: Int):\n  def toByteVector = Utf8.unsafeFrom(path).bytes\n\nobject RedisInterpreter:\n  def apply[K: ByteCodec, V: ByteCodec](config: RedisConfig, dir: RedisPath): Resource[IO, RedisInterpreter[K, V]] = \n    object CodecImpl extends RedisCodec[K, V]:\n      @SuppressWarnings(Array(\"org.wartremover.warts.Throw\"))\n      def decodeKey(bytes: ByteBuffer): K =\n        ByteCodec[K].decode(ByteVector(bytes)) match\n          case Right(DecodeResult(v, r)) if r.isEmpty => v\n          case _ => throw new Exception(s\"Fail to decode $bytes\")\n      @SuppressWarnings(Array(\"org.wartremover.warts.Throw\"))\n      def decodeValue(bytes: ByteBuffer): V =\n        ByteCodec[V].decode(ByteVector(bytes)) match\n          case Right(DecodeResult(v, r)) if r.isEmpty => v\n          case _ => throw new Exception(s\"Fail to decode $bytes\")\n      def encodeKey(key: K): ByteBuffer = \n        if (key != UInt256.EmptyBytes) (dir.toByteVector ++ ByteCodec[K].encode(key)).toByteBuffer\n        else dir.toByteVector.toByteBuffer\n      def encodeValue(value: V): ByteBuffer =\n        ByteCodec[V].encode(value).toByteBuffer\n\n    val uri = RedisURI.Builder.redis(config.host, config.port).withDatabase(dir.db).build()\n    val client = IO(RedisClient.create(uri))\n\n    Resource\n      .make(client)(c => IO(c.close()))\n      .map(c => new RedisInterpreter[K, V](c.connect(CodecImpl).async(), dir))\n"
  },
  {
    "path": "modules/node/src/main/scala/io/leisuremeta/chain/node/store/interpreter/SwayInterpreter.scala",
    "content": "package io.leisuremeta.chain\npackage node\npackage store\npackage interpreter\n\nimport java.nio.file.Path\n\nimport scala.concurrent.ExecutionContext\n\nimport cats.Monad\nimport cats.data.EitherT\nimport cats.effect.{IO, Resource}\nimport cats.implicits._\n\nimport scodec.bits.ByteVector\nimport swaydb.Map\nimport swaydb.data.order.KeyOrder\nimport swaydb.data.slice.Slice\nimport swaydb.serializers.Serializer\nimport swaydb.serializers.Default.ByteArraySerializer\n\nimport lib.codec.byte.{ByteCodec, ByteDecoder, ByteEncoder, DecodeResult}\nimport lib.datatype.BigNat\nimport lib.failure.DecodingFailure\n\nimport Bag.given\n\n@SuppressWarnings(Array(\"org.wartremover.warts.ImplicitParameter\"))\nclass SwayInterpreter[K, V: ByteCodec](\n    map: Map[K, Array[Byte], Nothing, IO],\n    dir: Path,\n) extends KeyValueStore[IO, K, V] {\n\n  def get(key: K): EitherT[IO, DecodingFailure, Option[V]] = for {\n    _ <- EitherT.pure[IO, DecodingFailure](\n      scribe.debug(s\"===> $dir: Geting with key: $key\")\n    )\n    arrayOption <- EitherT.right(map.get(key))\n    _ <- EitherT.pure[IO, DecodingFailure](\n      scribe.debug(\n        s\"===> $dir: Got value: ${arrayOption.map(ByteVector.view).map(_.toHex)}\"\n      )\n    )\n    decodeResult <- arrayOption.traverse { array =>\n      EitherT.fromEither[IO](\n        ByteDecoder[V].decode(ByteVector.view(array))\n      )\n    }\n  } yield decodeResult.map(_.value)\n\n  def put(key: K, value: V): IO[Unit] = for {\n    _ <- Monad[IO].pure(scribe.debug(s\"===> $dir: Putting $key -> $value\"))\n    _ <- map.put(key, ByteEncoder[V].encode(value).toArray)\n  } yield ()\n\n  def remove(key: K): IO[Unit] = map.remove(key).map(_ => ())\n}\n\nobject SwayInterpreter {\n\n  def apply[K: ByteCodec, V: ByteCodec](dir: Path): Resource[IO, SwayInterpreter[K, V]] = {\n\n    val map: IO[Map[K, Array[Byte], Nothing, IO]] =\n      given KeyOrder[Slice[Byte]] = KeyOrder.default\n      given KeyOrder[K] = null\n      given ExecutionContext = swaydb.configs.level.DefaultExecutionContext.compactionEC\n      swaydb.persistent.Map[K, Array[Byte], Nothing, IO](dir)\n\n    Resource\n      .make(map)(_.close())\n      .map(new SwayInterpreter[K, V](_, dir))\n  }\n\n  def ensureNoRemainder[A](\n      decoded: DecodeResult[A],\n      msg: String,\n  ): Either[DecodingFailure, A] =\n    Either.cond(decoded.remainder.isEmpty, decoded.value, DecodingFailure(msg))\n\n  given scala.reflect.ClassTag[Nothing] = scala.reflect.Manifest.Nothing\n  given swaydb.data.sequencer.Sequencer[IO] = null\n  given swaydb.core.build.BuildValidator = swaydb.core.build.BuildValidator.DisallowOlderVersions(swaydb.data.DataType.Map)\n\n\n  def reverseBignatStoreIndex[A: ByteCodec](dir: Path): IO[SwayInterpreter[BigNat, A]] = {\n    given KeyOrder[Slice[Byte]] = KeyOrder.reverseLexicographic\n    val map = swaydb.persistent.Map[BigNat, Array[Byte], Nothing, IO](dir)\n    map.map(new SwayInterpreter[BigNat, A](_, dir))\n  }\n\n  \n  @SuppressWarnings(Array(\"org.wartremover.warts.Throw\"))\n  implicit def byteCodecToSerialize[A: ByteCodec]: Serializer[A] =\n    new Serializer[A] {\n      override def write(data: A): Slice[Byte] =\n        Slice[Byte](ByteEncoder[A].encode(data).toArray)\n      override def read(data: Slice[Byte]): A =\n        ByteDecoder[A].decode(ByteVector view data.toArray) match {\n          case Right(DecodeResult(value, remainder)) if remainder.isEmpty =>\n            value\n          case anything =>\n            throw new Exception(s\"Fail to decode $data: $anything\")\n        }\n    }\n\n}\n"
  },
  {
    "path": "modules/node/src/test/scala/io/leisuremeta/chain/node/dapp/PlayNommDAppTest.scala",
    "content": "package io.leisuremeta.chain\npackage node\npackage dapp\n\nimport api.model.*\nimport api.model.token.*\nimport lib.codec.byte.ByteEncoder.ops.*\nimport lib.crypto.{CryptoOps, KeyPair}\nimport lib.crypto.Hash.ops.*\nimport lib.crypto.Sign.ops.*\nimport lib.datatype.{BigNat, Utf8}\nimport lib.failure.DecodingFailure\nimport lib.merkle.MerkleTrie.NodeStore\nimport lib.merkle.MerkleTrieState\nimport repository.TransactionRepository\nimport store.KeyValueStore\n\nimport cats.data.{EitherT, Kleisli, StateT}\nimport cats.effect.IO\nimport cats.syntax.all.*\nimport munit.CatsEffectSuite\n\nclass PlayNommDAppTest extends CatsEffectSuite:\n  given emptyNodeStore: NodeStore[IO] = Kleisli.liftF(EitherT.pure(None))\n  given inMemoryStore[K, V]: KeyValueStore[IO, K, V] =\n    new KeyValueStore[IO, K, V]:\n      val store: collection.mutable.Map[K, V] = collection.mutable.Map.empty\n      def get(key: K): EitherT[IO, DecodingFailure, Option[V]] =\n        store.get(key).pure[EitherT[IO, DecodingFailure, *]]\n      def put(key: K, value: V): IO[Unit] =\n        IO.delay:\n          store.put(key, value)\n          ()\n      def remove(key: K): IO[Unit] =\n        IO.delay:\n          store.remove(key)\n          ()\n  given txRepo: TransactionRepository[IO] = TransactionRepository.fromStores[IO]\n  given initialPlayNommState: PlayNommState[IO] = PlayNommState.build[IO]\n\n  val aliceKey = CryptoOps.fromPrivate:\n    BigInt(\n      \"b229e76b742616db3ac2c5c2418f44063fcc5fcc52a08e05d4285bdb31acba06\",\n      16,\n    )\n\n  val alicePKS  = PublicKeySummary.fromPublicKeyHash(aliceKey.publicKey.toHash)\n  val alice     = Account(Utf8.unsafeFrom(\"alice\"))\n  val bob       = Account(Utf8.unsafeFrom(\"bob\"))\n  val carol     = Account(Utf8.unsafeFrom(\"carol\"))\n  val networkId = NetworkId(BigNat.unsafeFromLong(2021L))\n  val mintGroup = GroupId(Utf8.unsafeFrom(\"mint-group\"))\n  val testToken = TokenDefinitionId(Utf8.unsafeFrom(\"test-token\"))\n\n  def sign(account: Account, key: KeyPair)(tx: Transaction): Signed.Tx =\n    key.sign(tx).map(sig => Signed(AccountSignature(sig, account), tx)) match\n      case Right(signedTx) => signedTx\n      case Left(msg)       => throw Exception(msg)\n\n  def signAlice = sign(alice, aliceKey)\n\n  def readFungibleSnapshotAndTotalSupplySnapshot(\n      account: Account,\n  ): StateT[\n    EitherT[IO, PlayNommDAppFailure, *],\n    MerkleTrieState,\n    (Option[BigNat], Option[BigNat]),\n  ] = for\n    snapshotStream <- PlayNommState[IO].token.fungibleSnapshot\n      .reverseStreamFrom((alice, testToken).toBytes, None)\n      .mapK:\n        PlayNommDAppFailure.mapExternal:\n          \"Failed to get fungible snapshot stream\"\n    snapshotHeadList <- StateT\n      .liftF:\n        snapshotStream.head.compile.toList\n      .mapK:\n        PlayNommDAppFailure.mapExternal:\n          \"Failed to get snapshot stream head\"\n    totalAmountSnapshotStream <- PlayNommState[IO].token.totalSupplySnapshot\n      .reverseStreamFrom(testToken.toBytes, None)\n      .mapK:\n        PlayNommDAppFailure.mapExternal:\n          \"Failed to get total supply snapshot stream\"\n    totalAmountSnapshotHeadList <- StateT\n      .liftF:\n        totalAmountSnapshotStream.head.compile.toList\n      .mapK:\n        PlayNommDAppFailure.mapExternal:\n          \"Failed to get total amount snapshot stream head\"\n  yield (\n    snapshotHeadList.headOption\n      .map(_._2)\n      .map(_.values.foldLeft(BigNat.Zero)(BigNat.add)),\n    totalAmountSnapshotHeadList.headOption.map(_._2),\n  )\n\n  val fixture: IO[MerkleTrieState] =\n    val txs: Seq[Transaction] = IndexedSeq(\n      Transaction.AccountTx.CreateAccount(\n        networkId = networkId,\n        createdAt = java.time.Instant.parse(\"2023-01-11T19:01:00.00Z\"),\n        account = alice,\n        ethAddress = None,\n        guardian = None,\n      ),\n      Transaction.GroupTx.CreateGroup(\n        networkId = networkId,\n        createdAt = java.time.Instant.parse(\"2023-01-11T19:02:00.00Z\"),\n        groupId = mintGroup,\n        name = Utf8.unsafeFrom(\"Mint Group\"),\n        coordinator = alice,\n      ),\n      Transaction.GroupTx.AddAccounts(\n        networkId = networkId,\n        createdAt = java.time.Instant.parse(\"2023-01-11T19:03:00.00Z\"),\n        groupId = mintGroup,\n        accounts = Set(alice),\n      ),\n      Transaction.TokenTx.DefineToken(\n        networkId = networkId,\n        createdAt = java.time.Instant.parse(\"2023-01-11T19:04:00.00Z\"),\n        definitionId = testToken,\n        name = Utf8.unsafeFrom(\"Test Token\"),\n        symbol = Some(Utf8.unsafeFrom(\"TST\")),\n        minterGroup = Some(mintGroup),\n        nftInfo = None,\n      ),\n    )\n\n    txs\n      .map(signAlice(_))\n      .traverse(PlayNommDApp[IO](_))\n      .runS(MerkleTrieState.empty)\n      .value\n      .flatMap:\n        case Right(state)  => IO.pure(state)\n        case Left(failure) => IO.raiseError(new Exception(failure.toString))\n\n  test(\"Account is added to group\"):\n    val program =\n      for findOption <- PlayNommState[IO].group.groupAccount\n          .get((mintGroup, alice))\n      yield findOption.nonEmpty\n\n    for\n      state  <- fixture\n      result <- program.runA(state).value\n    yield assertEquals(result, Right(true))\n\n  test(\"Snapshot is created successfully\"):\n\n    val txs = Seq(\n      Transaction.TokenTx.CreateSnapshots(\n        networkId = networkId,\n        createdAt = java.time.Instant.parse(\"2023-01-11T19:05:00.00Z\"),\n        definitionIds = Set(testToken),\n        memo = Some(Utf8.unsafeFrom(\"Snapshot #0\")),\n      ),\n    )\n\n    val program = for\n      _ <- txs.map(signAlice(_)).traverse(PlayNommDApp[IO](_))\n      snapshotStateOption <- PlayNommState[IO].token.snapshotState\n        .get(testToken)\n        .mapK:\n          PlayNommDAppFailure.mapExternal:\n            \"Failed to get snapshot state\"\n    yield snapshotStateOption\n\n    for\n      state  <- fixture\n      result <- program.runA(state).value\n      snapshotStateOption <- IO.fromEither:\n        result.leftMap(failure => new Exception(failure.msg))\n    yield assertEquals(\n      snapshotStateOption.map(_.snapshotId),\n      Some(SnapshotState.SnapshotId(BigNat.One)),\n    )\n\n  test(\"Minting and snapshotting reflects in fungible snapshot balance\"):\n\n    val txs = Seq(\n      Transaction.TokenTx.MintFungibleToken(\n        networkId = networkId,\n        createdAt = java.time.Instant.parse(\"2023-01-11T19:05:00.00Z\"),\n        definitionId = testToken,\n        outputs = Map(\n          alice -> BigNat.unsafeFromLong(100L),\n        ),\n      ),\n      Transaction.TokenTx.CreateSnapshots(\n        networkId = networkId,\n        createdAt = java.time.Instant.parse(\"2023-01-11T19:06:00.00Z\"),\n        definitionIds = Set(testToken),\n        memo = Some(Utf8.unsafeFrom(\"Snapshot #1\")),\n      ),\n    )\n\n    val program = for\n      _ <- txs.map(signAlice(_)).traverse(PlayNommDApp[IO](_))\n      snapshotTuple <- readFungibleSnapshotAndTotalSupplySnapshot(alice)\n    yield snapshotTuple\n\n    for\n      state  <- fixture\n      result <- program.runA(state).value\n      snapshotStateTuple <- IO.fromEither:\n        result.leftMap(failure => new Exception(failure.msg))\n      (snapshotStateOption, totalAmountOption) = snapshotStateTuple\n      snapShotSum = snapshotStateOption.map(_.toBigInt)\n      totalAmount = totalAmountOption.map(_.toBigInt)\n    yield\n      assertEquals(snapShotSum, Some(BigInt(100L)))\n      assertEquals(totalAmount, Some(BigInt(100L)))\n\n  test(\"mint -> snapshot -> snapshot reflects last mint amount\"):\n\n    val txs = Seq(\n      Transaction.TokenTx.MintFungibleToken(\n        networkId = networkId,\n        createdAt = java.time.Instant.parse(\"2023-01-11T19:05:00.00Z\"),\n        definitionId = testToken,\n        outputs = Map(\n          alice -> BigNat.unsafeFromLong(100L),\n        ),\n      ),\n      Transaction.TokenTx.CreateSnapshots(\n        networkId = networkId,\n        createdAt = java.time.Instant.parse(\"2023-01-11T19:06:00.00Z\"),\n        definitionIds = Set(testToken),\n        memo = Some(Utf8.unsafeFrom(\"Snapshot #1\")),\n      ),\n      Transaction.TokenTx.CreateSnapshots(\n        networkId = networkId,\n        createdAt = java.time.Instant.parse(\"2023-01-11T19:07:00.00Z\"),\n        definitionIds = Set(testToken),\n        memo = Some(Utf8.unsafeFrom(\"Snapshot #2\")),\n      ),\n    )\n\n    val program = for\n      _ <- txs.map(signAlice(_)).traverse(PlayNommDApp[IO](_))\n      snapshotTuple <- readFungibleSnapshotAndTotalSupplySnapshot(alice)\n    yield snapshotTuple\n\n    for\n      state  <- fixture\n      result <- program.runA(state).value\n      snapshotStateTuple <- IO.fromEither:\n        result.leftMap(failure => new Exception(failure.msg))\n      (snapshotStateOption, totalAmountOption) = snapshotStateTuple\n      snapShotSum = snapshotStateOption.map(_.toBigInt)\n      totalAmount = totalAmountOption.map(_.toBigInt)\n    yield\n      assertEquals(snapShotSum, Some(BigInt(100L)))\n      assertEquals(totalAmount, Some(BigInt(100L)))\n"
  },
  {
    "path": "modules/node-proxy/src/main/resources/migration-node.json",
    "content": "{}\r\n"
  },
  {
    "path": "modules/node-proxy/src/main/scala/io/leisuremeta/chain/node/proxy/NodeProxyApi.scala",
    "content": "package io.leisuremeta.chain\npackage node\npackage proxy\n\nimport java.time.Instant\n\nimport sttp.model.MediaType\nimport sttp.tapir.*\n\nimport api.model.*\nimport api.model.account.EthAddress\nimport api.model.token.*\nimport api.model.creator_dao.*\n\nobject NodeProxyApi:\n  val jsonType = MediaType.ApplicationJson.toString\n\n  @SuppressWarnings(Array(\"org.wartremover.warts.Any\"))\n  val getTxSetEndpoint = endpoint.get\n    .in(\"tx\" / query[String](\"block\"))\n    .out(statusCode.and(stringJsonBody))\n    .out(header(\"Content-Type\", jsonType))\n\n  \n  @SuppressWarnings(Array(\"org.wartremover.warts.Any\"))\n  val getTxEndpoint = endpoint.get\n    .in(\"tx\" / path[String])\n    .out(statusCode.and(stringJsonBody))\n    .out(header(\"Content-Type\", jsonType))\n\n  @SuppressWarnings(Array(\"org.wartremover.warts.Any\"))\n  val postTxEndpoint =\n    endpoint.post\n      .in(\"tx\")\n      .in(stringBody)\n      .out(statusCode.and(stringJsonBody))\n      .out(header(\"Content-Type\", jsonType))\n\n  @SuppressWarnings(Array(\"org.wartremover.warts.Any\"))\n  val postTxHashEndpoint = endpoint.post\n    .in(\"txhash\")\n    .in(stringBody)\n    .out(statusCode.and(stringJsonBody))\n    .out(header(\"Content-Type\", jsonType))\n\n  @SuppressWarnings(Array(\"org.wartremover.warts.Any\"))\n  val getStatusEndpoint =\n    endpoint.get.in(\"status\")\n      .out(statusCode.and(stringJsonBody))\n      .out(header(\"Content-Type\", jsonType))\n\n  @SuppressWarnings(Array(\"org.wartremover.warts.Any\"))\n  val getAccountEndpoint =\n    endpoint.get\n      .in(\"account\" / path[Account])\n      .out(statusCode.and(stringJsonBody))\n      .out(header(\"Content-Type\", jsonType))\n\n  @SuppressWarnings(Array(\"org.wartremover.warts.Any\"))\n  val getEthEndpoint =\n    endpoint.get\n      .in(\"eth\" / path[EthAddress])\n      .out(statusCode.and(stringJsonBody))\n      .out(header(\"Content-Type\", jsonType))\n\n  @SuppressWarnings(Array(\"org.wartremover.warts.Any\"))\n  val getGroupEndpoint =\n    endpoint.get\n      .in(\"group\" / path[GroupId])\n      .out(statusCode.and(stringJsonBody))\n      .out(header(\"Content-Type\", jsonType))\n\n  @SuppressWarnings(Array(\"org.wartremover.warts.Any\"))\n  val getBlockListEndpoint =\n    endpoint.get\n      .in:\n        \"block\" / query[Option[String]](\"from\")\n          .and(query[Option[Int]](\"limit\"))\n      .out(statusCode.and(stringJsonBody))\n      .out(header(\"Content-Type\", jsonType))\n\n  @SuppressWarnings(Array(\"org.wartremover.warts.Any\"))\n  val getBlockEndpoint =\n    endpoint.get\n      .in(\"block\" / path[String])\n      .out(statusCode.and(stringJsonBody))\n      .out(header(\"Content-Type\", jsonType))\n\n  @SuppressWarnings(Array(\"org.wartremover.warts.Any\"))\n  val getTokenDefinitionEndpoint =\n    endpoint.get\n      .in(\"token-def\" / path[TokenDefinitionId])\n      .out(statusCode.and(stringJsonBody))\n      .out(header(\"Content-Type\", jsonType))\n\n  @SuppressWarnings(Array(\"org.wartremover.warts.Any\"))\n  val getBalanceEndpoint =\n    endpoint.get\n      .in(\"balance\" / path[Account].and(query[String](\"movable\")))\n      .out(statusCode.and(stringJsonBody))\n      .out(header(\"Content-Type\", jsonType))\n\n  @SuppressWarnings(Array(\"org.wartremover.warts.Any\"))\n  val getNftBalanceEndpoint =\n    endpoint.get\n      .in(\"nft-balance\" / path[Account].and(query[Option[String]](\"movable\")))\n      .out(statusCode.and(stringJsonBody))\n      .out(header(\"Content-Type\", jsonType))\n\n  @SuppressWarnings(Array(\"org.wartremover.warts.Any\"))\n  val getTokenEndpoint =\n    endpoint.get\n      .in(\"token\" / path[TokenId])\n      .out(statusCode.and(stringJsonBody))\n      .out(header(\"Content-Type\", jsonType))\n\n  @SuppressWarnings(Array(\"org.wartremover.warts.Any\"))\n  val getTokenHistoryEndpoint =\n    endpoint.get\n      .in(\"token-hist\" / path[String])\n      .out(statusCode.and(stringJsonBody))\n      .out(header(\"Content-Type\", jsonType))\n\n  @SuppressWarnings(Array(\"org.wartremover.warts.Any\"))\n  val getOwnersEndpoint =\n    endpoint.get\n      .in(\"owners\" / path[TokenDefinitionId])\n      .out(statusCode.and(stringJsonBody))\n      .out(header(\"Content-Type\", jsonType))\n\n  @SuppressWarnings(Array(\"org.wartremover.warts.Any\"))\n  val getAccountActivityEndpoint =\n    endpoint.get\n      .in(\"activity\" / \"account\" / path[Account])\n      .out(statusCode.and(stringJsonBody))\n      .out(header(\"Content-Type\", jsonType))\n\n  @SuppressWarnings(Array(\"org.wartremover.warts.Any\"))\n  val getTokenActivityEndpoint =\n    endpoint.get\n      .in(\"activity\" / \"token\" / path[TokenId])\n      .out(statusCode.and(stringJsonBody))\n      .out(header(\"Content-Type\", jsonType))\n\n  @SuppressWarnings(Array(\"org.wartremover.warts.Any\"))\n  val getAccountSnapshotEndpoint =\n    endpoint.get\n      .in(\"snapshot\" / \"account\" / path[Account])\n      .out(statusCode.and(stringJsonBody))\n      .out(header(\"Content-Type\", jsonType))\n\n  @SuppressWarnings(Array(\"org.wartremover.warts.Any\"))\n  val getTokenSnapshotEndpoint =\n    endpoint.get\n      .in(\"snapshot\" / \"token\" / path[TokenId])\n      .out(statusCode.and(stringJsonBody))\n      .out(header(\"Content-Type\", jsonType))\n\n  @SuppressWarnings(Array(\"org.wartremover.warts.Any\"))\n  val getOwnershipSnapshotEndpoint =\n    endpoint.get\n      .in(\"snapshot\" / \"ownership\" / path[TokenId])\n      .out(statusCode.and(stringJsonBody))\n      .out(header(\"Content-Type\", jsonType))\n\n  @SuppressWarnings(Array(\"org.wartremover.warts.Any\"))\n  val getOwnershipSnapshotMapEndpoint =\n    endpoint.get\n      .in:\n        \"snapshot\" / \"ownership\" / query[Option[TokenId]](\"from\")\n          .and(query[Option[Int]](\"limit\"))\n      .out(statusCode.and(stringJsonBody))\n      .out(header(\"Content-Type\", jsonType))\n\n  @SuppressWarnings(Array(\"org.wartremover.warts.Any\"))\n  val getOwnershipRewardedEndpoint =\n    endpoint.get\n      .in(\"rewarded\" / \"ownership\" / path[TokenId])\n      .out(statusCode.and(stringJsonBody))\n      .out(header(\"Content-Type\", jsonType))\n\n  @SuppressWarnings(Array(\"org.wartremover.warts.Any\"))\n  val getRewardEndpoint =\n    endpoint.get\n      .in:\n        \"reward\" / path[Account]\n          .and(query[Option[Instant]](\"timestamp\"))\n          .and(query[Option[Account]](\"dao-account\"))\n          .and(query[Option[String]](\"reward-amount\"))\n      .out(statusCode.and(stringJsonBody))\n      .out(header(\"Content-Type\", jsonType))\n\n  @SuppressWarnings(Array(\"org.wartremover.warts.Any\"))\n  val getDaoInfoEndpoint =\n    endpoint.get\n      .in(\"dao\" / path[GroupId])\n      .out(statusCode.and(stringJsonBody))\n      .out(header(\"Content-Type\", jsonType))\n\n  @SuppressWarnings(Array(\"org.wartremover.warts.Any\"))\n  val getSnapshotStateEndpoint =\n    endpoint.get\n      .in(\"snapshot-state\" / path[TokenDefinitionId])\n      .out(statusCode.and(stringJsonBody))\n      .out(header(\"Content-Type\", jsonType))\n\n  @SuppressWarnings(Array(\"org.wartremover.warts.Any\"))\n  val getFungibleSnapshotBalanceEndpoint =\n    endpoint.get\n      .in:\n        \"snapshot-balance\" / path[Account] / path[TokenDefinitionId] / path[String]\n      .out(statusCode.and(stringJsonBody))\n      .out(header(\"Content-Type\", jsonType))\n\n  @SuppressWarnings(Array(\"org.wartremover.warts.Any\"))\n  val getNftSnapshotBalanceEndpoint =\n    endpoint.get\n      .in:\n        \"nft-snapshot-balance\" / path[Account] / path[TokenDefinitionId] / path[String]\n      .out(statusCode.and(stringJsonBody))\n      .out(header(\"Content-Type\", jsonType))\n\n  @SuppressWarnings(Array(\"org.wartremover.warts.Any\"))\n  val getVoteProposalEndpoint =\n    endpoint.get\n      .in(\"vote\" / \"proposal\" / path[String])\n      .out(statusCode.and(stringJsonBody))\n      .out(header(\"Content-Type\", jsonType))\n\n  @SuppressWarnings(Array(\"org.wartremover.warts.Any\"))\n  val getAccountVotesEndpoint =\n    endpoint.get\n      .in(\"vote\" / \"account\" / path[String] / path[Account])\n      .out(statusCode.and(stringJsonBody))\n      .out(header(\"Content-Type\", jsonType))\n\n  @SuppressWarnings(Array(\"org.wartremover.warts.Any\"))\n  val getVoteCountEndpoint =\n    endpoint.get\n      .in(\"vote\" / \"count\" / path[String])\n      .out(statusCode.and(stringJsonBody))\n      .out(header(\"Content-Type\", jsonType))\n\n  @SuppressWarnings(Array(\"org.wartremover.warts.Any\"))\n  val getCreatorDaoInfoEndpoint =\n    endpoint.get\n      .in(\"creator-dao\" / path[CreatorDaoId])\n      .out(statusCode.and(stringJsonBody))\n      .out(header(\"Content-Type\", jsonType))\n\n  @SuppressWarnings(Array(\"org.wartremover.warts.Any\"))\n  val getCreatorDaoMemberEndpoint =\n    endpoint.get\n      .in:\n        \"creator-dao\" / path[CreatorDaoId] / \"member\"\n          .and(query[Option[Account]](\"from\"))\n          .and(query[Option[Int]](\"limit\"))\n      .out(statusCode.and(stringJsonBody))\n      .out(header(\"Content-Type\", jsonType))\n\n  // enum Movable:\n  //   case Free, Locked\n  // object Movable:\n  //   @SuppressWarnings(Array(\"org.wartremover.warts.ToString\"))\n  //   given Codec[String, Movable, TextPlain] = Codec.string.mapDecode {\n  //     (s: String) =>\n  //       s match\n  //         case \"free\"   => DecodeResult.Value(Movable.Free)\n  //         case \"locked\" => DecodeResult.Value(Movable.Locked)\n  //         case _ => DecodeResult.Error(s, new Exception(s\"invalid movable: $s\"))\n  //   }(_.toString.toLowerCase(Locale.ENGLISH))\n\n  //   @SuppressWarnings(Array(\"org.wartremover.warts.ToString\"))\n  //   given Codec[String, Option[Movable], TextPlain] = Codec.string.mapDecode {\n  //     (s: String) =>\n  //       s match\n  //         case \"free\"   => DecodeResult.Value(Some(Movable.Free))\n  //         case \"locked\" => DecodeResult.Value(Some(Movable.Locked))\n  //         case \"all\"    => DecodeResult.Value(None)\n  //         case _ => DecodeResult.Error(s, new Exception(s\"invalid movable: $s\"))\n  //   }(_.fold(\"\")(_.toString.toLowerCase(Locale.ENGLISH)))\n"
  },
  {
    "path": "modules/node-proxy/src/main/scala/io/leisuremeta/chain/node/proxy/NodeProxyApp.scala",
    "content": "package io.leisuremeta.chain\npackage node\npackage proxy\n\nimport cats.effect.std.Dispatcher\nimport cats.effect.Async\nimport cats.effect.kernel.Resource\nimport com.linecorp.armeria.server.Server\n\nimport sttp.tapir.server.interceptor.log.DefaultServerLog\nimport sttp.tapir.server.armeria.cats.{\n  ArmeriaCatsServerInterpreter,\n  ArmeriaCatsServerOptions,\n}\nimport sttp.tapir.server.ServerEndpoint\nimport sttp.capabilities.fs2.Fs2Streams\n\nimport io.leisuremeta.chain.node.proxy.{NodeProxyApi as Api}\nimport service.InternalApiService\n\nimport api.model.creator_dao.CreatorDaoId\nimport api.model.account.EthAddress\nimport api.model.*\nimport api.model.token.*\n\nfinal case class NodeProxyApp[F[_]: Async](\n    apiService: InternalApiService[F],\n):\n\n  def getBlockServerEndpoint = Api.getBlockEndpoint.serverLogicSuccess:\n    (blockHash: String) =>\n      apiService.getBlock(blockHash)\n\n  def getAccountServerEndpoint: ServerEndpoint[Fs2Streams[F], F] =\n    Api.getAccountEndpoint.serverLogicSuccess: (a: Account) =>\n      apiService.getAccount(a)\n\n  def getEthServerEndpoint = Api.getEthEndpoint.serverLogicSuccess:\n    (ethAddress: EthAddress) =>\n      apiService.getEthAccount(ethAddress)\n\n  def getGroupServerEndpoint = Api.getGroupEndpoint.serverLogicSuccess:\n    (g: GroupId) =>\n      apiService.getGroupInfo(g)\n\n  def getBlockListServerEndpoint = Api.getBlockListEndpoint.serverLogicSuccess:\n    (fromOption, limitOption) =>\n      apiService.getBlockList(fromOption, limitOption)\n\n  def getStatusServerEndpoint = Api.getStatusEndpoint.serverLogicSuccess: _ =>\n    apiService.getStatus\n\n  def getTokenDefServerEndpoint = Api.getTokenDefinitionEndpoint.serverLogicSuccess:\n    (tokenDefinitionId: TokenDefinitionId) =>\n      apiService.getTokenDef(tokenDefinitionId)\n\n  def getBalanceServerEndpoint = Api.getBalanceEndpoint.serverLogicSuccess:\n    (account, movable) =>\n      apiService.getBalance(account, movable)\n\n  def getNftBalanceServerEndpoint = Api.getNftBalanceEndpoint.serverLogicSuccess:\n    (account, movable) =>\n      apiService.getNftBalance(account, movable)\n\n  def getTokenServerEndpoint = Api.getTokenEndpoint.serverLogicSuccess:\n    (tokenId: TokenId) =>\n      apiService.getToken(tokenId)\n\n  def getTokenHistoryServerEndpoint = Api.getTokenHistoryEndpoint.serverLogicSuccess:\n    (txHash: String) =>\n      apiService.getTokenHistory(txHash)\n\n  def getOwnersServerEndpoint = Api.getOwnersEndpoint.serverLogicSuccess:\n    (tokenDefinitionId: TokenDefinitionId) =>\n      apiService.getOwners(tokenDefinitionId)\n\n  def getAccountActivityServerEndpoint =\n    Api.getAccountActivityEndpoint.serverLogicSuccess: (account: Account) =>\n      apiService.getAccountActivity(account)\n\n  def getTokenActivityServerEndpoint =\n    Api.getTokenActivityEndpoint.serverLogicSuccess: (tokenId: TokenId) =>\n      apiService.getTokenActivity(tokenId)\n\n  def getAccountSnapshotServerEndpoint =\n    Api.getAccountSnapshotEndpoint.serverLogicSuccess: (account: Account) =>\n      apiService.getAccountSnapshot(account)\n\n  def getTokenSnapshotServerEndpoint =\n    Api.getTokenSnapshotEndpoint.serverLogicSuccess: (tokenId: TokenId) =>\n      apiService.getTokenSnapshot(tokenId)\n\n\n  def getOwnershipSnapshotServerEndpoint =\n    Api.getOwnershipSnapshotEndpoint.serverLogicSuccess: (tokenId: TokenId) =>\n      apiService.getOwnershipSnapshot(tokenId)\n\n  def getOwnershipSnapshotMapServerEndpoint =\n    Api.getOwnershipSnapshotMapEndpoint.serverLogicSuccess:\n      (from: Option[TokenId], limit: Option[Int]) =>\n        apiService.getOwnershipSnapshotMap(from, limit)\n\n  def getOwnershipRewardedServerEndpoint =\n    Api.getOwnershipRewardedEndpoint.serverLogicSuccess: (tokenId: TokenId) =>\n      apiService.getOwnershipRewarded(tokenId)\n\n  def getDaoInfoServerEndpoint =\n    Api.getDaoInfoEndpoint.serverLogicSuccess: (groupId: GroupId) =>\n      apiService.getDaoInfo(groupId)\n\n  def getTxServerEndpoint = Api.getTxEndpoint.serverLogicSuccess: (txHash: String) =>\n    apiService.getTx(txHash)\n\n  def getTxSetServerEndpoint =\n    Api.getTxSetEndpoint.serverLogicSuccess: (block: String) =>\n      apiService.getTxSet(block)\n\n  def postTxHashServerEndpoint =\n    Api.postTxHashEndpoint.serverLogicSuccess: (txs: String) =>\n      scribe.info(s\"received postTxHash request: $txs\")\n      apiService.postTxHash(txs)\n\n  def postTxServerEndpoint =\n    Api.postTxEndpoint.serverLogicSuccess: (txs: String) =>\n      scribe.info(s\"received postTx request: $txs\")\n      apiService.postTx(txs)\n\n  def getSnapshotStateServerEndpoint =\n    Api.getSnapshotStateEndpoint.serverLogicSuccess:\n      (tokenDefinitionId: TokenDefinitionId) =>\n        apiService.getSnapshotState(tokenDefinitionId)\n\n  def getFungibleSnapshotBalanceServerEndpoint =\n    Api.getFungibleSnapshotBalanceEndpoint.serverLogicSuccess:\n      (\n          account: Account,\n          tokenDefinitionId: TokenDefinitionId,\n          snapshotId: String,\n      ) =>\n        apiService\n          .getFungibleSnapshotBalance(account, tokenDefinitionId, snapshotId)\n          \n\n  def getNftSnapshotBalanceServerEndpoint =\n    Api.getNftSnapshotBalanceEndpoint.serverLogicSuccess:\n      (\n          account: Account,\n          tokenDefinitionId: TokenDefinitionId,\n          snapshotId: String,\n      ) =>\n        apiService\n          .getNftSnapshotBalance(account, tokenDefinitionId, snapshotId)\n          \n\n  def getVoteProposalServerEndpoint = Api.getVoteProposalEndpoint.serverLogicSuccess:\n    (proposalId: String) =>\n      apiService.getVoteProposal(proposalId)\n\n  def getAccountVotesServerEndpoint = Api.getAccountVotesEndpoint.serverLogicSuccess:\n    (proposalId: String, account: Account) =>\n      apiService.getAccountVotes(proposalId, account)\n\n  def getVoteCountServerEndpoint = Api.getVoteCountEndpoint.serverLogicSuccess:\n    (proposalId: String) =>\n      apiService.getVoteCount(proposalId)\n\n  def getCreatorDaoInfoServerEndpoint =\n    Api.getCreatorDaoInfoEndpoint.serverLogicSuccess: (creatorDaoId: CreatorDaoId) =>\n      apiService.getCreatorDaoInfo(creatorDaoId)\n\n  def getCreatorDaoMemberServerEndpoint =\n    Api.getCreatorDaoMemberEndpoint.serverLogicSuccess:\n      (creatorDaoId: CreatorDaoId, from: Option[Account], limit: Option[Int]) =>\n        apiService.getCreatorDaoMember(creatorDaoId, from, limit)\n\n  def proxyNodeEndpoints = List(\n    getAccountServerEndpoint,\n    getEthServerEndpoint,\n    getBlockListServerEndpoint,\n    getBlockServerEndpoint,\n    getGroupServerEndpoint,\n    getStatusServerEndpoint,\n    getTxServerEndpoint,\n    getTokenDefServerEndpoint,\n    getBalanceServerEndpoint,\n    getNftBalanceServerEndpoint,\n    getTokenServerEndpoint,\n    getTokenHistoryServerEndpoint,\n    getOwnersServerEndpoint,\n    getTxSetServerEndpoint,\n    getAccountActivityServerEndpoint,\n    getTokenActivityServerEndpoint,\n    getAccountSnapshotServerEndpoint,\n    getTokenSnapshotServerEndpoint,\n    getOwnershipSnapshotServerEndpoint,\n    getOwnershipSnapshotMapServerEndpoint,\n//    getRewardServerEndpoint,\n    getOwnershipRewardedServerEndpoint,\n    getDaoInfoServerEndpoint,\n    postTxServerEndpoint,\n    postTxHashServerEndpoint,\n    getSnapshotStateServerEndpoint,\n    getFungibleSnapshotBalanceServerEndpoint,\n    getNftSnapshotBalanceServerEndpoint,\n    getVoteProposalServerEndpoint,\n    getAccountVotesServerEndpoint,\n    getVoteCountServerEndpoint,\n    getCreatorDaoInfoServerEndpoint,\n    getCreatorDaoMemberServerEndpoint,\n  )\n\n  def getServer[IO](\n      dispatcher: Dispatcher[F],\n  ): F[Server] = Async[F].fromCompletableFuture:\n    def log[F[_]: Async](\n        level: scribe.Level,\n    )(msg: String, exOpt: Option[Throwable])(using\n        mdc: scribe.mdc.MDC,\n    ): F[Unit] = Async[F].delay:\n      exOpt match\n        case None     => scribe.log(level, mdc, msg)\n        case Some(ex) => scribe.log(level, mdc, msg, ex)\n    val serverLog = DefaultServerLog(\n      doLogWhenReceived = log(scribe.Level.Info)(_, None),\n      doLogWhenHandled = log(scribe.Level.Info),\n      doLogAllDecodeFailures = log(scribe.Level.Info),\n      doLogExceptions =\n        (msg: String, ex: Throwable) => Async[F].delay(scribe.warn(msg, ex)),\n      noLog = Async[F].pure(()),\n    )\n    val serverOptions = ArmeriaCatsServerOptions\n      .customiseInterceptors[F](dispatcher)\n      .serverLog(serverLog)\n      .options\n    val tapirService = ArmeriaCatsServerInterpreter[F](serverOptions)\n      .toService(proxyNodeEndpoints)\n    val server = Server.builder\n      .maxRequestLength(128 * 1024 * 1024)\n      .requestTimeout(java.time.Duration.ofMinutes(10))\n      .http(8080)\n      .service(tapirService)\n      .build\n    Async[F].delay:\n      server.start().thenApply(_ => server)\n\n  def resource: F[Resource[F, Server]] = Async[F].delay:\n    for\n      dispatcher <- Dispatcher.parallel[F]\n      server     <- Resource.fromAutoCloseable(getServer(dispatcher))\n    yield server\n"
  },
  {
    "path": "modules/node-proxy/src/main/scala/io/leisuremeta/chain/node/proxy/NodeProxyMain.scala",
    "content": "package io.leisuremeta.chain.node\npackage proxy\n\nimport cats.effect.{ExitCode, IO, IOApp}\nimport cats.effect.Ref\nimport sttp.client3.armeria.cats.ArmeriaCatsBackend\nimport sttp.client3.*\nimport cats.syntax.*\nimport cats.syntax.all._\nimport com.linecorp.armeria.client.ClientFactory\nimport com.linecorp.armeria.client.WebClient\nimport com.linecorp.armeria.client.encoding.DecodingClient\nimport cats.effect.kernel.Async\nimport cats.syntax.flatMap.toFlatMapOps\nimport model.NodeConfig\nimport service.*\n\nobject NodeProxyMain extends IOApp:\n  \n  def newClientFactory(options: SttpBackendOptions): ClientFactory =\n    val builder = ClientFactory\n      .builder()\n      .connectTimeoutMillis(options.connectionTimeout.toMillis)\n    options.proxy.fold(builder.build()) { proxy =>\n      builder\n        .proxyConfig(proxy.asJavaProxySelector)\n        .build()\n    }\n\n  def webClient(options: SttpBackendOptions): WebClient = \n    WebClient\n      .builder()\n      .decorator(\n        DecodingClient\n          .builder()\n          .autoFillAcceptEncoding(false)\n          .strictContentEncoding(false)\n          .newDecorator()\n      )\n      .factory(newClientFactory(options))\n      .build()\n\n  def run[F[_]: Async]: F[ExitCode] =\n    ArmeriaCatsBackend\n    .resourceUsingClient[F](webClient(SttpBackendOptions.Default))\n    .use { backend =>\n        for \n          blocker        <- Ref.of[F, Boolean](true)\n          queue          <- PostTxQueue[F]\n          nodeConfg      <- NodeWatchService.nodeConfig.flatMap (\n                              _.fold(Async[F].raiseError[NodeConfig], Async[F].pure))\n          blockchainUrls <- Ref.of[F, List[String]](List(nodeConfg.oldNodeAddress))\n          internalApiSvc =  InternalApiService[F](backend, blocker, blockchainUrls, queue)\n          _              <- NodeWatchService.startOnNew(internalApiSvc, blockchainUrls, blocker, queue) \n          appResource    <- NodeProxyApp[F](internalApiSvc).resource\n          exitcode       <- appResource.useForever.as(ExitCode.Success)\n        yield exitcode\n      }\n  override def run(args: List[String]): IO[ExitCode] = {\n    run[IO]\n  }\n  \n\n"
  },
  {
    "path": "modules/node-proxy/src/main/scala/io/leisuremeta/chain/node/proxy/model/NodeConfig.scala",
    "content": "package io.leisuremeta.chain.node.proxy\npackage model\n\nimport io.leisuremeta.chain.lib.datatype.BigNat\n\nfinal case class NodeConfig(\n  blockNumber: Option[BigNat],\n  oldNodeAddress: String,\n  newNodeAddress: Option[String],\n)\n"
  },
  {
    "path": "modules/node-proxy/src/main/scala/io/leisuremeta/chain/node/proxy/model/TxModel.scala",
    "content": "package io.leisuremeta.chain.node.proxy.model\n\nimport io.circe._\n\ncase class TxModel (\n  signedTx: String,\n  result: Option[String]\n)\n\nobject TxModel {\n  implicit val txModelDecoder: Decoder[TxModel] = new Decoder[TxModel] {\n    final def apply(c: HCursor): Decoder.Result[TxModel] = {\n      for {\n        signedTxJson <- c.downField(\"signedTx\").as[Json]\n        signedTx = signedTxJson.noSpaces\n        result <- c.downField(\"result\").as[Option[String]].orElse(Right(None))\n      } yield {\n        TxModel(signedTx, result)\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "modules/node-proxy/src/main/scala/io/leisuremeta/chain/node/proxy/service/InternalApiService.scala",
    "content": "package io.leisuremeta.chain\npackage node.proxy\npackage service\n\nimport io.circe.parser.decode\nimport sttp.client3.*\nimport sttp.model.{Uri, StatusCode}\nimport cats.implicits.*\nimport cats.effect.*\nimport cats.effect.Async\nimport cats.effect.Ref\nimport cats.effect.kernel.Async\nimport scala.concurrent.duration.*\nimport sttp.model.MediaType\nimport io.leisuremeta.chain.node.proxy.model.TxModel\nimport io.circe.generic.auto.*\n\nimport api.model.*\nimport api.model.account.EthAddress\nimport api.model.creator_dao.CreatorDaoId\nimport api.model.token.*\nimport api.model.Block.*\n\nobject InternalApiService:\n  def apply[F[_]: Async](\n      backend: SttpBackend[F, Any],\n      blocker: Ref[F, Boolean],\n      baseUrlsLock: Ref[F, List[String]],\n      queue: PostTxQueue[F],\n  ): InternalApiService[F] =\n    // Async[F].delay(new InternalApiService[F](backend, blocker, baseUrlsLock))\n    new InternalApiService[F](backend, blocker, baseUrlsLock, queue)\n\nclass InternalApiService[F[_]: Async](\n    backend: SttpBackend[F, Any],\n    blocker: Ref[F, Boolean],\n    baseUrlsLock: Ref[F, List[String]],\n    queue: PostTxQueue[F],\n):\n  // val baseUrl = \"http://lmc.leisuremeta.io\"\n  // val baseUrl = \"http://test.chain.leisuremeta.io\"\n\n  // def backend[F[_]: Async]: SttpBackend[F, Any] =\n  //   ArmeriaCatsBackend.usingClient[F](webClient(SttpBackendOptions.Default))\n  // def injectQueue(postQueue: PostTxQueue[F]) =\n  //   queue = Some(postQueue)\n\n  def getAsString(\n      uri: Uri,\n  ): F[(StatusCode, String)] =\n    for\n      _ <- jobOrBlock\n      result <- basicRequest\n        .response(asStringAlways)\n        .get(uri)\n        .send(backend)\n        .map(res => (res.code, res.body))\n    yield\n    // scribe.info(s\"getAsString result: $result\")\n    result\n\n  def getAsResponse(\n      uri: Uri,\n  ): F[(StatusCode, Response[String])] =\n    for\n      _ <- jobOrBlock\n      result <- basicRequest\n        .response(asStringAlways)\n        .get(uri)\n        .send(backend)\n    yield (result.code, result)\n\n  def getTxAsResponse[A: io.circe.Decoder](\n      uri: Uri,\n  ): F[(StatusCode, Either[String, A])] =\n    for\n      _ <- jobOrBlock\n      result <- basicRequest\n        .get(uri)\n        .send(backend)\n        .map { response =>\n          (\n            response.code,\n            for\n              body <- response.body\n              a    <- decode[A](body).leftMap(_.getMessage())\n            yield a,\n          )\n        }\n    yield\n    // scribe.info(s\"getAsString result: $result\")\n    result\n\n  def postAsString(\n      uri: Uri,\n      body: String,\n  ): F[(StatusCode, String)] =\n    for\n      _ <- jobOrBlock\n      result <- basicRequest\n        .response(asStringAlways)\n        .post(uri)\n        .body(body)\n        .send(backend)\n        .map(res => (res.code, res.body))\n    yield result\n\n  def postAsResponse(\n      uri: Uri,\n      body: String,\n  ): F[(StatusCode, Response[String])] =\n    for\n      _ <- jobOrBlock\n      result <- basicRequest\n        .response(asStringAlways)\n        .post(uri)\n        .contentType(MediaType.ApplicationJson)\n        .body(body)\n        .send(backend)\n    yield (result.code, result)\n\n  def getBlock(\n      blockHash: String,\n  ): F[(StatusCode, String)] =\n    baseUrlsLock.get\n      .flatMap { urls =>\n        urls.traverse { baseUrl =>\n          getAsString(uri\"$baseUrl/block/$blockHash\")\n        }\n      }\n      .map(_.head)\n\n  def getAccount(\n      account: Account,\n  ): F[(StatusCode, String)] =\n    baseUrlsLock.get\n      .flatMap { urls =>\n        urls.traverse { baseUrl =>\n          getAsString(uri\"$baseUrl/account/${account.utf8.value}\")\n        }\n      }\n      .map(_.head)\n\n  def getEthAccount(\n      ethAddress: EthAddress,\n  ): F[(StatusCode, String)] =\n    baseUrlsLock.get\n      .flatMap { urls =>\n        urls.traverse { baseUrl =>\n          getAsString(uri\"$baseUrl/eth/$ethAddress\")\n        }\n      }\n      .map(_.head)\n\n  def getGroupInfo(\n      groupId: GroupId,\n  ): F[(StatusCode, String)] =\n    baseUrlsLock.get\n      .flatMap { urls =>\n        urls.traverse { baseUrl =>\n          getAsString(uri\"$baseUrl/group/$groupId\")\n        }\n      }\n      .map(_.head)\n\n  def getBlockList(\n      fromOption: Option[String],\n      limitOption: Option[Int],\n  ): F[(StatusCode, String)] =\n    baseUrlsLock.get\n      .flatMap { urls =>\n        urls.traverse { baseUrl =>\n          getAsString(uri\"$baseUrl/block?from=${fromOption\n              .getOrElse(\"\")}&limit=${limitOption.getOrElse(\"\")}\")\n        }\n      }\n      .map(_.head)\n\n  def getStatus: F[(StatusCode, String)] =\n    baseUrlsLock.get\n      .flatMap { urls =>\n        urls.traverse { baseUrl =>\n          getAsString(uri\"$baseUrl/status\")\n        }\n      }\n      .map(_.head)\n\n  def getTokenDef(\n      tokenDefinitionId: TokenDefinitionId,\n  ): F[(StatusCode, String)] =\n    baseUrlsLock.get\n      .flatMap { urls =>\n        urls.traverse { baseUrl =>\n          getAsString(uri\"$baseUrl/token-def/$tokenDefinitionId\")\n        }\n      }\n      .map(_.head)\n\n  def getBalance(\n      account: Account,\n      movable: String,\n  ): F[(StatusCode, String)] =\n    baseUrlsLock.get\n      .flatMap { urls =>\n        urls.traverse { baseUrl =>\n          getAsString(uri\"$baseUrl/balance/$account?movable=${movable}\")\n        }\n      }\n      .map(_.head)\n\n  def getNftBalance(\n      account: Account,\n      movable: Option[String],\n  ): F[(StatusCode, String)] =\n    baseUrlsLock.get\n      .flatMap { urls =>\n        urls.traverse { baseUrl =>\n          getAsString(uri\"$baseUrl/nft-balance/$account?movable=${movable}\")\n        }\n      }\n      .map(_.head)\n\n  def getToken(\n      tokenId: TokenId,\n  ): F[(StatusCode, String)] =\n    baseUrlsLock.get\n      .flatMap { urls =>\n        urls.traverse { baseUrl =>\n          getAsString(uri\"$baseUrl/token/$tokenId\")\n        }\n      }\n      .map(_.head)\n\n  def getTokenHistory(\n      txHash: String,\n  ): F[(StatusCode, String)] =\n    baseUrlsLock.get\n      .flatMap { urls =>\n        urls.traverse { baseUrl =>\n          getAsString(uri\"$baseUrl/token-hist/$txHash\")\n        }\n      }\n      .map(_.head)\n\n  def getOwners(\n      tokenDefinitionId: TokenDefinitionId,\n  ): F[(StatusCode, String)] =\n    baseUrlsLock.get\n      .flatMap { urls =>\n        urls.traverse { baseUrl =>\n          getAsString(uri\"$baseUrl/owners/$tokenDefinitionId\")\n        }\n      }\n      .map(_.head)\n\n  def getAccountActivity(\n      account: Account,\n  ): F[(StatusCode, String)] =\n    baseUrlsLock.get\n      .flatMap { urls =>\n        urls.traverse { baseUrl =>\n          getAsString(uri\"$baseUrl/activity/account/$account\")\n        }\n      }\n      .map(_.head)\n\n  def getTokenActivity(\n      tokenId: TokenId,\n  ): F[(StatusCode, String)] =\n    baseUrlsLock.get\n      .flatMap { urls =>\n        urls.traverse { baseUrl =>\n          getAsString(uri\"$baseUrl/activity/token/$tokenId\")\n        }\n      }\n      .map(_.head)\n\n  def getAccountSnapshot(\n      account: Account,\n  ): F[(StatusCode, String)] =\n    baseUrlsLock.get\n      .flatMap { urls =>\n        urls.traverse { baseUrl =>\n          getAsString(uri\"$baseUrl/snapshot/account/$account\")\n        }\n      }\n      .map(_.head)\n\n  def getTokenSnapshot(\n      tokenId: TokenId,\n  ): F[(StatusCode, String)] =\n    baseUrlsLock.get\n      .flatMap { urls =>\n        urls.traverse { baseUrl =>\n          getAsString(uri\"$baseUrl/snapshot/token/$tokenId\")\n        }\n      }\n      .map(_.head)\n\n  def getStatusEndpoint: F[(StatusCode, String)] =\n    baseUrlsLock.get\n      .flatMap { urls =>\n        urls.traverse { baseUrl =>\n          getAsString(uri\"$baseUrl/status\")\n        }\n      }\n      .map(_.head)\n\n  def getOwnershipSnapshot(\n      tokenId: TokenId,\n  ): F[(StatusCode, String)] =\n    baseUrlsLock.get\n      .flatMap { urls =>\n        urls.traverse { baseUrl =>\n          getAsString(uri\"$baseUrl/snapshot/ownership/$tokenId\")\n        }\n      }\n      .map(_.head)\n\n  def getOwnershipSnapshotMap(\n      tokenId: Option[TokenId],\n      limit: Option[Int],\n  ): F[(StatusCode, String)] =\n    baseUrlsLock.get\n      .flatMap { urls =>\n        urls.traverse { baseUrl =>\n          getAsString(\n            uri\"$baseUrl/snapshot/ownership?from=$tokenId&limit=$limit\",\n          )\n        }\n      }\n      .map(_.head)\n\n  def getOwnershipRewarded(\n      tokenId: TokenId,\n  ): F[(StatusCode, String)] =\n    baseUrlsLock.get\n      .flatMap { urls =>\n        urls.traverse { baseUrl =>\n          getAsString(uri\"$baseUrl/rewarded/ownership/$tokenId\")\n        }\n      }\n      .map(_.head)\n\n  def getReward(\n      tokenId: TokenId,\n  ): F[(StatusCode, String)] =\n    baseUrlsLock.get\n      .flatMap { urls =>\n        urls.traverse { baseUrl =>\n          getAsString(uri\"$baseUrl/rewarded/ownership/$tokenId\")\n        }\n      }\n      .map(_.head)\n\n  def getDaoInfo(groupId: GroupId): F[(StatusCode, String)] =\n    baseUrlsLock.get\n      .flatMap: urls =>\n        urls.traverse: baseUrl =>\n          getAsString(uri\"$baseUrl/dao/$groupId\")\n      .map(_.head)\n\n  def getTx(\n      txHash: String,\n  ): F[(StatusCode, String)] =\n    baseUrlsLock.get\n      .flatMap { urls =>\n        urls.traverse { baseUrl =>\n          getAsString(uri\"$baseUrl/tx/$txHash\")\n        }\n      }\n      .map(_.head)\n\n  def getTxFromOld(\n      txHash: String,\n  ): F[(StatusCode, Either[String, TxModel])] =\n    baseUrlsLock.get.map(_.head).flatMap { baseUrl =>\n      getTxAsResponse[TxModel](uri\"$baseUrl/tx/$txHash\")\n    }\n\n  def getTxSet(\n      txHash: String,\n  ): F[(StatusCode, String)] =\n    baseUrlsLock.get\n      .flatMap { urls =>\n        urls.traverse { baseUrl =>\n          getAsString(uri\"$baseUrl/tx/$txHash\")\n        }\n      }\n      .map(_.head)\n\n  def postTx(\n      txs: String,\n  ): F[(StatusCode, String)] =\n    queue.push(txs)\n    baseUrlsLock.get\n      .flatMap { urls =>\n        urls.traverse { baseUrl =>\n          postAsString(\n            uri\"$baseUrl/tx\",\n            txs,\n          )\n        }\n      }\n      .map(_.head)\n\n  def postTx(\n      baseUrl: String,\n      txs: String,\n  ): F[(StatusCode, Response[String])] =\n    postAsResponse(\n      uri\"$baseUrl/tx\",\n      txs,\n    )\n\n  def postTxHash(\n      txs: String,\n  ): F[(StatusCode, String)] =\n    baseUrlsLock.get\n      .flatMap { urls =>\n        urls.traverse { baseUrl =>\n          postAsString(\n            uri\"$baseUrl/txhash\",\n            txs,\n          )\n        }\n      }\n      .map(_.head)\n\n  def jobOrBlock: F[Unit] =\n    blocker.get.flatMap { value =>\n      value match\n        case false =>\n          Async[F].sleep(3.second)\n            >> jobOrBlock\n        case true => Async[F].unit\n    }\n\n  def bestBlock(baseUri: String): F[(StatusCode, Block)] =\n    for\n      res <- get[NodeStatus](uri\"$baseUri/status\")\n      (_, nodeStatus) = res\n      bestBlock <- block(baseUri, nodeStatus.bestHash.toUInt256Bytes.toHex)\n    yield bestBlock\n\n  def block(baseUri: String, blockHash: String): F[(StatusCode, Block)] =\n    get[Block](uri\"$baseUri/block/$blockHash\")\n\n  def postTxs(baseUri: String, body: String): F[(StatusCode, String)] =\n    postAsString(uri\"$baseUri/tx\", body)\n\n  def getAsOption[A: io.circe.Decoder](uri: Uri): F[Option[A]] =\n    basicRequest\n      .get(uri)\n      .send(backend)\n      .map { response =>\n        response.body.toOption.flatMap { body =>\n          decode[A](body).toOption\n        }\n      }\n\n  def get[A: io.circe.Decoder](uri: Uri): F[(StatusCode, A)] =\n    basicRequest\n      .get(uri)\n      .send(backend)\n      .flatMap { response =>\n        response.body match\n          case Right(body) =>\n            decode[A](body) match\n              case Right(a)    => Async[F].pure((response.code, a))\n              case Left(error) => Async[F].raiseError(error)\n          case Left(error) =>\n            Async[F].raiseError(\n              new Exception(s\"Error in response body: $error\"),\n            )\n      }\n\n  def getSnapshotState(\n      tokenDefinitionId: TokenDefinitionId,\n  ): F[(StatusCode, String)] =\n    baseUrlsLock.get.flatMap { urls =>\n      urls.headOption match\n        case Some(baseUrl) =>\n          val uri = uri\"$baseUrl/snapshot-state/$tokenDefinitionId\"\n          getAsString(uri)\n        case None =>\n          Async[F].pure(\n            (StatusCode.ServiceUnavailable, \"No base URL available\"),\n          )\n    }\n\n  def getFungibleSnapshotBalance(\n      account: Account,\n      tokenDefinitionId: TokenDefinitionId,\n      snapshotId: String,\n  ): F[(StatusCode, String)] =\n    baseUrlsLock.get.flatMap { urls =>\n      urls.headOption match\n        case Some(baseUrl) =>\n          val uri =\n            uri\"$baseUrl/snapshot-balance/$account/$tokenDefinitionId/$snapshotId\"\n          getAsString(uri)\n        case None =>\n          Async[F].pure(\n            (StatusCode.ServiceUnavailable, \"No base URL available\"),\n          )\n    }\n\n  def getNftSnapshotBalance(\n      account: Account,\n      tokenDefinitionId: TokenDefinitionId,\n      snapshotId: String,\n  ): F[(StatusCode, String)] =\n    baseUrlsLock.get.flatMap { urls =>\n      urls.headOption match\n        case Some(baseUrl) =>\n          val uri =\n            uri\"$baseUrl/nft-snapshot-balance/$account/$tokenDefinitionId/$snapshotId\"\n          getAsString(uri)\n        case None =>\n          Async[F].pure(\n            (StatusCode.ServiceUnavailable, \"No base URL available\"),\n          )\n    }\n\n  def getVoteProposal(\n      proposalId: String,\n  ): F[(StatusCode, String)] =\n    baseUrlsLock.get.flatMap { urls =>\n      urls.headOption match\n        case Some(baseUrl) =>\n          val uri = uri\"$baseUrl/vote/proposal/$proposalId\"\n          getAsString(uri)\n        case None =>\n          Async[F].pure(\n            (StatusCode.ServiceUnavailable, \"No base URL available\"),\n          )\n    }\n\n  def getAccountVotes(\n      proposalId: String,\n      account: Account,\n  ): F[(StatusCode, String)] =\n    baseUrlsLock.get.flatMap { urls =>\n      urls.headOption match\n        case Some(baseUrl) =>\n          val uri = uri\"$baseUrl/vote/account/$proposalId/$account\"\n          getAsString(uri)\n        case None =>\n          Async[F].pure(\n            (StatusCode.ServiceUnavailable, \"No base URL available\"),\n          )\n    }\n\n  def getVoteCount(\n      proposalId: String,\n  ): F[(StatusCode, String)] =\n    baseUrlsLock.get.flatMap { urls =>\n      urls.headOption match\n        case Some(baseUrl) =>\n          val uri = uri\"$baseUrl/vote/count/$proposalId\"\n          getAsString(uri)\n        case None =>\n          Async[F].pure(\n            (StatusCode.ServiceUnavailable, \"No base URL available\"),\n          )\n    }\n\n  def getCreatorDaoInfo(\n      creatorDaoId: CreatorDaoId,\n  ): F[(StatusCode, String)] =\n    baseUrlsLock.get.flatMap { urls =>\n      urls.headOption match\n        case Some(baseUrl) =>\n          val uri = uri\"$baseUrl/creator-dao/$creatorDaoId\"\n          getAsString(uri)\n        case None =>\n          Async[F].pure(\n            (StatusCode.ServiceUnavailable, \"No base URL available\"),\n          )\n    }\n\n  def getCreatorDaoMember(\n      creatorDaoId: CreatorDaoId,\n      from: Option[Account],\n      limit: Option[Int],\n  ): F[(StatusCode, String)] =\n    baseUrlsLock.get.flatMap { urls =>\n      urls.headOption match\n        case Some(baseUrl) =>\n          val uri = uri\"$baseUrl/creator-dao/$creatorDaoId/member\"\n            .addParam(\"from\", from.map(_.utf8.value).getOrElse(\"\"))\n            .addParam(\"limit\", limit.map(_.toString).getOrElse(\"\"))\n          getAsString(uri)\n        case None =>\n          Async[F].pure(\n            (StatusCode.ServiceUnavailable, \"No base URL available\"),\n          )\n    }\n"
  },
  {
    "path": "modules/node-proxy/src/main/scala/io/leisuremeta/chain/node/proxy/service/NodeBalancer.scala",
    "content": "package io.leisuremeta.chain.node.proxy\npackage service\n\nimport java.nio.file.{Files, Paths, StandardOpenOption}\nimport cats.implicits.*\nimport io.leisuremeta.chain.api.model.Block\nimport cats.effect.Ref\nimport cats.effect.kernel.Async\nimport sttp.model.StatusCode\nimport model.NodeConfig\nimport fs2.io.file.Path\nimport fs2.text.utf8\nimport scala.concurrent.duration._\nimport io.leisuremeta.chain.lib.datatype.BigNat\nimport io.leisuremeta.chain.node.proxy.model.TxModel\nimport fs2.Chunk\n\nimport fs2.{Stream, text}\nimport fs2.Pipe\n\ncase class NodeBalancer[F[_]: Async] (\n  apiService:   InternalApiService[F],\n  blocker:      Ref[F, Boolean],\n  baseUrlsLock: Ref[F, List[String]],\n  nodeConfig:   NodeConfig,\n  queue:        PostTxQueue[F]\n):\n  // startBlock: bestBlock in old blockchain\n  def logDiffTxsLoop(startBlock: Block, endBlockNumber: BigNat): F[Unit] =\n    def getTxWithExponentialRetry(txHash: String, retries: Int, delay: FiniteDuration): F[Option[String]] =\n      apiService.getTxFromOld(txHash).flatMap { (_, res) =>\n        res match\n          case Right(txModel) => Async[F].pure(Some(txModel.signedTx))\n          case Left(err) if retries > 0 => Async[F].sleep(delay) \n                                            *> getTxWithExponentialRetry(txHash, retries - 1, delay * 2)\n          case _ => Async[F].pure(None)\n      }\n      \n    def loop(currBlock: Block): F[Unit] = \n      if (currBlock.header.number != endBlockNumber) {\n        println(s\"block download number: ${currBlock.header.number}\")\n        val parentHash = currBlock.header.parentHash.toUInt256Bytes.toBytes.toHex\n        for \n          txList    <- currBlock.transactionHashes.toList.traverse { txHash => \n                         getTxWithExponentialRetry(txHash.toUInt256Bytes.toBytes.toHex, 5, 1.second) }\n          filteredTxList = txList.flatMap(identity)\n          _         <- filteredTxList match \n                      case head :: tail => appendLog(\"diff-txs.json\", s\"[${filteredTxList.mkString(\",\")}]\")\n                      case Nil => Async[F].unit\n          res       <- apiService.block(nodeConfig.oldNodeAddress, parentHash)\n          (_, prevBlock) = res\n          _         <- loop(prevBlock)\n        yield ()\n      } else {\n        scribe.info(\"logDiffTxsLoop 종료\")\n        Async[F].unit\n      }\n\n    loop(startBlock)\n\n  def appendLog(path: String, json: String): F[Unit] = Async[F].blocking:\n    val _ = java.nio.file.Files.write(\n      Paths.get(path),\n      (json + \"\\n\").getBytes,\n      StandardOpenOption.CREATE,\n      StandardOpenOption.WRITE,\n      StandardOpenOption.APPEND,\n    )\n  \n  def postTxWithExponentialRetry(line: String, retries: Int, delay: FiniteDuration): F[String] =\n    println(s\"request txs: $line\")\n    apiService.postTx(nodeConfig.newNodeAddress.get, line).flatMap { (_, res) => //(statusCode, res) =>\n      println(s\"generated hash: $res\")\n      val code = res.code\n      if code.isSuccess \n        then Async[F].pure(res.body)\n      else if (code.isServerError || res.body.isEmpty) && retries > 0\n        then Async[F].sleep(delay) \n              *> postTxWithExponentialRetry(line, retries - 1, delay * 2)\n      else Async[F].pure(\"\")\n    }\n\n  def createTxsToNewBlockchain: F[Option[String]] = \n    val path = fs2.io.file.Path(\"diff-txs.json\")\n\n    def processLinesReversed(): F[Option[String]] = \n      def reverseLines(): F[Option[String]] = \n        def reversePipe: Pipe[F, String, String] = _.flatMap { s =>\n          Stream.chunk(Chunk.from(s.split('\\n').toVector.reverse))\n        }.fold(Vector.empty[String]) { (acc, line) =>\n          line +: acc\n        }.flatMap(Stream.emits)\n\n        fs2.io.file.Files.forAsync[F]\n          .readAll(path)\n          .through(fs2.text.utf8.decode)\n          .through(text.lines)\n          .through(reversePipe)\n          .scan((Option.empty[String], Option.empty[String])) { case ((_, prev), txs) =>\n            if (txs.isBlank) (None, prev)\n            else (Some(txs), Some(txs))\n          }\n          .evalMap { case (txsOpt, lastTxs) => \n            txsOpt match \n              case None => Async[F].pure(lastTxs)\n              case Some(txs) => postTxWithExponentialRetry(txs, 5, 1.second).as(lastTxs)\n          }\n          .last\n          .compile\n          .lastOrError\n          .map(_.flatten)\n          .flatMap(Async[F].pure)\n\n      reverseLines()\n    processLinesReversed()\n\n\n  // def createTxsToNewBlockchain1[F[_]: Async](implicit C: Concurrent[F], console: Console[F]): F[Unit] = \n  //   val path = Paths.get(\"diff-txs.json\")\n\n  //   def reverseFile(): F[Unit] =\n  //     Stream.eval(fs2.io.file.Files[F].size(path)).flatMap { size =>\n  //       Stream.unfoldLoopEval(size - 1L) { offset =>\n  //         Async[F].uncancelable { _ =>\n  //           fs2.io.file.Files[F].readRange(path, 1, offset, offset + 1)\n  //             .through(fs2.text.utf8.decode)\n  //             .compile\n  //             .string\n  //             .map { char =>\n  //               (char, if (char.nonEmpty) Some(offset - 1L) else None)\n  //             }\n  //         }\n  //       }\n  //     }\n  //     .through(fs2.text.utf8.encode)\n  //     .through(fs2.text.utf8.decode)\n  //     .through(text.lines)\n  //     .evalMap { txs =>\n  //       if (txs.isBlank) Async[F].unit\n  //       else postTxWithExponentialRetry(txs, 5, 1.second).as(())\n  //     }\n  //     .last\n  //     .compile\n  //     .lastOrError\n\n  //   reverseFile()\n      \n\n  def deleteAllFiles: F[Unit] = Async[F].blocking:\n    val diffTxs = Paths.get(\"diff-txs.json\")\n    val _ = Files.deleteIfExists(diffTxs)\n\n  def run(): F[Unit] =\n    def loop(endBlockNumber: BigNat, lastTxsOpt: Option[String]): F[Unit] = \n      val newNodeAddr = nodeConfig.newNodeAddress.get\n      val oldNodeAddr = nodeConfig.oldNodeAddress\n      scribe.info(s\"[oldNode: $oldNodeAddr]\", s\"-> [newNode: $newNodeAddr]\")\n      for \n        response   <- apiService.bestBlock(oldNodeAddr)\n        (_, startBlock) = response\n        _          <- if (startBlock.header.number == endBlockNumber) {\n                        for \n                          _ <- blocker.set(false)\n                          _ <- queue.pollsAfter(lastTxsOpt).flatMap { jsons => \n                                 jsons.traverse { txs => postTxWithExponentialRetry(txs, 5, 1.second)}\n                               } \n                          _ <- baseUrlsLock.getAndUpdate{_.appended(nodeConfig.newNodeAddress.get)} \n                          _ <- blocker.set(true) // 양쪽 모두 릴레이 시작.\n                          _ <- Async[F].delay(scribe.info(\"마이그레이션 성공. 양쪽 모두 API 릴레이 시작\"))\n                          newNodeCfg <- NodeWatchService.waitTerminateSig\n                          _ <- Async[F].delay(scribe.info(s\"now api request only relayed to ${newNodeCfg.oldNodeAddress}\"))\n                          _ <- baseUrlsLock.set(List(newNodeCfg.oldNodeAddress))\n                        yield ()\n                      } else {\n                        logDiffTxsLoop(startBlock, endBlockNumber)\n                        >> createTxsToNewBlockchain.flatMap { lastTxs => \n                          val validLastTxs = lastTxs match\n                            case Some(lastTxs) => Some(lastTxs)\n                            case None => lastTxsOpt\n                          deleteAllFiles\n                          >> loop(startBlock.header.number, validLastTxs) \n                        }\n                      }\n      yield ()\n\n    loop(nodeConfig.blockNumber.getOrElse(\n          throw new NoSuchElementException(\"local migration 이 완료된 blockNumber를 적어주세요.\")),\n         None)\n\n  "
  },
  {
    "path": "modules/node-proxy/src/main/scala/io/leisuremeta/chain/node/proxy/service/NodeWatchService.scala",
    "content": "package io.leisuremeta.chain.node.proxy\npackage service\n\nimport java.nio.file.{Files, Paths}\n\nimport scala.concurrent.duration._\nimport scala.jdk.CollectionConverters._\nimport scala.util.Try\n\nimport cats.effect.{Async, Ref}\nimport cats.syntax.all._\n\nimport io.circe.generic.auto._\nimport io.circe.parser.decode\n\nimport model.NodeConfig\n\n\nobject NodeWatchService:\n  def nodeConfig[F[_]: Async]: F[Either[Throwable, NodeConfig]] = Async[F].blocking {\n    // val path = Paths.get(\"/Users/jichangho/playnomm/leisuremeta-chain/migration-node.json\")\n    // val path = Paths.get(\"/Users/user/playnomm/source_code/leisuremeta-chain/migration-node.json\")\n    val path = Paths.get(\"/home/rocky/nodeproxy/migration-node.json\")\n\n      for \n        json <- Try(Files.readAllLines(path).asScala.mkString(\"\\n\")).toEither\n        nodeConfig <- decode[NodeConfig](json)\n      yield nodeConfig\n  }\n\n  def newNodeWatchLoop[F[_]: Async](\n    apiService: InternalApiService[F], \n    blcUrls:    Ref[F, List[String]],\n    blocker:    Ref[F, Boolean],\n    queue:      PostTxQueue[F]\n  ): F[Unit] = \n    def loop: F[Option[Unit]] = \n      nodeConfig.flatMap { nodeConfigEither =>\n        scribe.info(s\"newNodeWatchLoop\")\n        nodeConfigEither match \n          case Right(nodeConfig) => \n            (nodeConfig.oldNodeAddress, nodeConfig.newNodeAddress) match \n              case (oldNodeAddress, Some(newNodeAddress)) if !newNodeAddress.trim.isEmpty => \n                scribe.info(s\"nodeConfig: $nodeConfig\")\n                blcUrls.set(List(nodeConfig.oldNodeAddress)) \n                *> NodeBalancer(apiService, blocker, blcUrls, nodeConfig, queue).run() \n                *> Async[F].pure(Some(()))\n              case (oldNodeAddress, _) => // newNodeAddress is None | isEmpty\n                for \n                  urls <- blcUrls.get\n                  _    <- if urls.isEmpty then blcUrls.set(List(oldNodeAddress))\n                          else if !urls.head.equals(oldNodeAddress) then blcUrls.set(List(oldNodeAddress))\n                          else Async[F].unit\n                yield Some(())\n          case Left(error) =>\n            scribe.error(s\"Error decoding Node Config: $error\\nreconfigure migration-node.json file.\")\n            Async[F].pure(Some(()))\n      }\n\n    loop.flatMap {\n      case Some(_) => Async[F].sleep(5.seconds) >> newNodeWatchLoop(apiService, blcUrls, blocker, queue)\n      case None    => Async[F].unit\n    }\n  \n  def startOnNew[F[_]: Async](\n    apiService: InternalApiService[F], \n    blcUrls:    Ref[F, List[String]], \n    blocker:    Ref[F, Boolean],\n    queue:      PostTxQueue[F]\n  ): F[Unit] = \n    Async[F].executionContext.flatMap { executionContext =>\n      Async[F].startOn(\n        newNodeWatchLoop(apiService, blcUrls, blocker, queue),\n        executionContext\n      ) *> Async[F].unit\n    } \n\n  def waitTerminateSig[F[_]: Async]: F[NodeConfig] =\n    def loop: F[NodeConfig] =\n      nodeConfig.flatMap { nodeCfgEither =>\n        nodeCfgEither match\n          case Right(nodeConfig) =>\n            nodeConfig.newNodeAddress match\n              case Some(address) if address.isBlank() => \n                        Async[F].delay(scribe.info(\"마이그레이션 종료.\")) \n                         >> Async[F].pure(nodeConfig)\n              case _ => Async[F].delay(scribe.info(\"종료 시그널 기다리는 중..\")) \n                         >> Async[F].sleep(10.second) \n                         >> loop\n          case Left(error) =>\n            scribe.error(s\"Error decoding Node Config: $error\\nreconfigure migration-node.json file.\")\n            Async[F].sleep(5.second)\n            loop\n      }\n    loop\n"
  },
  {
    "path": "modules/node-proxy/src/main/scala/io/leisuremeta/chain/node/proxy/service/PostTxQueue.scala",
    "content": "package io.leisuremeta.chain.node.proxy.service\n\nimport cats.effect.kernel.Async\nimport cats.syntax.all.*\nimport cats.effect.std.Queue\n\nobject PostTxQueue:\n  def apply[F[_]: Async]: F[PostTxQueue[F]] =\n    Queue.circularBuffer[F, String](5).map { \n      queue => new PostTxQueue[F](queue)\n    }\n\nclass PostTxQueue[F[_]: Async](\n  queue: Queue[F, String]\n):\n  def push(txJson: String): F[Unit] =\n    queue.offer(txJson)\n\n  def pollsAfter(lastTxOpt: Option[String]): F[List[String]] =\n    lastTxOpt match\n      case None => Async[F].pure(List.empty)\n      case Some(lastTx) => \n        queue.tryTakeN(None).map {  \n          _.dropWhile(_ != lastTx)\n           .drop(1)\n        }\n\n  def peek(): F[Unit] =\n    for {\n      items <- queue.tryTakeN(None)\n      _     <- items.traverse { item => Async[F].delay(println(item)) }\n      _     <- items.traverse(queue.offer(_))\n    } yield ()\n    \n    \n  "
  },
  {
    "path": "project/Settings.scala.sample",
    "content": "import sbt._\n\nobject Settings {\n    val flywaySettings = new {\n        lazy val url = \"jdbc:postgresql://127.0.0.1:5432/postgres\"\n        lazy val user = \"postgres\"\n        lazy val pwd = \"1234\"\n        lazy val schemas = Seq(\"public\")\n        lazy val locations = Seq(\"db/test\", \"db/common\")\n    }\n}\n"
  },
  {
    "path": "project/build.properties",
    "content": "sbt.version=1.10.0\n\n"
  },
  {
    "path": "project/plugins.sbt",
    "content": "addDependencyTreePlugin\n\naddSbtPlugin(\"org.wartremover\" % \"sbt-wartremover\" % \"3.4.3\")\n\naddSbtPlugin(\"io.spray\" % \"sbt-revolver\" % \"0.10.0\")\n\naddSbtPlugin(\"org.scala-js\"       % \"sbt-scalajs\"              % \"1.16.0\")\naddSbtPlugin(\"org.portable-scala\" % \"sbt-scalajs-crossproject\" % \"1.3.2\")\naddSbtPlugin(\"ch.epfl.scala\"      % \"sbt-scalajs-bundler\"      % \"0.21.1\")\n\naddSbtPlugin(\"org.scalameta\"             % \"sbt-scalafmt\" % \"2.5.2\")\naddSbtPlugin(\"ch.epfl.scala\"             % \"sbt-scalafix\" % \"0.12.1\")\naddSbtPlugin(\"io.github.davidgregory084\" % \"sbt-tpolecat\" % \"0.4.4\")\n\naddSbtPlugin(\"org.scalablytyped.converter\" % \"sbt-converter\" % \"1.0.0-beta44\")\n\naddSbtPlugin(\"com.eed3si9n\" % \"sbt-assembly\" % \"2.2.0\")\n\naddSbtPlugin(\"io.github.davidmweber\" % \"flyway-sbt\" % \"7.4.0\")\n"
  }
]