Repository: leisuremeta/leisuremeta-chain Branch: main Commit: 5a2cd546471a Files: 290 Total size: 924.1 KB Directory structure: gitextract_d4szp668/ ├── .gitignore ├── .jvmopts ├── .scalafix.conf ├── .scalafmt.conf ├── README.md ├── build.sbt ├── docs/ │ ├── LeisureMeta_Chain_API.md │ ├── api_with_example.md │ ├── creator-dao-documentation.md │ └── dao-voting-system-design-english.md ├── modules/ │ ├── api/ │ │ ├── src/ │ │ │ └── main/ │ │ │ └── scala/ │ │ │ └── io/ │ │ │ └── leisuremeta/ │ │ │ └── chain/ │ │ │ └── api/ │ │ │ ├── LeisureMetaChainApi.scala │ │ │ └── model/ │ │ │ ├── Account.scala │ │ │ ├── AccountData.scala │ │ │ ├── AccountSignature.scala │ │ │ ├── Block.scala │ │ │ ├── GroupData.scala │ │ │ ├── GroupId.scala │ │ │ ├── NetworkId.scala │ │ │ ├── NodeStatus.scala │ │ │ ├── PublicKeySummary.scala │ │ │ ├── Signed.scala │ │ │ ├── StateRoot.scala │ │ │ ├── Transaction.scala │ │ │ ├── TransactionWithResult.scala │ │ │ ├── account/ │ │ │ │ ├── EthAddress.scala │ │ │ │ ├── ExternalChain.scala │ │ │ │ └── ExternalChainAddress.scala │ │ │ ├── agenda/ │ │ │ │ └── AgendaId.scala │ │ │ ├── api_model/ │ │ │ │ ├── AccountInfo.scala │ │ │ │ ├── ActivityInfo.scala │ │ │ │ ├── BalanceInfo.scala │ │ │ │ ├── BlockInfo.scala │ │ │ │ ├── CreatorDaoInfo.scala │ │ │ │ ├── GroupInfo.scala │ │ │ │ ├── NftBalanceInfo.scala │ │ │ │ ├── RewardInfo.scala │ │ │ │ └── TxInfo.scala │ │ │ ├── creator_dao/ │ │ │ │ ├── CreatorDaoData.scala │ │ │ │ └── CreatorDaoId.scala │ │ │ ├── reward/ │ │ │ │ ├── ActivityLog.scala │ │ │ │ ├── ActivityRewardLog.scala │ │ │ │ ├── ActivitySnapshot.scala │ │ │ │ ├── DaoActivity.scala │ │ │ │ ├── DaoInfo.scala │ │ │ │ ├── OwnershipRewardLog.scala │ │ │ │ └── OwnershipSnapshot.scala │ │ │ ├── token/ │ │ │ │ ├── NftInfo.scala │ │ │ │ ├── NftInfoWithPrecision.scala │ │ │ │ ├── NftState.scala │ │ │ │ ├── Rarity.scala │ │ │ │ ├── SnapshotState.scala │ │ │ │ ├── TokenDefinition.scala │ │ │ │ ├── TokenDefinitionId.scala │ │ │ │ ├── TokenDetail.scala │ │ │ │ └── TokenId.scala │ │ │ └── voting/ │ │ │ ├── Proposal.scala │ │ │ ├── ProposalId.scala │ │ │ └── VoteType.scala │ │ └── tx_type.txt │ ├── archive/ │ │ └── src/ │ │ └── main/ │ │ └── scala/ │ │ └── io/ │ │ └── leisuremeta/ │ │ └── chain/ │ │ └── archive/ │ │ └── ArchiveMain.scala │ ├── bulk-insert/ │ │ └── src/ │ │ └── main/ │ │ └── scala/ │ │ └── io/ │ │ └── leisuremeta/ │ │ └── chain/ │ │ └── bulkinsert/ │ │ ├── BulkInsertMain.scala │ │ ├── FungibleBalanceState.scala │ │ ├── InvalidTx.scala │ │ ├── NftBalanceState.scala │ │ └── RecoverTx.scala │ ├── eth-gateway/ │ │ └── src/ │ │ └── main/ │ │ └── scala/ │ │ └── io/ │ │ └── leisuremeta/ │ │ └── chain/ │ │ └── gateway/ │ │ └── eth/ │ │ └── EthGatewayMain.scala │ ├── eth-gateway-common/ │ │ └── src/ │ │ ├── main/ │ │ │ └── scala/ │ │ │ └── io/ │ │ │ └── leisuremeta/ │ │ │ └── chain/ │ │ │ └── gateway/ │ │ │ └── eth/ │ │ │ └── common/ │ │ │ ├── GatewayApi.scala │ │ │ ├── GatewayConf.scala │ │ │ ├── GatewayDecryptService.scala │ │ │ ├── GatewayResource.scala │ │ │ ├── GatewayServer.scala │ │ │ ├── GatewaySimpleConf.scala │ │ │ ├── GatewayWeb3Service.scala │ │ │ └── client/ │ │ │ ├── GatewayApiClient.scala │ │ │ ├── GatewayDatabaseClient.scala │ │ │ └── GatewayKmsClient.scala │ │ └── test/ │ │ └── scala/ │ │ └── io/ │ │ └── leisuremeta/ │ │ └── chain/ │ │ └── gateway/ │ │ └── eth/ │ │ └── common/ │ │ └── GatewayServerTest.scala │ ├── eth-gateway-deposit/ │ │ └── src/ │ │ └── main/ │ │ ├── resources/ │ │ │ └── application.conf.sample │ │ └── scala/ │ │ └── io/ │ │ └── leisuremeta/ │ │ └── chain/ │ │ └── gateway/ │ │ └── eth/ │ │ └── EthGatewayDepositMain.scala │ ├── eth-gateway-setup/ │ │ └── src/ │ │ └── main/ │ │ ├── resources/ │ │ │ └── application.conf.sample │ │ └── scala/ │ │ └── io/ │ │ └── leisuremeta/ │ │ └── chain/ │ │ └── gateway/ │ │ └── eth/ │ │ └── setup/ │ │ ├── EthGatewaySetupConfig.scala │ │ ├── EthGatewaySetupMain.scala │ │ └── EthGatewaySetupSimpleConfig.scala │ ├── eth-gateway-withdraw/ │ │ └── src/ │ │ └── main/ │ │ ├── resources/ │ │ │ └── application.conf.sample │ │ └── scala/ │ │ └── io/ │ │ └── leisuremeta/ │ │ └── chain/ │ │ └── gateway/ │ │ └── eth/ │ │ └── EthGatewayWithdrawMain.scala │ ├── jvm-client/ │ │ └── src/ │ │ └── main/ │ │ └── scala/ │ │ └── io/ │ │ └── leisuremeta/ │ │ └── chain/ │ │ └── jvmclient/ │ │ └── JvmClientMain.scala │ ├── lib/ │ │ ├── js/ │ │ │ └── src/ │ │ │ └── main/ │ │ │ └── scala/ │ │ │ └── io/ │ │ │ └── leisuremeta/ │ │ │ └── chain/ │ │ │ └── lib/ │ │ │ └── crypto/ │ │ │ └── CryptoOps.scala │ │ ├── jvm/ │ │ │ └── src/ │ │ │ └── main/ │ │ │ └── scala/ │ │ │ └── io/ │ │ │ └── leisuremeta/ │ │ │ └── chain/ │ │ │ └── lib/ │ │ │ └── crypto/ │ │ │ └── CryptoOps.scala │ │ └── shared/ │ │ └── src/ │ │ ├── main/ │ │ │ └── scala/ │ │ │ └── io/ │ │ │ └── leisuremeta/ │ │ │ └── chain/ │ │ │ └── lib/ │ │ │ ├── application/ │ │ │ │ └── DAppState.scala │ │ │ ├── codec/ │ │ │ │ └── byte/ │ │ │ │ ├── ByteCodec.scala │ │ │ │ ├── ByteDecoder.scala │ │ │ │ └── ByteEncoder.scala │ │ │ ├── crypto/ │ │ │ │ ├── Hash.scala │ │ │ │ ├── KeyPair.scala │ │ │ │ ├── PublicKey.scala │ │ │ │ ├── Recover.scala │ │ │ │ ├── Sign.scala │ │ │ │ └── Signature.scala │ │ │ ├── datatype/ │ │ │ │ ├── BigNat.scala │ │ │ │ ├── UInt256.scala │ │ │ │ └── Utf8.scala │ │ │ ├── failure/ │ │ │ │ └── LmChainFailure.scala │ │ │ ├── merkle/ │ │ │ │ ├── MerkleTrie.scala │ │ │ │ ├── MerkleTrieNode.scala │ │ │ │ ├── MerkleTrieState.scala │ │ │ │ ├── MerkleTrieStateDiff.scala │ │ │ │ └── package.scala │ │ │ └── util/ │ │ │ ├── iron/ │ │ │ │ └── package.scala │ │ │ └── refined/ │ │ │ └── bitVector.scala │ │ └── test/ │ │ └── scala/ │ │ └── io/ │ │ └── leisuremeta/ │ │ └── chain/ │ │ └── lib/ │ │ ├── codec/ │ │ │ └── ByteCodecTest.scala │ │ ├── crypto/ │ │ │ └── CryptoOpsTest.scala │ │ ├── datatype/ │ │ │ ├── BigNatTest.scala │ │ │ └── UInt256Test.scala │ │ └── merkle/ │ │ ├── MerkleTrieNodeTest.scala │ │ ├── MerkleTrieTest.scala │ │ └── NibblesTest.scala │ ├── lmscan-agent/ │ │ └── src/ │ │ └── main/ │ │ ├── resources/ │ │ │ └── application.conf.sample │ │ └── scala/ │ │ └── io/ │ │ └── leisuremeta/ │ │ └── chain/ │ │ └── lmscan/ │ │ └── agent/ │ │ ├── ScanAgentApp.scala │ │ ├── ScanAgentConfig.scala │ │ ├── ScanAgentMain.scala │ │ ├── apps/ │ │ │ ├── BalanceStoreApp.scala │ │ │ ├── NftStoreApp.scala │ │ │ ├── NodeDataStoreApp.scala │ │ │ └── SummaryStoreApp.scala │ │ └── service/ │ │ ├── RequestService.scala │ │ ├── StoreService.scala │ │ └── store/ │ │ ├── AccountStore.scala │ │ ├── BalanceStore.scala │ │ ├── BlockStore.scala │ │ ├── NftStore.scala │ │ ├── SummaryStore.scala │ │ └── TxStore.scala │ ├── lmscan-backend/ │ │ ├── docs/ │ │ │ └── flyway.md │ │ └── src/ │ │ ├── main/ │ │ │ ├── resources/ │ │ │ │ ├── application.sample.properties │ │ │ │ └── db/ │ │ │ │ ├── dist/ │ │ │ │ │ ├── V20230116164800__Alter_ts_pgdefault.sql │ │ │ │ │ ├── V20230116164800__Alter_ts_pgglobal.sql │ │ │ │ │ └── V20230116164801__Create_r_playnomm.sql │ │ │ │ ├── common/ │ │ │ │ │ ├── V20230116164802__Create_t_account.sql │ │ │ │ │ ├── V20230116164803__Create_t_block.sql │ │ │ │ │ ├── V20230116164805__Create_t_nft.sql │ │ │ │ │ └── V20230116164809__Create_t_transaction.sql │ │ │ │ ├── seed/ │ │ │ │ │ └── R__001_Seed_account.sql │ │ │ │ └── test/ │ │ │ │ └── V20230116164801__Create_r_playnomm.sql │ │ │ └── scala/ │ │ │ └── io/ │ │ │ └── leisuremeta/ │ │ │ └── chain/ │ │ │ └── lmscan/ │ │ │ └── backend/ │ │ │ ├── LmscanBackendMain.scala │ │ │ ├── docs/ │ │ │ │ └── Lmscan_API.md │ │ │ ├── entity/ │ │ │ │ ├── Account.scala │ │ │ │ ├── AccountMapper.scala │ │ │ │ ├── Balance.scala │ │ │ │ ├── Block.scala │ │ │ │ ├── CollectionInfo.scala │ │ │ │ ├── Nft.scala │ │ │ │ ├── NftFile.scala │ │ │ │ ├── NftInfo.scala │ │ │ │ ├── NftOwner.scala │ │ │ │ ├── NftSeason.scala │ │ │ │ ├── Summary.scala │ │ │ │ ├── Tx.scala │ │ │ │ ├── TxState.scala │ │ │ │ └── Validator.scala │ │ │ ├── repository/ │ │ │ │ ├── AccountRepository.scala │ │ │ │ ├── BlockRepository.scala │ │ │ │ ├── CommonQuery.scala │ │ │ │ ├── NftFileRepository.scala │ │ │ │ ├── NftInfoRepository.scala │ │ │ │ ├── NftOwnerRepository.scala │ │ │ │ ├── NftRepository.scala │ │ │ │ ├── SummaryRepository.scala │ │ │ │ ├── TransactionRepository.scala │ │ │ │ └── ValidatorRepository.scala │ │ │ └── service/ │ │ │ ├── AccountService.scala │ │ │ ├── BlockService.scala │ │ │ ├── NftService.scala │ │ │ ├── SearchService.scala │ │ │ ├── SummaryService.scala │ │ │ ├── TransactionService.scala │ │ │ └── ValidatorService.scala │ │ └── test/ │ │ └── scala/ │ │ └── EmbeddedPostgreFlywayTest.scala │ ├── lmscan-common/ │ │ ├── .js/ │ │ │ └── package.json │ │ ├── js/ │ │ │ └── package.json │ │ └── shared/ │ │ └── src/ │ │ └── main/ │ │ └── scala/ │ │ └── io/ │ │ └── leisuremeta/ │ │ ├── ExplorerAPI.scala │ │ └── model/ │ │ ├── AccountDetail.scala │ │ ├── AccountInfo.scala │ │ ├── ApiModel.scala │ │ ├── BlockDetail.scala │ │ ├── BlockInfo.scala │ │ ├── NftActivity.scala │ │ ├── NftDetail.scala │ │ ├── NftFileModel.scala │ │ ├── NftInfo.scala │ │ ├── NftOwnerInfo.scala │ │ ├── NftSeasonModel.scala │ │ ├── PageNavigation.scala │ │ ├── PageResponse.scala │ │ ├── SearchResult.scala │ │ ├── SummaryModel.scala │ │ ├── TxDetail.scala │ │ ├── TxInfo.scala │ │ └── Validator.scala │ ├── lmscan-frontend/ │ │ ├── .parcelrc │ │ ├── assets/ │ │ │ ├── css/ │ │ │ │ ├── desktop.css │ │ │ │ ├── footer.css │ │ │ │ ├── index.html │ │ │ │ ├── loading.css │ │ │ │ ├── mobile.css │ │ │ │ ├── reset.css │ │ │ │ ├── style.css │ │ │ │ └── tooltip.css │ │ │ ├── index.html │ │ │ └── load-main.js │ │ ├── package.json │ │ ├── project/ │ │ │ └── build.properties │ │ ├── readme.md │ │ └── src/ │ │ └── main/ │ │ └── scala/ │ │ └── io/ │ │ └── leisuremeta/ │ │ └── chain/ │ │ └── lmscan/ │ │ └── frontend/ │ │ ├── LmscanFrontendApp.scala │ │ ├── components/ │ │ │ ├── BoardView.scala │ │ │ ├── Footer.scala │ │ │ ├── Loader.scala │ │ │ ├── NavBar.scala │ │ │ ├── SearchView.scala │ │ │ ├── common/ │ │ │ │ ├── Body.scala │ │ │ │ ├── Head.scala │ │ │ │ ├── Pagination.scala │ │ │ │ └── Table.scala │ │ │ └── detail/ │ │ │ ├── AccountDetailTable.scala │ │ │ ├── blockDetailTable.scala │ │ │ ├── nftDetailTable.scala │ │ │ └── txDetailTableCommon.scala │ │ ├── controllers/ │ │ │ ├── Model.scala │ │ │ └── Msg.scala │ │ ├── layouts/ │ │ │ └── DefaultLayout.scala │ │ ├── pages/ │ │ │ ├── AccountDetailPage.scala │ │ │ ├── AccountPage.scala │ │ │ ├── BlockDetailPage.scala │ │ │ ├── BlockPage.scala │ │ │ ├── ErrorPage.scala │ │ │ ├── MainPage.scala │ │ │ ├── NftPage.scala │ │ │ ├── NftTokenPage.scala │ │ │ ├── NtfDetailPage.scala │ │ │ ├── TxDetailPage.scala │ │ │ ├── TxPage.scala │ │ │ ├── VdDetailPage.scala │ │ │ └── VdPage.scala │ │ └── utils/ │ │ ├── Cell.scala │ │ ├── DataProcess.scala │ │ └── ValidData.scala │ ├── node/ │ │ └── src/ │ │ ├── main/ │ │ │ ├── resources/ │ │ │ │ └── application.conf.sample │ │ │ └── scala/ │ │ │ └── io/ │ │ │ └── leisuremeta/ │ │ │ └── chain/ │ │ │ └── node/ │ │ │ ├── NodeApp.scala │ │ │ ├── NodeConfig.scala │ │ │ ├── NodeMain.scala │ │ │ ├── dapp/ │ │ │ │ ├── PlayNommDApp.scala │ │ │ │ ├── PlayNommDAppFailure.scala │ │ │ │ ├── PlayNommState.scala │ │ │ │ └── submodule/ │ │ │ │ ├── PlayNommDAppAccount.scala │ │ │ │ ├── PlayNommDAppAgenda.scala │ │ │ │ ├── PlayNommDAppCreatorDao.scala │ │ │ │ ├── PlayNommDAppGroup.scala │ │ │ │ ├── PlayNommDAppReward.scala │ │ │ │ ├── PlayNommDAppToken.scala │ │ │ │ ├── PlayNommDAppVoting.scala │ │ │ │ └── package.scala │ │ │ ├── repository/ │ │ │ │ ├── BlockRepository.scala │ │ │ │ ├── StateRepository.scala │ │ │ │ └── TransactionRepository.scala │ │ │ ├── service/ │ │ │ │ ├── BlockService.scala │ │ │ │ ├── LocalStatusService.scala │ │ │ │ ├── NodeInitializationService.scala │ │ │ │ ├── RewardService.scala │ │ │ │ ├── StateReadService.scala │ │ │ │ └── TransactionService.scala │ │ │ └── store/ │ │ │ ├── HashStore.scala │ │ │ ├── KeyValueStore.scala │ │ │ ├── SingleValueStore.scala │ │ │ └── interpreter/ │ │ │ ├── Bag.scala │ │ │ ├── MultiInterpreter.scala │ │ │ ├── RedisInterpreter.scala │ │ │ └── SwayInterpreter.scala │ │ └── test/ │ │ └── scala/ │ │ └── io/ │ │ └── leisuremeta/ │ │ └── chain/ │ │ └── node/ │ │ └── dapp/ │ │ └── PlayNommDAppTest.scala │ └── node-proxy/ │ └── src/ │ └── main/ │ ├── resources/ │ │ └── migration-node.json │ └── scala/ │ └── io/ │ └── leisuremeta/ │ └── chain/ │ └── node/ │ └── proxy/ │ ├── NodeProxyApi.scala │ ├── NodeProxyApp.scala │ ├── NodeProxyMain.scala │ ├── model/ │ │ ├── NodeConfig.scala │ │ └── TxModel.scala │ └── service/ │ ├── InternalApiService.scala │ ├── NodeBalancer.scala │ ├── NodeWatchService.scala │ └── PostTxQueue.scala └── project/ ├── Settings.scala.sample ├── build.properties └── plugins.sbt ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ *.class *.log # config application.conf application.properties # generated by scalajs lmchain.js dev.js prod.js client-fastopt-bundle.js client-fastopt-bundle.js.map # sway db data folder sway # workshheets *.worksheet.sc # gateway log unsent-deposits.json sent-deposits.logs last-block-read.json # npm node_modules # parcel .parcel-cache dist dist.zip .env # settings Settings.scala # mac .DS_Store # archive txs.archive txs1.archive # bulk-insert invalid-txs.csv ================================================ FILE: .jvmopts ================================================ -Xms1g -Xmx4g ================================================ FILE: .scalafix.conf ================================================ OrganizeImports { coalesceToWildcardImportThreshold = 10 # Int.MaxValue expandRelative = false groupExplicitlyImportedImplicitsSeparately = false groupedImports = Merge groups = ["re:javax?\\.", "scala.", "cats.", "*", "hedgehog."] importSelectorsOrder = Ascii importsOrder = SymbolsFirst removeUnused = false targetDialect = Scala3 } ================================================ FILE: .scalafmt.conf ================================================ version = 3.7.3 runner.dialect = scala3 align.preset = more rewrite.trailingCommas.style = always newlines.beforeTypeBounds = fold rewrite.scala3 { convertToNewSyntax = true removeOptionalBraces = yes } fileOverride { "glob:**/*.sbt" { runner.dialect = sbt1 } "glob:**/**/*.sbt" { runner.dialect = sbt1 } "glob:**/**/**/*.sbt" { runner.dialect = sbt1 } } ================================================ FILE: README.md ================================================ # LeisureMetaverse Blockchain LeisureMetaverse 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. Scala 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. LeisureMetaverse 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. ## LM Scan ### Dev Mode #### Run Backend ```bash sbt ~lmscanBackend/reStart ``` #### Run ScalaJS ```bash sbt ~lmscanFrontend/fastLinkJS ``` #### Run Frontend ```bash cd modules/lmscan-frontend yarn start ``` ### Build Mode #### Assembly Backend ```bash sbt lmscanBackend/assembly ``` #### Build ScalaJS ```bash sbt lmscanFrontend/fullLinkJS ``` #### Build Frontend ```bash cd modules/lmscan-frontend yarn build ``` ================================================ FILE: build.sbt ================================================ val V = new { val Scala = "3.4.3" val ScalaGroup = "3.4" val catsEffect = "3.5.4" val tapir = "1.10.6" val sttp = "3.9.6" val circe = "0.15.0-M1" val refined = "0.11.1" val iron = "2.5.0" val scodecBits = "1.1.38" val shapeless = "3.4.1" val fs2 = "3.10.2" val typesafeConfig = "1.4.3" val pureconfig = "0.17.6" val bouncycastle = "1.70" val sway = "0.16.2" val lettuce = "6.3.2.RELEASE" val jasync = "2.2.4" val okhttp3LoggingInterceptor = "4.12.0" val web3J = "4.9.6" val awsSdk = "2.20.75" val scribe = "3.13.4" val hedgehog = "0.10.1" val organiseImports = "0.6.0" val munitCatsEffect = "2.0.0-RC1" val tyrian = "0.9.0" val scalaJavaTime = "2.3.0" val jsSha3 = "0.8.0" val elliptic = "6.5.4" val typesElliptic = "6.4.12" val pgEmbedded = "1.0.3" val quill = "4.8.0" val postgres = "42.7.3" val flywayCore = "9.22.3" val sqlite = "3.48.0.0" val doobieVersion = "1.0.0-RC5" val hikari = "6.2.1" } val Dependencies = new { lazy val node = Seq( libraryDependencies ++= Seq( "com.softwaremill.sttp.tapir" %% "tapir-armeria-server-cats" % V.tapir, "com.softwaremill.sttp.tapir" %% "tapir-json-circe" % V.tapir, "com.outr" %% "scribe-slf4j" % V.scribe, "com.typesafe" % "config" % V.typesafeConfig, ("io.swaydb" %% "swaydb" % V.sway).cross(CrossVersion.for3Use2_13), "io.lettuce" % "lettuce-core" % V.lettuce, ), excludeDependencies ++= Seq( "org.scala-lang.modules" % "scala-collection-compat_2.13", "org.scala-lang.modules" % "scala-java8-compat_2.13", ), ) lazy val jvmClient = Seq( libraryDependencies ++= Seq( "com.softwaremill.sttp.client3" %% "armeria-backend-cats" % V.sttp, "com.softwaremill.sttp.tapir" %% "tapir-sttp-client" % V.tapir, ), excludeDependencies ++= Seq( "org.scala-lang.modules" % "scala-collection-compat_2.13", "org.scala-lang.modules" % "scala-java8-compat_2.13", ), ) lazy val ethGateway = Seq( libraryDependencies ++= Seq( "com.softwaremill.sttp.tapir" %% "tapir-armeria-server-cats" % V.tapir, "com.softwaremill.sttp.tapir" %% "tapir-json-circe" % V.tapir, "com.softwaremill.sttp.client3" %% "armeria-backend-cats" % V.sttp, "com.softwaremill.sttp.tapir" %% "tapir-sttp-client" % V.tapir, "com.outr" %% "scribe-slf4j" % V.scribe, "com.github.pureconfig" %% "pureconfig-core" % V.pureconfig, "com.typesafe" % "config" % V.typesafeConfig, "org.web3j" % "core" % V.web3J, "org.web3j" % "contracts" % V.web3J, "com.squareup.okhttp3" % "logging-interceptor" % V.okhttp3LoggingInterceptor, "com.github.jasync-sql" % "jasync-mysql" % V.jasync, "software.amazon.awssdk" % "kms" % V.awsSdk, ), ) lazy val archive = Seq( libraryDependencies ++= Seq( "com.softwaremill.sttp.client3" %% "armeria-backend-cats" % V.sttp, "com.outr" %% "scribe-slf4j" % V.scribe, "com.typesafe" % "config" % V.typesafeConfig, ), ) lazy val api = Seq( libraryDependencies ++= Seq( "org.typelevel" %%% "shapeless3-deriving" % V.shapeless, "org.typelevel" %%% "cats-effect" % V.catsEffect, "com.softwaremill.sttp.tapir" %%% "tapir-json-circe" % V.tapir, "com.softwaremill.sttp.client3" %%% "core" % V.sttp, ), ) lazy val lib = Seq( libraryDependencies ++= Seq( "org.typelevel" %%% "cats-effect" % V.catsEffect, "io.circe" %%% "circe-generic" % V.circe, "io.circe" %%% "circe-parser" % V.circe, "io.circe" %%% "circe-refined" % V.circe, "eu.timepit" %%% "refined" % V.refined, "io.github.iltotore" %%% "iron" % V.iron, "io.github.iltotore" %%% "iron-circe" % V.iron, "org.scodec" %%% "scodec-bits" % V.scodecBits, "org.typelevel" %%% "shapeless3-typeable" % V.shapeless, "co.fs2" %%% "fs2-core" % V.fs2, ), ) lazy val libJVM = Seq( libraryDependencies ++= Seq( "org.bouncycastle" % "bcprov-jdk15on" % V.bouncycastle, "com.outr" %% "scribe-slf4j" % V.scribe, ), ) lazy val libJS = Seq( libraryDependencies ++= Seq( "com.outr" %%% "scribe" % V.scribe, ), Compile / npmDependencies ++= Seq( "js-sha3" -> V.jsSha3, "elliptic" -> V.elliptic, "@types/elliptic" -> V.typesElliptic, ), ) lazy val catsEffectTests = Def.settings( libraryDependencies ++= Seq( "org.typelevel" %% "munit-cats-effect" % V.munitCatsEffect % Test, ), Test / fork := true, ) lazy val tests = Def.settings( libraryDependencies ++= Seq( "qa.hedgehog" %%% "hedgehog-munit" % V.hedgehog % Test, "com.opentable.components" % "otj-pg-embedded" % V.pgEmbedded % Test, "org.flywaydb" % "flyway-core" % V.flywayCore, ), Test / fork := true, ) lazy val lmscanCommon = Seq( libraryDependencies ++= Seq( "org.typelevel" %%% "cats-effect" % V.catsEffect, "io.circe" %%% "circe-generic" % V.circe, "io.circe" %%% "circe-parser" % V.circe, "io.circe" %%% "circe-refined" % V.circe, "eu.timepit" %%% "refined" % V.refined, "com.softwaremill.sttp.tapir" %%% "tapir-core" % V.tapir, "com.softwaremill.sttp.tapir" %%% "tapir-json-circe" % V.tapir, "org.scodec" %%% "scodec-bits" % V.scodecBits, "co.fs2" %%% "fs2-core" % V.fs2, "io.getquill" %% "quill-jasync-postgres" % V.quill, "com.outr" %% "scribe" % V.scribe, ), ) lazy val lmscanFrontend = Seq( libraryDependencies ++= Seq( "io.indigoengine" %%% "tyrian-io" % V.tyrian, "qa.hedgehog" %%% "hedgehog-munit" % V.hedgehog % Test, ), ) lazy val lmscanBackend = Seq( libraryDependencies ++= Seq( "com.softwaremill.sttp.tapir" %% "tapir-armeria-server-cats" % V.tapir, "com.softwaremill.sttp.client3" %% "armeria-backend-cats" % V.sttp, "com.typesafe" % "config" % V.typesafeConfig, "org.postgresql" % "postgresql" % V.postgres, "com.opentable.components" % "otj-pg-embedded" % V.pgEmbedded, ), ) lazy val lmscanAgent = Seq( libraryDependencies ++= Seq( "com.outr" %% "scribe-file" % V.scribe, "org.slf4j" % "slf4j-nop" % "2.0.17", "org.xerial" % "sqlite-jdbc" % V.sqlite, "com.softwaremill.sttp.client3" %% "fs2" % V.sttp, "com.github.pureconfig" %% "pureconfig-core" % V.pureconfig, "org.tpolecat" %% "doobie-core" % V.doobieVersion, "org.tpolecat" %% "doobie-postgres" % V.doobieVersion, "org.tpolecat" %% "doobie-specs2" % V.doobieVersion, "org.tpolecat" %% "doobie-hikari" % V.doobieVersion, "com.zaxxer" % "HikariCP" % V.hikari, ), ) lazy val nodeProxy = Seq( libraryDependencies ++= Seq( "com.outr" %% "scribe-slf4j" % V.scribe, "com.typesafe" % "config" % V.typesafeConfig, "com.softwaremill.sttp.tapir" %% "tapir-armeria-server-cats" % V.tapir, "com.softwaremill.sttp.client3" %% "armeria-backend-cats" % V.sttp, "io.circe" %% "circe-generic" % V.circe, "io.circe" %% "circe-parser" % V.circe, "io.circe" %% "circe-refined" % V.circe, "com.squareup.okhttp3" % "logging-interceptor" % V.okhttp3LoggingInterceptor, "org.typelevel" %% "cats-effect" % V.catsEffect, "co.fs2" %%% "fs2-core" % V.fs2, ), ) } ThisBuild / organization := "org.leisuremeta" ThisBuild / version := "0.0.1-SNAPSHOT" ThisBuild / scalaVersion := V.Scala ThisBuild / scalafixDependencies += "com.github.liancheng" %% "organize-imports" % V.organiseImports ThisBuild / semanticdbEnabled := true lazy val root = (project in file(".")) .aggregate( node, api.jvm, api.js, lib.jvm, lib.js, archive, bulkInsert, ethGatewaySetup, ethGatewayCommon, ethGatewayDeposit, ethGatewayWithdraw, lmscanCommon.jvm, lmscanCommon.js, lmscanFrontend, lmscanBackend, lmscanAgent, nodeProxy, ) lazy val node = (project in file("modules/node")) .settings(Dependencies.node) .settings(Dependencies.tests) .settings(Dependencies.catsEffectTests) .settings( name := "leisuremeta-chain-node", assemblyMergeStrategy := { case x if x `contains` "reactor/core" => MergeStrategy.first case x if x `contains` "io.netty.versions.properties" => MergeStrategy.first case PathList("META-INF", "native", "lib", xs @ _*) => MergeStrategy.first case x if x `contains` "module-info.class" => MergeStrategy.discard case x => val oldStrategy = (ThisBuild / assemblyMergeStrategy).value oldStrategy(x) }, ) .dependsOn(api.jvm) lazy val ethGatewayCommon = (project in file("modules/eth-gateway-common")) .settings(Dependencies.ethGateway) .settings(Dependencies.catsEffectTests) .settings( name := "leisuremeta-chain-eth-gateway-common", Compile / compile / wartremoverErrors ++= Warts .allBut(Wart.Any, Wart.AsInstanceOf, Wart.Nothing, Wart.Recursion), ) .dependsOn(api.jvm) lazy val ethGatewaySetup = (project in file("modules/eth-gateway-setup")) .settings(Dependencies.ethGateway) .settings(Dependencies.catsEffectTests) .settings( name := "leisuremeta-chain-eth-gateway-setup", ) .dependsOn(ethGatewayCommon) lazy val ethGatewayDeposit = (project in file("modules/eth-gateway-deposit")) .settings(Dependencies.ethGateway) .settings(Dependencies.tests) .settings( name := "leisuremeta-chain-eth-gateway-deposit", assemblyMergeStrategy := { case x if x `contains` "okio.kotlin_module" => MergeStrategy.first case x if x `contains` "io.netty.versions.properties" => MergeStrategy.first case x if x `contains` "native/lib/libnetty-unix-common.a" => MergeStrategy.first case x if x `contains` "module-info.class" => MergeStrategy.discard case x => val oldStrategy = (ThisBuild / assemblyMergeStrategy).value oldStrategy(x) }, Compile / compile / wartremoverErrors ++= Warts .allBut( Wart.Any, Wart.AsInstanceOf, Wart.Nothing, Wart.Recursion, Wart.SeqApply, ), ) .dependsOn(ethGatewayCommon) lazy val ethGatewayWithdraw = (project in file("modules/eth-gateway-withdraw")) .settings(Dependencies.ethGateway) .settings(Dependencies.tests) .settings( name := "leisuremeta-chain-eth-gateway-withdraw", assemblyMergeStrategy := { case x if x `contains` "okio.kotlin_module" => MergeStrategy.first case x if x `contains` "io.netty.versions.properties" => MergeStrategy.first case x if x `contains` "module-info.class" => MergeStrategy.discard case x => val oldStrategy = (ThisBuild / assemblyMergeStrategy).value oldStrategy(x) }, Compile / compile / wartremoverErrors ++= Warts .allBut( Wart.Any, Wart.AsInstanceOf, Wart.Nothing, Wart.Recursion, Wart.TripleQuestionMark, ), ) .dependsOn(ethGatewayCommon) lazy val archive = (project in file("modules/archive")) .settings(Dependencies.archive) .settings(Dependencies.tests) .settings( name := "leisuremeta-chain-archive", Compile / run / fork := true, assemblyMergeStrategy := { case x if x `contains` "io.netty.versions.properties" => MergeStrategy.first case PathList("META-INF", "native", "lib", xs @ _*) => MergeStrategy.first case x if x `contains` "module-info.class" => MergeStrategy.discard case x => val oldStrategy = (ThisBuild / assemblyMergeStrategy).value oldStrategy(x) }, ) .dependsOn(api.jvm) lazy val bulkInsert = (project in file("modules/bulk-insert")) .dependsOn(node) .settings( name := "leisuremeta-chain-bulk-insert", Compile / run / fork := true, excludeDependencies ++= Seq( "org.scala-lang.modules" % "scala-collection-compat_2.13", "org.scala-lang.modules" % "scala-java8-compat_2.13", ), assemblyMergeStrategy := { case PathList("reactor", "core", "scheduler", xs @ _*) => MergeStrategy.preferProject case x if x `contains` "libnetty-unix-common.a" => MergeStrategy.first case x if x `contains` "io.netty.versions.properties" => MergeStrategy.first case PathList("META-INF", "native", "lib", xs @ _*) => MergeStrategy.first case PathList("reactor", "core", "scheduler", xs @ _*) => MergeStrategy.first case x if x `contains` "module-info.class" => MergeStrategy.discard case x => val oldStrategy = (ThisBuild / assemblyMergeStrategy).value oldStrategy(x) }, ) lazy val jvmClient = (project in file("modules/jvm-client")) .settings(Dependencies.jvmClient) .dependsOn(node) .settings( name := "leisuremeta-chain-jvm-client", Compile / run / fork := true, ) lazy val api = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Pure) .in(file("modules/api")) .settings(Dependencies.api) .settings( scalacOptions ++= Seq( "-Xmax-inlines:64", ), Compile / compile / wartremoverErrors ++= Warts.allBut(Wart.NoNeedImport), ) .dependsOn(lib) lazy val lib = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Full) .in(file("modules/lib")) .settings(Dependencies.lib) .settings(Dependencies.tests) .settings( scalacOptions ++= Seq( "-Wconf:msg=Alphanumeric method .* is not declared infix:s", ), Compile / compile / wartremoverErrors ++= Warts .allBut(Wart.SeqApply, Wart.SeqUpdated), ) .jvmSettings(Dependencies.libJVM) .jsSettings(Dependencies.libJS) .jsSettings( useYarn := true, Test / scalaJSLinkerConfig ~= { _.withModuleKind(ModuleKind.CommonJSModule) }, scalacOptions ++= Seq( "-scalajs", ), Test / fork := false, ) .jsConfigure { project => project .enablePlugins(ScalaJSBundlerPlugin) .enablePlugins(ScalablyTypedConverterPlugin) } lazy val lmscanCommon = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Full) .in(file("modules/lmscan-common")) .settings(Dependencies.lmscanCommon) .settings(Dependencies.tests) .jvmSettings( scalacOptions ++= Seq( "-Xmax-inlines:64", ), Test / fork := true, ) .jsSettings( useYarn := true, scalaJSLinkerConfig ~= { _.withModuleKind(ModuleKind.CommonJSModule) }, scalacOptions ++= Seq( "-scalajs", "-Xmax-inlines:64", ), externalNpm := { scala.sys.process.Process("yarn", baseDirectory.value).! baseDirectory.value }, Test / fork := false, // Compile / compile / wartremoverErrors ++= Warts.all, ) .jsConfigure { project => project .enablePlugins(ScalaJSBundlerPlugin) .enablePlugins(ScalablyTypedConverterExternalNpmPlugin) } lazy val lmscanFrontend = (project in file("modules/lmscan-frontend")) .enablePlugins(ScalaJSPlugin) .enablePlugins(ScalablyTypedConverterExternalNpmPlugin) .settings(Dependencies.lmscanFrontend) .settings( name := "leisuremeta-chain-lmscan-frontend", scalaJSLinkerConfig ~= { _.withModuleKind(ModuleKind.CommonJSModule) }, externalNpm := { scala.sys.process.Process("yarn", baseDirectory.value).! baseDirectory.value }, scalacOptions ++= Seq( "-scalajs", ), ) .dependsOn(lmscanCommon.js, api.js) lazy val lmscanBackend = (project in file("modules/lmscan-backend")) .enablePlugins(FlywayPlugin) .settings(Dependencies.lmscanBackend) .settings(Dependencies.tests) .settings( name := "leisuremeta-chain-lmscan-backend", assemblyMergeStrategy := { case PathList("scala", "tools", "asm", xs @ _*) => MergeStrategy.first case PathList("io", "getquill", xs @ _*) => MergeStrategy.first case x if x `contains` "io.netty.versions.properties" => MergeStrategy.first case x if x `contains` "scala-asm.properties" => MergeStrategy.first case x if x `contains` "compiler.properties" => MergeStrategy.first case x if x `contains` "native/lib/libnetty-unix-common.a" => MergeStrategy.first case x if x `contains` "module-info.class" => MergeStrategy.discard case x => val oldStrategy = (ThisBuild / assemblyMergeStrategy).value oldStrategy(x) }, ) .settings( flywayUrl := Settings.flywaySettings.url, flywayUser := Settings.flywaySettings.user, flywayPassword := Settings.flywaySettings.pwd, flywaySchemas := Settings.flywaySettings.schemas, flywayLocations ++= Settings.flywaySettings.locations, ) .dependsOn(lmscanCommon.jvm) lazy val lmscanAgent = (project in file("modules/lmscan-agent")) .settings(Dependencies.lmscanAgent) .settings(Dependencies.tests) .settings(Dependencies.catsEffectTests) .settings( Compile / run / fork := true, ) .settings( name := "leisuremeta-chain-lmscan-agent", assemblyMergeStrategy := { case PathList("scala", "tools", "asm", xs @ _*) => MergeStrategy.first case PathList("io", "getquill", xs @ _*) => MergeStrategy.first case x if x `contains` "libnetty-unix-common.a" => MergeStrategy.first case x if x `contains` "io.netty.versions.properties" => MergeStrategy.first case x if x `contains` "scala-asm.properties" => MergeStrategy.first case x if x `contains` "compiler.properties" => MergeStrategy.first case x if x `contains` "module-info.class" => MergeStrategy.discard case x => val oldStrategy = (ThisBuild / assemblyMergeStrategy).value oldStrategy(x) }, ) .dependsOn(api.jvm) .dependsOn(lmscanBackend) lazy val nodeProxy = (project in file("modules/node-proxy")) .settings(Dependencies.nodeProxy) .settings(Dependencies.tests) .settings( name := "leisuremeta-chain-node-proxy", assemblyMergeStrategy := { case x if x `contains` "okio.kotlin_module" => MergeStrategy.first case x if x `contains` "io.netty.versions.properties" => MergeStrategy.first case x if x `contains` "native/lib/libnetty-unix-common.a" => MergeStrategy.first case x if x `contains` "module-info.class" => MergeStrategy.discard case x => val oldStrategy = (ThisBuild / assemblyMergeStrategy).value oldStrategy(x) }, ) .dependsOn(api.jvm) ================================================ FILE: docs/LeisureMeta_Chain_API.md ================================================ # LeisureMeta Chain API ## API with Top Priority `GET` **/balance/{accountName}** 계정 잔고 조회 > `param` movable: 잔고의 이동 가능성 여부 > > * 'free': 유동 자산 > * 'locked': 예치 자산 * Response: Map[TokenDefinitionID, BalanceInfo] * Token Definition ID: 토큰 정의 ID (string) * BalanceInfo * Total Amount: 해당 토큰 총 금액/수량 (NFT의 경우 랜덤박스 갯수) * Map[TxHash, Tx]: 사용하지 않은 트랜잭션 해시 목록 `GET` **/nft-balance/{accountName}** 계정 NFT 잔고 조회 > `param` *(optional)* movable: 잔고의 이동 가능성 여부 > > * 'free': 유동 자산 > * 'locked': 예치 자산 > * 'all': 전체 자산 * Response: Map[TokenID, NftBalanceInfo] * NftBalanceInfo * TokenDefinitionId * TxHash * Tx `GET` **/activity/account/{account}** 계정 활동내역 조회 * Response: Seq[ActivityInfo] * ActivityInfo * timestamp * point * description * txHash `GET` **/activity/token/{tokenId}** 토큰이 받은 활동내역 조회 * Response: Seq[ActivityInfo] * ActivityInfo * timestamp * point * description * txHash `GET` **/reward/{accountName}** 보상 조회 > `param` *(optional)* timestamp: 기준 시점. 없으면 가장 최근 보상. (월요일 0시 ~ 일요일 23시59분 주기) > > `param` *(optional)* dao-account: 마스터 다오 계정. 없으면 `DAO-M` 사용 > > `param` *(optional)* reward-amount: 리워드 총량. 없으면 마스터 다오 계정의 현재 LM 밸런스. * Response: * account: 계정이름 * reward: 보상 * total: 총 보상량 * activity: 활동보상 * token: 토큰이 받은 사용자액션 보상 * rarity: 보유 토큰의 희귀도에 따른 보상 * bonus: 모더레이터인 경우 주어지는 추가 보상 총합 * point: 활동내역에 따르는 보상 포인트(1/1000 포인트 단위의 정수) * activity: 활동 내역. * like: 좋아요 * comment: 댓글 * share: 공유 * report: 신고 * token: 토큰이 받은 내역 * like: 좋아요 * comment: 댓글 * share: 공유 * report: 신고 * rarity: Map[String, Number] 희귀도에 따르는 포인트 * timestamp: 기준 시점 * totalNumberOfDao: 시스템에 개설된 다오 총 수 `GET` **/dao/{groupID}** 특정 그룹의 DAO 정보 조회 * Response: DaoInfo DAO 정보 * DaoInfo 현재까지 정해진 필드값들 * NumberOfModerator: 모더레이터 숫자 `GET` **/owners/{definitionID}** 특정 컬렉션 NFT들의 보유자 정보 조회 * Response: * Map[TokenID, AccountName] `GET` **/snapshot/ownership/** 받을 토큰 소유보상 점수 조회 > `param` *(optional)* from: 조회를 시작할 token id. 주어지지 않으면 "" > > `param` *(optional)* limit: 조회할 총 갯수. 디폴트값은 100 * Response: [TokenId, OwnershipSnapshot] * OwnershipSnapshot * account * timestamp 기준 시점 * point 포인트. 일반적으론 해당 NFT의 Rarity 점수 * definitionId 보상받을 토큰 종류. 일반적으론 LM * amount 보상량 `GET` **/rewarded/ownership/{tokenID}** 최근에 받은 토큰 소유보상 조회 * Response: OwnershipRewardLog * OwnershipRewardLog * OwnershipShapshot * ExecuteReward TxHash `GET` **/creator-dao/{daoID}** 특정 크리에이터 DAO 정보 조회 * Response: CreatorDaoInfo 크리에이터 DAO 정보 * CreatorDaoInfo * id: CreatorDaoId * name: Utf8 * description: Utf8 * founder: Account * coordinator: Account * moderators: Set[Account] `GET` **/creator-dao/{daoID}/member** 특정 크리에이터 DAO의 멤버 목록 조회 > `param` *(optional)* from(Account): 기준 계정 > > `param` *(optional)* limit: 갯수 제한. 기본값 100 > * Response: Seq[Account] 회원 목록 `POST` **/tx** 트랜잭션 제출 * 아래의 트랜잭션 목록 참조 * Array로 한 번에 여러개의 트랜잭션 제출 가능 ### Blockchain Explorer 지원용 API `GET` **/status** 블록체인 현재상태 조회 (최신 블록 hash, 블록 number 포함) `GET` **/block **블록 목록 조회 > `param` *(optional)* from: 찾기 시작할 블록 해시. 없으면 최신 블록 > > `param` *(optional)* limit: 가져올 블록 갯수. 디폴트 50. `GET` **/block/{blockHash}** 블록 상세정보 조회 (포함된 트랜잭션 해시 목록 포함) `GET` **/tx** 특정 블록에 포함된 트랜잭션 목록 조회 > `param` block: 찾을 블록 해시 `GET` **/tx/{transactionHash}** 트랜잭션 상세정보 조회 ### Response HTTP Status Codes * 요청했을 때 해당 내용이 없는 경우: 404 Not Found * 서명이 올바르지 않은 경우: 401 Unauthorized * 트랜잭션이 invalid한 경우: 400 Bad Request * 블록체인 노드 내부 오류: 500 Internal Server Error ## Transactions * 모든 트랜잭션 공통 필드 * "networkId": 다른 네트워크에 똑같은 트랜잭션을 보내는 것을 막기 위한 필드. * "createdAt": 트랜잭션 생성시각 * Format * 서명주체 * Fields: 트랜잭션을 제출할 때 포함시켜야 하는 필드 목록 * *(optional)* Computed Fields: 블록에 기록될 때 노드에 의해 덧붙여지는 필드들 ### Account * CreateAccount 계정 생성 * > 사용자 서명 * Fields * account: Account 계정 이름 * ethAddress: *(optional)* 이더리움 주소 * guardian: *(optional)* Account * 계정에 공개키를 추가할 수 있는 권한을 가진 계정 지정. 일반적으로는 `playnomm` * Example (private key `b229e76b742616db3ac2c5c2418f44063fcc5fcc52a08e05d4285bdb31acba06` 으로 서명한 예시) ```json [ { "sig" : { "sig" : { "v" : 28, "r" : "495c3bcc143eea328c11b7ec55069dd4fb16c26463999f9dbc085094c3b59423", "s" : "707a75e433abd208cfb76d4e0cdbc04b1ce2389e3a1f866348ef2e3ea5785e93" }, "account" : "alice" }, "value" : { "AccountTx" : { "CreateAccount" : { "networkId" : 1000, "createdAt" : "2020-05-22T09:00:00Z", "account" : "alice", "ethAddress" : null, "guardian" : null } } } } ] ``` ```json ["822380e575e482e829fc9f45ffd0f99f4f0987ccbec0c0a5de5fd640f42a9100"] ``` * CreateAccountWithExternalChainAddresses 외부 블록체인 주소를 가진 계정 생성 * > 사용자 서명 * Fields * account: Account 계정 이름 * externalChainAddresses: 외부 블록체인 주소. 현재는 `eth`, `sol` 두 가지 지원. * guardian: *(optional)* Account * 계정에 공개키를 추가할 수 있는 권한을 가진 계정 지정. 일반적으로는 `playnomm` * memo: *(optional)* 메모 * Example (private key `b229e76b742616db3ac2c5c2418f44063fcc5fcc52a08e05d4285bdb31acba06` 으로 서명한 예시) ```json [ { "sig" : { "sig" : { "v" : 28, "r" : "2f7a53986a387961047566ab8d31fcdbbe6cc96529cdbfccb68fb268700f2bdf", "s" : "56f993f2cca6a5f7410e04a5aa849c7698f1e0966d98b90638d7472cd9eb3210" }, "account" : "alice" }, "value" : { "AccountTx" : { "CreateAccountWithExternalChainAddresses" : { "networkId" : 2021, "createdAt" : "2023-01-11T19:01:30Z", "account" : "bob", "externalChainAddresses" : { "eth" : "99f681d29754aeee1426ef991b745a4f662e620c" }, "guardian" : "alice", "memo" : null } } } } ``` ```json ["d795dc9205ec5ecb3097fe0ca0326e6597c6ecae497a0876b8cc3737d823264a"] ``` * UpdateAccount 계정 생성 * > 사용자 서명 혹은 Guardian 서명 * Fields * account: Account 계정 이름 * ethAddress: *(optional)* 이더리움 주소 * guardian: *(optional)* Account * 계정에 공개키를 추가할 수 있는 권한을 가진 계정 지정. 일반적으로는 `playnomm` * Example ```json [ { "sig" : { "sig" : { "v" : 28, "r" : "22c14ac6fbdce52c256640f1e36851ef901ea1b5cfebc3a430283a89df99bc11", "s" : "3474ebcc861c2d31a60d363356c4c89c196d450432b33bedadfb94d66edf2ffd" }, "account" : "alice" }, "value" : { "AccountTx" : { "UpdateAccount" : { "networkId" : 1000, "createdAt" : "2020-05-22T09:00:00Z", "account" : "alice", "ethAddress" : "0xefD277f6da7ac53e709392044AE98220Df142753", "guardian" : null } } } } ] ``` ```json ["7730dadeff5be3bfd63fdec8853d6301a5ec0e3b8c815a4d7e0ba20e8c52517d"] ``` * UpdateAccountWithExternalChainAddresses 계정 생성 * > 사용자 서명 혹은 Guardian 서명 * Fields * account: Account 계정 이름 * externalChainAddresses: 외부 블록체인 주소. 현재는 `eth`, `sol` 두 가지 지원. * guardian: *(optional)* Account * 계정에 공개키를 추가할 수 있는 권한을 가진 계정 지정. 일반적으로는 `playnomm` * memo: *(optional)* 메모 * Example ```json [ { "sig" : { "sig" : { "v" : 27, "r" : "bf7dfb91669233e120a07707084f2b9879d9620cd297096f862d52d53fe9988d", "s" : "10d652ba45a81ad572aeae6c25ffd2e6339e8f049d379ae24d5a947f531d5d65" }, "account" : "alice" }, "value" : { "AccountTx" : { "UpdateAccountWithExternalChainAddresses" : { "networkId" : 2021, "createdAt" : "2023-01-11T19:01:40Z", "account" : "bob", "externalChainAddresses" : { "eth" : "99f681d29754aeee1426ef991b745a4f662e620c" }, "guardian" : "alice", "memo" : "bob updated" } } } } ] ``` ```json ["a0e28414a97b3fabb49cf3b77757219b8e1a7205ba3cc4f5e618019b36bc38c3"] ``` * AddPublicKeySummaries 계정에 사용할 공개키요약 추가 * > 사용자 서명 혹은 Guardian 서명 * Fields * account: Account 계정 이름 * summaries: Map[PublicKeySummary, String] * 추가할 공개키요약과 간단한 설명 * 만약 설명이 `"permanant"` 인 경우 해당 public key summary 는 유효기간 없이 무제한 사용 * Result * Removed: Map[PublicKeySummary, Descrption(string)] * Example ```json [ { "sig" : { "sig" : { "v" : 27, "r" : "816df20e4ff581fd2056689b48be73cca29e4f81977e5c42754e598757434c51", "s" : "4e43aef8d836e79380067365cd7a4a452df5f52b73ec78463bdc7cdea2e11ca0" }, "account" : "alice" }, "value" : { "AccountTx" : { "AddPublicKeySummaries" : { "networkId" : 1000, "createdAt" : "2020-05-22T09:00:00Z", "account" : "alice", "summaries" : { "5b6ed47b96cd913eb938b81ee3ea9e7dc9affbff" : "another key" } } } } } ] ``` ```json ["e996dcbabcf8a86208bcc8d683778f5d6b5d1b8ff950c9e60cc72b66fc619cca"] ``` * RemovePublicKeySummaries 계정에 사용할 공개키요약 삭제 * > 사용자 서명 혹은 Guardian 서명 * Fields * Account: AccountName (string) * Summaries: Set[PublicKeySummary] * RemoveAccount 계정 삭제 * > 사용자 서명 혹은 Guardian 서명 * Fields * Account: AccountName (string) ### Group * CreateGroup 그룹 생성 * > Coordinator 서명 * Fields * GroupID(string) * Name: GroupName(string) * Coordinator: AccountName(string) * 그룹 조정자. 그룹에 계정 추가, 삭제 및 그룹 해산 권한을 가짐 * Example ```json [ { "sig" : { "sig" : { "v" : 28, "r" : "aab6f7ccc108b8e75601c726d43270c1a60f38f830136dfe293a2633dc86a0dd", "s" : "3cc1b610df7a421f9ae560853d5f07005a20c6ad225a00861a76e5e91aa183c0" }, "account" : "alice" }, "value" : { "GroupTx" : { "CreateGroup" : { "networkId" : 1000, "createdAt" : "2022-06-08T09:00:00Z", "groupId" : "mint-group", "name" : "mint group", "coordinator" : "alice" } } } } ] ``` ```json ["adb9440aeef2de4697774657ebbcce9c1e5b01423e0a21da90da355458400c75"] ``` * DisbandGroup 그룹 해산 * > Coordinator 서명 * Fields * GroupID(string) * AddAccounts 그룹에 계정 추가 * > Coordinator 서명 * Fields * GroupID(string) * Accounts: Set[AccountName(string)] * Example ```json [ { "sig" : { "sig" : { "v" : 28, "r" : "2dd00a2ebf07ff2d09d6e9bcd889ddc775c17989827e3e19b5e8d1744c021466", "s" : "05bd60fef3d45463e22e5c157c814a7cbd1681410b67b0233c97ce7116d60729" }, "account" : "alice" }, "value" : { "GroupTx" : { "AddAccounts" : { "networkId" : 1000, "createdAt" : "2022-06-08T09:00:00Z", "groupId" : "mint-group", "accounts" : [ "alice", "bob" ] } } } } ] ``` ```json ["015a8cced717ca40a528d9518e8494961a4c4e7fde1422304b751814ed181e00"] ``` * RemoveAccounts 그룹에 계정 삭제 * > Coordinator 서명 * Fields * GroupID(string) * Accounts: Set[AccountName(string)] * ReplaceCoordinator 그룹 조정자 변경 * > Coordinator 서명 * Fields * GroupID(string) * NewCoordinator: AccountName(string) ### Token * DefineToken 토큰 정의. Fungible Token, NFT 공히 사용한다. (랜덤박스 포함) * > MinterGroup에 속한 Account의 서명 * Fields * definitionId: TokenDefinitionID(string) * name: String * *(optional)* Symbol(string) * *(optional)* MinterGroup: GroupID(string) 신규토큰발행 권한을 가진 그룹 * *(optional)* NftInfo * minter: AccountName(string) * rarity: Map[(Rarity(string), Weight] * *(optional)* DataUrl(string) * *(optional)* ContentHash: uint256 * Example ```json [ { "sig" : { "sig" : { "v" : 28, "r" : "ce2b48b7da96eef22a2b92170fb81865adb99cbcae99a2b81bb7ce9b4ba990b6", "s" : "35a708c9ffc1b7ef4e88389255f883c96e551a404afc4627e3f6ca32a617bae6" }, "account" : "alice" }, "value" : { "TokenTx" : { "DefineToken" : { "networkId" : 1000, "createdAt" : "2020-05-22T09:01:00Z", "definitionId" : "test-token", "name" : "test-token", "symbol" : "TT", "minterGroup" : "mint-group", "nftInfo" : { "Some" : { "value" : { "minter" : "alice", "rarity" : { "LGDY" : 8, "UNIQ" : 4, "EPIC" : 2, "RARE" : 1 }, "dataUrl" : "https://www.playnomm.com/data/test-token.json", "contentHash" : "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" } } } } } } } ] ``` ```json ["b0cfd8da5ef347762b60162c772148902b54abca4760fb53e3eb752f8b953664"] ``` * DefineTokenWithPrecision Precision이 있는 토큰 정의. Fungible Token, NFT 공히 사용한다. (랜덤박스 포함) * > MinterGroup에 속한 Account의 서명 * Fields * definitionId: TokenDefinitionID(string) * name: String * *(optional)* Symbol(string) * *(optional)* MinterGroup: GroupID(string) 신규토큰발행 권한을 가진 그룹 * *(optional)* NftInfo * minter: AccountName(string) * rarity: Map[(Rarity(string), Weight] * precision: int 소숫점 자릿수 * *(optional)* DataUrl(string) * *(optional)* ContentHash: uint256 * Example ```json [ { "sig" : { "sig" : { "v" : 27, "r" : "74a1fa40be985b0c9bcf92df0262317a336f585fa24e261780b2ab6ff89d3f6a", "s" : "4cea4a8ad18df36a2c140366f8afee36441757921aa351fedf0b53d82307e9c2" }, "account" : "alice" }, "value" : { "TokenTx" : { "DefineTokenWithPrecision" : { "networkId" : 2021, "createdAt" : "2023-01-11T19:01:00Z", "definitionId" : "nft-with-precision", "name" : "NFT with precision", "symbol" : "NFTWP", "minterGroup" : "mint-group", "nftInfo" : { "Some" : { "value" : { "minter" : "alice", "rarity" : { "LGDY" : 100, "UNIQ" : 66, "EPIC" : 33, "RARE" : 10 }, "precision" : 2, "dataUrl" : "https://www.playnomm.com/data/nft-with-precision.json", "contentHash" : "2475a387f22c248c5a3f09cea0ef624484431c1eaf8ffbbf98a4a27f43fabc84" } } } } } } } ] ``` ```json ["6d49236405972c01322db054338da2c7ab6fd9662d2a64c9bc1ab4026da9fb8f"] ``` * MintFungibleToken * > MinterGroup에 속한 Account의 서명 * Fields * TokenDefinitionID(string) * Outputs: Map[AccountName, Amount] * Example ```json [ { "sig" : { "sig" : { "v" : 28, "r" : "76fb1b3be81101638c9ce070628db035ad7d86d3363d664da0c5afe254494e90", "s" : "7ffb1c751fe4f5341c75341e4a51373139a7f730a56a08078ac89b6e1a77fc76" }, "account" : "alice" }, "value" : { "TokenTx" : { "MintFungibleToken" : { "networkId" : 1000, "createdAt" : "2020-05-22T09:01:00Z", "definitionId" : "test-token", "outputs" : { "alice" : 100 } } } } } ] ``` ```json ["a3f35adb3d5d08692a7350e61aaa28da992a4280ad8e558953898ef96a0051ca"] ``` * BurnFungibleToken * MinterGroup에 속한 Account의 서명 * Fields * definitionId: TokenDefinitionId * amount * Inputs: Set[Signed.TxHash] * Result * outputAmount * MintNFT * > MinterGroup에 속한 Account의 서명 * Fields * TokenDefinitionID(string) * TokenID(string) * Rarity(string) * DataUrl(string) * ContentHash: uint256 * Output: AccountName * Example ```json [ { "sig" : { "sig" : { "v" : 27, "r" : "0a914259cc0e8513512ea6356fc3056efe104e84756cf23a6c1c1aff7a580613", "s" : "71a15b331b9e7337a018b442ee978a15f0d86e71ca53d2f54a9a8ccb92646cf9" }, "account" : "alice" }, "value" : { "TokenTx" : { "MintNFT" : { "networkId" : 1000, "createdAt" : "2022-06-08T09:00:00Z", "tokenDefinitionId" : "test-token", "tokenId" : "2022061710000513118", "rarity" : "EPIC", "dataUrl" : "https://d3j8b1jkcxmuqq.cloudfront.net/temp/collections/TEST_NOMM4/NFT_ITEM/F7A92FB1-B29F-4E6F-BEF1-47C6A1376D68.jpg", "contentHash" : "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", "output" : "alice" } } } } ] ``` ```json ["6040003b0020245ce82f352bed95dee2636442efee4e5a15ee3911c67910b657"] ``` * MintNFTWithMemo * > MinterGroup에 속한 Account의 서명 * Fields * TokenDefinitionID(string) * TokenID(string) * Rarity(string) * DataUrl(string) * ContentHash: uint256 * Output: AccountName * Example ```json [ { "sig" : { "sig" : { "v" : 28, "r" : "d1c7f699ff24b4767e3728f79b13d3d930fa1be02cb511481010fbbaecf538c0", "s" : "298829e3f5b03d4b3f87766b655eb3632099f6ea737e5e0d02da6ba03fcd72dd" }, "account" : "alice" }, "value" : { "TokenTx" : { "MintNFTWithMemo" : { "networkId" : 2021, "createdAt" : "2023-01-11T19:05:00Z", "tokenDefinitionId" : "nft-with-precision", "tokenId" : "2022061710000513118", "rarity" : "EPIC", "dataUrl" : "https://d3j8b1jkcxmuqq.cloudfront.net/temp/collections/TEST_NOMM4/NFT_ITEM/F7A92FB1-B29F-4E6F-BEF1-47C6A1376D68.jpg", "contentHash" : "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", "output" : "alice", "memo" : "Test Minting NFT #2022061710000513118" } } } } ] ``` ```json ["018edc66aa45e303a2621e5a981c2a2ed5f262802498888814a1844c04b12bd3"] ``` * BurnNFT * > 토큰 소유자 서명 * Fields * TokenDefinitionID(string) * Input: SignedTxHash * UpdateNFT * > MinterGroup 에 속한 account 서명 * Fields * TokenDefinitionID(string) * Example ```json [ { "sig": { "sig": { "v": 28, "r": "1ec82ef3e977dd8e6857e6d77b7955e57bc8d7081730198372f4740c588f0c80", "s": "65031c7011d8aceae4bfbd90049b2bb4c458050988368b3ea3017fb7402c0c03" }, "account": "alice" }, "value": { "TokenTx": { "UpdateNFT": { "networkId": 2021, "createdAt": "2023-01-11T19:06:00Z", "tokenDefinitionId": "nft-with-precision", "tokenId": "2022061710000513118", "rarity": "EPIC", "dataUrl": "https://d3j8b1jkcxmuqq.cloudfront.net/temp/collections/TEST_NOMM4/NFT_ITEM/F7A92FB1-B29F-4E6F-BEF1-47C6A1376D68.jpg", "contentHash": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", "output": "alice", "memo": "Test Updating NFT #2022061710000513118" } } } ] ``` ```json ["e4d85bd90857a9be1e363a10c6543de0a7826966378e2bdb0195572a87e7c1be"] ``` * TransferFungibleToken * > 토큰 보유자 서명 * Fields * TokenDefinitionID(string) * Inputs: Set[SignedTxHash]: UTXO Hash, 모든 토큰 종류는 동일해야 함 * Outputs: Map[AccountName, Amount] * *(optional)* Memo(string) * Example ```json [ { "sig" : { "sig" : { "v" : 28, "r" : "09a5f46d29bd8598f04cb6db32627aadd562e30e181135c2898594080db6aa79", "s" : "340abd1b6618d3bbf4b586294a4f902942f597672330563a43591a14be0a6504" }, "account" : "alice" }, "value" : { "TokenTx" : { "TransferFungibleToken" : { "networkId" : 1000, "createdAt" : "2022-06-09T09:00:00Z", "tokenDefinitionId" : "test-token", "inputs" : [ "a3f35adb3d5d08692a7350e61aaa28da992a4280ad8e558953898ef96a0051ca" ], "outputs" : { "bob" : 10, "alice" : 90 }, "memo" : "transfer from alice to bob" } } } } ] ``` ```json ["cb3848af6eb3c006c8aa663711d5fcfa2d6b1ccdcaf9837e273a96cc5386785e"] ``` * TransferNFT * > 토큰 보유자 서명 * Fields * TokenDefinitionID(string) * TokenID(string) * Input: SignedTxHash * Output: AccountName * *(optional)* Memo(string) * Example ```json [ { "sig" : { "sig" : { "v" : 27, "r" : "c443ed5eda3d484bcda7bf77f030d3f6c20e4130d9bc4e03ca75df3074b40239", "s" : "2e7a19f1baee2099ccbef500e7ceb03c5053957a55085ef52b21c022c43242d9" }, "account" : "alice" }, "value" : { "TokenTx" : { "TransferNFT" : { "networkId" : 1000, "createdAt" : "2022-06-09T09:00:00Z", "definitionId" : "test-token", "tokenId" : "2022061710000513118", "input" : "6040003b0020245ce82f352bed95dee2636442efee4e5a15ee3911c67910b657", "output" : "bob", "memo" : null } } } } ] ``` ```json ["1e46633eb70ec8ea484aeb0ef2e7916021b4fcc591712c4ce0514c63c897c6c9"] ``` * EntrustFungibleToken 토큰 위임 * > 토큰 보유자 서명 * Fields * definitionId: TokenDefinitionId 맡길 토큰 종류 * amount: 맡길 토큰 수량 * inputs: Set[SignedTxHash] 입력에 사용할 트랜잭션 해시값. * to: Account 위임할 계정. 일반적으로 playnomm * Results * remainder: Amount 자신에게 돌아오는 수량 * Example ```json [ { "sig" : { "sig" : { "v" : 27, "r" : "8d438670820bb788f0ef7106aa55c5fa2fa9c898eaded4d92f29d3c21a99c127", "s" : "1545783ca442a5ae2fdd347c79286a1c62256cd91ac76cb392f28dc190ac9c8a" }, "account" : "alice" }, "value" : { "TokenTx" : { "EntrustFungibleToken" : { "networkId" : 1000, "createdAt" : "2022-06-09T09:00:00Z", "definitionId" : "test-token", "amount" : 1000, "inputs" : [ "a3f35adb3d5d08692a7350e61aaa28da992a4280ad8e558953898ef96a0051ca" ], "to" : "alice" } } } } ] ``` ```json ["45df6a88e74ea44f2d759251fed5a3c319e7cf9c37fafa7471418fec7b26acce"] ``` * EntrustNFT NFT 위임 * > NFT 보유자 서명 * Fields * definitionId(string) * tokenId(string) * input: SignedTxHash * to: Account 위임할 계정. 일반적으로 playnomm * Example ```json [ { "sig" : { "sig" : { "v" : 27, "r" : "05705f380f7a7fbad853094f69ff1527703476be30d2ac19f90a24a7900100c0", "s" : "37fac4695829b188ebe3d8238259a212ba52588c4593a51ef81631ab9ab90581" }, "account" : "alice" }, "value" : { "TokenTx" : { "EntrustNFT" : { "networkId" : 1000, "createdAt" : "2020-06-09T09:00:00Z", "definitionId" : "test-token", "tokenId" : "2022061710000513118", "input" : "6040003b0020245ce82f352bed95dee2636442efee4e5a15ee3911c67910b657", "to" : "alice" } } } } ] ``` ```json ["10cb0802f3dfc85abb502bad260120a424fc583016db84d384904c1c0a580955"] ``` * DisposeEntrustedFungibleToken 위임된 토큰 처분 * 위임받은 계정(일반적으로 playnomm) 서명 * Fields * definitionID(string) * inputs: Set[SignedTxHash]: EntrustFungibleToken 트랜잭션의 UTXO Hash * outputs: Map[AccountName, Amount] * 토큰을 받아갈 계정과 받아갈 양. 비어 있으면 전체를 원주인에게 반환한다. * Example ```json [ { "sig" : { "sig" : { "v" : 28, "r" : "fb6c99c0e26da04e8dc0855ea629708a17a8deabfabb5a488ba9faa001c4a31f", "s" : "7de70d3fd15176451e46856af2dbedf05e58d7cfc0bfb0e0fac1b6d06550f5d3" }, "account" : "alice" }, "value" : { "TokenTx" : { "DisposeEntrustedFungibleToken" : { "networkId" : 1000, "createdAt" : "2020-06-10T09:00:00Z", "definitionId" : "test-token", "inputs" : [ "45df6a88e74ea44f2d759251fed5a3c319e7cf9c37fafa7471418fec7b26acce" ], "outputs" : { "bob" : 1000 } } } } } ] ``` ```json ["377fef6a1d85707bb7d84c9b3f5f2a2e409ce57084fbb15a6b200a1237d04119"] ``` * DisposeEntrustedNFT 위임된 NFT 처분 * 위임받은 계정(일반적으로 playnomm) 서명 * Fields * definitionID(string) * tokenID(string) * input: SignedTxHash * output: Option[AccountName] * NFT를 받아갈 계정. 없으면 원주인에게로 반환한다. * Example ```json [ { "sig" : { "sig" : { "v" : 28, "r" : "a03080b98925010e241783482e83a5fdfc25343406564a4e3fc4e6b2535657d3", "s" : "1de0ede5ebeba4aea455094ac1b58fc24ad943f0a5422a93f60a4f2b8b59b982" }, "account" : "alice" }, "value" : { "TokenTx" : { "DisposeEntrustedNFT" : { "networkId" : 1000, "createdAt" : "2020-06-10T09:00:00Z", "definitionId" : "test-token", "tokenId" : "2022061710000513118", "input" : "10cb0802f3dfc85abb502bad260120a424fc583016db84d384904c1c0a580955", "output" : "bob" } } } } ] ``` ```json ["83c783f31b95cc4a713a921ec1df0725c6675b999ba6285a70c1f777615e4281"] ``` * CreateSnapshots * > MinterGroup에 속한 Account의 서명 * Fields * definitionID(string) * tokenID(string) * definitionIds: Set[TokenDefinitionId] * *(optional)* Memo(string) * Example ```json [ { "sig" : { "sig" : { "v" : 27, "r" : "2a771418871b3fcfa43a0b00821fce6d9ecec40a1cf2c5ebff4489377c7d0f01", "s" : "640b2af02ee4a713d22d2e16e0acd2c61ee1195aa16254cc6481a926d772d866" }, "account" : "alice" }, "value" : { "TokenTx" : { "CreateSnapshots" : { "networkId" : 2021, "createdAt" : "2023-01-11T19:09:00Z", "definitionIds" : [ "LM", "nft-with-precision" ], "memo" : "Snapshot for NFT" } } } } ] ``` ```json ["e9fecfafd40e655ac761730bcbb9be524f39370ffa9a272f875275d6cdc50818"] ``` ### Reward * RegisterDao 신규 DAO 등록. Group은 미리 생성해 두어야 한다. * > Group Coordinator 서명. 일반적으로는 `playnomm` * Fields * GroupId(string) * DaoAccountName(string) * 다오 보상 충전용 계정. 여기에 들어온 금액을 매주 정해진 룰에 따라 보상한다. Unique account이어야 한다. * Moderators: Set[Account] * 최초 모더레이터 목록 * Example ```json [ { "sig" : { "sig" : { "v" : 27, "r" : "d4b2d1cfe009e0e5b6dea67779fd898a7f1718e7b1869b5b36b6daacc68e88f6", "s" : "42d8c69e964109ceab5996abdbc59d53661904e6b56337599e9c5beebe665d51" }, "account" : "alice" }, "value" : { "RewardTx" : { "RegisterDao" : { "networkId" : 1000, "createdAt" : "2020-06-09T09:00:00Z", "groupId" : "sample-dao-group-id", "daoAccountName" : "sample-dao-group-account", "moderators" : [ "alice" ] } } } } ] ``` ```json ["dabd1e1603805080722c6397568e6fc4ef384736a2bf95bc52e0f53acd43bea3"] ``` * UpdateDao DAO 정보 업데이트. 그룹 조정자가 업데이트 권한을 갖는다. * > Group Coordinator 서명. 일반적으로는 `playnomm` * Fields * GroupId(string) * Moderators: Set[Account] * 모더레이터 목록 * RecordActivity 활동정보 추가. 그룹 조정자가 업데이트 권한을 갖는다. * > Group Coordinator 서명. 일반적으로는 `playnomm` * Fields * timestamp: 기준시점 * userActivity: Map[AccountName, Seq[DaoActivity]] 사용자활동 요약 정보 * DaoActivity 활동정보 * point 총 점수 * description 어떤 활동으로 받은 점수인지 간략한 표시 * tokenReceived: Map[TokenId, Seq[DaoActivity]] 토큰이 받은 사용자활동 요약정보 * DaoActivity 활동정보 * point 총 점수 * description 어떤 활동으로 받은 점수인지 간략한 표시 * Example ```json [ { "sig" : { "sig" : { "v" : 27, "r" : "95aff6586d03fa7c66165d9bb49f2a2fd54650f2776c728401c664622d5e2d4c", "s" : "2cff82c55822d3266add84ea5853dbc86cf47f24e5787080b76e58681477ba09" }, "account" : "alice" }, "value" : { "RewardTx" : { "RecordActivity" : { "networkId" : 2021, "createdAt" : "2023-01-10T18:01:00Z", "timestamp" : "2023-01-09T09:00:00Z", "userActivity" : { "bob" : [ { "point" : 3, "description" : "like" } ], "carol" : [ { "point" : 3, "description" : "like" } ] }, "tokenReceived" : { "text-20230109-0000" : [ { "point" : 2, "description" : "like" } ], "text-20230109-0001" : [ { "point" : 2, "description" : "like" } ], "text-20230109-0002" : [ { "point" : 2, "description" : "like" } ] } } } } } ] ``` ```json ["f08043c06fa17ffaf5c86121db683f5aa879bbf0194de3cac703b0572feaa4cd"] ``` * OfferReward 보상 제공. TransferFungibleToken과 같은 형태로 보상을 실행한다 * > 보상을 보낼 계정 * Fields * TokenDefinitionID(string) * Inputs: Set[SignedTxHash]: UTXO Hash, 모든 토큰 종류는 동일해야 함 * Outputs: Map[AccountName, Amount] * *(optional)* Memo(string) * Example ```json ``` ```json ``` * BuildSnapshot: 보상을 위한 스냅샷 생성. 사용자가 한 활동, 토큰이 받은 활동, 토큰 소유보상의 세 가지 스냅샷을 동시에 만든다 * > 보상 실행 주체. 일반적으로 Playnomm * Fields * timestamp: 보상 기준 시점. 이 시점 일주일 전부터 현재 시점까지의 자료를 모아 스냅샷을 생성한다. * accountAmount: 계정활동 총 보상량 * tokenAmount: 토큰이 받을 총 보상량 * ownershipAmount: 토큰 보유에 따르는 총 보상량 * Example ```json [ { "sig" : { "sig" : { "v" : 28, "r" : "004b940e651bb950350157116fbfedf5ec98eed68068cea2b666a9e2b52b9588", "s" : "17eb8460877a7d212fac4a59caf7abf1cb96c145f5cae41a8ffce55df226f003" }, "account" : "alice" }, "value" : { "RewardTx" : { "BuildSnapshot" : { "networkId" : 2021, "createdAt" : "2023-01-11T18:01:00Z", "timestamp" : "2023-01-09T09:00:00Z", "accountAmount" : 0, "tokenAmount" : 0, "ownershipAmount" : 100000000000000000000000 } } } } ] ``` ```json ["da140a6816e9437c0583b34f64636ba9b3fca02721f2ff90b03460c061067cfa"] ``` * ExecuteOwnershipReward: 스냅샷의 자료를 기반으로 토큰 소유 보상 실행. * 보상 실행 주체. 일반적으로 Playnomm * Fields * definitionId 보상에 지급할 토큰 정의 ID. 일반적으로 LM. * inputs: Set[TxHash] 보상에 사용할 UTXO * targets: Set[TokenId] 보상할 개별 NFT 토큰 ID * Results * outputs: Map[Account, Amount] 각 계정별 보상결과 * Example ```json [ { "sig" : { "sig" : { "v" : 27, "r" : "2289a570405738a66d75c1eeae451f899cbcc3bd7fd98b4b4d5aaf807c965211", "s" : "0364409abf9829ae5ca38b9c31ee0bcc5ce4dabcff3a5d0be180dd925ec51096" }, "account" : "alice" }, "value" : { "RewardTx" : { "ExecuteOwnershipReward" : { "networkId" : 2021, "createdAt" : "2023-01-11T18:01:00Z", "inputs" : [ "270650f92f584d9dbbffb99f3a915dc908fbea28bc3dbf34b8cdbe49c4070611" ], "targets" : [ "1234567890", "1234567891" ] } } } } ] ``` ```json ["c7824fd901b71918f10663a2990988b3a933353aebc5d1b80f39d78ce43be1ca"] ``` ### AgendaTx * SuggestSimpleAgenda 투표 의제 제안. * > 투표 의제를 제안할 계정. 일반적으로 playNomm * Fields * title(string) * votingToken: TokenDefinitionId(string) 일반적으로 LM * voteStart: Instant * voteEnd: Instant * voteOption: Map[String, String] * Example ```json [ { "sig" : { "sig" : { "v" : 27, "r" : "dc6e9660b33fdc71b14675e7a7a888fe32e4b3bb6264a3a4e90f572518e53aa8", "s" : "069a1c0c2c602f2342384d545aab17e5dd2629efc9f8605dead14df599b5fc96" }, "account" : "alice" }, "value" : { "AgendaTx" : { "SuggestSimpleAgenda" : { "networkId" : 2021, "createdAt" : "2023-01-11T18:01:00Z", "title" : "Let the world know about LeisureMeta!", "votingToken" : "LM", "voteStart" : "2023-01-11T18:01:00Z", "voteEnd" : "2023-01-12T18:01:00Z", "voteOptions" : { "1" : "Yes", "2" : "No" } } } } } ] ``` ```json ["2475a387f22c248c5a3f09cea0ef624484431c1eaf8ffbbf98a4a27f43fabc84"] ``` * VoteSimpleAgenda 투표. * > 투표하는 사용자계정 * Fields * agendaTxHash: 투표할 SuggestSimpleAgenda 트랜잭션의 tx hash * selectedOption: 투표내용 * Example ```json [ { "sig" : { "sig" : { "v" : 28, "r" : "89a108a5a933a8d04486384dc90521d0ca5faba1d3a09524068c22936aa2b5ea", "s" : "2347e77fa7d1a4f6d10712bb7c5cfb1746f0aef65825dbf03f201fe5e594ee2f" }, "account" : "alice" }, "value" : { "AgendaTx" : { "VoteSimpleAgenda" : { "networkId" : 2021, "createdAt" : "2023-01-11T19:01:00Z", "agendaTxHash" : "2475a387f22c248c5a3f09cea0ef624484431c1eaf8ffbbf98a4a27f43fabc84", "selectedOption" : "1" } } } } ] ``` ```json ["07dd86c19884881e1ef037eac4553b735545c03612c5fe368a07189464ad154b"] ``` ## Other API | Method | URL | Description | | ------ | --------------------------------- | -------------------------------- | | `GET` | **/account/{accountName}** | 계정정보 조회 | | `GET` | **/eth/{ethAddress}** | 이더리움 주소와 연동된 계정 조회 | | `GET` | **/dao** | DAO 목록 조회 | | `GET` | **/group/{groupID}** | 그룹 정보 조회 | | `GET` | **/offering/{offeringID}** | Offering 정보 조회 | | `GET` | **/status** | 블록체인 상태 조회 | | `GET` | **/token-def/{definitionID}** | 토큰 정의 정보 조회 | | `GET` | **/token/{tokenID}** | 토큰 정보 조회 | | `GET` | **/token-hist/{txHash}** | 토큰 과거 정보 조회 | | `GET` | **/snapshot/account/{account}** | 보상받을 활동 조회 | | `GET` | **/snapshot/token/{tokenID}** | 보상받을 토큰 점수 조회 | | `GET` | **/snapshot/ownership/{tokenID}** | 받을 토큰 소유보상 점수 조회 | | `GET` | **/rewarded/account/{account}** | 최근에 받은 활동보상 조회 | | `GET` | **/rewarded/token/{tokenID}** | 최근에 받은 토큰보상 조회 | | `GET` | **/rewarded/ownership/{tokenID}** | 최근에 받은 토큰 소유보상 조회 | | `GET` | **/snapshot-state/{definitionID}** | 토큰정의 스냅샷 상태 조회 | | `GET` | **/snapshot-balance/ {Account}/{TokenDefinitionID}/{SnapshotID}** | 토큰 스냅샷 잔고 조회 | | `GET` | **/nft-snapshot-balance/ {Account}/{TokenDefinitionID}/{SnapshotID}** | NFT 스냅샷 잔고 조회 | ## State Merkle Trie로 관리되는 블록체인 내부 상태들. 키가 사전식으로 정렬되어 있어서 순회 가능하고, StateRoot로 요약가능하다. ### Account * NameState: AccountName => AccountData * AccountData * *(optional)* ethAddress * *(optional)* guardian (account) * AccountKeyState: (AccountName, PublicKeySummary) => Desription * Description에는 추가된 시각이 포함되어 있어야 함 ### Group * GroupState: GroupID => GroupInfo * GroupInfo * Group Name * Coordinator * GroupAccountState: (GroupID, AccountName) => () ### Token * TokenDefinitionState: TokenDefinitionID(string)=> TokenDefinition * TokenDefinition * TokenDefinitionID(string) * Name(string) * *(optional)* Symbol(string) * *(optional)* AdminGroup: GroupId * TotalAmount * *(optional)* NftInfo * Minter: AccountName(string) * Rarity: Map[(Rarity(string), Weight)] * DataUrl(string) * ContentHash: uint256 * NftState: TokenID => NftState * NftState * TokenID * TokenDefinitionID * Rarity * Weight * CurrentOwner: Account * RarityState: (TokenDefinitionID, Rarity, TokenID) => () * FungibleBalanceState: (AccountName, TokenDefinitionID, TransactionHash) => () * NftBalanceState: (AccountName, TokenID, TransactionHash) => () * EntrustFungibleBalanceState: (AccountName, AccountName, TokenDefinitionId, TransactionHash) => () * EntrustNftBalanceState: (AccountName, AccountName, TokenId, TransactionHash) => () ### Reward * DaoState: GroupID => DaoInfo * DaoInfo * Moderators: Set[AccountName] * AccountActivityState: (Account, Instant) => Seq[ActivityLog] * ActivityLog * account 포인트를 획득한 계정 * point 총 점수 * description 묘사 * txHash 근거가 되는 RecordActivity 트랜잭션 해시값 * TokenReceivedState: (TokenId, Instant) => Seq[ActivityLog] * AccountSnapshotState: (Account) => ActivitySnapshot * ActivitySnapshot * account * from: Instant * to: Instant * point 총 포인트 * definitionId 보상받을 토큰 종류. 일반적으론 LM * amount 보상량 * backlog: Set[TxHash] 해당 카운트의 근거 RecordActivity의 집합 * TokenSnapshotState: (TokenId) => ActivitySnapshot * OwnershipSnapshotState: (TokenId) => OwnershipSnapshot * OwnershipSnapshot * account * timestamp 기준 시점 * point 포인트. 일반적으론 해당 NFT의 Rarity 점수 * definitionId 보상받을 토큰 종류. 일반적으론 LM * amount 보상량 * AccountRewardedState: (Account) => ActivityRewardLog * ActivityRewardLog * ActivitySnapshot * ExecuteReward TxHash * TokenRewardedState: (TokenId) => ActivityRewardLog * OwnershipRewardedState: (TokenId) => OwnershipRewardLog * OwnershipRewardLog * OwnershipShapshot * ExecuteReward TxHash ================================================ FILE: docs/api_with_example.md ================================================ # LeisureMeta Chain API with Example `POST` **/txhash** 트랜잭션 해시값 계산 아직 해시값 계산 모듈을 제공하지 않으므로, 여기에 트랜잭션을 보내면 해시값을 계산해준다. 나온 해시값에 서명해서 정식으로 트랜잭션을 집어넣으면 된다. ```json [ { "AccountTx" : { "CreateAccount" : { "networkId" : 1000, "createdAt" : "2020-05-22T09:00:00Z", "account" : "alice", "guardian" : null } } } ] ``` ```json ["396fb3ef2ecdb800126027a802e26eb2e7e1d47fee28f24287fb836cdafc6f1e"] ``` `POST`**/tx** 트랜잭션 제출 (Private Key `b229e76b742616db3ac2c5c2418f44063fcc5fcc52a08e05d4285bdb31acba06`으로 서명한 예시) ```json [ { "sig" : { "sig" : { "v" : 27, "r" : "c0cf8bb197d5f0a562fd76200f09480f676f31970e982f65bc1efd707504ef73", "s" : "7ad50c3987ce4a9007d093d25caaf701436824dafc6290d8e477b8f1c8b6771d" }, "account" : "alice" }, "value" : { "AccountTx" : { "CreateAccount" : { "networkId" : 1000, "createdAt" : "2020-05-22T09:00:00Z", "account" : "alice", "guardian" : null } } } } ] ``` ```json ["396fb3ef2ecdb800126027a802e26eb2e7e1d47fee28f24287fb836cdafc6f1e"] ``` `GET` **/status** 노드 현재상태조회 트랜잭션 하나가 들어가서 블록이 하나 생성되었으므로 block number 값이 1이 된다. (추후 트랜잭션이 없는 빈 블록 하나를 더 찍어서 거래완결을 표시할 예정) ```json { "networkId": 1000, "genesisHash": "50f1634b0534d9eaff9bb4084b38839f710b5822a599a10c3b106a19a4315127", "bestHash": "a9735ba3420e7d9be5f26b28b035f3141d4586e1015c237a67aa46c90a65b8ca", "number": 1 } ``` `GET` **/block**/a9735ba3420e7d9be5f26b28b035f3141d4586e1015c237a67aa46c90a65b8ca 블록 정보 조회 best hash값을 넣어서 최신 블록의 정보를 조회한다 ```json { "header": { "number": 1, "parentHash": "50f1634b0534d9eaff9bb4084b38839f710b5822a599a10c3b106a19a4315127", "stateRoot": { "account": { "namesRoot": "7a3a362149605d574b2eccdf85a0bfe7ca579fd2cfa0a2c19a2b601731d5ddbd", "keyRoot": null } }, "transactionsRoot": "962dfb46a6439d48efd72e1a21356911f1f5882843c76a3c2b2a2709d44b25eb", "timestamp": "2022-05-29T18:13:54.425Z" }, "transactionHashes": [ "396fb3ef2ecdb800126027a802e26eb2e7e1d47fee28f24287fb836cdafc6f1e" ], "votes": [ { "v": 28, "r": "f6d37c7994cb9f5f84b2a100c2346d6a0aec7e48e14872096cd32a90dc3c43ec", "s": "52ad670b041581c3d1d246fb09df36b8a1201db76f4cb2113e0759e16541be20" } ] } ``` `GET` **/tx**/396fb3ef2ecdb800126027a802e26eb2e7e1d47fee28f24287fb836cdafc6f1e 트랜잭션 정보 조회 블록에 기록된 트랜잭션 해시값 하나에 대한 정보를 취득한다 ```json { "signedTx": { "sig": { "sig": { "v": 27, "r": "c0cf8bb197d5f0a562fd76200f09480f676f31970e982f65bc1efd707504ef73", "s": "7ad50c3987ce4a9007d093d25caaf701436824dafc6290d8e477b8f1c8b6771d" }, "account": "alice" }, "value": { "AccountTx": { "CreateAccount": { "networkId": 1000, "createdAt": "2020-05-22T09:00:00Z", "account": "alice", "guardian": null } } } }, "result": null } ``` `GET` **/account**/alice 계정정보 조회 ```json { "guardian": null, "publicKeySummaries": { "99f681d29754aeee1426ef991b745a4f662e620c": { "description": "Automatically added in account creation", "addedAt": "2020-05-22T09:00:00Z" } } } ``` ================================================ FILE: docs/creator-dao-documentation.md ================================================ # Creator Dao ## DAO 정보 * id(Utf8): DAO ID * name(Utf8): 이름 * description(Utf8): 설명 ## DAO 참여자 * Founder 창립자 * 크리에이터 본인 * DAO에 관한 모든 권한 * Coordinator 시스템 관리자 * playNomm 계정으로 세팅 * DAO에 관한 모든 권한 * Moderator 일반 관리자 * 해산 제외한 나머지 관리권한 * Member 참여자 * 표결 참여 * Applicant * 가입 신청한 사람 * 그 외 * 가입신청 ## 액션 * Coordinator만 가능 * ReplaceCoordinator * Founder, Coordinator만 가능 * DAO 개설 * 관리자 임명, 해임, DAO 해산 * Moderator 이상 가능 * DAO 정보변경 * 회원 가입, 탈퇴, 안건 발의 * Member 이상 가능 * 표결 참여 * 그 외 * Dao 가입신청 ## 트랜잭션 ### CreateCreatorDao (개설) Founder나 Coordinator가 새로운 DAO를 만든다. ```json { "sig": { "sig": { "v": 27, "r": "62d7c7ddf8bea783b8ed59906b2f5db00b9e53031d6407933d7c4a80c7157f35", "s": "2d546c7d0f0fdf058e5bdf74b39cb2d3db34aa1dcdd6b2a76ea6504655b12b0f" } "account": "founder", }, "value": { "CreatorDaoTx": { "CreateCreatorDao": { "networkId": 102, "createdAt": "2024-03-15T09:28:41.339Z", "id": "dao_001", "name": "Art Creators DAO", "description": "A DAO for digital art creators", "founder": "creator001", "coordinator": "playnomm" } } } } ``` ### UpdateCreatorDao (정보변경) Moderator 이상 권한을 가진 사용자가 DAO 정보를 수정한다. ```json { "sig": { "sig": { "v": 27, "r": "72d7c7ddf8bea783b8ed59906b2f5db00b9e53031d6407933d7c4a80c7157f35", "s": "3d546c7d0f0fdf058e5bdf74b39cb2d3db34aa1dcdd6b2a76ea6504655b12b0f" }, "account": "moderator" }, "value": { "CreatorDaoTx": { "UpdateCreatorDao": { "networkId": 102, "createdAt": "2024-03-15T10:28:41.339Z", "id": "dao_001", "name": "Digital Art Creators DAO", "description": "A DAO for digital art creators and collectors" } } } } ``` ### DisbandCreatorDao (해산) Founder나 Coordinator만 DAO를 해산할 수 있다. ```json { "sig": { "sig": { "v": 27, "r": "82d7c7ddf8bea783b8ed59906b2f5db00b9e53031d6407933d7c4a80c7157f35", "s": "4d546c7d0f0fdf058e5bdf74b39cb2d3db34aa1dcdd6b2a76ea6504655b12b0f" }, "account": "founder" }, "value": { "CreatorDaoTx": { "DisbandCreatorDao": { "networkId": 102, "createdAt": "2024-03-15T11:28:41.339Z", "id": "dao_001" } } } } ``` ### ReplaceCoordinator (코디네이터 변경) 현재 Coordinator만 새로운 Coordinator를 지정할 수 있다. ```json { "sig": { "sig": { "v": 27, "r": "92d7c7ddf8bea783b8ed59906b2f5db00b9e53031d6407933d7c4a80c7157f35", "s": "5d546c7d0f0fdf058e5bdf74b39cb2d3db34aa1dcdd6b2a76ea6504655b12b0f" }, "account": "coordinator" }, "value": { "CreatorDaoTx": { "ReplaceCoordinator": { "networkId": 102, "createdAt": "2024-03-15T12:28:41.339Z", "id": "dao_001", "newCoordinator": "playnomm2" } } } } ``` ### AddMembers (참여자 추가) Moderator 이상 권한을 가진 사용자가 새로운 멤버를 추가할 수 있다. ```json { "sig": { "sig": { "v": 27, "r": "a2d7c7ddf8bea783b8ed59906b2f5db00b9e53031d6407933d7c4a80c7157f35", "s": "6d546c7d0f0fdf058e5bdf74b39cb2d3db34aa1dcdd6b2a76ea6504655b12b0f" }, "account": "moderator" }, "value": { "CreatorDaoTx": { "AddMembers": { "networkId": 102, "createdAt": "2024-03-15T13:28:41.339Z", "id": "dao_001", "members": ["user001", "user002", "user003"] } } } } ``` ### RemoveMembers (참여자 제외) Moderator 이상 권한을 가진 사용자가 멤버를 제외할 수 있다. ```json { "sig": { "sig": { "v": 27, "r": "b2d7c7ddf8bea783b8ed59906b2f5db00b9e53031d6407933d7c4a80c7157f35", "s": "7d546c7d0f0fdf058e5bdf74b39cb2d3db34aa1dcdd6b2a76ea6504655b12b0f" }, "account": "moderator", } }, "value": { "CreatorDaoTx": { "RemoveMembers": { "networkId": 102, "createdAt": "2024-03-15T14:28:41.339Z", "id": "dao_001", "members": ["user003"] } } } } ``` ### PromoteModerators (관리자 임명) Founder나 Coordinator가 일반 멤버를 Moderator로 승급시킬 수 있다. ```json { "sig": { "sig": { "v": 27, "r": "d2d7c7ddf8bea783b8ed59906b2f5db00b9e53031d6407933d7c4a80c7157f35", "s": "9d546c7d0f0fdf058e5bdf74b39cb2d3db34aa1dcdd6b2a76ea6504655b12b0f" }, "account": "founder" }, "value": { "CreatorDaoTx": { "PromoteModerators": { "networkId": 102, "createdAt": "2024-03-15T15:28:41.339Z", "id": "dao_001", "members": ["user001"] } } } } ``` ### DemoteModerators (관리자 해임) Founder나 Coordinator가 Moderator를 일반 멤버로 강등시킬 수 있다. ```json { "sig": { "sig": { "v": 27, "r": "d2d7c7ddf8bea783b8ed59906b2f5db00b9e53031d6407933d7c4a80c7157f35", "s": "9d546c7d0f0fdf058e5bdf74b39cb2d3db34aa1dcdd6b2a76ea6504655b12b0f" }, "account": "founder" }, "value": { "CreatorDaoTx": { "DemoteModerators": { "networkId": 102, "createdAt": "2024-03-15T16:28:41.339Z", "id": "dao_001", "members": ["user001"] } } } } ``` ## 공통 사항 - 모든 트랜잭션에는 networkId와 createdAt을 포함한다. - 서명은 NamedSignature 형식을 사용하고, 권한에 맞는 이름을 포함한다. - 멤버 관련 작업(추가/제거/승급/강등)은 배열로 여러 계정을 한번에 처리할 수 있다. ================================================ FILE: docs/dao-voting-system-design-english.md ================================================ # LeisureMeta DAO Voting System Design Document ## 1. Project Background and Objective LeisureMeta 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. LeisureMeta Chain inherits the advantages of existing DAO systems while providing the following innovative features: 1. High Scalability and Low Cost: - Addresses the high gas fees and low throughput issues faced by existing Ethereum-based DAOs. - Internal transactions on LeisureMeta Chain are gas-free, enabling frequent voting and decision-making in a cost-effective manner. 2. Integrated Snapshot Functionality: - While existing solutions like ERC20Snapshot supported snapshot-based voting, LeisureMeta Chain implements this at the native level. - Snapshots can be created and queried consistently for all tokens and NFTs on the chain, enabling voting with various assets without complex smart contracts. 3. Seamless Integration of Diverse Voting Methods: - Supports various voting methods such as one person one vote, token-weighted, and NFT-based voting within a single system. - Unlike existing solutions that required separate contracts or implementations for each method, LeisureMeta Chain allows easy implementation of all methods through a unified API. 4. Optimized for Creator Economy: - Enables closer relationships between creators and fans through governance utilizing NFTs and fan tokens. - While existing DAO systems mainly focused on finance or protocol governance, LeisureMeta is specialized in building a new economic ecosystem centered around creators. 5. Enhanced User Experience: - All functions including voting, token trading, and NFT minting occur on a single chain, greatly improving user experience. - Eliminates the complexity and high entry barriers of existing multi-chain solutions while utilizing the advantages of each function. 6. Flexible Scalability: - The modular structure of LeisureMeta Chain allows for easy addition of new voting methods or governance models in the future. - This will be a crucial advantage in the rapidly evolving Web3 ecosystem. This 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. ## 2. System Overview LeisureMeta's DAO Voting system has the following characteristics: 1. A blockchain-based transparent and tamper-proof voting system 2. Voting based on token holdings at a specific point in time using snapshot functionality 3. Support for three voting methods: One person one vote, Fungible Token-based, and NFT-based 4. Flexible voting group settings using Token Definition ID 5. High accessibility due to internal transactions without Gas Fees ## 3. Technical Design ### 3.1 Snapshot Functionality LeisureMeta's snapshot feature records token holdings at a specific point in time, allowing for balance inquiries regardless of subsequent token movements. Related APIs: - `GET /snapshot-state/{definitionID}`: Query token definition snapshot state - `GET /snapshot-balance/{Account}/{TokenDefinitionID}/{SnapshotID}`: Query token snapshot balance - `GET /nft-snapshot-balance/{Account}/{TokenDefinitionID}/{SnapshotID}`: Query NFT snapshot balance ### 3.2 DAO Voting System API #### 3.2.1 Create Vote Proposal ```json POST /tx { "VotingTx": { "CreateVoteProposal": { "networkId": 2021, "createdAt": "2023-06-21T18:01:00Z", "proposalId": "PROPOSAL-2023-001", "title": "Community Fund Usage Proposal", "description": "Fund allocation for Creator Support Program", "votingPower": { "LM": 12345 }, "voteStart": "2023-06-22T00:00:00Z", "voteEnd": "2023-06-29T23:59:59Z", "voteType": "TOKEN_WEIGHTED", "voteOptions": { "1": "Approve", "2": "Reject", "3": "Abstain" }, "quorum": 1000000, // Minimum participation (e.g., 1,000,000 LM) "passThreshold": 0.66 // Approval threshold (66%) } } } ``` Example of NFT-based voting: ```json POST /tx { "VotingTx": { "CreateVoteProposal": { "networkId": 2021, "createdAt": "2023-06-21T18:01:00Z", "proposalId": "PROPOSAL-2023-002", "title": "Approval for New NFT Collection Launch", "description": "Voting for approval of a new NFT collection proposed by the community", "votingPower": { "NFT-COLLECTION-001": 12347, "NFT-COLLECTION-002": 12348 }, "voteStart": "2023-06-22T00:00:00Z", "voteEnd": "2023-06-29T23:59:59Z", "voteType": "NFT_BASED", "voteOptions": { "1": "Approve", "2": "Reject" }, "quorum": 100, // Minimum participation (number of NFTs) "passThresholdNumer": 51, // Approval threshold numerator(51%) "passThresholdDemon": 100, // Approval threshold denominator(100%) } } } ``` #### 3.2.2 Cast Vote ```json POST /tx { "VotingTx": { "CastVote": { "networkId": 2021, "createdAt": "2023-06-23T10:30:00Z", "proposalId": "PROPOSAL-2023-001", "selectedOption": "1" } } } ``` #### 3.2.3 Tally Votes ```json POST /tx { "VotingTx": { "TallyVotes": { "networkId": 2021, "createdAt": "2023-06-30T00:01:00Z", "proposalId": "PROPOSAL-2023-001" } } } ``` #### 3.2.4 Query Vote Proposal ``` GET /vote-proposal/{proposalId} ``` Response: ```json { "proposalId": "PROPOSAL-2023-001", "title": "Community Fund Usage Proposal", "description": "Fund allocation for Creator Support Program", "votingTokens": ["LM"], "snapshotId": 12345, "voteStart": "2023-06-22T00:00:00Z", "voteEnd": "2023-06-29T23:59:59Z", "voteOptions": { "1": "Approve", "2": "Reject", "3": "Abstain" }, "quorum": 1000000, "passThreshold": 0.66, "status": "In Progress", "currentResults": { "1": 3500000, "2": 1200000, "3": 300000 }, "totalVotes": 5000000 } ``` #### 3.2.5 Query User Voting History ``` GET /vote-history/{account} ``` Response: ```json [ { "proposalId": "PROPOSAL-2023-001", "votedAt": "2023-06-23T10:30:00Z", "selectedOption": "1" }, // ... other voting history ] ``` ### 3.3 Voting Type Implementation Voting types are specified through the `voteType` field in `CreateVoteProposal`: 1. One person one vote (`ONE_PERSON_ONE_VOTE`): Implemented to allow only one vote per account 2. Fungible Token-based voting (`TOKEN_WEIGHTED`): Voting rights proportional to LM token holdings 3. NFT-based voting (`NFT_BASED`): Voting rights based on NFT holdings Implementation for each voting type: 1. One person one vote: - The `votingTokens` field is ignored. - Each account can cast one vote if it exists at the snapshot point. 2. Fungible Token-based voting (LM token): - "LM" is specified in the `votingTokens`. - Voting rights are granted based on LM token holdings at the snapshot point. 3. NFT-based voting: - The Token Definition ID of the NFT collection is specified in `votingTokens` (e.g., "NFT-COLLECTION-001"). - Voting rights are granted based on the holdings of the specified NFT collection at the snapshot point. - Each NFT has equal weight. ### 3.4 Utilizing Token Definition ID The `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. ### 3.5 Utilizing Snapshot ID When 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. ## 4. Security and Transparency - Transaction signatures: All transactions must be signed with the user's private key. - Blockchain records: All voting-related transactions are permanently recorded on the blockchain, ensuring transparency. - Snapshot-based voting: Voting rights are granted based on token holdings at a specific point in time, preventing vote manipulation. - API security: HTTPS is used to enhance API communication security, and appropriate authentication/authorization mechanisms are implemented. ## 5. Socioeconomic Impact LeisureMeta's DAO Voting system is expected to have the following socioeconomic impacts: 1. Democratization of the creator economy: Strengthening the relationship between creators and fans through direct participation in decision-making 2. Transparent fund management: Ensuring transparency in fund execution through a blockchain-based voting system 3. Community-driven growth: Encouraging active participation of community members through the DAO structure 4. New utilization of digital assets: Increasing the utility of digital assets through governance participation using NFTs and tokens 5. Decentralized decision-making: Providing opportunities for more stakeholders to participate in decision-making, moving away from centralized power structures ## 6. Implementation Roadmap 1. Implement and test snapshot functionality - Build snapshot ID generation and management system 2. Implement DAO Voting system API - Implement transaction processing for `CreateVoteProposal`, `CastVote`, `TallyVotes` - Implement logic for each voting type - Implement APIs for querying vote proposals and history 3. Implement blockchain state management - Design state structure for storing vote proposals, participation, and results 4. Develop client application - Implement user interface - Implement communication logic with API 5. Testing and security audit 6. Beta version release and feedback collection 7. Official version release ## 7. Legal Considerations This 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. Key considerations: 1. Data privacy: Protect user information and comply with relevant regulations such as GDPR 2. Securities law: Ensure token-based voting does not violate securities regulations 3. Anti-Money Laundering (AML): Consider introducing KYC procedures if necessary 4. Smart contract audit: Regularly conduct security audits to eliminate vulnerabilities ## 8. Conclusion LeisureMeta'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. ================================================ FILE: modules/api/src/main/scala/io/leisuremeta/chain/api/LeisureMetaChainApi.scala ================================================ package io.leisuremeta.chain package api import java.time.Instant import java.util.Locale import io.circe.KeyEncoder import io.circe.generic.auto.* import scodec.bits.ByteVector import sttp.model.StatusCode import sttp.tapir.* import sttp.tapir.CodecFormat.TextPlain import sttp.tapir.generic.auto.* import sttp.tapir.json.circe.* import lib.crypto.{Hash, Signature} import lib.datatype.{BigNat, UInt256, UInt256BigInt, UInt256Bytes, Utf8} import api.model.{ Account, AccountSignature, Block, GroupId, NodeStatus, Signed, Transaction, TransactionWithResult, } import api.model.account.EthAddress import api.model.api_model.{ AccountInfo, ActivityInfo, BalanceInfo, BlockInfo, CreatorDaoInfo, GroupInfo, NftBalanceInfo, RewardInfo, TxInfo, } import api.model.creator_dao.CreatorDaoId import api.model.token.{ NftState, SnapshotState, TokenDefinition, TokenDefinitionId, TokenId, } import api.model.reward.{ ActivitySnapshot, DaoInfo, OwnershipSnapshot, OwnershipRewardLog, } import api.model.voting.{ProposalId, Proposal} import api.model.token.SnapshotState.* import io.leisuremeta.chain.api.model.Signed.TxHash object LeisureMetaChainApi: given Schema[UInt256Bytes] = Schema.string given Schema[UInt256BigInt] = Schema(SchemaType.SInteger()) given Schema[BigNat] = Schema.schemaForBigInt.map[BigNat] { (bigint: BigInt) => BigNat.fromBigInt(bigint).toOption } { (bignat: BigNat) => bignat.toBigInt } given Schema[Utf8] = Schema.string given [K: KeyEncoder, V: Schema]: Schema[Map[K, V]] = Schema.schemaForMap[K, V](KeyEncoder[K].apply) given [A]: Schema[Hash.Value[A]] = Schema.string given Schema[Signature.Header] = Schema(SchemaType.SInteger()) given Schema[Transaction] = Schema.derived[Transaction] given hashValueCodec[A]: Codec[String, Hash.Value[A], TextPlain] = Codec.string.mapDecode { (s: String) => ByteVector .fromHexDescriptive(s) .left .map(new Exception(_)) .flatMap(UInt256.from) match case Left(e) => DecodeResult.Error(s, e) case Right(v) => DecodeResult.Value(Hash.Value(v)) }(_.toUInt256Bytes.toBytes.toHex) given bignatCodec: Codec[String, BigNat, TextPlain] = Codec.bigInt.mapDecode { (n: BigInt) => BigNat.fromBigInt(n) match case Left(e) => DecodeResult.Error(n.toString(10), new Exception(e)) case Right(v) => DecodeResult.Value(v) }(_.toBigInt) final case class ServerError(msg: String) sealed trait UserError: def msg: String final case class Unauthorized(msg: String) extends UserError final case class NotFound(msg: String) extends UserError final case class BadRequest(msg: String) extends UserError val baseEndpoint = endpoint.errorOut( oneOf[Either[ServerError, UserError]]( oneOfVariantFromMatchType( StatusCode.Unauthorized, jsonBody[Right[ServerError, Unauthorized]] .description("invalid signature"), ), oneOfVariantFromMatchType( StatusCode.NotFound, jsonBody[Right[ServerError, NotFound]].description("not found"), ), oneOfVariantFromMatchType( StatusCode.BadRequest, jsonBody[Right[ServerError, BadRequest]].description("bad request"), ), oneOfVariantFromMatchType( StatusCode.InternalServerError, jsonBody[Left[ServerError, UserError]].description( "internal server error", ), ), ), ) @SuppressWarnings(Array("org.wartremover.warts.Any")) val getTxSetEndpoint = baseEndpoint.get .in("tx" / query[Block.BlockHash]("block")) .out(jsonBody[Set[TxInfo]]) @SuppressWarnings(Array("org.wartremover.warts.Any")) val getTxEndpoint = baseEndpoint.get .in("tx" / path[Signed.TxHash]) .out(jsonBody[TransactionWithResult]) @SuppressWarnings(Array("org.wartremover.warts.Any")) val postTxEndpoint = baseEndpoint.post .in("tx") .in(jsonBody[Seq[Signed.Tx]]) .out(jsonBody[Seq[Hash.Value[TransactionWithResult]]]) @SuppressWarnings(Array("org.wartremover.warts.Any")) val postTxHashEndpoint = baseEndpoint.post .in("txhash") .in(jsonBody[Seq[Transaction]]) .out(jsonBody[Seq[Hash.Value[Transaction]]]) @SuppressWarnings(Array("org.wartremover.warts.Any")) val getStatusEndpoint = baseEndpoint.get.in("status").out(jsonBody[NodeStatus]) @SuppressWarnings(Array("org.wartremover.warts.Any")) val getAccountEndpoint = baseEndpoint.get .in("account" / path[Account]) .out(jsonBody[AccountInfo]) @SuppressWarnings(Array("org.wartremover.warts.Any")) val getEthEndpoint = baseEndpoint.get .in("eth" / path[EthAddress]) .out(jsonBody[Account]) @SuppressWarnings(Array("org.wartremover.warts.Any")) val getGroupEndpoint = baseEndpoint.get .in("group" / path[GroupId]) .out(jsonBody[GroupInfo]) @SuppressWarnings(Array("org.wartremover.warts.Any")) val getBlockListEndpoint = baseEndpoint.get .in( "block" / query[Option[Block.BlockHash]]("from") .and(query[Option[Int]]("limit")), ) .out(jsonBody[List[BlockInfo]]) @SuppressWarnings(Array("org.wartremover.warts.Any")) val getBlockEndpoint = baseEndpoint.get .in("block" / path[Block.BlockHash]) .out(jsonBody[Block]) @SuppressWarnings(Array("org.wartremover.warts.Any")) val getTokenDefinitionEndpoint = baseEndpoint.get .in("token-def" / path[TokenDefinitionId]) .out(jsonBody[TokenDefinition]) @SuppressWarnings(Array("org.wartremover.warts.Any")) val getBalanceEndpoint = baseEndpoint.get .in("balance" / path[Account].and(query[Movable]("movable"))) .out(jsonBody[Map[TokenDefinitionId, BalanceInfo]]) @SuppressWarnings(Array("org.wartremover.warts.Any")) val getNftBalanceEndpoint = baseEndpoint.get .in("nft-balance" / path[Account].and(query[Option[Movable]]("movable"))) .out(jsonBody[Map[TokenId, NftBalanceInfo]]) @SuppressWarnings(Array("org.wartremover.warts.Any")) val getTokenEndpoint = baseEndpoint.get .in("token" / path[TokenId]) .out(jsonBody[NftState]) @SuppressWarnings(Array("org.wartremover.warts.Any")) val getTokenHistoryEndpoint = baseEndpoint.get .in("token-hist" / path[Hash.Value[TransactionWithResult]]) .out(jsonBody[NftState]) @SuppressWarnings(Array("org.wartremover.warts.Any")) val getOwnersEndpoint = baseEndpoint.get .in("owners" / path[TokenDefinitionId]) .out(jsonBody[Map[TokenId, Account]]) @SuppressWarnings(Array("org.wartremover.warts.Any")) val getAccountActivityEndpoint = baseEndpoint.get .in("activity" / "account" / path[Account]) .out(jsonBody[Seq[ActivityInfo]]) @SuppressWarnings(Array("org.wartremover.warts.Any")) val getTokenActivityEndpoint = baseEndpoint.get .in("activity" / "token" / path[TokenId]) .out(jsonBody[Seq[ActivityInfo]]) @SuppressWarnings(Array("org.wartremover.warts.Any")) val getAccountSnapshotEndpoint = baseEndpoint.get .in("snapshot" / "account" / path[Account]) .out(jsonBody[ActivitySnapshot]) @SuppressWarnings(Array("org.wartremover.warts.Any")) val getTokenSnapshotEndpoint = baseEndpoint.get .in("snapshot" / "token" / path[TokenId]) .out(jsonBody[ActivitySnapshot]) @SuppressWarnings(Array("org.wartremover.warts.Any")) val getOwnershipSnapshotEndpoint = baseEndpoint.get .in("snapshot" / "ownership" / path[TokenId]) .out(jsonBody[OwnershipSnapshot]) @SuppressWarnings(Array("org.wartremover.warts.Any")) val getOwnershipSnapshotMapEndpoint = baseEndpoint.get .in { "snapshot" / "ownership" / query[Option[TokenId]]("from") .and(query[Option[Int]]("limit")) } .out(jsonBody[Map[TokenId, OwnershipSnapshot]]) @SuppressWarnings(Array("org.wartremover.warts.Any")) val getOwnershipRewardedEndpoint = baseEndpoint.get .in("rewarded" / "ownership" / path[TokenId]) .out(jsonBody[OwnershipRewardLog]) @SuppressWarnings(Array("org.wartremover.warts.Any")) val getRewardEndpoint = baseEndpoint.get .in { "reward" / path[Account] .and(query[Option[Instant]]("timestamp")) .and(query[Option[Account]]("dao-account")) .and(query[Option[BigNat]]("reward-amount")) } .out(jsonBody[RewardInfo]) @SuppressWarnings(Array("org.wartremover.warts.Any")) val getDaoEndpoint = baseEndpoint.get .in("dao" / path[GroupId]) .out(jsonBody[DaoInfo]) @SuppressWarnings(Array("org.wartremover.warts.Any")) val getSnapshotStateEndpoint = baseEndpoint.get .in("snapshot-state" / path[TokenDefinitionId]) .out(jsonBody[SnapshotState]) @SuppressWarnings(Array("org.wartremover.warts.Any")) val getFungibleSnapshotBalanceEndpoint = baseEndpoint.get .in: "snapshot-balance" / path[Account] / path[TokenDefinitionId] / path[SnapshotState.SnapshotId] .out(jsonBody[Map[Hash.Value[TransactionWithResult], BigNat]]) @SuppressWarnings(Array("org.wartremover.warts.Any")) val getNftSnapshotBalanceEndpoint = baseEndpoint.get .in: "nft-snapshot-balance" / path[Account] / path[TokenDefinitionId] / path[SnapshotState.SnapshotId] .out(jsonBody[Set[TokenId]]) @SuppressWarnings(Array("org.wartremover.warts.Any")) val getVoteProposalEndpoint = baseEndpoint.get .in("vote" / "proposal" / path[ProposalId]) .out(jsonBody[Proposal]) @SuppressWarnings(Array("org.wartremover.warts.Any")) val getAccountVotesEndpoint = baseEndpoint.get .in("vote" / "account" / path[ProposalId] / path[Account]) .out(jsonBody[(Utf8, BigNat)]) @SuppressWarnings(Array("org.wartremover.warts.Any")) val getVoteCountEndpoint = baseEndpoint.get .in("vote" / "count" / path[ProposalId]) .out(jsonBody[Map[Utf8, BigNat]]) @SuppressWarnings(Array("org.wartremover.warts.Any")) val getCreatorDaoInfoEndpoint = baseEndpoint.get .in("creator-dao" / path[CreatorDaoId]) .out(jsonBody[CreatorDaoInfo]) @SuppressWarnings(Array("org.wartremover.warts.Any")) val getCreatorDaoMemberEndpoint = baseEndpoint.get .in: "creator-dao" / path[CreatorDaoId] / "member" .and(query[Option[Account]]("from")) .and(query[Option[Int]]("limit")) .out(jsonBody[Seq[Account]]) enum Movable: case Free, Locked object Movable: @SuppressWarnings(Array("org.wartremover.warts.ToString")) given Codec[String, Movable, TextPlain] = Codec.string.mapDecode { (s: String) => s match case "free" => DecodeResult.Value(Movable.Free) case "locked" => DecodeResult.Value(Movable.Locked) case _ => DecodeResult.Error(s, new Exception(s"invalid movable: $s")) }(_.toString.toLowerCase(Locale.ENGLISH)) @SuppressWarnings(Array("org.wartremover.warts.ToString")) given Codec[String, Option[Movable], TextPlain] = Codec.string.mapDecode { (s: String) => s match case "free" => DecodeResult.Value(Some(Movable.Free)) case "locked" => DecodeResult.Value(Some(Movable.Locked)) case "all" => DecodeResult.Value(None) case _ => DecodeResult.Error(s, new Exception(s"invalid movable: $s")) }(_.fold("")(_.toString.toLowerCase(Locale.ENGLISH))) ================================================ FILE: modules/api/src/main/scala/io/leisuremeta/chain/api/model/Account.scala ================================================ package io.leisuremeta.chain package api.model import cats.Eq import io.circe.{Decoder, Encoder, KeyDecoder, KeyEncoder} import sttp.tapir.{Codec, DecodeResult, Schema} import sttp.tapir.CodecFormat.TextPlain import lib.codec.byte.{ByteDecoder, ByteEncoder} import lib.datatype.Utf8 opaque type Account = Utf8 object Account: def apply(value: Utf8): Account = value extension (a: Account) def utf8: Utf8 = a given Encoder[Account] = Encoder.encodeString.contramap(_.utf8.value) given Decoder[Account] = Decoder.decodeString.emap(Utf8.from(_).left.map(_.getMessage)).map(apply) given KeyDecoder[Account] = Utf8.utf8CirceKeyDecoder given KeyEncoder[Account] = Utf8.utf8CirceKeyEncoder given Codec[String, Account, TextPlain] = Codec.string.mapDecode{ (s: String) => Utf8.from(s) match case Left(e) => DecodeResult.Error(s, e) case Right(a) => DecodeResult.Value(Account(a)) }(_.utf8.value) given Schema[Account] = Schema.string given ByteDecoder[Account] = Utf8.utf8ByteDecoder given ByteEncoder[Account] = Utf8.utf8ByteEncoder given Eq[Account] = Eq.fromUniversalEquals ================================================ FILE: modules/api/src/main/scala/io/leisuremeta/chain/api/model/AccountData.scala ================================================ package io.leisuremeta.chain package api.model import java.time.Instant import account.{ExternalChain, ExternalChainAddress} import lib.datatype.Utf8 final case class AccountData( externalChainAddresses: Map[ExternalChain, ExternalChainAddress], guardian: Option[Account], lastChecked: Instant, memo: Option[Utf8], ) ================================================ FILE: modules/api/src/main/scala/io/leisuremeta/chain/api/model/AccountSignature.scala ================================================ package io.leisuremeta.chain package api.model import io.circe.{Decoder, Encoder} import io.circe.generic.semiauto.* import lib.crypto.Signature final case class AccountSignature( sig: Signature, account: Account, ) object AccountSignature: given Decoder[AccountSignature] = deriveDecoder given Encoder[AccountSignature] = deriveEncoder ================================================ FILE: modules/api/src/main/scala/io/leisuremeta/chain/api/model/Block.scala ================================================ package io.leisuremeta.chain package api.model import java.time.Instant import cats.kernel.Eq import scodec.bits.ByteVector import lib.codec.byte.{ByteDecoder, ByteEncoder} import lib.crypto.{Hash, Recover, Sign, Signature} import lib.datatype.BigNat import lib.merkle.MerkleTrieNode final case class Block( header: Block.Header, transactionHashes: Set[Signed.TxHash], votes: Set[Signature], ) object Block: type BlockHash = Hash.Value[Block] final case class Header( number: BigNat, parentHash: BlockHash, stateRoot: StateRoot, transactionsRoot: Option[MerkleTrieNode.MerkleRoot], timestamp: Instant, ) object Header: given eqHeader: Eq[Header] = Eq.fromUniversalEquals given headerHash: Hash[Header] = Hash.build given encoder: ByteEncoder[Header] with def encode(header: Header): ByteVector = ByteEncoder[BigNat].encode(header.number) ++ ByteEncoder[BlockHash].encode(header.parentHash) ++ ByteEncoder[StateRoot].encode(header.stateRoot) ++ ByteEncoder[Option[MerkleTrieNode.MerkleRoot]].encode(header.transactionsRoot) ++ ByteEncoder[Instant].encode(header.timestamp) given decoder: ByteDecoder[Header] = for number <- ByteDecoder[BigNat] parentHash <- ByteDecoder[BlockHash] stateRoot <- ByteDecoder[StateRoot] transactionsRoot <- ByteDecoder[Option[MerkleTrieNode.MerkleRoot]] timestamp <- ByteDecoder[Instant] yield Header(number, parentHash, stateRoot, transactionsRoot, timestamp) object ops: extension (blockHash: Hash.Value[Block]) def toHeaderHash: Hash.Value[Header] = Hash.Value[Header](blockHash.toUInt256Bytes) extension (headerHash: Hash.Value[Header]) def toBlockHash: Hash.Value[Block] = Hash.Value[Block](headerHash.toUInt256Bytes) given blockHash: Hash[Block] = Header.headerHash.contramap(_.header) given signBlock: Sign[Block.Header] = Sign.build given recoverBlockHeader: Recover[Block.Header] = Recover.build ================================================ FILE: modules/api/src/main/scala/io/leisuremeta/chain/api/model/GroupData.scala ================================================ package io.leisuremeta.chain package api.model import lib.datatype.Utf8 final case class GroupData( name: Utf8, coordinator: Account, ) ================================================ FILE: modules/api/src/main/scala/io/leisuremeta/chain/api/model/GroupId.scala ================================================ package io.leisuremeta.chain package api.model import io.circe.{Decoder, Encoder} import sttp.tapir.{Codec, DecodeResult, Schema} import sttp.tapir.CodecFormat.TextPlain import lib.codec.byte.{ByteDecoder, ByteEncoder} import lib.datatype.Utf8 opaque type GroupId = Utf8 object GroupId: def apply(utf8: Utf8): GroupId = utf8 extension (a: GroupId) def utf8: Utf8 = a given Encoder[GroupId] = Encoder.encodeString.contramap(_.utf8.value) given Decoder[GroupId] = Decoder.decodeString.emap(Utf8.from(_).left.map(_.getMessage)).map(apply) given Schema[GroupId] = Schema.string given ByteDecoder[GroupId] = Utf8.utf8ByteDecoder.map(GroupId(_)) given ByteEncoder[GroupId] = Utf8.utf8ByteEncoder.contramap(_.utf8) given Codec[String, GroupId, TextPlain] = Codec.string.mapDecode{ (s: String) => Utf8.from(s) match case Left(e) => DecodeResult.Error(s, e) case Right(a) => DecodeResult.Value(GroupId(a)) }(_.utf8.value) ================================================ FILE: modules/api/src/main/scala/io/leisuremeta/chain/api/model/NetworkId.scala ================================================ package io.leisuremeta.chain package api.model import io.circe.{Decoder, Encoder} import sttp.tapir.Schema import lib.codec.byte.{ByteDecoder, ByteEncoder} import lib.datatype.BigNat opaque type NetworkId = BigNat object NetworkId: def apply(value: BigNat): NetworkId = value given ByteDecoder[NetworkId] = BigNat.bignatByteDecoder given ByteEncoder[NetworkId] = BigNat.bignatByteEncoder given Decoder[NetworkId] = BigNat.bignatCirceDecoder given Encoder[NetworkId] = BigNat.bignatCirceEncoder given Schema[NetworkId] = Schema.schemaForBigInt.map[NetworkId] { (bigint: BigInt) => BigNat.fromBigInt(bigint).toOption.map(apply) }{(bignat: BigNat) => bignat.toBigInt} ================================================ FILE: modules/api/src/main/scala/io/leisuremeta/chain/api/model/NodeStatus.scala ================================================ package io.leisuremeta.chain package api.model import lib.datatype.BigNat final case class NodeStatus( networkId: NetworkId, genesisHash: Block.BlockHash, bestHash: Block.BlockHash, number: BigNat, ) ================================================ FILE: modules/api/src/main/scala/io/leisuremeta/chain/api/model/PublicKeySummary.scala ================================================ package io.leisuremeta.chain package api.model import java.time.Instant import cats.Eq import cats.syntax.eq.given import io.circe.{Decoder, Encoder, KeyDecoder, KeyEncoder} import scodec.bits.ByteVector import sttp.tapir.Schema import lib.codec.byte.{ByteDecoder, ByteEncoder} import lib.crypto.{Hash, PublicKey} import lib.datatype.Utf8 opaque type PublicKeySummary = ByteVector object PublicKeySummary: final case class Info( description: Utf8, addedAt: Instant, expiresAt: Option[Instant], ) def apply(bytes: ByteVector): Either[String, PublicKeySummary] = Either.cond( bytes.size === 20, bytes, "PublicKeySummary must be 20 bytes", ) def fromHex(hexString: String): Either[String, PublicKeySummary] = for bytes <- ByteVector.fromHexDescriptive(hexString) summary <- apply(bytes) yield summary def fromPublicKeyHash(hash: Hash.Value[PublicKey]): PublicKeySummary = hash.toUInt256Bytes.toBytes takeRight 20 extension (pks: PublicKeySummary) def toBytes: ByteVector = pks given Eq[PublicKeySummary] = Eq.fromUniversalEquals given Decoder[PublicKeySummary] = Decoder.decodeString.emap { (s: String) => val (f, b) = s `splitAt` 2 for _ <- Either.cond( f === "0x", (), s"PublicKeySummary string not starting 0x: $f", ) summary <- fromHex(b) yield summary } given Encoder[PublicKeySummary] = Encoder.encodeString.contramap(summary => s"0x${summary.toString}") given KeyDecoder[PublicKeySummary] = KeyDecoder.instance(fromHex(_).toOption) given KeyEncoder[PublicKeySummary] = KeyEncoder.encodeKeyString.contramap(_.toHex) given Schema[PublicKeySummary] = Schema.string given ByteDecoder[PublicKeySummary] = ByteDecoder.fromFixedSizeBytes(20)(identity) given ByteEncoder[PublicKeySummary] = (bytes: ByteVector) => bytes ================================================ FILE: modules/api/src/main/scala/io/leisuremeta/chain/api/model/Signed.scala ================================================ package io.leisuremeta.chain package api.model import scodec.bits.ByteVector import sttp.tapir.{Codec, DecodeResult} import sttp.tapir.CodecFormat.TextPlain import lib.crypto.Hash import lib.datatype.UInt256 import io.circe.{Decoder, Encoder} final case class Signed[A](sig: AccountSignature, value: A) object Signed: type Tx = Signed[Transaction] type TxHash = Hash.Value[Tx] object TxHash{ given txHashCodec: Codec[String, TxHash, TextPlain] = Codec.string.mapDecode{ (s: String) => ByteVector.fromHexDescriptive(s).left.map(new Exception(_)).flatMap(UInt256.from) match case Left(e) => DecodeResult.Error(s, e) case Right(v) => DecodeResult.Value(Hash.Value(v)) }(_.toUInt256Bytes.toBytes.toHex) } given signedHash[A: Hash]: Hash[Signed[A]] = Hash[A].contramap(_.value) given txhashDecoder: Decoder[TxHash] = Hash.Value.circeValueDecoder[Tx] given txhashEncoder: Encoder[TxHash] = Hash.Value.circeValueEncoder[Tx] import io.circe.generic.semiauto.* given signedDecoder[A: Decoder]: Decoder[Signed[A]] = deriveDecoder given signedEncoder[A: Encoder]: Encoder[Signed[A]] = deriveEncoder ================================================ FILE: modules/api/src/main/scala/io/leisuremeta/chain/api/model/StateRoot.scala ================================================ package io.leisuremeta.chain package api.model import cats.Eq import io.circe.{Decoder, Encoder} import sttp.tapir.* import lib.codec.byte.{ByteDecoder, ByteEncoder} import lib.crypto.Hash import lib.merkle.MerkleTrieNode.MerkleRoot opaque type StateRoot = Option[MerkleRoot] object StateRoot: def apply(main: Option[MerkleRoot]): StateRoot = main val empty: StateRoot = None extension (sr: StateRoot) def main: Option[MerkleRoot] = sr given byteDecoder: ByteDecoder[StateRoot] = ByteDecoder.optionByteDecoder[MerkleRoot] given byteEncoder: ByteEncoder[StateRoot] = ByteEncoder.optionByteEncoder[MerkleRoot] given circeDecoder: Decoder[StateRoot] = Decoder.decodeOption[MerkleRoot] given circeEncoder: Encoder[StateRoot] = Encoder.encodeOption[MerkleRoot] given tapirSchema: Schema[StateRoot] = Schema.string given eqStateRoot: Eq[StateRoot] = Eq.fromUniversalEquals ================================================ FILE: modules/api/src/main/scala/io/leisuremeta/chain/api/model/Transaction.scala ================================================ package io.leisuremeta.chain package api.model import java.time.Instant import io.circe.{Decoder, Encoder} import io.circe.generic.semiauto.* import scodec.bits.ByteVector import account.{EthAddress, ExternalChain, ExternalChainAddress} //import agenda.AgendaId import creator_dao.CreatorDaoId import reward.DaoActivity import voting.{ProposalId, VoteType} import lib.crypto.{Hash, Recover, Sign} import lib.codec.byte.{ByteDecoder, ByteEncoder} import lib.codec.byte.ByteEncoder.ops.* import lib.datatype.{BigNat, UInt256Bytes, Utf8} import token.{Rarity, NftInfo, NftInfoWithPrecision, TokenDefinitionId, TokenId} sealed trait TransactionResult object TransactionResult: given txResultByteEncoder: ByteEncoder[TransactionResult] = (txr: TransactionResult) => txr match case r: Transaction.AccountTx.AddPublicKeySummariesResult => ByteVector.fromByte(0) ++ r.toBytes case r: Transaction.TokenTx.BurnFungibleTokenResult => ByteVector.fromByte(1) ++ r.toBytes case r: Transaction.TokenTx.EntrustFungibleTokenResult => ByteVector.fromByte(2) ++ r.toBytes case r: Transaction.RewardTx.ExecuteRewardResult => ByteVector.fromByte(3) ++ r.toBytes case r: Transaction.RewardTx.ExecuteOwnershipRewardResult => ByteVector.fromByte(4) ++ r.toBytes case r: Transaction.AgendaTx.VoteSimpleAgendaResult => ByteVector.fromByte(5) ++ r.toBytes given txResultByteDecoder: ByteDecoder[TransactionResult] = ByteDecoder.byteDecoder.flatMap { case 0 => ByteDecoder[Transaction.AccountTx.AddPublicKeySummariesResult].widen case 1 => ByteDecoder[Transaction.TokenTx.BurnFungibleTokenResult].widen case 2 => ByteDecoder[Transaction.TokenTx.EntrustFungibleTokenResult].widen case 3 => ByteDecoder[Transaction.RewardTx.ExecuteRewardResult].widen case 4 => ByteDecoder[Transaction.RewardTx.ExecuteOwnershipRewardResult].widen case 5 => ByteDecoder[Transaction.AgendaTx.VoteSimpleAgendaResult].widen } given txResultCirceEncoder: Encoder[TransactionResult] = deriveEncoder[TransactionResult] given txResultCirceDecoder: Decoder[TransactionResult] = deriveDecoder[TransactionResult] sealed trait Transaction: def networkId: NetworkId def createdAt: Instant // def memo: Option[Utf8] object Transaction: sealed trait AccountTx extends Transaction: def account: Account object AccountTx: final case class CreateAccount( networkId: NetworkId, createdAt: Instant, account: Account, ethAddress: Option[EthAddress], guardian: Option[Account], // memo: Option[Utf8], ) extends AccountTx final case class CreateAccountWithExternalChainAddresses( networkId: NetworkId, createdAt: Instant, account: Account, externalChainAddresses: Map[ExternalChain, ExternalChainAddress], guardian: Option[Account], memo: Option[Utf8], ) extends AccountTx final case class UpdateAccount( networkId: NetworkId, createdAt: Instant, account: Account, ethAddress: Option[EthAddress], guardian: Option[Account], // memo: Option[Utf8], ) extends AccountTx final case class UpdateAccountWithExternalChainAddresses( networkId: NetworkId, createdAt: Instant, account: Account, externalChainAddresses: Map[ExternalChain, ExternalChainAddress], guardian: Option[Account], memo: Option[Utf8], ) extends AccountTx final case class AddPublicKeySummaries( networkId: NetworkId, createdAt: Instant, account: Account, summaries: Map[PublicKeySummary, Utf8], // memo: Option[Utf8], ) extends AccountTx final case class AddPublicKeySummariesResult( removed: Map[PublicKeySummary, Utf8], ) extends TransactionResult // final case class RemovePublicKeySummaries( // networkId: NetworkId, // createdAt: Instant, // account: Account, // summaries: Set[PublicKeySummary], // ) extends AccountTx // // final case class RemoveAccount( // networkId: NetworkId, // createdAt: Instant, // account: Account, // ) extends AccountTx given txByteDecoder: ByteDecoder[AccountTx] = ByteDecoder[BigNat].flatMap { bignat => bignat.toBigInt.toInt match case 0 => ByteDecoder[CreateAccount].widen case 1 => ByteDecoder[UpdateAccount].widen case 2 => ByteDecoder[AddPublicKeySummaries].widen // case 3 => ByteDecoder[RemovePublicKeySummaries].widen // case 4 => ByteDecoder[RemoveAccount].widen case 5 => ByteDecoder[CreateAccountWithExternalChainAddresses].widen case 6 => ByteDecoder[UpdateAccountWithExternalChainAddresses].widen } given txByteEncoder: ByteEncoder[AccountTx] = (atx: AccountTx) => atx match case tx: CreateAccount => build(0)(tx) case tx: UpdateAccount => build(1)(tx) case tx: AddPublicKeySummaries => build(2)(tx) // case tx: RemovePublicKeySummaries => build(3)(tx) // case tx: RemoveAccount => build(4)(tx) case tx: CreateAccountWithExternalChainAddresses => build(5)(tx) case tx: UpdateAccountWithExternalChainAddresses => build(6)(tx) given txCirceDecoder: Decoder[AccountTx] = deriveDecoder given txCirceEncoder: Encoder[AccountTx] = deriveEncoder end AccountTx sealed trait GroupTx extends Transaction object GroupTx: final case class CreateGroup( networkId: NetworkId, createdAt: Instant, groupId: GroupId, name: Utf8, coordinator: Account, // memo: Option[Utf8], ) extends GroupTx // final case class DisbandGroup( // networkId: NetworkId, // createdAt: Instant, // groupId: GroupId, // ) extends GroupTx final case class AddAccounts( networkId: NetworkId, createdAt: Instant, groupId: GroupId, accounts: Set[Account], // memo: Option[Utf8], ) extends GroupTx // final case class RemoveAccounts( // networkId: NetworkId, // createdAt: Instant, // groupId: GroupId, // accounts: Set[Account], // ) extends GroupTx // final case class ReplaceCoordinator( // networkId: NetworkId, // createdAt: Instant, // groupId: GroupId, // newCoordinator: Account, // ) extends GroupTx given txByteDecoder: ByteDecoder[GroupTx] = ByteDecoder[BigNat].flatMap { bignat => bignat.toBigInt.toInt match case 0 => ByteDecoder[CreateGroup].widen case 2 => ByteDecoder[AddAccounts].widen } given txByteEncoder: ByteEncoder[GroupTx] = (atx: GroupTx) => atx match case tx: CreateGroup => build(0)(tx) case tx: AddAccounts => build(2)(tx) given txCirceDecoder: Decoder[GroupTx] = deriveDecoder given txCirceEncoder: Encoder[GroupTx] = deriveEncoder end GroupTx sealed trait TokenTx extends Transaction object TokenTx: final case class DefineToken( networkId: NetworkId, createdAt: Instant, definitionId: TokenDefinitionId, name: Utf8, symbol: Option[Utf8], minterGroup: Option[GroupId], nftInfo: Option[NftInfo], // memo: Option[Utf8], ) extends TokenTx final case class DefineTokenWithPrecision( networkId: NetworkId, createdAt: Instant, definitionId: TokenDefinitionId, name: Utf8, symbol: Option[Utf8], minterGroup: Option[GroupId], nftInfo: Option[NftInfoWithPrecision], // memo: Option[Utf8], ) extends TokenTx final case class MintFungibleToken( networkId: NetworkId, createdAt: Instant, definitionId: TokenDefinitionId, outputs: Map[Account, BigNat], // memo: Option[Utf8], ) extends TokenTx with FungibleBalance final case class MintNFT( networkId: NetworkId, createdAt: Instant, tokenDefinitionId: TokenDefinitionId, tokenId: TokenId, rarity: Rarity, dataUrl: Utf8, contentHash: UInt256Bytes, output: Account, // memo: Option[Utf8], ) extends TokenTx with NftBalance final case class MintNFTWithMemo( networkId: NetworkId, createdAt: Instant, tokenDefinitionId: TokenDefinitionId, tokenId: TokenId, rarity: Rarity, dataUrl: Utf8, contentHash: UInt256Bytes, output: Account, memo: Option[Utf8], ) extends TokenTx with NftBalance final case class BurnFungibleToken( networkId: NetworkId, createdAt: Instant, definitionId: TokenDefinitionId, amount: BigNat, inputs: Set[Signed.TxHash], // memo: Option[Utf8], ) extends TokenTx with FungibleBalance final case class BurnFungibleTokenResult( outputAmount: BigNat, ) extends TransactionResult final case class BurnNFT( networkId: NetworkId, createdAt: Instant, definitionId: TokenDefinitionId, input: Signed.TxHash, // memo: Option[Utf8], ) extends TokenTx final case class UpdateNFT( networkId: NetworkId, createdAt: Instant, tokenDefinitionId: TokenDefinitionId, tokenId: TokenId, rarity: Rarity, dataUrl: Utf8, contentHash: UInt256Bytes, output: Account, memo: Option[Utf8], ) extends TokenTx final case class TransferFungibleToken( networkId: NetworkId, createdAt: Instant, tokenDefinitionId: TokenDefinitionId, inputs: Set[Signed.TxHash], outputs: Map[Account, BigNat], memo: Option[Utf8], ) extends TokenTx with FungibleBalance final case class TransferNFT( networkId: NetworkId, createdAt: Instant, definitionId: TokenDefinitionId, tokenId: TokenId, input: Signed.TxHash, output: Account, memo: Option[Utf8], ) extends TokenTx with NftBalance final case class EntrustFungibleToken( networkId: NetworkId, createdAt: Instant, definitionId: TokenDefinitionId, amount: BigNat, inputs: Set[Signed.TxHash], to: Account, // memo: Option[Utf8], ) extends TokenTx with FungibleBalance final case class EntrustFungibleTokenResult( remainder: BigNat, ) extends TransactionResult final case class EntrustNFT( networkId: NetworkId, createdAt: Instant, definitionId: TokenDefinitionId, tokenId: TokenId, input: Signed.TxHash, to: Account, // memo: Option[Utf8], ) extends TokenTx final case class DisposeEntrustedFungibleToken( networkId: NetworkId, createdAt: Instant, definitionId: TokenDefinitionId, inputs: Set[Signed.TxHash], outputs: Map[Account, BigNat], // memo: Option[Utf8], ) extends TokenTx with FungibleBalance final case class DisposeEntrustedNFT( networkId: NetworkId, createdAt: Instant, definitionId: TokenDefinitionId, tokenId: TokenId, input: Signed.TxHash, output: Option[Account], // memo: Option[Utf8], ) extends TokenTx with NftBalance final case class CreateSnapshots( networkId: NetworkId, createdAt: Instant, definitionIds: Set[TokenDefinitionId], memo: Option[Utf8], ) extends TokenTx given txByteDecoder: ByteDecoder[TokenTx] = ByteDecoder[BigNat].flatMap { bignat => bignat.toBigInt.toInt match case 0 => ByteDecoder[DefineToken].widen case 1 => ByteDecoder[MintFungibleToken].widen case 2 => ByteDecoder[MintNFT].widen case 4 => ByteDecoder[TransferFungibleToken].widen case 5 => ByteDecoder[TransferNFT].widen case 6 => ByteDecoder[BurnFungibleToken].widen case 7 => ByteDecoder[BurnNFT].widen case 8 => ByteDecoder[EntrustFungibleToken].widen case 9 => ByteDecoder[EntrustNFT].widen case 10 => ByteDecoder[DisposeEntrustedFungibleToken].widen case 11 => ByteDecoder[DisposeEntrustedNFT].widen case 12 => ByteDecoder[DefineTokenWithPrecision].widen case 13 => ByteDecoder[UpdateNFT].widen case 14 => ByteDecoder[MintNFTWithMemo].widen case 15 => ByteDecoder[CreateSnapshots].widen } given txByteEncoder: ByteEncoder[TokenTx] = (ttx: TokenTx) => ttx match case tx: DefineToken => build(0)(tx) case tx: MintFungibleToken => build(1)(tx) case tx: MintNFT => build(2)(tx) case tx: TransferFungibleToken => build(4)(tx) case tx: TransferNFT => build(5)(tx) case tx: BurnFungibleToken => build(6)(tx) case tx: BurnNFT => build(7)(tx) case tx: EntrustFungibleToken => build(8)(tx) case tx: EntrustNFT => build(9)(tx) case tx: DisposeEntrustedFungibleToken => build(10)(tx) case tx: DisposeEntrustedNFT => build(11)(tx) case tx: DefineTokenWithPrecision => build(12)(tx) case tx: UpdateNFT => build(13)(tx) case tx: MintNFTWithMemo => build(14)(tx) case tx: CreateSnapshots => build(15)(tx) given txCirceDecoder: Decoder[TokenTx] = deriveDecoder given txCirceEncoder: Encoder[TokenTx] = deriveEncoder end TokenTx sealed trait RewardTx extends Transaction object RewardTx: final case class RegisterDao( networkId: NetworkId, createdAt: Instant, groupId: GroupId, daoAccountName: Account, moderators: Set[Account], // memo: Option[Utf8], ) extends RewardTx final case class UpdateDao( networkId: NetworkId, createdAt: Instant, groupId: GroupId, moderators: Set[Account], memo: Option[Utf8], ) extends RewardTx final case class RecordActivity( networkId: NetworkId, createdAt: Instant, timestamp: Instant, userActivity: Map[Account, Seq[DaoActivity]], tokenReceived: Map[TokenId, Seq[DaoActivity]], memo: Option[Utf8], ) extends RewardTx final case class OfferReward( networkId: NetworkId, createdAt: Instant, tokenDefinitionId: TokenDefinitionId, inputs: Set[Signed.TxHash], outputs: Map[Account, BigNat], memo: Option[Utf8], ) extends RewardTx with FungibleBalance final case class BuildSnapshot( networkId: NetworkId, createdAt: Instant, timestamp: Instant, accountAmount: BigNat, tokenAmount: BigNat, ownershipAmount: BigNat, memo: Option[Utf8], ) extends RewardTx final case class ExecuteReward( networkId: NetworkId, createdAt: Instant, daoAccount: Option[Account], memo: Option[Utf8], ) extends RewardTx with FungibleBalance final case class ExecuteRewardResult( outputs: Map[Account, BigNat], ) extends TransactionResult final case class ExecuteOwnershipReward( networkId: NetworkId, createdAt: Instant, definitionId: TokenDefinitionId, inputs: Set[Hash.Value[TransactionWithResult]], targets: Set[TokenId], memo: Option[Utf8], ) extends RewardTx with FungibleBalance final case class ExecuteOwnershipRewardResult( outputs: Map[Account, BigNat], ) extends TransactionResult given txByteDecoder: ByteDecoder[RewardTx] = ByteDecoder[BigNat].flatMap { bignat => bignat.toBigInt.toInt match case 0 => ByteDecoder[RegisterDao].widen case 1 => ByteDecoder[UpdateDao].widen case 2 => ByteDecoder[RecordActivity].widen case 3 => ByteDecoder[OfferReward].widen case 4 => ByteDecoder[BuildSnapshot].widen case 6 => ByteDecoder[ExecuteReward].widen case 9 => ByteDecoder[ExecuteOwnershipReward].widen } given txByteEncoder: ByteEncoder[RewardTx] = (rtx: RewardTx) => rtx match case tx: RegisterDao => build(0)(tx) case tx: UpdateDao => build(1)(tx) case tx: RecordActivity => build(2)(tx) case tx: OfferReward => build(3)(tx) case tx: BuildSnapshot => build(4)(tx) case tx: ExecuteReward => build(6)(tx) case tx: ExecuteOwnershipReward => build(9)(tx) given txCirceDecoder: Decoder[RewardTx] = deriveDecoder given txCirceEncoder: Encoder[RewardTx] = deriveEncoder end RewardTx sealed trait AgendaTx extends Transaction object AgendaTx: final case class SuggestSimpleAgenda( networkId: NetworkId, createdAt: Instant, title: Utf8, votingToken: TokenDefinitionId, voteStart: Instant, voteEnd: Instant, voteOptions: Map[Utf8, Utf8], ) extends AgendaTx final case class VoteSimpleAgenda( networkId: NetworkId, createdAt: Instant, agendaTxHash: Hash.Value[TransactionWithResult], selectedOption: Utf8, memo: Option[Utf8], ) extends AgendaTx final case class VoteSimpleAgendaResult( votingAmount: BigNat, ) extends TransactionResult given txByteDecoder: ByteDecoder[AgendaTx] = ByteDecoder[BigNat].flatMap { bignat => bignat.toBigInt.toInt match case 0 => ByteDecoder[SuggestSimpleAgenda].widen case 1 => ByteDecoder[VoteSimpleAgenda].widen } given txByteEncoder: ByteEncoder[AgendaTx] = (rtx: AgendaTx) => rtx match case tx: SuggestSimpleAgenda => build(0)(tx) case tx: VoteSimpleAgenda => build(1)(tx) given txCirceDecoder: Decoder[AgendaTx] = deriveDecoder given txCirceEncoder: Encoder[AgendaTx] = deriveEncoder end AgendaTx sealed trait VotingTx extends Transaction object VotingTx: /* "CreateVoteProposal": { "networkId": 2021, "createdAt": "2023-06-21T18:01:00Z", "proposalId": "PROPOSAL-2023-002", "title": "Approval for New NFT Collection Launch", "description": "Voting for approval of a new NFT collection proposed by the community", "votingPower": { "NFT-COLLECTION-001": 12347, "NFT-COLLECTION-002": 12348 }, "voteStart": "2023-06-22T00:00:00Z", "voteEnd": "2023-06-29T23:59:59Z", "voteType": "NFT_BASED", "voteOptions": { "1": "Approve", "2": "Reject" }, "quorum": 100, // Minimum participation (number of NFTs) "passThresholdNumer": 51, // Approval threshold numerator(51%) "passThresholdDemon": 100, // Approval threshold denominator(100%) } */ final case class CreateVoteProposal( networkId: NetworkId, createdAt: Instant, proposalId: ProposalId, title: Utf8, description: Utf8, votingPower: Map[TokenDefinitionId, BigNat], voteStart: Instant, voteEnd: Instant, voteType: VoteType, voteOptions: Map[Utf8, Utf8], quorum: BigNat, passThresholdNumer: BigNat, passThresholdDenom: BigNat, ) extends VotingTx /* "CastVote": { "networkId": 2021, "createdAt": "2023-06-23T10:30:00Z", "proposalId": "PROPOSAL-2023-001", "selectedOption": "1" } */ final case class CastVote( networkId: NetworkId, createdAt: Instant, proposalId: ProposalId, selectedOption: Utf8, ) extends VotingTx /* "TallyVotes": { "networkId": 2021, "createdAt": "2023-06-30T00:01:00Z", "proposalId": "PROPOSAL-2023-001" } */ final case class TallyVotes( networkId: NetworkId, createdAt: Instant, proposalId: ProposalId, ) extends VotingTx given txByteDecoder: ByteDecoder[VotingTx] = ByteDecoder[BigNat].flatMap: bignat => bignat.toBigInt.toInt match case 0 => ByteDecoder[CreateVoteProposal].widen case 1 => ByteDecoder[CastVote].widen case 2 => ByteDecoder[TallyVotes].widen given txByteEncoder: ByteEncoder[VotingTx] = (vtx: VotingTx) => vtx match case tx: CreateVoteProposal => build(0)(tx) case tx: CastVote => build(1)(tx) case tx: TallyVotes => build(2)(tx) given txCirceDecoder: Decoder[VotingTx] = deriveDecoder given txCirceEncoder: Encoder[VotingTx] = deriveEncoder end VotingTx sealed trait CreatorDaoTx extends Transaction object CreatorDaoTx: /* { "sig": { "NamedSignature": { "name": "founder", "sig": { "v": 27, "r": "62d7c7ddf8bea783b8ed59906b2f5db00b9e53031d6407933d7c4a80c7157f35", "s": "2d546c7d0f0fdf058e5bdf74b39cb2d3db34aa1dcdd6b2a76ea6504655b12b0f" } } }, "value": { "CreatorDaoTx": { "CreateCreatorDao": { "networkId": 102, "createdAt": "2024-03-15T09:28:41.339Z", "id": "dao_001", "name": "Art Creators DAO", "description": "A DAO for digital art creators", "founder": "creator001", "coordinator": "playnomm" } } } } */ final case class CreateCreatorDao( networkId: NetworkId, createdAt: Instant, id: CreatorDaoId, name: Utf8, description: Utf8, founder: Account, coordinator: Account, ) extends CreatorDaoTx /* ```json { "sig": { "NamedSignature": { "name": "moderator", "sig": { "v": 27, "r": "72d7c7ddf8bea783b8ed59906b2f5db00b9e53031d6407933d7c4a80c7157f35", "s": "3d546c7d0f0fdf058e5bdf74b39cb2d3db34aa1dcdd6b2a76ea6504655b12b0f" } } }, "value": { "CreatorDaoTx": { "UpdateCreatorDao": { "networkId": 102, "createdAt": "2024-03-15T10:28:41.339Z", "id": "dao_001", "name": "Digital Art Creators DAO", "description": "A DAO for digital art creators and collectors" } } } } ``` */ final case class UpdateCreatorDao( networkId: NetworkId, createdAt: Instant, id: CreatorDaoId, name: Utf8, description: Utf8, ) extends CreatorDaoTx /* ```json { "sig": { "NamedSignature": { "name": "founder", "sig": { "v": 27, "r": "82d7c7ddf8bea783b8ed59906b2f5db00b9e53031d6407933d7c4a80c7157f35", "s": "4d546c7d0f0fdf058e5bdf74b39cb2d3db34aa1dcdd6b2a76ea6504655b12b0f" } } }, "value": { "CreatorDaoTx": { "DisbandCreatorDao": { "networkId": 102, "createdAt": "2024-03-15T11:28:41.339Z", "id": "dao_001" } } } } ``` */ final case class DisbandCreatorDao( networkId: NetworkId, createdAt: Instant, id: CreatorDaoId, ) extends CreatorDaoTx final case class ReplaceCoordinator( networkId: NetworkId, createdAt: Instant, id: CreatorDaoId, newCoordinator: Account, ) extends CreatorDaoTx final case class AddMembers( networkId: NetworkId, createdAt: Instant, id: CreatorDaoId, members: Set[Account], ) extends CreatorDaoTx final case class RemoveMembers( networkId: NetworkId, createdAt: Instant, id: CreatorDaoId, members: Set[Account], ) extends CreatorDaoTx final case class PromoteModerators( networkId: NetworkId, createdAt: Instant, id: CreatorDaoId, members: Set[Account], ) extends CreatorDaoTx final case class DemoteModerators( networkId: NetworkId, createdAt: Instant, id: CreatorDaoId, members: Set[Account], ) extends CreatorDaoTx given txByteDecoder: ByteDecoder[CreatorDaoTx] = ByteDecoder[BigNat].flatMap: bignat => bignat.toBigInt.toInt match case 0 => ByteDecoder[CreateCreatorDao].widen case 1 => ByteDecoder[UpdateCreatorDao].widen case 2 => ByteDecoder[DisbandCreatorDao].widen case 3 => ByteDecoder[ReplaceCoordinator].widen case 4 => ByteDecoder[AddMembers].widen case 5 => ByteDecoder[RemoveMembers].widen case 6 => ByteDecoder[PromoteModerators].widen case 7 => ByteDecoder[DemoteModerators].widen given txByteEncoder: ByteEncoder[CreatorDaoTx] = (cdtx: CreatorDaoTx) => cdtx match case tx: CreateCreatorDao => build(0)(tx) case tx: UpdateCreatorDao => build(1)(tx) case tx: DisbandCreatorDao => build(2)(tx) case tx: ReplaceCoordinator => build(3)(tx) case tx: AddMembers => build(4)(tx) case tx: RemoveMembers => build(5)(tx) case tx: PromoteModerators => build(6)(tx) case tx: DemoteModerators => build(7)(tx) given txCirceDecoder: Decoder[CreatorDaoTx] = deriveDecoder given txCirceEncoder: Encoder[CreatorDaoTx] = deriveEncoder end CreatorDaoTx private def build[A: ByteEncoder](discriminator: Long)(tx: A): ByteVector = ByteEncoder[BigNat].encode(BigNat.unsafeFromLong(discriminator)) ++ ByteEncoder[A].encode(tx) given txByteDecoder: ByteDecoder[Transaction] = ByteDecoder[BigNat].flatMap: bignat => bignat.toBigInt.toInt match case 0 => ByteDecoder[AccountTx].widen case 1 => ByteDecoder[GroupTx].widen case 2 => ByteDecoder[TokenTx].widen case 3 => ByteDecoder[RewardTx].widen case 4 => ByteDecoder[AgendaTx].widen case 5 => ByteDecoder[VotingTx].widen case 6 => ByteDecoder[CreatorDaoTx].widen given txByteEncoder: ByteEncoder[Transaction] = (tx: Transaction) => tx match case tx: AccountTx => build(0)(tx) case tx: GroupTx => build(1)(tx) case tx: TokenTx => build(2)(tx) case tx: RewardTx => build(3)(tx) case tx: AgendaTx => build(4)(tx) case tx: VotingTx => build(5)(tx) case tx: CreatorDaoTx => build(6)(tx) given txHash: Hash[Transaction] = Hash.build given txSign: Sign[Transaction] = Sign.build given txRecover: Recover[Transaction] = Recover.build given txCirceDecoder: Decoder[Transaction] = deriveDecoder given txCirceEncoder: Encoder[Transaction] = deriveEncoder sealed trait FungibleBalance sealed trait NftBalance: def tokenId: TokenId sealed trait DealSuggestion: def originalSuggestion: Option[Signed.TxHash] ================================================ FILE: modules/api/src/main/scala/io/leisuremeta/chain/api/model/TransactionWithResult.scala ================================================ package io.leisuremeta.chain package api.model import lib.crypto.Hash final case class TransactionWithResult ( signedTx: Signed.Tx, result: Option[TransactionResult], ) object TransactionWithResult: @SuppressWarnings(Array("org.wartremover.warts.Overloading")) inline def apply[Tx <: Transaction](signedTx: Signed[Tx])( inline resultOption: Option[TransactionResult], ): TransactionWithResult = def widenTx: Signed.Tx = Signed(signedTx.sig, signedTx.value) import scala.compiletime.* inline signedTx.value match case ap: Transaction.AccountTx.AddPublicKeySummaries => inline resultOption match case Some(Transaction.AccountTx.AddPublicKeySummariesResult(removed)) => TransactionWithResult(widenTx, resultOption) case other => error("wrong result type: expected AddPublicKeySummariesResult") case bt: Transaction.TokenTx.BurnFungibleToken => inline resultOption match case Some(Transaction.TokenTx.BurnFungibleTokenResult(amount)) => TransactionWithResult(widenTx, resultOption) case other => error("wrong result type: expected BurnFungibleTokenResult") case bt: Transaction.TokenTx.EntrustFungibleToken => inline resultOption match case Some(Transaction.TokenTx.EntrustFungibleTokenResult(amount)) => TransactionWithResult(widenTx, resultOption) case other => error("wrong result type: expected EntrustFungibleTokenResult") case xr: Transaction.RewardTx.ExecuteReward => inline resultOption match case Some(Transaction.RewardTx.ExecuteRewardResult(outputs)) => TransactionWithResult(widenTx, resultOption) case other => error("wrong result type: expected ExecuteRewardResult") case _ => inline resultOption match case None => TransactionWithResult(widenTx, resultOption) case other => error( "wrong result type: expected None but " + codeOf(resultOption), ) given Hash[TransactionWithResult] = Hash[Transaction].contramap(_.signedTx.value) object ops: extension [A](txHash: Hash.Value[A]) def toResultHashValue: Hash.Value[TransactionWithResult] = Hash.Value[TransactionWithResult](txHash.toUInt256Bytes) def toSignedTxHash: Hash.Value[Signed.Tx] = Hash.Value[Signed.Tx](txHash.toUInt256Bytes) ================================================ FILE: modules/api/src/main/scala/io/leisuremeta/chain/api/model/account/EthAddress.scala ================================================ package io.leisuremeta.chain package api.model package account import io.circe.{Decoder, Encoder, KeyDecoder, KeyEncoder} import sttp.tapir.{Codec, DecodeResult, Schema} import sttp.tapir.CodecFormat.TextPlain import lib.codec.byte.{ByteDecoder, ByteEncoder} import lib.datatype.Utf8 opaque type EthAddress = Utf8 object EthAddress: def apply(utf8: Utf8): EthAddress = utf8 extension (a: EthAddress) def utf8: Utf8 = a given Decoder[EthAddress] = Utf8.utf8CirceDecoder given Encoder[EthAddress] = Utf8.utf8CirceEncoder given Schema[EthAddress] = Schema.string given KeyDecoder[EthAddress] = Utf8.utf8CirceKeyDecoder given KeyEncoder[EthAddress] = Utf8.utf8CirceKeyEncoder given ByteDecoder[EthAddress] = Utf8.utf8ByteDecoder.map(EthAddress(_)) given ByteEncoder[EthAddress] = Utf8.utf8ByteEncoder.contramap(_.utf8) given Codec[String, EthAddress, TextPlain] = Codec.string.mapDecode{ (s: String) => Utf8.from(s) match case Left(e) => DecodeResult.Error(s, e) case Right(a) => DecodeResult.Value(EthAddress(a)) }(_.utf8.value) ================================================ FILE: modules/api/src/main/scala/io/leisuremeta/chain/api/model/account/ExternalChain.scala ================================================ package io.leisuremeta.chain package api.model.account import cats.syntax.either.* import io.circe.{Decoder, Encoder, KeyDecoder, KeyEncoder} import sttp.tapir.{Codec, DecodeResult}//, Schema} import sttp.tapir.CodecFormat.TextPlain import lib.codec.byte.{ByteDecoder, ByteEncoder} import lib.datatype.BigNat import lib.failure.DecodingFailure import java.util.Locale enum ExternalChain(val name: String, val abbr: String): case ETH extends ExternalChain("Ethereum", "eth") case SOL extends ExternalChain("Solana", "sol") object ExternalChain : def fromAbbr(abbr: String): Option[ExternalChain] = abbr.toLowerCase(Locale.US) match case "eth" => Some(ETH) case "sol" => Some(SOL) case _ => None given Decoder[ExternalChain] = Decoder.decodeString.emap: fromAbbr(_).toRight("Unknown public chain") given Encoder[ExternalChain] = Encoder.encodeString.contramap(_.abbr) given KeyDecoder[ExternalChain] = KeyDecoder.instance(fromAbbr(_)) given KeyEncoder[ExternalChain] = KeyEncoder.encodeKeyString.contramap(_.abbr) given ByteDecoder[ExternalChain] = BigNat.bignatByteDecoder.emap: (bn: BigNat) => bn.toBigInt.toInt match case 0 => ExternalChain.ETH.asRight[DecodingFailure] case 1 => ExternalChain.SOL.asRight[DecodingFailure] case _ => DecodingFailure("Unknown public chain").asLeft[ExternalChain] given ByteEncoder[ExternalChain] = BigNat.bignatByteEncoder.contramap: case ExternalChain.ETH => BigNat.Zero case ExternalChain.SOL => BigNat.One given Codec[String, ExternalChain, TextPlain] = Codec.string .mapDecode: abbr => fromAbbr(abbr) match case Some(chain) => DecodeResult.Value(chain) case None => DecodeResult.Error(abbr, DecodingFailure(s"Invalid public chain: $abbr")) .apply(_.abbr) ================================================ FILE: modules/api/src/main/scala/io/leisuremeta/chain/api/model/account/ExternalChainAddress.scala ================================================ package io.leisuremeta.chain package api.model package account import io.circe.{Decoder, Encoder, KeyDecoder, KeyEncoder} import sttp.tapir.{Codec, DecodeResult, Schema} import sttp.tapir.CodecFormat.TextPlain import lib.codec.byte.{ByteDecoder, ByteEncoder} import lib.datatype.Utf8 opaque type ExternalChainAddress = Utf8 object ExternalChainAddress: def apply(utf8: Utf8): ExternalChainAddress = utf8 extension (a: ExternalChainAddress) def utf8: Utf8 = a given Decoder[ExternalChainAddress] = Utf8.utf8CirceDecoder given Encoder[ExternalChainAddress] = Utf8.utf8CirceEncoder given Schema[ExternalChainAddress] = Schema.string given KeyDecoder[ExternalChainAddress] = Utf8.utf8CirceKeyDecoder given KeyEncoder[ExternalChainAddress] = Utf8.utf8CirceKeyEncoder given ByteDecoder[ExternalChainAddress] = Utf8.utf8ByteDecoder.map(ExternalChainAddress(_)) given ByteEncoder[ExternalChainAddress] = Utf8.utf8ByteEncoder.contramap(_.utf8) given Codec[String, ExternalChainAddress, TextPlain] = Codec.string.mapDecode{ (s: String) => Utf8.from(s) match case Left(e) => DecodeResult.Error(s, e) case Right(a) => DecodeResult.Value(ExternalChainAddress(a)) }(_.utf8.value) ================================================ FILE: modules/api/src/main/scala/io/leisuremeta/chain/api/model/agenda/AgendaId.scala ================================================ package io.leisuremeta.chain package api.model.agenda import io.circe.{Decoder, Encoder, KeyDecoder, KeyEncoder} import sttp.tapir.{Codec, DecodeResult, Schema} import sttp.tapir.CodecFormat.TextPlain import lib.codec.byte.{ByteDecoder, ByteEncoder} import lib.datatype.Utf8 opaque type AgendaId = Utf8 object AgendaId: def apply(id: Utf8): AgendaId = id extension (id: AgendaId) def utf8: Utf8 = id given Decoder[AgendaId] = Utf8.utf8CirceDecoder given Encoder[AgendaId] = Utf8.utf8CirceEncoder given Schema[AgendaId] = Schema.string given KeyDecoder[AgendaId] = Utf8.utf8CirceKeyDecoder given KeyEncoder[AgendaId] = Utf8.utf8CirceKeyEncoder given ByteDecoder[AgendaId] = Utf8.utf8ByteDecoder.map(AgendaId(_)) given ByteEncoder[AgendaId] = Utf8.utf8ByteEncoder.contramap(_.utf8) given Codec[String, AgendaId, TextPlain] = Codec.string.mapDecode{ (s: String) => Utf8.from(s) match case Left(e) => DecodeResult.Error(s, e) case Right(a) => DecodeResult.Value(AgendaId(a)) }(_.utf8.value) given cats.Eq[AgendaId] = cats.Eq.fromUniversalEquals ================================================ FILE: modules/api/src/main/scala/io/leisuremeta/chain/api/model/api_model/AccountInfo.scala ================================================ package io.leisuremeta.chain package api.model package api_model import account.* import lib.datatype.Utf8 final case class AccountInfo( externalChainAddresses: Map[ExternalChain, ExternalChainAddress], ethAddress: Option[EthAddress], guardian: Option[Account], memo: Option[Utf8], publicKeySummaries: Map[PublicKeySummary, PublicKeySummary.Info], ) ================================================ FILE: modules/api/src/main/scala/io/leisuremeta/chain/api/model/api_model/ActivityInfo.scala ================================================ package io.leisuremeta.chain package api.model package api_model import java.time.Instant import lib.crypto.Hash import lib.datatype.Utf8 final case class ActivityInfo( timestamp: Instant, point: BigInt, description: Utf8, txHash: Hash.Value[TransactionWithResult], ) ================================================ FILE: modules/api/src/main/scala/io/leisuremeta/chain/api/model/api_model/BalanceInfo.scala ================================================ package io.leisuremeta.chain package api.model package api_model import io.circe.generic.semiauto.* import lib.crypto.Hash import lib.datatype.BigNat final case class BalanceInfo( totalAmount: BigNat, unused: Map[Hash.Value[TransactionWithResult], TransactionWithResult], ) ================================================ FILE: modules/api/src/main/scala/io/leisuremeta/chain/api/model/api_model/BlockInfo.scala ================================================ package io.leisuremeta.chain package api.model package api_model import java.time.Instant import lib.datatype.BigNat final case class BlockInfo( blockNumber: BigNat, timestamp: Instant, blockHash: Block.BlockHash, txCount: Int, ) ================================================ FILE: modules/api/src/main/scala/io/leisuremeta/chain/api/model/api_model/CreatorDaoInfo.scala ================================================ package io.leisuremeta.chain package api.model package api_model import lib.datatype.Utf8 import creator_dao.CreatorDaoId final case class CreatorDaoInfo( id: CreatorDaoId, name: Utf8, description: Utf8, founder: Account, coordinator: Account, moderators: Set[Account], ) ================================================ FILE: modules/api/src/main/scala/io/leisuremeta/chain/api/model/api_model/GroupInfo.scala ================================================ package io.leisuremeta.chain.api.model package api_model final case class GroupInfo( data: GroupData, accounts: Set[Account], ) ================================================ FILE: modules/api/src/main/scala/io/leisuremeta/chain/api/model/api_model/NftBalanceInfo.scala ================================================ package io.leisuremeta.chain package api.model package api_model import io.circe.generic.semiauto.* import token.TokenDefinitionId import lib.crypto.Hash import lib.datatype.Utf8 final case class NftBalanceInfo( tokenDefinitionId: TokenDefinitionId, txHash: Hash.Value[TransactionWithResult], tx: TransactionWithResult, memo: Option[Utf8], ) ================================================ FILE: modules/api/src/main/scala/io/leisuremeta/chain/api/model/api_model/RewardInfo.scala ================================================ package io.leisuremeta.chain package api.model package api_model import java.time.Instant import lib.datatype.BigNat import token.Rarity import reward.DaoActivity final case class RewardInfo( account: Account, reward: RewardInfo.Reward, point: RewardInfo.Point, timestamp: Instant, totalNumberOfDao: BigNat, ) object RewardInfo: final case class Reward( total: BigNat, activity: BigNat, token: BigNat, rarity: BigNat, bonus: BigNat, ) final case class Point( activity: DaoActivity, token: DaoActivity, rarity: Map[Rarity, BigNat], ) ================================================ FILE: modules/api/src/main/scala/io/leisuremeta/chain/api/model/api_model/TxInfo.scala ================================================ package io.leisuremeta.chain package api.model package api_model import java.time.Instant final case class TxInfo( txHash: Signed.TxHash, createdAt: Instant, account: Account, `type`: String, ) ================================================ FILE: modules/api/src/main/scala/io/leisuremeta/chain/api/model/creator_dao/CreatorDaoData.scala ================================================ package io.leisuremeta.chain package api.model package creator_dao import lib.datatype.Utf8 final case class CreatorDaoData( id: CreatorDaoId, name: Utf8, description: Utf8, founder: Account, coordinator: Account, ) ================================================ FILE: modules/api/src/main/scala/io/leisuremeta/chain/api/model/creator_dao/CreatorDaoId.scala ================================================ package io.leisuremeta.chain package api.model package creator_dao import io.circe.{Decoder, Encoder, KeyDecoder, KeyEncoder} import sttp.tapir.{Codec, DecodeResult, Schema} import sttp.tapir.CodecFormat.TextPlain import lib.codec.byte.{ByteDecoder, ByteEncoder} import lib.datatype.Utf8 opaque type CreatorDaoId = Utf8 object CreatorDaoId: def apply(utf8: Utf8): CreatorDaoId = utf8 extension (a: CreatorDaoId) def utf8: Utf8 = a given Decoder[CreatorDaoId] = Utf8.utf8CirceDecoder given Encoder[CreatorDaoId] = Utf8.utf8CirceEncoder given Schema[CreatorDaoId] = Schema.string given KeyDecoder[CreatorDaoId] = Utf8.utf8CirceKeyDecoder given KeyEncoder[CreatorDaoId] = Utf8.utf8CirceKeyEncoder given ByteDecoder[CreatorDaoId] = Utf8.utf8ByteDecoder.map(CreatorDaoId(_)) given ByteEncoder[CreatorDaoId] = Utf8.utf8ByteEncoder.contramap(_.utf8) given Codec[String, CreatorDaoId, TextPlain] = Codec.string.mapDecode { (s: String) => Utf8.from(s) match case Left(e) => DecodeResult.Error(s, e) case Right(a) => DecodeResult.Value(CreatorDaoId(a)) }(_.utf8.value) given cats.Eq[CreatorDaoId] = cats.Eq.fromUniversalEquals ================================================ FILE: modules/api/src/main/scala/io/leisuremeta/chain/api/model/reward/ActivityLog.scala ================================================ package io.leisuremeta.chain package api.model package reward import lib.crypto.Hash import lib.datatype.Utf8 final case class ActivityLog( point: BigInt, description: Utf8, txHash: Hash.Value[TransactionWithResult], ) ================================================ FILE: modules/api/src/main/scala/io/leisuremeta/chain/api/model/reward/ActivityRewardLog.scala ================================================ package io.leisuremeta.chain package api.model package reward import lib.crypto.Hash final case class ActivityRewardLog( activitySnapshot: ActivitySnapshot, txHash: Hash.Value[TransactionWithResult], ) ================================================ FILE: modules/api/src/main/scala/io/leisuremeta/chain/api/model/reward/ActivitySnapshot.scala ================================================ package io.leisuremeta.chain package api.model package reward import java.time.Instant import lib.datatype.BigNat import token.TokenDefinitionId final case class ActivitySnapshot( account: Account, from: Instant, to: Instant, point: BigInt, definitionId: TokenDefinitionId, amount: BigNat, backlogs: Set[Signed.TxHash], ) ================================================ FILE: modules/api/src/main/scala/io/leisuremeta/chain/api/model/reward/DaoActivity.scala ================================================ package io.leisuremeta.chain package api.model.reward import io.circe.{Decoder, Encoder} import io.circe.generic.semiauto.* import lib.datatype.Utf8 final case class DaoActivity( point: BigInt, description: Utf8, ) object DaoActivity: given circeDecoder: Decoder[DaoActivity] = deriveDecoder given circeEncoder: Encoder[DaoActivity] = deriveEncoder ================================================ FILE: modules/api/src/main/scala/io/leisuremeta/chain/api/model/reward/DaoInfo.scala ================================================ package io.leisuremeta.chain.api.model package reward final case class DaoInfo( moderators: Set[Account], ) ================================================ FILE: modules/api/src/main/scala/io/leisuremeta/chain/api/model/reward/OwnershipRewardLog.scala ================================================ package io.leisuremeta.chain package api.model package reward import lib.crypto.Hash final case class OwnershipRewardLog( ownershipSnapshot: OwnershipSnapshot, txHash: Hash.Value[TransactionWithResult], ) ================================================ FILE: modules/api/src/main/scala/io/leisuremeta/chain/api/model/reward/OwnershipSnapshot.scala ================================================ package io.leisuremeta.chain package api.model package reward import java.time.Instant import lib.datatype.BigNat import token.TokenDefinitionId final case class OwnershipSnapshot( account: Account, timestamp: Instant, point: BigNat, definitionId: TokenDefinitionId, amount: BigNat, ) ================================================ FILE: modules/api/src/main/scala/io/leisuremeta/chain/api/model/token/NftInfo.scala ================================================ package io.leisuremeta.chain package api.model package token import io.circe.{Decoder, Encoder} import io.circe.generic.semiauto.* import lib.datatype.{BigNat, UInt256Bytes, Utf8} final case class NftInfo( minter: Account, rarity: Map[Rarity, BigNat], dataUrl: Utf8, contentHash: UInt256Bytes, ) object NftInfo: given Decoder[NftInfo] = deriveDecoder given Encoder[NftInfo] = deriveEncoder ================================================ FILE: modules/api/src/main/scala/io/leisuremeta/chain/api/model/token/NftInfoWithPrecision.scala ================================================ package io.leisuremeta.chain package api.model package token import io.circe.{Decoder, Encoder} import io.circe.generic.semiauto.* import lib.datatype.{BigNat, UInt256Bytes, Utf8} final case class NftInfoWithPrecision( minter: Account, rarity: Map[Rarity, BigNat], precision: BigNat, dataUrl: Utf8, contentHash: UInt256Bytes, ) object NftInfoWithPrecision: def fromNftInfo(nftInfo: NftInfo): NftInfoWithPrecision = NftInfoWithPrecision( nftInfo.minter, nftInfo.rarity, BigNat.Zero, nftInfo.dataUrl, nftInfo.contentHash, ) given Decoder[NftInfoWithPrecision] = deriveDecoder given Encoder[NftInfoWithPrecision] = deriveEncoder ================================================ FILE: modules/api/src/main/scala/io/leisuremeta/chain/api/model/token/NftState.scala ================================================ package io.leisuremeta.chain package api.model package token import lib.crypto.Hash import lib.datatype.{BigNat, Utf8} final case class NftState( tokenId: TokenId, tokenDefinitionId: TokenDefinitionId, rarity: Rarity, weight: BigNat, currentOwner: Account, memo: Option[Utf8], lastUpdateTx: Hash.Value[TransactionWithResult], previousState: Option[Hash.Value[TransactionWithResult]], ) ================================================ FILE: modules/api/src/main/scala/io/leisuremeta/chain/api/model/token/Rarity.scala ================================================ package io.leisuremeta.chain package api.model package token import io.circe.{Decoder, Encoder, KeyDecoder, KeyEncoder} import sttp.tapir.Schema import lib.codec.byte.{ByteDecoder, ByteEncoder} import lib.datatype.Utf8 opaque type Rarity = Utf8 object Rarity: def apply(value: Utf8): Rarity = value extension (a: Rarity) def utf8: Utf8 = a given Encoder[Rarity] = Utf8.utf8CirceEncoder given Decoder[Rarity] = Utf8.utf8CirceDecoder given KeyEncoder[Rarity] = Utf8.utf8CirceKeyEncoder given KeyDecoder[Rarity] = Utf8.utf8CirceKeyDecoder given Schema[Rarity] = Schema.string given ByteDecoder[Rarity] = Utf8.utf8ByteDecoder.map(Rarity(_)) given ByteEncoder[Rarity] = Utf8.utf8ByteEncoder.contramap(_.utf8) ================================================ FILE: modules/api/src/main/scala/io/leisuremeta/chain/api/model/token/SnapshotState.scala ================================================ package io.leisuremeta.chain package api.model package token import java.time.Instant import io.circe.{Decoder, Encoder} import io.circe.generic.semiauto.* import sttp.tapir.{Codec, DecodeResult, Schema} import sttp.tapir.CodecFormat.TextPlain import lib.codec.byte.{ByteDecoder, ByteEncoder} import lib.datatype.{BigNat, Utf8} final case class SnapshotState( snapshotId: SnapshotState.SnapshotId, createdAt: Instant, txHash: Signed.TxHash, memo: Option[Utf8], ) object SnapshotState: opaque type SnapshotId = BigNat object SnapshotId: def apply(value: BigNat): SnapshotId = value given snapshotIdByteEncoder: ByteEncoder[SnapshotId] = BigNat.bignatByteEncoder given snapshotIdByteDecoder: ByteDecoder[SnapshotId] = BigNat.bignatByteDecoder given snapshotIdCirceDecoder: Decoder[SnapshotId] = BigNat.bignatCirceDecoder given snapshotIdCirceEncoder: Encoder[SnapshotId] = BigNat.bignatCirceEncoder given schema: Schema[SnapshotId] = Schema.schemaForBigInt .map[BigNat]: (bigint: BigInt) => BigNat.fromBigInt(bigint).toOption .apply: (bignat: BigNat) => bignat.toBigInt given tapirCodec: Codec[String, SnapshotId, TextPlain] = Codec.string .mapDecode: (s: String) => BigNat.fromBigInt(BigInt(s)) match case Right(bignat) => DecodeResult.Value(bignat) case Left(msg) => DecodeResult.Error(s, new Exception(msg)) .apply: (bignat: BigNat) => bignat.toBigInt.toString(10) val Zero: SnapshotId = BigNat.Zero extension (id: SnapshotId) def inc: SnapshotId = increase def increase: SnapshotId = BigNat.add(id, BigNat.One) ================================================ FILE: modules/api/src/main/scala/io/leisuremeta/chain/api/model/token/TokenDefinition.scala ================================================ package io.leisuremeta.chain package api.model package token import lib.datatype.{BigNat, Utf8} final case class TokenDefinition( id: TokenDefinitionId, name: Utf8, symbol: Option[Utf8], adminGroup: Option[GroupId], totalAmount: BigNat, nftInfo: Option[NftInfoWithPrecision], ) ================================================ FILE: modules/api/src/main/scala/io/leisuremeta/chain/api/model/token/TokenDefinitionId.scala ================================================ package io.leisuremeta.chain package api.model package token import cats.Eq import io.circe.{Decoder, Encoder, KeyDecoder, KeyEncoder} import sttp.tapir.{Codec, DecodeResult, Schema} import sttp.tapir.CodecFormat.TextPlain import lib.codec.byte.{ByteDecoder, ByteEncoder} import lib.datatype.Utf8 opaque type TokenDefinitionId = Utf8 object TokenDefinitionId: def apply(utf8: Utf8): TokenDefinitionId = utf8 extension (a: TokenDefinitionId) def utf8: Utf8 = a given Decoder[TokenDefinitionId] = Utf8.utf8CirceDecoder given Encoder[TokenDefinitionId] = Utf8.utf8CirceEncoder given KeyDecoder[TokenDefinitionId] = Utf8.utf8CirceKeyDecoder given KeyEncoder[TokenDefinitionId] = Utf8.utf8CirceKeyEncoder given Schema[TokenDefinitionId] = Schema.string given ByteDecoder[TokenDefinitionId] = Utf8.utf8ByteDecoder.map(TokenDefinitionId(_)) given ByteEncoder[TokenDefinitionId] = Utf8.utf8ByteEncoder.contramap(_.utf8) given Codec[String, TokenDefinitionId, TextPlain] = Codec.string.mapDecode{ (s: String) => Utf8.from(s) match case Left(e) => DecodeResult.Error(s, e) case Right(a) => DecodeResult.Value(TokenDefinitionId(a)) }(_.utf8.value) given Eq[TokenDefinitionId] = Eq.fromUniversalEquals ================================================ FILE: modules/api/src/main/scala/io/leisuremeta/chain/api/model/token/TokenDetail.scala ================================================ package io.leisuremeta.chain package api.model.token import io.circe.{Decoder, Encoder} import io.circe.generic.semiauto.* import lib.datatype.BigNat sealed trait TokenDetail object TokenDetail: final case class FungibleDetail(amount: BigNat) extends TokenDetail final case class NftDetail(tokenId: TokenId) extends TokenDetail given tokenDetailCirceEncoder: Encoder[TokenDetail] = deriveEncoder given tokenDetailCirceDecoder: Decoder[TokenDetail] = deriveDecoder ================================================ FILE: modules/api/src/main/scala/io/leisuremeta/chain/api/model/token/TokenId.scala ================================================ package io.leisuremeta.chain package api.model package token import io.circe.{Decoder, Encoder, KeyDecoder, KeyEncoder} import sttp.tapir.{Codec, DecodeResult, Schema} import sttp.tapir.CodecFormat.TextPlain import lib.codec.byte.{ByteDecoder, ByteEncoder} import lib.datatype.Utf8 opaque type TokenId = Utf8 object TokenId: def apply(utf8: Utf8): TokenId = utf8 extension (a: TokenId) def utf8: Utf8 = a given Decoder[TokenId] = Utf8.utf8CirceDecoder given Encoder[TokenId] = Utf8.utf8CirceEncoder given Schema[TokenId] = Schema.string given KeyDecoder[TokenId] = Utf8.utf8CirceKeyDecoder given KeyEncoder[TokenId] = Utf8.utf8CirceKeyEncoder given ByteDecoder[TokenId] = Utf8.utf8ByteDecoder.map(TokenId(_)) given ByteEncoder[TokenId] = Utf8.utf8ByteEncoder.contramap(_.utf8) given Codec[String, TokenId, TextPlain] = Codec.string.mapDecode{ (s: String) => Utf8.from(s) match case Left(e) => DecodeResult.Error(s, e) case Right(a) => DecodeResult.Value(TokenId(a)) }(_.utf8.value) given cats.Eq[TokenId] = cats.Eq.fromUniversalEquals ================================================ FILE: modules/api/src/main/scala/io/leisuremeta/chain/api/model/voting/Proposal.scala ================================================ package io.leisuremeta.chain package api.model package voting import java.time.Instant import lib.datatype.{BigNat, Utf8} import token.TokenDefinitionId final case class Proposal( createdAt: Instant, proposalId: ProposalId, title: Utf8, description: Utf8, votingPower: Map[TokenDefinitionId, BigNat], voteStart: Instant, voteEnd: Instant, voteType: VoteType, voteOptions: Map[Utf8, Utf8], quorum: BigNat, passThresholdNumer: BigNat, passThresholdDenom: BigNat, isActive: Boolean, ) ================================================ FILE: modules/api/src/main/scala/io/leisuremeta/chain/api/model/voting/ProposalId.scala ================================================ package io.leisuremeta.chain package api.model.voting import cats.Eq import io.circe.{Decoder, Encoder} import sttp.tapir.{Codec, DecodeResult, Schema} import sttp.tapir.CodecFormat.TextPlain import lib.codec.byte.{ByteDecoder, ByteEncoder} import lib.datatype.Utf8 opaque type ProposalId = Utf8 object ProposalId: def apply(utf8: Utf8): ProposalId = utf8 extension (proposalId: ProposalId) def value: Utf8 = proposalId end extension given byteEncoder: ByteEncoder[ProposalId] = Utf8.utf8ByteEncoder given byteDecoder: ByteDecoder[ProposalId] = Utf8.utf8ByteDecoder given circeEncoder: Encoder[ProposalId] = Utf8.utf8CirceEncoder given circeDecoder: Decoder[ProposalId] = Utf8.utf8CirceDecoder given eq: Eq[ProposalId] = Eq.fromUniversalEquals given schema: Schema[ProposalId] = Schema.string given bignatCodec: Codec[String, ProposalId, TextPlain] = Codec.string .mapDecode: (s: String) => Utf8.from(s) match case Left(e) => DecodeResult.Error(s, e) case Right(v) => DecodeResult.Value(ProposalId(v)) .apply: (b: ProposalId) => b.value.value ================================================ FILE: modules/api/src/main/scala/io/leisuremeta/chain/api/model/voting/VoteType.scala ================================================ package io.leisuremeta.chain package api.model.voting import scala.util.Try import cats.Eq import cats.syntax.either.* import cats.syntax.eq.catsSyntaxEq import io.circe.{Decoder, Encoder} import lib.codec.byte.{ByteDecoder, ByteEncoder} import lib.datatype.BigNat import lib.failure.DecodingFailure enum VoteType(val name: String): case ONE_PERSON_ONE_VOTE extends VoteType("ONE_PERSON_ONE_VOTE") case TOKEN_WEIGHTED extends VoteType("TOKEN_WEIGHTED") case NFT_BASED extends VoteType("NFT_BASED") object VoteType: given eq: Eq[VoteType] = Eq.fromUniversalEquals given circeEncoder: Encoder[VoteType] = Encoder.encodeString.contramap(_.name) given circeDecoder: Decoder[VoteType] = Decoder.decodeString.emap: str => VoteType.values.find(_.name === str).toRight: s"VoteType $str is not valid" given byteEncoder: ByteEncoder[VoteType] = BigNat.bignatByteEncoder.contramap: voteType => BigNat.unsafeFromBigInt(BigInt(voteType.ordinal)) given byteDecoder: ByteDecoder[VoteType] = BigNat.bignatByteDecoder.emap: bignat => Try(VoteType.fromOrdinal(bignat.toBigInt.toInt)).toEither.leftMap: err => DecodingFailure: s"VoteType $bignat is not valid: ${err.getMessage}" ================================================ FILE: modules/api/tx_type.txt ================================================ 모든 트랜잭션 공통 create Transaction Transaction - TokenTx // LM Token - MintFungibleToken - TransferFungibleToken - EntrustFungibleToken - DisposeEntrustedFungibleToken - BurnFungibleToken // NFT Token - DefineToken - MintNFT - TransferNFT - EntrustNFT - DisposeEntrustedNFT - BurnNFT - AccountTx - CreateAccount => insertAccount - UpdateAccount => updateAccount - AddPublicKeySummaries - GroupTx - CreateGroup - AddAccounts - RewardTx - RegisterDao - UpdateDao - RecordActivity - BuildSnapshot - ExecuteAccountReward (Fungible) - ExecuteTokenReward. (Fungible) - ExecuteOwnershipReward. (Fungible) NFT 테이블 NFT_TX 테이블 로 변경. backend 서버에서 NFT_activities 리스트는 tx 테이블에서 조회해서 주기. ================================================ FILE: modules/archive/src/main/scala/io/leisuremeta/chain/archive/ArchiveMain.scala ================================================ package io.leisuremeta.chain package archive import java.nio.file.{Files, Paths, StandardOpenOption} import java.time.Instant import scala.concurrent.duration.* //import scala.io.Source import cats.Monad import cats.data.EitherT import cats.effect.{ExitCode, IO, IOApp} import cats.syntax.bifunctor.* //import cats.syntax.eq.* //import cats.syntax.flatMap.toFlatMapOps import cats.syntax.functor.* import cats.syntax.traverse.* import io.circe.generic.auto.* import io.circe.parser.decode //import io.circe.refined.* import io.circe.syntax.* import sttp.client3.* import sttp.client3.armeria.cats.ArmeriaCatsBackend import sttp.model.Uri import api.model.* import lib.crypto.{Hash, Signature} import lib.datatype.* final case class PBlock( header: PHeader, transactionHashes: Set[Signed.TxHash], votes: Set[Signature], ) final case class PHeader( number: BigNat, parentHash: Block.BlockHash, timestamp: Instant, ) object ArchiveMain extends IOApp: // val baseUri = "http://test.chain.leisuremeta.io:8080" // val baseUri = "http://localhost:7080" val baseUri = "http://localhost:8081" val archiveFileName = "txs1.archive" def logTxs(contents: String): IO[Unit] = IO.blocking: val path = Paths.get(archiveFileName) val _ = Files.write( path, contents.getBytes, StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.APPEND, ) def get[F[_]: Monad, A: io.circe.Decoder]( backend: SttpBackend[F, Any], )(uri: Uri): EitherT[F, String, A] = EitherT: basicRequest .get(uri) .send(backend) .map: response => for body <- response.body a <- decode[A](body).leftMap(_.getMessage()) yield a def put[F[_]: Monad]( backend: SttpBackend[F, Any], )(uri: Uri)(line: String): EitherT[F, String, List[Signed.TxHash]] = EitherT: // println(s"Req: $line") basicRequest .post(uri) .body(line) .send(backend) .map: response => // println(s"Response: $response") val result = for body <- response.body a <- decode[List[Signed.TxHash]](body).leftMap(_.getMessage()) yield a result def getTransaction[F[_]: Monad]( backend: SttpBackend[F, Any], )(txHash: Signed.TxHash): EitherT[F, String, TransactionWithResult] = get[F, TransactionWithResult](backend) .apply: uri"$baseUri/tx/${txHash.toUInt256Bytes.toBytes.toHex}" .leftMap: msg => scribe.error(s"error msg: $msg") msg def getBlock[F[_]: Monad]( backend: SttpBackend[F, Any], )(blockHash: Block.BlockHash): EitherT[F, String, PBlock] = get[F, PBlock](backend): uri"$baseUri/block/${blockHash.toUInt256Bytes.toBytes.toHex}" def getStatus[F[_]: Monad]( backend: SttpBackend[F, Any], ): EitherT[F, String, NodeStatus] = get[F, NodeStatus](backend)(uri"$baseUri/status") def loop[F[_]: Monad]( backend: SttpBackend[F, Any], )(next: Block.BlockHash, genesis: Block.BlockHash, until: BigInt, count: Long)( run: ( BigNat, Block.BlockHash, Set[Signed.TxHash], ) => EitherT[F, String, Unit], ): EitherT[F, String, Long] = for block <- getBlock[F](backend)(next) number = block.header.number.toBigInt _ <- EitherT.pure(scribe.info(s"block ${number}: $next")) _ <- EitherT.cond( number > until, (), s"block number $number is greater than $until", ) _ <- run(block.header.number, next, block.transactionHashes).recover: msg => scribe.error(s"error msg: $msg") () count1 <- loop[F](backend)(block.header.parentHash, genesis, until, count + 1)(run) yield count1 def run(args: List[String]): IO[ExitCode] = val until = BigInt("0") // val until = BigInt("12751183") for _ <- ArmeriaCatsBackend .resource[IO]: SttpBackendOptions.Default.connectionTimeout(10.minutes) .use: backend => val program = for status <- getStatus[IO](backend) block <- getBlock[IO](backend)(status.bestHash) count <- loop[IO](backend)( status.bestHash, status.genesisHash, until, 0, ): (blockNumber, blockHash, txSet) => txSet.toList .sortBy(_.toUInt256Bytes.toHex) .traverse: txHash => for tx <- getTransaction[IO](backend)(txHash) txString = tx.signedTx.asJson.noSpaces _ <- EitherT.right: logTxs: s"$blockNumber\t${txHash.toUInt256Bytes.toHex}\t$txString\n" yield () .as(()) yield println(s"total number of block: $count") // val from = 0 // val to = 1000000 // Source.fromFile(archiveFileName).getLines.to(LazyList).zipWithIndex.take(to).drop(from).traverse{ // (line, i) => //// println(s"$i: $line") // put[IO](backend)(uri"$baseUri/tx")(line).recover{ // case msg: String => // println(s"Error: $msg") // println(s"Error Request: $line") // Nil // } // } program.value yield ExitCode.Success ================================================ FILE: modules/bulk-insert/src/main/scala/io/leisuremeta/chain/bulkinsert/BulkInsertMain.scala ================================================ package io.leisuremeta.chain package bulkinsert import scala.io.Source import cats.data.{EitherT, Kleisli} import cats.effect.{Async, ExitCode, IO, IOApp, Resource} import cats.syntax.all.* import fs2.Stream import io.circe.parser.decode import scodec.bits.ByteVector import api.model.{Block, Signed, StateRoot} import api.model.TransactionWithResult.ops.* import lib.crypto.{CryptoOps, KeyPair} import lib.crypto.Hash.ops.* import lib.crypto.Sign.ops.* import lib.datatype.BigNat import lib.merkle.* import lib.merkle.MerkleTrie.NodeStore import node.NodeConfig import node.dapp.{PlayNommDApp, PlayNommDAppFailure, PlayNommState} import node.repository.{BlockRepository, StateRepository, TransactionRepository} import node.service.NodeInitializationService def bulkInsert[F[_] : Async: BlockRepository: TransactionRepository: StateRepository: PlayNommState: InvalidTxLogger]( config: NodeConfig, source: Source, from: String, until: String, ): EitherT[F, String, Unit] = for bestBlock <- NodeInitializationService .initialize[F](config.genesis.timestamp) merkleState = MerkleTrieState.fromRootOption(bestBlock.header.stateRoot.main) indexWithTxsStream = Stream .fromIterator[EitherT[F, String, *]](source.getLines(), 1) .filterNot(_ === "[]") .zipWithIndex .evalMap: (line, index) => if index % 1000L === 0L then scribe.info(s"Processing line #$index") line.split("\t").toList match case blockNumber :: txHash :: jsonString :: Nil => EitherT .fromEither[F]: decode[Signed.Tx](jsonString) .leftMap: e => scribe.error(s"Error decoding line #$blockNumber: $txHash: $jsonString: $e") e.getMessage() .map(tx => (blockNumber, tx)) case _ => scribe.error(s"Error parsing line: $line") EitherT.leftT[F, (String, Signed.Tx)](s"Error parsing line: $line") .groupAdjacentBy(_._1) .dropWhile(_._1 =!= from) .takeWhile(_._1 =!= until) .map: case (blockNumber, chunk) => (blockNumber, chunk.toList.map(_._2)) localKeyPair: KeyPair = val privateKey = scala.sys.env .get("LMNODE_PRIVATE_KEY") .map(BigInt(_, 16)) .orElse(config.local.`private`) .get CryptoOps.fromPrivate(privateKey) stateStream = indexWithTxsStream.evalMapAccumulate((bestBlock, merkleState)): case ((previousBlock, ms), (blockNumber, txs)) => val program = for result <- Stream .fromIterator[EitherT[F, PlayNommDAppFailure, *]](txs.iterator, 1) .evalMapAccumulate(ms): (ms, tx) => // scribe.info(s"signer: ${tx.sig.account}") // scribe.info(s"tx: ${tx.value}") // PlayNommDApp[F](tx) // .run(ms) // .map: (ms, txWithResult) => // (ms, Option(txWithResult)) // .recoverWith: _ => // RecoverTx(ms, tx) RecoverTx(ms, tx) .recoverWith: failure => PlayNommDApp[F](tx) .run(ms) .map: (ms, txWithResult) => (ms, Option(txWithResult)) .leftMap: failure2 => scribe.info(s"Error: $failure") failure2 .map: result => if BigInt(blockNumber) % 100 === 0 then scribe.info(s"#$blockNumber: ${result._1.root}") result .compile .toList .leftMap: e => scribe.error(s"Error building txs #$blockNumber: $txs: $e") e .leftSemiflatTap: e => StateRepository[F] .put(ms) .leftMap: f => scribe.error(s"Fail to put state: ${f.msg}") .value (states, txWithResultOptions) = result.unzip finalState = states.last txWithResults = txWithResultOptions.flatten txHashes = txWithResults.map(_.toHash) txState = txs .map(_.toHash) .sortBy(_.toUInt256Bytes.toBytes) .foldLeft(MerkleTrieState.empty): (state, txHash) => given idNodeStore: NodeStore[cats.Id] = Kleisli.pure(None) MerkleTrie .put[cats.Id]( txHash.toUInt256Bytes.toBytes.toNibbles, ByteVector.empty, ) .runS(state) .value .getOrElse(state) stateRoot1 = StateRoot(finalState.root) now = (previousBlock.header.timestamp :: txs.map(_.value.createdAt)) .maxBy(_.getEpochSecond()) blockNumber = BigNat.add(previousBlock.header.number, BigNat.One) header = Block.Header( number = blockNumber, parentHash = previousBlock.toHash, stateRoot = stateRoot1, transactionsRoot = txState.root, timestamp = now, ) sig <- EitherT .fromEither(header.toHash.signBy(localKeyPair)) .leftMap: msg => scribe.error(s"Fail to sign header: $msg") PlayNommDAppFailure.internal(s"Fail to sign header: $msg") block = Block( header = header, transactionHashes = txHashes.toSet.map(_.toSignedTxHash), votes = Set(sig), ) _ <- BlockRepository[F] .put(block) .leftMap: e => scribe.error(s"Fail to put block: $e") PlayNommDAppFailure.internal(s"Fail to put block: ${e.msg}") finalState1 <- if blockNumber.toBigInt % 10000 === 0 then StateRepository[F] .put(finalState) .leftMap: e => scribe.error(s"Fail to put state: $e") PlayNommDAppFailure.internal: s"Fail to put state: ${e.msg}" .map: _ => MerkleTrieState.fromRootOption(finalState.root) else EitherT.pure(finalState) _ <- txWithResults.traverse: txWithResult => EitherT.liftF: TransactionRepository[F].put(txWithResult) yield ((block, finalState1), (blockNumber, txWithResults)) program .leftMap: e => scribe.error(s"Error applying txs #$blockNumber: $txs: $e") e.msg result <- stateStream.last.compile.toList finalState <- EitherT.fromOption[F]( result.headOption.flatten.map(_._1._2), "Fail to get final state", ) _ <- StateRepository[F] .put(finalState) .leftMap: e => scribe.error(s"Fail to put state: $e") s"Fail to put state: ${e.msg}" yield scribe.info(s"Last: ${result.flatten}") () def fileResource[F[_]: Async](fileName: String): Resource[F, Source] = Resource.fromAutoCloseable: Async[F].delay(Source.fromFile(fileName)) object BulkInsertMain extends IOApp: val from = "1" // val from = "3513172" val until = "100000000" // val until = "3513173" override def run(args: List[String]): IO[ExitCode] = import com.typesafe.config.ConfigFactory import node.NodeMain import node.repository.StateRepository.given NodeConfig .load[IO](IO.blocking(ConfigFactory.load)) .value .flatMap: case Left(err) => IO(println(err)).as(ExitCode.Error) case Right(config) => scribe.info(s"Loaded config: $config") val program = for source <- fileResource[IO]("txs.archive") given BlockRepository[IO] <- NodeMain.getBlockRepo(config) given TransactionRepository[IO] <- NodeMain.getTransactionRepo(config) given StateRepository[IO] <- NodeMain.getStateRepo(config) given InvalidTxLogger[IO] <- InvalidTxLogger.file[IO]: "invalid-txs.csv" result <- Resource.eval: given PlayNommState[IO] = PlayNommState.build[IO] bulkInsert[IO](config, source, from, until).value.map: case Left(err) => scribe.error(s"Error: $err") ExitCode.Error case Right(_) => scribe.info(s"Done") ExitCode.Success yield result program.use(IO.pure) // fileResource[IO]("txs.archive") // .use: source => // NftBalanceState.build(source).flatTap: state => // IO.pure: // state.free.foreach(println) // state.locked.foreach(println) // FungibleBalanceState.build(source).flatTap: state => // IO.pure: // state.free.foreach(println) // state.locked.foreach(println) // .as(ExitCode.Success) ================================================ FILE: modules/bulk-insert/src/main/scala/io/leisuremeta/chain/bulkinsert/FungibleBalanceState.scala ================================================ package io.leisuremeta.chain package bulkinsert import scala.io.Source import cats.data.{EitherT} import cats.effect.{IO, Sync} import cats.syntax.all.* import fs2.Stream import io.circe.parser.decode import api.model.{Account, Signed, Transaction} import lib.crypto.Hash import lib.crypto.Hash.ops.* import lib.datatype.BigNat final case class FungibleBalanceState( free: Map[Account, Map[Signed.TxHash, (Signed.Tx, BigNat)]], locked: Map[Account, Map[Signed.TxHash, (Signed.Tx, BigNat, Account)]], ): def addFree( account: Account, tx: Signed.Tx, amount: BigNat, ): FungibleBalanceState = val txHash = tx.toHash val accountMap = free.getOrElse(account, Map.empty) val txMap = accountMap.updated(txHash, (tx, amount)) copy(free = free.updated(account, txMap)) def addLocked( account: Account, tx: Signed.Tx, amount: BigNat, from: Account, ): FungibleBalanceState = val txHash = tx.toHash val accountMap = locked.getOrElse(account, Map.empty) val txMap = accountMap.updated(txHash, (tx, amount, from)) copy(locked = locked.updated(account, txMap)) def removeFree( account: Account, txHash: Signed.TxHash, ): FungibleBalanceState = val accountMap = free.getOrElse(account, Map.empty) val txMap = accountMap.removed(txHash) copy(free = free.updated(account, txMap)) def removeLocked( account: Account, txHash: Signed.TxHash, ): FungibleBalanceState = val accountMap = locked.getOrElse(account, Map.empty) val txMap = accountMap.removed(txHash) copy(locked = locked.updated(account, txMap)) object FungibleBalanceState: def empty: FungibleBalanceState = FungibleBalanceState(Map.empty, Map.empty) def build( source: Source, ): IO[FungibleBalanceState] = val indexWithTxsStream = Stream .fromIterator[EitherT[IO, String, *]](source.getLines(), 1) .evalMap: line => line.split("\t").toList match case blockNumber :: txHash :: jsonString :: Nil => EitherT .fromEither[IO]: decode[Signed.Tx](jsonString) .leftMap: e => scribe.error(s"Error decoding line #$blockNumber: $txHash: $jsonString: $e") e.getMessage() .map(tx => (BigInt(blockNumber).longValue, tx)) case _ => scribe.error(s"Error parsing line: $line") EitherT.leftT[IO, (Long, Signed.Tx)](s"Error parsing line: $line") .groupAdjacentBy(_._1) .map: case (blockNumber, chunk) => (blockNumber, chunk.toList.map(_._2)) //val indexWithTxsStream = Stream // .fromIterator[EitherT[IO, String, *]](source.getLines(), 1) // .zipWithIndex // .filterNot(_._1 === "[]") // .evalMap: (line, index) => // EitherT // .fromEither[IO]: // decode[Seq[Signed.Tx]](line) // .leftMap: e => // scribe.error(s"Error decoding line #$index: $line: $e") // e.getMessage() // .map(txs => (index, txs)) def logWrongTx( from: Account, amount: BigInt, tx: Signed.Tx, inputs: Map[Signed.TxHash, BigNat], ): Unit = println(s"$from\t$amount\t${tx.value.toHash}\t$tx") inputs.foreach { (txHash, amount) => println(s"===> $txHash : $amount") } val stateStream = indexWithTxsStream.evalMapAccumulate[EitherT[ IO, String, *, ], FungibleBalanceState, (Long, Seq[Signed.Tx])]( FungibleBalanceState.empty, ): case (balanceState, (index, txs)) => // if index % 10000 === 0 then // println(s"Index: $index") val finalState = txs.foldLeft(balanceState): (state, tx) => tx.value match case fb: Transaction.FungibleBalance => fb match case mt: Transaction.TokenTx.MintFungibleToken => mt.outputs.foldLeft(state): case (state, (to, amount)) => state.addFree(to, tx, amount) case tt: Transaction.TokenTx.TransferFungibleToken => val inputList = tt.inputs.toList val inputAmounts = inputList.map: inputTxHash => state.free .get(tx.sig.account) .getOrElse(Map.empty) .get(inputTxHash) .fold { // scribe.error(s"input $inputTxHash is not exist in tx $txHash") BigNat.Zero }(_._2) val inputs = inputList.zip(inputAmounts).toMap val inputTotal = inputAmounts.fold(BigNat.Zero)(BigNat.add) val outputTotal = tt.outputs.map(_._2).fold(BigNat.Zero)(BigNat.add) val remainder = inputTotal.toBigInt - outputTotal.toBigInt if remainder < 0 then logWrongTx( tx.sig.account, -remainder, tx, inputs, ) val afterRemovingInput = tt.inputs.foldLeft(state): case (state, inputTxHash) => state.removeFree(tx.sig.account, inputTxHash) val afterAddingOutput = tt.outputs.foldLeft(afterRemovingInput): case (state, (to, amount)) => state.addFree(to, tx, amount) afterAddingOutput case bt: Transaction.TokenTx.BurnFungibleToken => val inputList = bt.inputs.toList val inputAmounts = inputList.map: inputTxHash => state.free .get(tx.sig.account) .getOrElse(Map.empty) .get(inputTxHash) .fold { // scribe.error(s"input $inputTxHash is not exist in tx $txHash") BigNat.Zero }(_._2) val inputs = inputList.zip(inputAmounts).toMap val inputTotal = inputAmounts.fold(BigNat.Zero)(BigNat.add) val burnAmount = bt.amount.toBigInt val remainder = inputTotal.toBigInt - burnAmount if remainder < 0 then logWrongTx( tx.sig.account, -remainder, tx, inputs, ) val afterRemovingInput = bt.inputs.foldLeft(state): case (state, inputTxHash) => state.removeFree(tx.sig.account, inputTxHash) afterRemovingInput.addFree( tx.sig.account, tx, BigNat.unsafeFromBigInt(remainder.max(0)), ) case et: Transaction.TokenTx.EntrustFungibleToken => val inputList = et.inputs.toList val inputAmounts = inputList.map: inputTxHash => state.free .get(tx.sig.account) .getOrElse(Map.empty) .get(inputTxHash) .fold { // scribe.error(s"input $inputTxHash is not exist in tx $txHash") BigNat.Zero }(_._2) val inputs = inputList.zip(inputAmounts).toMap val inputTotal = inputAmounts.fold(BigNat.Zero)(BigNat.add) val entrustAmount = et.amount.toBigInt val remainder = inputTotal.toBigInt - entrustAmount if remainder < 0 then logWrongTx( tx.sig.account, -remainder, tx, inputs, ) val afterRemovingInput = et.inputs.foldLeft(state): case (state, inputTxHash) => state.removeFree(tx.sig.account, inputTxHash) val afterAddingOutput = afterRemovingInput .addLocked(et.to, tx, et.amount, tx.sig.account) .addFree( tx.sig.account, tx, BigNat.unsafeFromBigInt(remainder.max(0)), ) afterAddingOutput case dt: Transaction.TokenTx.DisposeEntrustedFungibleToken => val inputList = dt.inputs.toList val inputAmounts = inputList.map: inputTxHash => state.locked .get(tx.sig.account) .getOrElse(Map.empty) .get(inputTxHash) .fold { // scribe.error(s"input $inputTxHash is not exist in tx $txHash") BigNat.Zero }(_._2) val inputs = inputList.zip(inputAmounts).toMap val inputTotal = inputAmounts.fold(BigNat.Zero)(BigNat.add) val outputTotal = dt.outputs.map(_._2).fold(BigNat.Zero)(BigNat.add) val remainder = inputTotal.toBigInt - outputTotal.toBigInt if remainder < 0 then logWrongTx( tx.sig.account, -remainder, tx, inputs, ) val afterRemovingInput = dt.inputs.foldLeft(state): case (state, inputTxHash) => state.removeLocked(tx.sig.account, inputTxHash) val afterAddingOutput = dt.outputs.foldLeft(afterRemovingInput): case (state, (to, amount)) => state.addFree(to, tx, amount) afterAddingOutput case or: Transaction.RewardTx.OfferReward => val inputList = or.inputs.toList val inputAmounts = inputList.map: inputTxHash => state.free .get(tx.sig.account) .getOrElse(Map.empty) .get(inputTxHash) .fold { // scribe.error(s"input $inputTxHash is not exist in tx $txHash") BigNat.Zero }(_._2) val inputs = inputList.zip(inputAmounts).toMap val inputTotal = inputAmounts.fold(BigNat.Zero)(BigNat.add) val outputTotal = or.outputs.map(_._2).fold(BigNat.Zero)(BigNat.add) val remainder = inputTotal.toBigInt - outputTotal.toBigInt if remainder < 0 then logWrongTx( tx.sig.account, -remainder, tx, inputs, ) val afterRemovingInput = or.inputs.foldLeft(state): case (state, inputTxHash) => state.removeFree(tx.sig.account, inputTxHash) val afterAddingOutput = or.outputs.foldLeft(afterRemovingInput): case (state, (to, amount)) => state.addFree(to, tx, amount) afterAddingOutput case er: Transaction.RewardTx.ExecuteReward => ??? case er: Transaction.RewardTx.ExecuteOwnershipReward => ??? case _ => state EitherT.pure[IO, String]((finalState, (index, txs))) stateStream.last.compile.toList .map(_.headOption.flatten.get._1) .value .map: case Left(err) => scribe.error(s"Error building balance map: $err") FungibleBalanceState.empty case Right(balanceState) => balanceState ================================================ FILE: modules/bulk-insert/src/main/scala/io/leisuremeta/chain/bulkinsert/InvalidTx.scala ================================================ package io.leisuremeta.chain package bulkinsert import java.time.Instant import cats.effect.{Async, Resource} import cats.effect.std.Console import api.model.{Account, Transaction} import api.model.token.{TokenId} import lib.datatype.BigNat import lib.crypto.Hash.ops.* final case class InvalidTx( signer: Account, reason: InvalidReason, amountToBurn: BigNat, tx: Transaction, wrongNftInput: Option[TokenId] = None, createdAt: Instant, memo: String = "", ): def txType: String = tx.getClass.getSimpleName enum InvalidReason: case OutputMoreThanInput, InputAlreadyUsed, BalanceNotExist, CanceledBalance, NoNftInfo trait InvalidTxLogger[F[_]]: def log(invalidTx: InvalidTx): F[Unit] object InvalidTxLogger: def apply[F[_]: InvalidTxLogger]: InvalidTxLogger[F] = summon def console[F[_]: Async: Console]: InvalidTxLogger[F] = invalidTx => Console[F].println(invalidTx) def file[F[_]: Async](filename: String): Resource[F, InvalidTxLogger[F]] = Resource .make: import java.io.{File, FileOutputStream, PrintWriter} Async[F].delay( new PrintWriter(new FileOutputStream(new File(filename), true)), ) .apply: out => Async[F].delay: out.flush() out.close() .map: out => case InvalidTx(signer, reason, amountToBurn, tx, wrongNftInput, createdAt, memo) => Async[F].delay: val fields = Seq( createdAt, tx.toHash.toUInt256Bytes.toHex, tx.getClass.getSimpleName, signer, reason, amountToBurn, wrongNftInput, memo, ) out.println(fields.mkString(",")) out.flush() ================================================ FILE: modules/bulk-insert/src/main/scala/io/leisuremeta/chain/bulkinsert/NftBalanceState.scala ================================================ package io.leisuremeta.chain package bulkinsert import scala.io.Source import cats.data.EitherT import cats.effect.IO import cats.syntax.all.* import fs2.Stream import io.circe.parser.decode import api.model.* import api.model.token.* import lib.crypto.Hash.ops.* import lib.datatype.Utf8 final case class NftBalanceState( free: Map[Account, Map[Signed.TxHash, (Signed.Tx, TokenId)]], locked: Map[Account, Map[Signed.TxHash, (Signed.Tx, TokenId, Account)]], tokenOwner: Map[TokenId, Account], ): def addFree( account: Account, tx: Signed.Tx, tokenId: TokenId, ): NftBalanceState = val txHash = tx.toHash val accountMap = free.getOrElse(account, Map.empty) val txMap = accountMap.updated(txHash, (tx, tokenId)) val tokenOwner1 = tokenOwner.updated(tokenId, account) copy(free = free.updated(account, txMap), tokenOwner = tokenOwner1) def addLocked( account: Account, tx: Signed.Tx, tokenId: TokenId, from: Account, ): NftBalanceState = val txHash = tx.toHash val accountMap = locked.getOrElse(account, Map.empty) val txMap = accountMap.updated(txHash, (tx, tokenId, from)) copy(locked = locked.updated(account, txMap)) def removeFree( account: Account, txHash: Signed.TxHash, ): NftBalanceState = val accountMap = free.getOrElse(account, Map.empty) val txMap = accountMap.removed(txHash) val tokenOwner1 = accountMap.get(txHash) .fold: // println(s"No locked balance of account $account with $txHash") tokenOwner .apply: (_, tokenId) => tokenOwner.removed(tokenId) copy(free = free.updated(account, txMap), tokenOwner = tokenOwner1) def removeLocked( account: Account, txHash: Signed.TxHash, ): NftBalanceState = val accountMap = locked.getOrElse(account, Map.empty) val txMap = accountMap.removed(txHash) val tokenOwner1 = accountMap.get(txHash) .fold: // println(s"No locked balance of account $account with $txHash") tokenOwner .apply: (_, tokenId, _) => tokenOwner.removed(tokenId) copy(locked = locked.updated(account, txMap), tokenOwner = tokenOwner1) object NftBalanceState: def empty: NftBalanceState = NftBalanceState(Map.empty, Map.empty, Map.empty) def build( source: Source, ): IO[NftBalanceState] = val indexWithTxsStream = Stream .fromIterator[EitherT[IO, String, *]](source.getLines(), 1) .evalMap: line => line.split("\t").toList match case blockNumber :: txHash :: jsonString :: Nil => EitherT .fromEither[IO]: decode[Signed.Tx](jsonString) .leftMap: e => scribe.error(s"Error decoding line #$blockNumber: $txHash: $jsonString: $e") e.getMessage() .map(tx => (BigInt(blockNumber).longValue, tx)) case _ => scribe.error(s"Error parsing line: $line") EitherT.leftT[IO, (Long, Signed.Tx)](s"Error parsing line: $line") .groupAdjacentBy(_._1) .map: case (blockNumber, chunk) => (blockNumber, chunk.toList.map(_._2)) // val indexWithTxsStream = Stream // .fromIterator[EitherT[IO, String, *]](source.getLines(), 1) // .zipWithIndex // .filterNot(_._1 === "[]") // .evalMap: (line, index) => // EitherT // .fromEither[IO]: // decode[Seq[Signed.Tx]](line) // .leftMap: e => // scribe.error(s"Error decoding line #$index: $line: $e") // e.getMessage() // .map(txs => (index, txs)) def logWrongTx( from: Account, tokenId: TokenId, tx: Signed.Tx, ): Unit = println(s"No free NFT balance of $from with tokenId $tokenId: ${tx.toHash}: $tx") () def logWrongEntrustTx( from: Account, tokenId: TokenId, tx: Signed.Tx, ): Unit = println(s"No entrust NFT balance of $from with tokenId $tokenId: ${tx.toHash}: $tx") () val stateStream = indexWithTxsStream.evalMapAccumulate[EitherT[IO, String, *], NftBalanceState, (Long, Seq[Signed.Tx])](NftBalanceState.empty): case (balanceState, (index, txs)) => val finalState = txs.foldLeft(balanceState): (state, tx) => tx.value match case mn: Transaction.TokenTx.MintNFT => state.addFree(mn.output, tx, mn.tokenId) case bn: Transaction.TokenTx.BurnNFT => state.removeFree(tx.sig.account, bn.input) case tn: Transaction.TokenTx.TransferNFT => val inputOption = for nftBalance <- state.free.get(tx.sig.account) txAndTokenId <- nftBalance.get(tn.input) yield txAndTokenId inputOption match case Some((inputTx, tokenId)) => state .removeFree(tx.sig.account, tn.input) .addFree(tn.output, tx, tokenId) case None => if tx.sig.account === Account(Utf8.unsafeFrom("playnomm")) then val currentOwnerOption = state.free.toSeq .flatMap: (account, map) => map.toSeq.map: case (txHash, (tx, tokenId)) => (tokenId, account, txHash) .find(_._1 === tn.tokenId) currentOwnerOption .fold(state): (_, owner, txHash) => state.removeFree(owner, txHash) .addFree(tn.output, tx, tn.tokenId) else logWrongTx(tx.sig.account, tn.tokenId, tx) state case en: Transaction.TokenTx.EntrustNFT => val inputOption = for nftBalance <- state.free.get(tx.sig.account) txAndTokenId <- nftBalance.find(_._2._2 === en.tokenId).map(_._2) yield txAndTokenId // if en.tokenId.utf8.value === "2022101110000890000000405" then // scribe.info(s"EntrustNFT(${tx.toHash}): ${tx}") // scribe.info(s"Balance of signer: ${state.free.get(tx.sig.account)}") // scribe.info(s"input: $inputOption") inputOption match case Some((inputTx, tokenId)) => state .removeFree(tx.sig.account, en.input) .addLocked(en.to, tx, tokenId, tx.sig.account) case None => logWrongTx(tx.sig.account, en.tokenId, tx) state case dn: Transaction.TokenTx.DisposeEntrustedNFT => val inputOption = for nftBalance <- state.locked.get(tx.sig.account) txTokenIdAndFrom <- nftBalance.get(dn.input) yield txTokenIdAndFrom inputOption match case Some((inputTx, tokenId, from)) => // Remove the NFT from the locked state state .removeLocked(tx.sig.account, dn.input) // Add the NFT to the free state .addFree(dn.output.getOrElse(from), tx, tokenId) case None => // If the NFT is not in the locked state, check the free state val to = for output <- dn.output nftBalance <- state.free.get(output) txAndTokenId <- nftBalance.find(_._2._2 === dn.tokenId).map(_._2) yield txAndTokenId // If the NFT is not in the free state, check if the transaction is from the game owner if to.isEmpty then if tx.sig.account === Account(Utf8.unsafeFrom("playnomm")) then // If the transaction is from the game owner, add the NFT to the free state val currentOwnerOption = state.free.toSeq .flatMap: (account, map) => map.toSeq.map: case (txHash, (tx, tokenId)) => (tokenId, account, txHash) .find(_._1 === dn.tokenId) val state1 = currentOwnerOption .fold(state): (_, owner, txHash) => state.removeFree(owner, txHash) dn.output.fold(state1): output => state1.addFree(output, tx, dn.tokenId) else // If the transaction is not from the game owner, log an error logWrongEntrustTx(tx.sig.account, dn.tokenId, tx) state else // If the NFT is in the free state, return the existing state state case _ => state EitherT.pure[IO, String]((finalState, (index, txs))) stateStream.last.compile.toList .map(_.headOption.flatten.get._1) .value .map: case Left(err) => scribe.error(s"Error building balance map: $err") NftBalanceState.empty case Right(balanceState) => balanceState ================================================ FILE: modules/bulk-insert/src/main/scala/io/leisuremeta/chain/bulkinsert/RecoverTx.scala ================================================ package io.leisuremeta.chain package bulkinsert import java.time.temporal.ChronoUnit import cats.Monad import cats.data.{EitherT, OptionT, StateT} import cats.effect.Async import cats.syntax.all.* import api.model.{ Account, AccountData, PublicKeySummary, Signed, Transaction, TransactionWithResult, } import api.model.TransactionWithResult.ops.* import api.model.account.{ExternalChain, ExternalChainAddress} import api.model.token.{NftState, Rarity, TokenDefinitionId, TokenId} import lib.codec.byte.ByteEncoder.ops.* import lib.crypto.Hash import lib.crypto.Hash.ops.* import lib.datatype.{BigNat, Utf8} import lib.merkle.MerkleTrieState import node.dapp.{PlayNommDApp, PlayNommDAppFailure, PlayNommState} import node.dapp.submodule.* import node.repository.TransactionRepository object RecoverTx: def apply[F[_]: Async: TransactionRepository: PlayNommState: InvalidTxLogger]( ms: MerkleTrieState, signedTx: Signed.Tx, ): EitherT[ F, PlayNommDAppFailure, (MerkleTrieState, Option[TransactionWithResult]), ] = val sig = signedTx.sig val tx = signedTx.value tx match case ca: Transaction.AccountTx.CreateAccount => val program = for accountInfoOption <- PlayNommDAppAccount.getAccountInfo( ca.account, ) // _ <- checkExternal( // accountInfoOption.isEmpty, // s"${ca.account} already exists", // ) // _ <- checkExternal( // sig.account == ca.account || // Some(sig.account) == ca.guardian, // s"Signer ${sig.account} is neither ${ca.account} nor its guardian", // ) initialPKS <- PlayNommDAppAccount.getPKS(sig, ca) keyInfo = PublicKeySummary.Info( addedAt = ca.createdAt, description = Utf8.unsafeFrom(s"automatically added at account creation"), expiresAt = Some(ca.createdAt.plus(40, ChronoUnit.DAYS)), ) _ <- if Option(sig.account) === ca.guardian then unit else PlayNommState[F].account.key .put((ca.account, initialPKS), keyInfo) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to put account key ${ca.account}" accountData = AccountData( guardian = ca.guardian, externalChainAddresses = ca.ethAddress.fold(Map.empty): ethAddress => Map( ExternalChain.ETH -> ExternalChainAddress(ethAddress.utf8), ) , lastChecked = ca.createdAt, memo = None, ) _ <- PlayNommState[F].account.name .put(ca.account, accountData) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to put account ${ca.account}" _ <- ca.ethAddress.fold(unit): ethAddress => PlayNommState[F].account.externalChainAddresses .put( (ExternalChain.ETH, ExternalChainAddress(ethAddress.utf8)), ca.account, ) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to update eth address ${ca.ethAddress}" yield TransactionWithResult(Signed(sig, ca))(None) program .run(ms) .map: (ms, txWithResult) => (ms, Option(txWithResult)) .leftMap: msg => PlayNommDAppFailure.internal(s"Fail to recover error: $msg") case ua: Transaction.AccountTx.UpdateAccount => val program = for accountDataOption <- PlayNommDAppAccount.getAccountInfo( ua.account, ) accountData <- fromOption( accountDataOption, s"${ua.account} does not exists", ) // _ <- checkExternal( // sig.account == ua.account || // Some(sig.account) == accountData.guardian, // s"Signer ${sig.account} is neither ${ua.account} nor its guardian", // ) accountData1 = accountData.copy( guardian = ua.guardian, externalChainAddresses = ua.ethAddress.fold(Map.empty): ethAddress => Map( ExternalChain.ETH -> ExternalChainAddress(ethAddress.utf8), ) , lastChecked = ua.createdAt, memo = None, ) _ <- PlayNommState[F].account.name .put(ua.account, accountData1) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to put account ${ua.account}" _ <- ua.ethAddress.fold(unit): ethAddress => PlayNommState[F].account.externalChainAddresses .put( (ExternalChain.ETH, ExternalChainAddress(ethAddress.utf8)), ua.account, ) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to update eth address ${ua.ethAddress}" yield TransactionWithResult(Signed(sig, ua))(None) program .run(ms) .map: (ms, txWithResult) => (ms, Option(txWithResult)) .leftMap: msg => PlayNommDAppFailure.internal(s"Fail to recover error: $msg") // case ap: Transaction.AccountTx.AddPublicKeySummaries => // val program = for // account <- PlayNommState[F].account.name // .get(ap.account) // .map: account => // scribe.info(s"Account Data: $account") // account // _ <- StateT.liftF: // EitherT.leftT[F, (MerkleTrieState, TransactionWithResult)]: // s"Not recovered yet: ${e.msg}" // yield TransactionWithResult(signedTx)(None) // // program // .run(ms) // .map: (ms, txWithResult) => // (ms, Option(txWithResult)) // .leftMap: msg => // PlayNommDAppFailure.internal(s"Fail to recover error: $msg") case tf: Transaction.TokenTx.TransferFungibleToken => val sig = signedTx.sig // val tx = signedTx.value val program = for // _ <- PlayNommDAppAccount.verifySignature(sig, tx) tokenDef <- PlayNommDAppToken.getTokenDefinition( tf.tokenDefinitionId, ) inputAmount <- PlayNommDAppToken.getFungibleBalanceTotalAmounts( tf.inputs.map(_.toResultHashValue), sig.account, ) outputAmount = tf.outputs.values.foldLeft(BigNat.Zero)(BigNat.add) diffBigInt = inputAmount.toBigInt - outputAmount.toBigInt _ <- StateT.liftF: if diffBigInt < 0 then // scribe.info(s"DiffBigInt: $diffBigInt") EitherT.right: InvalidTxLogger[F].log: InvalidTx( signer = sig.account, reason = InvalidReason.OutputMoreThanInput, amountToBurn = BigNat.unsafeFromBigInt(diffBigInt.abs), tx = tf, createdAt = tf.createdAt, ) else EitherT.pure(()) txWithResult = TransactionWithResult(Signed(sig, tf))(None) txHash = txWithResult.toHash invalidInputs <- removeInputUtxos( sig.account, tf.inputs.map(_.toResultHashValue), tf.tokenDefinitionId, ) _ <- StateT.liftF: if invalidInputs.isEmpty then EitherT.pure(()) else invalidInputs .traverse(TransactionRepository[F].get) .leftMap(e => PlayNommDAppFailure.internal(s"Fail to get tx: $e"), ) .semiflatMap: txOptions => val sum = txOptions .map: txOption => txOption.fold(BigNat.Zero)( PlayNommDAppToken.tokenBalanceAmount(sig.account), ) .foldLeft(BigNat.Zero)(BigNat.add) // scribe.info(s"Sum of Invalid Tx Inputs: $sum") InvalidTxLogger[F].log: InvalidTx( signer = sig.account, reason = InvalidReason.InputAlreadyUsed, amountToBurn = sum, tx = tf, createdAt = tf.createdAt, ) _ <- tf.inputs.toList.traverse: inputTxHash => PlayNommDAppToken .removeFungibleSnapshot( sig.account, tf.tokenDefinitionId, inputTxHash.toResultHashValue, ) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to remove fungible snapshot of $inputTxHash" _ <- tf.outputs.toSeq.traverse: case (account, _) => PlayNommDAppToken.putBalance( account, tf.tokenDefinitionId, txHash, ) *> PlayNommDAppToken .addFungibleSnapshot( account, tf.tokenDefinitionId, txHash, outputAmount, ) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to add fungible snapshot of $account" totalAmount <- fromEitherInternal: val either = BigNat.fromBigInt(tokenDef.totalAmount.toBigInt - diffBigInt) // if either.isLeft then // scribe.info(s"Total Amount: \t${tokenDef.totalAmount}") // scribe.info(s"DiffBigInt: \t$diffBigInt") // scribe.info(s"Input Amount: \t$inputAmount") // scribe.info(s"Output Amount: \t$outputAmount") // scribe.info(s"Diff: \t$diffBigInt") // scribe.info(s"Either: \t$either") either _ <- PlayNommDAppToken.putTokenDefinition( tf.tokenDefinitionId, tokenDef.copy(totalAmount = totalAmount), ) diffEither = BigNat.fromBigInt(diffBigInt) _ <- diffEither match case Right(diff) => PlayNommDAppToken .removeTotalSupplySnapshot(tf.tokenDefinitionId, diff) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to remove total supply snapshot of ${tf.tokenDefinitionId}" case Left(_) => PlayNommDAppToken .addTotalSupplySnapshot( tf.tokenDefinitionId, BigNat.unsafeFromBigInt(diffBigInt.abs), ) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to add total supply snapshot of ${tf.tokenDefinitionId}" yield txWithResult program .run(ms) .map: (ms, txWithResult) => (ms, Option(txWithResult)) .leftMap: msg => PlayNommDAppFailure.internal(s"Fail to recover error: $msg") case mn: Transaction.TokenTx.MintNFT => val program = for tokenDefOption <- PlayNommState[F].token.definition .get(mn.tokenDefinitionId) .mapK: PlayNommDAppFailure.mapExternal: s"No token definition of ${mn.tokenDefinitionId}" nftStateOption <- PlayNommState[F].token.nftState .get(mn.tokenId) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to get nft state of ${mn.tokenId}" // _ <- checkExternal( // nftStateOption.isEmpty, // s"NFT ${mn.tokenId} is already minted", // ) txWithResult = TransactionWithResult(Signed(sig, mn))(None) txHash = txWithResult.toHash _ <- PlayNommState[F].token.nftBalance .put((mn.output, mn.tokenId, txHash), ()) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to put token balance (${mn.output}, ${mn.tokenId}, $txHash)" _ <- PlayNommDAppToken .addNftSnapshot(mn.output, mn.tokenDefinitionId, mn.tokenId) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to add nft snapshot of ${mn.tokenId}" rarity: Map[Rarity, BigNat] = val rarityMapOption: Option[Map[Rarity, BigNat]] = for tokenDef <- tokenDefOption nftInfo <- tokenDef.nftInfo yield nftInfo.rarity rarityMapOption.getOrElse: Map( Rarity(Utf8.unsafeFrom("LGDY")) -> BigNat .unsafeFromLong(16), Rarity(Utf8.unsafeFrom("UNIQ")) -> BigNat .unsafeFromLong(12), Rarity(Utf8.unsafeFrom("EPIC")) -> BigNat.unsafeFromLong(8), Rarity(Utf8.unsafeFrom("RARE")) -> BigNat.unsafeFromLong(4), ) weight = rarity.getOrElse(mn.rarity, BigNat.unsafeFromLong(2L)) nftState = NftState( tokenId = mn.tokenId, tokenDefinitionId = mn.tokenDefinitionId, rarity = mn.rarity, weight = weight, currentOwner = mn.output, memo = None, lastUpdateTx = txHash, previousState = None, ) _ <- PlayNommState[F].token.nftState .put(mn.tokenId, nftState) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to put nft state of ${mn.tokenId}" _ <- PlayNommState[F].token.nftHistory .put(txHash, nftState) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to put nft history of ${mn.tokenId} of $txHash" _ <- PlayNommState[F].token.rarityState .put((mn.tokenDefinitionId, mn.rarity, mn.tokenId), ()) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to put rarity state of ${mn.tokenDefinitionId}, ${mn.rarity}, ${mn.tokenId}" yield txWithResult program .run(ms) .map: (ms, txWithResult) => (ms, Option(txWithResult)) .leftMap: msg => PlayNommDAppFailure.internal(s"Fail to recover error: $msg") case tn: Transaction.TokenTx.TransferNFT => val sig = signedTx.sig val txWithResult = TransactionWithResult(Signed(sig, tn))(None) val txHash = txWithResult.toHash val utxoKey = (sig.account, tn.tokenId, tn.input.toResultHashValue) val program = for isRemoveSuccessful <- PlayNommState[F].token.nftBalance .remove(utxoKey) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to remove nft balance of $utxoKey" _ <- if isRemoveSuccessful then StateT.liftF(EitherT.pure(())) else if sig.account === Account(Utf8.unsafeFrom("playnomm")) then removePreviousNftBalance(tn.tokenId, signedTx) else StateT.liftF: EitherT.right: InvalidTxLogger[F].log: InvalidTx( signer = sig.account, reason = InvalidReason.BalanceNotExist, amountToBurn = BigNat.Zero, tx = tn, wrongNftInput = Some(tn.tokenId), createdAt = tn.createdAt, ) newUtxoKey = (tn.output, tn.tokenId, txHash) _ <- PlayNommState[F].token.nftBalance .put(newUtxoKey, ()) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to put nft balance of $newUtxoKey" nftStateOption <- PlayNommState[F].token.nftState .get(tn.tokenId) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to get nft state of ${tn.tokenId}" nftState <- fromOption( nftStateOption, s"Empty NFT State: ${tn.tokenId}", ) nftState1 = nftState.copy(currentOwner = tn.output) _ <- PlayNommState[F].token.nftState .put(tn.tokenId, nftState1) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to put nft state of ${tn.tokenId}" yield txWithResult program .run(ms) .map: (ms, txWithResult) => (ms, Option(txWithResult)) .leftMap: msg => PlayNommDAppFailure.internal(s"Fail to recover error: $msg") case bf: Transaction.TokenTx.BurnFungibleToken => val program = for // _ <- PlayNommDAppAccount.verifySignature(sig, tx) tokenDef <- PlayNommDAppToken.getTokenDefinition(bf.definitionId) inputAmount <- PlayNommDAppToken.getFungibleBalanceTotalAmounts( bf.inputs.map(_.toResultHashValue), sig.account, ) outputAmount <- fromEitherExternal: BigNat.tryToSubtract(inputAmount, bf.amount) result = Transaction.TokenTx.BurnFungibleTokenResult(outputAmount) txWithResult = TransactionWithResult(Signed(sig, bf))(Some(result)) txHash = txWithResult.toHash _ <- removeInputUtxos( sig.account, bf.inputs.map(_.toResultHashValue), bf.definitionId, ) _ <- bf.inputs.toList.traverse: inputTxHash => PlayNommDAppToken .removeFungibleSnapshot( sig.account, bf.definitionId, inputTxHash.toResultHashValue, ) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to remove fungible snapshot of $inputTxHash" _ <- if outputAmount === BigNat.Zero then unit else PlayNommDAppToken.putBalance( sig.account, bf.definitionId, txWithResult.toHash, ) *> PlayNommDAppToken .addFungibleSnapshot( sig.account, bf.definitionId, txWithResult.toHash, outputAmount, ) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to add fungible snapshot of ${sig.account}" totalAmount <- fromEitherInternal: BigNat.tryToSubtract(tokenDef.totalAmount, bf.amount) _ <- PlayNommDAppToken.putTokenDefinition( bf.definitionId, tokenDef.copy(totalAmount = totalAmount), ) _ <- PlayNommDAppToken .removeTotalSupplySnapshot(bf.definitionId, bf.amount) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to remove total supply snapshot of ${bf.definitionId}" yield txWithResult program .run(ms) .map: (ms, txWithResult) => (ms, Option(txWithResult)) .leftMap: msg => PlayNommDAppFailure.internal(s"Fail to recover error: $msg") case bn: Transaction.TokenTx.BurnNFT => val program = for // _ <- PlayNommDAppAccount.verifySignature(sig, tx) tokenId <- getNftTokenId(bn.input.toResultHashValue) txWithResult = TransactionWithResult(Signed(sig, bn))(None) txHash = txWithResult.toHash utxoKey = (sig.account, tokenId, bn.input.toResultHashValue) isRemoveSuccessful <- PlayNommState[F].token.nftBalance .remove(utxoKey) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to remove nft balance of $utxoKey" _ <- if isRemoveSuccessful then StateT.liftF(EitherT.pure(())) else StateT.liftF: EitherT.right: InvalidTxLogger[F].log: InvalidTx( signer = sig.account, reason = InvalidReason.BalanceNotExist, amountToBurn = BigNat.Zero, tx = bn, wrongNftInput = Some(tokenId), createdAt = bn.createdAt, ) nftStateOption <- PlayNommState[F].token.nftState .get(tokenId) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to get nft state of ${tokenId}" _ <- nftStateOption.traverse: nftState => for _ <- PlayNommState[F].token.nftState .remove(tokenId) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to remove nft state of ${tokenId}" _ <- PlayNommState[F].token.rarityState .remove((bn.definitionId, nftState.rarity, tokenId)) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to remove rarity state of ${tokenId}" yield () _ <- PlayNommDAppToken .removeNftSnapshot[F](sig.account, bn.definitionId, tokenId) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to remove nft snapshot of ${tokenId}" yield txWithResult program .run(ms) .map: (ms, txWithResult) => (ms, Option(txWithResult)) .leftMap: msg => PlayNommDAppFailure.internal(s"Fail to recover error: $msg") case ef: Transaction.TokenTx.EntrustFungibleToken => val sig = signedTx.sig // val tx = signedTx.value val program = for // _ <- PlayNommDAppAccount.verifySignature(sig, tx) tokenDef <- PlayNommDAppToken.getTokenDefinition(ef.definitionId) inputAmount <- PlayNommDAppToken.getFungibleBalanceTotalAmounts( ef.inputs.map(_.toResultHashValue), sig.account, ) diffBigInt = inputAmount.toBigInt - ef.amount.toBigInt diff <- StateT.liftF: if diffBigInt < 0 then EitherT.right: InvalidTxLogger[F] .log: InvalidTx( signer = sig.account, reason = InvalidReason.OutputMoreThanInput, amountToBurn = BigNat.unsafeFromBigInt(diffBigInt.abs), tx = ef, createdAt = ef.createdAt, ) .as(BigNat.Zero) else EitherT .fromEither(BigNat.fromBigInt(diffBigInt)) .leftMap: msg => PlayNommDAppFailure.internal( s"Fail to convert diff to BigNat: $msg", ) result = Transaction.TokenTx.EntrustFungibleTokenResult(diff) txWithResult = TransactionWithResult(Signed(sig, ef))( Some(result), ) txHash = txWithResult.toHash invalidInputs <- removeInputUtxos( sig.account, ef.inputs.map(_.toResultHashValue), ef.definitionId, ) _ <- StateT.liftF: if invalidInputs.isEmpty then EitherT.pure(()) else invalidInputs .traverse(TransactionRepository[F].get) .leftMap: e => PlayNommDAppFailure.internal(s"Fail to get tx: $e") .semiflatMap: txOptions => val sum = txOptions .map: txOption => txOption.fold(BigNat.Zero): PlayNommDAppToken.tokenBalanceAmount(sig.account) .foldLeft(BigNat.Zero)(BigNat.add) InvalidTxLogger[F].log: InvalidTx( signer = sig.account, reason = InvalidReason.InputAlreadyUsed, amountToBurn = sum, tx = ef, createdAt = ef.createdAt, ) _ <- PlayNommState[F].token.entrustFungibleBalance .put((sig.account, ef.to, ef.definitionId, txHash), ()) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to put entrust fungible balance of (${sig.account}, ${ef.to}, ${ef.definitionId}, ${txHash})" _ <- PlayNommDAppToken.putBalance( sig.account, ef.definitionId, txHash, ) yield txWithResult program .run(ms) .map: (ms, txWithResult) => (ms, Option(txWithResult)) .leftMap: msg => PlayNommDAppFailure.internal(s"Fail to recover error: $msg") case ef: Transaction.TokenTx.EntrustNFT => val txWithResult = TransactionWithResult(Signed(sig, ef))(None) val txHash = txWithResult.toHash val program = for // _ <- PlayNommDAppAccount.verifySignature(sig, tx) inputTxHashes <- PlayNommState[F].token.nftBalance .streamWithPrefix((sig.account, ef.tokenId).toBytes) .flatMapF: stream => stream.map(_._1._3).compile.toList .mapK: PlayNommDAppFailure.mapInternal: s"Fail to get nft balance stream of (${sig.account}, ${ef.tokenId})" txWithResultOption <- inputTxHashes.headOption match case None => // scribe.info(s"No input tx hash found for ${sig.account} and ${ef.tokenId}") StateT.liftF: EitherT.right: InvalidTxLogger[F] .log: InvalidTx( signer = sig.account, reason = InvalidReason.BalanceNotExist, amountToBurn = BigNat.Zero, tx = ef, wrongNftInput = Some(ef.tokenId), createdAt = ef.createdAt, ) .map(_ => None) case Some(inputTxHash) => // scribe.info(s"Input tx hash found for ${sig.account} and ${ef.tokenId}: $inputTxHash") val utxoKey = (sig.account, ef.tokenId, inputTxHash) for isRemoveSuccessful <- PlayNommState[F].token.nftBalance .remove(utxoKey) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to remove nft balance of $utxoKey" _ <- StateT.liftF: // scribe.info(s"Is remove successful: $isRemoveSuccessful") if !isRemoveSuccessful then EitherT.right: InvalidTxLogger[F].log: InvalidTx( signer = sig.account, reason = InvalidReason.BalanceNotExist, amountToBurn = BigNat.Zero, tx = ef, wrongNftInput = Some(ef.tokenId), createdAt = ef.createdAt, ) else EitherT.pure(()) newUtxoKey = (sig.account, ef.to, ef.tokenId, txHash) _ <- PlayNommState[F].token.entrustNftBalance .put(newUtxoKey, ()) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to put entrust nft balance of $newUtxoKey" yield // scribe.info(s"Succeed to recover EntrustNFT: $txWithResult") Some(txWithResult) yield txWithResultOption program .run(ms) .leftMap: msg => PlayNommDAppFailure.internal(s"Fail to recover error: $msg") case de: Transaction.TokenTx.DisposeEntrustedFungibleToken => val program = for // _ <- PlayNommDAppAccount.verifySignature(sig, tx) tokenDef <- PlayNommDAppToken.getTokenDefinition(de.definitionId) inputHashList = de.inputs.map(_.toResultHashValue) inputMap <- PlayNommDAppToken.getEntrustedInputs( inputHashList, sig.account, ) inputAmount = inputMap.values.foldLeft(BigNat.Zero)(BigNat.add) outputAmount = de.outputs.values.foldLeft(BigNat.Zero)(BigNat.add) // _ <- checkExternal( // inputAmount === outputAmount, // s"Output amount is not equal to input amount $inputAmount", // ) txWithResult = TransactionWithResult(Signed(sig, de))(None) txHash = txWithResult.toHash _ <- inputMap.toList .map(_._1) .traverse: (account, txHash) => PlayNommState[F].token.entrustFungibleBalance .remove((account, sig.account, de.definitionId, txHash)) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to remove entrust fungible balance of (${account}, ${sig.account}, ${de.definitionId}, ${txHash})" *> PlayNommDAppToken .removeFungibleSnapshot[F](account, de.definitionId, txHash) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to remove fungible snapshot of $txHash" _ <- de.outputs.toList.traverse: (account, amount) => PlayNommState[F].token.fungibleBalance .put((account, de.definitionId, txHash), ()) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to put fungible balance of (${account}, ${de.definitionId}, ${txHash})" *> PlayNommDAppToken .addFungibleSnapshot[F]( account, de.definitionId, txHash, amount, ) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to put fungible snapshot of $txHash" yield txWithResult program .run(ms) .map: (ms, txWithResult) => (ms, Option(txWithResult)) .leftMap: msg => PlayNommDAppFailure.internal(s"Fail to recover error: $msg") case de: Transaction.TokenTx.DisposeEntrustedNFT => val txWithResult = TransactionWithResult(Signed(sig, de))(None) val txHash = txWithResult.toHash val program = for inputOption <- StateT.liftF: TransactionRepository[F] .get(de.input.toResultHashValue) .leftMap: e => PlayNommDAppFailure.internal: s"Fail to get tx ${de.input}: ${e.msg}" .map: txOption => txOption.flatMap: txWithResult => txWithResult.signedTx.value match case ef: Transaction.TokenTx.EntrustNFT if ef.to === sig.account => Some( ( txWithResult, ef.tokenId, txWithResult.signedTx.sig.account, ), ) case _ => None txWithResultOption <- inputOption match case Some((inputTx, tokenId, from)) => val utxoKey = ( from, sig.account, de.tokenId, de.input.toResultHashValue, ) for isRemoveSuccessful <- PlayNommState[ F, ].token.entrustNftBalance .remove(utxoKey) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to remove entrust nft balance of $utxoKey" _ <- if isRemoveSuccessful then PlayNommDAppToken .removeNftSnapshot[F](from, de.definitionId, de.tokenId) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to remove nft snapshot of ${de.tokenId}" else for _ <- removePreviousNftBalance[F](de.tokenId, signedTx) _ <- StateT .liftF: EitherT.right: InvalidTxLogger[F].log: InvalidTx( signer = sig.account, reason = InvalidReason.BalanceNotExist, amountToBurn = BigNat.Zero, tx = de, wrongNftInput = Some(de.tokenId), createdAt = de.createdAt, ) _ <- PlayNommDAppToken .removeNftSnapshot[F](from, de.definitionId, de.tokenId) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to remove nft snapshot of ${de.tokenId}" yield () toAccount = de.output.getOrElse(from) newUtxoKey = (toAccount, de.tokenId, txHash) _ <- PlayNommState[F].token.nftBalance .put(newUtxoKey, ()) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to put nft balance of $newUtxoKey" _ <- PlayNommDAppToken .addNftSnapshot[F]( de.output.getOrElse(from), de.definitionId, de.tokenId, ) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to add nft snapshot of ${de.tokenId}" nftStateOption <- PlayNommState[F].token.nftState .get(de.tokenId) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to get nft state of ${de.tokenId}" nftState <- fromOption( nftStateOption, s"Empty NFT State: ${de.tokenId}", ) nftState1 = nftState.copy(currentOwner = toAccount) _ <- PlayNommState[F].token.nftState .put(de.tokenId, nftState1) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to put nft state of ${de.tokenId}" yield Option(txWithResult) case None => for to <- de.output.traverse: output => PlayNommState[F].token.nftBalance .streamWithPrefix(output.toBytes) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to get nft balance of ${output}" .flatMapF: stream => stream .filter: case ((account, tokenId, txHash), _) => tokenId == de.tokenId .head .compile .toList .map: list => list.headOption.map(_._1._1) .leftMap: msg => PlayNommDAppFailure.internal: s"Fail to get nft balance stream: $msg" txWithResultOption <- to.flatten match case Some(account) => unit[F].map: _ => None case None => for stateOption <- PlayNommState[F].token.nftState .get(de.tokenId) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to get nft state of ${de.tokenId}" currentOwnerOption = stateOption.map(_.currentOwner) yield Some(txWithResult) yield txWithResultOption yield txWithResultOption program .run(ms) .leftMap: msg => PlayNommDAppFailure.internal(s"Fail to recover error: $msg") case tf: Transaction.RewardTx.OfferReward => val sig = signedTx.sig // val tx = signedTx.value val program = for // _ <- PlayNommDAppAccount.verifySignature(sig, tx) tokenDef <- PlayNommDAppToken.getTokenDefinition( tf.tokenDefinitionId, ) inputAmount <- PlayNommDAppToken.getFungibleBalanceTotalAmounts( tf.inputs.map(_.toResultHashValue), sig.account, ) outputAmount = tf.outputs.values.foldLeft(BigNat.Zero)(BigNat.add) diffBigInt = inputAmount.toBigInt - outputAmount.toBigInt _ <- StateT.liftF: if diffBigInt < 0 then EitherT.right: InvalidTxLogger[F].log: InvalidTx( signer = sig.account, reason = InvalidReason.OutputMoreThanInput, amountToBurn = BigNat.unsafeFromBigInt(diffBigInt.abs), tx = tf, createdAt = tf.createdAt, ) else EitherT.pure(()) txWithResult = TransactionWithResult(Signed(sig, tf))(None) txHash = txWithResult.toHash invalidInputs <- removeInputUtxos( sig.account, tf.inputs.map(_.toResultHashValue), tf.tokenDefinitionId, ) _ <- StateT.liftF: if invalidInputs.isEmpty then EitherT.pure(()) else invalidInputs .traverse(TransactionRepository[F].get) .leftMap(e => PlayNommDAppFailure.internal(s"Fail to get tx: $e"), ) .semiflatMap: txOptions => val sum = txOptions .map: txOption => txOption.fold(BigNat.Zero)( PlayNommDAppToken.tokenBalanceAmount(sig.account), ) .foldLeft(BigNat.Zero)(BigNat.add) InvalidTxLogger[F].log: InvalidTx( signer = sig.account, reason = InvalidReason.InputAlreadyUsed, amountToBurn = sum, tx = tf, createdAt = tf.createdAt, ) _ <- tf.inputs.toList.traverse: inputTxHash => PlayNommDAppToken .removeFungibleSnapshot( sig.account, tf.tokenDefinitionId, inputTxHash.toResultHashValue, ) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to remove fungible snapshot of $inputTxHash" _ <- tf.outputs.toSeq.traverse: case (account, _) => PlayNommDAppToken.putBalance( account, tf.tokenDefinitionId, txHash, ) *> PlayNommDAppToken .addFungibleSnapshot( account, tf.tokenDefinitionId, txHash, outputAmount, ) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to add fungible snapshot of $account" totalAmount <- fromEitherInternal: BigNat.fromBigInt(tokenDef.totalAmount.toBigInt - diffBigInt) _ <- PlayNommDAppToken.putTokenDefinition( tf.tokenDefinitionId, tokenDef.copy(totalAmount = totalAmount), ) diffEither = BigNat.fromBigInt(diffBigInt) _ <- diffEither match case Right(diff) => PlayNommDAppToken .removeTotalSupplySnapshot(tf.tokenDefinitionId, diff) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to remove total supply snapshot of ${tf.tokenDefinitionId}" case Left(_) => PlayNommDAppToken .addTotalSupplySnapshot( tf.tokenDefinitionId, BigNat.unsafeFromBigInt(diffBigInt.abs), ) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to add total supply snapshot of ${tf.tokenDefinitionId}" yield txWithResult program .run(ms) .map: (ms, txWithResult) => (ms, Option(txWithResult)) .leftMap: msg => PlayNommDAppFailure.internal(s"Fail to recover error: $msg") // case ss: Transaction.AgendaTx.SuggestSimpleAgenda => // EitherT.pure((ms, Some(TransactionWithResult(Signed(sig, ss))(None)))) // // case vs: Transaction.AgendaTx.VoteSimpleAgenda => // EitherT.pure((ms, Some(TransactionWithResult(Signed(sig, vs))(None)))) case _ => PlayNommDApp[F](signedTx) .run(ms) .map: (ms, txWithResult) => (ms, Option(txWithResult)) def removePreviousNftBalance[F[_] : Async: PlayNommState: TransactionRepository: InvalidTxLogger]( tokenId: TokenId, signedTx: Signed.Tx, ): StateT[EitherT[F, PlayNommDAppFailure, *], MerkleTrieState, Unit] = val removePreviousNftBalanceOptionT = for nftState <- OptionT: PlayNommState[F].token.nftState .get(tokenId) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to get nft state of ${tokenId}" currentOwner = nftState.currentOwner nftBalanceStream <- OptionT.liftF: PlayNommState[F].token.nftBalance .streamWithPrefix((currentOwner, tokenId).toBytes) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to get nft balance of $currentOwner" currentBalance <- OptionT: StateT.liftF: nftBalanceStream.head.compile.toList .leftMap: msg => PlayNommDAppFailure.internal( s"Fail to get nft balance of $currentOwner: $msg", ) .map(_.headOption) currentUtxoHash = currentBalance._1._3 _ <- OptionT.liftF: for _ <- PlayNommState[F].token.nftBalance .remove((currentOwner, tokenId, currentUtxoHash)) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to remove nft balance of $currentOwner" _ <- StateT.liftF: EitherT.right: InvalidTxLogger[F].log: InvalidTx( signer = signedTx.sig.account, reason = InvalidReason.CanceledBalance, amountToBurn = BigNat.Zero, tx = signedTx.value, wrongNftInput = Some(tokenId), createdAt = signedTx.value.createdAt, ) yield () yield () removePreviousNftBalanceOptionT.value.map(_ => ()) def getNftTokenId[F[_]: Monad: TransactionRepository]( utxoHash: Hash.Value[TransactionWithResult], ): StateT[EitherT[F, PlayNommDAppFailure, *], MerkleTrieState, TokenId] = StateT.liftF: TransactionRepository[F] .get(utxoHash) .leftMap(e => PlayNommDAppFailure.internal(s"Fail to get tx: ${e.msg}")) .subflatMap: txOption => Either.fromOption( txOption, PlayNommDAppFailure.internal(s"There is no tx of $utxoHash"), ) .flatMap: txWithResult => txWithResult.signedTx.value match case nb: Transaction.NftBalance => EitherT.pure: nb match case mn: Transaction.TokenTx.MintNFT => mn.tokenId case tn: Transaction.TokenTx.TransferNFT => tn.tokenId case den: Transaction.TokenTx.DisposeEntrustedNFT => den.tokenId case mnm: Transaction.TokenTx.MintNFTWithMemo => mnm.tokenId case _ => EitherT.leftT: PlayNommDAppFailure.external: s"Tx $txWithResult is not a nft balance" def removeInputUtxos[F[_]: Monad: PlayNommState]( account: Account, inputs: Set[Hash.Value[TransactionWithResult]], definitionId: TokenDefinitionId, ): StateT[EitherT[F, PlayNommDAppFailure, *], MerkleTrieState, List[ Hash.Value[TransactionWithResult], ]] = val inputList = inputs.toList for removeResults <- inputList.traverse: txHash => PlayNommState[F].token.fungibleBalance .remove((account, definitionId, txHash)) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to remove fingible balance ($account, $definitionId, $txHash)" invalidUtxos = inputList.zip(removeResults).filterNot(_._2).map(_._1) yield invalidUtxos ================================================ FILE: modules/eth-gateway/src/main/scala/io/leisuremeta/chain/gateway/eth/EthGatewayMain.scala ================================================ ================================================ FILE: modules/eth-gateway-common/src/main/scala/io/leisuremeta/chain/gateway/eth/common/GatewayApi.scala ================================================ package io.leisuremeta.chain.gateway.eth.common import io.circe.generic.auto.* import sttp.model.StatusCode import sttp.tapir.* import sttp.tapir.json.circe.* import sttp.tapir.generic.auto.given object GatewayApi: final case class GatewayRequest( key: String, doublyEncryptedFrontPartBase64: String, ) final case class GatewayResponse( singlyEncryptedBase64: String, ) sealed trait GatewayApiError final case class ServerError(msg: String) extends GatewayApiError sealed trait UserError extends GatewayApiError: def msg: String final case class Unauthorized(msg: String) extends UserError final case class NotFound(msg: String) extends UserError final case class BadRequest(msg: String) extends UserError val postDecryptEndpoint: PublicEndpoint[GatewayRequest, GatewayApiError, GatewayResponse, Any] = endpoint.post .in(jsonBody[GatewayRequest]) .out(jsonBody[GatewayResponse]) .errorOut: oneOf[GatewayApiError]( oneOfVariant(statusCode(StatusCode.BadRequest).and(jsonBody[BadRequest])), oneOfVariant(statusCode(StatusCode.Unauthorized).and(jsonBody[Unauthorized])), oneOfVariant(statusCode(StatusCode.NotFound).and(jsonBody[NotFound])), oneOfVariant(statusCode(StatusCode.InternalServerError).and(jsonBody[ServerError])), ) ================================================ FILE: modules/eth-gateway-common/src/main/scala/io/leisuremeta/chain/gateway/eth/common/GatewayConf.scala ================================================ package io.leisuremeta.chain.gateway.eth.common import pureconfig.* import pureconfig.generic.derivation.default.* final case class GatewayConf( ethChainId: Int, ethContractAddress: String, gatewayEthAddress: String, gatewayEndpoint: String, localServerPort: Int, lmEndpoint: String, kmsAlias: String, encryptedEthEndpoint: String, encryptedDatabaseEndpoint: String, databaseTableName: String, databaseValueColumn: String, targetGateway: String, ) derives ConfigReader object GatewayConf: def loadOrThrow(): GatewayConf = ConfigSource.default.loadOrThrow[GatewayConf] ================================================ FILE: modules/eth-gateway-common/src/main/scala/io/leisuremeta/chain/gateway/eth/common/GatewayDecryptService.scala ================================================ package io.leisuremeta.chain.gateway.eth.common import cats.data.EitherT import cats.effect.{Async, Resource} import scodec.bits.ByteVector import client.* object GatewayDecryptService: def getPlainTextResource[F[_] : Async: GatewayApiClient: GatewayDatabaseClient: GatewayKmsClient]( key: String, ): EitherT[F, String, Resource[F, Array[Byte]]] = for doublyEncryptedFrontOption <- GatewayDatabaseClient[F].select(key) doublyEncryptedFront <- EitherT.fromOption( doublyEncryptedFrontOption, s"Value not found for key: ${key}", ) singlyEncryptedBase64 <- GatewayApiClient[F].get( key, doublyEncryptedFront, ) bytes <- EitherT.fromEither: ByteVector.fromBase64Descriptive(singlyEncryptedBase64) plaintextResource <- GatewayKmsClient[F].decrypt(bytes.toArray) yield plaintextResource def getEth[F[_] : Async: GatewayApiClient: GatewayDatabaseClient: GatewayKmsClient] : EitherT[F, String, Resource[F, Array[Byte]]] = getPlainTextResource[F]("ETH") def getLm[F[_] : Async: GatewayApiClient: GatewayDatabaseClient: GatewayKmsClient] : EitherT[F, String, Resource[F, Array[Byte]]] = getPlainTextResource[F]("LM") def getLmD[F[_] : Async: GatewayApiClient: GatewayDatabaseClient: GatewayKmsClient] : EitherT[F, String, Resource[F, Array[Byte]]] = getPlainTextResource[F]("LM-D") def getSimplifiedPlainTextResource[F[_]: Async: GatewayKmsClient]( encryptedBase64: String, ): EitherT[F, String, Resource[F, Array[Byte]]] = for bytes <- EitherT.fromEither: ByteVector.fromBase64Descriptive(encryptedBase64) plaintextResource <- GatewayKmsClient[F].decrypt(bytes.toArrayUnsafe) yield plaintextResource ================================================ FILE: modules/eth-gateway-common/src/main/scala/io/leisuremeta/chain/gateway/eth/common/GatewayResource.scala ================================================ package io.leisuremeta.chain.gateway.eth.common import cats.data.EitherT import cats.effect.{Async, Resource} import org.web3j.protocol.Web3j import scodec.bits.ByteVector import sttp.client3.* import sttp.client3.armeria.cats.ArmeriaCatsBackend import client.* import cats.effect.std.Dispatcher object GatewayResource: def getAllResource[F[_]: Async](conf: GatewayConf): Resource[ EitherT[F, String, *], (GatewayKmsClient[F], Web3j, GatewayDatabaseClient[F], SttpBackend[F, Any]), ] = for kms <- GatewayKmsClient .make[F](conf.kmsAlias) .mapK(EitherT.liftK[F, String]) web3jBytes <- Resource.eval: EitherT.fromEither: ByteVector.fromBase64Descriptive(conf.encryptedEthEndpoint) web3jPlaintextResource <- Resource.eval(kms.decrypt(web3jBytes.toArray)) web3jPlaintext <- web3jPlaintextResource.mapK(EitherT.liftK[F, String]) web3jEndpoint = String(web3jPlaintext, "UTF-8") web3j <- GatewayWeb3Service .web3Resource[F](web3jEndpoint) .mapK(EitherT.liftK[F, String]) dbBytes <- Resource.eval: EitherT.fromEither: ByteVector.fromBase64Descriptive(conf.encryptedDatabaseEndpoint) dbPlaintextResource <- Resource.eval(kms.decrypt(dbBytes.toArray)) dbPlaintext <- dbPlaintextResource.mapK(EitherT.liftK[F, String]) dbEndpoint = String(dbPlaintext, "UTF-8") db <- GatewayDatabaseClient .make[F](dbEndpoint, conf.databaseTableName, conf.databaseValueColumn) .mapK(EitherT.liftK[F, String]) sttp <- ArmeriaCatsBackend.resource[F]().mapK(EitherT.liftK[F, String]) dispatcher <- Dispatcher.parallel[F].mapK(EitherT.liftK[F, String]) server <- GatewayServer .make[F](dispatcher, conf.localServerPort, db, kms) .mapK(EitherT.liftK[F, String]) yield (kms, web3j, db, sttp) def getSimpleResource[F[_]: Async]( conf: GatewaySimpleConf, ): Resource[EitherT[F, String, *], (GatewayKmsClient[F], Web3j, SttpBackend[F, Any])] = for kms <- GatewayKmsClient .make[F](conf.kmsAlias) .mapK(EitherT.liftK[F, String]) web3jBytes <- Resource.eval: EitherT.fromEither: ByteVector.fromBase64Descriptive(conf.encryptedEthEndpoint) web3jPlaintextResource <- Resource.eval(kms.decrypt(web3jBytes.toArray)) web3jPlaintext <- web3jPlaintextResource.mapK(EitherT.liftK[F, String]) web3jEndpoint = String(web3jPlaintext, "UTF-8") web3j <- GatewayWeb3Service .web3Resource[F](web3jEndpoint) .mapK(EitherT.liftK[F, String]) sttp <- ArmeriaCatsBackend.resource[F]().mapK(EitherT.liftK[F, String]) yield (kms, web3j, sttp) ================================================ FILE: modules/eth-gateway-common/src/main/scala/io/leisuremeta/chain/gateway/eth/common/GatewayServer.scala ================================================ package io.leisuremeta.chain.gateway.eth.common import cats.data.EitherT import cats.effect.{Async, Resource} import cats.effect.std.Dispatcher import cats.syntax.flatMap.* import com.linecorp.armeria.server.Server import scodec.bits.ByteVector import sttp.capabilities.fs2.Fs2Streams import sttp.tapir.server.ServerEndpoint import sttp.tapir.server.armeria.cats.{ ArmeriaCatsServerInterpreter, ArmeriaCatsServerOptions, } import sttp.tapir.server.interceptor.log.DefaultServerLog import client.{GatewayDatabaseClient, GatewayKmsClient} object GatewayServer: def gatewayServerEndpoint[F[_]: Async]( dbClient: GatewayDatabaseClient[F], kmsClient: GatewayKmsClient[F], ): ServerEndpoint.Full[ Unit, Unit, GatewayApi.GatewayRequest, GatewayApi.GatewayApiError, GatewayApi.GatewayResponse, Any, F, ] = def decrypt( key: String, doublyEncryptedFrontPartBase64: String, ): EitherT[F, String, Resource[F, Array[Byte]]] = for frontBytes <- EitherT.fromEither[F]: ByteVector.fromBase64Descriptive(doublyEncryptedFrontPartBase64) doublyEncryptedBackPartBase64Option <- dbClient.select(key) backPartBase64 <- EitherT.fromOption( doublyEncryptedBackPartBase64Option, s"Value not found for key: ${key}", ) backBytes <- EitherT.fromEither[F]: ByteVector.fromBase64Descriptive(backPartBase64) plaintextResource <- kmsClient.decrypt: (frontBytes ++ backBytes).toArrayUnsafe yield plaintextResource GatewayApi.postDecryptEndpoint.serverLogic: request => decrypt( request.key, request.doublyEncryptedFrontPartBase64, ).value .flatMap: case Left(errorMsg) => Async[F].delay: Left(GatewayApi.ServerError(errorMsg)) case Right(resource) => resource.use: plaintext => Async[F].delay: Right( GatewayApi.GatewayResponse( ByteVector.view(plaintext).toBase64, ), ) def make[F[_]: Async]( dispatcher: Dispatcher[F], port: Int, dbClient: GatewayDatabaseClient[F], kmsClient: GatewayKmsClient[F], ): Resource[F, Server] = val serverEndpoint = gatewayServerEndpoint[F](dbClient, kmsClient) Resource.fromAutoCloseable(getServer(dispatcher, port, serverEndpoint)) @SuppressWarnings(Array("org.wartremover.warts.Null")) def getServer[F[_]: Async]( dispatcher: Dispatcher[F], port: Int, serverEndpoint: ServerEndpoint[Fs2Streams[F], F], ): F[Server] = Async[F].fromCompletableFuture: def log[F[_]: Async]( level: scribe.Level, )(msg: String, exOpt: Option[Throwable])(using mdc: scribe.mdc.MDC, ): F[Unit] = Async[F].delay: exOpt match case None => scribe.log(level, mdc, msg) case Some(ex) => scribe.log(level, mdc, msg, ex) val serverLog = DefaultServerLog( doLogWhenReceived = log(scribe.Level.Info)(_, None), doLogWhenHandled = log(scribe.Level.Info), doLogAllDecodeFailures = log(scribe.Level.Info), doLogExceptions = (msg: String, ex: Throwable) => Async[F].delay(scribe.warn(msg, ex)), noLog = Async[F].pure(()), ) val serverOptions = ArmeriaCatsServerOptions .customiseInterceptors[F](dispatcher) .serverLog(serverLog) .options val tapirService = ArmeriaCatsServerInterpreter[F](serverOptions) .toService(serverEndpoint) val server = Server.builder .maxRequestLength(128 * 1024 * 1024) .requestTimeout(java.time.Duration.ofMinutes(10)) .http(port) .service(tapirService) .build Async[F].delay: server.start().thenApply(_ => server) ================================================ FILE: modules/eth-gateway-common/src/main/scala/io/leisuremeta/chain/gateway/eth/common/GatewaySimpleConf.scala ================================================ package io.leisuremeta.chain.gateway.eth.common import pureconfig.* import pureconfig.generic.derivation.default.* final case class GatewaySimpleConf( ethChainId: Int, ethLmContractAddress: String, ethMultisigContractAddress: String, gatewayEthAddress: String, depositExempts: Seq[String], lmEndpoint: String, kmsAlias: String, encryptedEthEndpoint: String, encryptedEthPrivate: String, encryptedLmPrivate: String, targetGateway: String, ) derives ConfigReader object GatewaySimpleConf: def loadOrThrow(): GatewaySimpleConf = ConfigSource.default.loadOrThrow[GatewaySimpleConf] ================================================ FILE: modules/eth-gateway-common/src/main/scala/io/leisuremeta/chain/gateway/eth/common/GatewayWeb3Service.scala ================================================ package io.leisuremeta.chain.gateway.eth.common import cats.effect.{Async, Resource} import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor import org.web3j.protocol.Web3j import org.web3j.protocol.http.HttpService object GatewayWeb3Service: def web3Resource[F[_]: Async](url: String): Resource[F, Web3j] = Resource.make { val interceptor = HttpLoggingInterceptor() interceptor.setLevel(HttpLoggingInterceptor.Level.BASIC) val client = OkHttpClient .Builder() .addInterceptor(interceptor) .build() Async[F].delay(Web3j.build(new HttpService(url, client))) }(web3j => Async[F].delay(web3j.shutdown())) ================================================ FILE: modules/eth-gateway-common/src/main/scala/io/leisuremeta/chain/gateway/eth/common/client/GatewayApiClient.scala ================================================ package io.leisuremeta.chain.gateway.eth.common package client import cats.data.EitherT import cats.effect.Async import cats.syntax.bifunctor.* import cats.syntax.functor.* import sttp.client3.SttpBackend import sttp.model.Uri import sttp.tapir.DecodeResult import sttp.tapir.client.sttp.SttpClientInterpreter trait GatewayApiClient[F[_]]: def get( key: String, doublyEncryptedFrontPartBase64: String, ): EitherT[F, String, String] object GatewayApiClient: def apply[F[_]: GatewayApiClient]: GatewayApiClient[F] = summon def make[F[_]: Async]( backend: SttpBackend[F, Any], uri: Uri ): GatewayApiClient[F] = val sttpClient = SttpClientInterpreter().toClient( GatewayApi.postDecryptEndpoint, Some(uri), backend, ) @SuppressWarnings(Array("org.wartremover.warts.ToString")) def sanitize[A, B]( result: DecodeResult[Either[A, B]], ): Either[String, B] = result match case DecodeResult.Value(v) => v.leftMap(_.toString) case f: DecodeResult.Failure => Left(s"Fail: ${f.toString()}") new GatewayApiClient[F]: override def get( key: String, doublyEncryptedFrontPartBase64: String, ): EitherT[F, String, String] = EitherT .apply: sttpClient( GatewayApi.GatewayRequest(key, doublyEncryptedFrontPartBase64), ).map(sanitize) .map(_.singlyEncryptedBase64) ================================================ FILE: modules/eth-gateway-common/src/main/scala/io/leisuremeta/chain/gateway/eth/common/client/GatewayDatabaseClient.scala ================================================ package io.leisuremeta.chain.gateway.eth.common.client import scala.jdk.CollectionConverters.* import cats.data.EitherT import cats.effect.{Async, Resource} import cats.syntax.functor.* import com.github.jasync.sql.db.QueryResult import com.github.jasync.sql.db.mysql.MySQLConnectionBuilder import com.github.jasync.sql.db.ConnectionPoolConfigurationBuilder trait GatewayDatabaseClient[F[_]]: def select( key: String, ): EitherT[F, String, Option[String]] object GatewayDatabaseClient: def apply[F[_]: GatewayDatabaseClient]: GatewayDatabaseClient[F] = summon @SuppressWarnings(Array("org.wartremover.warts.OptionPartial")) def make[F[_]: Async]( dbEndpoint: String, tableName: String, valueColumn: String, ): Resource[F, GatewayDatabaseClient[F]] = Resource .make: Async[F].blocking: val reg = """^jdbc:mysql://(.*):(\d{4,5})/(.*)\?user=(.*)&password=(.*)$""".r val builder = for res <- reg.findFirstMatchIn(dbEndpoint) config = ConnectionPoolConfigurationBuilder() _ = config.setHost(res.group(1)) _ = config.setPort(res.group(2).toInt) _ = config.setDatabase(res.group(3)) _ = config.setUsername(res.group(4)) _ = config.setPassword(res.group(5)) yield config MySQLConnectionBuilder.createConnectionPool(builder.get) .apply: connection => Async[F] .fromCompletableFuture: Async[F].delay(connection.disconnect()) .map(_ => ()) .map: connection => new GatewayDatabaseClient[F]: override def select(key: String): EitherT[F, String, Option[String]] = Async[F] .attemptT: Async[F] .fromCompletableFuture: Async[F].delay: connection.sendPreparedStatement( s"SELECT GTWY_SE_CODE, ${valueColumn} FROM ${tableName} WHERE GTWY_SE_CODE = ?", List(key).asJava, ) .map: (queryResult: QueryResult) => scribe.info(s"Query result: ${queryResult}") queryResult.getRows.asScala.headOption.map( _.getString(valueColumn), ) .leftMap(_.getMessage()) ================================================ FILE: modules/eth-gateway-common/src/main/scala/io/leisuremeta/chain/gateway/eth/common/client/GatewayKmsClient.scala ================================================ package io.leisuremeta.chain.gateway.eth.common.client import cats.data.EitherT import cats.effect.{Async, Resource} import cats.syntax.functor.* import software.amazon.awssdk.core.SdkBytes import software.amazon.awssdk.services.kms.KmsAsyncClient import software.amazon.awssdk.services.kms.model.DecryptRequest trait GatewayKmsClient[F[_]]: def decrypt( cipherText: Array[Byte], ): EitherT[F, String, Resource[F, Array[Byte]]] object GatewayKmsClient: def apply[F[_]: GatewayKmsClient]: GatewayKmsClient[F] = summon def make[F[_]: Async](alias: String): Resource[F, GatewayKmsClient[F]] = Resource .fromAutoCloseable(Async[F].delay(KmsAsyncClient.create())) .map: kmsAsyncClient => new GatewayKmsClient[F]: override def decrypt( cipherText: Array[Byte], ): EitherT[F, String, Resource[F, Array[Byte]]] = Async[F] .attemptT: Async[F] .fromCompletableFuture: Async[F].delay: kmsAsyncClient.decrypt: DecryptRequest .builder() .keyId(s"alias/${alias}") .ciphertextBlob(SdkBytes.fromByteArray(cipherText)) .build() .map: decryptResponse => Resource .make: Async[F].delay: decryptResponse.plaintext().asByteArrayUnsafe() .apply: byteArray => Async[F].delay: val emptyArray = Array.fill[Byte](byteArray.length)(0x00) System.arraycopy( emptyArray, 0, byteArray, 0, byteArray.length, ) .leftMap(_.getMessage()) ================================================ FILE: modules/eth-gateway-common/src/test/scala/io/leisuremeta/chain/gateway/eth/common/GatewayServerTest.scala ================================================ package io.leisuremeta.chain.gateway.eth.common import cats.effect.IO class GatewayServerTest extends munit.CatsEffectSuite: test("should start and stop server"): IO(42).assertEquals(42) ================================================ FILE: modules/eth-gateway-deposit/src/main/resources/application.conf.sample ================================================ eth-chain-id = 11155111 eth-lm-contract-address = "0x02886136510172E313932EA66FE7bDC4d4C2fcc4" eth-multisig-contract-address = "0x02886136510172E313932EA66FE7bDC4d4C2fcc4" gateway-eth-address = "0x3E984778B16b5a0fE489Aac0b716078202400b6A" deposit-exempts = [ "0x3E984778B16b5a0fE489Aac0b716078202400b6A", ] lm-endpoint = "http://127.0.0.1:8080" kms-alias = "gateway_withdraw_key" encrypted-eth-endpoint = "..." encrypted-eth-private = "..." encrypted-lm-private = "..." target-gateway = "eth-gateway" ================================================ FILE: modules/eth-gateway-deposit/src/main/scala/io/leisuremeta/chain/gateway/eth/EthGatewayDepositMain.scala ================================================ package io.leisuremeta.chain package gateway.eth //import java.math.BigInteger import java.nio.charset.StandardCharsets import java.nio.file.{Files, Paths, StandardOpenOption} import java.util.{Arrays, Locale} import java.time.Instant //import java.time.temporal.ChronoUnit import scala.jdk.CollectionConverters.* //import scala.jdk.FutureConverters.* import scala.concurrent.duration.* import scala.util.Try import cats.data.EitherT import cats.effect.{Async, Clock, ExitCode, IO, IOApp, Resource} import cats.syntax.applicativeError.* import cats.syntax.apply.* //import cats.syntax.bifunctor.* import cats.syntax.eq.* import cats.syntax.flatMap.* import cats.syntax.functor.* import cats.syntax.traverse.* //import com.github.jasync.sql.db.{Connection, QueryResult} //import com.github.jasync.sql.db.mysql.MySQLConnectionBuilder //import com.typesafe.config.{Config, ConfigFactory} import io.circe.Encoder import io.circe.syntax.given import io.circe.generic.auto.* import io.circe.parser.decode import org.web3j.abi.{ EventEncoder, // FunctionEncoder, FunctionReturnDecoder, TypeReference, } import org.web3j.abi.datatypes.{Address, Event, Type} import org.web3j.abi.datatypes.generated.Uint256 //import org.web3j.crypto.{Credentials, MnemonicUtils} import org.web3j.protocol.Web3j import org.web3j.protocol.core.{ DefaultBlockParameter, // DefaultBlockParameterName, } import org.web3j.protocol.core.methods.request.{EthFilter} import org.web3j.protocol.core.methods.response.EthLog.{LogResult, LogObject} //import org.web3j.protocol.core.methods.response.TransactionReceipt //import org.web3j.protocol.http.HttpService //import org.web3j.tx.RawTransactionManager //import org.web3j.tx.response.PollingTransactionReceiptProcessor import sttp.client3.* import sttp.model.{MediaType, StatusCode} import lib.crypto.CryptoOps//, KeyPair} //import lib.crypto.Hash.ops.* import lib.crypto.Sign.ops.* import lib.datatype.* import api.model.* //import api.model.TransactionWithResult.ops.* import api.model.api_model.{AccountInfo, BalanceInfo} import api.model.token.* import common.* import common.client.* object EthGatewayDepositMain extends IOApp: case class TransferTokenEvent( blockNumber: BigInt, txHash: String, from: String, to: String, value: BigInt, ) extension [A](logResult: LogResult[A]) def toTransferTokenEvent: TransferTokenEvent = val log = logResult.get.asInstanceOf[LogObject].get val typeRefAddress: TypeReference[Address] = new TypeReference[Address]() {} val typeRefUint256: TypeReference[Type[?]] = (new TypeReference[Uint256]() {}).asInstanceOf[TypeReference[Type[?]]] val topics = log.getTopics.asScala.toVector val from: Address = FunctionReturnDecoder .decodeIndexedValue[Address](topics(1), typeRefAddress) .asInstanceOf[Address] val to: Address = FunctionReturnDecoder .decodeIndexedValue[Address](topics(2), typeRefAddress) .asInstanceOf[Address] val amount = FunctionReturnDecoder .decode(log.getData, List(typeRefUint256).asJava) .asScala .headOption.map: amount => amount .asInstanceOf[Uint256] .getValue .fold(BigInt(0))(BigInt(_)) TransferTokenEvent( blockNumber = BigInt(log.getBlockNumber), txHash = log.getTransactionHash, from = from.getValue, to = to.getValue, value = amount, ) def writeUnsentDeposits[F[_]: Async]( deposits: Seq[TransferTokenEvent], ): F[Unit] = Async[F].blocking: val path = Paths.get("unsent-deposits.json") val json = deposits.asJson.spaces2 val _ = Files.write( path, json.getBytes(StandardCharsets.UTF_8), StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING, ) def readUnsentDeposits[F[_]: Async](): F[Seq[TransferTokenEvent]] = Async[F].blocking: val path = Paths.get("unsent-deposits.json") val seqEither = for json <- Try(Files.readAllLines(path).asScala.mkString("\n")).toEither seq <- decode[Seq[TransferTokenEvent]](json) yield seq seqEither match case Right(seq) => seq case Left(e) => e.printStackTrace() scribe.error(s"Error reading unsent deposits: ${e.getMessage}") Seq.empty def logSentDeposits[F[_]: Async]( deposits: Seq[(Account, TransferTokenEvent)], ): F[Unit] = if deposits.isEmpty then Async[F].unit else Async[F].blocking: val path = Paths.get("sent-deposits.logs") val jsons = deposits.map(_.asJson.noSpaces).mkString("", "\n", "\n") val _ = Files.write( path, jsons.getBytes(StandardCharsets.UTF_8), StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.APPEND, ) def writeLastBlockRead[F[_]: Async](blockNumber: BigInt): F[Unit] = Async[F].blocking: val path = Paths.get("last-block-read.json") val json = blockNumber.asJson.spaces2 val _ = Files.write( path, json.getBytes(StandardCharsets.UTF_8), StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING, ) def readLastBlockRead[F[_]: Async](): F[BigInt] = Async[F].blocking: val path = Paths.get("last-block-read.json") val blockNumberEither = for json <- Try(Files.readAllLines(path).asScala.mkString("\n")).toEither blockNumber <- decode[BigInt](json) yield blockNumber blockNumberEither match case Right(blockNumber) => blockNumber case Left(e) => scribe.error(s"Error reading last block read: ${e.getMessage}") BigInt(0) def getTransferTokenEvents[F[_]: Async]( web3j: Web3j, contractAddress: String, fromBlock: DefaultBlockParameter, toBlock: DefaultBlockParameter, ): F[Seq[TransferTokenEvent]] = val TransferEvent = new Event( "Transfer", Arrays.asList( new TypeReference[Address]() {}, new TypeReference[Address]() {}, new TypeReference[Uint256]() {}, ), ) val filter = new EthFilter(fromBlock, toBlock, contractAddress) filter.addSingleTopic(EventEncoder.encode(TransferEvent)) Async[F] .fromCompletableFuture: Async[F].delay(web3j.ethGetLogs(filter).sendAsync()) .map: ethLog => ethLog.getLogs.asScala.map(_.toTransferTokenEvent).toSeq def submitTx[F[_]: Async: GatewayKmsClient]( sttp: SttpBackend[F, Any], lmEndpoint: String, encryptedLmPrivate: String, account: Account, tx: Transaction, ): F[Unit] = GatewayDecryptService .getSimplifiedPlainTextResource[F](encryptedLmPrivate) .value .flatMap: case Left(msg) => scribe.error(s"Failed to get LM private: $msg") Async[F].sleep(10.seconds) *> submitTx[F](sttp, lmEndpoint, encryptedLmPrivate, account, tx) case Right(lmPrivateResource) => lmPrivateResource.use: lmPrivateArray => val keyPair = CryptoOps.fromPrivate(BigInt(lmPrivateArray)) val Right(sig) = keyPair.sign(tx): @unchecked val signedTxs = Seq(Signed(AccountSignature(sig, account), tx)) scribe.info(s"Sending signed transactions: $signedTxs") given bodyJsonSerializer[A: Encoder]: BodySerializer[A] = (a: A) => val serialized = a.asJson.noSpaces StringBody(serialized, "UTF-8", MediaType.ApplicationJson) basicRequest .response(asStringAlways) .post(uri"http://$lmEndpoint/tx") .body(signedTxs) .send(sttp) .map: response => scribe.info(s"Response: $response") def mintLM[F[_]: Async: Clock: GatewayKmsClient]( sttp: SttpBackend[F, Any], lmEndpoint: String, encryptedLmPrivate: String, toAccount: Account, amount: BigInt, targetGateway: String, ): F[Unit] = val networkId = NetworkId(BigNat.unsafeFromLong(1000L)) val lmDef = TokenDefinitionId(Utf8.unsafeFrom("LM")) val account = Account(Utf8.unsafeFrom(targetGateway)) Clock[F].realTimeInstant.flatMap: now => val mintFungibleToken = Transaction.TokenTx.MintFungibleToken( networkId = networkId, createdAt = now, definitionId = lmDef, outputs = Map(toAccount -> BigNat.unsafeFromBigInt(amount)), ) submitTx[F](sttp, lmEndpoint, encryptedLmPrivate, account, mintFungibleToken) def findAccountByEthAddress[F[_]: Async]( sttp: SttpBackend[F, Any], lmEndpoint: String, ethAddress: String, ): F[Option[Account]] = def findAccountByEthAddress0( ethAddress: String, ): F[Option[Account]] = scribe.info(s"requesting eth address $ethAddress 's LM account") Async[F] .attempt: basicRequest .response(asStringAlways) .get(uri"http://$lmEndpoint/eth/$ethAddress") .send(sttp) .map: response => scribe.info(s"eth address $ethAddress response: $response") if response.code.isSuccess then Some( Account(Utf8.unsafeFrom(response.body.drop(1).dropRight(1))), ) else scribe.info(s"Account $ethAddress not found: ${response.body}") None .map(_.toOption.flatten) findAccountByEthAddress0(ethAddress).flatMap: accountOption => if accountOption.isEmpty && ethAddress.startsWith("0x") then findAccountByEthAddress0(ethAddress.drop(2)) else Async[F].pure(accountOption) def checkDepositAndMint[F[_]: Async: GatewayKmsClient]( sttp: SttpBackend[F, Any], web3j: Web3j, lmEndpoint: String, encryptedLmPrivate: String, ethContract: String, multisigContractAddress: String, exemptAddressSet: Set[String], startBlockNumber: BigInt, endBlockNumber: BigInt, targetGateway: String, ): F[Unit] = for events <- getTransferTokenEvents[F]( web3j, ethContract.toLowerCase(Locale.ENGLISH), DefaultBlockParameter.valueOf(startBlockNumber.bigInteger), DefaultBlockParameter.valueOf(endBlockNumber.bigInteger), ) _ <- Async[F].delay(scribe.info(s"events: $events")) depositEvents = events.filter: e => e.to.toLowerCase(Locale.ENGLISH) === multisigContractAddress.toLowerCase(Locale.ENGLISH) && !exemptAddressSet.contains(e.to.toLowerCase(Locale.ENGLISH)) _ <- Async[F].delay: scribe.info(s"current deposit events: $depositEvents") oldEvents <- readUnsentDeposits[F]() _ <- Async[F].delay(scribe.info(s"old deposit events: $oldEvents")) allEvents = depositEvents ++ oldEvents _ <- Async[F].delay(scribe.info(s"all deposit events: $allEvents")) eventAndAccountOptions <- allEvents.toList.traverse: event => // scribe.info(s"current event: $event") // val amount = event.value // val toAccount = Account(Utf8.unsafeFrom(event.to)) findAccountByEthAddress(sttp, lmEndpoint, event.from).map: (accountOption: Option[Account]) => scribe.info: s"eth address ${event.from}'s LM account: $accountOption" (event, accountOption) (known, unknown) = eventAndAccountOptions.partition(_._2.nonEmpty) toMints = known.flatMap: case (event, Some(account)) => List((account, event)) case (event, None) => scribe.error(s"Internal error: Account ${event.from} not found") Nil _ <- Async[F].delay(scribe.info(s"toMints: $toMints")) _ <- toMints.traverse: (account, event) => mintLM[F](sttp, lmEndpoint, encryptedLmPrivate, account, event.value, targetGateway) _ <- logSentDeposits[F](toMints.map(_._1).zip(known.map(_._1))) unsent = unknown.map(_._1) _ <- Async[F].delay(scribe.info(s"unsent: $unsent")) _ <- writeUnsentDeposits[F](unsent) yield () def checkLoop[F[_]: Async: GatewayKmsClient]( sttp: SttpBackend[F, Any], web3j: Web3j, conf: GatewaySimpleConf, ): F[Unit] = def run: F[Unit] = for _ <- Async[F].delay: scribe.info(s"Checking for deposit / withdrawal events") lastBlockNumber <- readLastBlockRead[F]() startBlockNumber = lastBlockNumber + 1 blockNumber <- Async[F] .fromCompletableFuture(Async[F].delay(web3j.ethBlockNumber.sendAsync())) .map(_.getBlockNumber) .map(BigInt(_)) _ <- Async[F].delay(scribe.info(s"blockNumber: $blockNumber")) endBlockNumber = (startBlockNumber + 10000) min (blockNumber - 6) _ <- Async[F].delay: scribe.info(s"startBlockNumber: $startBlockNumber") scribe.info: s"blockNumber: $blockNumber, endBlockNumber: $endBlockNumber" _ <- checkDepositAndMint[F]( sttp = sttp, web3j = web3j, lmEndpoint = conf.lmEndpoint, encryptedLmPrivate = conf.encryptedLmPrivate, ethContract = conf.ethLmContractAddress, multisigContractAddress = conf.ethMultisigContractAddress, exemptAddressSet = conf.depositExempts.map(_.toLowerCase(Locale.ENGLISH)).toSet, startBlockNumber = startBlockNumber, endBlockNumber = endBlockNumber, targetGateway = conf.targetGateway, ) _ <- writeLastBlockRead[F](endBlockNumber) _ <- Async[F].delay(scribe.info(s"Deposit check finished.")) yield () def loop: F[Unit] = for _ <- run.orElse(Async[F].unit) _ <- Async[F].sleep(10000.millis) _ <- loop yield () loop def getGasPrice[F[_]: Async](web3j: Web3j): F[BigInt] = Async[F] .fromCompletableFuture: Async[F].delay(web3j.ethGasPrice.sendAsync()) .map: ethGasPrice => BigInt(ethGasPrice.getGasPrice) def getBalance[F[_]: Async]( sttp: SttpBackend[F, Any], lmEndpoint: String, targetGateway: String, ): F[Option[BalanceInfo]] = val lmDef = TokenDefinitionId(Utf8.unsafeFrom("LM")) basicRequest .response(asStringAlways) .get(uri"http://$lmEndpoint/balance/$targetGateway?movable=all") .send(sttp) .map: response => if response.code.isSuccess then decode[Map[TokenDefinitionId, BalanceInfo]](response.body) match case Right(balanceInfoMap) => balanceInfoMap.get(lmDef) case Left(error) => scribe.error(s"Error decoding balance info: $error") scribe.error(s"response: ${response.body}") None else if response.code.code === StatusCode.NotFound.code then scribe.info: s"balance of account $targetGateway not found: ${response.body}" None else scribe.error(s"Error getting balance: ${response.body}") None def getAccountInfo[F[_]: Async]( sttp: SttpBackend[F, Any], lmAddress: String, account: Account, ): F[Option[AccountInfo]] = basicRequest .response(asStringAlways) .get(uri"http://$lmAddress/account/${account.utf8.value}") .send(sttp) .map: response => if response.code.isSuccess then decode[AccountInfo](response.body) match case Right(accountInfo) => Some(accountInfo) case Left(error) => scribe.error(s"Error decoding account info: $error") None else if response.code.code === StatusCode.NotFound.code then scribe.info(s"account info not found: ${response.body}") None else scribe.error(s"Error getting account info: ${response.body}") None def run(args: List[String]): IO[ExitCode] = val conf = GatewaySimpleConf.loadOrThrow() GatewayResource .getSimpleResource[IO](conf) .use: (kms, web3j, sttp) => given GatewayKmsClient[IO] = kms EitherT.liftF: checkLoop[IO]( sttp = sttp, web3j = web3j, conf = conf, ) .value .map: case Left(error) => scribe.error(s"Error: $error") case Right(result) => scribe.info(s"Result: $result") .as(ExitCode.Success) ================================================ FILE: modules/eth-gateway-setup/src/main/resources/application.conf.sample ================================================ // eth-private = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" // lm-private = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" // deposit-db { // host = "some.url1" // port = 3306 // db = "some-db" // table = "SOME_TABLE" // value-column = "PRIVKY" // user = "user" // password = "password" // } // withdraw-db { // host = "some.url2" // port = 3306 // db = "some-db" // table = "SOME_TABLE" // value-column = "PRIVKY" // user = "user" // password = "password" // } // db-write-account { // user = "writeuser" // password = "writepassword"" // } // deposit-kms-alias = "deposit_kms" // withdraw-kms-alias = "withdraw_kms" // eth-endpoint = "https://goerli.infura.io/v3/1234567890abcdef1234567890abcdef" eth-private = "0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" lm-private = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" kms-alias = "gateway_withdraw_key" eth-endpoint = "https://sepolia.infura.io/v3/1234567890abcdef1234567890abcdef" ================================================ FILE: modules/eth-gateway-setup/src/main/scala/io/leisuremeta/chain/gateway/eth/setup/EthGatewaySetupConfig.scala ================================================ package io.leisuremeta.chain.gateway.eth.setup import pureconfig.* import pureconfig.generic.derivation.default.* import EthGatewaySetupConfig.* final case class EthGatewaySetupConfig( ethPrivate: String, lmPrivate: String, depositDb: DbConfig, withdrawDb: DbConfig, dbWriteAccount: DbWriteAccountConfig, depositKmsAlias: String, withdrawKmsAlias: String, ethEndpoint: String, ) derives ConfigReader object EthGatewaySetupConfig: def apply(): EthGatewaySetupConfig = ConfigSource.default.loadOrThrow[EthGatewaySetupConfig] final case class DbConfig( host: String, port: Int, db: String, table: String, valueColumn: String, user: String, password: String, ) final case class DbWriteAccountConfig( user: String, password: String, ) ================================================ FILE: modules/eth-gateway-setup/src/main/scala/io/leisuremeta/chain/gateway/eth/setup/EthGatewaySetupMain.scala ================================================ package io.leisuremeta.chain.gateway.eth.setup import scala.jdk.CollectionConverters.* //import scala.jdk.FutureConverters.* import cats.effect.{Async, ExitCode, IO, IOApp, Resource} import cats.syntax.flatMap.* import cats.syntax.functor.* import com.github.jasync.sql.db.{Connection, QueryResult} import com.github.jasync.sql.db.mysql.MySQLConnectionBuilder import scodec.bits.ByteVector //import software.amazon.awssdk.auth.credentials.{ // AwsCredentials, // StaticCredentialsProvider, //} import software.amazon.awssdk.core.SdkBytes //import software.amazon.awssdk.regions.Region import software.amazon.awssdk.services.kms.KmsAsyncClient import software.amazon.awssdk.services.kms.model.{ DecryptRequest, // GenerateDataKeyRequest, } //import software.amazon.awssdk.services.kms.model.DataKeySpec import software.amazon.awssdk.services.kms.model.EncryptRequest //import software.amazon.awssdk.services.kms.model.EncryptionAlgorithmSpec object EthGatewaySetupMain extends IOApp: def connectDatabase[F[_]: Async]( host: String, db: String, table: String, user: String, password: String, ): Resource[F, Connection] = Resource .make: Async[F].blocking: MySQLConnectionBuilder.createConnectionPool: s"jdbc:mysql://${host}:3306/${db}?user=${user}&password=${password}" .apply: connection => Async[F] .fromCompletableFuture: Async[F].delay(connection.disconnect()) .map(_ => ()) def connectKms[F[_]: Async]: Resource[F, KmsAsyncClient] = Resource.fromAutoCloseable(Async[F].delay(KmsAsyncClient.create())) def encrypt[F[_]: Async](kmsClient: KmsAsyncClient, alias: String)( data: Array[Byte], ): F[Array[Byte]] = Async[F] .fromCompletableFuture: Async[F].delay: kmsClient.encrypt: EncryptRequest .builder() .keyId(s"alias/${alias}") .plaintext(SdkBytes.fromByteArray(data)) .build() .map(_.ciphertextBlob().asByteArrayUnsafe()) def decrypt[F[_]: Async](kmsClient: KmsAsyncClient, alias: String)( data: Array[Byte], ): F[Array[Byte]] = Async[F] .fromCompletableFuture: Async[F].delay: kmsClient.decrypt: DecryptRequest .builder() .keyId(s"alias/${alias}") .ciphertextBlob(SdkBytes.fromByteArray(data)) .build() .map(_.plaintext().asByteArrayUnsafe()) def encryptDepositSide[F[_]: Async]( kmsClient: KmsAsyncClient, config: EthGatewaySetupConfig, )(data: Array[Byte]): F[Array[Byte]] = for cipherText1 <- encrypt[F](kmsClient, config.depositKmsAlias)(data) cipherText2 <- encrypt[F](kmsClient, config.withdrawKmsAlias)(cipherText1) yield cipherText2 def encryptWithdrawSide[F[_]: Async]( kmsClient: KmsAsyncClient, config: EthGatewaySetupConfig, )(data: Array[Byte]): F[Array[Byte]] = for cipherText1 <- encrypt[F](kmsClient, config.withdrawKmsAlias)(data) cipherText2 <- encrypt[F](kmsClient, config.depositKmsAlias)(cipherText1) yield cipherText2 def divide(cipherText: Array[Byte]): (String, String) = val bytes = ByteVector(cipherText) val (front, back) = bytes.splitAt(bytes.size / 2) (front.toBase64, back.toBase64) def saveToDatabase[F[_]: Async](conn: Connection)( tableName: String, valueColumn: String, key: String, value: String, ): F[Unit] = Async[F] .fromCompletableFuture: Async[F].delay: conn.sendPreparedStatement( s"INSERT INTO ${tableName} (GTWY_SE_CODE, ${valueColumn}) VALUES (?, ?) ON DUPLICATE KEY UPDATE GTWY_SE_CODE = ?", List(key, value, value).asJava, ) .map: (queryResult: QueryResult) => scribe.info(s"Query result: ${queryResult}") () def encryptAndDivide[F[_]: Async](encrypt: Array[Byte] => F[Array[Byte]])( plainText: Array[Byte], ): F[(String, String)] = for cipherText <- encrypt(plainText) (front, back) = divide(cipherText) yield (front, back) def saveFrontAndBack[F[_]: Async]( frontDb: Connection, frontTable: String, frontValueColumn: String, backDb: Connection, backTable: String, backValueColumn: String, frontAndBack: Map[String, (String, String)], key: String, ): F[Unit] = for _ <- saveToDatabase[F](frontDb)(frontTable, key, frontValueColumn, frontAndBack(key)._1) _ <- saveToDatabase[F](backDb)(backTable, key, backValueColumn, frontAndBack(key)._2) yield () def encryptAndSave[F[_]: Async](encrypt: Array[Byte] => F[Array[Byte]])( frontDb: Connection, frontTable: String, frontValueColumn: String, backDb: Connection, backTable: String, backValueColumn: String, key: String, plainText: Array[Byte], ): F[Unit] = for cipherText <- encrypt(plainText) (front, back) = divide(cipherText) _ <- saveToDatabase[F](frontDb)(frontTable, key, frontValueColumn, front) _ <- saveToDatabase[F](backDb)(backTable, key, backValueColumn, back) yield () def hexToByteArray(hex: String): Array[Byte] = ByteVector.fromValidHex(hex).toArrayUnsafe def allEncryptAndDivide[F[_]: Async]( config: EthGatewaySetupConfig, kmsClient: KmsAsyncClient, ): F[Map[String, (String, String)]] = for lmd <- encryptAndDivide[F](encryptDepositSide[F](kmsClient, config))( hexToByteArray(config.lmPrivate), ) lm <- encryptAndDivide[F](encryptWithdrawSide[F](kmsClient, config))( hexToByteArray(config.lmPrivate), ) eth <- encryptAndDivide[F](encryptWithdrawSide[F](kmsClient, config))( hexToByteArray(config.ethPrivate), ) yield Map("LM-D" -> lmd, "LM" -> lm, "ETH" -> eth) def allSaveFrontAndBack[F[_]: Async]( config: EthGatewaySetupConfig, depositDb: Connection, withdrawDb: Connection, frontAndBack: Map[String, (String, String)], ): F[Unit] = for _ <- saveFrontAndBack[F]( depositDb, config.depositDb.table, config.depositDb.valueColumn, withdrawDb, config.withdrawDb.table, config.withdrawDb.valueColumn, frontAndBack, "LM-D", ) _ <- saveFrontAndBack[F]( withdrawDb, config.withdrawDb.table, config.withdrawDb.valueColumn, depositDb, config.depositDb.table, config.depositDb.valueColumn, frontAndBack, "LM", ) _ <- saveFrontAndBack[F]( withdrawDb, config.withdrawDb.table, config.withdrawDb.valueColumn, depositDb, config.depositDb.table, config.depositDb.valueColumn, frontAndBack, "ETH", ) yield () def run(args: List[String]): IO[ExitCode] = // val config = EthGatewaySetupConfig() // // val resources = for // kmsClient <- connectKms[IO] // depositDb <- connectDatabase[IO]( // config.depositDb.host, // config.depositDb.db, // config.depositDb.table, // config.dbWriteAccount.user, // config.dbWriteAccount.password, // ) // withdrawDb <- connectDatabase[IO]( // config.withdrawDb.host, // config.withdrawDb.db, // config.withdrawDb.table, // config.dbWriteAccount.user, // config.dbWriteAccount.password, // ) // yield (kmsClient, depositDb, withdrawDb) // // connectKms[IO].use: kmsClient => // def dbEndpoint(conf: EthGatewaySetupConfig.DbConfig): Array[Byte] = // s"jdbc:mysql://${conf.host}:${conf.port}/${conf.db}?user=${conf.user}&password=${conf.password}".getBytes("UTF-8") // // val depositEndpoint = dbEndpoint(config.depositDb) // val withdrawEndpoint = dbEndpoint(config.withdrawDb) // // def toBase64(bytes: Array[Byte]): String = // ByteVector.view(bytes).toBase64 // // for // encryptedDepositDb <- encrypt[IO](kmsClient, config.depositKmsAlias)(depositEndpoint) // decryptedDepositDb <- decrypt[IO](kmsClient, config.depositKmsAlias)(encryptedDepositDb) // encryptedWithdrawDb <- encrypt[IO](kmsClient, config.withdrawKmsAlias)(withdrawEndpoint) // decryptedWithdrawDb <- decrypt[IO](kmsClient, config.withdrawKmsAlias)(encryptedWithdrawDb) // encryptedEthEndpointWithDepositKey <- encrypt[IO](kmsClient, config.depositKmsAlias): // config.ethEndpoint.getBytes("UTF-8") // encryptedEthEndpointWithWithdrawKey <- encrypt[IO](kmsClient, config.withdrawKmsAlias): // config.ethEndpoint.getBytes("UTF-8") // decryptedEthEndpointWithDepositKey <- decrypt[IO](kmsClient, config.depositKmsAlias): // encryptedEthEndpointWithDepositKey // decryptedEthEndpointWithWithdrawKey <- decrypt[IO](kmsClient, config.withdrawKmsAlias): // encryptedEthEndpointWithWithdrawKey // yield // println(s"Deposit DB: ${toBase64(encryptedDepositDb)}") // println(s"Decrypted Deposit DB: ${String(decryptedDepositDb, "UTF-8")}") // println(s"Withdraw DB: ${toBase64(encryptedWithdrawDb)}") // println(s"Decrypted Withdraw DB: ${String(decryptedWithdrawDb, "UTF-8")}") // println(s"ETH Endpoint with Deposit Key: ${toBase64(encryptedEthEndpointWithDepositKey)}") // println(s"ETH Endpoint with Withdraw Key: ${toBase64(encryptedEthEndpointWithWithdrawKey)}") // println(s"Decrypted ETH Endpoint with Deposit Key: ${String(decryptedEthEndpointWithDepositKey, "UTF-8")}") // println(s"Decrypted ETH Endpoint with Withdraw Key: ${String(decryptedEthEndpointWithWithdrawKey, "UTF-8")}") // ExitCode.Success // resources.use: (kmsClient, depositDb, withdrawDb) => // allEncryptAndDivide[IO](config, kmsClient) // .map: (keys: Map[String, (String, String)]) => // keys.foreach: // case (key, (front, back)) => // println(s""""${key}" -> ("${front}", "${back}")""") // () // .as(ExitCode.Success) // val keys = Map( // "LM-D" -> ("", ""), // "LM" -> ("", ""), // "ETH" -> ("", ""), // ) // // allSaveFrontAndBack[IO](config, depositDb, withdrawDb, keys) // .as(ExitCode.Success) val config = EthGatewaySetupSimpleConfig() extension (ba: Array[Byte]) def toHex: String = ByteVector.view(ba).toHex def toBase64: String = ByteVector.view(ba).toBase64 connectKms[IO].use: kmsClient => val Right(ethPrivate) = ByteVector.fromHexDescriptive(config.ethPrivate): @unchecked val Right(lmPrivate) = ByteVector.fromHexDescriptive(config.lmPrivate): @unchecked for encryptedEthEndpoint <- encrypt[IO](kmsClient, config.kmsAlias)(config.ethEndpoint.getBytes("UTF-8")) decryptedEthEndpoint <- decrypt[IO](kmsClient, config.kmsAlias)(encryptedEthEndpoint) encryptedEthPrivate <- encrypt[IO](kmsClient, config.kmsAlias)(ethPrivate.toArrayUnsafe) decryptedEthPrivate <- decrypt[IO](kmsClient, config.kmsAlias)(encryptedEthPrivate) encryptedLmPrivate <- encrypt[IO](kmsClient, config.kmsAlias)(lmPrivate.toArrayUnsafe) decryptedLmPrivate <- decrypt[IO](kmsClient, config.kmsAlias)(encryptedLmPrivate) yield println(s"Encrypted Eth Endpoint: ${encryptedEthEndpoint.toBase64}") println(s"Decrypted Eth Endpoint: ${new String(decryptedEthEndpoint, "UTF-8")}") println(s"Encrypted Eth Private: ${encryptedEthPrivate.toBase64}") println(s"Decrypted Eth Private: ${decryptedEthPrivate.toHex}") println(s"Encrypted LM Private: ${encryptedLmPrivate.toBase64}") println(s"Decrypted LM Private: ${decryptedLmPrivate.toHex}") ExitCode.Success ================================================ FILE: modules/eth-gateway-setup/src/main/scala/io/leisuremeta/chain/gateway/eth/setup/EthGatewaySetupSimpleConfig.scala ================================================ package io.leisuremeta.chain.gateway.eth.setup import pureconfig.* import pureconfig.generic.derivation.default.* import EthGatewaySetupConfig.* final case class EthGatewaySetupSimpleConfig( ethPrivate: String, lmPrivate: String, kmsAlias: String, ethEndpoint: String, ) derives ConfigReader object EthGatewaySetupSimpleConfig: def apply(): EthGatewaySetupSimpleConfig = ConfigSource.default.loadOrThrow[EthGatewaySetupSimpleConfig] final case class DbConfig( host: String, port: Int, db: String, table: String, valueColumn: String, user: String, password: String, ) final case class DbWriteAccountConfig( user: String, password: String, ) ================================================ FILE: modules/eth-gateway-withdraw/src/main/resources/application.conf.sample ================================================ //eth-chain-id = 5777 //eth-contract-address = "0x02886136510172E313932EA66FE7bDC4d4C2fcc4" //gateway-eth-address = "0x3E984778B16b5a0fE489Aac0b716078202400b6A" //gateway-endpoint = "http://127.0.0.1:8081" //local-server-port = 8082 //lm-endpoint = "http://127.0.0.1:8080" //kms-alias = "gateway_withdraw_key" //encrypted-eth-endpoint = "..." //encrypted-database-endpoint = "..." //database-table-name = "..." //database-value-column = "..." eth-chain-id = 11155111 eth-lm-contract-address = "0x02886136510172E313932EA66FE7bDC4d4C2fcc4" eth-multisig-contract-address = "0x02886136510172E313932EA66FE7bDC4d4C2fcc4" gateway-eth-address = "0x3E984778B16b5a0fE489Aac0b716078202400b6A" deposit-exempts = [ "0x3E984778B16b5a0fE489Aac0b716078202400b6A", ] lm-endpoint = "http://127.0.0.1:8080" kms-alias = "gateway_withdraw_key" encrypted-eth-endpoint = "..." encrypted-eth-private = "..." encrypted-lm-private = "..." target-gateway = "eth-gateway" ================================================ FILE: modules/eth-gateway-withdraw/src/main/scala/io/leisuremeta/chain/gateway/eth/EthGatewayWithdrawMain.scala ================================================ package io.leisuremeta.chain package gateway.eth import java.math.{BigInteger, MathContext} import java.nio.file.{Files, Paths, StandardOpenOption} import java.time.Instant import java.util.{ArrayList, Collections, Locale} //import java.util.concurrent.CompletableFuture import scala.collection.mutable.ArrayBuffer import scala.concurrent.duration.* import scala.jdk.CollectionConverters.* //import scala.jdk.OptionConverters.* import scala.util.Try import cats.data.EitherT import cats.effect.{Async, Clock, ExitCode, IO, IOApp, Resource} import cats.syntax.apply.* import cats.syntax.applicativeError.* import cats.syntax.either.* import cats.syntax.eq.* import cats.syntax.flatMap.* import cats.syntax.functor.* import cats.syntax.traverse.* //import com.github.jasync.sql.db.mysql.MySQLConnectionBuilder //import com.typesafe.config.{Config, ConfigFactory} import io.circe.Encoder import io.circe.generic.auto.* import io.circe.parser.decode import io.circe.syntax.given //import okhttp3.OkHttpClient //import okhttp3.logging.HttpLoggingInterceptor import org.web3j.abi.{FunctionEncoder, TypeReference} import org.web3j.abi.datatypes.{Address, Function, Type} import org.web3j.abi.datatypes.generated.Uint256 import org.web3j.crypto.{Credentials, RawTransaction} import org.web3j.protocol.Web3j import org.web3j.protocol.core.{ DefaultBlockParameter, DefaultBlockParameterName, Request as Web3jRequest, Response as Web3jResponse, } //import org.web3j.protocol.core.methods.response.EthFeeHistory.FeeHistory import org.web3j.protocol.core.methods.response.TransactionReceipt import org.web3j.protocol.exceptions.TransactionException import org.web3j.tx.RawTransactionManager import org.web3j.tx.response.PollingTransactionReceiptProcessor import scodec.bits.ByteVector //import software.amazon.awssdk.auth.credentials.{ // AwsCredentials, // StaticCredentialsProvider, //} //import software.amazon.awssdk.core.SdkBytes //import software.amazon.awssdk.regions.Region //import software.amazon.awssdk.services.kms.KmsAsyncClient //import software.amazon.awssdk.services.kms.model.DecryptRequest import sttp.client3.* import sttp.model.{MediaType, StatusCode} import lib.crypto.{CryptoOps, Hash} import lib.crypto.Hash.ops.* import lib.crypto.Sign.ops.* import lib.datatype.* import api.model.* import api.model.TransactionWithResult.ops.* import api.model.api_model.{AccountInfo, BalanceInfo, NftBalanceInfo} import api.model.token.* import common.* import common.client.* import org.web3j.abi.FunctionReturnDecoder import java.nio.charset.StandardCharsets object EthGatewayWithdrawMain extends IOApp: def submitTx[F[_]: Async: GatewayKmsClient]( sttp: SttpBackend[F, Any], lmAddress: String, account: Account, tx: Transaction, encryptedLmPrivateBase64: String, ): F[Unit] = GatewayDecryptService .getSimplifiedPlainTextResource[F](encryptedLmPrivateBase64) .value .flatMap: case Left(msg) => scribe.error(s"Failed to get LM private: $msg") Async[F].sleep(10.seconds) *> submitTx[F]( sttp, lmAddress, account, tx, encryptedLmPrivateBase64, ) case Right(lmPrivateResource) => lmPrivateResource.use: lmPrivateArray => val keyPair = CryptoOps.fromPrivate(BigInt(lmPrivateArray)) val Right(sig) = keyPair.sign(tx): @unchecked val signedTxs = Seq(Signed(AccountSignature(sig, account), tx)) scribe.info(s"Sending signed transactions: $signedTxs") given bodyJsonSerializer[A: Encoder]: BodySerializer[A] = (a: A) => val serialized = a.asJson.noSpaces StringBody(serialized, "UTF-8", MediaType.ApplicationJson) basicRequest .response(asStringAlways) .post(uri"http://$lmAddress/tx") .body(signedTxs) .send(sttp) .map: response => scribe.info(s"Response: $response") def initialLmSecretCheck[F[_]: Async: GatewayKmsClient]( sttp: SttpBackend[F, Any], web3j: Web3j, conf: GatewaySimpleConf, ): EitherT[F, String, Unit] = for gatewayInfoResponse <- EitherT.liftF: basicRequest .response(asStringAlways) .get(uri"http://${conf.lmEndpoint}/account/${conf.targetGateway}") .send(sttp) gatewayAccountInfo <- EitherT.fromEither[F]: decode[AccountInfo](gatewayInfoResponse.body).leftMap(_.getMessage()) lmSecretResource <- GatewayDecryptService .getSimplifiedPlainTextResource[F](conf.encryptedLmPrivate) pks <- EitherT.liftF: lmSecretResource.use: lmSecretArray => Async[F].delay: val keyPair = CryptoOps.fromPrivate(BigInt(1, lmSecretArray)) PublicKeySummary.fromPublicKeyHash(keyPair.publicKey.toHash) _ <- EitherT.cond[F]( gatewayAccountInfo.publicKeySummaries.contains(pks), (), s"Fail to check lm secret. ${conf.targetGateway} does not have pks ${pks}", ) yield () def checkLoop[F[_]: Async: GatewayKmsClient]( sttp: SttpBackend[F, Any], web3j: Web3j, conf: GatewaySimpleConf, ): F[Unit] = def run: F[Unit] = for _ <- Async[F].delay(scribe.info(s"Withdrawal check started")) _ <- checkLmWithdrawal[F]( sttp, web3j, conf, ) _ <- Async[F].delay(scribe.info(s"Withdrawal check finished")) yield () def loop: F[Unit] = for _ <- run.orElse(Async[F].unit) _ <- Async[F].sleep(10000.millis) _ <- loop yield () loop def getFungibleBalance[F[_]: Async]( sttp: SttpBackend[F, Any], lmEndpoint: String, targetGateway: String, ): F[Map[TokenDefinitionId, BalanceInfo]] = basicRequest .response(asStringAlways) .get(uri"http://$lmEndpoint/balance/$targetGateway?movable=free") .send(sttp) .map: response => if response.code.isSuccess then decode[Map[TokenDefinitionId, BalanceInfo]](response.body) match case Right(balanceInfoMap) => balanceInfoMap case Left(error) => scribe.error(s"Error decoding balance info: $error") scribe.error(s"response: ${response.body}") Map.empty else if response.code.code === StatusCode.NotFound.code then scribe.info( s"balance of account $targetGateway not found: ${response.body}", ) Map.empty else scribe.error(s"Error getting balance: ${response.body}") Map.empty def getNftBalance[F[_]: Async]( sttp: SttpBackend[F, Any], lmEndpoint: String, targetGateway: String, ): F[Map[TokenId, NftBalanceInfo]] = basicRequest .response(asStringAlways) .get(uri"http://$lmEndpoint/nft-balance/$targetGateway?movable=free") .send(sttp) .map: response => if response.code.isSuccess then decode[Map[TokenId, NftBalanceInfo]](response.body) match case Right(balanceInfoMap) => balanceInfoMap case Left(error) => scribe.error(s"Error decoding nft-balance info: $error") scribe.error(s"response: ${response.body}") Map.empty else if response.code.code === StatusCode.NotFound.code then scribe.info( s"nft-balance of account $targetGateway not found: ${response.body}", ) Map.empty else scribe.error(s"Error getting nft-balance: ${response.body}") Map.empty def getAccountInfo[F[_]: Async]( sttp: SttpBackend[F, Any], lmEndpoint: String, account: Account, ): F[Option[AccountInfo]] = basicRequest .response(asStringAlways) .get(uri"http://$lmEndpoint/account/${account.utf8.value}") .send(sttp) .map: response => if response.code.isSuccess then decode[AccountInfo](response.body) match case Right(accountInfo) => Some(accountInfo) case Left(error) => scribe.error(s"Error decoding account info: $error") None else if response.code.code === StatusCode.NotFound.code then scribe.info(s"account info not found: ${response.body}") None else scribe.error(s"Error getting account info: ${response.body}") None def checkLmWithdrawal[F[_]: Async: Clock: GatewayKmsClient]( sttp: SttpBackend[F, Any], web3j: Web3j, conf: GatewaySimpleConf, ): F[Unit] = getFungibleBalance(sttp, conf.lmEndpoint, conf.targetGateway) .flatMap { (balanceMap: Map[TokenDefinitionId, BalanceInfo]) => val gatewayAccount = Account(Utf8.unsafeFrom(conf.targetGateway)) val LM = TokenDefinitionId(Utf8.unsafeFrom("LM")) balanceMap .get(LM) .toList .flatMap(_.unused.toSeq) .filterNot(_._2.signedTx.sig.account === gatewayAccount) .traverse { case (txHash, txWithResult) => txWithResult.signedTx.value match case tx: Transaction.TokenTx.TransferFungibleToken => { scribe.info: s"Try to handle ${txWithResult.signedTx.sig.account}'s tx: $tx" for amount <- EitherT.fromOption[F]( tx.outputs.get(gatewayAccount), s"No output amount to send to gateway", ) accountInfo <- EitherT.fromOptionF( getAccountInfo( sttp, conf.lmEndpoint, txWithResult.signedTx.sig.account, ), s"No account info of ${txWithResult.signedTx.sig.account}", ) ethAddress <- EitherT.fromOption[F]( accountInfo.ethAddress, s"No eth address of ${txWithResult.signedTx.sig.account}", ) _ <- EitherT.liftF: requestEthLmMultisigTransfer( web3j = web3j, ethChainId = conf.ethChainId, multiSigContractAddress = conf.ethMultisigContractAddress, encryptedEthPrivate = conf.encryptedEthPrivate, gatewayEthAddress = conf.gatewayEthAddress, txId = txHash, receiverEthAddress = ethAddress.utf8.value, amount = amount, ) now <- EitherT.liftF(Clock[F].realTimeInstant) tx1 = Transaction.TokenTx.TransferFungibleToken( networkId = NetworkId(BigNat.unsafeFromLong(1000L)), createdAt = now, tokenDefinitionId = LM, inputs = Set(txHash.toSignedTxHash), outputs = Map(gatewayAccount -> amount), memo = Some(Utf8.unsafeFrom { s"After withdrawing of ${txWithResult.signedTx.sig.account}'s $amount" }), ) _ <- EitherT.liftF: submitTx( sttp, conf.lmEndpoint, gatewayAccount, tx1, conf.encryptedLmPrivate, ) yield () }.leftMap { msg => scribe.error(msg) msg }.value case _ => Async[F].delay(().asRight[String]) } } .as(()) // def checkNftWithdrawal[F[_] // : Async: Clock: GatewayApiClient: GatewayDatabaseClient: GatewayKmsClient]( // sttp: SttpBackend[F, Any], // web3j: Web3j, // conf: GatewayConf, // ): F[Unit] = getNftBalance(sttp, conf.lmEndpoint, conf.targetGateway) // .flatMap { (balanceMap: Map[TokenId, NftBalanceInfo]) => // // val gatewayAccount = Account(Utf8.unsafeFrom(conf.targetGateway)) // // balanceMap.toSeq.traverse { case (tokenId, balanceInfo) => // balanceInfo.tx.signedTx.value match // case tx: Transaction.TokenTx.TransferNFT // if balanceInfo.tx.signedTx.sig.account =!= gatewayAccount => // { // for // info <- OptionT: // getAccountInfo( // sttp, // conf.lmEndpoint, // balanceInfo.tx.signedTx.sig.account, // ) // ethAddress <- OptionT.fromOption[F](info.ethAddress) // _ <- OptionT.liftF: // mintEthNft[F]( // web3j = web3j, // ethChainId = conf.ethChainId, // ethNftContract = conf.ethContractAddress, // gatewayEthAddress = conf.gatewayEthAddress, // receiverEthAddress = ethAddress.utf8.value, // tokenId = tokenId, // ) // now <- OptionT.liftF(Clock[F].realTimeInstant) // tx1 = tx.copy( // createdAt = now, // input = balanceInfo.tx.signedTx.toHash, // output = gatewayAccount, // memo = // Some(Utf8.unsafeFrom("gateway balance after withdrawal")), // ) // _ <- OptionT.liftF: // submitTx[F](sttp, conf.lmEndpoint, gatewayAccount, tx1) // yield () // }.value // case _ => Async[F].delay(None) // } // } // .as(()) // def transferEthLM[F[_] // : Async: GatewayApiClient: GatewayDatabaseClient: GatewayKmsClient]( // web3j: Web3j, // ethChainId: Int, // ethLmContract: String, // gatewayEthAddress: String, // receiverEthAddress: String, // amount: BigNat, // ): F[Unit] = // // scribe.info(s"Transfer eth LM to ${receiverEthAddress}") // // val mintParams = new ArrayList[Type[?]]() // mintParams.add(new Address(receiverEthAddress)) // mintParams.add(new Uint256(amount.toBigInt.bigInteger)) // // val returnTypes = Collections.emptyList[TypeReference[?]]() // // val transferTxData = FunctionEncoder.encode: // new Function("transfer", mintParams, returnTypes) // // sendEthTransaction[F]( // web3j = web3j, // ethChainId = ethChainId, // contractAddress = ethLmContract, // txData = transferTxData, // gatewayEthAddress = gatewayEthAddress, // ).as(()) def requestEthLmMultisigTransfer[F[_]: Async: GatewayKmsClient]( web3j: Web3j, ethChainId: Int, gatewayEthAddress: String, multiSigContractAddress: String, encryptedEthPrivate: String, txId: Hash.Value[TransactionWithResult], receiverEthAddress: String, amount: BigNat, ): F[Unit] = scribe.info(s"Transfer eth LM to ${receiverEthAddress}") val mintParams = new ArrayList[Type[?]]() mintParams.add(new Uint256(txId.toUInt256Bytes.toBigInt.bigInteger)) mintParams.add(new Address(receiverEthAddress)) mintParams.add(new Uint256(amount.toBigInt.bigInteger)) val returnTypes = Collections.emptyList[TypeReference[?]]() val transferTxData = FunctionEncoder.encode: new Function("addTransaction", mintParams, returnTypes) sendEthTransaction[F]( web3j = web3j, ethChainId = ethChainId, contractAddress = multiSigContractAddress, txData = transferTxData, gatewayEthAddress = gatewayEthAddress, encryptedEthPrivate = encryptedEthPrivate, ).as(()) // def mintEthNft[F[_] // : Async: GatewayApiClient: GatewayDatabaseClient: GatewayKmsClient]( // web3j: Web3j, // ethChainId: Int, // ethNftContract: String, // gatewayEthAddress: String, // receiverEthAddress: String, // tokenId: TokenId, // ): F[Unit] = // // val tokenIdBigInt = BigInt(tokenId.utf8.value) // // val mintParams = new ArrayList[Type[?]]() // mintParams.add(new Address(receiverEthAddress)) // mintParams.add(new Uint256(tokenIdBigInt.bigInteger)) // // val returnTypes = Collections.emptyList[TypeReference[?]]() // // val mintTxData = FunctionEncoder.encode { // new Function("safeMint", mintParams, returnTypes) // } // // sendEthTransaction[F]( // web3j = web3j, // ethChainId = ethChainId, // contractAddress = ethNftContract, // txData = mintTxData, // gatewayEthAddress = gatewayEthAddress, // ).as(()) def requestToF[F[_]: Async, A, B, C <: Web3jResponse[B], D]( request: Web3jRequest[A, C], )(map: C => D): F[D] = Async[F] .recoverWith: Async[F] .fromCompletableFuture(Async[F].delay(request.sendAsync())) .map(map) .apply: case t: Throwable => scribe.error(t) Async[F].sleep(10.seconds) *> requestToF(request)(map) def sendEthTransaction[F[_]: Async: GatewayKmsClient]( web3j: Web3j, ethChainId: Int, contractAddress: String, txData: String, gatewayEthAddress: String, encryptedEthPrivate: String, ): F[Unit] = GatewayDecryptService .getSimplifiedPlainTextResource[F](encryptedEthPrivate) .value .flatMap: case Left(msg) => scribe.error(s"Fail to get eth private key: $msg") Async[F].sleep(10.seconds) *> sendEthTransaction[F]( web3j, ethChainId, contractAddress, txData, gatewayEthAddress, encryptedEthPrivate, ) case Right(ethResource) => ethResource.use: ethPrivateByteArray => val ethPrivate = ByteVector.view(ethPrivateByteArray).toHex val credential = Credentials.create(ethPrivate) assert( credential.getAddress() === gatewayEthAddress .toLowerCase(Locale.ENGLISH), s"invalid gateway eth address: ${credential.getAddress} vs $gatewayEthAddress", ) val TX_END_CHECK_DURATION = 20000 val TX_END_CHECK_RETRY = 9 val receiptProcessor = new PollingTransactionReceiptProcessor( web3j, TX_END_CHECK_DURATION, TX_END_CHECK_RETRY, ) val manager = new RawTransactionManager( web3j, credential, ethChainId, receiptProcessor, ) val GAS_LIMIT = 600_000 def loop( lastTrial: Option[(BigInteger, BigInteger, String)], ): F[Unit] = def getMaxPriorityFeePerGas(): F[BigInteger] = requestToF(web3j.ethMaxPriorityFeePerGas()): _.getMaxPriorityFeePerGas() def getBaseFee(): F[BigInteger] = val blockCount: String = BigInt(9).toString(16) val newestBlock: DefaultBlockParameter = DefaultBlockParameterName.LATEST val rewardPercentiles: java.util.List[java.lang.Double] = ArrayBuffer[java.lang.Double](0, 0.5, 1, 1.5, 3, 80).asJava requestToF { web3j.ethFeeHistory(blockCount, newestBlock, rewardPercentiles) }.apply: response => val history = response.getFeeHistory() val baseFees = history.getBaseFeePerGas().asScala.toList val mean = BigDecimal(baseFees.map(BigInt(_)).sum) / 10 val std = baseFees .map(x => BigDecimal( (BigDecimal(x) - mean) .pow(2) .bigDecimal .sqrt(MathContext.DECIMAL32), ), ) .sum / 10 val targetBaseFees = (mean + std + 0.5).toBigInt targetBaseFees.bigInteger def getNonce(): F[BigInteger] = requestToF { web3j.ethGetTransactionCount( gatewayEthAddress, DefaultBlockParameterName.LATEST, ) }(_.getTransactionCount()) def sendNewTx( baseFee: BigInteger, maxPriorityFeePerGas: BigInteger, ): F[Option[String]] = for nonce <- getNonce() _ <- Async[F].delay { scribe.info(s"Nonce: $nonce") } tx = RawTransaction.createTransaction( ethChainId, nonce, BigInteger.valueOf(GAS_LIMIT), contractAddress, BigInteger.ZERO, txData, maxPriorityFeePerGas, baseFee `add` maxPriorityFeePerGas, ) txResponseOption <- Async[F] .blocking { manager.signAndSend(tx) } .map: resp => if resp.hasError() then val e = resp.getError() scribe.info: s"Error in sending tx: #(${e.getCode()}) ${e.getMessage()}" writeFailLog(txData) else scribe.info(s"Sending Eth Tx: ${resp.getResult()}") Option(resp.getResult) yield txResponseOption def getReceipt( txResponse: String, ): F[Either[Throwable, TransactionReceipt]] = Async[F].blocking: Try( receiptProcessor.waitForTransactionReceipt(txResponse), ).toEither for _ <- Async[F].delay { scribe.info(s"Last trial: ${lastTrial}") } baseFee <- getBaseFee() _ <- Async[F].delay { scribe.info(s"New base fee: ${baseFee}") } maxPriorityFeePerGas <- getMaxPriorityFeePerGas() _ <- Async[F].delay: scribe.info(s"Max Priority Fee Per Gas: $maxPriorityFeePerGas") txIdOption <- lastTrial match case Some((oldBaseFee, oldPriorityFee, txId)) if (oldBaseFee `add` oldPriorityFee).compareTo( baseFee `add` maxPriorityFeePerGas, ) >= 0 => scribe.info(s"New base fee is less than old one") Async[F].pure(Some(txId)) case _ => sendNewTx(baseFee, maxPriorityFeePerGas).map: _.orElse(lastTrial.map(_._3)) _ <- txIdOption match case Some(txId) => for receiptEither <- getReceipt(txId) _ <- receiptEither match case Left(e) => e match case te: TransactionException => scribe.info(s"Timeout: ${te.getMessage()}") Async[F].delay(()) // loop(Some(baseFee, maxPriorityFeePerGas, txId)) case _ => scribe.error: s"Fail to send transaction: ${e.getMessage()}" Async[F].delay(()) // loop(Some(baseFee, maxPriorityFeePerGas, txId)) case Right(receipt) => if receipt.isStatusOK() then Async[F].delay: scribe.info: s"transaction ${receipt.getTransactionHash()} saved to block #${receipt.getBlockNumber()}" // BigInt(receipt.getBlockNumber()) () else scribe.error: s"transaction ${receipt.getTransactionHash()} failed with receipt:${receipt}" Async[F].delay(()) // loop(None) yield () case None => // Async[F].sleep(1.minute) *> loop(None) Async[F].delay(()) yield () loop(None) @SuppressWarnings(Array("org.wartremover.warts.MutableDataStructures")) def writeFailLog[F[_]: Async](txData: String): F[Unit] = Async[F].blocking: val typeRefAddress: TypeReference[Type[?]] = (new TypeReference[Address]() {}).asInstanceOf[TypeReference[Type[?]]] val typeRefUint256: TypeReference[Type[?]] = (new TypeReference[Uint256]() {}).asInstanceOf[TypeReference[Type[?]]] val parseTx = FunctionReturnDecoder.decode(txData.drop(10), List(typeRefUint256, typeRefAddress, typeRefUint256).asJava).asScala val path = Paths.get("failed_tx") val res = parseTx.map(e => e.getValue).mkString("", " ", "\n") val _ = Files.write( path, res.getBytes(StandardCharsets.UTF_8), StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.APPEND, ) def run(args: List[String]): IO[ExitCode] = val conf = GatewaySimpleConf.loadOrThrow() GatewayResource .getSimpleResource[IO](conf) .use: (kms, web3j, sttp) => given GatewayKmsClient[IO] = kms initialLmSecretCheck[IO](sttp, web3j, conf) *> EitherT.liftF: checkLoop[IO]( sttp = sttp, web3j = web3j, conf = conf, ) .value .map: case Left(error) => scribe.error(s"Error: $error") case Right(result) => scribe.info(s"Result: $result") .as(ExitCode.Success) ================================================ FILE: modules/jvm-client/src/main/scala/io/leisuremeta/chain/jvmclient/JvmClientMain.scala ================================================ package io.leisuremeta.chain package jvmclient import cats.effect.{ExitCode, IO, IOApp} import cats.syntax.traverse.* import com.typesafe.config.ConfigFactory import io.circe.syntax.* import scodec.bits.hex import sttp.client3.* import sttp.client3.armeria.cats.ArmeriaCatsBackend //import sttp.tapir.client.sttp.SttpClientInterpreter //import api.LeisureMetaChainApi import api.model.* import api.model.account.* //import api.model.reward.* import api.model.token.* //import api.model.TransactionWithResult.ops.* import lib.datatype.* import lib.crypto.* import lib.crypto.Hash.ops.* import lib.crypto.Sign.ops.* import node.NodeConfig import scodec.bits.ByteVector object JvmClientMain extends IOApp: java.security.Security.addProvider( new org.bouncycastle.jce.provider.BouncyCastleProvider(), ) val aliceKey = CryptoOps.fromPrivate( BigInt( "b229e76b742616db3ac2c5c2418f44063fcc5fcc52a08e05d4285bdb31acba06", 16, ), ) val alicePKS = PublicKeySummary.fromPublicKeyHash(aliceKey.publicKey.toHash) val alice = Account(Utf8.unsafeFrom("alice")) val bob = Account(Utf8.unsafeFrom("bob")) val carol = Account(Utf8.unsafeFrom("carol")) def sign(account: Account, key: KeyPair)(tx: Transaction): Signed.Tx = key.sign(tx).map { sig => Signed(AccountSignature(sig, account), tx) } match case Right(signedTx) => signedTx case Left(msg) => throw Exception(msg) def signAlice = sign(alice, aliceKey) val txs: IndexedSeq[Transaction] = IndexedSeq( Transaction.AccountTx.CreateAccount( networkId = NetworkId(BigNat.unsafeFromLong(2021L)), createdAt = java.time.Instant.parse("2023-01-11T19:01:00.00Z"), account = alice, ethAddress = None, guardian = None, // memo = None, ), Transaction.AccountTx.CreateAccountWithExternalChainAddresses( networkId = NetworkId(BigNat.unsafeFromLong(2021L)), createdAt = java.time.Instant.parse("2023-01-11T19:01:30.00Z"), account = bob, externalChainAddresses = Map( ExternalChain.ETH -> ExternalChainAddress( Utf8.unsafeFrom(alicePKS.toBytes.toHex), ), ), guardian = Some(alice), memo = None, ), Transaction.AccountTx.UpdateAccountWithExternalChainAddresses( networkId = NetworkId(BigNat.unsafeFromLong(2021L)), createdAt = java.time.Instant.parse("2023-01-11T19:01:40.00Z"), account = bob, externalChainAddresses = Map( ExternalChain.ETH -> ExternalChainAddress( Utf8.unsafeFrom(alicePKS.toBytes.toHex), ), ), guardian = Some(alice), memo = Some(Utf8.unsafeFrom("bob updated")), ), Transaction.GroupTx.CreateGroup( networkId = NetworkId(BigNat.unsafeFromLong(2021L)), createdAt = java.time.Instant.parse("2023-01-11T19:02:00.00Z"), groupId = GroupId(Utf8.unsafeFrom("mint-group")), name = Utf8.unsafeFrom("Mint Group"), coordinator = alice, // memo = None, ), Transaction.GroupTx.AddAccounts( networkId = NetworkId(BigNat.unsafeFromLong(2021L)), createdAt = java.time.Instant.parse("2023-01-11T19:03:00.00Z"), groupId = GroupId(Utf8.unsafeFrom("mint-group")), accounts = Set(alice), // memo = None, ), Transaction.TokenTx.DefineToken( networkId = NetworkId(BigNat.unsafeFromLong(2021L)), createdAt = java.time.Instant.parse("2023-01-11T19:04:00.00Z"), definitionId = TokenDefinitionId(Utf8.unsafeFrom("LM")), name = Utf8.unsafeFrom("LeisureMeta"), symbol = Some(Utf8.unsafeFrom("LM")), minterGroup = Some(GroupId(Utf8.unsafeFrom("mint-group"))), nftInfo = None, ), Transaction.TokenTx.DefineTokenWithPrecision( networkId = NetworkId(BigNat.unsafeFromLong(2021L)), createdAt = java.time.Instant.parse("2023-01-11T19:05:00.00Z"), definitionId = TokenDefinitionId(Utf8.unsafeFrom("nft-with-precision")), name = Utf8.unsafeFrom("NFT with precision"), symbol = Some(Utf8.unsafeFrom("NFTWP")), minterGroup = Some(GroupId(Utf8.unsafeFrom("mint-group"))), nftInfo = Some( NftInfoWithPrecision( minter = alice, rarity = Map( Rarity(Utf8.unsafeFrom("LGDY")) -> BigNat.unsafeFromLong(100), Rarity(Utf8.unsafeFrom("UNIQ")) -> BigNat.unsafeFromLong(66), Rarity(Utf8.unsafeFrom("EPIC")) -> BigNat.unsafeFromLong(33), Rarity(Utf8.unsafeFrom("RARE")) -> BigNat.unsafeFromLong(10), ), precision = BigNat.unsafeFromLong(2), dataUrl = Utf8.unsafeFrom( "https://www.playnomm.com/data/nft-with-precision.json", ), contentHash = UInt256 .from( hex"2475a387f22c248c5a3f09cea0ef624484431c1eaf8ffbbf98a4a27f43fabc84", ) .toOption .get, ), ), // memo = None, ), Transaction.TokenTx.MintNFT( networkId = NetworkId(BigNat.unsafeFromLong(2021L)), createdAt = java.time.Instant.parse("2023-01-11T19:06:00.00Z"), tokenDefinitionId = TokenDefinitionId(Utf8.unsafeFrom("nft-with-precision")), tokenId = TokenId(Utf8.unsafeFrom("2022061710000513118")), rarity = Rarity(Utf8.unsafeFrom("EPIC")), dataUrl = Utf8.unsafeFrom( "https://d3j8b1jkcxmuqq.cloudfront.net/temp/collections/TEST_NOMM4/NFT_ITEM/F7A92FB1-B29F-4E6F-BEF1-47C6A1376D68.jpg", ), contentHash = UInt256 .from( hex"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", ) .toOption .get, output = alice, // memo = None, ), Transaction.TokenTx.MintNFTWithMemo( networkId = NetworkId(BigNat.unsafeFromLong(2021L)), createdAt = java.time.Instant.parse("2023-01-11T19:07:00.00Z"), tokenDefinitionId = TokenDefinitionId(Utf8.unsafeFrom("nft-with-precision")), tokenId = TokenId(Utf8.unsafeFrom("2022061710000513118")), rarity = Rarity(Utf8.unsafeFrom("EPIC")), dataUrl = Utf8.unsafeFrom( "https://d3j8b1jkcxmuqq.cloudfront.net/temp/collections/TEST_NOMM4/NFT_ITEM/F7A92FB1-B29F-4E6F-BEF1-47C6A1376D68.jpg", ), contentHash = UInt256 .from( hex"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", ) .toOption .get, output = alice, memo = Some(Utf8.unsafeFrom("Test Minting NFT #2022061710000513118")), ), Transaction.TokenTx.UpdateNFT( networkId = NetworkId(BigNat.unsafeFromLong(2021L)), createdAt = java.time.Instant.parse("2023-01-11T19:08:00.00Z"), tokenDefinitionId = TokenDefinitionId(Utf8.unsafeFrom("nft-with-precision")), tokenId = TokenId(Utf8.unsafeFrom("2022061710000513118")), rarity = Rarity(Utf8.unsafeFrom("EPIC")), dataUrl = Utf8.unsafeFrom( "https://d3j8b1jkcxmuqq.cloudfront.net/temp/collections/TEST_NOMM4/NFT_ITEM/F7A92FB1-B29F-4E6F-BEF1-47C6A1376D68.jpg", ), contentHash = UInt256 .from( hex"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", ) .toOption .get, output = alice, memo = Some(Utf8.unsafeFrom("Test Updating NFT #2022061710000513118")), ), Transaction.TokenTx.CreateSnapshots( networkId = NetworkId(BigNat.unsafeFromLong(2021L)), createdAt = java.time.Instant.parse("2023-01-11T19:09:00.00Z"), definitionIds = Set( TokenDefinitionId(Utf8.unsafeFrom("LM")), TokenDefinitionId(Utf8.unsafeFrom("nft-with-precision")), ), memo = Some(Utf8.unsafeFrom("Snapshot for NFT")), ), ) override def run(args: List[String]): IO[ExitCode] = ArmeriaCatsBackend.resource[IO]().use { backend => val loadConfig = IO.blocking(ConfigFactory.load) NodeConfig.load[IO](loadConfig).value.flatMap { case Right(config) => // val baseUri = uri"http://localhost:${config.local.port}" // val postTxClient = SttpClientInterpreter().toClient( // LeisureMetaChainApi.postTxEndpoint, // Some(baseUri), // backend, // ) txs.toList .traverse: tx => val signedTx = signAlice(tx) val json = Seq(signedTx).asJson.spaces2 println(json) println(Seq(tx.toHash).asJson.noSpaces) IO.unit // for response <- // postTxClient(Seq(signedTx)) // yield // println(response) // ExitCode.Success .as(ExitCode.Success) case Left(err) => IO.println(err).as(ExitCode.Error) } } ================================================ FILE: modules/lib/js/src/main/scala/io/leisuremeta/chain/lib/crypto/CryptoOps.scala ================================================ package io.leisuremeta.chain.lib package crypto import scala.scalajs.js.JSConverters.* import scala.scalajs.js.typedarray.Uint8Array import io.github.iltotore.iron.* import scodec.bits.ByteVector import typings.bnJs.bnJsStrings.hex import typings.elliptic.mod.{ec as EC} import typings.elliptic.mod.ec.{KeyPair as JsKeyPair} import typings.jsSha3.mod.{keccak256 as jsKeccak256} import datatype.UInt256 object CryptoOps: val ec: EC = new EC("secp256k1") def generate(): KeyPair = ec.genKeyPair().toScala def fromPrivate(privateKey: BigInt): KeyPair = ec.keyFromPrivate(privateKey.toByteArray.toUint8Array).toScala @SuppressWarnings(Array("org.wartremover.warts.Throw")) def keccak256(input: Array[Byte]): Array[Byte] = val hexString: String = jsKeccak256.hex(input.toUint8Array) ByteVector .fromHexDescriptive(hexString) .fold(e => throw new Exception(e), _.toArray) def sign( keyPair: KeyPair, transactionHash: Array[Byte], ): Either[String, Signature] = val jsSig = keyPair.toJs.sign(transactionHash.toUint8Array) val recoveryParamEither:Either[String, Int] = (jsSig.recoveryParam: Any) match case d: Double => Right[String, Int](d.toInt) case other => Left[String, Int](s"Expected double but received: $other") for recoveryParam <- recoveryParamEither v <- (27 + recoveryParam).refineEither[Signature.HeaderRange] r <- UInt256.from(BigInt(jsSig.r.toString_hex(hex), 16)).left.map(_.msg) s <- UInt256.from(BigInt(jsSig.s.toString_hex(hex), 16)).left.map(_.msg) yield Signature(v, r, s) @SuppressWarnings(Array("org.wartremover.warts.TripleQuestionMark")) def recover( signature: Signature, hashArray: Array[Byte], ): Either[String, PublicKey] = ??? extension(jsKeyPair: JsKeyPair) @SuppressWarnings(Array("org.wartremover.warts.Throw")) def toScala: KeyPair = val privHex = jsKeyPair.getPrivate().toString_hex(hex) val pubKey = jsKeyPair.getPublic() val xHex = pubKey.getX().toString_hex(hex) val yHex = pubKey.getY().toString_hex(hex) val xBigInt = BigInt(xHex, 16) val yBigInt = BigInt(yHex, 16) val pBigInt = BigInt(privHex, 16) (for p <- UInt256.from(pBigInt) x <- UInt256.from(xBigInt) y <- UInt256.from(yBigInt) yield KeyPair(p, PublicKey(x, y))).getOrElse( throw new Exception(s"Wrong keyPair: $privHex, $xHex, $yHex"), ) extension(keyPair: KeyPair) def toJs: JsKeyPair = ec.keyFromPrivate(keyPair.privateKey.toByteArray.toUint8Array) extension(byteArray: Array[Byte]) def toUint8Array: Uint8Array = Uint8Array.from[Byte](byteArray.toJSArray, _.toShort) ================================================ FILE: modules/lib/jvm/src/main/scala/io/leisuremeta/chain/lib/crypto/CryptoOps.scala ================================================ package io.leisuremeta.chain.lib package crypto import java.math.BigInteger import java.security.{KeyPairGenerator, SecureRandom, Security} import java.security.spec.ECGenParameterSpec import java.util.Arrays import cats.Eq import cats.syntax.either.given import cats.syntax.eq.given import io.github.iltotore.iron.* import org.bouncycastle.asn1.x9.{X9ECParameters, X9IntegerConverter} import org.bouncycastle.crypto.digests.SHA256Digest import org.bouncycastle.crypto.ec.CustomNamedCurves import org.bouncycastle.crypto.params.{ ECDomainParameters, ECPrivateKeyParameters, } import org.bouncycastle.crypto.signers.{ECDSASigner, HMacDSAKCalculator} import org.bouncycastle.jcajce.provider.asymmetric.ec.{ BCECPrivateKey, BCECPublicKey, } import org.bouncycastle.jcajce.provider.digest.Keccak import org.bouncycastle.jce.provider.BouncyCastleProvider import org.bouncycastle.math.ec.{ ECAlgorithms, ECPoint, FixedPointCombMultiplier, } import org.bouncycastle.math.ec.custom.sec.SecP256K1Curve import shapeless3.typeable.syntax.typeable.cast import datatype.{UInt256, UInt256BigInt} import failure.UInt256RefineFailure @SuppressWarnings(Array("org.wartremover.warts.Equals")) object CryptoOps: locally: if Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null then val _ = Security.addProvider(new BouncyCastleProvider()) val secureRandom: SecureRandom = new SecureRandom() @SuppressWarnings(Array("org.wartremover.warts.Throw")) def generate(): KeyPair = val gen = KeyPairGenerator.getInstance("ECDSA", "BC") val spec = new ECGenParameterSpec("secp256k1") gen.initialize(spec, secureRandom) val pair = gen.generateKeyPair val keyPairOption = for bcecPrivate <- pair.getPrivate.cast[BCECPrivateKey] bcecPublic <- pair.getPublic.cast[BCECPublicKey] privateKey <- UInt256.from(BigInt(bcecPrivate.getD)).toOption publicKey <- PublicKey .fromByteArray(bcecPublic.getQ.getEncoded(false).tail) .toOption yield KeyPair(privateKey, publicKey) keyPairOption.getOrElse: throw new Exception(s"Wrong keypair result: $pair") @SuppressWarnings(Array("org.wartremover.warts.Throw")) def fromPrivate(privateKey: BigInt): KeyPair = val point: ECPoint = new FixedPointCombMultiplier() .multiply(Curve.getG, privateKey.bigInteger mod Curve.getN) val encoded: Array[Byte] = point.getEncoded(false) val keypairEither: Either[UInt256RefineFailure, KeyPair] = for private256 <- UInt256.from(privateKey) public <- PublicKey.fromByteArray: Arrays.copyOfRange(encoded, 1, encoded.length) yield KeyPair(private256, public) keypairEither match case Right(keypair) => keypair case Left(UInt256RefineFailure(msg)) => throw new Exception(msg) given Eq[Array[Byte]] = Eq.fromUniversalEquals given Eq[Option[PublicKey]] = Eq.fromUniversalEquals def keccak256(input: Array[Byte]): Array[Byte] = val kecc = new Keccak.Digest256() kecc.update(input, 0, input.length) kecc.digest() def sign( keyPair: KeyPair, transactionHash: Array[Byte], ): Either[String, Signature] = val signer = new ECDSASigner(new HMacDSAKCalculator(new SHA256Digest())) signer.init( true, new ECPrivateKeyParameters(keyPair.privateKey.bigInteger, Curve), ) val Array(r, sValue) = signer.generateSignature(transactionHash) val s = if sValue.compareTo(HalfCurveOrder) > 0 then Curve.getN subtract sValue else sValue for r256 <- UInt256.from(BigInt(r)).leftMap(_.msg) s256 <- UInt256.from(BigInt(s)).leftMap(_.msg) recId <- (0 until 4) .find: id => recoverFromSignature(id, r256, s256, transactionHash) === Some(keyPair.publicKey) .toRight: "Could not construct a recoverable key. The credentials might not be valid." v <- (recId + 27).refineEither[Signature.HeaderRange] yield Signature(v, r256, s256) def recover( signature: Signature, hashArray: Array[Byte], ): Either[String, PublicKey] = val header = signature.v & 0xff val recId = header - 27 recoverFromSignature(recId, signature.r, signature.s, hashArray) .toRight("Could not recover public key from signature") private def recoverFromSignature( recId: Int, r: UInt256BigInt, s: UInt256BigInt, message: Array[Byte], ): Option[PublicKey] = val n = Curve.getN val x = r.bigInteger add (n multiply BigInteger.valueOf(recId.toLong / 2)) val prime = SecP256K1Curve.q if x.compareTo(prime) >= 0 then None else val R = def decompressKey(xBN: BigInteger, yBit: Boolean): ECPoint = val x9 = new X9IntegerConverter() val compEnc: Array[Byte] = x9.integerToBytes(xBN, 1 + x9.getByteLength(Curve.getCurve())) compEnc(0) = if yBit then 0x03 else 0x02 Curve.getCurve().decodePoint(compEnc) decompressKey(x, (recId & 1) === 1) if !R.multiply(n).isInfinity() then None else val e = new BigInteger(1, message) val eInv = BigInteger.ZERO subtract e mod n val rInv = r.bigInteger modInverse n val srInv = rInv multiply s.bigInteger mod n val eInvrInv = rInv multiply eInv mod n val q: ECPoint = ECAlgorithms.sumOfTwoMultiplies(Curve.getG(), eInvrInv, R, srInv) PublicKey.fromByteArray(q.getEncoded(false).tail).toOption val CurveParams: X9ECParameters = CustomNamedCurves.getByName("secp256k1") val Curve: ECDomainParameters = new ECDomainParameters( CurveParams.getCurve, CurveParams.getG, CurveParams.getN, CurveParams.getH, ) val HalfCurveOrder: BigInteger = CurveParams.getN.shiftRight(1) ================================================ FILE: modules/lib/shared/src/main/scala/io/leisuremeta/chain/lib/application/DAppState.scala ================================================ package io.leisuremeta.chain.lib package application import cats.Monad import cats.data.{EitherT, StateT} import cats.syntax.either.catsSyntaxEither import cats.syntax.traverse.toTraverseOps import fs2.Stream import scodec.bits.ByteVector import codec.byte.ByteCodec import codec.byte.ByteDecoder.ops.* import codec.byte.ByteEncoder.ops.* import merkle.* import merkle.MerkleTrie.NodeStore import io.leisuremeta.chain.lib.merkle.toNibbles trait DAppState[F[_], K, V]: type ErrorOrF[A] = EitherT[F, String, A] def get(k: K): StateT[ErrorOrF, MerkleTrieState, Option[V]] def put(k: K, v: V): StateT[ErrorOrF, MerkleTrieState, Unit] def remove(k: K): StateT[ErrorOrF, MerkleTrieState, Boolean] def streamWithPrefix( prefixBytes: ByteVector, ): StateT[ErrorOrF, MerkleTrieState, Stream[ErrorOrF, (K, V)]] def streamFrom( keyBytes: ByteVector, ): StateT[ErrorOrF, MerkleTrieState, Stream[ErrorOrF, (K, V)]] def reverseStreamFrom( keyPrefix: ByteVector, keySuffix: Option[ByteVector], ): StateT[ErrorOrF, MerkleTrieState, Stream[ErrorOrF, (K, V)]] object DAppState: case class WithCommonPrefix(prefix: String): /** @param name * must be alpha-numeric * @return * DAppState[F, K, V] */ def ofName[F[_]: Monad: NodeStore, K: ByteCodec, V: ByteCodec]( name: String, ): DAppState[F, K, V] = scribe.info: s"Building DAppState from WithCommonPrefix $prefix of name $name" DAppState.ofName[F, K, V](s"$prefix-$name") def ofName[F[_]: Monad: NodeStore, K: ByteCodec, V: ByteCodec]( name: String, ): DAppState[F, K, V] = scribe.info(s"Initializing DAppState with name $name") @SuppressWarnings(Array("org.wartremover.warts.Throw")) val nameBytes: ByteVector = ByteVector.encodeUtf8(name) match case Left(e) => throw e case Right(bytes) => bytes new DAppState[F, K, V]: def get(k: K): StateT[ErrorOrF, MerkleTrieState, Option[V]] = for bytesOption <- MerkleTrie.get[F]((nameBytes ++ k.toBytes).toNibbles) vOption <- StateT.liftF: EitherT.fromEither: bytesOption.traverse(_.to[V].leftMap(_.msg)) yield // scribe.info(s"state $name get($k) result: $vOption") vOption def put(k: K, v: V): StateT[ErrorOrF, MerkleTrieState, Unit] = MerkleTrie .put[F]((nameBytes ++ k.toBytes).toNibbles, v.toBytes) // .map { _ => scribe.info(s"state $name put($k, $v)") } def remove(k: K): StateT[ErrorOrF, MerkleTrieState, Boolean] = MerkleTrie.remove[F]((nameBytes ++ k.toBytes).toNibbles) def streamFrom( keyBytes: ByteVector, ): StateT[ErrorOrF, MerkleTrieState, Stream[ErrorOrF, (K, V)]] = val prefixNibbles = (nameBytes ++ keyBytes).toNibbles MerkleTrie .streamFrom(prefixNibbles) .map: binaryStream => binaryStream .takeWhile(_._1.value.startsWith(nameBytes.bits)) .evalMap: (kNibbles, vBytes) => EitherT .fromEither: for k <- kNibbles.bytes.drop(nameBytes.size).to[K] v <- vBytes.to[V] yield (k, v) .leftMap(_.msg) def streamWithPrefix( prefixBytes: ByteVector, ): StateT[ErrorOrF, MerkleTrieState, Stream[ErrorOrF, (K, V)]] = val prefixNibbles = (nameBytes ++ prefixBytes).toNibbles MerkleTrie .streamFrom(prefixNibbles) .map: binaryStream => binaryStream .takeWhile: (kNibbles, _) => val flag = kNibbles.value.startsWith(prefixNibbles.value) // scribe.info(s"state $name streamWithPrefix(${prefixBytes.toHex}) ${kNibbles.value.toHex} $flag") flag .evalMap: (kNibbles, vBytes) => EitherT .fromEither: for k <- kNibbles.bytes.drop(nameBytes.size).to[K] v <- vBytes.to[V] yield // scribe.info(s"state $name streamWithPrefix($prefixBytes) $k -> $v") (k, v) .leftMap(_.msg) def reverseStreamFrom( keyPrefix: ByteVector, keySuffix: Option[ByteVector], ): StateT[ErrorOrF, MerkleTrieState, Stream[ErrorOrF, (K, V)]] = val prefixNibbles = (nameBytes ++ keyPrefix).toNibbles MerkleTrie .reverseStreamFrom(prefixNibbles, keySuffix.map(_.toNibbles)) .map: binaryStream => binaryStream .evalMap: (kNibbles, vBytes) => EitherT .fromEither: for k <- kNibbles.bytes.drop(nameBytes.size).to[K] v <- vBytes.to[V] yield (k, v) .leftMap(_.msg) ================================================ FILE: modules/lib/shared/src/main/scala/io/leisuremeta/chain/lib/codec/byte/ByteCodec.scala ================================================ package io.leisuremeta.chain.lib package codec.byte import scodec.bits.ByteVector import failure.DecodingFailure trait ByteCodec[A] extends ByteDecoder[A] with ByteEncoder[A] object ByteCodec: def apply[A](implicit bc: ByteCodec[A]): ByteCodec[A] = bc given [A](using decoder: ByteDecoder[A], encoder: ByteEncoder[A], ): ByteCodec[A] = new ByteCodec[A]: override def decode( bytes: ByteVector, ): Either[DecodingFailure, DecodeResult[A]] = decoder.decode(bytes) override def encode(a: A): ByteVector = encoder.encode(a) ================================================ FILE: modules/lib/shared/src/main/scala/io/leisuremeta/chain/lib/codec/byte/ByteDecoder.scala ================================================ package io.leisuremeta.chain.lib package codec.byte import java.time.Instant import scala.compiletime.{erasedValue, summonInline} import scala.deriving.Mirror import scala.reflect.{ClassTag, classTag} import cats.syntax.eq.catsSyntaxEq import cats.syntax.either.* import eu.timepit.refined.api.Refined import eu.timepit.refined.auto.autoUnwrap import eu.timepit.refined.numeric.NonNegative import eu.timepit.refined.refineV import scodec.bits.ByteVector import failure.DecodingFailure trait ByteDecoder[A]: def decode(bytes: ByteVector): Either[DecodingFailure, DecodeResult[A]] def map[B](f: A => B): ByteDecoder[B] = bytes => decode(bytes).map { case DecodeResult(a, remainder) => DecodeResult(f(a), remainder) } def emap[B](f: A => Either[DecodingFailure, B]): ByteDecoder[B] = bytes => for decoded <- decode(bytes) converted <- f(decoded.value) yield DecodeResult(converted, decoded.remainder) def flatMap[B](f: A => ByteDecoder[B]): ByteDecoder[B] = bytes => decode(bytes).flatMap { case DecodeResult(a, remainder) => f(a).decode(remainder) } def widen[AA >: A]: ByteDecoder[AA] = map(identity) final case class DecodeResult[+A](value: A, remainder: ByteVector) object ByteDecoder: def apply[A: ByteDecoder]: ByteDecoder[A] = summon object ops: extension (bytes: ByteVector) def to[A: ByteDecoder]: Either[DecodingFailure, A] = for result <- ByteDecoder[A].decode(bytes) DecodeResult(a, r) = result _ <- Either.cond( r.isEmpty, (), DecodingFailure(s"non empty remainder: $r"), ) yield a private def decoderProduct[A]( p: Mirror.ProductOf[A], elems: => List[ByteDecoder[?]], ): ByteDecoder[A] = (bytes: ByteVector) => def reverse(tuple: Tuple): Tuple = @SuppressWarnings(Array("org.wartremover.warts.Any")) @annotation.tailrec def loop(tuple: Tuple, acc: Tuple): Tuple = tuple match case _: EmptyTuple => acc case t *: ts => loop(ts, t *: acc) loop(tuple, EmptyTuple) @annotation.tailrec def loop( elems: List[ByteDecoder[?]], bytes: ByteVector, acc: Tuple, ): Either[DecodingFailure, DecodeResult[A]] = elems match case Nil => (DecodeResult(p.fromProduct(reverse(acc)), bytes)) .asRight[DecodingFailure] case decoder :: rest => scribe.debug(s"Decoder: $decoder") scribe.debug(s"Bytes to decode: $bytes") decoder.decode(bytes) match case Left(failure) => failure.asLeft[DecodeResult[A]] case Right(DecodeResult(value, remainder)) => scribe.debug(s"Decoded: $value") loop(rest, remainder, value *: acc) loop(elems, bytes, EmptyTuple) @SuppressWarnings(Array("org.wartremover.warts.Recursion")) inline def summonAll[T <: Tuple]: List[ByteDecoder[?]] = inline erasedValue[T] match case _: EmptyTuple => Nil case _: (t *: ts) => summonInline[ByteDecoder[t]] :: summonAll[ts] inline given derived[T](using p: Mirror.ProductOf[T]): ByteDecoder[T] = lazy val elemInstances: List[ByteDecoder[?]] = summonAll[p.MirroredElemTypes] decoderProduct(p, elemInstances) given unitByteDecoder: ByteDecoder[Unit] = bytes => Right[DecodingFailure, DecodeResult[Unit]]( DecodeResult((), bytes), ) type BigNat = BigInt Refined NonNegative @SuppressWarnings(Array("org.wartremover.warts.Throw")) def unsafeFromBigInt(n: BigInt): BigNat = refineV[NonNegative](n) match case Right(nat) => nat case Left(e) => throw new Exception(e) def fromFixedSizeBytes[T: ClassTag]( size: Long, )(f: ByteVector => T): ByteDecoder[T] = bytes => Either.cond( bytes.size >= size, bytes splitAt size match case (front, back) => DecodeResult(f(front), back) , DecodingFailure( s"Too short bytes to decode ${classTag[T]}; required $size bytes, but receiced ${bytes.size} bytes: $bytes", ), ) // @SuppressWarnings(Array("org.wartremover.warts.OptionPartial")) // given ByteDecoder[UInt256BigInt] = fromFixedSizeBytes(32) { bytes => // UInt256.from(BigInt(1, bytes.toArray)).toOption.get // } // // @SuppressWarnings(Array("org.wartremover.warts.OptionPartial")) // given ByteDecoder[UInt256Bytes] = fromFixedSizeBytes(32) { // UInt256.from(_).toOption.get // } given byteDecoder: ByteDecoder[Byte] = fromFixedSizeBytes(1)(_.toByte()) given booleanDecoder: ByteDecoder[Boolean] = fromFixedSizeBytes(1): bytes => bytes.head =!= 0x00.toByte given longDecoder: ByteDecoder[Long] = fromFixedSizeBytes(8)(_.toLong()) given instantDecoder: ByteDecoder[Instant] = ByteDecoder[Long] map Instant.ofEpochMilli given bignatByteDecoder: ByteDecoder[BigNat] = bytes => Either.cond(bytes.nonEmpty, bytes, DecodingFailure("Empty bytes")).flatMap { nonEmptyBytes => val head: Int = nonEmptyBytes.head & 0xff val tail: ByteVector = nonEmptyBytes.tail if head <= 0x80 then Right[DecodingFailure, DecodeResult[BigNat]]( DecodeResult(unsafeFromBigInt(BigInt(head)), tail), ) else if head <= 0xf8 then val size = head - 0x80 if tail.size < size then Left[DecodingFailure, DecodeResult[BigNat]]( DecodingFailure( s"required byte size $size, but $tail", ), ) else val (front, back) = tail.splitAt(size.toLong) Right[DecodingFailure, DecodeResult[BigNat]]( DecodeResult(unsafeFromBigInt(BigInt(1, front.toArray)), back), ) else val sizeOfNumber = head - 0xf8 + 1 if tail.size < sizeOfNumber then Left[DecodingFailure, DecodeResult[BigNat]]( DecodingFailure( s"required byte size $sizeOfNumber, but $tail", ), ) else val (sizeBytes, data) = tail.splitAt(sizeOfNumber.toLong) val size = BigInt(1, sizeBytes.toArray).toLong if data.size < size then Left[DecodingFailure, DecodeResult[BigNat]]( DecodingFailure( s"required byte size $size, but $data", ), ) else val (front, back) = data.splitAt(size) Right[DecodingFailure, DecodeResult[BigNat]]( DecodeResult(unsafeFromBigInt(BigInt(1, front.toArray)), back), ) } given bigintByteDecoder: ByteDecoder[BigInt] = ByteDecoder[BigNat].map{ case x if x % 2 === 0 => x / 2 case x => (x - 1) / (-2) } def sizedListDecoder[A: ByteDecoder](size: BigNat): ByteDecoder[List[A]] = bytes => @annotation.tailrec def loop( bytes: ByteVector, count: BigInt, acc: List[A], ): Either[DecodingFailure, DecodeResult[List[A]]] = if count === BigInt(0) then Right[DecodingFailure, DecodeResult[List[A]]]( DecodeResult(acc.reverse, bytes), ) else ByteDecoder[A].decode(bytes) match case Left(failure) => Left[DecodingFailure, DecodeResult[List[A]]](failure) case Right(DecodeResult(value, remainder)) => loop(remainder, count - 1, value :: acc) loop(bytes, size, Nil) given mapByteDecoder[K: ByteDecoder, V: ByteDecoder]: ByteDecoder[Map[K, V]] = bignatByteDecoder flatMap sizedListDecoder[(K, V)] map (_.toMap) given optionByteDecoder[A: ByteDecoder]: ByteDecoder[Option[A]] = bignatByteDecoder flatMap sizedListDecoder[A] map (_.headOption) given setByteDecoder[A: ByteDecoder]: ByteDecoder[Set[A]] = bignatByteDecoder flatMap sizedListDecoder[A] map (_.toSet) @SuppressWarnings(Array("org.wartremover.warts.AsInstanceOf")) given seqByteDecoder[A: ByteDecoder]: ByteDecoder[Seq[A]] = bignatByteDecoder flatMap sizedListDecoder[A] map (_.asInstanceOf[Seq[A]]) ================================================ FILE: modules/lib/shared/src/main/scala/io/leisuremeta/chain/lib/codec/byte/ByteEncoder.scala ================================================ package io.leisuremeta.chain.lib package codec.byte import java.time.Instant import scala.compiletime.{erasedValue, summonInline} import scala.deriving.Mirror import scala.reflect.ClassTag import cats.syntax.eq.catsSyntaxEq import eu.timepit.refined.api.Refined import eu.timepit.refined.auto.autoUnwrap import eu.timepit.refined.numeric.NonNegative import eu.timepit.refined.refineV import scodec.bits.ByteVector import datatype.UInt256 trait ByteEncoder[A]: def encode(a: A): ByteVector def contramap[B](f: B => A): ByteEncoder[B] = b => encode(f(b)) object ByteEncoder: def apply[A: ByteEncoder]: ByteEncoder[A] = summon object ops: extension [A](a: A) def toBytes(implicit be: ByteEncoder[A]): ByteVector = be.encode(a) type BigNat = BigInt Refined NonNegative @SuppressWarnings(Array("org.wartremover.warts.Throw")) def unsafeFromBigInt(n: BigInt): BigNat = refineV[NonNegative](n) match case Right(nat) => nat case Left(e) => throw new Exception(e) @SuppressWarnings(Array("org.wartremover.warts.AsInstanceOf", "org.wartremover.warts.Any")) private def encoderProduct[A]( // p: Mirror.ProductOf[A], elems: => List[ByteEncoder[?]], ): ByteEncoder[A] = (a: A) => a.asInstanceOf[Product].productIterator.zip(elems).map{ case (aElem, encoder) => encoder.asInstanceOf[ByteEncoder[Any]].encode(aElem) }.foldLeft(ByteVector.empty)(_ ++ _) @SuppressWarnings(Array("org.wartremover.warts.Recursion")) inline def summonAll[T <: Tuple]: List[ByteEncoder[?]] = inline erasedValue[T] match case _: EmptyTuple => Nil case _: (t *: ts) => summonInline[ByteEncoder[t]] :: summonAll[ts] inline given derived[T](using p: Mirror.ProductOf[T]): ByteEncoder[T] = lazy val elemInstances: List[ByteEncoder[?]] = summonAll[p.MirroredElemTypes] encoderProduct(elemInstances) given unitByteEncoder: ByteEncoder[Unit] = _ => ByteVector.empty given [A: UInt256.Ops]: ByteEncoder[UInt256.Refined[A]] = _.toBytes given instantEncoder: ByteEncoder[Instant] = ByteVector fromLong _.toEpochMilli given bignatByteEncoder: ByteEncoder[BigNat] = bignat => val bytes = ByteVector.view(bignat.toByteArray).dropWhile(_ === 0x00.toByte) if bytes.isEmpty then ByteVector(0x00.toByte) else if bignat <= 0x80 then bytes else val size = bytes.size if size < (0xf8 - 0x80) + 1 then ByteVector.fromByte((size + 0x80).toByte) ++ bytes else val sizeBytes = ByteVector.fromLong(size).dropWhile(_ === 0x00.toByte) ByteVector.fromByte( (sizeBytes.size + 0xf8 - 1).toByte, ) ++ sizeBytes ++ bytes given booleanEncoder: ByteEncoder[Boolean] = case true => ByteVector(0x01.toByte) case false => ByteVector(0x00.toByte) @SuppressWarnings(Array("org.wartremover.warts.AsInstanceOf")) given bigintByteEncoder: ByteEncoder[BigInt] = ByteEncoder[BigNat].contramap { case n if n >= 0 => (n * 2).asInstanceOf[BigNat] case n => (n * (-2) + 1).asInstanceOf[BigNat] } given listByteEncoder[A: ByteEncoder]: ByteEncoder[List[A]] = (list: List[A]) => list.foldLeft(bignatByteEncoder.encode(unsafeFromBigInt(list.size))) { case (acc, a) => acc ++ ByteEncoder[A].encode(a) } given mapByteEncoder[K: ByteEncoder, V: ByteEncoder]: ByteEncoder[Map[K, V]] = listByteEncoder[(K, V)].contramap(_.toList) given optionByteEncoder[A: ByteEncoder]: ByteEncoder[Option[A]] = listByteEncoder.contramap(_.toList) given setByteEncoder[A: ByteEncoder]: ByteEncoder[Set[A]] = (set: Set[A]) => set .map(ByteEncoder[A].encode) .toList .sorted .foldLeft { bignatByteEncoder.encode(unsafeFromBigInt(set.size)) }(_ ++ _) given seqByteEncoder[A: ByteEncoder]: ByteEncoder[Seq[A]] = (seq: Seq[A]) => seq.foldLeft(bignatByteEncoder.encode(unsafeFromBigInt(seq.size))) { case (acc, a) => acc ++ ByteEncoder[A].encode(a) } ================================================ FILE: modules/lib/shared/src/main/scala/io/leisuremeta/chain/lib/crypto/Hash.scala ================================================ package io.leisuremeta.chain.lib package crypto import cats.Eq import cats.Contravariant import io.circe.{Decoder, Encoder, KeyEncoder, KeyDecoder} import scodec.bits.ByteVector import codec.byte.{ByteDecoder, ByteEncoder} import datatype.{UInt256, UInt256Bytes} trait Hash[A]: def apply(a: A): Hash.Value[A] def contramap[B](f: B => A): Hash[B] = (b: B) => Hash.Value[B](apply(f(b)).toUInt256Bytes) object Hash: def apply[A: Hash]: Hash[A] = summon opaque type Value[A] = UInt256Bytes object Value: def apply[A](uint256: UInt256Bytes): Value[A] = uint256 given circeValueDecoder[A]: Decoder[Value[A]] = UInt256.uint256bytesCirceDecoder.map(Value[A](_)) given circeValueEncoder[A]: Encoder[Value[A]] = UInt256.uint256bytesCirceEncoder.contramap[Value[A]](_.toUInt256Bytes) given circeKeyDecoder[A]: KeyDecoder[Value[A]] = (str) => for bytes <- ByteVector.fromHex(str) uint256 <- UInt256.from(bytes).toOption yield Value[A](uint256) given circeKeyEncoder[A]: KeyEncoder[Value[A]] = KeyEncoder.encodeKeyString.contramap[Value[A]](_.toUInt256Bytes.toBytes.toHex) given byteValueDecoder[A]: ByteDecoder[Value[A]] = UInt256.uint256bytesByteDecoder.map(Value[A](_)) given byteValueEncoder[A]: ByteEncoder[Value[A]] = UInt256.uint256bytesByteEncoder.contramap[Value[A]](_.toUInt256Bytes) given eqValue[A]: Eq[Value[A]] = Eq.fromUniversalEquals extension [A](value: Value[A]) def toUInt256Bytes: UInt256Bytes = value object ops: extension [A](a: A) def toHash(using h: Hash[A]): Value[A] = h(a) given contravariant: Contravariant[Hash] = new Contravariant[Hash]: override def contramap[A, B](fa: Hash[A])(f: B => A): Hash[B] = fa.contramap(f) @SuppressWarnings(Array("org.wartremover.warts.OptionPartial")) def build[A: ByteEncoder]: Hash[A] = (a: A) => val bytes = ByteEncoder[A].encode(a) val h = ByteVector.view(CryptoOps.keccak256(bytes.toArray)) Value[A](UInt256.from(h).toOption.get) ================================================ FILE: modules/lib/shared/src/main/scala/io/leisuremeta/chain/lib/crypto/KeyPair.scala ================================================ package io.leisuremeta.chain.lib package crypto import datatype.UInt256BigInt final case class KeyPair(privateKey: UInt256BigInt, publicKey: PublicKey) { override lazy val toString: String = s"KeyPair(${privateKey.toBytes.toHex}, $publicKey)" } ================================================ FILE: modules/lib/shared/src/main/scala/io/leisuremeta/chain/lib/crypto/PublicKey.scala ================================================ package io.leisuremeta.chain.lib package crypto import cats.implicits.given import scodec.bits.ByteVector import codec.byte.{ByteDecoder, ByteEncoder} import datatype.{UInt256BigInt, UInt256} import failure.UInt256RefineFailure import io.leisuremeta.chain.lib.failure.DecodingFailure final case class PublicKey(x: UInt256BigInt, y: UInt256BigInt): def toBytes: ByteVector = x.toBytes ++ y.toBytes def toBigInt: BigInt = BigInt(1, toBytes.toArray) override def toString: String = s"PublicKey($toBytes)" object PublicKey: @SuppressWarnings(Array("org.wartremover.warts.Nothing")) def fromByteArray( array: Array[Byte], ): Either[UInt256RefineFailure, PublicKey] = if array.length =!= 64 then Left( UInt256RefineFailure(s"Public key array size are not 64: $array"), ) else val (xArr, yArr) = array splitAt 32 for x <- UInt256.from(BigInt(1, xArr)) y <- UInt256.from(BigInt(1, yArr)) yield PublicKey(x, y) val pubkeyByteEncoder: ByteEncoder[PublicKey] = _.toBytes given pubkeyByteDecoder: ByteDecoder[PublicKey] = ByteDecoder.fromFixedSizeBytes(64)(identity).emap { bytes => fromByteArray(bytes.toArray).left.map(e => DecodingFailure(e.msg)) } given Hash[PublicKey] = Hash.build ================================================ FILE: modules/lib/shared/src/main/scala/io/leisuremeta/chain/lib/crypto/Recover.scala ================================================ package io.leisuremeta.chain.lib package crypto trait Recover[A]: def apply(a: A, signature: Signature)(implicit hash: Hash[A], ): Option[PublicKey] = fromHash(hash(a), signature) def fromHash( hashValue: Hash.Value[A], signature: Signature, ): Option[PublicKey] object Recover: def apply[A: Recover]: Recover[A] = summon def build[A]: Recover[A] = (hashValue: Hash.Value[A], signature: Signature) => CryptoOps.recover(signature, hashValue.toUInt256Bytes.toArray).toOption object ops: extension [A](hashValue: Hash.Value[A]) def recover(signature: Signature)(using r: Recover[A], ): Option[PublicKey] = r.fromHash(hashValue, signature) ================================================ FILE: modules/lib/shared/src/main/scala/io/leisuremeta/chain/lib/crypto/Sign.scala ================================================ package io.leisuremeta.chain.lib.crypto trait Sign[A]: def apply(a: A, keyPair: KeyPair)(implicit hash: Hash[A], ): Either[String, Signature] = byHash(hash(a), keyPair) def byHash( hashValue: Hash.Value[A], keyPair: KeyPair, ): Either[String, Signature] object Sign: def apply[A: Sign]: Sign[A] = summon def build[A]: Sign[A] = (hashValue: Hash.Value[A], keyPair: KeyPair) => CryptoOps.sign(keyPair, hashValue.toUInt256Bytes.toBytes.toArray) object ops: extension (keyPair: KeyPair) def sign[A: Hash: Sign](a: A): Either[String, Signature] = Sign[A].apply(a, keyPair) extension [A](hashValue: Hash.Value[A]) def signBy(keyPair: KeyPair)(using sign: Sign[A], ): Either[String, Signature] = sign.byHash(hashValue, keyPair) ================================================ FILE: modules/lib/shared/src/main/scala/io/leisuremeta/chain/lib/crypto/Signature.scala ================================================ package io.leisuremeta.chain.lib package crypto import cats.syntax.either.* import io.circe.{Decoder, Encoder} import io.circe.generic.semiauto.* import io.github.iltotore.iron.* import io.github.iltotore.iron.constraint.numeric.* import io.github.iltotore.iron.circe.given import scodec.bits.ByteVector import codec.byte.{ByteDecoder, ByteEncoder, DecodeResult} import datatype.UInt256BigInt import failure.DecodingFailure final case class Signature( v: Signature.Header, r: UInt256BigInt, s: UInt256BigInt, ): override lazy val toString: String = s"Signature($v, ${r.toBytes.toHex}, ${s.toBytes.toHex})" object Signature: type HeaderRange = Interval.Closed[27, 34] type Header = Int :| HeaderRange given headerEncoder: ByteEncoder[Header] = ByteVector `fromByte` _.toByte given headerDecoder: ByteDecoder[Header] = ByteDecoder[Byte] .decode(_) .flatMap: case DecodeResult(b, remainder) => b.toInt .refineEither[HeaderRange] .map(DecodeResult(_, remainder)) .leftMap(DecodingFailure(_)) given sigEncoder: ByteEncoder[Signature] = ByteEncoder.derived given sigDecoder: ByteDecoder[Signature] = ByteDecoder.derived given sigCirceEncoder: Encoder[Signature] = deriveEncoder[Signature] given sigCirceDecoder: Decoder[Signature] = deriveDecoder[Signature] ================================================ FILE: modules/lib/shared/src/main/scala/io/leisuremeta/chain/lib/datatype/BigNat.scala ================================================ package io.leisuremeta.chain.lib package datatype import scala.math.Ordering import cats.Eq import cats.implicits.given import eu.timepit.refined.api.Refined import eu.timepit.refined.numeric.NonNegative import eu.timepit.refined.refineV import io.circe.{ Decoder as CirceDecoder, Encoder as CirceEncoder, } import io.circe.refined.* import codec.byte.{ByteDecoder, ByteEncoder} opaque type BigNat = BigInt Refined NonNegative object BigNat: @SuppressWarnings(Array("org.wartremover.warts.OptionPartial")) val Zero: BigNat = refineV[NonNegative](BigInt(0)).toOption.get @SuppressWarnings(Array("org.wartremover.warts.OptionPartial")) val One: BigNat = refineV[NonNegative](BigInt(1)).toOption.get def fromBigInt(n: BigInt): Either[String, BigNat] = refineV[NonNegative](n) extension (bignat: BigNat) @annotation.targetName("plus") def +(that: BigNat): BigNat = BigNat.add(bignat, that) @annotation.targetName("times") def *(that: BigNat): BigNat = BigNat.multiply(bignat, that) @annotation.targetName("diviedBy") def /(that: BigNat): BigNat = BigNat.divide(bignat, that) def toBigInt: BigInt = bignat.value def floorAt(e: Int): BigNat = val n = BigInt(10).pow(e) unsafeFromBigInt(bignat.value / n * n) @SuppressWarnings(Array("org.wartremover.warts.Throw")) def unsafeFromBigInt(n: BigInt): BigNat = fromBigInt(n) match case Right(nat) => nat case Left(e) => throw new Exception(e) def unsafeFromLong(long: Long): BigNat = unsafeFromBigInt(BigInt(long)) @SuppressWarnings(Array("org.wartremover.warts.Throw")) def add(x: BigNat, y: BigNat): BigNat = refineV[NonNegative](x.value + y.value) match case Right(nat) => nat case Left(e) => throw new Exception(e) @SuppressWarnings(Array("org.wartremover.warts.Throw")) def multiply(x: BigNat, y: BigNat): BigNat = refineV[NonNegative](x.value * y.value) match case Right(nat) => nat case Left(e) => throw new Exception(e) @SuppressWarnings(Array("org.wartremover.warts.Throw")) def divide(x: BigNat, y: BigNat): BigNat = refineV[NonNegative](x.value / y.value) match case Right(nat) => nat case Left(e) => throw new Exception(e) def tryToSubtract(x: BigNat, y: BigNat): Either[String, BigNat] = fromBigInt(x.toBigInt - y.toBigInt) def max(x: BigNat, y: BigNat): BigNat = if x.toBigInt >= y.toBigInt then x else y def min(x: BigNat, y: BigNat): BigNat = if x.toBigInt <= y.toBigInt then x else y given bignatByteDecoder: ByteDecoder[BigNat] = ByteDecoder.bignatByteDecoder given bignatByteEncoder: ByteEncoder[BigNat] = ByteEncoder.bignatByteEncoder given bignatCirceDecoder: CirceDecoder[BigNat] = refinedDecoder[BigInt, NonNegative, Refined] given bignatCirceEncoder: CirceEncoder[BigNat] = refinedEncoder[BigInt, NonNegative, Refined] given bignatOrdering: Ordering[BigNat] = Ordering.by(_.toBigInt) given bignatEq: Eq[BigNat] = Eq.by(_.toBigInt) ================================================ FILE: modules/lib/shared/src/main/scala/io/leisuremeta/chain/lib/datatype/UInt256.scala ================================================ package io.leisuremeta.chain.lib package datatype import scala.util.Try import cats.syntax.eq.given import io.circe.{Decoder, Encoder} import scodec.bits.ByteVector import codec.byte.{ByteDecoder, ByteEncoder} import failure.{DecodingFailure, UInt256RefineFailure} type UInt256Bytes = UInt256.Refined[ByteVector] type UInt256BigInt = UInt256.Refined[BigInt] object UInt256: trait Refine[A] type Refined[A] = A & Refine[A] def from[A: Ops](value: A): Either[UInt256RefineFailure, Refined[A]] = Ops[A].from(value) extension [A: Ops](value: Refined[A]) def toBytes: ByteVector = Ops[A].toBytes(value) def toBigInt: BigInt = Ops[A].toBigInt(value) trait Ops[A]: def from(value: A): Either[UInt256RefineFailure, Refined[A]] def toBytes(value: A): ByteVector def toBigInt(value: A): BigInt object Ops: def apply[A: Ops]: Ops[A] = summon given Ops[ByteVector] = new Ops[ByteVector]: @SuppressWarnings(Array("org.wartremover.warts.AsInstanceOf")) def from( value: ByteVector, ): Either[UInt256RefineFailure, Refined[ByteVector]] = Either.cond( value.size === 32L, value.asInstanceOf[UInt256Bytes], UInt256RefineFailure( s"Incorrect sized bytes to be UInt256: $value", ), ) def toBytes(value: ByteVector): ByteVector = value def toBigInt(value: ByteVector): BigInt = BigInt(1, value.toArray) given Ops[BigInt] = new Ops[BigInt]: @SuppressWarnings(Array("org.wartremover.warts.AsInstanceOf")) def from(value: BigInt): Either[UInt256RefineFailure, Refined[BigInt]] = Either.cond( value >= 0L && value.bitLength <= 256, value.asInstanceOf[UInt256BigInt], UInt256RefineFailure( s"Bigint out of range to be UInt256: $value", ), ) def toBytes(value: BigInt): ByteVector = ByteVector.view(value.toByteArray).takeRight(32L).padLeft(32L) def toBigInt(value: BigInt): BigInt = value end Ops given uint256bytesCirceEncoder: Encoder[UInt256Bytes] = Encoder[String].contramap[UInt256Bytes](_.toBytes.toHex) given uint256bytesCirceDecoder: Decoder[UInt256Bytes] = Decoder.decodeString.emap((str: String) => for bytes <- ByteVector.fromHexDescriptive(str) refined <- UInt256.from(bytes).left.map(_.msg) yield refined, ) given uint256bigintCirceEncoder: Encoder[UInt256BigInt] = Encoder[String].contramap[UInt256BigInt](_.toBytes.toHex) given uint256bigintCirceDecoder: Decoder[UInt256BigInt] = Decoder.decodeString.emap((str: String) => for bigint <- Try(BigInt(str, 16)).toEither.left.map(_.getMessage) refined <- UInt256.from(bigint).left.map(_.msg) yield refined, ) given uint256bytesByteDecoder: ByteDecoder[UInt256Bytes] = ByteDecoder .fromFixedSizeBytes(32)(identity) .emap(UInt256.from(_).left.map(e => DecodingFailure(e.msg))) given uint256bytesByteEncoder: ByteEncoder[UInt256Bytes] = _.toBytes given uint256bigintByteDecoder: ByteDecoder[UInt256BigInt] = ByteDecoder .fromFixedSizeBytes(32)(bytes => BigInt(1, bytes.toArray)) .emap(UInt256.from(_).left.map(e => DecodingFailure(e.msg))) given uint256bigintByteEncoder: ByteEncoder[UInt256BigInt] = _.toBytes @SuppressWarnings(Array("org.wartremover.warts.OptionPartial")) val EmptyBytes: UInt256Bytes = UInt256.from(ByteVector.low(32)).toOption.get ================================================ FILE: modules/lib/shared/src/main/scala/io/leisuremeta/chain/lib/datatype/Utf8.scala ================================================ package io.leisuremeta.chain.lib package datatype import java.nio.charset.{CharacterCodingException, StandardCharsets} import cats.Eq import io.circe.{Decoder, Encoder, KeyDecoder, KeyEncoder} import scodec.bits.ByteVector import codec.byte.{ByteDecoder, ByteEncoder} import failure.DecodingFailure opaque type Utf8 = String object Utf8: def from(s: String): Either[CharacterCodingException, Utf8] = ByteVector.encodeUtf8(s).map(_ => s) @SuppressWarnings(Array("org.wartremover.warts.Throw")) def unsafeFrom(s: String): Utf8 = from(s).fold(e => throw e, identity) extension (u: Utf8) def value: String = u def bytes: ByteVector = ByteVector.view(u.getBytes(StandardCharsets.UTF_8)) given eq: Eq[Utf8] = Eq.fromUniversalEquals given utf8CirceDecoder: Decoder[Utf8] = Decoder.decodeString.emap(from(_).left.map(_.getMessage)) given utf8CirceEncoder: Encoder[Utf8] = Encoder.encodeString given utf8CirceKeyDecoder: KeyDecoder[Utf8] = KeyDecoder.instance(from(_).toOption) given utf8CirceKeyEncoder: KeyEncoder[Utf8] = KeyEncoder.encodeKeyString given utf8ByteDecoder: ByteDecoder[Utf8] = ByteDecoder[BigNat].flatMap { (b: BigNat) => ByteDecoder .fromFixedSizeBytes[ByteVector](b.toBigInt.toLong)(identity) .emap(_.decodeUtf8.left.map { e => DecodingFailure(e.getMessage) }) } @SuppressWarnings(Array("org.wartremover.warts.Throw")) given utf8ByteEncoder: ByteEncoder[Utf8] = (utf8: String) => val encoded = ByteVector.encodeUtf8(utf8) match case Right(v) => v case Left(e) => throw e ByteEncoder[BigNat].encode(BigNat.unsafeFromLong(encoded.size)) ++ encoded ================================================ FILE: modules/lib/shared/src/main/scala/io/leisuremeta/chain/lib/failure/LmChainFailure.scala ================================================ package io.leisuremeta.chain.lib.failure import scala.util.control.NoStackTrace sealed trait LmChainFailure extends NoStackTrace: def msg: String final case class EncodingFailure(msg: String) extends LmChainFailure final case class DecodingFailure(msg: String) extends LmChainFailure final case class UInt256RefineFailure(msg: String) extends LmChainFailure ================================================ FILE: modules/lib/shared/src/main/scala/io/leisuremeta/chain/lib/merkle/MerkleTrie.scala ================================================ package io.leisuremeta.chain.lib package merkle import cats.Monad import cats.data.{EitherT, Kleisli, OptionT, StateT} import cats.syntax.eq.given import fs2.Stream import io.github.iltotore.iron.* import scodec.bits.{BitVector, ByteVector} import crypto.Hash.ops.* import MerkleTrieNode.{Children, MerkleHash} object MerkleTrie: type NodeStore[F[_]] = Kleisli[EitherT[F, String, *], MerkleHash, Option[MerkleTrieNode]] @SuppressWarnings(Array("org.wartremover.warts.Recursion")) def get[F[_]: Monad: NodeStore]( key: Nibbles, ): StateT[EitherT[F, String, *], MerkleTrieState, Option[ByteVector]] = StateT.inspectF: (state: MerkleTrieState) => val optionT = for node <- OptionT(getNode[F](state)) stripped <- OptionT.fromOption[EitherT[F, String, *]]: key.stripPrefix(node.prefix) value <- stripped.unCons .fold: OptionT.fromOption[EitherT[F, String, *]](node.getValue) .apply: (head, remainder) => for children <- OptionT.fromOption[EitherT[F, String, *]]: node.getChildren nextRoot <- OptionT.fromOption[EitherT[F, String, *]]: children(head) value <- OptionT: get[F](remainder.assumeNibbles).runA: state.copy(root = Some(nextRoot)) yield value yield value optionT.value @SuppressWarnings(Array("org.wartremover.warts.Recursion")) def put[F[_]: Monad: NodeStore]( key: Nibbles, value: ByteVector, ): StateT[EitherT[F, String, *], MerkleTrieState, Unit] = StateT.modifyF: (state: MerkleTrieState) => getNodeAndStateRoot[F](state).flatMap: case None => val leaf = MerkleTrieNode.leaf(key, value) val leafHash = leaf.toHash EitherT.rightT[F, String]: state.copy( root = Some(leafHash), diff = state.diff.add(leafHash, leaf), ) case Some((node, root)) => val prefix0: Nibbles = node.prefix val (commonPrefix, remainder0, remainder1) = getCommonPrefixNibbleAndRemainders(prefix0, key) def putLeaf(value0: ByteVector): EitherT[F, String, MerkleTrieState] = EitherT.rightT[F, String]: (remainder0.unCons, remainder1.unCons) match case (None, None) => if value0 === value then state else val leaf1 = MerkleTrieNode.leaf(prefix0, value) val leaf1Hash = leaf1.toHash state.copy( root = Some(leaf1Hash), diff = state.diff .remove(root, node) .add(leaf1Hash, leaf1), ) case (None, Some((index10, prefix10))) => val leaf1 = MerkleTrieNode.leaf(prefix10.assumeNibbles, value) val leaf1Hash = leaf1.toHash val children: Children = Children.empty .updateChild(index10, Some(leaf1Hash)) val branch = MerkleTrieNode.branchWithData( node.prefix, children, value0, ) val branchHash = branch.toHash state.copy( root = Some(branchHash), diff = state.diff .remove(root, node) .add(branchHash, branch) .add(leaf1Hash, leaf1), ) case (Some((index00, prefix00)), None) => val leaf0 = MerkleTrieNode.leaf(prefix00.assumeNibbles, value0) val leaf0Hash = leaf0.toHash val children: Children = Children.empty .updateChild(index00, Some(leaf0Hash)) val branch = MerkleTrieNode.branchWithData( commonPrefix, children, value, ) val branchHash = branch.toHash state.copy( root = Some(branchHash), diff = state.diff .remove(root, node) .add(branchHash, branch) .add(leaf0Hash, leaf0), ) case (Some((index00, prefix00)), Some((index10, prefix10))) => val leaf0 = MerkleTrieNode.leaf(prefix00.assumeNibbles, value0) val leaf0Hash = leaf0.toHash val leaf1 = MerkleTrieNode.leaf(prefix10.assumeNibbles, value) val leaf1Hash = leaf1.toHash val children: Children = Children.empty .updateChild(index00, Some(leaf0Hash)) .updateChild(index10, Some(leaf1Hash)) val branch = MerkleTrieNode.branch( commonPrefix, children, ) val branchHash = branch.toHash state.copy( root = Some(branchHash), diff = state.diff .remove(root, node) .add(branchHash, branch) .add(leaf0Hash, leaf0) .add(leaf1Hash, leaf1), ) def putNode( children: MerkleTrieNode.Children, ): EitherT[F, String, MerkleTrieState] = (remainder0.unCons, remainder1.unCons) match case (None, None) => // key is equal to prefix0, so we need to update node value node.getValue match case Some(nodeValue) if nodeValue === value => EitherT.rightT[F, String](state) case _ => val branch1 = node.setValue(value) val branch1Hash = branch1.toHash EitherT.rightT[F, String]: state.copy( root = Some(branch1Hash), diff = state.diff .remove(root, node) .add(branch1Hash, branch1), ) case (None, Some((index10, prefix10))) => // key is starting with prefix0, but not equal to it children(index10) match case None => // child is empty, so we need to create a new leaf val leaf1 = MerkleTrieNode.leaf(prefix10.assumeNibbles, value) val leaf1Hash = leaf1.toHash val children1 = children.updateChild(index10, Some(leaf1Hash)) val branch1 = node.setChildren(children1) val branch1Hash = branch1.toHash EitherT.rightT[F, String]: state.copy( root = Some(branch1Hash), diff = state.diff .remove(root, node) .add(branch1Hash, branch1) .add(leaf1Hash, leaf1), ) case Some(childHash) => // child is not empty, so we need to update the child recursively put[F](prefix10.assumeNibbles, value) .runS(state.copy(root = Some(childHash))) .map: childState => // println(s"======> Child state: $childState") val children1 = children .updateChild(index10, childState.root) val branch1 = node.setChildren(children1) val branch1Hash = branch1.toHash childState.copy( root = Some(branch1Hash), diff = childState.diff .remove(root, node) .add(branch1Hash, branch1), ) case (Some((index00, prefix00)), None) => // prefix is larger than key val child0 = node.setPrefix(prefix00.assumeNibbles) val child0Hash = child0.toHash val children1 = MerkleTrieNode.Children.empty .updateChild(index00, Some(child0Hash)) val branch1 = MerkleTrieNode.branchWithData( commonPrefix, children1, value, ) val branch1Hash = branch1.toHash EitherT.rightT[F, String]: state.copy( root = Some(branch1Hash), diff = state.diff .remove(root, node) .add(branch1Hash, branch1) .add(child0Hash, child0), ) case (Some((index00, prefix00)), Some((index10, prefix10))) => val child0 = node.setPrefix(prefix00.assumeNibbles) val child0Hash = child0.toHash val child1 = MerkleTrieNode.leaf(prefix10.assumeNibbles, value) val child1Hash = child1.toHash val children1 = Children.empty .updateChild(index00, Some(child0Hash)) .updateChild(index10, Some(child1Hash)) val branch1 = MerkleTrieNode.branch( commonPrefix, children1, ) val branch1Hash = branch1.toHash EitherT.rightT[F, String]: state.copy( root = Some(branch1Hash), diff = state.diff .remove(root, node) .add(branch1Hash, branch1) .add(child0Hash, child0) .add(child1Hash, child1), ) node match case MerkleTrieNode.Leaf(_, value0) => putLeaf(value0) case MerkleTrieNode.Branch(_, children) => putNode(children) case MerkleTrieNode.BranchWithData(_, children, _) => putNode(children) @SuppressWarnings(Array("org.wartremover.warts.Recursion")) def remove[F[_]: Monad: NodeStore]( key: Nibbles, ): StateT[EitherT[F, String, *], MerkleTrieState, Boolean] = type ErrorOrF[A] = EitherT[F, String, A] StateT: (state: MerkleTrieState) => val optionT = OptionT(getNodeAndStateRoot[F](state)).flatMap: case ((node, root)) => node match case MerkleTrieNode.Leaf(prefix, _) => OptionT.when(prefix === key): state.copy(root = None, diff = state.diff.remove(root, node)) case MerkleTrieNode.BranchWithData(prefix, children, _) if prefix === key => val branch1: MerkleTrieNode = MerkleTrieNode.Branch(prefix, children) val branch1Hash = branch1.toHash OptionT.pure[ErrorOrF]: state.copy( root = Some(branch1Hash), diff = state.diff.remove(root, node).add(branch1Hash, branch1), ) case _ => for stripped <- OptionT.fromOption[ErrorOrF]: key.stripPrefix(node.prefix) (index1, key1) <- OptionT.fromOption[ErrorOrF]: stripped.unCons children <- OptionT.fromOption[ErrorOrF]: node.getChildren childHash <- OptionT.fromOption[ErrorOrF]: children(index1) childStateAndResult <- OptionT.liftF: remove[F](key1.assumeNibbles).run: state.copy(root = Some(childHash)) (childState, result) = childStateAndResult state1 <- OptionT.when[ErrorOrF, MerkleTrieState](result): val needToRemoveSelf = childState.root.isEmpty && children.count(_.nonEmpty) <= 1 && node.getValue.isEmpty if needToRemoveSelf then childState.copy( root = None, diff = childState.diff.remove(root, node), ) else val children1 = children.updateChild(index1, childState.root) val branch = node.setChildren(children1) val branchHash = branch.toHash childState.copy( root = Some(branchHash), diff = childState.diff .remove(root, node) .add(branchHash, branch), ) yield state1 optionT.value.map(_.fold((state, false))((_, true))) // @SuppressWarnings(Array("org.wartremover.warts.Var")) // var count = 0L @SuppressWarnings(Array("org.wartremover.warts.Overloading")) def streamFrom[F[_]: Monad: NodeStore]( key: Nibbles, ): StateT[EitherT[F, String, *], MerkleTrieState, Stream[ EitherT[F, String, *], (Nibbles, ByteVector), ]] = streamFrom[F](key, key) @SuppressWarnings(Array("org.wartremover.warts.Recursion")) def streamFrom[F[_]: Monad: NodeStore]( key: Nibbles, originalKey: Nibbles, ): StateT[EitherT[F, String, *], MerkleTrieState, Stream[ EitherT[F, String, *], (Nibbles, ByteVector), ]] = type ErrorOrF[A] = EitherT[F, String, A] // scribe.info(s"""#${count}\t streamFrom: "${key.value.toHex}", originalKey: "${originalKey.value.toHex}"""") // scribe.info(s"""#${count}\t originalKey UTF8: "${originalKey.value.bytes.decodeUtf8}"""") // count += 1L StateT.inspectF: (state: MerkleTrieState) => scribe.debug(s"from: $key, $state") def branchStream( prefix: Nibbles, children: MerkleTrieNode.Children, value: Option[ByteVector], ): OptionT[ErrorOrF, Stream[ErrorOrF, (Nibbles, ByteVector)]] = def runFrom(key: Nibbles)( hashWithIndex: (Option[MerkleHash], Int), ): Stream[EitherT[F, String, *], (Nibbles, ByteVector)] = Stream .eval: streamFrom(key, originalKey) .runA(state.copy(root = hashWithIndex._1)) .flatten .map: (key, a) => val indexNibble = BitVector.fromInt(hashWithIndex._2, 4).assumeNibbles val key1 = Nibbles.combine(prefix, indexNibble, key) (key1, a) // scribe.info(s"Branch: $key <= $prefix: ${key <= prefix}") if key <= prefix then // key is less than or equal to prefix, so all of its value and children should be included val initialValue: Stream[ErrorOrF, (Nibbles, ByteVector)] = value.fold(Stream.empty): bytes => Stream.eval(EitherT.pure((prefix, bytes))) val tailStream = Stream .emits: children.toList.zipWithIndex.filter(_._1.nonEmpty) .flatMap(runFrom(Nibbles.empty)) OptionT.liftF: EitherT.rightT[F, String]: initialValue ++ tailStream else for keyRemainder <- OptionT.fromOption[ErrorOrF]: // if key is not starting with prefix (fail to strip) there is no stream to return key.stripPrefix(prefix) (index1, key1) <- OptionT.fromOption[ErrorOrF]: keyRemainder.unCons // keyRemainder is not empty here (key > prefix) targetChildrenWithIndex = children.toList.zipWithIndex.drop(index1) stream <- OptionT.liftF: EitherT.rightT[F, String]: targetChildrenWithIndex match case Nil => Stream.empty case x :: xs => val head = Stream.emit(x).flatMap(runFrom(key1.assumeNibbles)) val tail = Stream .emits(xs.filter(_._1.nonEmpty)) .flatMap(runFrom(Nibbles.empty)) head ++ tail yield stream OptionT(getNode[F](state)) .flatMap: case MerkleTrieNode.Leaf(prefix, value) => // scribe.info(s"Leaf: $key <= $prefix: ${key <= prefix}") // scribe.info(s"#$count\tLeaf: ${prefix.value.toHex}") OptionT.when(key <= prefix): Stream.emit((prefix, value)) case MerkleTrieNode.Branch(prefix, children) => // scribe.info(s"#$count\tBranch: ${prefix.value.toHex}") // scribe.info(s"Children Size: ${children.flatten.size}") branchStream(prefix, children, None) case MerkleTrieNode.BranchWithData(prefix, children, value) => // scribe.info(s"#$count\tBranch: ${prefix.value.toHex}") // scribe.info(s"Children Size: ${children.flatten.size}") branchStream(prefix, children, Some(value)) .value .map: _.getOrElse: // scribe.info(s"#${count}\tNo node found for key: ${key.value.toHex}") Stream.empty /** @param keyPrefix: * the key prefix to get the stream from. This prefix must be included. * @param keySuffix: * optional key suffix. If this suffix is provided, the stream's key * iterates over values less than keyPrefix + keySuffix. * @return * the stream of values starting with the given key prefix. */ @SuppressWarnings(Array("org.wartremover.warts.Recursion")) def reverseStreamFrom[F[_]: Monad: NodeStore]( keyPrefix: Nibbles, keySuffix: Option[Nibbles], ): StateT[EitherT[F, String, *], MerkleTrieState, Stream[ EitherT[F, String, *], (Nibbles, ByteVector), ]] = StateT.inspectF: (state: MerkleTrieState) => scribe.debug(s"from: ($keyPrefix, $keySuffix): $state") def reverseBranchStream( prefix: Nibbles, children: MerkleTrieNode.Children, value: Option[ByteVector], ): OptionT[EitherT[F, String, *], Stream[ EitherT[F, String, *], (Nibbles, ByteVector), ]] = def reverseRunFrom(keyPrefix: Nibbles, keySuffix: Option[Nibbles])( hashWithIndex: (Option[MerkleHash], Int), ): Stream[EitherT[F, String, *], (Nibbles, ByteVector)] = // scribe.info(s"reverseRunFrom: $key, $hashWithIndex") Stream .eval: reverseStreamFrom(keyPrefix, keySuffix) .runA(state.copy(root = hashWithIndex._1)) .flatten .map: (key, a) => val indexNibble = BitVector.fromInt(hashWithIndex._2, 4).assumeNibbles val key1 = Nibbles.combine(prefix, indexNibble, key) (key1, a) def reverseRunAllOptionT = val lastStream = value.fold(Stream.empty): bytes => Stream.eval: EitherT.rightT[F, String]: (prefix, bytes) val initStream = Stream .emits(children.toList.zipWithIndex.reverse) .flatMap(reverseRunFrom(Nibbles.empty, None)) OptionT.liftF: EitherT.rightT[F, String]: initStream ++ lastStream def streamFromKeySuffix( keySuffix: Nibbles, ): OptionT[EitherT[F, String, *], Stream[ EitherT[F, String, *], (Nibbles, ByteVector), ]] = keySuffix.unCons match case None => reverseRunAllOptionT case Some((index1, key1)) => val targetChildren = children.toList.zipWithIndex .take(index1 + 1) .reverse targetChildren match case Nil => OptionT.none case x :: xs => OptionT.liftF: EitherT.rightT[F, String]: val headStream = Stream .emit(x) .flatMap: reverseRunFrom(Nibbles.empty, Some(key1.assumeNibbles)) val tailStream = Stream .emits(xs.filter(_._1.nonEmpty)) .flatMap: reverseRunFrom(Nibbles.empty, None) val lastStream = value.fold(Stream.empty): bytes => Stream.emit((prefix, bytes)) headStream ++ tailStream ++ lastStream keyPrefix.stripPrefix(prefix) match // keyPrefix is not starting with prefix case None => prefix.stripPrefix(keyPrefix) match // prefix is not starting with keyPrefix so we don't need to include it case None => OptionT.none // prefix is starting with keyPrefix so we need to include it case Some(prefixRemainder) => keySuffix.fold(reverseRunAllOptionT): keySuffix1 => if prefixRemainder <= keySuffix1 then reverseRunAllOptionT else // Here, keySuffix1 < prefixRemainder keySuffix1.stripPrefix(prefixRemainder) match case None => OptionT.none case Some(keySuffix2) => streamFromKeySuffix(keySuffix2.assumeNibbles) case Some(keyRemainder) => keyRemainder.unCons match case None => keySuffix.fold(reverseRunAllOptionT)(streamFromKeySuffix) case Some((index1, key1)) => children.toList.zipWithIndex.take(index1 + 1).reverse match case Nil => OptionT.none case x :: xs => OptionT.liftF: EitherT.rightT[F, String]: Stream .emit(x) .flatMap: reverseRunFrom(key1.assumeNibbles, keySuffix) OptionT(getNode[F](state)) .flatMap: case MerkleTrieNode.Leaf(prefix, value) => prefix.stripPrefix(keyPrefix) match case None => OptionT.none case Some(prefixRemainder) => keySuffix match case None => OptionT.pure: Stream.emit((prefix, value)) case Some(suffix) => OptionT.when(prefixRemainder <= suffix): Stream.emit((prefix, value)) case MerkleTrieNode.Branch(prefix, children) => reverseBranchStream(prefix, children, None) case MerkleTrieNode.BranchWithData(prefix, children, value) => reverseBranchStream(prefix, children, Some(value)) .value .map(_.getOrElse(Stream.empty)) def getNodeAndStateRoot[F[_]: Monad](state: MerkleTrieState)(using ns: NodeStore[F], ): EitherT[F, String, Option[(MerkleTrieNode, MerkleHash)]] = state.root.fold(EitherT.rightT[F, String](None)): root => state.diff .get(root) .fold(ns.run(root).map(_.map((_, root)))): node => EitherT.rightT[F, String](Some((node, root))) def getNode[F[_]: Monad](state: MerkleTrieState)(using ns: NodeStore[F], ): EitherT[F, String, Option[MerkleTrieNode]] = state.root.fold(EitherT.rightT[F, String](None)): root => state.diff .get(root) .fold(ns.run(root)): node => EitherT.rightT[F, String](Some(node)) def getCommonPrefixNibbleAndRemainders( nibbles0: Nibbles, nibbles1: Nibbles, ): (Nibbles, Nibbles, Nibbles) = val commonPrefixNibbleSize: Int = (nibbles0.value ^ nibbles1.value) .grouped(4L) .takeWhile(_ === BitVector.low(4L)) .size val nextPrefixBitSize = commonPrefixNibbleSize.toLong * 4L val remainder0 = nibbles0.value drop nextPrefixBitSize val (commonPrefix, remainder1) = nibbles1.value splitAt nextPrefixBitSize ( commonPrefix.assumeNibbles, remainder0.assumeNibbles, remainder1.assumeNibbles, ) end MerkleTrie ================================================ FILE: modules/lib/shared/src/main/scala/io/leisuremeta/chain/lib/merkle/MerkleTrieNode.scala ================================================ package io.leisuremeta.chain.lib package merkle import scala.compiletime.constValue import cats.Eq import io.github.iltotore.iron.* import io.github.iltotore.iron.constraint.collection.* import scodec.bits.{BitVector, ByteVector} import codec.byte.{ByteDecoder, ByteEncoder, DecodeResult} import datatype.{BigNat, UInt256} import crypto.Hash import failure.DecodingFailure sealed trait MerkleTrieNode: def prefix: Nibbles def getChildren: Option[MerkleTrieNode.Children] = this match case MerkleTrieNode.Leaf(_, _) => None case MerkleTrieNode.Branch(_, children) => Some(children) case MerkleTrieNode.BranchWithData(_, children, _) => Some(children) def getValue: Option[ByteVector] = this match case MerkleTrieNode.Leaf(_, value) => Some(value) case MerkleTrieNode.Branch(_, _) => None case MerkleTrieNode.BranchWithData(_, _, value) => Some(value) def setPrefix(prefix: Nibbles): MerkleTrieNode = this match case MerkleTrieNode.Leaf(_, value) => MerkleTrieNode.Leaf(prefix, value) case MerkleTrieNode.Branch(_, children) => MerkleTrieNode.Branch(prefix, children) case MerkleTrieNode.BranchWithData(_, key, value) => MerkleTrieNode.BranchWithData(prefix, key, value) def setChildren( children: MerkleTrieNode.Children, ): MerkleTrieNode = this match case MerkleTrieNode.Leaf(prefix, value) => MerkleTrieNode.BranchWithData(prefix, children, value) case MerkleTrieNode.Branch(prefix, _) => MerkleTrieNode.Branch(prefix, children) case MerkleTrieNode.BranchWithData(prefix, _, value) => MerkleTrieNode.BranchWithData(prefix, children, value) def setValue(value: ByteVector): MerkleTrieNode = this match case MerkleTrieNode.Leaf(prefix, _) => MerkleTrieNode.Leaf(prefix, value) case MerkleTrieNode.Branch(prefix, children) => MerkleTrieNode.BranchWithData(prefix, children, value) case MerkleTrieNode.BranchWithData(prefix, children, _) => MerkleTrieNode.BranchWithData(prefix, children, value) override def toString: String = @SuppressWarnings(Array("org.wartremover.warts.Any")) val childrenString = getChildren.fold("[]"): (childrenRefined) => (0 until 16) .map: i => f"${i}%x: ${childrenRefined(i)}" .mkString("[", ",", "]") s"MerkleTrieNode(${prefix.value.toHex}, $childrenString, $getValue)" object MerkleTrieNode: final case class Leaf(prefix: Nibbles, value: ByteVector) extends MerkleTrieNode final case class Branch(prefix: Nibbles, children: Children) extends MerkleTrieNode final case class BranchWithData( prefix: Nibbles, children: Children, value: ByteVector, ) extends MerkleTrieNode def leaf(prefix: Nibbles, value: ByteVector): MerkleTrieNode = Leaf(prefix, value) def branch( prefix: Nibbles, children: Children, ): MerkleTrieNode = Branch(prefix, children) def branchWithData( prefix: Nibbles, children: Children, value: ByteVector, ): MerkleTrieNode = BranchWithData(prefix, children, value) type MerkleHash = Hash.Value[MerkleTrieNode] type MerkleRoot = MerkleHash type Children = Vector[Option[MerkleHash]] :| ChildrenCondition type ChildrenCondition = Length[StrictEqual[16]] extension (c: Children) def updateChild(i: Int, v: Option[MerkleHash]): Children = c.updated(i, v).assume object Children: inline def empty: Children = Vector.fill(16)(Option.empty[MerkleHash]).assume given Eq[MerkleTrieNode] = Eq.fromUniversalEquals given merkleTrieNodeEncoder: ByteEncoder[MerkleTrieNode] = node => val encodePrefix: ByteVector = val prefixNibbleSize: Long = node.prefix.value.size / 4 ByteEncoder[BigNat].encode( BigNat.unsafeFromBigInt(BigInt(prefixNibbleSize)), ) ++ node.prefix.bytes def encodeChildren(children: MerkleTrieNode.Children): ByteVector = val existenceBytes = BitVector.bits(children.map(_.nonEmpty)).bytes children .flatMap(_.toList) .foldLeft(existenceBytes)(_ ++ _.toUInt256Bytes) def encodeValue(value: ByteVector): ByteVector = ByteEncoder[BigNat].encode( BigNat.unsafeFromBigInt(BigInt(value.size)), ) ++ value val encoded = node match case Leaf(_, value) => ByteVector.fromByte(1) ++ encodePrefix ++ encodeValue(value) case Branch(_, children) => ByteVector.fromByte(2) ++ encodePrefix ++ encodeChildren(children) case BranchWithData(_, children, value) => ByteVector.fromByte(3) ++ encodePrefix ++ encodeChildren( children, ) ++ encodeValue(value) encoded given merkleTrieNodeDecoder: ByteDecoder[MerkleTrieNode] = val childrenDecoder: ByteDecoder[MerkleTrieNode.Children] = ByteDecoder .fromFixedSizeBytes(2)(_.bits) .flatMap { (existenceBits) => (bytes) => type LoopType = Either[DecodingFailure, DecodeResult[ Vector[Option[MerkleHash]], ]] @annotation.tailrec @SuppressWarnings(Array("org.wartremover.warts.Nothing")) def loop( bits: BitVector, bytes: ByteVector, acc: List[Option[MerkleHash]], ): LoopType = bits.headOption match case None => Right(DecodeResult(acc.reverse.toVector, bytes)) case Some(false) => loop(bits.tail, bytes, None :: acc) case Some(true) => val (hashBytes, rest) = bytes.splitAt(32) UInt256.from(hashBytes) match case Left(err) => Left(DecodingFailure(err.msg)) case Right(hash) => loop( bits.tail, rest, Some(Hash.Value[MerkleTrieNode](hash)) :: acc, ) end loop loop(existenceBits, bytes, Nil) } .map(_.assume) val valueDecoder: ByteDecoder[ByteVector] = ByteDecoder[BigNat].flatMap { (size) => ByteDecoder.fromFixedSizeBytes(size.toBigInt.toLong)(identity) } ByteDecoder.byteDecoder .emap: b => Either.cond(1 <= b && b <= 3, b, DecodingFailure(s"wrong range: $b")) .flatMap: case 1 => for prefix <- nibblesByteDecoder value <- valueDecoder yield Leaf(prefix, value) case 2 => for prefix <- nibblesByteDecoder children <- childrenDecoder yield Branch(prefix, children) case 3 => for prefix <- nibblesByteDecoder children <- childrenDecoder value <- valueDecoder yield BranchWithData(prefix, children, value) given merkleTrieNodeHash: Hash[MerkleTrieNode] = Hash.build ================================================ FILE: modules/lib/shared/src/main/scala/io/leisuremeta/chain/lib/merkle/MerkleTrieState.scala ================================================ package io.leisuremeta.chain.lib.merkle import cats.syntax.eq.* import MerkleTrieNode.MerkleRoot final case class MerkleTrieState( root: Option[MerkleRoot], base: Option[MerkleRoot], diff: MerkleTrieStateDiff, ): def rebase(that: MerkleTrieState): Either[String, MerkleTrieState] = def right: MerkleTrieState = val map1 = this.diff.toMap .map: case (k, (v, count)) => val thatCount = that.diff.toMap.get(k).fold(0)(_._2) (k, (v, count + thatCount)) .toMap this.copy( base = that.root, diff = MerkleTrieStateDiff(map1), ) end right Either.cond( this.base === that.base, right, s"Different base", ) object MerkleTrieState: def empty: MerkleTrieState = MerkleTrieState( None, None, MerkleTrieStateDiff.empty, ) def fromRoot(root: MerkleRoot): MerkleTrieState = MerkleTrieState( root = Some(root), base = Some(root), diff = MerkleTrieStateDiff.empty, ) def fromRootOption(root: Option[MerkleRoot]): MerkleTrieState = MerkleTrieState( root = root, base = root, diff = MerkleTrieStateDiff.empty, ) ================================================ FILE: modules/lib/shared/src/main/scala/io/leisuremeta/chain/lib/merkle/MerkleTrieStateDiff.scala ================================================ package io.leisuremeta.chain.lib.merkle import MerkleTrieNode.MerkleHash opaque type MerkleTrieStateDiff = Map[MerkleHash, (MerkleTrieNode, Int)] object MerkleTrieStateDiff: def apply( map: Map[MerkleHash, (MerkleTrieNode, Int)], ): MerkleTrieStateDiff = map def empty: MerkleTrieStateDiff = Map.empty extension (diff: MerkleTrieStateDiff) def get(hash: MerkleHash): Option[MerkleTrieNode] = diff.get(hash).flatMap { case (node, count) if count > 0 => Some(node) case _ => None } def foreach(f: (MerkleHash, MerkleTrieNode) => Unit): Unit = for diffItem <- diff (hash, (node, count)) = diffItem if count > 0 yield f(hash, node) () def add( hash: MerkleHash, node: MerkleTrieNode, ): MerkleTrieStateDiff = diff.get(hash).fold(diff + (hash -> (node, 1))) { case (node, -1) => diff - hash case (node, count) => diff + (hash -> (node, count + 1)) } def remove( hash: MerkleHash, node: MerkleTrieNode, ): MerkleTrieStateDiff = diff.get(hash).fold(diff + (hash -> (node, -1))) { case (node, 1) => diff - hash case (node, count) => diff + (hash -> (node, count - 1)) } def toList: List[(MerkleHash, (MerkleTrieNode, Int))] = diff.toList def toMap: Map[MerkleHash, (MerkleTrieNode, Int)] = diff end extension ================================================ FILE: modules/lib/shared/src/main/scala/io/leisuremeta/chain/lib/merkle/package.scala ================================================ package io.leisuremeta.chain.lib package merkle import scala.compiletime.{summonInline} import cats.Eq import cats.syntax.either.* import cats.syntax.eq.given import io.github.iltotore.iron.* import io.github.iltotore.iron.constraint.collection.* import io.github.iltotore.iron.constraint.numeric.* import scodec.bits.{BitVector, ByteVector} import codec.byte.{ByteDecoder, ByteEncoder} import codec.byte.ByteEncoder.ops.* import datatype.BigNat import failure.DecodingFailure import util.iron.given opaque type Nibbles = BitVector :| Nibbles.NibbleCond object Nibbles: type NibbleCond = Length[Multiple[4L]] val empty: Nibbles = BitVector.empty.assumeNibbles def combine(nibbles: Nibbles*): Nibbles = nibbles.foldLeft(BitVector.empty)(_ ++ _.value).assumeNibbles extension (nibbles: Nibbles) def value: BitVector = nibbles def bytes: ByteVector = nibbles.bytes def nibbleSize: Long = nibbles.size / 4L def unCons: Option[(Int, Nibbles)] = if nibbles.isEmpty then None else val head = nibbles.value.take(4).toInt(signed = false) val tail = nibbles.value.drop(4).assumeNibbles Some((head, tail)) def stripPrefix(prefix: Nibbles): Option[Nibbles] = if nibbles.startsWith(prefix) then Some(nibbles.drop(prefix.size).assumeNibbles) else None def compareTo(that: Nibbles): Int = val thisBytes = nibbles.bytes val thatBytes = that.bytes val minSize = thisBytes.size min thatBytes.size (0L `until` minSize) .find: i => thisBytes.get(i) =!= thatBytes.get(i) .fold(thisBytes.size compareTo thatBytes.size): i => (thisBytes.get(i) & 0xff) compare (thatBytes.get(i) & 0xff) def <=(that: Nibbles): Boolean = compareTo(that) <= 0 def <(that: Nibbles): Boolean = compareTo(that) < 0 def >=(that: Nibbles): Boolean = compareTo(that) >= 0 def >(that: Nibbles): Boolean = compareTo(that) > 0 extension (bitVector: BitVector) def refineToNibble: Either[String, Nibbles] = bitVector.refineEither[Length[Multiple[4L]]] def assumeNibbles: Nibbles = bitVector.assume[Nibbles.NibbleCond] extension (byteVector: ByteVector) def toNibbles: Nibbles = byteVector.bits.refineUnsafe[Length[Multiple[4L]]] given nibblesByteEncoder: ByteEncoder[Nibbles] = (nibbles: Nibbles) => BigNat.unsafeFromLong(nibbles.size / 4).toBytes ++ nibbles.bytes given nibblesByteDecoder: ByteDecoder[Nibbles] = ByteDecoder[BigNat].flatMap: nibbleSize => val nibbleSizeLong = nibbleSize.toBigInt.toLong ByteDecoder .fromFixedSizeBytes((nibbleSizeLong + 1) / 2): nibbleBytes => val bitsSize = nibbleSizeLong * 4 val padSize = bitsSize - nibbleBytes.size * 8 val nibbleBits = if padSize > 0 then nibbleBytes.bits.padLeft(padSize) else nibbleBytes.bits nibbleBits.take(bitsSize) .emap(_.refineToNibble.leftMap(DecodingFailure(_))) given nibblesEq: Eq[Nibbles] = Eq.fromUniversalEquals ================================================ FILE: modules/lib/shared/src/main/scala/io/leisuremeta/chain/lib/util/iron/package.scala ================================================ package io.leisuremeta.chain.lib.util.iron import scala.compiletime.{summonInline} import io.github.iltotore.iron.* import io.github.iltotore.iron.constraint.collection.* import scodec.bits.{BitVector, ByteVector} @SuppressWarnings(Array("org.wartremover.warts.ImplicitParameter")) class LengthBitVector[C, Impl <: Constraint[Long, C]](using Impl) extends Constraint[BitVector, Length[C]]: override inline def test(value: BitVector): Boolean = summonInline[Impl].test(value.size) override inline def message: String = s"Length: (${summonInline[Impl].message})" @SuppressWarnings(Array("org.wartremover.warts.ImplicitParameter")) inline given [C, Impl <: Constraint[Long, C]](using inline impl: Impl, ): LengthBitVector[C, Impl] = new LengthBitVector[C, Impl] @SuppressWarnings(Array("org.wartremover.warts.ImplicitParameter")) class LengthByteVector[C, Impl <: Constraint[Long, C]](using Impl) extends Constraint[ByteVector, Length[C]]: override inline def test(value: ByteVector): Boolean = summonInline[Impl].test(value.size) override inline def message: String = s"Length: (${summonInline[Impl].message})" @SuppressWarnings(Array("org.wartremover.warts.ImplicitParameter")) inline given [C, Impl <: Constraint[Long, C]](using inline impl: Impl, ): LengthByteVector[C, Impl] = new LengthByteVector[C, Impl] ================================================ FILE: modules/lib/shared/src/main/scala/io/leisuremeta/chain/lib/util/refined/bitVector.scala ================================================ package io.leisuremeta.chain.lib.util.refined import eu.timepit.refined.api.Validate import eu.timepit.refined.collection.Size import eu.timepit.refined.internal.Resources import scodec.bits.BitVector object bitVector extends BitVectorValidate private[refined] trait BitVectorValidate: given bitVectorSizeValidate[P, RP](using v: Validate.Aux[Long, P, RP], ): Validate.Aux[BitVector, Size[P], Size[v.Res]] = new Validate[BitVector, Size[P]]: override type R = Size[v.Res] override def validate(t: BitVector): Res = val r = v.validate(t.size) r.as(Size(r)) override def showExpr(t: BitVector): String = v.showExpr(t.size) override def showResult(t: BitVector, r: Res): String = val size = t.size val nested = v.showResult(size, r.detail.p) Resources.predicateTakingResultDetail(s"size($t) = $size", r, nested) ================================================ FILE: modules/lib/shared/src/test/scala/io/leisuremeta/chain/lib/codec/ByteCodecTest.scala ================================================ package io.leisuremeta.chain.lib package codec import hedgehog.munit.HedgehogSuite import hedgehog.* import codec.byte.{ByteDecoder, ByteEncoder, DecodeResult} class ByteCodecTest extends HedgehogSuite: property("roundtrip of bigint byte codec") { for bignat <- Gen .bytes(Range.linear(1, 64)) .map(BigInt(_)) .forAll yield val encoded = ByteEncoder[BigInt].encode(bignat) ByteDecoder[BigInt].decode(encoded) match case Right(DecodeResult(decoded, remainder)) => Result.all( List( decoded ==== bignat, Result.assert(remainder.isEmpty), ), ) case _ => Result.failure } ================================================ FILE: modules/lib/shared/src/test/scala/io/leisuremeta/chain/lib/crypto/CryptoOpsTest.scala ================================================ package io.leisuremeta.chain.lib package crypto import hedgehog.munit.HedgehogSuite import hedgehog.* import scodec.bits.* class CryptoOpsTest extends HedgehogSuite: test("keccak256 #1") { withMunitAssertions { assertions => val expected = hex"c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470" val result = ByteVector.view(CryptoOps.keccak256(Array.empty)) assertions.assertEquals(result, expected) } } test("keccak256 #2") { withMunitAssertions { assertions => val expected = hex"4d741b6f1eb29cb2a9b9911c82f56fa8d73b04959d3d9d222895df6c0b28aa15" val result = ByteVector.view( CryptoOps.keccak256( "The quick brown fox jumps over the lazy dog".getBytes(), ), ) assertions.assertEquals(result, expected) } } test("keccak256 #3") { withMunitAssertions { assertions => val expected = hex"578951e24efd62a3d63a86f7cd19aaa53c898fe287d2552133220370240b572d" val result = ByteVector.view( CryptoOps.keccak256( "The quick brown fox jumps over the lazy dog.".getBytes(), ), ) assertions.assertEquals(result, expected) } } test("keypair") { withMunitAssertions { assertions => val keyPair = CryptoOps.fromPrivate( BigInt( "10e93a6c964aa6bc089f84e4fe3fb37583f3e1162891a689dd99bb629520f3df", 16, ), ) val expected = hex"e72699136b12ffd11549616ff047cd5ec93665cd6f13b859030a3c99d14842abc27a7442bc05143db53c41407a7059c85def28f6749b86b3123c48be3085e459" assertions.assertEquals(keyPair.publicKey.toBytes, expected) } } ================================================ FILE: modules/lib/shared/src/test/scala/io/leisuremeta/chain/lib/datatype/BigNatTest.scala ================================================ package io.leisuremeta.chain.lib package datatype //import eu.timepit.refined.auto.autoUnwrap import io.circe.Decoder //import io.circe.generic.auto.given //import io.circe.refined.given import io.circe.syntax.given import hedgehog.munit.HedgehogSuite import hedgehog.* import codec.byte.{ByteDecoder, ByteEncoder, DecodeResult} class BigNatTest extends HedgehogSuite: property("roundtrip of bignat byte codec") { for bignat <- Gen .bytes(Range.linear(0, 64)) .map(BigInt(1, _)) .map(BigNat.unsafeFromBigInt) .forAll yield val encoded = ByteEncoder[BigNat].encode(bignat) ByteDecoder[BigNat].decode(encoded) match case Right(DecodeResult(decoded, remainder)) => Result.all( List( decoded ==== bignat, Result.assert(remainder.isEmpty), ), ) case _ => Result.failure } property("roundtrip of bignat circe codec") { for bignat <- Gen .bytes(Range.linear(0, 64)) .map(BigInt(1, _)) .map(BigNat.unsafeFromBigInt) .forAll yield val encoded = bignat.asJson Decoder[BigNat].decodeJson(encoded) match case Right(decoded) => decoded ==== bignat case _ => Result.failure } ================================================ FILE: modules/lib/shared/src/test/scala/io/leisuremeta/chain/lib/datatype/UInt256Test.scala ================================================ package io.leisuremeta.chain.lib.datatype import hedgehog.munit.HedgehogSuite import hedgehog.* import scodec.bits.ByteVector class UInt256Test extends HedgehogSuite: property("roundtrip of uint256bytes") { for bytes <- Gen.bytes(Range.singleton(32)).map(ByteVector.view).forAll yield val roundTrip = for uint256bytes <- UInt256.from(bytes) yield uint256bytes.toBytes roundTrip ==== Right(bytes) } property("roundtrip of uint256bigint") { for bigint <- Gen.bytes(Range.singleton(32)).map(BigInt(1, _)).forAll yield val roundTrip = for uint256bigint <- UInt256.from(bigint) yield uint256bigint.toBigInt roundTrip ==== Right(bigint) } property("roundtrip of uint256bytes -> uint256bigint -> uint256bytes") { for bytes <- Gen.bytes(Range.singleton(32)).map(ByteVector.view).forAll yield val roundTrip = for uint256bytes <- UInt256.from(bytes) bigint = uint256bytes.toBigInt uint256bigint <- UInt256.from(bigint) yield uint256bigint.toBytes roundTrip ==== Right(bytes) } property("roundtrip of uint256bigint -> uint256bytes -> uint256bigint") { for bigint <- Gen.bytes(Range.singleton(32)).map(BigInt(1, _)).forAll yield val roundTrip = for uint256bigint <- UInt256.from(bigint) bytes = uint256bigint.toBytes uint256bytes <- UInt256.from(bytes) yield uint256bytes.toBigInt roundTrip ==== Right(bigint) } ================================================ FILE: modules/lib/shared/src/test/scala/io/leisuremeta/chain/lib/merkle/MerkleTrieNodeTest.scala ================================================ package io.leisuremeta.chain.lib package merkle import io.github.iltotore.iron.assume import scodec.bits.ByteVector import hedgehog.* import hedgehog.munit.HedgehogSuite import codec.byte.{ByteDecoder, ByteEncoder, DecodeResult} import crypto.Hash import datatype.UInt256 class MerkleTrieNodeTest extends HedgehogSuite: val genPrefix: Gen[Nibbles] = for bytes <- Gen.bytes(Range.linear(0, 64)) byteVector = ByteVector.view(bytes) bits <- Gen.element1( byteVector.bits, byteVector.bits.drop(4), ) yield bits.assumeNibbles def genChildren: Gen[MerkleTrieNode.Children] = Gen .list[Option[MerkleTrieNode.MerkleHash]]( Gen.frequency1( 1 -> Gen.constant(None), 9 -> Gen.bytes(Range.singleton(32)).map { (byteArray) => UInt256.from(ByteVector.view(byteArray)).toOption.map { Hash.Value[MerkleTrieNode](_) } }, ), Range.singleton(16), ) .map: (list) => list.toVector.assume[MerkleTrieNode.ChildrenCondition] def genValue: Gen[ByteVector] = Gen.sized(size => Gen.bytes(Range.linear(0, size.value.abs)), ) map ByteVector.view def genLeaf: Gen[MerkleTrieNode] = for prefix <- genPrefix value <- genValue yield MerkleTrieNode.Leaf(prefix, value) def genBranch: Gen[MerkleTrieNode] = for prefix <- genPrefix children <- genChildren yield MerkleTrieNode.Branch(prefix, children) def genBranchWithData: Gen[MerkleTrieNode] = for prefix <- genPrefix children <- genChildren value <- genValue yield MerkleTrieNode.BranchWithData(prefix, children, value) def genMerkleTrieNode: Gen[MerkleTrieNode] = for node <- Gen.choice1( genLeaf, genBranch, genBranchWithData, ) yield node property("roundtrip of MerkleTrieNode byte codec"): for node <- genMerkleTrieNode.forAll yield val encoded = ByteEncoder[MerkleTrieNode].encode(node) ByteDecoder[MerkleTrieNode].decode(encoded) match case Right(DecodeResult(decoded, remainder)) => Result.all( List( decoded ==== node, Result.assert(remainder.isEmpty), ), ) case Left(error) => println(s"=== error: ${error.msg}") println(s"=== encoded: $encoded") Result.failure ================================================ FILE: modules/lib/shared/src/test/scala/io/leisuremeta/chain/lib/merkle/MerkleTrieTest.scala ================================================ package io.leisuremeta.chain.lib package merkle import hedgehog.munit.HedgehogSuite import hedgehog.* import hedgehog.state.* import scala.collection.immutable.SortedMap import cats.Id import cats.arrow.FunctionK import cats.data.{EitherT, Kleisli} import cats.effect.SyncIO import cats.syntax.all.* import fs2.Stream import scodec.bits.ByteVector import scodec.bits.hex import codec.byte.{ByteDecoder, ByteEncoder} import datatype.BigNat import MerkleTrie.NodeStore import MerkleTrieNode.{MerkleHash, MerkleRoot} class MerkleTrieTest extends HedgehogSuite: given ByteEncoder[ByteVector] = (bytes: ByteVector) => import ByteEncoder.ops.* BigNat.unsafeFromBigInt(bytes.size).toBytes ++ bytes given ByteDecoder[ByteVector] = ByteDecoder[BigNat].flatMap { size => ByteDecoder.fromFixedSizeBytes(size.toBigInt.toLong)(identity) } case class State( current: SortedMap[ByteVector, ByteVector], hashLog: Map[SortedMap[ByteVector, ByteVector], Option[MerkleRoot]], ) object State: def empty: State = State(SortedMap.empty, Map.empty) case class Get(key: Nibbles) case class Put(key: Nibbles, value: ByteVector) case class Remove(key: Nibbles) case class StreamFrom(key: Nibbles) case class ReverseStreamFrom(keyPrefix: Nibbles, keySuffix: Option[Nibbles]) given emptyIdNodeStore: NodeStore[Id] = Kleisli: (_: MerkleHash) => EitherT.rightT[Id, String](None) given emptyIoNodeStore: NodeStore[SyncIO] = Kleisli: (_: MerkleHash) => EitherT.rightT[SyncIO, String](None) val initialState = MerkleTrieState.empty var merkleTrieState: MerkleTrieState = initialState val genByteVector = Gen.bytes(Range.linear(0, 64)).map(ByteVector.view) def commandGet: CommandIO[State] = new Command[State, Get, Option[ByteVector]]: override def gen(s: State): Option[Gen[Get]] = Some( (s.current.keys.toList match case Nil => genByteVector case h :: t => Gen.frequency1( 80 -> Gen.element(h, t), 20 -> genByteVector, ) ).map(bytes => Get(bytes.toNibbles)), ) override def execute( env: Environment, i: Get, ): Either[String, Option[ByteVector]] = val program = MerkleTrie.get[Id](i.key) program.runA(merkleTrieState).value override def update(s: State, i: Get, o: Var[Option[ByteVector]]): State = s override def ensure( env: Environment, s0: State, s: State, i: Get, o: Option[ByteVector], ): Result = s.current.get(i.key.bytes) ==== o def commandPut: CommandIO[State] = new Command[State, Put, Unit]: override def gen(s: State): Option[Gen[Put]] = Some(for key <- genByteVector value <- genByteVector yield Put(key.toNibbles, value)) override def execute(env: Environment, i: Put): Either[String, Unit] = // println(s"===> execute: $i") val program = MerkleTrie.put[Id](i.key, i.value) program.runS(merkleTrieState).value.map { (newState: MerkleTrieState) => merkleTrieState = newState } override def update(s: State, i: Put, o: Var[Unit]): State = // println(s"===> Command Put (${i}) update: ${s.current}") val current1 = s.current + ((i.key.bytes -> i.value)) val stateRoot = merkleTrieState.root val hashLog1 = s.hashLog + ((current1 -> stateRoot)) // println(s"===> After Command Put(${i}) update: ${current1}") State(current1, hashLog1) override def ensure( env: Environment, s0: State, s: State, i: Put, o: Unit, ): Result = Result.all( List( // s0.hashLog.get(s.current).fold(Result.success) { // (rootOption: Option[MerkleRoot[K, V]]) => // if s.hashLog.get(s.current) != Some(rootOption) then // println(s"===> current: ${s.current}") // s.hashLog.get(s.current) ==== Some(rootOption) // }, merkleTrieState.root.fold(Result.success) { (root: MerkleRoot) => val result = merkleTrieState.diff.get(root).nonEmpty if result == false then println(s"====> failed: $i with state ${s0.current}") Result.assert(result) }, s.current.get(i.key.bytes) ==== Some(i.value), ), ) def commandRemove: CommandIO[State] = new Command[State, Remove, Boolean]: override def gen(s: State): Option[Gen[Remove]] = Some( (s.current.keys.toList match case Nil => genByteVector case h :: t => Gen.frequency1( 80 -> Gen.element(h, t), 20 -> genByteVector, ) ).map(bytes => Remove(bytes.toNibbles)), ) override def execute(env: Environment, i: Remove): Either[String, Boolean] = val program = MerkleTrie.remove[Id](i.key) program.run(merkleTrieState).value.map { case (state1, result) => merkleTrieState = state1 result } override def update(s: State, i: Remove, o: Var[Boolean]): State = val current1 = s.current - i.key.bytes val stateRoot = merkleTrieState.root val hashLog1 = s.hashLog + ((current1 -> stateRoot)) State(current1, hashLog1) override def ensure( env: Environment, s0: State, s: State, i: Remove, o: Boolean, ): Result = Result.all( List( s0.current.contains(i.key.bytes) ==== o, s.current.get(i.key.bytes) ==== None, ), ) type S = Stream[EitherT[Id, String, *], (Nibbles, ByteVector)] def commandStreamFrom: CommandIO[State] = new Command[State, StreamFrom, S]: override def gen(s: State): Option[Gen[StreamFrom]] = Some( (s.current.keys.toList match case Nil => genByteVector case h :: t => Gen.frequency1( 80 -> Gen.element(h, t), 20 -> genByteVector, ) ).map(bytes => StreamFrom(bytes.toNibbles)), ) override def execute(env: Environment, i: StreamFrom): Either[String, S] = val program = MerkleTrie.streamFrom[Id](i.key) program.runA(merkleTrieState).value override def update(s: State, i: StreamFrom, o: Var[S]): State = s override def ensure( env: Environment, s0: State, s: State, i: StreamFrom, o: S, ): Result = val toId = new FunctionK[EitherT[Id, String, *], Id]: override def apply[A](fa: EitherT[Id, String, A]): Id[A] = fa.value.toOption.get val expected = s.current.iteratorFrom(i.key.bytes).take(10).toList.map { (k, v) => (k.bits, v) } expected ==== o .take(10) .translate[EitherT[Id, String, *], Id](toId) .compile .toList def commandReverseStreamFrom: CommandIO[State] = new Command[State, ReverseStreamFrom, S]: override def gen(s: State): Option[Gen[ReverseStreamFrom]] = val keyGen = s.current.keys.toList match case Nil => genByteVector case h :: t => Gen.frequency1( 80 -> Gen.element(h, t), 20 -> genByteVector, ) Some: for bytes <- keyGen suffixSize <- Gen.frequency1( 80 -> Gen.int(Range.linear(0, bytes.size.toInt)), 20 -> Gen.constant(0), ) yield val (prefix, suffix) = bytes.splitAt(bytes.size - suffixSize) val suffixOption: Option[Nibbles] = Option.when(suffixSize > 0)(suffix.toNibbles) ReverseStreamFrom(prefix.toNibbles, suffixOption) override def execute( env: Environment, i: ReverseStreamFrom, ): Either[String, S] = val program = MerkleTrie.reverseStreamFrom[Id](i.keyPrefix, i.keySuffix) program.runA(merkleTrieState).value override def update(s: State, i: ReverseStreamFrom, o: Var[S]): State = s override def ensure( env: Environment, s0: State, s: State, i: ReverseStreamFrom, o: S, ): Result = val toId = new FunctionK[EitherT[Id, String, *], Id]: override def apply[A](fa: EitherT[Id, String, A]): Id[A] = fa.value.toOption.get val withPrefix = s.current.filter(_._1.startsWith(i.keyPrefix.bytes)) val expected = i.keySuffix .fold(withPrefix): suffix => withPrefix.filter(_._1 <= i.keyPrefix.bytes ++ suffix.bytes) .takeRight(10) .toList .reverse .map: (k, v) => (k.bits, v) val result = o .take(10) .translate[EitherT[Id, String, *], Id](toId) .compile .toList // if expected != result then // println(s"===> ReverseStreamFrom: (${i.keyPrefix.bytes}, ${i.keySuffix.map(_.bytes)})") // println(s"===> result: ${result}") // println(s"===> expected: $expected") expected ==== result test("put same key value twice expect not to change state") { withMunitAssertions { assertions => val initialState = MerkleTrieState.empty val program = MerkleTrie.put[Id](ByteVector.empty.toNibbles, ByteVector.empty) val resultEitherT = for state1 <- program.runS(initialState) state2 <- program.runS(state1) yield assertions.assertEquals(state1, state2) resultEitherT.value } } test("put 10 -> put empty with empty -> put 10") { withMunitAssertions { assertions => val initialState = MerkleTrieState.empty val put10 = MerkleTrie.put[Id](hex"10".toNibbles, ByteVector.empty) val putEmptyWithEmpty = MerkleTrie.put[Id](ByteVector.empty.toNibbles, ByteVector.empty) // val forPrint = for // state1 <- put10.runS(initialState) // state2 <- putEmptyWithEmpty.runS(state1) // state3 <- put10.runS(state2) // yield // Seq(state1, state2, state3).zipWithIndex.foreach{ (s, i) => // println(s"====== State #${i + 1} ======") // println(s"root: ${s.root}") // s.diff.foreach{ (hash, node) => println(s" $hash: $node") } // } val initialProgram = for _ <- put10 _ <- putEmptyWithEmpty yield () val resultEitherT = for state1 <- initialProgram.runS(initialState) state2 <- put10.runS(state1) yield assertions.assertEquals(state1, state2) resultEitherT.value } } test( "put (10, empty) -> put (empty, empty) -> put (10, 00) -> put (10, empty)", ) { withMunitAssertions { assertions => val initialState = MerkleTrieState.empty val put10withEmpty = MerkleTrie.put[Id](hex"10".toNibbles, ByteVector.empty) val putEmptyWithEmpty = MerkleTrie.put[Id](ByteVector.empty.toNibbles, ByteVector.empty) val put10with10 = MerkleTrie.put[Id](hex"10".toNibbles, hex"10") // val forPrint = for // state1 <- put10withEmpty.runS(initialState) // _ <- EitherT.pure[Id, String](println(s"===> state1: ${state1}")) // state2 <- putEmptyWithEmpty.runS(state1) // _ <- EitherT.pure[Id, String](println(s"===> state2: ${state2}")) // state3 <- put10with10.runS(state2) // _ <- EitherT.pure[Id, String](println(s"===> state3: ${state3}")) // state4 <- put10withEmpty.runS(state3) // _ <- EitherT.pure[Id, String](println(s"===> state4: ${state4}")) // yield // Seq(state1, state2, state3, state4).zipWithIndex.foreach{ (s, i) => // println(s"====== State #${i + 1} ======") // println(s"root: ${s.root}") // s.diff.foreach{ (hash, node) => println(s" $hash: $node") } // } val program = for _ <- put10withEmpty _ <- putEmptyWithEmpty _ <- put10with10 _ <- put10withEmpty yield () program.runS(initialState).value match case Right(state) => assertions.assert(state.diff.get(state.root.get).nonEmpty) case Left(error) => assertions.fail(error) } } test("put (empty, empty) -> put (00, 00) -> get (empty)") { withMunitAssertions { assertions => val initialState = MerkleTrieState.empty val putEmptyWithEmpty = MerkleTrie.put[Id](ByteVector.empty.toNibbles, ByteVector.empty) val put00_00 = MerkleTrie.put[Id](hex"00".toNibbles, hex"00") val getEmpty = MerkleTrie.get[Id](ByteVector.empty.toNibbles) val program = for _ <- putEmptyWithEmpty _ <- put00_00 value <- getEmpty yield assertions.assertEquals(value, Some(ByteVector.empty)) // for // state1 <- putEmptyWithEmpty.runS(initialState) // state2 <- put00_00.runS(state1) // state3 <- getEmpty.runS(state2) // yield // Seq(state1, state2, state3).zipWithIndex.foreach: (s, i) => // println(s"====== State #${i + 1} ======") // println(s"root: ${s.root}") // s.diff.foreach{ (hash, node) => println(s" $hash: $node") } program.runA(initialState).value } } test("put 00 -> put 0000 -> put empty -> get empty") { withMunitAssertions { assertions => val initialState = MerkleTrieState.empty val put00 = MerkleTrie.put[Id](hex"00".toNibbles, ByteVector.empty) val put0000 = MerkleTrie.put[Id](hex"0000".toNibbles, ByteVector.empty) val putEmpty = MerkleTrie.put[Id](ByteVector.empty.toNibbles, ByteVector.empty) val getEmpty = MerkleTrie.get[Id](ByteVector.empty.toNibbles) val program = for _ <- put00 _ <- put0000 _ <- putEmpty value <- getEmpty yield assertions.assertEquals(value, Some(ByteVector.empty)) // val forPrint = for // state1 <- put00.runS(initialState) // state2 <- put0000.runS(state1) // state3 <- putEmpty.runS(state2) // yield // Seq(state1, state2, state3).zipWithIndex.foreach{ (s, i) => // println(s"====== State #${i + 1} ======") // println(s"root: ${s.root}") // s.diff.foreach{ (hash, node) => println(s" $hash: $node") } // } program.runA(initialState).value } } test("put 0700 -> put 07 -> put 10 -> get empty") { withMunitAssertions { assertions => val initialState = MerkleTrieState.empty val put0700 = MerkleTrie.put[Id](hex"0700".toNibbles, ByteVector.empty) val put07 = MerkleTrie.put[Id](hex"07".toNibbles, ByteVector.empty) val put10 = MerkleTrie.put[Id](hex"10".toNibbles, ByteVector.empty) val getEmpty = MerkleTrie.get[Id](ByteVector.empty.toNibbles) val program = for _ <- put0700 _ <- put07 _ <- put10 value <- getEmpty yield assertions.assertEquals(value, None) // val forPrint = for // state1 <- put0700.runS(initialState) // state2 <- put07.runS(state1) // state3 <- put10.runS(state2) // yield // Seq(state1, state2, state3).zipWithIndex.foreach{ (s, i) => // println(s"====== State #${i + 1} ======") // println(s"root: ${s.root}") // s.diff.foreach{ (hash, node) => println(s" $hash: $node") } // } program.runA(initialState).value } } test("put 00 -> put 01 -> get 00") { withMunitAssertions { assertions => val initialState = MerkleTrieState.empty val put00 = MerkleTrie.put[Id](hex"00".toNibbles, ByteVector.empty) val put01 = MerkleTrie.put[Id](hex"01".toNibbles, ByteVector.empty) val get00 = MerkleTrie.get[Id](hex"00".toNibbles) val program = for _ <- put00 _ <- put01 value <- get00 yield assertions.assertEquals(value, Some(ByteVector.empty)) // val forPrint = for // state1 <- put00.runS(initialState) // state2 <- put01.runS(state1) // yield // Seq(state1, state2).zipWithIndex.foreach{ (s, i) => // println(s"====== State #${i + 1} ======") // println(s"root: ${s.root}") // s.diff.foreach{ (hash, node) => println(s" $hash: $node") } // } program.runA(initialState).value } } test("put(00, empty) -> put(01, empty) -> put(00, 00) -> get 01") { withMunitAssertions { assertions => val initialState = MerkleTrieState.empty val put00 = MerkleTrie.put[Id](hex"00".toNibbles, ByteVector.empty) val put01 = MerkleTrie.put[Id](hex"01".toNibbles, ByteVector.empty) val put00_00 = MerkleTrie.put[Id](hex"00".toNibbles, hex"00") val get01 = MerkleTrie.get[Id](hex"01".toNibbles) val program = for _ <- put00 _ <- put01 _ <- put00_00 value <- get01 yield value // val forPrint = for // state1 <- put00.runS(initialState) // state2 <- put01.runS(state1) // state3 <- put00_00.runS(state2) // yield // Seq(state1, state2, state3).zipWithIndex.foreach{ (s, i) => // println(s"====== State #${i + 1} ======") // println(s"root: ${s.root}") // s.diff.foreach{ (hash, node) => println(s" $hash: $node") } // } val result = program.runA(initialState).value assertions.assertEquals(result, Right(Some(ByteVector.empty))) } } test("put 50 -> put 5000 -> remove 00") { withMunitAssertions { assertions => val initialState = MerkleTrieState.empty def put(key: ByteVector) = MerkleTrie.put[Id](key.toNibbles, ByteVector.empty) def remove(key: ByteVector) = MerkleTrie.remove[Id](key.toNibbles) val program = for _ <- put(hex"50") _ <- put(hex"5000") result <- remove(hex"00") yield result // val forPrint = for // state1 <- put(hex"50").runS(initialState) // state2 <- put(hex"5000").runS(state1) // result <- remove(hex"00").run(state2) // yield // Seq(state1, state2, result._1).zipWithIndex.foreach{ (s, i) => // println(s"====== State #${i + 1} ======") // println(s"root: ${s.root}") // s.diff.foreach{ (hash, node) => println(s" $hash: $node") } // } // println(s"result: ${result._2}") val result = program.runA(initialState).value assertions.assertEquals(result, Right(false)) } } test("put d0 -> put d000 -> put empty -> put 000000 -> remove d000") { withMunitAssertions { assertions => val initialState = MerkleTrieState.empty def put(key: ByteVector) = MerkleTrie.put[Id](key.toNibbles, ByteVector.empty) def remove(key: ByteVector) = MerkleTrie.remove[Id](key.toNibbles) val program = for _ <- put(hex"d0") _ <- put(hex"d000") _ <- put(hex"") _ <- put(hex"000000") _ <- remove(hex"d000") yield () // val forPrint = for // state1 <- put(hex"d0").runS(initialState) // state2 <- put(hex"d000").runS(state1) // state3 <- put(hex"").runS(state2) // state4 <- put(hex"000000").runS(state3) // state5 <- remove(hex"d000").runS(state4) // yield // Seq(state1, state2, state3, state4).zipWithIndex.foreach{ (s, i) => // println(s"====== State #${i + 1} ======") // println(s"root: ${s.root}") // s.diff.foreach{ (hash, node) => println(s" $hash: $node") } // } val result = program.runA(initialState).value assertions.assertEquals(result, Right(())) } } test("put 80 -> streamFrom 00"): withMunitAssertions: assertions => def put(key: ByteVector) = MerkleTrie.put[SyncIO](key.toNibbles, ByteVector.empty) def streamFrom(key: ByteVector) = MerkleTrie.streamFrom[SyncIO](key.toNibbles) val program = for _ <- put(hex"80") value <- streamFrom(hex"00") yield value val resultIO = program .runA(initialState) .flatMap: stream => stream.compile.toList .value val result = resultIO.unsafeRunSync() val expected: List[(Nibbles, ByteVector)] = List((hex"80".toNibbles, ByteVector.empty)) assertions.assertEquals(result, expected.asRight[String]) test("put 80 -> put empty -> streamFrom 00"): withMunitAssertions: assertions => def put(key: ByteVector) = MerkleTrie.put[SyncIO](key.toNibbles, ByteVector.empty) def streamFrom(key: ByteVector) = MerkleTrie.streamFrom[SyncIO](key.toNibbles) val program = for _ <- put(hex"80") _ <- put(ByteVector.empty) value <- streamFrom(hex"01") yield value // val forPrint = for // (state1, _) <- put(hex"80").run(initialState) // (state2, _) <- put(ByteVector.empty).run(state1) // (state3, value) <- streamFrom(hex"01").run(state2) // resultList <- value.compile.toList // yield // Seq(state1, state2, state3).zipWithIndex.foreach: (s, i) => // println(s"====== State #${i + 1} ======") // println(s"root: ${s.root}") // s.diff.foreach { (hash, node) => println(s" $hash: $node") } // println(s"========") // println(s"result: ${resultList}") // // value // // val result = forPrint // .flatMap(_.compile.toList) // .value // .unsafeRunSync() val resultIO = program .runA(initialState) .flatMap: stream => stream.compile.toList .value val result = resultIO.unsafeRunSync() val expected: List[(Nibbles, ByteVector)] = List((hex"80".toNibbles, ByteVector.empty)) assertions.assertEquals(result, expected.asRight[String]) test("put empty -> reverseStreamFrom (00, None)"): withMunitAssertions: assertions => def put(key: ByteVector) = MerkleTrie.put[SyncIO](key.toNibbles, ByteVector.empty) def reverseStreamFrom(keyPrefix: ByteVector, keySuffix: Option[Nibbles]) = MerkleTrie.reverseStreamFrom[SyncIO](keyPrefix.toNibbles, keySuffix) val program = for _ <- put(ByteVector.empty) value <- reverseStreamFrom(hex"00", None) yield value // val forPrint = for // (state1, _) <- put(ByteVector.empty).run(initialState) // (state2, value) <- reverseStreamFrom(hex"00", None).run(state1) // resultList <- value.compile.toList // yield // Seq(state1, state2).zipWithIndex.foreach: (s, i) => // println(s"====== State #${i + 1} ======") // println(s"root: ${s.root}") // s.diff.foreach { (hash, node) => println(s" $hash: $node") } // println(s"========") // println(s"result: ${resultList}") // // value // // forPrint // .flatMap(_.compile.toList) // .value // .unsafeRunSync() val resultIO = program .runA(initialState) .flatMap: stream => stream.compile.toList .value resultIO.unsafeRunSync() val result = resultIO.unsafeRunSync() val expected: List[(Nibbles, ByteVector)] = List.empty assertions.assertEquals(result, expected.asRight[String]) test("put 00 -> reverseStreamFrom (empty, None)"): withMunitAssertions: assertions => def put(key: ByteVector) = MerkleTrie.put[SyncIO](key.toNibbles, ByteVector.empty) def reverseStreamFrom(keyPrefix: ByteVector, keySuffix: Option[Nibbles]) = MerkleTrie.reverseStreamFrom[SyncIO](keyPrefix.toNibbles, keySuffix) val program = for _ <- put(hex"00") value <- reverseStreamFrom(ByteVector.empty, None) yield value // val forPrint = for // (state1, _) <- put(hex"00").run(initialState) // (state2, value) <- reverseStreamFrom(ByteVector.empty, None).run(state1) // resultList <- value.compile.toList // yield // Seq(state1, state2).zipWithIndex.foreach: (s, i) => // println(s"====== State #${i + 1} ======") // println(s"root: ${s.root}") // s.diff.foreach { (hash, node) => println(s" $hash: $node") } // println(s"========") // println(s"result: ${resultList}") // // value // // forPrint // .flatMap(_.compile.toList) // .value // .unsafeRunSync() val resultIO = program .runA(initialState) .flatMap: stream => stream.compile.toList .value resultIO.unsafeRunSync() val result = resultIO.unsafeRunSync() val expected: List[(Nibbles, ByteVector)] = List((hex"00".toNibbles, ByteVector.empty)) assertions.assertEquals(result, expected.asRight[String]) test("put empty -> put 00 -> reverseStreamFrom (00, None)"): withMunitAssertions: assertions => def put(key: ByteVector) = MerkleTrie.put[SyncIO](key.toNibbles, ByteVector.empty) def reverseStreamFrom(keyPrefix: ByteVector, keySuffix: Option[Nibbles]) = MerkleTrie.reverseStreamFrom[SyncIO](keyPrefix.toNibbles, keySuffix) val program = for _ <- put(ByteVector.empty) _ <- put(hex"00") value <- reverseStreamFrom(hex"00", None) yield value // val forPrint = for // (state1, _) <- put(ByteVector.empty).run(initialState) // (state2, _) <- put(hex"00").run(state1) // (state3, value) <- reverseStreamFrom(hex"00", None).run(state2) // resultList <- value.compile.toList // yield // Seq(state1, state2, state3).zipWithIndex.foreach: (s, i) => // println(s"====== State #${i + 1} ======") // println(s"root: ${s.root}") // s.diff.foreach { (hash, node) => println(s" $hash: $node") } // println(s"========") // println(s"result: ${resultList}") // value // // forPrint // .flatMap(_.compile.toList) // .value // .unsafeRunSync() val resultIO = program .runA(initialState) .flatMap: stream => stream.compile.toList .value resultIO.unsafeRunSync() val result = resultIO.unsafeRunSync() val expected: List[(Nibbles, ByteVector)] = List( (hex"00".toNibbles, ByteVector.empty), ) assertions.assertEquals(result, expected.asRight[String]) test("put 00 -> put 0000 -> reverseStreamFrom (empty, None)"): withMunitAssertions: assertions => def put(key: ByteVector) = MerkleTrie.put[SyncIO](key.toNibbles, ByteVector.empty) def reverseStreamFrom(keyPrefix: ByteVector, keySuffix: Option[Nibbles]) = MerkleTrie.reverseStreamFrom[SyncIO](keyPrefix.toNibbles, keySuffix) val program = for _ <- put(hex"00") _ <- put(hex"0000") value <- reverseStreamFrom(ByteVector.empty, None) yield value val resultIO = program .runA(initialState) .flatMap: stream => stream.compile.toList .value resultIO.unsafeRunSync() val result = resultIO.unsafeRunSync() val expected: List[(Nibbles, ByteVector)] = List( (hex"0000".toNibbles, ByteVector.empty), (hex"00".toNibbles, ByteVector.empty), ) assertions.assertEquals(result, expected.asRight[String]) test("put 0000 -> put 10 -> reverseStreamFrom (10, None)"): withMunitAssertions: assertions => def put(key: ByteVector) = MerkleTrie.put[SyncIO](key.toNibbles, ByteVector.empty) def reverseStreamFrom(keyPrefix: ByteVector, keySuffix: Option[Nibbles]) = MerkleTrie.reverseStreamFrom[SyncIO](keyPrefix.toNibbles, keySuffix) val program = for _ <- put(hex"0000") _ <- put(hex"10") value <- reverseStreamFrom(hex"10", None) yield value val resultIO = program .runA(initialState) .flatMap: stream => stream.compile.toList .value resultIO.unsafeRunSync() val result = resultIO.unsafeRunSync() val expected: List[(Nibbles, ByteVector)] = List( (hex"10".toNibbles, ByteVector.empty), ) assertions.assertEquals(result, expected.asRight[String]) property("test merkle trie"): sequential( range = Range.linear(1, 100), initial = State.empty, commands = List( commandGet, commandPut, commandRemove, commandStreamFrom, commandReverseStreamFrom, ), cleanup = () => merkleTrieState = MerkleTrieState.empty, ) ================================================ FILE: modules/lib/shared/src/test/scala/io/leisuremeta/chain/lib/merkle/NibblesTest.scala ================================================ package io.leisuremeta.chain.lib package merkle import scodec.bits.ByteVector import codec.byte.{ByteDecoder, ByteEncoder, DecodeResult} import codec.byte.ByteEncoder.ops.* import failure.DecodingFailure import hedgehog.* import hedgehog.munit.HedgehogSuite class NibblesTest extends HedgehogSuite: property("roundtrip of nibbles byte codec"): val nibblesGen = for bytes <- Gen.bytes(Range.linear(0, 64)) byteVector = ByteVector.view(bytes) bits <- Gen.element1( byteVector.bits, byteVector.bits.drop(4), ) yield bits.assumeNibbles nibblesGen.forAll.map: nibbles => val encoded = nibbles.toBytes val decodedEither = ByteDecoder[Nibbles].decode(encoded) decodedEither match case Right(DecodeResult(decoded, remainder)) => Result.all( List( decoded ==== nibbles, Result.assert(remainder.isEmpty), ), ) case Left(DecodingFailure(msg)) => println(s"Encoded: ${encoded.toHex}") println(s"Decoding Failure: ${msg}") Result.failure ================================================ FILE: modules/lmscan-agent/src/main/resources/application.conf.sample ================================================ scan = "" base = "" market { key = "", token = 1, } es { key = "", lm = "", addrs = [ "" ], } remote { driver = "org.postgresql.Driver", url = "jdbc:postgresql://URL", } local { driver = "org.sqlite.JDBC", url = "jdbc:sqlite:sample.db", } ================================================ FILE: modules/lmscan-agent/src/main/scala/io/leisuremeta/chain/lmscan/agent/ScanAgentApp.scala ================================================ package io.leisuremeta.chain package lmscan.agent import cats.effect.* import cats.implicits.* import cats.data.* import cats.effect.kernel.instances.all.* import sttp.client3.SttpBackend import sttp.client3.httpclient.fs2.HttpClientFs2Backend import doobie.util.transactor.Transactor import com.zaxxer.hikari.HikariConfig import doobie.hikari.* import io.leisuremeta.chain.lmscan.agent.service.* import io.leisuremeta.chain.lmscan.agent.service.RequestServiceApp import io.leisuremeta.chain.lmscan.agent.apps.DataStoreApp import io.leisuremeta.chain.lmscan.agent.apps.BalanceApp import io.leisuremeta.chain.lmscan.agent.apps.NftApp import cats.effect.std.Queue import io.leisuremeta.chain.api.model.Signed.TxHash import io.leisuremeta.chain.api.model.TransactionWithResult import io.leisuremeta.chain.api.model.Transaction.TokenTx import io.leisuremeta.chain.api.model.Account object ScanAgentResource: def transactorBuilder[F[_]: Async](conf: DBConfig) = for conf <- Resource.pure: val config = new HikariConfig() config.setDriverClassName(conf.driver) config.setJdbcUrl(conf.url) config xa <- HikariTransactor.fromHikariConfig[F](conf) yield xa def build[F[_]: Async](conf: ScanAgentConfig): Resource[ EitherT[F, String, *], ( Transactor[F], Transactor[F], SttpBackend[F, Any], ), ] = for postgres <- transactorBuilder[F](conf.remote).mapK( EitherT.liftK[F, String], ) sqlite <- transactorBuilder[F](conf.local).mapK(EitherT.liftK[F, String]) sttp <- HttpClientFs2Backend.resource[F]().mapK(EitherT.liftK[F, String]) yield (postgres, sqlite, sttp) object LoopCheckerApp: def build[F[_]: Async]( remote: RemoteStoreApp[F], local: LocalStoreApp[F], client: RequestServiceApp[F], base: String, ): F[(DataStoreApp[F], BalanceApp[F], NftApp[F])] = val nftQ = Queue.bounded[F, (TxHash, TokenTx, Account)](1000) val balQ = Queue.bounded[F, (TxHash, TransactionWithResult)](1000) for qa <- nftQ qb <- balQ storeApp <- Async[F].pure: DataStoreApp.build(qa, qb)(remote, client, base) balApp <- Async[F].pure: BalanceApp.build(qb)(remote, local) nftApp <- Async[F].pure: NftApp.build(qa)(remote, client) yield (storeApp, balApp, nftApp) def run[F[_]: Async]( remote: RemoteStoreApp[F], local: LocalStoreApp[F], client: RequestServiceApp[F], base: String, ): F[Unit] = for (a, b, c) <- build[F](remote, local, client, base) _ <- a.run &> b.run &> c.run yield () ================================================ FILE: modules/lmscan-agent/src/main/scala/io/leisuremeta/chain/lmscan/agent/ScanAgentConfig.scala ================================================ package io.leisuremeta.chain.lmscan.agent import pureconfig.* import pureconfig.generic.derivation.default.* final case class ScanAgentConfig( base: String, remote: DBConfig, local: DBConfig, scan: String, market: MarketConfig, es: ESConfig, ) derives ConfigReader final case class MarketConfig( key: String, token: Int, ) final case class ESConfig( key: String, lm: String, addrs: List[String], ) final case class DBConfig( driver: String, url: String, user: Option[String], password: Option[String], ) object ScanAgentConfig: def load: ScanAgentConfig = ConfigSource.default.loadOrThrow[ScanAgentConfig] ================================================ FILE: modules/lmscan-agent/src/main/scala/io/leisuremeta/chain/lmscan/agent/ScanAgentMain.scala ================================================ package io.leisuremeta.chain package lmscan.agent import cats.effect.* import service.RequestService import cats.data.EitherT import io.leisuremeta.chain.lmscan.agent.service.StoreService import scribe.file.* import scribe.format.* import io.leisuremeta.chain.lmscan.agent.apps.SummaryStoreApp object ScanAgentMain extends IOApp: scribe.Logger.root .withHandler( writer = FileWriter( "logs" / ("agent-" % year % "-" % month % "-" % day % ".log"), ), formatter = formatter"[$threadName] $positionAbbreviated - $messages$newLine", minimumLevel = Some(scribe.Level.Error), ) .replace() def run(args: List[String]): IO[ExitCode] = val conf = ScanAgentConfig.load ScanAgentResource .build[IO](conf) .use: (post, sqlite, server) => val client = RequestService.build[IO](server) val remote = StoreService.buildRemote(post) val local = StoreService.buildLocal(sqlite) val summary = SummaryStoreApp.build[IO](conf.market, conf.es)( remote, client, ) EitherT.liftF: summary.run &> LoopCheckerApp.run(remote, local, client, conf.base) .value .as(ExitCode.Success) ================================================ FILE: modules/lmscan-agent/src/main/scala/io/leisuremeta/chain/lmscan/agent/apps/BalanceStoreApp.scala ================================================ package io.leisuremeta.chain.lmscan.agent package apps import cats.effect.* import cats.effect.kernel.instances.all.* import cats.implicits.* import io.leisuremeta.chain.lmscan.agent.service.* import io.leisuremeta.chain.lib.crypto.Hash import cats.effect.std.Queue import io.leisuremeta.chain.api.model.Transaction import io.leisuremeta.chain.api.model.Transaction.* import io.leisuremeta.chain.api.model.Transaction.TokenTx.* import io.leisuremeta.chain.api.model.Transaction.RewardTx.* import io.leisuremeta.chain.api.model.Signed.TxHash import io.leisuremeta.chain.api.model.TransactionWithResult import scala.concurrent.duration.DurationInt import cats.data.NonEmptyList trait BalanceApp[F[_]]: def run: F[Unit] case class Balance( address: String, free: BigDecimal, ) case class Ledger( hash: String, address: String, free: Option[String], used: Option[String], ) enum LedgerType: case InputLedger( hash: String, address: String, free: BigInt, ) case SpendLedger( hash: String, address: String, used: String, ) case LockLedger( hash: String, address: String, locked: BigInt, ) case SpendLockLedger( hash: String, used: String, ) object BalanceApp: def build[F[_]: Async]( balQ: Queue[F, (TxHash, TransactionWithResult)], )(remote: RemoteStoreApp[F], local: LocalStoreApp[F]): BalanceApp[F] = new BalanceApp[F]: def run = for _ <- init _ <- loop yield () def init: F[Unit] = for _ <- local.balRepo.createLedgerTable _ <- local.balRepo.createLockLedgerTable yield () def loop: F[Unit] = for x <- balQ.tryTakeN(Some(100)) balOps <- x.flatMap(toBalanceOp).pure[F] (a, b, c, d) = balOps.foldLeft( ( List.empty[LedgerType.InputLedger], List.empty[LedgerType.SpendLedger], List.empty[LedgerType.LockLedger], List.empty[LedgerType.SpendLockLedger], ), )((acc, v) => v match case i: LedgerType.InputLedger => (i :: acc._1, acc._2, acc._3, acc._4) case s: LedgerType.SpendLedger => (acc._1, s :: acc._2, acc._3, acc._4) case l: LedgerType.LockLedger => (acc._1, acc._2, l :: acc._3, acc._4) case r: LedgerType.SpendLockLedger => (acc._1, acc._2, acc._3, r :: acc._4), ) _ <- local.balRepo.addInputLedger(a) _ <- local.balRepo.addSpendLedger(b) _ <- local.balRepo.addLockLedger(c) _ <- local.balRepo.addSpendLockLedger(d) updates = balOps.foldLeft( Set.empty[String], )((acc, v) => v match case LedgerType.InputLedger(_, address, free) => acc + address case LedgerType.SpendLedger(_, address, _) => acc + address case _ => acc, ) freeArr <- if updates.isEmpty then Async[F].pure(Right(Nil)) else local.balRepo .getLedger( NonEmptyList.fromListUnsafe(updates.toList), ) balMap = freeArr match case Left(e) => Map.empty[String, BigDecimal] case Right(v) => v .foldLeft(Map.empty[String, BigDecimal])((acc, b) => acc.get(b._1) match case Some(v) => acc + (b._1 -> (v + BigDecimal(b._2))) case None => acc + (b._1 -> BigDecimal(b._2)), ) _ <- if balMap.isEmpty then Async[F].pure(Right(0)) else remote.balRepo.updateBalance(balMap.toList) _ <- Async[F].delay: scribe.info("balance updated: " + balMap.size) _ <- Async[F].sleep(20.seconds) r <- run yield r def toBalanceOp( hash: TxHash, tx: TransactionWithResult, ) = val signer = tx.signedTx.sig.account tx.signedTx.value match case t: MintFungibleToken => t.outputs .map((acc, nat) => LedgerType.InputLedger( hash.toUInt256Bytes.toHex, acc.toString, nat.toBigInt, ), ) .toList case t: BurnFungibleToken => val inputs = t.inputs .map(h => LedgerType.SpendLedger( h.toUInt256Bytes.toHex, signer.toString, hash.toUInt256Bytes.toHex, ), ) .toList tx.result match case Some(BurnFungibleTokenResult(v)) => LedgerType.InputLedger( hash.toUInt256Bytes.toHex, signer.toString, v.toBigInt, ) :: inputs case _ => inputs case t: TransferFungibleToken => t.outputs .map((acc, nat) => LedgerType.InputLedger( hash.toUInt256Bytes.toHex, acc.toString, nat.toBigInt, ), ) .toList ::: t.inputs .map(h => LedgerType.SpendLedger( h.toUInt256Bytes.toHex, signer.toString, hash.toUInt256Bytes.toHex, ), ) .toList case t: OfferReward => t.outputs .map((acc, nat) => LedgerType.InputLedger( hash.toUInt256Bytes.toHex, acc.toString, nat.toBigInt, ), ) .toList ::: t.inputs .map(h => LedgerType.SpendLedger( h.toUInt256Bytes.toHex, signer.toString, hash.toUInt256Bytes.toHex, ), ) .toList case t: EntrustFungibleToken => val l = LedgerType.LockLedger( hash.toUInt256Bytes.toHex, t.to.toString, t.amount.toBigInt, ) :: t.inputs .map(h => LedgerType.SpendLedger( h.toUInt256Bytes.toHex, signer.toString, hash.toUInt256Bytes.toHex, ), ) .toList tx.result match case Some(EntrustFungibleTokenResult(v)) => LedgerType.InputLedger( hash.toUInt256Bytes.toHex, signer.toString, v.toBigInt, ) :: l case _ => l case t: DisposeEntrustedFungibleToken => t.inputs .map(h => LedgerType.SpendLockLedger( h.toUInt256Bytes.toHex, hash.toUInt256Bytes.toHex, ), ) .toList ::: t.outputs .map((acc, nat) => LedgerType.InputLedger( hash.toUInt256Bytes.toHex, acc.toString, nat.toBigInt, ), ) .toList case _ => Nil ================================================ FILE: modules/lmscan-agent/src/main/scala/io/leisuremeta/chain/lmscan/agent/apps/NftStoreApp.scala ================================================ package io.leisuremeta.chain.lmscan.agent package apps import cats.effect.* import cats.implicits.* import io.leisuremeta.chain.lmscan.agent.service.* import cats.effect.std.Queue import io.leisuremeta.chain.api.model.Transaction import io.leisuremeta.chain.api.model.Transaction.* import io.leisuremeta.chain.api.model.Transaction.TokenTx.* import scala.concurrent.duration.DurationInt import io.leisuremeta.chain.api.model.Account import io.circe.Decoder import io.circe.generic.semiauto.* import cats.data.EitherT import io.leisuremeta.chain.api.model.Signed.TxHash import cats.Monad trait NftApp[F[_]]: def run: F[Unit] object NftApp: case class Nft( txHash: String, tokenId: String, action: String, fromAddr: String, toAddr: String, eventTime: Long, ) case class NftFile( tokenId: String, tokenDefId: String, collectionName: String, nftName: String, nftUri: String, creatorDescription: String, dataUrl: String, rarity: String, creator: String, eventTime: Long, ) case class NftOwner( tokenId: String, ownerAddr: String, eventTime: Long, ) case class NftMetaInfo( Creator_description: String, Collection_description: String, Rarity: String, NFT_checksum: String, Collection_name: String, Creator: String, NFT_name: String, NFT_URI: String, ) given Decoder[NftMetaInfo] = deriveDecoder[NftMetaInfo] def build[F[_]: Async: Monad](nftQ: Queue[F, (TxHash, TokenTx, Account)])( remote: RemoteStoreApp[F], client: RequestServiceApp[F], ): NftApp[F] = new NftApp[F]: def run: F[Unit] = for q <- nftQ.tryTakeN(Some(1000)) _ = scribe.info(s"nftQ size: ${q.size}") files <- q .traverse((_, tx, _) => getNftFileReq(tx).value) fs <- remote.nftRepo.putNftFileList(files.mapFilter(_.toOption)) (nfts, owners) <- Async[F].pure: q.mapFilter(parseTxToRaw).separate ns <- remote.nftRepo.putNftList(nfts) os <- remote.nftRepo.putNftOwnerList(owners) _ <- Async[F].sleep(30.seconds) r <- run yield r def getNftFileReq(tx: TokenTx) = tx match case tx: MintNFT => for info <- client.getResult[NftMetaInfo](tx.dataUrl.toString) res = NftFile( tx.tokenId.toString, tx.tokenDefinitionId.toString, info.Collection_name, info.NFT_name, info.NFT_URI, info.Creator_description, tx.dataUrl.toString, info.Rarity, info.Creator, tx.createdAt.getEpochSecond, ) yield res case tx: MintNFTWithMemo => for info <- client.getResult[NftMetaInfo](tx.dataUrl.toString) res = NftFile( tx.tokenId.toString, tx.tokenDefinitionId.toString, info.Collection_name, info.NFT_name, info.NFT_URI, info.Creator_description, tx.dataUrl.toString, info.Rarity, info.Creator, tx.createdAt.getEpochSecond, ) yield res case _ => EitherT.leftT[F, NftFile]("Not MintNFT") def parseTxToRaw(hash: TxHash, tx: TokenTx, from: Account) = tx match case tx: MintNFT => Some( Nft( hash.toUInt256Bytes.toHex, tx.tokenId.toString, "MintNft", from.toString, tx.output.toString, tx.createdAt.getEpochSecond, ), NftOwner( tx.tokenId.toString, tx.output.toString, tx.createdAt.getEpochSecond, ), ) case tx: MintNFTWithMemo => Some( Nft( hash.toUInt256Bytes.toHex, tx.tokenId.toString, "MintNft", from.toString, tx.output.toString, tx.createdAt.getEpochSecond, ), NftOwner( tx.tokenId.toString, tx.output.toString, tx.createdAt.getEpochSecond, ), ) case tx: EntrustNFT => Some( Nft( hash.toUInt256Bytes.toHex, tx.tokenId.toString, "EntrustNft", tx.input.toUInt256Bytes.toHex, tx.to.toString, tx.createdAt.getEpochSecond, ), NftOwner( tx.tokenId.toString, tx.to.toString, tx.createdAt.getEpochSecond, ), ) case tx: TransferNFT => Some( Nft( hash.toUInt256Bytes.toHex, tx.tokenId.toString, "TransferNft", tx.input.toUInt256Bytes.toHex, tx.output.toString, tx.createdAt.getEpochSecond, ), NftOwner( tx.tokenId.toString, tx.output.toString, tx.createdAt.getEpochSecond, ), ) case tx: DisposeEntrustedNFT => Some( Nft( hash.toUInt256Bytes.toHex, tx.tokenId.toString, "DisposeEntrustedNft", tx.input.toUInt256Bytes.toHex, tx.output.map(_.toString).getOrElse(""), tx.createdAt.getEpochSecond, ), NftOwner( tx.tokenId.toString, tx.output.map(_.toString).getOrElse(""), tx.createdAt.getEpochSecond, ), ) case _ => None ================================================ FILE: modules/lmscan-agent/src/main/scala/io/leisuremeta/chain/lmscan/agent/apps/NodeDataStoreApp.scala ================================================ package io.leisuremeta.chain.lmscan.agent package apps import cats.effect.* import cats.effect.kernel.instances.all.* import cats.implicits.* import cats.data.* import io.leisuremeta.chain.lmscan.agent.service.* import io.leisuremeta.chain.lmscan.agent.service.RequestServiceApp import io.leisuremeta.chain.api.model.NodeStatus import io.leisuremeta.chain.api.model.Block.BlockHash import io.circe.*, io.circe.generic.semiauto.* import io.leisuremeta.chain.lmscan.backend.entity.Block import io.leisuremeta.chain.lib.crypto.Hash import io.leisuremeta.chain.lib.datatype.UInt256 import scodec.bits.ByteVector import cats.effect.std.Queue import io.leisuremeta.chain.api.model.Transaction import io.leisuremeta.chain.api.model.Transaction.* import io.leisuremeta.chain.api.model.Transaction.AccountTx.* import io.leisuremeta.chain.api.model.Transaction.GroupTx.* import io.leisuremeta.chain.api.model.Transaction.TokenTx.* import io.leisuremeta.chain.api.model.Transaction.RewardTx.* import io.leisuremeta.chain.api.model.Transaction.AgendaTx.* import io.leisuremeta.chain.api.model.Transaction.VotingTx.* import io.leisuremeta.chain.api.model.Transaction.CreatorDaoTx.* import io.leisuremeta.chain.api.model.Block as NodeBlock import io.leisuremeta.chain.api.model.Signed.TxHash import io.leisuremeta.chain.api.model.TransactionWithResult import java.time.Instant import scala.concurrent.duration.DurationInt import io.leisuremeta.chain.api.model.Account case class Tx( hash: String, signer: String, txType: Option[String] = None, blockHash: String, eventTime: Long, tokenType: String = "LM", blockNumber: Long, subType: Option[String], ) trait DataStoreApp[F[_]]: def run: F[Unit] object DataStoreApp: given Decoder[NodeStatus] = deriveDecoder[NodeStatus] given Decoder[NodeBlock] = deriveDecoder[NodeBlock] given Decoder[TransactionWithResult] = deriveDecoder[TransactionWithResult] extension (b: Block) def toHash = Hash.Value[NodeBlock]( UInt256 .from(ByteVector.fromHex(b.hash).get) .getOrElse(UInt256.EmptyBytes), ) def getParent = Hash.Value[NodeBlock]( UInt256 .from(ByteVector.fromHex(b.parentHash).get) .getOrElse(UInt256.EmptyBytes), ) def checkHashRange( from: BlockHash, to: BlockHash, ): Either[String, (String, BlockHash)] = if from != to then Right(from.toUInt256Bytes.toHex, to) else Left("Store block is latest") def build[F[_]: Async]( nftQ: Queue[F, (TxHash, TokenTx, Account)], balQ: Queue[F, (TxHash, TransactionWithResult)], )( db: RemoteStoreApp[F], client: RequestServiceApp[F], base: String, ): DataStoreApp[F] = new DataStoreApp[F]: def run: F[Unit] = toGen &> toBest def toGen: F[Unit] = for lowest <- db.blcRepo.getLowestBlock x = lowest .leftMap(_.getMessage) .flatMap: case Some(b) => Right(b) case None => Left("block not found") sEither <- client.getResult[NodeStatus](s"$base/status").value _ <- (x, sEither) match case (Right(blc), Right(status)) => if blc.number != 0 then storeBlockLoop(blc.getParent, status.genesisHash) else Async[F].unit case (Left(s), Right(status)) => storeBlockLoop(status.bestHash, status.genesisHash) case _ => Async[F].unit yield () def toBest: F[Unit] = for latest <- db.blcRepo.getLatestBlock x = latest .leftMap(_.getMessage) .flatMap: case Some(b) => Right(b) case None => Left("block not found") sEither <- client.getResult[NodeStatus](s"$base/status").value _ <- (x, sEither) match case (Right(blc), Right(status)) => scribe.info( s"storeHashRange: ${status.bestHash.toUInt256Bytes.toHex}, ${blc.hash}", ) storeBlockLoop(status.bestHash, blc.toHash) case _ => Async[F].unit _ <- Async[F].sleep(10.seconds) r <- toBest yield r def storeBlockLoop(from: BlockHash, to: BlockHash): F[Unit] = for next <- Async[F].delay: checkHashRange(from, to) res <- next match case Right((f, _)) => for blc <- client.getResult[NodeBlock](s"$base/block/$f").value r <- blc match case Right(b) => for _ <- db.blcRepo.putBlock( f, b.header.number.toBigInt.toLong, b.header.parentHash.toUInt256Bytes.toHex, b.transactionHashes.size, b.header.timestamp.getEpochSecond, ) _ <- storeTxLoop(f, b, b.transactionHashes.toList) r <- storeBlockLoop(b.header.parentHash, to) yield r case Left(_) => Async[F].delay: scribe.error(s"block not found: $f") yield r case Left(_) => Async[F].unit yield res def storeTxLoop( bHash: String, blc: NodeBlock, txs: List[TxHash], ): F[Unit] = txs match case Nil => Async[F].unit case x :: xs => for resE <- client .getResultWithJsonString[TransactionWithResult]( s"$base/tx/${x.toUInt256Bytes.toHex}", ) .value res <- resE match case Right((tx, json)) => val hash = x.toUInt256Bytes.toHex val (txEntity, accounts) = parseTxr(hash, bHash, tx, blc) for _ <- db.txRepo.putTx(txEntity) _ <- db.txRepo.putTxState( hash, blc.header.parentHash.toUInt256Bytes.toHex, tx, json, ) _ <- db.accRepo.putAccountMapper(hash, accounts) _ <- addNftQueue(x, tx) _ <- addBalanceQueue(x, tx) r <- storeTxLoop(bHash, blc, xs) yield r case Left(_) => Async[F].delay: scribe.error(s"tx not found: ${x.toUInt256Bytes.toHex}") yield res def parseTxr( hash: String, bHash: String, txr: TransactionWithResult, blc: NodeBlock, ) = val t = Tx( hash, txr.signedTx.sig.account.toString, None, bHash, txr.signedTx.value.createdAt.getEpochSecond, "LM", blc.header.number.toBigInt.toLong, None, ) txr.signedTx.value match case tx: AccountTx => parseAccountTx(t, tx) case tx: GroupTx => parseGroupTx(t, tx, txr.signedTx.sig.account) case tT: TokenTx => parseTokenTx(t, tT, txr.signedTx.sig.account) case tx: RewardTx => parseRewardTx(t, tx, txr.signedTx.sig.account) case tx: AgendaTx => parseAgendaTx(t, tx, txr.signedTx.sig.account) case tx: VotingTx => parseVotingTx(t, tx, txr.signedTx.sig.account) case tx: CreatorDaoTx => parseCreatorDaoTx(t, tx, txr.signedTx.sig.account) def parseAccountTx(c: Tx, a: AccountTx) = val t = c.copy(txType = Some("Account")) a match case tx: CreateAccount => ( t.copy( subType = Some("CreateAccount"), ), Set(tx.account), ) case tx: CreateAccountWithExternalChainAddresses => ( t.copy( subType = Some("CreateAccountWithExternalChainAddresses"), ), Set.empty, ) case tx: UpdateAccount => ( t.copy( subType = Some("UpdateAccount"), ), Set.empty, ) case tx: UpdateAccountWithExternalChainAddresses => ( t.copy( subType = Some("UpdateAccountWithExternalChainAddresses"), ), Set.empty, ) case tx: AddPublicKeySummaries => ( t.copy( subType = Some("AddPublicKeySummaries"), ), Set.empty, ) def parseGroupTx(c: Tx, g: GroupTx, signer: Account) = val t = c.copy(txType = Some("Group")) g match case tx: CreateGroup => ( t.copy( subType = Some("CreateGroup"), ), Set(tx.coordinator), ) case tx: AddAccounts => ( t.copy( subType = Some("AddAccounts"), ), tx.accounts + signer, ) def parseTokenTx(c: Tx, tt: TokenTx, signer: Account) = val t = c.copy(txType = Some("Token")) tt match case tx: DefineToken => ( t.copy( tokenType = tx.definitionId.toString, subType = Some("DefineToken"), ), Set(signer), ) case tx: DefineTokenWithPrecision => ( t.copy( tokenType = tx.definitionId.toString, subType = Some("DefineToken"), ), Set(signer), ) case tx: MintFungibleToken => ( t.copy( subType = Some("MintFungibleToken"), ), tx.outputs.keySet + signer, ) case _: MintNFT => ( t.copy( subType = Some("MintNFT"), ), Set(signer), ) case _: MintNFTWithMemo => ( t.copy( subType = Some("MintNFT"), ), Set(signer), ) case _: BurnFungibleToken => ( t.copy( subType = Some("BurnFungibleToken"), ), Set(signer), ) case _: BurnNFT => ( t.copy( subType = Some("BurnNFT"), ), Set(signer), ) case _: UpdateNFT => ( t.copy( subType = Some("UpdateNFT"), ), Set(signer), ) case tx: TransferFungibleToken => ( t.copy( subType = Some("TransferFungibleToken"), ), tx.outputs.keySet + signer, ) case tx: TransferNFT => ( t.copy( subType = Some("TransferNFT"), ), Set(signer, tx.output), ) case tx: EntrustFungibleToken => ( t.copy( subType = Some("EntrustFungibleToken"), ), Set(signer, tx.to), ) case tx: EntrustNFT => ( t.copy( subType = Some("EntrustNFT"), ), Set(signer, tx.to), ) case tx: DisposeEntrustedFungibleToken => ( t.copy( subType = Some("DisposeEntrustedFungibleToken"), ), tx.outputs.keySet + signer, ) case tx: DisposeEntrustedNFT => ( t.copy( subType = Some("DisposeEntrustedNFT"), ), Set(signer, tx.output.getOrElse(signer)), ) case _: CreateSnapshots => ( t.copy( subType = Some("CreateSnapshots"), ), Set(signer), ) def parseRewardTx(c: Tx, r: RewardTx, signer: Account) = val t = c.copy(txType = Some("Reward")) r match case tx: RegisterDao => ( t.copy( subType = Some("RegisterDao"), ), tx.moderators + tx.daoAccountName + signer, ) case tx: UpdateDao => ( t.copy( subType = Some("UpdateDao"), ), tx.moderators + signer, ) case tx: RecordActivity => ( t.copy( subType = Some("RecordActivity"), ), tx.userActivity.keySet + signer, ) case tx: OfferReward => ( t.copy( subType = Some("OfferReward"), ), tx.outputs.keySet + signer, ) case tx: BuildSnapshot => ( t.copy( subType = Some("BuildSnapshot"), ), Set(signer), ) case tx: ExecuteReward => ( t.copy( subType = Some("ExecuteReward"), ), Set(signer, tx.daoAccount.getOrElse(signer)), ) case tx: ExecuteOwnershipReward => ( t.copy( subType = Some("ExecuteOwnershipReward"), ), Set(signer), ) def parseAgendaTx(c: Tx, a: AgendaTx, signer: Account) = val t = c.copy(txType = Some("Agenda")) a match case _: SuggestSimpleAgenda => ( t.copy( subType = Some("SuggestSimpleAgenda"), ), Set(signer), ) case _: VoteSimpleAgenda => ( t.copy( subType = Some("VoteSimpleAgenda"), ), Set(signer), ) def parseVotingTx(c: Tx, v: VotingTx, signer: Account) = val t = c.copy(txType = Some("Voting")) v match case tx: CreateVoteProposal => ( t.copy( subType = Some("CreateVoteProposal"), ), Set(signer), ) case tx: CastVote => ( t.copy( subType = Some("CastVote"), ), Set(signer), ) case tx: TallyVotes => ( t.copy( subType = Some("TallyVotes"), ), Set(signer), ) def parseCreatorDaoTx(c: Tx, cd: CreatorDaoTx, signer: Account) = val t = c.copy(txType = Some("CreatorDao")) cd match case tx: CreateCreatorDao => ( t.copy( subType = Some("CreateCreatorDao"), ), Set(signer), ) case tx: UpdateCreatorDao => ( t.copy( subType = Some("UpdateCreatorDao"), ), Set(signer), ) case tx: DisbandCreatorDao => ( t.copy( subType = Some("DisbandCreatorDao"), ), Set(signer), ) case tx: ReplaceCoordinator => ( t.copy( subType = Some("ReplaceCoordinator"), ), Set(signer, tx.newCoordinator), ) case tx: AddMembers => ( t.copy( subType = Some("AddMembers"), ), tx.members + signer, ) case tx: RemoveMembers => ( t.copy( subType = Some("RemoveMembers"), ), tx.members + signer, ) case tx: PromoteModerators => ( t.copy( subType = Some("PromoteModerators"), ), tx.members + signer, ) case tx: DemoteModerators => ( t.copy( subType = Some("DemoteModerators"), ), tx.members + signer, ) def addNftQueue(hash: TxHash, tx: TransactionWithResult): F[Unit] = tx.signedTx.value match case v: MintNFT => nftQ.offer((hash, v, tx.signedTx.sig.account)) case v: MintNFTWithMemo => nftQ.offer((hash, v, tx.signedTx.sig.account)) case v: TransferNFT => nftQ.offer((hash, v, tx.signedTx.sig.account)) case v: EntrustNFT => nftQ.offer((hash, v, tx.signedTx.sig.account)) case v: DisposeEntrustedNFT => nftQ.offer((hash, v, tx.signedTx.sig.account)) case _ => Async[F].unit def addBalanceQueue(hash: TxHash, tx: TransactionWithResult): F[Unit] = tx.signedTx.value match case _: MintFungibleToken => balQ.offer(hash -> tx) case _: BurnFungibleToken => balQ.offer(hash -> tx) case _: TransferFungibleToken => balQ.offer(hash -> tx) case _: EntrustFungibleToken => balQ.offer(hash -> tx) case _: DisposeEntrustedFungibleToken => balQ.offer(hash -> tx) case _: OfferReward => balQ.offer(hash -> tx) case _ => Async[F].unit ================================================ FILE: modules/lmscan-agent/src/main/scala/io/leisuremeta/chain/lmscan/agent/apps/SummaryStoreApp.scala ================================================ package io.leisuremeta.chain.lmscan.agent.apps import cats.effect.* import io.leisuremeta.chain.lmscan.agent.service.RemoteStoreApp import io.leisuremeta.chain.lmscan.agent.service.RequestServiceApp import io.circe.Decoder import io.circe.generic.semiauto.* import cats.implicits.* import scala.concurrent.duration.* import io.leisuremeta.chain.lmscan.agent.ESConfig import io.leisuremeta.chain.lmscan.agent.MarketConfig import cats.data.EitherT trait SummaryStoreApp[F[_]]: def run: F[Unit] object SummaryStoreApp: case class TokenBalance(status: Int, message: String, result: BigDecimal) case class LmPrice(status: MarketStatus, data: TokenMap) case class MarketStatus(error_code: Int, error_message: Option[String]) case class TokenMap(`20315`: MarketData) case class MarketData( id: Int, name: String, symbol: String, last_updated: String, quote: Currency, circulating_supply: BigDecimal, ) case class Currency(USD: USDCurrency) case class USDCurrency( price: BigDecimal, last_updated: String, market_cap: BigDecimal, ) given Decoder[LmPrice] = deriveDecoder[LmPrice] given Decoder[TokenBalance] = deriveDecoder[TokenBalance] def build[F[_]: Async](market: MarketConfig, es: ESConfig)( remote: RemoteStoreApp[F], client: RequestServiceApp[F], ): SummaryStoreApp[F] = new SummaryStoreApp[F]: def run = val loop = for _ <- EitherT.liftF: Async[F].delay: scribe.info("start summary loop") balance <- getTotalBalance lmprice <- getLmPriceAndSupply data = lmprice.data.`20315` supply = data.circulating_supply cap = data.quote.USD.market_cap price = data.quote.USD.price _ <- EitherT( remote.summary .updateSummary( balance, cap, supply, price, ), ).leftMap(_.getMessage) _ <- EitherT( remote.summary.updateValidatorInfo, ).leftMap(_.getMessage) yield () for _ <- Async[F].sleep(10.minutes) _ <- loop.value r <- run yield r def getLmPriceAndSupply = client .getResultFromKeyApi[LmPrice]( s"https://pro-api.coinmarketcap.com/v2/cryptocurrency/quotes/latest?id=${market.token}", Map("X-CMC_PRO_API_KEY" -> market.key), ) def getTotalBalance = def url(addr: String) = s"https://api.etherscan.io/v2/api?chainid=1&module=account&action=tokenbalance&contractaddress=${es.lm}&address=${addr}&tag=latest&apikey=${es.key}" es.addrs .map(addr => client.getResult[TokenBalance](url(addr))) .parSequence .map( _.map(_.result) .fold(BigDecimal(0))((acc, x) => acc + x), ) ================================================ FILE: modules/lmscan-agent/src/main/scala/io/leisuremeta/chain/lmscan/agent/service/RequestService.scala ================================================ package io.leisuremeta.chain.lmscan.agent.service import sttp.client3.* import cats.effect.kernel.Async import io.circe.Decoder import io.circe.parser.decode import cats.implicits.toFunctorOps import cats.data.EitherT trait RequestServiceApp[F[_]: Async]: def getResult[A](url: String)(using Decoder[A]): EitherT[F, String, A] def getResultWithJsonString[A](url: String)(using Decoder[A], ): EitherT[F, String, (A, String)] def getResultFromKeyApi[A](url: String, hs: Map[String, String])(using Decoder[A], ): EitherT[F, String, A] object RequestService: def parseRes( res: Response[Either[String, String]], ): Either[String, String] = res.body def parseJson[A](json: String)(using Decoder[A]): Either[String, A] = decode(json) match case Left(v) => Left(v.toString) case Right(v) => Right(v) def build[F[_]: Async](backend: SttpBackend[F, Any]) = new RequestServiceApp[F]: def getResult[A](url: String)(using Decoder[A]) = getResultWithJsonString(url).fmap: v => v._1 def getResultWithJsonString[A](url: String)(using Decoder[A]) = EitherT.apply: basicRequest .get(uri"$url") .send(backend) .map: res => for body <- parseRes(res) json <- parseJson[A](body) yield (json, body) def getResultFromKeyApi[A](url: String, hs: Map[String, String])(using Decoder[A], ) = EitherT.apply: basicRequest .get(uri"$url") .headers(hs) .send(backend) .map: res => for body <- parseRes(res) json <- parseJson[A](body) yield json ================================================ FILE: modules/lmscan-agent/src/main/scala/io/leisuremeta/chain/lmscan/agent/service/StoreService.scala ================================================ package io.leisuremeta.chain.lmscan.agent package service import doobie.util.transactor.Transactor import doobie.* import cats.* import cats.effect.* trait RemoteStoreApp[F[_]: MonadCancelThrow]: val blcRepo: BlockRepository[F] val txRepo: TxRepository[F] val summary: SummaryRepository[F] val nftRepo: NftRepository[F] val balRepo: BalanceRepository[F] val accRepo: AccountRepository[F] trait LocalStoreApp[F[_]: MonadCancelThrow]: val balRepo: LocalBalanceRepository[F] object StoreService: def buildRemote[F[_]: MonadCancelThrow](xa: Transactor[F]) = new RemoteStoreApp[F]: val blcRepo: BlockRepository[F] = BlockRepository.build(xa) val txRepo: TxRepository[F] = TxRepository.build(xa) val summary: SummaryRepository[F] = SummaryRepository.build(xa) val nftRepo: NftRepository[F] = NftRepository.build(xa) val balRepo: BalanceRepository[F] = BalanceRepository.build(xa) val accRepo: AccountRepository[F] = AccountRepository.build(xa) def buildLocal[F[_]: MonadCancelThrow](xa: Transactor[F]) = new LocalStoreApp[F]: val balRepo: LocalBalanceRepository[F] = LocalBalanceRepository.build(xa) ================================================ FILE: modules/lmscan-agent/src/main/scala/io/leisuremeta/chain/lmscan/agent/service/store/AccountStore.scala ================================================ package io.leisuremeta.chain.lmscan.agent package service import doobie.util.transactor.Transactor import doobie.* import doobie.implicits.* import cats.* import cats.effect.* import io.leisuremeta.chain.api.model.Account case class AccountRepository[F[_]: MonadCancelThrow](xa: Transactor[F]): def putAccountMapper( hash: String, accounts: Set[Account], ) = Update[(String, String)](s"""insert into account_mapper (hash, address) values(?, ?) on conflict (hash, address) do nothing""") .updateMany(accounts.toList.map(a => (hash, a.toString))) .transact(xa) .attemptSql object AccountRepository: def build[F[_]: MonadCancelThrow](xa: Transactor[F]) = AccountRepository(xa) ================================================ FILE: modules/lmscan-agent/src/main/scala/io/leisuremeta/chain/lmscan/agent/service/store/BalanceStore.scala ================================================ package io.leisuremeta.chain.lmscan.agent package service import doobie.util.transactor.Transactor import doobie.* import doobie.implicits.* import cats.* import cats.effect.* import io.leisuremeta.chain.lmscan.agent.apps.* import cats.data.NonEmptyList given inputWrite: Write[LedgerType.InputLedger] = Write[(String, String, String)].contramap: v => (v.hash, v.address, v.free.toString) given ledgerWrite: Write[LedgerType.SpendLedger] = Write[(String, String, String)].contramap: v => (v.hash, v.address, v.used) given lockWrite: Write[LedgerType.LockLedger] = Write[(String, String, String)].contramap: v => (v.hash, v.address, v.locked.toString) given returnWrite: Write[LedgerType.SpendLockLedger] = Write[(String, String)].contramap: v => (v.hash, v.used) case class BalanceRepository[F[_]: MonadCancelThrow](xa: Transactor[F]): def updateBalance(txs: List[(String, BigDecimal)]) = Update[(String, BigDecimal)]( "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())", ).updateMany(txs).transact(xa).attemptSql case class LocalBalanceRepository[F[_]: MonadCancelThrow](xa: Transactor[F]): def createLedgerTable = sql"""create table if not exists ledger ( hash text, address text, free text, used text, primary key (hash, address) );""".update.run.transact(xa) def createLockLedgerTable = sql"""create table if not exists lock_ledger ( hash text, address text, locked text, used text, primary key (hash) );""".update.run.transact(xa) def addInputLedger(txs: List[LedgerType.InputLedger]) = Update[LedgerType.InputLedger]( "insert into ledger (hash, address, free) values(?, ?, ?) on conflict (hash, address) do update set free = excluded.free", ).updateMany(txs).transact(xa).attemptSql def addSpendLedger(txs: List[LedgerType.SpendLedger]) = Update[LedgerType.SpendLedger]( "insert into ledger (hash, address, used) values(?, ?, ?) on conflict (hash, address) do update set used = excluded.used", ).updateMany(txs).transact(xa).attemptSql def addLockLedger(txs: List[LedgerType.LockLedger]) = Update[LedgerType.LockLedger]( "insert into lock_ledger (hash, address, locked) values(?, ?, ?) on conflict (hash) do update set address = excluded.address, locked = excluded.locked", ).updateMany(txs).transact(xa).attemptSql def addSpendLockLedger(txs: List[LedgerType.SpendLockLedger]) = Update[LedgerType.SpendLockLedger]( "insert into lock_ledger (hash, used) values(?, ?) on conflict (hash) do update set used = excluded.used", ).updateMany(txs).transact(xa).attemptSql def getLedger( qs: NonEmptyList[String], ) = val q = fr"""select address, free from ledger where used is null and free is not null and """ ++ Fragments .in(fr"address", qs) q.query[(String, String)] .to[List] .transact(xa) .attemptSql object BalanceRepository: def build[F[_]: MonadCancelThrow](xa: Transactor[F]) = BalanceRepository(xa) object LocalBalanceRepository: def build[F[_]: MonadCancelThrow](xa: Transactor[F]) = LocalBalanceRepository(xa) ================================================ FILE: modules/lmscan-agent/src/main/scala/io/leisuremeta/chain/lmscan/agent/service/store/BlockStore.scala ================================================ package io.leisuremeta.chain.lmscan.agent package service import doobie.util.transactor.Transactor import doobie.* import doobie.implicits.* import cats.* import cats.effect.* import io.leisuremeta.chain.lmscan.backend.entity.Block case class BlockRepository[F[_]: MonadCancelThrow](xa: Transactor[F]): def getLatestBlock = sql"select number, hash, parent_hash, tx_count, event_time, created_at, proposer from block order by number desc limit 1" .query[Block] .option .transact(xa) .attemptSql def getLowestBlock = sql"select number, hash, parent_hash, tx_count, event_time, created_at, proposer from block order by number limit 1" .query[Block] .option .transact(xa) .attemptSql def putBlock(hash: String, num: Long, pHash: String, txC: Int, et: Long) = sql"""insert into block (hash, number, parent_hash, tx_count, event_time, proposer) values( $hash, $num, $pHash, $txC, $et, (select address from validator_info where ${et % 4} = id))""".update.run .transact(xa) object BlockRepository: def build[F[_]: MonadCancelThrow](xa: Transactor[F]) = BlockRepository(xa) ================================================ FILE: modules/lmscan-agent/src/main/scala/io/leisuremeta/chain/lmscan/agent/service/store/NftStore.scala ================================================ package io.leisuremeta.chain.lmscan.agent package service import doobie.util.transactor.Transactor import doobie.* import doobie.implicits.* import cats.* import cats.effect.* import io.leisuremeta.chain.lmscan.agent.apps.* import io.leisuremeta.chain.lmscan.agent.apps.NftApp.{Nft, NftFile, NftOwner} given nftWrite: Write[Nft] = Write[(String, String, String, String, String, Long)].contramap(nft => ( nft.txHash, nft.tokenId, nft.action, nft.fromAddr, nft.toAddr, nft.eventTime, ), ) given nftFileWrite: Write[NftFile] = Write[ ( String, String, String, String, String, String, String, String, String, Long, ), ].contramap(nft => ( nft.tokenId, nft.tokenDefId, nft.collectionName, nft.nftName, nft.nftUri, nft.creatorDescription, nft.dataUrl, nft.rarity, nft.creator, nft.eventTime, ), ) given nftOwnerWrite: Write[NftOwner] = Write[(String, String, Long)].contramap(nft => (nft.tokenId, nft.ownerAddr, nft.eventTime), ) case class NftRepository[F[_]: MonadCancelThrow](xa: Transactor[F]): def putNftList(nft: List[Nft]) = Update[Nft]( """insert into nft (tx_hash, token_id, action, from_addr, to_addr, event_time) values(?, ?, ?, ?, ?, ?) on conflict (tx_hash) do nothing""", ).updateMany(nft).transact(xa).attemptSql def putNftFileList(nft: List[NftFile]) = Update[NftFile]( "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", ).updateMany(nft).transact(xa).attemptSql def putNftOwnerList(nft: List[NftOwner]) = Update[NftOwner]( "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", ).updateMany(nft).transact(xa).attemptSql object NftRepository: def build[F[_]: MonadCancelThrow](xa: Transactor[F]) = NftRepository(xa) ================================================ FILE: modules/lmscan-agent/src/main/scala/io/leisuremeta/chain/lmscan/agent/service/store/SummaryStore.scala ================================================ package io.leisuremeta.chain.lmscan.agent package service import doobie.util.transactor.Transactor import doobie.* import doobie.implicits.* import cats.* import cats.effect.* case class SummaryRepository[F[_]: MonadCancelThrow](xa: Transactor[F]): def updateValidatorInfo = sql"""update validator_info set cnt = v.c from (select proposer p, count(1) c from block group by p) v where validator_info.address = v.p""".update.run .transact(xa) .attemptSql def updateSummary( balance: BigDecimal, cap: BigDecimal, supply: BigDecimal, price: BigDecimal, ) = sql""" INSERT INTO summary (lm_price, total_balance, market_cap, cir_supply, block_number, total_accounts, total_tx_size, total_nft) VALUES($price, $balance, $cap, $supply, (SELECT number FROM block ORDER BY number DESC LIMIT 1), (SELECT count(1) FROM tx WHERE sub_type = 'CreateAccount' or sub_type = 'CreateAccountWithExternalChainAddresses'), (SELECT count(1) FROM tx), (SELECT count(1) FROM nft) )""".update.run .transact(xa) .attemptSql object SummaryRepository: def build[F[_]: MonadCancelThrow](xa: Transactor[F]) = SummaryRepository(xa) ================================================ FILE: modules/lmscan-agent/src/main/scala/io/leisuremeta/chain/lmscan/agent/service/store/TxStore.scala ================================================ package io.leisuremeta.chain.lmscan.agent package service import doobie.util.transactor.Transactor import doobie.* import doobie.implicits.* import cats.* import cats.effect.* import io.leisuremeta.chain.api.model.TransactionWithResult import io.leisuremeta.chain.lmscan.agent.apps.Tx case class TxRepository[F[_]: MonadCancelThrow](xa: Transactor[F]): def putTxState( hash: String, bHash: String, tx: TransactionWithResult, json: String, ) = sql"""insert into tx_state (hash, block_hash, json, event_time) values( $hash, $bHash, $json, ${tx.signedTx.value.createdAt.getEpochSecond})""".update.run .transact(xa) .attemptSql def putTx( tx: Tx, ) = sql"""insert into tx (hash, signer, token_type, tx_type, sub_type, block_hash, block_number, event_time) values(${tx.hash}, ${tx.signer}, ${tx.tokenType}, ${tx.txType}, ${tx.subType}, ${tx.blockHash}, ${tx.blockNumber}, ${tx.eventTime})""".update.run .transact(xa) .attemptSql object TxRepository: def build[F[_]: MonadCancelThrow](xa: Transactor[F]) = TxRepository(xa) ================================================ FILE: modules/lmscan-backend/docs/flyway.md ================================================ 1. sbt flywayBaseline -> baselineVersion까지의 migration 제외한 현재 db의 baseline 지정 2. sbt flywayClean -> 해당 schema 모든 내용 삭제 3. sbt flywayMigrate -> baselineVersion 이후 migration version file update 4. sbt flywayInfo -> migration status info 출력 5. sbt flywayRepair -> flyway_schema_history table 수정 (DDL 트랜젝션 없이 db에 실패한 migration 삭제 및 잘못된 checksum 수정) 1. 처음 빈 스키마의 경우 -> sbt flywayMigrate 2. 처음 빈 스키마가 아닌 경우 (1) 스키마를 모두 비우고 해당 migration file로 다시 시작하는 경우 -> sbt flywayClean && sbt flywayMigrate (2) sbt flywayBaseline으로 flyway_schema_history 생성 후 sbt flywayMigrate 통해 특정 버전 이후부터 관리 및 적용 가능 3. 이후부터는 sbt flywayMigrate 명령어를 통해 변경된 migration이 있을경우마다 반영가능 1. https://davidmweber.github.io/flyway-sbt-docs/repair.html 2. https://flywaydb.org/documentation/command/migrate ================================================ FILE: modules/lmscan-backend/src/main/resources/application.sample.properties ================================================ ctx.dataSourceClassName=org.postgresql.ds.PGSimpleDataSource ctx.url=postgresql://... ctx.connectTimeout=30000s ctx.testTimeout=10s ctx.queryTimeout=10s ctx.getBalanceapi_url = "" ================================================ FILE: modules/lmscan-backend/src/main/resources/db/dist/V20230116164800__Alter_ts_pgdefault.sql ================================================ -- Tablespace: pg_default -- DROP TABLESPACE IF EXISTS pg_default; ALTER TABLESPACE pg_default OWNER TO rdsadmin; ================================================ FILE: modules/lmscan-backend/src/main/resources/db/dist/V20230116164800__Alter_ts_pgglobal.sql ================================================ -- Tablespace: pg_global -- DROP TABLESPACE IF EXISTS pg_global; ALTER TABLESPACE pg_global OWNER TO rdsadmin; ================================================ FILE: modules/lmscan-backend/src/main/resources/db/dist/V20230116164801__Create_r_playnomm.sql ================================================ -- Role: playnomm -- DROP ROLE IF EXISTS playnomm; CREATE ROLE playnomm WITH LOGIN NOSUPERUSER INHERIT CREATEDB CREATEROLE NOREPLICATION VALID UNTIL 'infinity'; GRANT rds_superuser TO playnomm; ================================================ FILE: modules/lmscan-backend/src/main/resources/db/common/V20230116164802__Create_t_account.sql ================================================ -- Table: public.account -- DROP TABLE IF EXISTS public.account; CREATE TABLE IF NOT EXISTS public.account ( id bigint NOT NULL, address character varying COLLATE pg_catalog."default", balance numeric(28,18), amount numeric(11,2), type character varying COLLATE pg_catalog."default", created_at bigint ) TABLESPACE pg_default; ALTER TABLE IF EXISTS public.account OWNER to playnomm; ================================================ FILE: modules/lmscan-backend/src/main/resources/db/common/V20230116164803__Create_t_block.sql ================================================ -- Table: public.block -- DROP TABLE IF EXISTS public.block; CREATE TABLE IF NOT EXISTS public.block ( id bigint NOT NULL, "number" bigint NOT NULL, hash character varying COLLATE pg_catalog."default" NOT NULL, parent_hash character varying COLLATE pg_catalog."default", tx_count bigint NOT NULL, event_time bigint NOT NULL, created_at bigint NOT NULL, CONSTRAINT block_pkey PRIMARY KEY (id) ) TABLESPACE pg_default; ALTER TABLE IF EXISTS public.block OWNER to playnomm; COMMENT ON COLUMN public.block.event_time IS '블록 발생시간'; ================================================ FILE: modules/lmscan-backend/src/main/resources/db/common/V20230116164805__Create_t_nft.sql ================================================ -- SEQUENCE: public.nft_id_seq -- DROP SEQUENCE IF EXISTS public.nft_id_seq; CREATE SEQUENCE IF NOT EXISTS public.nft_id_seq INCREMENT 1 START 1 MINVALUE 1 MAXVALUE 9223372036854775807 CACHE 1; ALTER SEQUENCE public.nft_id_seq OWNER TO playnomm; -- Table: public.nft -- DROP TABLE IF EXISTS public.nft; CREATE TABLE IF NOT EXISTS public.nft ( id bigint NOT NULL DEFAULT nextval('nft_id_seq'::regclass), token_id bigint, tx_hash character varying(64) COLLATE pg_catalog."default" NOT NULL, rarity character varying(32) COLLATE pg_catalog."default", owner character varying(40) COLLATE pg_catalog."default", action character varying(32) COLLATE pg_catalog."default", "from" character varying(40) COLLATE pg_catalog."default", "to" character varying(40) COLLATE pg_catalog."default", event_time bigint, created_at bigint, CONSTRAINT nft_pkey PRIMARY KEY (tx_hash) ) TABLESPACE pg_default; ALTER TABLE IF EXISTS public.nft OWNER to playnomm; ALTER SEQUENCE IF EXISTS public.nft_id_seq OWNED BY nft.id ================================================ FILE: modules/lmscan-backend/src/main/resources/db/common/V20230116164809__Create_t_transaction.sql ================================================ -- Table: public.transaction -- DROP TABLE IF EXISTS public.transaction; CREATE TABLE IF NOT EXISTS public.transaction ( id bigint NOT NULL GENERATED ALWAYS AS IDENTITY ( INCREMENT 1 START 1 MINVALUE 1 MAXVALUE 9223372036854775807 CACHE 1 ), hash character varying(64) COLLATE pg_catalog."default" NOT NULL, type character varying(32) COLLATE pg_catalog."default" NOT NULL, "from" character varying(64) COLLATE pg_catalog."default" NOT NULL, "to" character varying[] COLLATE pg_catalog."default" NOT NULL, value numeric(28,18) NOT NULL, block_hash character varying(64) COLLATE pg_catalog."default" NOT NULL, event_time bigint NOT NULL, created_at bigint NOT NULL, CONSTRAINT transaction_pkey PRIMARY KEY (id) ) TABLESPACE pg_default; ALTER TABLE IF EXISTS public.transaction OWNER to playnomm; ================================================ FILE: modules/lmscan-backend/src/main/resources/db/seed/R__001_Seed_account.sql ================================================ INSERT INTO account(id, balance, amount, created_at) VALUES (1, 1.1, 1.1, 20230116), (2, 2.2, 2.2, 20230116), (3, 3.3, 3.3, 20230116); -- ON CONFLICT(id) DO NOTHING; ================================================ FILE: modules/lmscan-backend/src/main/resources/db/test/V20230116164801__Create_r_playnomm.sql ================================================ -- Role: playnomm -- DROP ROLE IF EXISTS playnomm; DROP ROLE IF EXISTS playnomm; CREATE ROLE playnomm WITH LOGIN NOSUPERUSER INHERIT CREATEDB CREATEROLE NOREPLICATION VALID UNTIL 'infinity'; -- GRANT rds_superuser TO playnomm; ================================================ FILE: modules/lmscan-backend/src/main/scala/io/leisuremeta/chain/lmscan/backend/LmscanBackendMain.scala ================================================ package io.leisuremeta.chain.lmscan package backend import cats.effect.{ExitCode, IO, IOApp, Resource} import cats.effect.std.Dispatcher import com.linecorp.armeria.server.Server import sttp.capabilities.fs2.Fs2Streams import sttp.tapir.server.ServerEndpoint import sttp.tapir.server.armeria.cats.ArmeriaCatsServerInterpreter import sttp.tapir.* import common.ExploreApi import common.model.PageNavigation import io.leisuremeta.chain.lmscan.backend.service.* import cats.effect.Async import com.linecorp.armeria.server.HttpService; import sttp.tapir.server.armeria.cats.ArmeriaCatsServerOptions import sttp.tapir.server.interceptor.cors.CORSInterceptor import sttp.tapir.server.interceptor.cors.CORSConfig import sttp.tapir.server.interceptor.log.DefaultServerLog object BackendMain extends IOApp: def txPaging[F[_]: Async]: ServerEndpoint[Fs2Streams[F], F] = ExploreApi.getTxPageEndPoint.serverLogic { ( pageInfo, ) => TransactionService .getPage[F](pageInfo) .leftMap: case Right(msg) => Right(ExploreApi.BadRequest(msg)) case Left(msg) => Left(ExploreApi.ServerError(msg)) .value } def txDetail[F[_]: Async]: ServerEndpoint[Fs2Streams[F], F] = ExploreApi.getTxDetailEndPoint.serverLogic { (hash: String) => TransactionService .getDetail(hash) .leftMap: case Right(msg) => Right(ExploreApi.BadRequest(msg)) case Left(msg) => Left(ExploreApi.ServerError(msg)) .value } def blockPaging[F[_]: Async]: ServerEndpoint[Fs2Streams[F], F] = ExploreApi.getBlockPageEndPoint.serverLogic { (pageInfo: PageNavigation) => BlockService .getPage[F](pageInfo) .leftMap: case Right(msg) => Right(ExploreApi.BadRequest(msg)) case Left(msg) => Left(ExploreApi.ServerError(msg)) .value } def blockDetail[F[_]: Async]: ServerEndpoint[Fs2Streams[F], F] = ExploreApi.getBlockDetailEndPoint.serverLogic { (hash: String, p: Option[Int]) => BlockService .getDetail(hash, p.getOrElse(1)) .leftMap: case Right(msg) => Right(ExploreApi.BadRequest(msg)) case Left(msg) => Left(ExploreApi.ServerError(msg)) .value } def accountPaging[F[_]: Async]: ServerEndpoint[Fs2Streams[F], F] = ExploreApi.getAccountPageEndPoint.serverLogic { (pageInfo: PageNavigation) => AccountService .getPage[F](pageInfo) .leftMap: case Right(msg) => Right(ExploreApi.BadRequest(msg)) case Left(msg) => Left(ExploreApi.ServerError(msg)) .value } def accountDetail[F[_]: Async]: ServerEndpoint[Fs2Streams[F], F] = ExploreApi.getAccountDetailEndPoint.serverLogic { (address: String, p: Option[Int]) => AccountService .get(address, p.getOrElse(1)) .leftMap: case Right(msg) => Right(ExploreApi.BadRequest(msg)) case Left(msg) => Left(ExploreApi.ServerError(msg)) .value } def nftDetail[F[_]: Async]: ServerEndpoint[Fs2Streams[F], F] = ExploreApi.getNftDetailEndPoint.serverLogic { (tokenId: String) => NftService .getNftDetail(tokenId) .leftMap: case Right(msg) => Right(ExploreApi.BadRequest(msg)) case Left(msg) => Left(ExploreApi.ServerError(msg)) .value } def nftSeasonPaging[F[_]: Async]: ServerEndpoint[Fs2Streams[F], F] = ExploreApi.getNftSeasonEndPoint.serverLogic { (season: String, pageInfo: PageNavigation) => NftService .getSeasonPage[F](pageInfo, season) .leftMap: case Right(msg) => Right(ExploreApi.BadRequest(msg)) case Left(msg) => Left(ExploreApi.ServerError(msg)) .value } def nftPaging[F[_]: Async]: ServerEndpoint[Fs2Streams[F], F] = ExploreApi.getNftPageEndPoint.serverLogic { (pageInfo: PageNavigation) => NftService .getPage[F](pageInfo) .leftMap: case Right(msg) => Right(ExploreApi.BadRequest(msg)) case Left(msg) => Left(ExploreApi.ServerError(msg)) .value } def nftOwnerInfo[F[_]: Async]: ServerEndpoint[Fs2Streams[F], F] = ExploreApi.getNftOwnerInfoEndPoint.serverLogic { (tokenId: String) => NftService .getNftOwnerInfo(tokenId) .leftMap: case Right(msg) => Right(ExploreApi.BadRequest(msg)) case Left(msg) => Left(ExploreApi.ServerError(msg)) .value } def summaryMain[F[_]: Async]: ServerEndpoint[Fs2Streams[F], F] = ExploreApi.getSummaryMainEndPoint.serverLogic { Unit => SummaryService.getBoard .leftMap: case Right(msg) => Right(ExploreApi.BadRequest(msg)) case Left(msg) => Left(ExploreApi.ServerError(msg)) .value } def summaryChart[F[_]: Async]: ServerEndpoint[Fs2Streams[F], F] = ExploreApi.getSummaryChartEndPoint.serverLogic { (chartType: String) => val list = chartType match case "balance" => SummaryService.getList case _ => SummaryService.get5List list.leftMap: case Right(msg) => Right(ExploreApi.BadRequest(msg)) case Left(msg) => Left(ExploreApi.ServerError(msg)) .value } def keywordSearch[F[_]: Async]: ServerEndpoint[Fs2Streams[F], F] = ExploreApi.getKeywordSearchResult.serverLogic: (keyword: String) => SearchService .getKeywordSearch(keyword) .leftMap: case Right(msg) => Right(ExploreApi.BadRequest(msg)) case Left(msg) => Left(ExploreApi.ServerError(msg)) .value def valdatorPage[F[_]: Async]: ServerEndpoint[Fs2Streams[F], F] = ExploreApi.getValidators.serverLogic: _ => ValidatorService .getPage() .leftMap: case Right(msg) => Right(ExploreApi.BadRequest(msg)) case Left(msg) => Left(ExploreApi.ServerError(msg)) .value def valdatorDetail[F[_]: Async]: ServerEndpoint[Fs2Streams[F], F] = ExploreApi.getValidator.serverLogic: (address, p) => ValidatorService .get(address, p.getOrElse(1)) .leftMap: case Right(msg) => Right(ExploreApi.BadRequest(msg)) case Left(msg) => Left(ExploreApi.ServerError(msg)) .value def explorerEndpoints[F[_]: Async]: List[ServerEndpoint[Fs2Streams[F], F]] = List( txPaging[F], txDetail[F], blockPaging[F], blockDetail[F], accountPaging[F], accountDetail[F], nftPaging[F], nftSeasonPaging[F], nftDetail[F], nftOwnerInfo[F], summaryMain[F], summaryChart[F], keywordSearch[F], valdatorPage[F], valdatorDetail[F], ) def getServerResource[F[_]: Async]: Resource[F, Server] = for dispatcher <- Dispatcher.parallel[F] server <- Resource.fromAutoCloseable: def log[F[_]: Async]( level: scribe.Level, )(msg: String, exOpt: Option[Throwable])(using mdc: scribe.mdc.MDC, ): F[Unit] = Async[F].delay(exOpt match case None => scribe.log(level, mdc, msg) case Some(ex) => scribe.log(level, mdc, msg, ex), ) val serverLog = DefaultServerLog( doLogWhenReceived = log(scribe.Level.Info)(_, None), doLogWhenHandled = log(scribe.Level.Info), doLogAllDecodeFailures = log(scribe.Level.Error), doLogExceptions = (msg: String, ex: Throwable) => Async[F].delay(scribe.error(msg, ex)), noLog = Async[F].pure(()), ) Async[F].fromCompletableFuture: val options = ArmeriaCatsServerOptions .customiseInterceptors(dispatcher) .corsInterceptor(Some { CORSInterceptor .customOrThrow[F](CORSConfig.default) }) .serverLog(serverLog) .options val tapirService = ArmeriaCatsServerInterpreter[F](options) .toService(explorerEndpoints[F]) val server = Server.builder .annotatedService(tapirService) .http(8081) .maxRequestLength(128 * 1024 * 1024) .requestTimeout(java.time.Duration.ofMinutes(2)) .service(tapirService) .build Async[F].delay: scribe.info("server start / port: 8081") server.start().thenApply(_ => server) yield server override def run(args: List[String]): IO[ExitCode] = val program: Resource[IO, Server] = for server <- getServerResource[IO] yield server program.useForever.as(ExitCode.Success) ================================================ FILE: modules/lmscan-backend/src/main/scala/io/leisuremeta/chain/lmscan/backend/docs/Lmscan_API.md ================================================ # Lmscan API `GET` **/tx/list** 트랜잭션(Tx) 목록 페이지 조회 > `param` pageNo: 페이지 번호 > `param` sizePerRequest: 페이지 당 출력할 레코드 갯수 > `param` _(optional)_ accountAddr: 사용자 지갑 주소 > `param` _(optional)_ blockHash: 블록 해쉬 값 > (단, accountAddr / blockHash 모두 입력시 에러) - Response: PageResponse[TxInfo] * totalCount: 트랜잭션 레코드의 총 갯수 * totalPages: 'sizePerRequest' 파라미터 값에 따른 총 페이지 번호 * payload: 요청 페이지의 트랜잭션 목록 - hash: 트랜잭션 해쉬 값 - blockNumber: 블록 번호 - txType: 트랜잭션 형태에 따른 구분 (account / group / token / reward) - tokenType: 토큰 타입 구분 ( LM / NFT ) - createdAt: 트랜잭션 생성 시간 - Example (pageNo `0`, sizePerRequest: `3` 으로 요청한 예시) - http://localhost:8081/tx/list?pageNo=0&sizePerRequest=3 ```json { "totalCount": 21, "totalPages": 7, "payload": [ { "hash": "7913b313f68610159bca2cfcc0758a726494c442d8116200e1ec2f459642f2dc", "blockNumber": 14, "createdAt": 1673939878, "txType": "account", "tokenType": "LM", "signer": "26A463A0ED56A4A97D673A47C254728409C7B002", "value": "123456789.12345678912345678" }, { "hash": "6913b313f68610159bca2cfcc0758a726494c442d8116200e1ec2f459642f2db", "blockNumber": 12, "createdAt": 1673853478, "txType": "account", "tokenType": "LM", "signer": "26A463A0ED56A4A97D673A47C254728409C7B002", "value": "123456789.12345678912345678" }, { "hash": "5913b313f68610159bca2cfcc0758a726494c442d8116200e1ec2f459642f2da", "blockNumber": 15, "createdAt": 1673767078, "txType": "account", "tokenType": "LM", "signer": "26A463A0ED56A4A97D673A47C254728409C7B002", "value": "123456789.12345678912345678" } ] } ``` `GET` **/tx/{transactionHash}/detail** 특정 트랜잭션 상세정보 조회 > `param` transactionHash: 트랜잭션 해쉬 값 - Response: Option[TxDetail] - hash: 트랜잭션 해쉬 값 - txType: 트랜잭션 형태에 따른 구분 (account / group / token / reward) - signer: 해당 TX의 서명인(발신자) - amount: Tx에 의해 전송되는 LM - createdAt: Tx 가 Lmscan Db에 저장된 시간 - eventTime: 트랜잭션 생성 시간 - inputHashs: 인풋 트랜잭션 해쉬 목록 - transferHist: - json: 트랜잭션의 raw json - Example (transactionHash `1513b313f68610159bca2cfcc0758a726494c442d8116200e1ec2f459642f2da` 으로 요청한 예시) - http://localhost:8081/tx/1513b313f68610159bca2cfcc0758a726494c442d8116200e1ec2f459642f2da/detail ```json { "hash": "1513b313f68610159bca2cfcc0758a726494c442d8116200e1ec2f459642f2da", "createdAt": 1673767078, "signer": "26A463A0ED56A4A97D673A47C254728409C7B002", "txType": "account", "tokenType": "LM", "inputHashs": [ "4913b313f68610159bca2cfcc0758a726494c442d8116200e1ec2f459642f2da" ], "transferHist": [ { "toAddress": "b775871c85faae7eb5f6bcebfd28b1e1b412235c", "value": "123456789.12345678912345678" }, { "toAddress": "b775871c85faae7eb5f6bcebfd28b1e1b412235c", "value": "123456789.12345678912345678" } ], "json": "test" } ``` `GET` **/block/list** 블록 목록 페이지 조회 > `param` pageNo: 페이지 번호 > `param` sizePerRequest: 페이지 당 출력할 레코드 갯수 - Response: PageResponse[TxInfo] * totalCount: 트랜잭션 레코드의 총 갯수 * totalPages: 'sizePerRequest' 파라미터 값에 따른 총 페이지 번호 * payload: 요청 페이지의 트랜잭션 목록 - number: 블록 번호 - hash: 블록 해쉬 값 - txCount: 트랜잭션 갯수 - createdAt: 블록 생성 시간 - Example (pageNo `0`, sizePerRequest: `3` 으로 요청한 예시) - http://localhost:8081/block/list?pageNo=0&sizePerRequest=3 ```json { "totalCount": 2, "totalPages": 1, "payload": [ { "number": 123456789, "hash": "6913b313f68610159bca2cfcc0758a726494c442d8116200e1ec2f459642f2da", "txCount": 1234, "createdAt": 1675068555 }, { "number": 123456790, "hash": "2913b313f68610159bca2cfcc0758a726494c442d8116200e1ec2f459642f2dc", "txCount": 1234, "createdAt": 1675068555 } ] } ``` `GET` **/block/{blockHash}/detail** 특정 블록 상세정보 조회 > `param` blockHash: 블록 해쉬 값 - Response: Option[BlockDetail] - hash: 블록 해쉬 값 - parentHash: 이전 블록 해쉬 값 - number: 블록 번호 - txCount: 트랜잭션 갯수 - createdAt: 블록 생성 시간 - txs: 해당 블록의 트랜잭션 목록 () - Example (blockHash `6913b313f68610159bca2cfcc0758a726494c442d8116200e1ec2f459642f2da` 으로 요청한 예시) - http://localhost:8081/block/6913b313f68610159bca2cfcc0758a726494c442d8116200e1ec2f459642f2da/detail ```json { "hash": "6913b313f68610159bca2cfcc0758a726494c442d8116200e1ec2f459642f2da", "parentHash": "7913b313f68610159bca2cfcc0758a726494c442d8116200e1ec2f459642f2db", "number": 123456789, "timestamp": 1675068000, "txCount": 1234, "txs": [ { "hash": "7913b313f68610159bca2cfcc0758a726494c442d8116200e1ec2f459642f2dc", "blockNumber": 14, "createdAt": 1673939878, "txType": "account", "tokenType": "LM", "signer": "26A463A0ED56A4A97D673A47C254728409C7B002", "value": "123456789.12345678912345678" }, { "hash": "6913b313f68610159bca2cfcc0758a726494c442d8116200e1ec2f459642f2db", "blockNumber": 12, "createdAt": 1673853478, "txType": "account", "tokenType": "LM", "signer": "26A463A0ED56A4A97D673A47C254728409C7B002", "value": "123456789.12345678912345678" }, { "hash": "5913b313f68610159bca2cfcc0758a726494c442d8116200e1ec2f459642f2da", "blockNumber": 15, "createdAt": 1673767078, "txType": "account", "tokenType": "LM", "signer": "26A463A0ED56A4A97D673A47C254728409C7B002", "value": "123456789.12345678912345678" } ] } ``` `GET` **/account/{accountAddr}/detail** 특정 어카운트 상세정보 조회 > `param` accountAddr: 어카운트 해쉬 값 - Response: Option[AccountDetail] - address: 어카운트 주소 - balance: 보유 LM 토큰 수량 - value: 해당 토큰 수량의 달러화 환산 가치 - txHistory: 해당 어카운트의 트랜잭션 히스토리 - Example (pageNo `0`, sizePerRequest: `3` 으로 요청한 예시) - http://localhost:8081/account/26A463A0ED56A4A97D673A47C254728409C7B002/detail ```json { "address": "26A463A0ED56A4A97D673A47C254728409C7B002", "balance": 100.2222, "value": 12.32, "txHistory": [ { "hash": "7913b313f68610159bca2cfcc0758a726494c442d8116200e1ec2f459642f2dc", "blockNumber": 14, "createdAt": 1673939878, "txType": "account", "tokenType": "LM", "signer": "26A463A0ED56A4A97D673A47C254728409C7B002", "value": "123456789.12345678912345678" }, { "hash": "6913b313f68610159bca2cfcc0758a726494c442d8116200e1ec2f459642f2db", "blockNumber": 12, "createdAt": 1673853478, "txType": "account", "tokenType": "LM", "signer": "26A463A0ED56A4A97D673A47C254728409C7B002", "value": "123456789.12345678912345678" }, { "hash": "5913b313f68610159bca2cfcc0758a726494c442d8116200e1ec2f459642f2da", "blockNumber": 15, "createdAt": 1673767078, "txType": "account", "tokenType": "LM", "signer": "26A463A0ED56A4A97D673A47C254728409C7B002", "value": "123456789.12345678912345678" } ] } ``` `GET` **/nft/{tokenId}/detail** 특정 NFT 상세정보 조회 > `param` tokenId: nft 토큰 아이디 - Response: Option[NftDetail] - nftFile: Nft 파일 정보 - activities: 보유 LM 토큰 수량 - Example (pageNo `0`, sizePerRequest: `3` 으로 요청한 예시) - http://localhost:8081/nft/2022122110000930000002558/detail ```json { "nftFile": { "tokenId": "2022122110000930000002558", "tokenDefId": "test-token", "collectionName": "BPS-JinKei", "nftName": "#2558", "nftUri": "https://d2t5puzz68k49j.cloudfront.net/release/collections/BPS_JinKei/NFT_ITEM/CE298DB9-66E4-4258-9A73-A00E09899698.mp4", "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].", "dataUrl": "https://d2t5puzz68k49j.cloudfront.net/release/collections/BPS_JinKei/NFT_ITEM_META/CE298DB9-66E4-4258-9A73-A00E09899698.json", "rarity": "UNIQ", "creator": "JinKei", "eventTime": 1675069161, "createdAt": 1675069161, "owner": "b775871c85faae7eb5f6bcebfd28b1e1b412235c" }, "activities": [ { "txHash": "6913b313f68610159bca2cfcc0758a726494c442d8116200e1ec2f459642f2da", "action": "MintNFT", "fromAddr": "b775871c85faae7eb5f6bcebfd28b1e1b412235c", "toAddr": "b775871c85faae7eb5f6bcebfd28b1e1b412235c", "createdAt": 1675068858 } ] } ``` `GET` **/summary/main** 조회시점 기준 가장 최근 24시간 이내 통계 데이터 조회 - Response: Option[Summary] - Example - http://localhost:8081/summary/main ```json { "id": 1, "lmPrice": 0.394, "blockNumber": 123456789, "txCountInLatest24h": 123456789, "totalAccounts": 123456789, "createdAt": 1675612055 } ``` ================================================ FILE: modules/lmscan-backend/src/main/scala/io/leisuremeta/chain/lmscan/backend/entity/Account.scala ================================================ package io.leisuremeta.chain.lmscan.backend.entity final case class Account( address: String, createdAt: Long, eventTime: Long, ) ================================================ FILE: modules/lmscan-backend/src/main/scala/io/leisuremeta/chain/lmscan/backend/entity/AccountMapper.scala ================================================ package io.leisuremeta.chain.lmscan.backend.entity final case class AccountMapper( address: String, hash: String, eventTime: Long, ) ================================================ FILE: modules/lmscan-backend/src/main/scala/io/leisuremeta/chain/lmscan/backend/entity/Balance.scala ================================================ package io.leisuremeta.chain.lmscan.backend.entity final case class Balance( address: String, free: BigDecimal = BigDecimal(0), locked: BigDecimal = BigDecimal(0), updatedAt: Long, createdAt: Long, ) ================================================ FILE: modules/lmscan-backend/src/main/scala/io/leisuremeta/chain/lmscan/backend/entity/Block.scala ================================================ package io.leisuremeta.chain.lmscan.backend.entity import io.leisuremeta.chain.lmscan.common.model.BlockInfo final case class Block( number: Long, hash: String, parentHash: String, txCount: Long, eventTime: Long, createdAt: Long, proposer: String ): def toModel = BlockInfo( Some(number), Some(hash), Some(txCount), Some(createdAt), Some(proposer), ) ================================================ FILE: modules/lmscan-backend/src/main/scala/io/leisuremeta/chain/lmscan/backend/entity/CollectionInfo.scala ================================================ package io.leisuremeta.chain.lmscan.backend.entity import java.util.Date final case class CollectionInfo( tokenDefId: String, season: String, collectionName: String, collectionSn: Int, totalSupply: Option[Int], startDate: Option[Date], endDate: Option[Date], infoProg: Option[Int], thumbUrl: Option[String], ) ================================================ FILE: modules/lmscan-backend/src/main/scala/io/leisuremeta/chain/lmscan/backend/entity/Nft.scala ================================================ package io.leisuremeta.chain.lmscan package backend package entity final case class Nft( txHash: String, action: String, fromAddr: String, toAddr: String, eventTime: Long, createdAt: Long, tokenId: String, ) ================================================ FILE: modules/lmscan-backend/src/main/scala/io/leisuremeta/chain/lmscan/backend/entity/NftFile.scala ================================================ package io.leisuremeta.chain.lmscan.backend.entity final case class NftFile( tokenId: String, tokenDefId: String, collectionName: String, nftName: String, nftUri: String, creatorDescription: String, dataUrl: String, rarity: String, creator: String, eventTime: Long, createdAt: Long, // owner: String, ) ================================================ FILE: modules/lmscan-backend/src/main/scala/io/leisuremeta/chain/lmscan/backend/entity/NftInfo.scala ================================================ package io.leisuremeta.chain.lmscan.backend.entity import java.util.Date final case class NftInfo( season: String, seasonName: String, totalSupply: Option[Int], startDate: Option[Date], endDate: Option[Date], thumbUrl: Option[String], sort: Int, ) ================================================ FILE: modules/lmscan-backend/src/main/scala/io/leisuremeta/chain/lmscan/backend/entity/NftOwner.scala ================================================ package io.leisuremeta.chain.lmscan.backend.entity final case class NftOwner( tokenId: String = "", owner: String = "", createdAt: Long = 0, eventTime: Long = 0, ) ================================================ FILE: modules/lmscan-backend/src/main/scala/io/leisuremeta/chain/lmscan/backend/entity/NftSeason.scala ================================================ package io.leisuremeta.chain.lmscan.backend.entity import io.leisuremeta.chain.lmscan.common.model.NftSeasonModel final case class NftSeason( nftName: String, tokenId: String, tokenDefId: String, creator: String, rarity: String, dataUrl: String, collection: String, ): def toModel: NftSeasonModel = NftSeasonModel( Some(nftName), Some(tokenId), Some(tokenDefId), Some(creator), Some(rarity), Some(dataUrl), Some(collection), ) ================================================ FILE: modules/lmscan-backend/src/main/scala/io/leisuremeta/chain/lmscan/backend/entity/Summary.scala ================================================ package io.leisuremeta.chain.lmscan.backend.entity final case class Summary( lmPrice: Double, blockNumber: Long, totalAccounts: Long, createdAt: Long, totalTxSize: BigDecimal, totalBalance: BigDecimal, marketCap: Option[BigDecimal], cirSupply: Option[BigDecimal], totalNft: Option[Long], ) ================================================ FILE: modules/lmscan-backend/src/main/scala/io/leisuremeta/chain/lmscan/backend/entity/Tx.scala ================================================ package io.leisuremeta.chain.lmscan.backend.entity final case class Tx( hash: String, signer: String, txType: String, // col_name : type blockHash: String, eventTime: Long, createdAt: Long, tokenType: String, blockNumber: Long, subType: String, ) ================================================ FILE: modules/lmscan-backend/src/main/scala/io/leisuremeta/chain/lmscan/backend/entity/TxState.scala ================================================ package io.leisuremeta.chain.lmscan.backend.entity final case class TxState( hash: String, blockHash: String, json: String, eventTime: Long, createdAt: Long, ) ================================================ FILE: modules/lmscan-backend/src/main/scala/io/leisuremeta/chain/lmscan/backend/entity/Validator.scala ================================================ package io.leisuremeta.chain.lmscan.backend.entity import io.leisuremeta.chain.lmscan.common.model.NodeValidator final case class ValidatorInfo( address: String, power: Option[Double], cnt: Option[Long], name: Option[String], ): def toModel = NodeValidator.Validator( Some(address), power, cnt, name, ) ================================================ FILE: modules/lmscan-backend/src/main/scala/io/leisuremeta/chain/lmscan/backend/repository/AccountRepository.scala ================================================ package io.leisuremeta.chain.lmscan.backend.repository import cats.effect.kernel.Async import cats.data.EitherT import io.getquill.* import io.leisuremeta.chain.lmscan.backend.entity._ import io.getquill.autoQuote import io.leisuremeta.chain.lmscan.common.model._ object AccountRepository extends CommonQuery: import ctx.* def get[F[_]: Async]( addr: String, ): EitherT[F, String, Option[Balance]] = inline def detailQuery = quote { (addr: String) => query[Balance] .filter(_.address == addr) .take(1) } optionQuery(detailQuery(lift(addr))) def getPage[F[_]: Async]( pageNavInfo: PageNavigation, ): EitherT[F, String, PageResponse[Balance]] = val cntQuery = quote { query[Balance] .filter(_.address != "eth-gateway") // filter eth-gateway } def pagedQuery = quote { (pageNavInfo: PageNavigation) => val offset = sizePerRequest * pageNavInfo.pageNo val sizePerRequest = pageNavInfo.sizePerRequest query[Balance] .filter(_.address != "eth-gateway") .sortBy(a => a.free)(Ord.desc) .drop(offset) .take(sizePerRequest) } val res = for a <- countQuery(cntQuery) b <- seqQuery(pagedQuery(lift(pageNavInfo))) yield (a, b) res.map { (totalCnt, r) => val totalPages = calTotalPage(totalCnt, pageNavInfo.sizePerRequest) new PageResponse(totalCnt, totalPages, r) } ================================================ FILE: modules/lmscan-backend/src/main/scala/io/leisuremeta/chain/lmscan/backend/repository/BlockRepository.scala ================================================ package io.leisuremeta.chain.lmscan.backend.repository import io.leisuremeta.chain.lmscan.common.model.PageNavigation import io.leisuremeta.chain.lmscan.backend.entity.Block import cats.data.EitherT import cats.effect.Async import cats.implicits.* import io.getquill.* import java.sql.SQLException object BlockRepository extends CommonQuery: import ctx.* def getPage[F[_]: Async]( pageNavInfo: PageNavigation, cnt: Long, ): EitherT[F, String, Seq[Block]] = def pagedQuery = quote { (offset: Long, sizePerRequest: Int) => query[Block] .filter(t => t.number <= offset) .sortBy(t => t.number)(Ord.desc) .take(sizePerRequest) } val sizePerRequest = pageNavInfo.sizePerRequest val offset = sizePerRequest * pageNavInfo.pageNo seqQuery(pagedQuery(lift(cnt - offset), lift(sizePerRequest))) def getPageByProposer[F[_]: Async]( pageNavInfo: PageNavigation, cnt: Long, addr: String, ): EitherT[F, String, Seq[Block]] = def pagedQuery = quote { (offset: Long, sizePerRequest: Int, s: String) => query[Block] .filter(t => t.proposer == s) .filter(t => t.number <= offset) .sortBy(t => t.number)(Ord.desc) .take(sizePerRequest) } val sizePerRequest = pageNavInfo.sizePerRequest seqQuery(pagedQuery(lift(cnt), lift(sizePerRequest), lift(addr))) def getLast[F[_]: Async](): EitherT[F, String, Option[Block]] = inline def q = quote { () => query[Block] .sortBy(t => t.number)(Ord.desc) .take(1) } optionQuery(q()) def get[F[_]: Async]( hash: String, ): EitherT[F, String, Option[Block]] = inline def detailQuery = quote { (hash: String) => query[Block].filter(b => b.hash == hash).take(1) } optionQuery(detailQuery(lift(hash))) def getByNumber[F[_]: Async]( number: Long ): EitherT[F, String, Option[Block]] = inline def detailQuery = quote { (number: Long) => query[Block].filter(b => b.number == number).take(1) } optionQuery(detailQuery(lift(number))) ================================================ FILE: modules/lmscan-backend/src/main/scala/io/leisuremeta/chain/lmscan/backend/repository/CommonQuery.scala ================================================ package io.leisuremeta.chain.lmscan.backend.repository import cats.data.EitherT import cats.effect.kernel.Async import io.getquill.Query import java.sql.SQLException import cats.implicits.* import io.getquill.PostgresJAsyncContext import io.getquill.SnakeCase import io.getquill.* import scala.concurrent.ExecutionContext trait CommonQuery: val ctx = new PostgresJAsyncContext(SnakeCase, "ctx") inline def seqQuery[F[_]: Async, T]( inline query: Query[T], ): EitherT[F, String, Seq[T]] = EitherT { Async[F].recover { for given ExecutionContext <- Async[F].executionContext result <- Async[F] .fromFuture(Async[F].delay { ctx.run(query) }) .map(Either.right(_)) yield result } { case e: SQLException => Left(s"sql exception occured: " + e.getMessage()) case e: Exception => Left(e.getMessage()) } } inline def countQuery[F[_]: Async, T]( inline query: Query[T], ): EitherT[F, String, Long] = EitherT { Async[F].recover { for given ExecutionContext <- Async[F].executionContext result <- Async[F] .fromFuture(Async[F].delay { ctx.run(query.size) }) .map(Either.right(_)) yield result } { case e: SQLException => Left(s"sql exception occured: " + e.getMessage()) case e: Exception => Left(e.getMessage()) } } inline def optionSeqQuery[F[_]: Async, T]( inline query: Query[T], ): EitherT[F, String, Option[Seq[T]]] = EitherT { Async[F].recover { for given ExecutionContext <- Async[F].executionContext detail <- Async[F] .fromFuture(Async[F].delay { ctx.run(query) }) res = if detail.isEmpty then Left("Can't found match data") else Right(Some(detail)) yield res } { case e: SQLException => Left(s"sql exception occured: " + e.getMessage()) case e: Exception => Left(e.getMessage()) } } inline def optionQuery[F[_]: Async, T]( inline query: Query[T], ): EitherT[F, String, Option[T]] = EitherT { Async[F].recover { for given ExecutionContext <- Async[F].executionContext detail <- Async[F] .fromFuture(Async[F].delay { ctx.run(query) }) res = if detail.isEmpty then Left("Can't found match data") else Right(detail.headOption) yield res } { case e: SQLException => Left(s"sql exception occured: " + e.getMessage()) case e: Exception => Left(e.getMessage()) } } def calTotalPage(totalCnt: Long, sizePerRequest: Integer): Integer = Math.ceil(totalCnt.toDouble / sizePerRequest).toInt; ================================================ FILE: modules/lmscan-backend/src/main/scala/io/leisuremeta/chain/lmscan/backend/repository/NftFileRepository.scala ================================================ package io.leisuremeta.chain.lmscan.backend.repository import io.leisuremeta.chain.lmscan.backend.entity.NftFile import cats.effect.kernel.Async import cats.data.EitherT import io.getquill.* object NftFileRepository extends CommonQuery: import ctx.* def get[F[_]: Async]( tokenId: String, ): EitherT[F, String, Option[NftFile]] = inline def detailQuery = quote { (tokenId: String) => query[NftFile].filter(f => f.tokenId == tokenId).take(1) } optionQuery(detailQuery(lift(tokenId))) ================================================ FILE: modules/lmscan-backend/src/main/scala/io/leisuremeta/chain/lmscan/backend/repository/NftInfoRepository.scala ================================================ package io.leisuremeta.chain.lmscan package backend package repository import entity._ import common.model._ import cats.effect.kernel.Async import cats.data.EitherT import io.getquill.* import io.leisuremeta.chain.lmscan.backend.entity.NftSeason import entity.NftInfo import java.net.URLDecoder object NftInfoRepository extends CommonQuery: import ctx.* def getSeasonPage[F[_]: Async]( pageNavInfo: PageNavigation, seasonEnc: String, ): EitherT[F, String, PageResponse[NftSeason]] = val season = URLDecoder.decode(seasonEnc, "UTF-8") val cntQuery = quote: (season: String) => query[NftFile] .join(query[CollectionInfo].filter(s => s.season == season)) .on((n, c) => n.tokenDefId == c.tokenDefId) def pagedQuery = quote: (pageNavInfo: PageNavigation, season: String) => val offset = sizePerRequest * pageNavInfo.pageNo val sizePerRequest = pageNavInfo.sizePerRequest query[NftFile] .join(query[CollectionInfo].filter(s => s.season == season)) .on((n, c) => n.tokenDefId == c.tokenDefId) .map((n, c) => NftSeason( n.nftName, n.tokenId, n.tokenDefId, n.creator, n.rarity, n.dataUrl, c.collectionName, ) ) .sortBy(s => s.tokenId)(Ord.asc) .drop(offset) .take(sizePerRequest) val res = for a <- countQuery(cntQuery(lift(season))) b <- seqQuery(pagedQuery(lift(pageNavInfo), lift(season))) yield (a, b) res.map: (totalCnt, r) => val totalPages = calTotalPage(totalCnt, pageNavInfo.sizePerRequest) new PageResponse(totalCnt, totalPages, r) def getPage[F[_]: Async]( pageNavInfo: PageNavigation, ): EitherT[F, String, PageResponse[NftInfo]] = val cntQuery = quote: query[NftInfo] def pagedQuery = quote: (pageNavInfo: PageNavigation) => val offset = sizePerRequest * pageNavInfo.pageNo val sizePerRequest = pageNavInfo.sizePerRequest query[NftInfo] .sortBy(t => t.sort)(Ord.asc) .drop(offset) .take(sizePerRequest) val res = for a <- countQuery(cntQuery) b <- seqQuery(pagedQuery(lift(pageNavInfo))) yield (a, b) res.map: (totalCnt, r) => val totalPages = calTotalPage(totalCnt, pageNavInfo.sizePerRequest) new PageResponse(totalCnt, totalPages, r) def get[F[_]: Async]( tokenId: String, ): EitherT[F, String, Option[CollectionInfo]] = inline def detailQuery = quote: (tokenId: String) => query[CollectionInfo].filter(f => f.tokenDefId == tokenId).take(1) optionQuery(detailQuery(lift(tokenId))) ================================================ FILE: modules/lmscan-backend/src/main/scala/io/leisuremeta/chain/lmscan/backend/repository/NftOwnerRepository.scala ================================================ package io.leisuremeta.chain.lmscan.backend.repository import cats.effect.kernel.Async import cats.data.EitherT import io.getquill.* import io.leisuremeta.chain.lmscan.backend.entity.NftOwner object NftOwnerRepository extends CommonQuery: import ctx.* def get[F[_]: Async]( tokenId: String, ): EitherT[F, String, Option[NftOwner]] = inline def detailQuery = quote { (tokenId: String) => query[NftOwner].filter(f => f.tokenId == tokenId).take(1) } optionQuery(detailQuery(lift(tokenId))) ================================================ FILE: modules/lmscan-backend/src/main/scala/io/leisuremeta/chain/lmscan/backend/repository/NftRepository.scala ================================================ package io.leisuremeta.chain.lmscan.backend.repository import io.leisuremeta.chain.lmscan.backend.entity.Nft import io.leisuremeta.chain.lmscan.common.model.PageNavigation import io.leisuremeta.chain.lmscan.common.model.PageResponse import cats.effect.kernel.Async import cats.data.EitherT import io.getquill.* object NftRepository extends CommonQuery: import ctx.* def getPageByTokenId[F[_]: Async]( tokenId: String, pageNavInfo: PageNavigation, ): EitherT[F, String, PageResponse[Nft]] = val cntQuery = quote { query[Nft] } def pagedQuery = quote { (pageNavInfo: PageNavigation) => val sizePerRequest = pageNavInfo.sizePerRequest val offset = sizePerRequest * pageNavInfo.pageNo query[Nft] .filter(t => t.tokenId == lift(tokenId)) .sortBy(t => t.eventTime)(Ord.desc) .drop(offset) .take(sizePerRequest) } val res = for a <- countQuery(cntQuery) b <- seqQuery(pagedQuery(lift(pageNavInfo))) yield (a, b) res.map { (totalCnt, r) => val totalPages = calTotalPage(totalCnt, pageNavInfo.sizePerRequest) new PageResponse(totalCnt, totalPages, r) } ================================================ FILE: modules/lmscan-backend/src/main/scala/io/leisuremeta/chain/lmscan/backend/repository/SummaryRepository.scala ================================================ package io.leisuremeta.chain.lmscan.backend.repository import io.leisuremeta.chain.lmscan.backend.entity.Summary import cats.effect.kernel.Async import cats.data.EitherT import io.getquill.* object SummaryRepository extends CommonQuery: import ctx.* def get[F[_]: Async](n: Int = 0, l: Int = 1): EitherT[F, String, Option[Seq[Summary]]] = inline def detailQuery = quote { query[Summary].sortBy(t => t.createdAt)(Ord.desc).drop(lift(n)).take(lift(l)) } optionSeqQuery(detailQuery) ================================================ FILE: modules/lmscan-backend/src/main/scala/io/leisuremeta/chain/lmscan/backend/repository/TransactionRepository.scala ================================================ package io.leisuremeta.chain.lmscan.backend.repository import io.leisuremeta.chain.lmscan.common.model.PageNavigation import io.leisuremeta.chain.lmscan.common.model.PageResponse import io.leisuremeta.chain.lmscan.backend.entity.Tx import io.leisuremeta.chain.lmscan.backend.entity.TxState import io.leisuremeta.chain.lmscan.backend.entity.AccountMapper import cats.data.EitherT import cats.implicits.* import io.getquill.PostgresJAsyncContext import io.getquill.* import cats.effect.Async import scala.concurrent.Future trait TransactionRepository[F[_]]: def getPage( pageNavInfo: PageNavigation, ): EitherT[F, String, Seq[Tx]] object TransactionRepository extends CommonQuery: import ctx.* def apply[F[_]: TransactionRepository]: TransactionRepository[F] = summon def getPage[F[_]: Async]( pageNavInfo: PageNavigation, ): EitherT[F, String, Seq[Tx]] = inline def pagedQuery = quote { (offset: Int, sizePerRequest: Int) => query[Tx] .sortBy(t => (t.blockNumber, t.eventTime))(Ord(Ord.desc, Ord.desc)) .drop(offset) .take(sizePerRequest) } val sizePerRequest = pageNavInfo.sizePerRequest val offset = sizePerRequest * pageNavInfo.pageNo seqQuery(pagedQuery(lift(offset), lift(sizePerRequest))) def get[F[_]: Async]( hash: String, ): EitherT[F, String, Option[TxState]] = inline def detailQuery = quote { (hash: String) => query[TxState].filter(tx => tx.hash == hash).take(1) } optionQuery(detailQuery(lift(hash))) def getPageByAccount[F[_]: Async]( addr: String, pageNavInfo: PageNavigation, ): EitherT[F, String, PageResponse[Tx]] = val cntQuery = quote { query[AccountMapper].filter(t => t.address == lift(addr)) } inline def pagedQuery = quote { (pageNavInfo: PageNavigation) => val sizePerRequest = pageNavInfo.sizePerRequest val offset = sizePerRequest * pageNavInfo.pageNo query[Tx] .join( query[AccountMapper] .filter(t => t.address == lift(addr)) .sortBy(_.eventTime)(Ord.desc) .drop(offset) .take(sizePerRequest) ) .on((tx, mapper) => tx.hash == mapper.hash) .map((tx, _) => tx) } val res = for totalCnt <- countQuery(cntQuery) payload <- seqQuery(pagedQuery(lift(pageNavInfo))) yield (totalCnt, payload) res.map { (totalCnt, payload) => val totalPages = calTotalPage(totalCnt, pageNavInfo.sizePerRequest) new PageResponse(totalCnt, totalPages, payload) } def getTxPageByBlock[F[_]: Async]( hash: String, pageNavInfo: PageNavigation, ): EitherT[F, String, PageResponse[Tx]] = val cntQuery = quote { query[Tx].filter(t => t.blockHash == lift(hash)) } inline def pagedQuery = quote { (pageNavInfo: PageNavigation) => val sizePerRequest = pageNavInfo.sizePerRequest val offset = sizePerRequest * pageNavInfo.pageNo query[Tx] .filter(t => t.blockHash == lift(hash)) .drop(offset) .take(sizePerRequest) .sortBy(t => t.eventTime)(Ord.desc) } val res = for totalCnt <- countQuery(cntQuery) payload <- seqQuery(pagedQuery(lift(pageNavInfo))) yield (totalCnt, payload) res.map { (totalCnt, payload) => val totalPages = calTotalPage(totalCnt, pageNavInfo.sizePerRequest) new PageResponse(totalCnt, totalPages, payload) } ================================================ FILE: modules/lmscan-backend/src/main/scala/io/leisuremeta/chain/lmscan/backend/repository/ValidatorRepository.scala ================================================ package io.leisuremeta.chain.lmscan.backend.repository import cats.effect.kernel.Async import cats.data.EitherT import io.getquill.* import io.leisuremeta.chain.lmscan.backend.entity._ object ValidatorRepository extends CommonQuery: import ctx.* def get[F[_]: Async]( addr: String, ): EitherT[F, String, Option[ValidatorInfo]] = inline def detailQuery = quote { (addr: String) => query[ValidatorInfo] .filter(_.address == addr) .take(1) } optionQuery(detailQuery(lift(addr))) def getPage[F[_]: Async](): EitherT[F, String, Seq[ValidatorInfo]] = inline def q = quote: query[ValidatorInfo] for res <- seqQuery(q) yield res ================================================ FILE: modules/lmscan-backend/src/main/scala/io/leisuremeta/chain/lmscan/backend/service/AccountService.scala ================================================ package io.leisuremeta.chain.lmscan.backend.service import cats.effect.kernel.Async import cats.data.EitherT import io.leisuremeta.chain.lmscan.backend.repository.AccountRepository import io.leisuremeta.chain.lmscan.common.model.{ PageNavigation, PageResponse, AccountDetail, } import io.leisuremeta.chain.lmscan.common.model.AccountInfo import java.time.Instant import io.leisuremeta.chain.lmscan.backend.repository.SummaryRepository object AccountService: def get[F[_]: Async]( address: String, p: Int, ): EitherT[F, Either[String, String], Option[AccountDetail]] = val balRes = AccountRepository.get(address) .leftFlatMap: _ => EitherT.pure[F, Either[String, String]](None) val txPageRes = TransactionService.getPageByAccount( address, PageNavigation(p - 1, 20), ) for account <- balRes txPage <- txPageRes summary <- SummaryRepository.get().leftMap(Left(_)) price = summary match case Some(s) => BigDecimal(s.head.lmPrice) case None => BigDecimal(0) res <- account match case Some(x) => EitherT.rightT[F, Either[String, String]]( Some(AccountDetail( Some(x.address), Some(x.free), Some(x.free / BigDecimal("1E+18") * price), txPage.totalCount, txPage.totalPages, txPage.payload, ) )) case None => EitherT.rightT[F, Either[String, String]]( Some(AccountDetail( Some(address), None, None, txPage.totalCount, txPage.totalPages, txPage.payload, ) )) yield res def getPage[F[_]: Async]( pageNavInfo: PageNavigation, ): EitherT[F, Either[String, String], PageResponse[AccountInfo]] = for page <- AccountRepository.getPage(pageNavInfo).leftMap(Left(_)) summary <- SummaryRepository.get().leftMap(Left(_)) price = summary match case Some(s) => s.headOption match case Some(h) => BigDecimal(h.lmPrice) case None => BigDecimal(0) case None => BigDecimal(0) accInfos = page.payload.map((b) => val balance = b.free AccountInfo( Some(b.address), Some(balance), Some(Instant.ofEpochSecond(b.updatedAt)), Some(balance / BigDecimal("1E+18") * price), ) ) yield PageResponse(page.totalCount, page.totalPages, accInfos) ================================================ FILE: modules/lmscan-backend/src/main/scala/io/leisuremeta/chain/lmscan/backend/service/BlockService.scala ================================================ package io.leisuremeta.chain.lmscan.backend.service import cats.effect.kernel.Async import cats.data.EitherT import io.leisuremeta.chain.lmscan.common.model.PageNavigation import io.leisuremeta.chain.lmscan.common.model.PageResponse import io.leisuremeta.chain.lmscan.common.model.{BlockDetail, BlockInfo} import io.leisuremeta.chain.lmscan.backend.entity.Block import io.leisuremeta.chain.lmscan.backend.repository.BlockRepository object BlockService: def getPage[F[_]: Async]( pageNavInfo: PageNavigation, ): EitherT[F, Either[String, String], PageResponse[BlockInfo]] = for latestBlcOpt <- BlockRepository.getLast().leftMap: e => Left(e) cnt = latestBlcOpt.get.number page <- BlockRepository.getPage(pageNavInfo, cnt).leftMap: e => Left(e) blockInfos = page.map { block => BlockInfo( Some(block.number), Some(block.hash), Some(block.txCount), Some(block.eventTime), ) } yield PageResponse.from(cnt, pageNavInfo.sizePerRequest, blockInfos) def get[F[_]: Async]( hash: String, ): EitherT[F, Either[String, String], Option[Block]] = BlockRepository.get(hash).leftMap(Left(_)) def getByNumber[F[_]: Async]( number: Long, p: Int = 1, ): EitherT[F, Either[String, String], Option[BlockDetail]] = for block <- BlockRepository.getByNumber(number).leftMap(Left(_)) txPage <- TransactionService.getPageByBlock( block.map(_.hash).getOrElse(""), PageNavigation(p - 1, 20), ) blockInfo = block.map: bl => BlockDetail( Some(bl.hash), Some(bl.parentHash), Some(bl.number), Some(bl.eventTime), Some(bl.txCount), txPage.totalCount, txPage.totalPages, txPage.payload, ) yield blockInfo def getDetail[F[_]: Async]( hash: String, p: Int, ): EitherT[F, Either[String, String], Option[BlockDetail]] = for block <- get(hash) txPage <- TransactionService.getPageByBlock( hash, PageNavigation(p - 1, 20), ) blockInfo = block.map: bl => BlockDetail( Some(bl.hash), Some(bl.parentHash), Some(bl.number), Some(bl.eventTime), Some(bl.txCount), txPage.totalCount, txPage.totalPages, txPage.payload, ) yield blockInfo ================================================ FILE: modules/lmscan-backend/src/main/scala/io/leisuremeta/chain/lmscan/backend/service/NftService.scala ================================================ package io.leisuremeta.chain.lmscan.backend.service import cats.effect.kernel.Async import cats.data.EitherT import io.leisuremeta.chain.lmscan.backend.repository._ import io.leisuremeta.chain.lmscan.common.model._ import io.leisuremeta.chain.lmscan.backend.repository.NftOwnerRepository import io.leisuremeta.chain.lmscan.backend.entity._ object NftService: def getNftDetail[F[_]: Async]( tokenId: String, // tokenId ): EitherT[F, Either[String, String], Option[NftDetail]] = for page <- NftRepository.getPageByTokenId( tokenId, new PageNavigation(0, 10), ).leftMap(Left(_)) activities = page.payload.map(nft => NftActivity( Some(nft.txHash), Some(nft.action), Some(nft.fromAddr), Some(nft.toAddr), Some(nft.eventTime), ), ) nftOwner <- NftOwnerRepository.get(tokenId).leftMap(Left(_)) nft <- NftFileRepository.get(tokenId).leftMap(Left(_)) nftFile = nft.map(nftFile => NftFileModel( Some(nftFile.tokenId), Some(nftFile.tokenDefId), Some(nftFile.collectionName), Some(nftFile.nftName), Some(nftFile.nftUri), Some(nftFile.creatorDescription), Some(nftFile.dataUrl), Some(nftFile.rarity), Some(nftFile.creator), Some(nftFile.eventTime), Some(nftFile.createdAt), Some(nftOwner.getOrElse(new NftOwner).owner), ), ) yield Some(NftDetail(nftFile, Some(activities))) def getPage[F[_]: Async]( pageNavInfo: PageNavigation, ): EitherT[F, Either[String, String], PageResponse[NftInfoModel]] = for page <- NftInfoRepository.getPage(pageNavInfo).leftMap(Left(_)) nftInfos = page.payload.map { info => NftInfoModel( season = Some(info.season), seasonName = Some(info.seasonName), totalSupply = info.totalSupply, startDate = info.startDate.map(_.toInstant()), endDate = info.endDate.map(_.toInstant()), thumbUrl = info.thumbUrl, ) } yield PageResponse(page.totalCount, page.totalPages, nftInfos) def getSeasonPage[F[_]: Async]( pageNavInfo: PageNavigation, season: String, ): EitherT[F, Either[String, String], PageResponse[NftSeasonModel]] = for page <- NftInfoRepository.getSeasonPage(pageNavInfo, season).leftMap(Left(_)) seasons = page.payload.map(_.toModel) yield PageResponse(page.totalCount, page.totalPages, seasons) def getNftOwnerInfo[F[_]: Async]( tokenId: String, // tokenId ): EitherT[F, Either[String, String], Option[NftOwnerInfo]] = for nftOwner <- NftOwnerRepository.get(tokenId).leftMap(Left(_)) nft <- NftFileRepository.get(tokenId).leftMap(Left(_)) ownerInfo = NftOwnerInfo( nftOwner.map(_.owner), nft.map(_.dataUrl), ) yield Some(ownerInfo) ================================================ FILE: modules/lmscan-backend/src/main/scala/io/leisuremeta/chain/lmscan/backend/service/SearchService.scala ================================================ package io.leisuremeta.chain.lmscan.backend.service import io.leisuremeta.chain.lmscan.common.model._ import cats.data.EitherT import cats.effect.Async object SearchService: def getKeywordSearch[F[_]: Async](keyword: String): EitherT[F, Either[String, String], SearchResult] = keyword.toLongOption match case Some(n) => numSearch(n) case None => hashSearch(keyword) def numSearch[F[_]: Async](n: Long): EitherT[F, Either[String, String], SearchResult] = NftService.getNftDetail(n.toString) .flatMap: d => EitherT.pure[F, Either[String, String]](SearchResult.nft(d.get)) .leftFlatMap: _ => for blc <- BlockService.getByNumber(n) yield SearchResult.blc(blc.get) .leftFlatMap: _ => EitherT.pure[F, Either[String, String]](SearchResult.empty) def hashSearch[F[_]: Async](keyword: String): EitherT[F, Either[String, String], SearchResult] = TransactionService.getDetail(keyword) .flatMap: d => EitherT.pure[F, Either[String, String]](SearchResult.tx(d.get)) .leftFlatMap: _ => for blc <- BlockService.getDetail(keyword, 1) yield SearchResult.blc(blc.get) .leftFlatMap: _ => for acc <- AccountService.get(keyword, 1) yield SearchResult.acc(acc.get) .leftFlatMap: _ => EitherT.pure[F, Either[String, String]](SearchResult.empty) ================================================ FILE: modules/lmscan-backend/src/main/scala/io/leisuremeta/chain/lmscan/backend/service/SummaryService.scala ================================================ package io.leisuremeta.chain.lmscan.backend.service import io.leisuremeta.chain.lmscan.common.model.SummaryModel import io.leisuremeta.chain.lmscan.backend.repository.SummaryRepository import cats.effect.kernel.Async import cats.data.EitherT import io.leisuremeta.chain.lmscan.common.model.SummaryChart import io.leisuremeta.chain.lmscan.common.model.SummaryBoard import io.leisuremeta.chain.lmscan.backend.entity.Summary object SummaryService: extension (s: Summary) def toM: SummaryModel = SummaryModel( Some(s.lmPrice), Some(s.blockNumber), Some(s.totalAccounts), Some(s.createdAt), Some(s.totalTxSize.toLong), Some(s.totalBalance), s.marketCap, s.cirSupply, s.totalNft, ) extension (opt: Option[Summary]) def toM: SummaryModel = opt match case Some(s) => s.toM case None => SummaryModel() def get[F[_]: Async](n: Int): EitherT[F, Either[String, String], Option[SummaryModel]] = SummaryRepository.get(n, 1) .map: res => res.map(_.headOption.toM) .leftFlatMap: _ => EitherT.pure[F, Either[String ,String]](Some(SummaryModel())) def getBoard[F[_]: Async]: EitherT[F, Either[String, String], Option[SummaryBoard]] = for todayOpt <- get(0) yesterdayOpt <- get(143) model = todayOpt.zip(yesterdayOpt).map((today, yesterday) => SummaryBoard(today, yesterday)) yield model def get5List[F[_]: Async]: EitherT[F, Either[String, String], SummaryChart] = for summary <- SummaryRepository.get(0, 144 * 5 + 1).leftMap(Left(_)) model = summary.map( _.grouped(144).map(_.head.toM).toSeq ) chart = SummaryChart(model.get) yield chart def getList[F[_]: Async]: EitherT[F, Either[String, String], SummaryChart] = for summary <- SummaryRepository.get(0, 144 * 5).leftMap(Left(_)) model = summary.map(_.map(_.toM )) chart = SummaryChart(model.getOrElse(Seq())) yield chart ================================================ FILE: modules/lmscan-backend/src/main/scala/io/leisuremeta/chain/lmscan/backend/service/TransactionService.scala ================================================ package io.leisuremeta.chain.lmscan.backend.service import io.leisuremeta.chain.lmscan.backend.entity.Tx import io.leisuremeta.chain.lmscan.backend.entity.TxState import io.leisuremeta.chain.lmscan.common.model._ import io.leisuremeta.chain.lmscan.backend.repository._ import cats.data.EitherT import cats.effect.Async object TransactionService: def get[F[_]: Async]( hash: String, ): EitherT[F, String, Option[TxState]] = TransactionRepository.get(hash) def getDetail[F[_]: Async]( hash: String, ): EitherT[F, Either[String, String], Option[TxDetail]] = for trx <- TransactionRepository.get(hash).leftMap(Left(_)) detail = trx.map { tx => TxDetail( Some(tx.hash), Some(tx.eventTime), Some(tx.json), ) } yield detail def getPage[F[_]: Async]( pageNavInfo: PageNavigation, ): EitherT[F, Either[String, String], PageResponse[TxInfo]] = for summaryOpt <- SummaryService.get(0) page <- TransactionRepository.getPage(pageNavInfo).leftMap(Left(_)) summary = summaryOpt.getOrElse(SummaryModel()) cnt = Math.min(summary.totalTxSize.getOrElse(0L), 100000L) txInfo = convertToInfo(page) yield PageResponse.from(cnt, pageNavInfo.sizePerRequest, txInfo) def getPageByAccount[F[_]: Async]( address: String, pageNavInfo: PageNavigation, ): EitherT[F, Either[String, String], PageResponse[TxInfo]] = for page <- TransactionRepository.getPageByAccount(address, pageNavInfo).leftMap(Left(_)) yield page.copy(payload = convertToInfoForAccount(page.payload, address)) def getPageByBlock[F[_]: Async]( blockHash: String, pageNavInfo: PageNavigation, ): EitherT[F, Either[String, String], PageResponse[TxInfo]] = for page <- TransactionRepository.getTxPageByBlock(blockHash, pageNavInfo).leftMap(Left(_)) yield page.copy(payload = convertToInfo(page.payload)) def getPageByFilter[F[_]: Async]( pageInfo: PageNavigation, accountAddr: Option[String], blockHash: Option[String], subType: Option[String], ): EitherT[F, Either[String, String], PageResponse[TxInfo]] = (accountAddr, blockHash) match case (None, None) => getPage[F](pageInfo) case (_, _) => EitherT.left(Async[F].delay(Left("검색 파라미터를 하나만 입력해주세요."))) def convertToInfo(txs: Seq[Tx]): Seq[TxInfo] = txs.map { tx => TxInfo( Some(tx.hash), Some(tx.blockNumber), Some(tx.eventTime), Some(tx.txType), Some(tx.tokenType), Some(tx.signer), Some(tx.subType), None, None, ) } def convertToInfoForAccount(txs: Seq[Tx], address: String): Seq[TxInfo] = txs.map { tx => TxInfo( Some(tx.hash), Some(tx.blockNumber), Some(tx.eventTime), Some(tx.txType), Some(tx.tokenType), Some(tx.signer), Some(tx.subType), Some(if tx.signer == address then "Out" else "In"), None, ) } ================================================ FILE: modules/lmscan-backend/src/main/scala/io/leisuremeta/chain/lmscan/backend/service/ValidatorService.scala ================================================ package io.leisuremeta.chain.lmscan.backend.service import cats.effect.kernel.Async import cats.data.EitherT import io.leisuremeta.chain.lmscan.common.model.{ PageNavigation, PageResponse, } import io.leisuremeta.chain.lmscan.backend.repository.ValidatorRepository import io.leisuremeta.chain.lmscan.common.model.NodeValidator import io.leisuremeta.chain.lmscan.backend.repository.BlockRepository import io.leisuremeta.chain.lmscan.backend.entity.Block import com.typesafe.config.ConfigFactory object ValidatorService: val conf = ConfigFactory.load() val vdcnt = conf.getInt("vdcnt") def get[F[_]: Async]( address: String, p: Int, ): EitherT[F, Either[String, String], Option[NodeValidator.ValidatorDetail]] = for latestBlcOpt: Option[Block] <- BlockRepository.getLast().leftMap: e => Left(e) validator <- ValidatorRepository.get(address).leftMap(Left(_)) cnt = latestBlcOpt.get.number start = cnt - ((p - 1) * vdcnt) * 20 blcs <- BlockRepository.getPageByProposer( PageNavigation(p - 1, 20), start, address, ).leftMap(Left(_)) res = validator.map(v => NodeValidator.ValidatorDetail(v.toModel, PageResponse.from( cnt / vdcnt, 20, blcs.map(_.toModel) ))) yield res def getPage[F[_]: Async](): EitherT[F, Either[String, String], Seq[NodeValidator.Validator]] = for page <- ValidatorRepository.getPage().leftMap(Left(_)) res = page.map(_.toModel) yield res ================================================ FILE: modules/lmscan-backend/src/test/scala/EmbeddedPostgreFlywayTest.scala ================================================ import com.opentable.db.postgres.junit.EmbeddedPostgresRules import com.opentable.db.postgres.embedded.FlywayPreparer import org.junit.rules.TestRule import org.junit.runners.model.Statement import org.junit.runner.Description class EmbeddedPostgreFlywayTest extends munit.FunSuite { def withRule[T <: TestRule](rule: T)(testCode: T => Any): Unit = { rule( new Statement() { override def evaluate(): Unit = val _ = testCode(rule) }, Description.createSuiteDescription("JUnit rule wrapper") ).evaluate() } test("test") { withRule(EmbeddedPostgresRules.preparedDatabase(FlywayPreparer.forClasspathLocation("db/test", "db/common", "db/seed"))) { preparedDbRule => val c = preparedDbRule.getTestDatabase().getConnection() val s = c.createStatement() println("////////////////////////////////////////////////////// test of printing created table //////////////////////////////////////////////////////") val rs_t = s.executeQuery("SELECT table_name FROM information_schema.tables WHERE table_schema='public' AND table_type='BASE TABLE'") while (rs_t.next) { println(rs_t.getString("table_name")) } println("////////////////////////////////////////////////////// test of printing seed data //////////////////////////////////////////////////////") val rs_s = s.executeQuery("SELECT * FROM public.account") while (rs_s.next) { println( "id = %s, address = %s, balance = %s, amount = %s, type = %s, created_at = %s" .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")) ) } } } } ================================================ FILE: modules/lmscan-common/.js/package.json ================================================ { "dependencies": { "typescript": "^4.9.4" } } ================================================ FILE: modules/lmscan-common/js/package.json ================================================ { "dependencies": { "typescript": "^4.9.4" } } ================================================ FILE: modules/lmscan-common/shared/src/main/scala/io/leisuremeta/ExplorerAPI.scala ================================================ package io.leisuremeta.chain.lmscan.common import sttp.model.StatusCode import sttp.tapir.* import sttp.tapir.generic.auto.* import sttp.tapir.json.circe.* import io.circe.generic.auto.* import io.leisuremeta.chain.lmscan.common.model._ import io.circe.* object ExploreApi: opaque type Utf8 = String object Utf8: def apply(s: String): Utf8 = s extension (u: Utf8) def asString: String = u final case class ServerError(msg: String) sealed trait UserError: def msg: String final case class Unauthorized(msg: String) extends UserError final case class NotFound(msg: String) extends UserError final case class BadRequest(msg: String) extends UserError val baseEndpoint = endpoint.errorOut( oneOf[Either[ServerError, UserError]]( oneOfVariantFromMatchType( StatusCode.Unauthorized, jsonBody[Right[ServerError, Unauthorized]] .description("invalid signature"), ), oneOfVariantFromMatchType( StatusCode.NotFound, jsonBody[Right[ServerError, NotFound]] .description("not found"), ), oneOfVariantFromMatchType( StatusCode.BadRequest, jsonBody[Right[ServerError, BadRequest]] .description("bad request"), ), oneOfVariantFromMatchType( StatusCode.InternalServerError, jsonBody[Left[ServerError, UserError]] .description("internal server error"), ), ), ) val getTxPageEndPoint = baseEndpoint.get .in("tx" / "list") .in( sttp.tapir.EndpointInput.derived[PageNavigation], ) .out(jsonBody[PageResponse[TxInfo]]) val getTxDetailEndPoint = baseEndpoint.get .in("tx") .in(path[String]) // tx_hash .in("detail") .out(jsonBody[Option[TxDetail]]) val getBlockPageEndPoint = baseEndpoint.get .in("block" / "list") .in( sttp.tapir.EndpointInput.derived[PageNavigation], ) .out(jsonBody[PageResponse[BlockInfo]]) val getBlockDetailEndPoint = baseEndpoint.get .in("block") .in(path[String]) // block_hash .in("detail") .in(query[Option[Int]]("p")) .out(jsonBody[Option[BlockDetail]]) val getAccountPageEndPoint = baseEndpoint.get .in("account" / "list") .in( sttp.tapir.EndpointInput.derived[PageNavigation], ) .out(jsonBody[PageResponse[AccountInfo]]) val getAccountDetailEndPoint = baseEndpoint.get .in("account") .in(path[String]("accountAddr")) .in("detail") .in(query[Option[Int]]("p")) .out(jsonBody[Option[AccountDetail]]) val getNftPageEndPoint = baseEndpoint.get .in("nft" / "list") .in( sttp.tapir.EndpointInput.derived[PageNavigation], ) .out(jsonBody[PageResponse[NftInfoModel]]) val getNftSeasonEndPoint = baseEndpoint.get .in("nft") .in(path[String]("season")) .in( sttp.tapir.EndpointInput.derived[PageNavigation], ) .out(jsonBody[PageResponse[NftSeasonModel]]) val getNftDetailEndPoint = baseEndpoint.get .in("nft") .in(path[String]("tokenId")) .in("detail") .out(jsonBody[Option[NftDetail]]) val getNftOwnerInfoEndPoint = baseEndpoint.get .in("nft") .in(path[String]("tokenId")) .in("info") .out(jsonBody[Option[NftOwnerInfo]]) val getSummaryMainEndPoint = baseEndpoint.get .in("summary") .in("main") .out(jsonBody[Option[SummaryBoard]]) val getSummaryChartEndPoint = baseEndpoint.get .in("summary") .in("chart") .in(path[String]("chartType")) .out(jsonBody[SummaryChart]) val getTotalBalance = baseEndpoint.get .in("total") .in("balance") .out(jsonBody[Option[String]]) val getValanceFromChainProd = baseEndpoint.get .in("prod") .in("chain") .out(jsonBody[Option[String]]) val getKeywordSearchResult = baseEndpoint.get .in("search") .in(path[String]("keyword")) .out(jsonBody[SearchResult]) val getValidators = baseEndpoint.get .in("vds") .out(jsonBody[Seq[NodeValidator.Validator]]) val getValidator = baseEndpoint.get .in("vd") .in(path[String]) .in(query[Option[Int]]("p")) .out(jsonBody[Option[NodeValidator.ValidatorDetail]]) ================================================ FILE: modules/lmscan-common/shared/src/main/scala/io/leisuremeta/model/AccountDetail.scala ================================================ package io.leisuremeta.chain.lmscan.common.model import io.circe.Decoder import io.circe.generic.semiauto.deriveDecoder final case class AccountDetail( address: Option[String] = None, balance: Option[BigDecimal] = None, value: Option[BigDecimal] = None, totalCount: Long = 0L, totalPages: Int = 0, payload: Seq[TxInfo] = Seq(), ) extends ApiModel object AccountDetail: given Decoder[AccountDetail] = deriveDecoder[AccountDetail] ================================================ FILE: modules/lmscan-common/shared/src/main/scala/io/leisuremeta/model/AccountInfo.scala ================================================ package io.leisuremeta.chain.lmscan.common.model import java.time.Instant final case class AccountInfo( address: Option[String], balance: Option[BigDecimal], updated: Option[Instant], value: Option[BigDecimal] ) extends ApiModel ================================================ FILE: modules/lmscan-common/shared/src/main/scala/io/leisuremeta/model/ApiModel.scala ================================================ package io.leisuremeta.chain.lmscan.common.model trait ApiModel ================================================ FILE: modules/lmscan-common/shared/src/main/scala/io/leisuremeta/model/BlockDetail.scala ================================================ package io.leisuremeta.chain.lmscan.common.model import io.circe.Decoder import io.circe.generic.semiauto.deriveDecoder final case class BlockDetail( hash: Option[String] = None, parentHash: Option[String] = None, number: Option[Long] = None, timestamp: Option[Long] = None, txCount: Option[Long] = None, totalCount: Long = 0L, totalPages: Int = 0, payload: Seq[TxInfo] = Seq(), ) extends ApiModel object BlockDetail: given Decoder[BlockDetail] = deriveDecoder[BlockDetail] ================================================ FILE: modules/lmscan-common/shared/src/main/scala/io/leisuremeta/model/BlockInfo.scala ================================================ package io.leisuremeta.chain.lmscan.common.model import io.circe.Decoder import io.circe.generic.semiauto.deriveDecoder final case class BlockInfo( number: Option[Long] = None, hash: Option[String] = None, txCount: Option[Long] = None, createdAt: Option[Long] = None, proposer: Option[String] = None, ) extends ApiModel object BlockInfo: given Decoder[BlockInfo] = deriveDecoder[BlockInfo] ================================================ FILE: modules/lmscan-common/shared/src/main/scala/io/leisuremeta/model/NftActivity.scala ================================================ package io.leisuremeta.chain.lmscan.common.model import io.circe.Decoder import io.circe.generic.semiauto.deriveDecoder final case class NftActivity( txHash: Option[String] = None, action: Option[String] = None, fromAddr: Option[String] = None, toAddr: Option[String] = None, createdAt: Option[Long] = None, ) extends ApiModel object NftActivity: given Decoder[NftActivity] = deriveDecoder[NftActivity] ================================================ FILE: modules/lmscan-common/shared/src/main/scala/io/leisuremeta/model/NftDetail.scala ================================================ package io.leisuremeta.chain.lmscan.common.model import io.circe.Decoder import io.circe.generic.semiauto.deriveDecoder final case class NftDetail( nftFile: Option[NftFileModel] = None, activities: Option[Seq[NftActivity]] = None, ) extends ApiModel object NftDetail: given Decoder[NftDetail] = deriveDecoder[NftDetail] ================================================ FILE: modules/lmscan-common/shared/src/main/scala/io/leisuremeta/model/NftFileModel.scala ================================================ package io.leisuremeta.chain.lmscan.common.model import io.circe.Decoder import io.circe.generic.semiauto.deriveDecoder final case class NftFileModel( tokenId: Option[String] = None, tokenDefId: Option[String] = None, collectionName: Option[String] = None, nftName: Option[String] = None, nftUri: Option[String] = None, creatorDescription: Option[String] = None, dataUrl: Option[String] = None, rarity: Option[String] = None, creator: Option[String] = None, eventTime: Option[Long] = None, createdAt: Option[Long] = None, owner: Option[String] = None, ) extends ApiModel object NftFileModel: given Decoder[NftFileModel] = deriveDecoder[NftFileModel] ================================================ FILE: modules/lmscan-common/shared/src/main/scala/io/leisuremeta/model/NftInfo.scala ================================================ package io.leisuremeta.chain.lmscan.common.model import java.time.Instant final case class NftInfoModel( season: Option[String] = None, seasonName: Option[String] = None, totalSupply: Option[Int] = None, startDate: Option[Instant] = None, endDate: Option[Instant] = None, thumbUrl: Option[String] = None, ) extends ApiModel ================================================ FILE: modules/lmscan-common/shared/src/main/scala/io/leisuremeta/model/NftOwnerInfo.scala ================================================ package io.leisuremeta.chain.lmscan.common.model final case class NftOwnerInfo( owner: Option[String] = None, nftUrl: Option[String] = None, ) ================================================ FILE: modules/lmscan-common/shared/src/main/scala/io/leisuremeta/model/NftSeasonModel.scala ================================================ package io.leisuremeta.chain.lmscan.common.model final case class NftSeasonModel( nftName: Option[String] = None, tokenId: Option[String] = None, tokenDefId: Option[String] = None, creator: Option[String] = None, rarity: Option[String] = None, thumbUrl: Option[String] = None, collection: Option[String] = None, ) extends ApiModel ================================================ FILE: modules/lmscan-common/shared/src/main/scala/io/leisuremeta/model/PageNavigation.scala ================================================ package io.leisuremeta.chain.lmscan.common.model import sttp.tapir.EndpointIO.annotations.query import io.getquill.Ord import io.getquill.ast.Asc import io.getquill.ast.Desc case class PageNavigation( @query pageNo: Int, @query sizePerRequest: Int, // @query // orderByProperty: Option[String], // ex) id:desc ) extends ApiModel // ): // def orderBy(): OrderBy = // val items = this.orderByProperty.get.split(":") // val x = items(1) match // case "asc" => Ord(Asc) // case "desc" => Ord(Desc) // val direction = OrderBy.toDirection(items(1)) // new OrderBy(items(0), direction) case class OrderBy( property: String, direction: Ord[Any], ) object OrderBy: def toDirection(order: String): Ord[Any] = order match case "asc" => Ord(Asc) case "desc" => Ord(Desc) ================================================ FILE: modules/lmscan-common/shared/src/main/scala/io/leisuremeta/model/PageResponse.scala ================================================ package io.leisuremeta.chain.lmscan.common.model final case class PageResponse[T]( totalCount: Long = 0L, totalPages: Int = 0, payload: Seq[T] = Seq(), ) extends ApiModel object PageResponse: def from[T](total: Long, cnt: Int, seq: Seq[T]) = val totalPage = Math.ceil(total.toDouble / cnt).toInt; PageResponse(total, totalPage, seq) ================================================ FILE: modules/lmscan-common/shared/src/main/scala/io/leisuremeta/model/SearchResult.scala ================================================ package io.leisuremeta.chain.lmscan.common.model import io.circe.HCursor import io.circe.Decoder enum SearchResult: case acc(item: AccountDetail) case tx(item: TxDetail) case blc(item: BlockDetail) case nft(item: NftDetail) case empty object SearchResult: given Decoder[SearchResult] = (c: HCursor) => c.keys match case Some(k) if k.head == "acc" => c.downField("acc").get[AccountDetail]("item").map(item => SearchResult.acc(item)) case Some(k) if k.head == "tx" => c.downField("tx").get[TxDetail]("item").map(item => SearchResult.tx(item)) case Some(k) if k.head == "blc" => c.downField("blc").get[BlockDetail]("item").map(item => SearchResult.blc(item)) case Some(k) if k.head == "nft" => c.downField("nft").get[NftDetail]("item").map(item => SearchResult.nft(item)) case _ => Decoder.resultInstance.pure(SearchResult.empty) // res = acc.downField("item").as[AccountDetail] // SearchResult.acc(res.get.) ================================================ FILE: modules/lmscan-common/shared/src/main/scala/io/leisuremeta/model/SummaryModel.scala ================================================ package io.leisuremeta.chain.lmscan.common.model final case class SummaryModel( lmPrice: Option[Double] = None, blockNumber: Option[Long] = None, totalAccounts: Option[Long] = None, createdAt: Option[Long] = None, totalTxSize: Option[Long] = None, totalBalance: Option[BigDecimal] = None, marketCap: Option[BigDecimal] = None, cirSupply: Option[BigDecimal] = None, totalNft: Option[Long] = None, ) extends ApiModel final case class SummaryBoard( today: SummaryModel = SummaryModel(), yesterday: SummaryModel = SummaryModel(), ) extends ApiModel final case class SummaryChart( list: Seq[SummaryModel] = Seq() ) extends ApiModel ================================================ FILE: modules/lmscan-common/shared/src/main/scala/io/leisuremeta/model/TxDetail.scala ================================================ package io.leisuremeta.chain.lmscan.common.model import io.circe.Decoder import io.circe.generic.semiauto.deriveDecoder final case class TxDetail( hash: Option[String] = None, createdAt: Option[Long] = None, json: Option[String] = None, ) extends ApiModel object TxDetail: given Decoder[TxDetail] = deriveDecoder[TxDetail] ================================================ FILE: modules/lmscan-common/shared/src/main/scala/io/leisuremeta/model/TxInfo.scala ================================================ package io.leisuremeta.chain.lmscan.common.model import io.circe.Decoder import io.circe.generic.semiauto.deriveDecoder final case class TxInfo( hash: Option[String] = None, blockNumber: Option[Long] = None, createdAt: Option[Long] = None, txType: Option[String] = None, tokenType: Option[String] = None, signer: Option[String] = None, subType: Option[String] = None, inOut: Option[String] = None, value: Option[String] = None, ) extends ApiModel object TxInfo: given Decoder[TxInfo] = deriveDecoder[TxInfo] ================================================ FILE: modules/lmscan-common/shared/src/main/scala/io/leisuremeta/model/Validator.scala ================================================ package io.leisuremeta.chain.lmscan.common.model import io.circe.Decoder import io.circe.generic.semiauto.deriveDecoder object NodeValidator: case class Validator( address: Option[String] = None, power: Option[Double] = None, cnt: Option[Long] = None, name: Option[String] = None, ) extends ApiModel case class ValidatorList( payload: List[Validator] = Nil, ) extends ApiModel case class ValidatorDetail( validator: Validator = Validator(), page: PageResponse[BlockInfo] = PageResponse(), ) extends ApiModel given Decoder[Validator] = deriveDecoder[Validator] given Decoder[ValidatorList] = deriveDecoder[ValidatorList] given Decoder[ValidatorDetail] = deriveDecoder[ValidatorDetail] ================================================ FILE: modules/lmscan-frontend/.parcelrc ================================================ { "extends": "@parcel/config-default", "compressors": { "*.{html,css,js,svg,map}": [ "...", "@parcel/compressor-gzip", "@parcel/compressor-brotli" ] } } ================================================ FILE: modules/lmscan-frontend/assets/css/desktop.css ================================================ @charset "utf-8"; @import url(./tooltip.css); .con-wrap { min-height: calc(100vh - 350px); } .con-wrap > * { gap: var(--pad); } .board-comp { grid-column: span 3; grid-template-columns: repeat(2, 50%); gap: 8px; > * { grid-column: span 1; } > :first-child { grid-row: 1; grid-column: 1 / 3; font-weight: var(--bold); } > :nth-child(2) { grid-row: 2; grid-column: 1 / 3; font-size: var(--t-text); font-weight: var(--bold); color: var(--b1); } > :nth-child(3) { grid-row: 3; display: flex; gap: 11px; align-items: center; > :first-child { padding: 4px 6px; font-size: 10px; background-color: var(--w3); border-radius: var(--br); } > :last-child { font-size: 12px; font-weight: var(--bold); &.pos { color: var(--darken-pri); &::before { content: "+"; } } &.neg { color: var(--darken-sec); } } } svg { grid-row: 3 / 4; fill: transparent; aspect-ratio: 4; justify-self: end; align-self: self-end; width: 100%; path { stroke: var(--lighten-pri); stroke-width: 1; stroke-linejoin: round; } polyline { fill: var(--lightest-pri); } } @media screen and (max-width: 1200px) { grid-column: span 6; } } nav { display: flex; gap: calc(2 * var(--pad)); padding-top: var(--pad); padding-bottom: 12px; align-items: baseline; * { font-size: var(--t-text); color: var(--b1); font-weight: var(--bold); } > :first-child { font-size: 32px; cursor: pointer; } } .con-wrap > .table-container:not(:last-child) { margin-bottom: calc(var(--pad) - 8px); } .table-container { grid-column: span 6; background-color: var(--w0); border-radius: var(--br); display: flex; flex-direction: column; row-gap: calc(var(--pad) / 2); border: 1px solid var(--lightest-pri); padding: var(--pad); position: relative; } .detail.table-container { row-gap: var(--pad); } .main-table .table-container { row-gap: var(--br); } .table-head { font-weight: var(--bold); } .table-container .row { display: grid; padding-bottom: calc(var(--pad) / 2); border-bottom: 1px solid var(--lightest-pri); gap: var(--pad); } .table-container .row > * { overflow: hidden; text-overflow: ellipsis; } .table-container.blc .row { grid-template-columns: 2fr 2fr 7fr 1fr; } .table-container.tx-m .row { grid-template-columns: 5fr 2fr 5fr; } .table-container.tx .row { grid-template-columns: 4fr 1fr 1fr 4fr 2fr; } .table-container.accs .row { grid-template-columns: 5fr 3fr 2fr 2fr; } .table-container.nfts .row { grid-template-columns: 1fr 5fr 2fr 2fr 2fr; } .table-container.nft-token .row { grid-template-columns: 1fr 3fr 4fr 3fr 1fr; } .table-container.nft .row { grid-template-columns: 3fr 1fr 2fr 3fr 3fr; } .table-container.vds .row { grid-template-columns: 5fr 3fr 4fr; } .table-title { display: flex; justify-content: space-between; padding-bottom: calc(2 * var(--br)); > :first-child { font-size: var(--t-text); color: var(--b1); font-weight: var(--bold); } > :last-child { color: var(--w0); font-size: 10px; background-color: var(--primary); border-radius: var(--br); padding: 4px 16px; } } .page-title { display: block; font-size: var(--t-text); font-weight: var(--bold); color: var(--b1); } .detail .row { grid-template-columns: repeat(12, 1fr); padding-bottom: 0; border: none; :first-child { grid-column: span 4; } :last-child { grid-column: span 8; } &:last-child { padding-bottom: 0; } } .detail .row.tri { :first-child { font-weight: var(--bold); grid-column: span 2; } :nth-child(2) { grid-column: span 5; } :last-child { grid-column: span 5; } } .nft-detail { gap: 24px; } .nft-detail > img, .nft-detail > video { grid-column: span 4; width: 100%; } .nft-detail > .nft-title { grid-column: span 8; } .nft-detail > .table-container { grid-column: span 8; } .table-search{ display: flex; gap: var(--br); width: 100%; padding-top: var(--pad); align-items: center; justify-content: center; font-family: JSDongkang,Roboto,sans-serif; } .table-search a, .table-search p { border-radius: var(--br); padding: 5px 16px; background-color: var(--lightest-pri); &.dis { pointer-events: none; cursor: default; color: var(--g); } } .table-search p { cursor: pointer; } .type-search { border-radius: 5px; background-position: 50% 50%; padding: 3px 16px; width: 100px; background-color: var(--lightest-pri); } .blc-num, .tx-hash, .blc-hash, .acc-hash, .token-id { color: var(--darken-pri); cursor: pointer; } .search-container { display: grid; height: 3em; grid-template-columns: repeat(12, 1fr); } .search-container > * { font-size: 16px; } .search-container > :first-child { padding-left: var(--pad); grid-column: span 10; border-radius: var(--br) 0 0 var(--br); ::placeholder { color: var(--lightest-pri) } } .search-container > :last-child { display: flex; align-items: center; justify-content: center; cursor: pointer; border: 1px solid var(--darken-pri); border-radius: 0 var(--br) var(--br) 0; background-color: var(--darken-pri); color: var(--w0); grid-column: span 2; } input { border: 1px solid var(--lightest-pri); &:focus { outline: none; border-color: var(--primary); color: var(--primary); } } ================================================ FILE: modules/lmscan-frontend/assets/css/footer.css ================================================ footer { background-color: var(--w0); } footer * { color: #303972; font-size: 16px; } footer p > *, footer dt { color: var(--b1); } footer > div { display: flex; flex-direction: row; justify-content: space-between; gap: var(--pad); } footer .footer-left { flex-basis: 100%; display: flex; flex-wrap: wrap; flex-direction: column; gap: var(--pad); > :first-child { display: flex; align-items: end; gap: .5em; strong { font-size: var(--t-text); } } strong { font-size: 16px; font-weight: var(--bold); } } footer .footer-right{ flex-basis: 100%; display: flex; justify-content: center; dl { flex: 0 0 40%; } dt { font-size: 14px; font-weight: var(--bold); } dd{ padding-top: var(--br); display: flex; flex-wrap: wrap; flex-direction: column; gap: 12px; } a{ display: inline-flex; align-items: center; font-size: 10px; text-decoration: none; height: 20px; padding-left:35px; background-position: left top; background-size: auto 20px; background-repeat: no-repeat; &:hover{opacity: 0.8;} } } .icon-The-Moon-Labs{background-image: url(../img/footer/TMLS_303972.png);} .icon-leisuremetaverse{background-image: url(../img/footer/LM.png);} .icon-playnomm{background-image: url(../img/footer/playnomm.png);} .icon-lm-nova{background-image: url(../img/footer/ilike.png);} .icon-tme{background-image: url(../img/footer/tme.png);} .icon-github{background-image: url(../img/footer/Github.png);} .icon-telegram{background-image: url(../img/footer/icon_telegram.svg);} .icon-twitter{background-image: url(../img/footer/Tweeter.png);} .icon-discord{background-image: url(../img/footer/Discord.png);} @media screen and (max-width: 800px) { footer > div { flex-wrap: wrap; } } ================================================ FILE: modules/lmscan-frontend/assets/css/index.html ================================================ LeisureMeta Chain Block Explorer
================================================ FILE: modules/lmscan-frontend/assets/css/loading.css ================================================ div.loader-case { display: flex; padding: 24px 0; background: rgba(15, 14, 14, 0.4); grid-column: span 12; border-radius: var(--br); } .loader { position: relative; left: calc(50% - 24px); top: calc(50% - 24px); -webkit-animation: spin 2s linear infinite; /* Safari */ width: 48px; height: 48px; border-radius: 50%; border: 10px solid; 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); animation: rotation 1s linear infinite; } /* Safari */ @-webkit-keyframes spin { 0% { -webkit-transform: rotate(0deg); } 100% { -webkit-transform: rotate(360deg); } } @keyframes rotation { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } ================================================ FILE: modules/lmscan-frontend/assets/css/mobile.css ================================================ @charset "utf-8"; html { --pad: 20px; font-size: 10px; --t-text: 14px; --b-c: 2px solid var(--lightest-pri); } .con-wrap { min-height: calc(100vh - 350px); } .con-wrap > * { gap: var(--br); } .board-area { gap: 0; height: 190px; grid-template-rows: repeat(3, 1fr); border: 1px solid var(--lightest-pri); border-radius: var(--br); padding: var(--br) var(--pad); background-color: var(--w0); >:nth-child(2), >:nth-child(3), >:nth-child(6), >:nth-child(7), >:nth-child(8) { display: none; } >.board-comp:nth-child(4) { border-top: var(--b-c); border-bottom: var(--b-c); } } .board-comp, .board-comp.chart { grid-column: span 12; gap: var(--br) 0; padding: var(--br) 0; grid-template-columns: repeat(6, 1fr); border: none; border-radius: 0; > :first-child { grid-column: span 6; } > :nth-child(2) { grid-column: span 2; } > :nth-child(3) { grid-column: span 4; flex-direction: row-reverse; > :first-child { background-color: transparent; &::before { content: 'Last '; } } } svg { display: none; } } nav { display: flex; gap: calc(2 * var(--pad)); padding-top: var(--pad); padding-bottom: 12px; align-items: baseline; > :first-child { font-size: 32px; } } .table-container { grid-column: span 12; padding: var(--br) var(--pad); .row { grid-column: span 12; height: 23px; } } .table-container.blc .row { grid-template-columns: 2fr 2fr 6fr 2fr; } .table-container.tx-m .row { grid-template-columns: 5fr 2fr 5fr; } .table-container.tx .row { grid-template-columns: 5fr 2fr 5fr; >:nth-child(2), >:nth-child(5) { display: none; } } .table-container.accs .row { grid-template-columns: 9fr 3fr; >:nth-child(3), >:nth-child(4) { display: none; } } .table-container.nfts .row { grid-template-columns: 2fr 9fr 3fr; >:nth-child(4), >:nth-child(5) { display: none; } } .table-container.nft-token .row { grid-template-columns: 2fr 9fr 3fr; >:nth-child(3), >:nth-child(4) { display: none; } } .table-container.nft .row { grid-template-columns: 3fr 1fr 2fr 3fr 3fr; } .main-table { position: relative; height: 710px; background-color: var(--w0); border-radius: var(--br); border: 1px solid var(--lightest-pri); .table-container { padding-top: 0; position: absolute; bottom: 0; border: none; } .table-title { height: 0; > :first-child { width: calc(50vw - 2 * var(--pad)); height: 25px; font-size: 14px; font-weight: normal; border-bottom: var(--b-c); } > :last-child { display: none; } } .blc .table-title > :first-child { transform: translate(0, -30px); z-index: 2; } .tx-m .table-title > :first-child { text-align: right; transform: translate(100%, -30px); z-index: 2; } .table-title > :first-child:has(:checked) { border-color: var(--darken-pri); } .table-container:has(input:checked) { z-index: 2; } } .page-title { display: block; font-size: var(--t-text); font-weight: var(--bold); } .con-wrap:has(.detail.table-container) { gap: 0; } .detail.table-container:has(+ .page-title) { margin: var(--br) 0; } .detail.table-container + .page-title { margin-bottom: var(--br); } .detail.table-container:has(+ .detail.table-container) { border-bottom-left-radius: 0; border-bottom-right-radius: 0; border-bottom: 0; > :last-child { padding-bottom: var(--br); border-bottom: var(--b-c); } } .detail.table-container + .detail.table-container { border-top-left-radius: 0; border-top-right-radius: 0; border-top: 0; } .detail .row { border-bottom: none; height: auto; * { word-wrap: break-word; white-space: unset; } :first-child { grid-column: span 12; } :last-child { grid-column: span 12; } &:last-child { padding-bottom: 0; } } .detail .row.tri { :first-child { font-weight: var(--bold); grid-column: span 2; } :nth-child(2) { grid-column: span 5; } :last-child { grid-column: span 5; } } .nft-detail { grid-template-columns: 1fr; grid-template-rows: repeat(3, auto); gap: var(--br); } .nft-detail > img { grid-column: 1 / 2; grid-row: 2 / 3; border-radius: var(--br); } .nft-detail > .nft-title { grid-column: 1 / 2; } .nft-detail > .table-container { grid-column: 1 / 2; } .table-search{ padding-top: 0; > :first-child { display: none; } > :last-child { display: none; } } .table-search a, .table-search p { padding: 5px 16px; } .blc-num, .tx-hash, .blc-hash, .acc-hash, .token-id { color: var(--darken-pri); cursor: pointer; } .main > header { padding: 0; } .search-area { width: 100vw; padding: var(--pad); padding-bottom: 0; background-color: #f6f7fe; } .search-container > * { font-size: 16px; } .search-container > :first-child { grid-column: span 12; border-radius: var(--br); ::placeholder { color: var(--lightest-pri) } } .search-container > :last-child { display: none; } .err-wrap { display: flex; flex-direction: column; align-items: center; > :first-child { font-size: 32px; } > :nth-child(2) { font-size: var(--t-text); } } nav { flex-wrap: wrap; } ================================================ FILE: modules/lmscan-frontend/assets/css/reset.css ================================================ /* http://meyerweb.com/eric/tools/css/reset/ v2.0 | 20110126 License: none (public domain) */ html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td, article, aside, canvas, details, embed, figure, figcaption, footer, header, hgroup, menu, nav, output, ruby, section, summary, time, mark, audio, video { margin: 0; padding: 0; border: 0; font-size: 100%; font: inherit; vertical-align: baseline; } /* HTML5 display-role reset for older browsers */ article, aside, details, figcaption, figure, footer, header, hgroup, menu, nav, section { display: block; } body { line-height: 1; } ol, ul { list-style: none; } blockquote, q { quotes: none; } blockquote:before, blockquote:after, q:before, q:after { content: ''; content: none; } table { border-collapse: collapse; border-spacing: 0; } ================================================ FILE: modules/lmscan-frontend/assets/css/style.css ================================================ @charset "utf-8"; @import url(./footer.css); @import url(./loading.css); * { box-sizing: border-box; color: var(--b3); } html { font-family: 'Roboto', sans-serif; --primary: #3d5afe; --secondary: #ff7e40; --lightest-pri: #dadff7; --lighten-pri: #536dfe; --darken-pri: #304ffe; --lightest-sec: #ff9e80; --darken-sec: #ff3d00; --w0: #fff; --w1: #f5f5f5; --w2: #eee; --w3: #e0e0e0; --g: #9e9e9e; --b3: #757575; --b2: #616161; --b1: #424242; --b0: #000; --pad: 24px; --br: 8px; --t-text: 20px; font-size: 14px; --bold: 600; } a { cursor: pointer; } .main { background-color: #f6f7fe; } .main > * { display: flex; flex-flow: column; align-items: center; padding: var(--pad) 0; } .main > * > * { max-width: 1200px; width: calc(100vw - 2 * var(--pad)); } header { background-color: var(--w0); } .con-wrap { gap: 8px var(--pad); } .con-wrap > * { display: grid; grid-template-columns: repeat(12, 1fr); } .board-area { gap: var(--pad); margin-bottom: calc(var(--pad) - 8px); } .board-comp { background-color: var(--w0); border-radius: var(--br); border: 1px solid var(--lightest-pri); padding: 10px 16px; display: grid; > :first-child { font-weight: var(--bold); } > :nth-child(2) { font-size: var(--t-text); font-weight: var(--bold); color: var(--b1); } > :nth-child(3) { display: flex; align-items: center; gap: 11px; > :first-child { background-color: var(--w3); border-radius: var(--br); padding: 3px 5px; } > :last-child { font-weight: var(--bold); &.pos { color: var(--darken-pri); &::before { content: "+"; } } &.neg { color: var(--darken-sec); } } } } nav { display: flex; gap: calc(2 * var(--pad)); padding-top: var(--pad); padding-bottom: 12px; align-items: baseline; * { font-size: var(--t-text); color: var(--b1); font-weight: var(--bold); } > :first-child { font-size: 32px; cursor: pointer; } } .table-container { grid-column: span 6; background-color: var(--w0); border-radius: var(--br); display: flex; flex-direction: column; row-gap: 8px; border: 1px solid var(--lightest-pri); padding: var(--pad); position: relative; } .table-head { font-weight: var(--bold); } .table-container .row { display: grid; padding-bottom: 8px; border-bottom: 1px solid var(--lightest-pri); gap: var(--br); } .table-container .row > * { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .table-container.nfts .row, .table-container.nft-token .row { &.table-body { align-items: center; padding-top: 8px; } img { width: 48px; height: 48px; border-radius: var(--br); } } .table-container.nft .row { grid-template-columns: 3fr 1fr 2fr 3fr 3fr; } .table-title { display: flex; justify-content: space-between; > :first-child { font-size: var(--t-text); color: var(--b1); font-weight: var(--bold); } input { display: none; } > :last-child { color: var(--w0); background-color: var(--primary); border-radius: var(--br); padding: 4px 16px; } } .page-title { display: block; font-size: var(--t-text); font-weight: var(--bold); } .detail .row { grid-template-columns: repeat(12, 1fr); border: none; > :first-child { font-weight: var(--bold); grid-column: span 4; } :last-child { grid-column: span 8; } &:last-child { padding-bottom: 0; } } .detail .row.tri { :first-child { font-weight: var(--bold); grid-column: span 2; } :nth-child(2) { grid-column: span 5; } :last-child { grid-column: span 5; } } .inner { display: flex; flex-flow: column; gap: var(--pad); } .nft-detail > img { border: 1px solid var(--lightest-pri); border-radius: var(--br); grid-column: span 4; width: 100%; } .nft-detail > .nft-title { grid-column: span 8; } .nft-detail > .table-container { grid-column: span 8; } .table-search{ display: flex; gap: var(--br); width: 100%; padding-top: var(--pad); align-items: center; justify-content: center; font-family: JSDongkang,Roboto,sans-serif; } .table-search a, .table-search p { border-radius: var(--br); padding: 5px 16px; background-color: var(--lightest-pri); color: var(--darken-pri); &.dis { pointer-events: none; cursor: default; color: var(--g); } } .table-search p { cursor: pointer; } .type-search { border-radius: 5px; background-position: 50% 50%; padding: 3px 16px; width: 100px; background-color: var(--lightest-pri); } .blc-num, .tx-hash, .blc-hash, .acc-hash, .token-id { color: var(--darken-pri); cursor: pointer; } .search-container { display: grid; height: 3em; grid-template-columns: repeat(12, 1fr); } .search-container > * { font-size: 16px; } .search-container > :first-child { padding-left: var(--pad); grid-column: span 10; border-radius: var(--br) 0 0 var(--br); ::placeholder { color: var(--lightest-pri) } } .search-container > :last-child { display: flex; align-items: center; justify-content: center; cursor: pointer; border: 1px solid var(--darken-pri); border-radius: 0 var(--br) var(--br) 0; background-color: var(--darken-pri); color: var(--w0); grid-column: span 2; } .err-wrap { display: flex; flex-direction: column; align-items: center; justify-content: center; flex-grow: 1; * { color: var(--b1); } span { color: var(--darken-pri); cursor: pointer; } > :first-child { font-size: 32px; } > :nth-child(2) { font-size: var(--t-text); } } input { border: 1px solid var(--lightest-pri); &:focus { outline: none; border-color: var(--primary); color: var(--primary); } } ================================================ FILE: modules/lmscan-frontend/assets/css/tooltip.css ================================================ .row .cell[data-tooltip-text]:hover, [data-tooltip-text]:hover { position: relative; overflow: visible; } [data-tooltip-text]:after { -webkit-transition: bottom .3s ease-in-out, opacity .3s ease-in-out; -moz-transition: bottom .3s ease-in-out, opacity .3s ease-in-out; transition: bottom .3s ease-in-out, opacity .3s ease-in-out; background-color: rgba(0, 0, 0, 0.8); -webkit-box-shadow: 0px 0px 3px 1px rgba(50, 50, 50, 0.4); -moz-box-shadow: 0px 0px 3px 1px rgba(50, 50, 50, 0.4); box-shadow: 0px 0px 3px 1px rgba(50, 50, 50, 0.4); -webkit-border-radius: 5px; -moz-border-radius: 5px; border-radius: 5px; color: #FFFFFF; font-size: 14px; margin-bottom: 10px; padding: 7px 12px; position: absolute; width: auto; /* min-width: 50px; max-width: 300px; */ white-space: nowrap; word-wrap: break-word; z-index: 9999; opacity: 0; left: -9999px; top: 90%; content: attr(data-tooltip-text); } [data-tooltip-text]:hover:after { top: 130%; left: 0; opacity: 1; } ================================================ FILE: modules/lmscan-frontend/assets/index.html ================================================ LeisureMeta Chain Block Explorer
================================================ FILE: modules/lmscan-frontend/assets/load-main.js ================================================ (async () => { let m if (process.env.NODE_ENV === "production") { m = await import("../target/scala-3.4.1/leisuremeta-chain-lmscan-frontend-opt/main.js") } else { m = await import("../target/scala-3.4.1/leisuremeta-chain-lmscan-frontend-fastopt/main.js") } m.LmScan.launch("app-container") })() ================================================ FILE: modules/lmscan-frontend/package.json ================================================ { "name": "lmscan", "source": "assets/index.html", "browserslist": "> 0.5%, last 2 versions, not dead", "scripts": { "start": "parcel", "build": "parcel build" }, "devDependencies": { "@parcel/compressor-brotli": "^2.12.0", "@parcel/compressor-gzip": "^2.12.0", "@parcel/reporter-bundle-analyzer": "^2.12.0", "parcel": "^2.12.0", "process": "^0.11.10", "typescript": "^4.9.5" } } ================================================ FILE: modules/lmscan-frontend/project/build.properties ================================================ sbt.version=1.8.0 ================================================ FILE: modules/lmscan-frontend/readme.md ================================================ #### To start ```md # sbt 실행 cmd) sbt sbt) project lmscanFrontend sbt:leisuremeta-chain-lmscan-fronte) ~fastLinkJS # yarn 실행 cmd) cd modules/lmscan-frontend cmd) yarn start ``` # 배포 방법 ```md # 루트폴더 - fullLinkJS # lmscan-frontend 폴더 - yarn build ``` ================================================ FILE: modules/lmscan-frontend/src/main/scala/io/leisuremeta/chain/lmscan/frontend/LmscanFrontendApp.scala ================================================ package io.leisuremeta.chain.lmscan.frontend import cats.effect.IO import tyrian.* import scala.scalajs.js.annotation.* import io.leisuremeta.chain.lmscan.common.model._ import concurrent.duration.DurationInt @JSExportTopLevel("LmScan") object LmscanFrontendApp extends TyrianIOApp[Msg, Model]: def init(flags: Map[String, String]): (Model, Cmd[IO, Msg]) = (BaseModel(), Cmd.None) def update(model: Model): Msg => (Model, Cmd[IO, Msg]) = model.update def view(model: Model): Html[Msg] = model.view def subscriptions(model: Model): Sub[IO, Msg] = SearchView.detectSearch ++ Pagination.detectSearch ++ Sub.Batch( Sub.every[IO](1.second, "clock-ticks").map(UpdateTime.apply), Sub.every[IO](60.second, "refresh-ticks").map(_ => RefreshData), ) def router: Location => Msg = case loc: Location.Internal => loc.pathName match case s"/blcs/$page" => ToPage(BlcModel(page = page.toInt)) case s"/txs/$page" => ToPage(TxModel(page = page.toInt)) case s"/nfts/$page" => ToPage(NftModel(page = page.toInt)) case s"/accs/$page" => ToPage(AccModel(page = page.toInt)) case s"/tx/$hash" => ToPage(TxDetailModel(txDetail = TxDetail(hash = Some(hash)))) case s"/nft/$id/$page" => ToPage(NftTokenModel(page = page.toInt, id = id)) case s"/nft/$id" => ToPage(NftDetailModel(nftDetail = NftDetail(nftFile = Some(NftFileModel(tokenId = Some(id)))))) case s"/blc/$hash" => val p = loc.search.getOrElse("").split("=").last ToPage(BlcDetailModel(blcDetail = BlockDetail(hash = Some(hash)), page = p.toInt)) case s"/acc/$address" => val p = loc.search.getOrElse("").split("=").last ToPage(AccDetailModel(accDetail = AccountDetail(address = Some(address)), page = p.toInt)) case s"/vds" => ToPage(VdModel()) case s"/vds/$address" => val p = loc.search.getOrElse("").split("=").last ToPage(VdDetailModel(address = address, page = p.toInt)) case "/" => ToPage(BaseModel()) case "" => ToPage(BaseModel()) case _ => ToPage(ErrorModel(error = "")) case loc: Location.External => NavigateToUrl(loc.href) ================================================ FILE: modules/lmscan-frontend/src/main/scala/io/leisuremeta/chain/lmscan/frontend/components/BoardView.scala ================================================ package io.leisuremeta.chain.lmscan.frontend import tyrian.Html._ import tyrian.SVG._ import tyrian.* import io.leisuremeta.chain.lmscan.common.model._ object BoardView: def f(v: Long) = f"$v%,d" def f(v: BigDecimal) = f"$v%,.0f" val x = 400 val y = 100 def pricePan(summary: SummaryBoard, v: SummaryChart) = div(cls := "board-comp chart")( span("Price"), summary.today.lmPrice match case None => LoaderView.view case Some(v) => span("$" + v.toString.take(6)) , (summary.today.lmPrice, summary.yesterday.lmPrice) match case (Some(v), Some(w)) => val diff = (v - w) / w * 100 div( span("24H"), span(cls := s"${if diff >= 0 then "pos" else "neg"}")(f"$diff%3.2f %%") ) case (_, _) => div() , makeChart(v.list.map(vv => vv.lmPrice.getOrElse(0.0)).reverse.take(144).toList, x, y), ) def txPan(summary: SummaryBoard, v: SummaryChart) = div(cls := "board-comp chart")( span("Total Transaction"), span(f(summary.today.totalTxSize.getOrElse(0L))), drawDiff(summary.today.totalTxSize, summary.yesterday.totalTxSize, v => v.toString), makeChart(v.list.map(vv => vv.totalTxSize.map(_.toDouble).getOrElse(0.0)).reverse.toList, x, y), ) def balPan(summary: SummaryBoard, v: SummaryChart) = def f(v: BigDecimal) = f"${v / Math.pow(10, 18)}%,.0f" div(cls := "board-comp")( span("Total Balance in LMC"), span(f(summary.today.totalBalance.getOrElse(BigDecimal(0)))) , drawDiff(summary.today.totalBalance, summary.yesterday.totalBalance, f), makeChart(v.list.map(vv => vv.totalBalance.getOrElse(BigDecimal(0)).toDouble).reverse.take(144).toList, x, y), ) def drawDiff[T](oT: Option[T], oY: Option[T], f: T => String = (v: T) => v.toString)(using nu: Numeric[T]) = (oT, oY) match case (Some(t), Some(y)) => val diff = nu.minus(t, y) div( span("24H"), span(cls := s"${if nu.gteq(t, y) then "pos" else "neg"}")(f(diff)) ) case (Some(v), _) => div(span("24H"), span(f(v))) case (_, Some(v)) => div(span("24H"), span(f(v))) case _ => div(span("24H")) def accPan(summary: SummaryBoard, v: SummaryChart) = div(cls := "board-comp")( span("Total Accounts"), span(f(summary.today.totalAccounts.getOrElse(0L))), drawDiff(summary.today.totalAccounts, summary.yesterday.totalAccounts), makeChart(v.list.map(vv => vv.totalAccounts.getOrElse(0L).toDouble).reverse.take(144).toList, x, y), ) def normalize(xs: List[Double]): List[Double] = if xs.isEmpty then return xs val max = xs.max val min = xs.min val d = max - min if d == 0 then xs.map(_ => 0.5) else xs.map(x => (x - min) / d) def makeLine(ys: List[Double], x: Long, h: Long): String = val dx = x / (ys.size - 1).toDouble normalize(ys).zipWithIndex .map: case (y, i) if i == 0 => s"M0,${h - y * h} L${i * dx},${h - y * h}" case (y, i) => s"L${i * dx},${h - y * h}" .mkString("") def makeArea(ys: List[Double], x: Long, h: Long): String = val dx = x / (ys.size - 1).toDouble normalize(ys).zipWithIndex .map: case (y, i) if i == 0 => s"0,${h} 0,${h - y * h} ${i * dx},${h - y * h}" case (y, i) => s" ${i * dx},${h - y * h}" .mkString("", "", s" ${x},${h}") def makeChart(ys: List[Double], x: Long, h: Long): Html[Msg] = svg(viewBox := s"0, 0, ${x}, ${h}")( path(d := makeLine(ys, x, h)), polyline(points := makeArea(ys, x, h)) ) def marketCap(summary: SummaryBoard, v: SummaryChart) = div(cls := "board-comp")( span("Market cap"), span("$" + f(summary.today.marketCap.getOrElse(BigDecimal(0)))), drawDiff(summary.today.marketCap, summary.yesterday.marketCap, f), makeChart(v.list.map(vv => vv.marketCap.getOrElse(BigDecimal(0)).toDouble).reverse.take(144).toList, x, y), ) def cirSupply(summary: SummaryBoard, v: SummaryChart) = def f(v: BigDecimal) = f"$v%,.0f" div(cls := "board-comp")( span("Circulation Supply"), span(f(summary.today.cirSupply.getOrElse(BigDecimal(0))) + " LM"), drawDiff(summary.today.cirSupply, summary.yesterday.cirSupply, f), makeChart(v.list.map(vv => vv.cirSupply.getOrElse(BigDecimal(0)).toDouble).reverse.take(144).toList, x, y), ) def totalBlock(summary: SummaryBoard, v: SummaryChart) = div(cls := "board-comp")( span("Total Blocks"), span(f(summary.today.blockNumber.getOrElse(0L))), drawDiff(summary.today.blockNumber, summary.yesterday.blockNumber, f), makeChart(v.list.map(vv => vv.blockNumber.getOrElse(0L).toDouble).reverse.take(144).toList, x, y), ) def totalNft(summary: SummaryBoard, v: SummaryChart) = div(cls := "board-comp")( span("Total NFTs"), span(f(summary.today.totalNft.getOrElse(0L))), drawDiff(summary.today.totalNft, summary.yesterday.totalNft, f), makeChart(v.list.map(vv => vv.totalNft.getOrElse(0L).toDouble).reverse.take(144).toList, x, y), ) def view(model: BaseModel): Html[Msg] = (model.summary, model.chartData) match case (Some(summary), Some(v)) => div(cls := "board-area")( pricePan(summary, v), marketCap(summary, v), cirSupply(summary, v), balPan(summary, v), txPan(summary, v), accPan(summary, v), totalBlock(summary, v), totalNft(summary, v), ) case _ => div(cls := "board-area")(LoaderView.view) ================================================ FILE: modules/lmscan-frontend/src/main/scala/io/leisuremeta/chain/lmscan/frontend/components/Footer.scala ================================================ package io.leisuremeta.chain.lmscan.frontend import tyrian.Html.* import tyrian.* object Footer: def view(): Html[Msg] = div( div(cls := "footer-left")( p( strong("LMSCAN"), span("for LeisureMeta Chain"), ), strong("Copyright © LM LLC All Rights Reserved"), ), div(cls := "footer-right")( dl( dt("Company Family"), dd( a( href := "https://themoonlabs.net/", target := "_blank", cls := "icon-The-Moon-Labs", )("The Moon Labs"), a( href := "https://leisuremeta.io/", target := "_blank", cls := "icon-leisuremetaverse", )("LeisureMetaverse"), a( href := "https://www.ilikelm.com/", target := "_blank", cls := "icon-lm-nova", )("ILIKELM"), a( href := "https://www.themoonent.com/", target := "_blank", cls := "icon-tme", )("THE MOON ENTERTAINMENT"), ), ), dl( dt("Social"), dd( a( href := "https://github.com/leisuremeta/leisuremeta-chain", target := "_blank", cls := "icon-github", )("Github"), a( href := "https://t.me/LeisureMeta_Official", target := "_blank", cls := "icon-telegram", )("Telegram"), a( href := "https://twitter.com/LeisureMeta_LM", target := "_blank", cls := "icon-twitter", )("Twitter"), a( href := "https://discord.gg/leisuremetaofficial", target := "_blank", cls := "icon-discord", )("Discord"), ), ), ), ) ================================================ FILE: modules/lmscan-frontend/src/main/scala/io/leisuremeta/chain/lmscan/frontend/components/Loader.scala ================================================ package io.leisuremeta.chain.lmscan.frontend import tyrian.Html.* import tyrian.* object LoaderView: def view: Html[Msg] = div(cls := "loader-case")( div(cls := "loader")(), ) ================================================ FILE: modules/lmscan-frontend/src/main/scala/io/leisuremeta/chain/lmscan/frontend/components/NavBar.scala ================================================ package io.leisuremeta.chain.lmscan.frontend import tyrian.Html.* import tyrian.* object NavBar: val main = "Dashboard" val blc = "Blocks" val tx = "Transactions" val acc = "Accounts" val nft = "NFTs" val vd = "Validators" def view(model: Model): Html[Msg] = nav()( p(onClick(ToPage(BaseModel())))("LM SCAN") :: List( (main, ToPage(BaseModel())), (blc, ToPage(BlcModel(page = 1))), (tx, ToPage(TxModel(page = 1))), (acc, ToPage(AccModel(page = 1))), (nft, ToPage(NftModel(page = 1))), (vd, ToPage(VdModel())), ).map((name, msg) => a( cls := isActive(name, model), onClick(msg), )(span(name)) ), ) def isActive(name: String, model: Model) = model match case m: BaseModel => if name == main then "active" else "" case _: BlcModel if name == blc => "active" case _: BlcDetailModel if name == blc => "active" case _: TxModel if name == tx => "active" case _: TxDetailModel if name == tx => "active" case _: AccModel if name == acc => "active" case _: AccDetailModel if name == acc => "active" case _: NftModel if name == nft => "active" case _: NftTokenModel if name == nft => "active" case _: NftDetailModel if name == nft => "active" case _: VdModel if name == vd => "active" case _ => "" ================================================ FILE: modules/lmscan-frontend/src/main/scala/io/leisuremeta/chain/lmscan/frontend/components/SearchView.scala ================================================ package io.leisuremeta.chain.lmscan.frontend import tyrian.Html.* import tyrian.* import org.scalajs.dom._ import cats.effect.IO object SearchView: def view(model: Model): Html[Msg] = div(cls := "search-area")( div(cls := "search-container")( input( id := "global-search", onInput(s => GlobalInput(s)), value := s"${model.global.searchValue}", cls := "search-text", `placeholder` := ( "Search by address, transaction, NFT, block", ), ), div( onClick(GlobalSearch), cls := "search-icon", )( "Search >>" ), ), ) def detectSearch = Sub.Batch( Option(document.getElementById("global-search")) match case None => Sub.None case Some(el) => Sub.fromEvent[IO, KeyboardEvent, Msg]("keyup", el): e => e.key match case "Enter" => Some(GlobalSearch) case _ => None , ) ================================================ FILE: modules/lmscan-frontend/src/main/scala/io/leisuremeta/chain/lmscan/frontend/components/common/Body.scala ================================================ package io.leisuremeta.chain.lmscan.frontend import tyrian.Html.* import tyrian.* import io.leisuremeta.chain.lmscan.common.model._ object Body: def blocks(payload: List[BlockInfo], g: GlobalModel) = payload.map(v => div(cls := "row table-body")( gen.cell( Cell.BLOCK_NUMBER(v.hash, v.number), Cell.AGE(v.createdAt, g.current), Cell.BLOCK_HASH(v.hash), Cell.PlainLong(v.txCount), ), ), ) def accs(payload: List[AccountInfo]) = payload.map(v => div(cls := "row table-body")( gen.cell( Cell.ACCOUNT_HASH(v.address), Cell.Balance(v.balance), Cell.PriceS(v.value), Cell.DateS(v.updated), ), ), ) def txRow = (payload: List[TxInfo], g: GlobalModel) => payload.map(v => div(cls := "row table-body")( gen.cell( Cell.TX_HASH(v.hash), Cell.PlainLong(v.blockNumber), Cell.AGE(v.createdAt, g.current), Cell.ACCOUNT_HASH(v.signer), Cell.PlainStr(v.subType), ), ), ) def boardTxRow = (payload: List[TxInfo], g: GlobalModel) => payload .map(v => div(cls := "row table-body")( gen.cell( Cell.TX_HASH(v.hash), Cell.AGE(v.createdAt, g.current), Cell.ACCOUNT_HASH(v.signer), ), ), ) def nfts = (payload: List[NftInfoModel]) => payload .map(v => div(cls := "row table-body")( gen.cell( Cell.ImageS(v.thumbUrl), Cell.NftToken(v), Cell.PlainStr(v.totalSupply), Cell.DateS(v.startDate), Cell.DateS(v.endDate), ), ), ) def nftToken = (payload: List[NftSeasonModel]) => payload .map(v => div(cls := "row table-body")( gen.cell( Cell.NftDetail(v, v.nftName), Cell.PlainStr(v.collection), Cell.NftDetail(v, v.tokenId), Cell.PlainStr(v.creator), Cell.PlainStr(v.rarity), ), ), ) def nft = (payload: List[NftActivity], g: GlobalModel) => payload .map(v => div(cls := "row table-body")( gen.cell( Cell.TX_HASH(v.txHash), Cell.AGE(v.createdAt, g.current), Cell.PlainStr(v.action), Cell.ACCOUNT_HASH(v.fromAddr), Cell.ACCOUNT_HASH(v.toAddr), ), ), ) def vds = (payload: List[NodeValidator.Validator]) => payload .map(v => div(cls := "row table-body")( span( cls := s"cell acc-hash", onClick(ToPage(VdDetailModel(address = v.address.getOrElse("")))), )( v.address.getOrElse(""), ), span(v.power.getOrElse(0.0).toString + "%"), span(f"${v.cnt.getOrElse(0L)}%,d"), ), ) ================================================ FILE: modules/lmscan-frontend/src/main/scala/io/leisuremeta/chain/lmscan/frontend/components/common/Head.scala ================================================ package io.leisuremeta.chain.lmscan.frontend import tyrian.Html.* object Head: def template(hs: List[String], cs: String = "") = div(cls := s"row table-head $cs")(hs.map(span(_))) val block = template(List("Block", "Age", "Block hash", "TxN")) val accs = template(List("Address", "Balance", "Value", "Last Seen")) val nfts = template(List("", "Season", "Total Supply", "Sale Started", "Sale Ended")) val nftToken = template(List("NFT", "Collection", "Token ID", "Creator", "Rarity")) val nft = template(List("Tx Hash", "Timestamp", "Action", "From", "To")) val tx = template(List("Tx Hash", "Block", "Age", "Signer", "Subtype")) val tx2 = template(List("Tx Hash", "Block", "Age", "Signer", "Subtype", "Value")) val tx_dashBoard = template(List("Tx Hash", "Age", "Signer")) val vds = template(List("Address", "Voting Power", "Total Block Proposed")) ================================================ FILE: modules/lmscan-frontend/src/main/scala/io/leisuremeta/chain/lmscan/frontend/components/common/Pagination.scala ================================================ package io.leisuremeta.chain.lmscan.frontend import tyrian.Html.* import tyrian.* import io.leisuremeta.chain.lmscan.common.model.* import org.scalajs.dom._ import cats.effect.IO object Pagination: def toInt(s: String) = try Some(Integer.parseInt(s)) catch case _ => None def checkAndMake(s: String, last: Int, cur: Int) = val v = s.toIntOption.getOrElse(cur) if v < 1 then 1 else if v > last then last else v def view[T](model: PageModel) = val curPage = model.page val totalPage = model.data match case None => 0 case Some(v) => v match case PageResponse(totalCount, totalPages, payload) => totalPages.toInt def goTo(v: Int) = model match case _: BlcModel => ToPage(BlcModel(page = v)) case _: TxModel => ToPage(TxModel(page = v)) case _: AccModel => ToPage(AccModel(page = v)) case _: NftModel => ToPage(NftModel(page = v)) case n: BlcDetailModel=> ToPage(n.copy(page = v)) case n: AccDetailModel=> ToPage(n.copy(page = v)) case n: NftTokenModel => ToPage(n.copy(page = v)) case n: VdDetailModel => ToPage(n.copy(page = v)) def isDis(condition: Boolean) = if condition then "dis" else "" def toggleInput = TogglePageInput(!model.pageToggle) div(cls := s"table-search")( a( cls := s"${isDis(1 == curPage)}", onClick(goTo(1)), )("First"), a( cls := s"${isDis(curPage <= 1)}", onClick(goTo(curPage - 1)), )("<"), model.pageToggle match case true => input( id := "list-search", onInput(s => UpdateSearch(checkAndMake(s, totalPage, curPage))), value := s"${curPage}", cls := "type-search", ) case false => p(onClick(toggleInput))(s"${curPage} of ${totalPage}") , a( cls := s"${isDis(curPage >= totalPage)}", onClick(goTo(curPage + 1)), )(">"), a( cls := s"${isDis(curPage == totalPage)}", onClick(goTo(totalPage)), )("Last"), ) def detectSearch = Sub.Batch( Option(document.getElementById("list-search")) match case None => Sub.None case Some(el) => Sub.fromEvent[IO, KeyboardEvent, Msg]("keyup", el): e => e.key match case "Enter" => Some(ListSearch) case _ => None , ) ================================================ FILE: modules/lmscan-frontend/src/main/scala/io/leisuremeta/chain/lmscan/frontend/components/common/Table.scala ================================================ package io.leisuremeta.chain.lmscan package frontend import tyrian.Html.* import tyrian.* import common.model.* object Table: def mainView(model: BaseModel): Html[Msg] = div(cls := "table-area main-table")( div( cls := "blc table-container", )( div(cls := "table-title")( label(text("Latest Blocks"), input(name := "toggle-main", typ := "radio", checked)), a(onClick(ToPage(BlcModel(page = 1))))("More"), ) :: (model.blcs match case None => List(LoaderView.view) case Some(v) => Head.block :: Body.blocks(v.payload.toList, model.global)) ), div( cls := "tx-m table-container", )( div(cls := "table-title")( label(text("Latest transactions"), input(name := "toggle-main", typ := "radio")), a(onClick(ToPage(TxModel(page = 1))))("More"), ) :: (model.txs match case None => List(LoaderView.view) case Some(v) => Head.tx_dashBoard :: Body.boardTxRow(v.payload.toList, model.global)) ), ) def view(model: BlcModel) = div(cls := "table-container blc")( Head.block :: (model.data match case Some(v) => Body.blocks(v.payload.toList, model.global).appended(Pagination.view(model)) case None => List(LoaderView.view)), ) def view(model: AccModel) = div(cls := "table-container accs")( model.data match case None => List(LoaderView.view) case Some(data) => Head.accs :: Body.accs(data.payload.toList).appended(Pagination.view(model)) ) def view(model: NftModel) = div(cls := "table-container nfts")( nft(model.data), Pagination.view(model), model.data match case None => LoaderView.view case Some(_) => div(), ) def view(model: NftTokenModel) = div(cls := "table-container nft-token")( nftToken(model.data), Pagination.view(model), model.data match case None => LoaderView.view case Some(_) => div(), ) def view(model: TxModel): Html[Msg] = div(cls := "table-container tx")( Head.tx :: (model.data match case None => List(LoaderView.view) case Some(v) => Body.txRow(v.payload.toList, model.global).appended(Pagination.view(model)) ), ) def view(model: BlcDetailModel): Html[Msg] = div(cls := "table-container tx")( Head.tx :: Body.txRow(model.blcDetail.payload.toList, model.global).appended(Pagination.view(model)) ) def view(model: AccDetailModel): Html[Msg] = div(cls := "table-container tx")( Head.tx :: Body.txRow(model.accDetail.payload.toList, model.global).appended(Pagination.view(model)) ) def view(model: NftDetailModel): Html[Msg] = div(cls := "table-container nft")( model.nftDetail.activities match case None => List(Head.nft, LoaderView.view) case Some(v) => Head.nft :: Body.nft(v.toList, model.global), ) def view(model: VdModel): Html[Msg] = div(cls := "table-container vds")( Head.vds :: Body.vds(model.payload) ) def view(model: VdDetailModel): Html[Msg] = div(cls := "table-container blc")( Head.block :: Body.blocks(model.blcs.payload.toList, model.global).appended(Pagination.view(model)) ) def nft(list: Option[PageResponse[NftInfoModel]]) = div( list match case Some(v) => Head.nfts :: Body.nfts(v.payload.toList) case None => List(Head.nfts), ) def nftToken(list: Option[PageResponse[NftSeasonModel]]) = div( list match case Some(v) => Head.nftToken :: Body.nftToken(v.payload.toList) case None => List(Head.nftToken), ) ================================================ FILE: modules/lmscan-frontend/src/main/scala/io/leisuremeta/chain/lmscan/frontend/components/detail/AccountDetailTable.scala ================================================ package io.leisuremeta.chain.lmscan.frontend import tyrian.Html.* import tyrian.* import io.leisuremeta.chain.lmscan.common.model.* object AccountDetailTable: def view(data: AccountDetail) = div( cls := "detail table-container", )( div(cls := "row")( gen.cell( Cell.Head("Account", "cell type-detail-head"), Cell.PlainStr(data.address, "cell type-detail-body"), ), ), div(cls := "row")( gen.cell( Cell.Head("Balance", "cell type-detail-head"), Cell.Balance( data.balance, "cell type-detail-body", ), ), ), div(cls := "row")( gen.cell( Cell.Head("Value", "cell type-detail-head"), Cell.PriceS( data.value, "cell type-detail-body", ), ), ), ) ================================================ FILE: modules/lmscan-frontend/src/main/scala/io/leisuremeta/chain/lmscan/frontend/components/detail/blockDetailTable.scala ================================================ package io.leisuremeta.chain.lmscan.frontend import tyrian.Html.* import tyrian.* import io.leisuremeta.chain.lmscan.common.model.BlockDetail object BlockDetailTable: def view(data: BlockDetail) = div( cls := "detail table-container", )( div(cls := "row")( gen.cell( Cell.Head("Block Number", "cell type-detail-head"), Cell.PlainStr(data.number, "cell type-detail-body"), ), ), div(cls := "row")( gen.cell( Cell.Head("Timestamp", "cell type-detail-head"), Cell.DATE(data.timestamp, "cell type-detail-body"), ), ), div(cls := "row")( gen.cell( Cell.Head("Block hash", "cell type-detail-head"), Cell.PlainStr(data.hash, "cell type-detail-body"), ), ), div(cls := "row")( gen.cell( Cell.Head("Parent hash", "cell type-detail-head"), Cell.BLOCK_HASH(data.parentHash), ), ), div(cls := "row")( gen.cell( Cell.Head("Transaction count", "cell type-detail-head"), Cell.PlainStr(data.txCount, "cell type-detail-body"), ), ), ) ================================================ FILE: modules/lmscan-frontend/src/main/scala/io/leisuremeta/chain/lmscan/frontend/components/detail/nftDetailTable.scala ================================================ package io.leisuremeta.chain.lmscan.frontend import tyrian.Html.* import tyrian.* import io.leisuremeta.chain.lmscan.common.model.* object NftDetailTable: def view(data: NftDetail) = val nftFile = data.nftFile.getOrElse(NftFileModel()) div(cls := "nft-detail")( gen.cell(Cell.Image(nftFile.nftUri))(0), div(cls := "detail table-container")( div(cls := "row")( span("NFT Name"), span(nftFile.nftName.getOrElse("-")), ), div(cls := "row")( span("Collection Name"), span(nftFile.collectionName.getOrElse("-")), ), div(cls := "row")( span("Token ID"), span(nftFile.tokenId.getOrElse("-")), ), div(cls := "row")( span("Definition ID"), span(nftFile.tokenDefId.getOrElse("-")), ), div(cls := "row")( span("Rarity"), span(nftFile.rarity.getOrElse("-")), ), div(cls := "row")( span("Creator"), span(nftFile.creator.getOrElse("-")), ), div(cls := "row")( span("Owner"), ParseHtml.fromAccHash(nftFile.owner), ), div(cls := "row")( span("Issue Date"), ParseHtml.fromDate(nftFile.createdAt), ), ), ) ================================================ FILE: modules/lmscan-frontend/src/main/scala/io/leisuremeta/chain/lmscan/frontend/components/detail/txDetailTableCommon.scala ================================================ package io.leisuremeta.chain package lmscan.frontend import tyrian.Html.* import tyrian.* import lib.datatype.BigNat import api.model._ import api.model.token._ import api.model.Signed.TxHash import api.model.Transaction._ import api.model.Transaction.AccountTx._ import api.model.Transaction.TokenTx._ import api.model.Transaction.GroupTx._ import api.model.Transaction.RewardTx._ import api.model.Transaction.AgendaTx._ import api.model.Transaction.CreatorDaoTx._ import api.model.Transaction.VotingTx._ object TxDetailTableCommon: def row(head: String, value: String) = div(cls := "row")(span(head), span(value)) def rowCustom(head: String, custom: Html[Msg]) = div(cls := "row")(span(head), custom) def rowInputHead = row("Input", "Transaction Hash") def rowTriOutput(s: String) = div(cls := "row tri")(span("Output"), span("To"), span(s)) def rowInputBody(input: TxHash, i: Int = 0) = div(cls := "row")(span(s"${i + 1}"), ParseHtml.fromTxHash(input.toUInt256Bytes.toHex)) def rowAccToken(output: Account, v: TokenId) = div(cls := "row tri")(span("1"), ParseHtml.fromAccHash(Some(output.toString)), ParseHtml.fromTokenId(v.toString)) def rowAccBal(outputs: (Account, BigNat), i: Int) = div(cls := "row tri")( span(s"${i + 1}"), ParseHtml.fromAccHash(Some(outputs._1.toString())), ParseHtml.fromBal(Some(BigDecimal(outputs._2.toString))), ) def getNFromId(id: String) = id.drop(16).dropWhile(c => !c.isDigit || c == '0').prepended('#') def view(data: Option[TransactionWithResult]) = val result = for tx <- data res = tx.signedTx.value match case t: Transaction.TokenTx => tokenView(t) case t: Transaction.AccountTx => accountView(t) case t: Transaction.GroupTx => groupView(t) case t: Transaction.RewardTx => rewardView(t) case t: Transaction.AgendaTx => agendaView(t, tx.result) case t: Transaction.VotingTx => votingView(t) case t: Transaction.CreatorDaoTx => creatorDaoView(t) yield res result.getOrElse(List(div(""))).prepended(div(cls := "page-title")("Transaction Values")) def tokenView(tx: TokenTx) = tx match case nft: DefineToken => div(cls := "detail table-container")( row("Definition ID", nft.definitionId.toString), row("Name", nft.name.toString()), ) :: List() case nft: DefineTokenWithPrecision => div(cls := "detail table-container")( row("Definition ID", nft.definitionId.toString), row("Name", nft.name.toString()), ) :: List() case nft: MintNFT => div(cls := "detail table-container")( rowCustom("Token ID", ParseHtml.fromTokenId(nft.tokenId.toString)), // row("Collection Name", nft.collectionName), row("No", getNFromId(nft.tokenId.toString)), rowCustom("Data URI", a(href := nft.dataUrl.toString, target := "_blank")(nft.dataUrl.toString)), row("Content Hash", nft.contentHash.toHex), row("Rarity", nft.rarity.toString), ) :: List() case nft: MintNFTWithMemo => div(cls := "detail table-container")( rowCustom("Token ID", ParseHtml.fromTokenId(nft.tokenId.toString)), // row("Collection Name", nft.collectionName), row("No", getNFromId(nft.tokenId.toString)), rowCustom("Data URI", a(href := nft.dataUrl.toString, target := "_blank")(nft.dataUrl.toString)), row("Content Hash", nft.contentHash.toHex), row("Rarity", nft.rarity.toString), ) :: List() case nft: UpdateNFT => div(cls := "detail table-container")( rowTriOutput("Token ID"), rowAccToken(nft.output, nft.tokenId), ) :: List() case nft: TransferNFT => div(cls := "detail table-container")( rowInputHead, rowInputBody(nft.input), ) :: div(cls := "detail table-container")( rowTriOutput("Token ID"), rowAccToken(nft.output, nft.tokenId), ) :: List() case nft: BurnNFT => div(cls := "detail table-container")( row("Definition ID", nft.definitionId.toString), ) :: List() case nft: EntrustNFT => div(cls := "detail table-container")( rowInputHead, rowInputBody(nft.input), ) :: div(cls := "detail table-container")( rowTriOutput("Token ID"), rowAccToken(nft.to, nft.tokenId), ) :: List() case nft: DisposeEntrustedNFT => div(cls := "detail table-container")( rowInputHead, rowInputBody(nft.input), ) :: div(cls := "detail table-container")( rowTriOutput("Token ID"), rowAccToken(nft.output.get, nft.tokenId), ) :: List() case tx: MintFungibleToken => div(cls := "detail table-container")( rowTriOutput("Value") :: tx.outputs.zipWithIndex .map((d, i) => rowAccBal(d, i)) .toList, ) :: List() case tx: TransferFungibleToken => div(cls := "detail table-container")( row("Definition ID", tx.tokenDefinitionId.toString), ) :: div(cls := "detail table-container")( rowInputHead :: tx.inputs .zipWithIndex .toList .sortBy(_._2) .map((a, i) => rowInputBody(a, i)) ) :: div(cls := "detail table-container")( rowTriOutput("Value") :: tx.outputs.zipWithIndex .map((d, i) => rowAccBal(d, i)) .toList ) :: List() case tx: EntrustFungibleToken => div(cls := "detail table-container")( rowInputHead :: tx.inputs.zipWithIndex .map((a, i) => rowInputBody(a, i)) .toList ) :: div(cls := "detail table-container")( rowTriOutput("Value"), rowAccBal((tx.to, tx.amount), 0), ) :: List() case tx: DisposeEntrustedFungibleToken => div(cls := "detail table-container")( rowInputHead :: tx.inputs.zipWithIndex .map((a, i) => rowInputBody(a, i)) .toList ) :: div(cls := "detail table-container")( rowTriOutput("Value") :: tx.outputs.zipWithIndex .map((d, i) => rowAccBal(d, i)) .toList ) :: List() case tx: BurnFungibleToken => div(cls := "detail table-container")( row("Ammount", tx.amount.toString), ) :: List() case tx: CreateSnapshots => div(cls := "detail table-container")( rowCustom("Definition ID", div(cls := "inner")(tx.definitionIds.map(di => span(di.toString)).toList)), ) :: List() def accountView(tx: AccountTx) = tx match case tx: CreateAccount => div(cls := "detail table-container")( rowCustom( "Account", ParseHtml.fromAccHash(Some(tx.account.toString)), ), ) :: List() case tx: UpdateAccount => div(cls := "detail table-container")( rowCustom( "Account", ParseHtml.fromAccHash(Some(tx.account.toString)), ), row("Ethereum Address", tx.ethAddress.get.toString), rowCustom( "Guardian", ParseHtml.fromAccHash(tx.guardian.map(_.toString), false), ), ) :: List() case tx: AddPublicKeySummaries => div(cls := "detail table-container")( rowCustom( "Account", ParseHtml.fromAccHash(Some(tx.account.toString)), ), row("PublicKey Summary", tx.summaries.keys.head.toBytes.toHex), ) :: List() case _ => List() def groupView(tx: GroupTx) = tx match case tx: CreateGroup => div(cls := "detail table-container")( row("Group ID", tx.groupId.toString), row("Coordinator Account", tx.coordinator.toString), ) :: List() case tx: AddAccounts => div(cls := "detail table-container")( row("Group ID", tx.groupId.toString), row("Account", tx.accounts.toList(0).toString()), ) :: List() def rewardView(tx: RewardTx) = tx match case tx: OfferReward => div(cls := "detail table-container")( row("Definition ID", tx.tokenDefinitionId.toString), ) :: div(cls := "detail table-container")( rowInputHead :: tx.inputs.zipWithIndex .map((a, i) => rowInputBody(a, i)) .toList ) :: List() div(cls := "detail table-container")( rowTriOutput("Value") :: tx.outputs.zipWithIndex .map((d, i) => rowAccBal(d, i)) .toList, ) :: List() case tx: RegisterDao => div(cls := "detail table-container")( row("Group ID", tx.groupId.toString), row("DAO Account Name", tx.daoAccountName.toString), if tx.moderators.size > 0 then row("Moderators", tx.moderators.mkString(",")) else Empty, ) :: List() case tx: UpdateDao => div(cls := "detail table-container")( row("Group ID", tx.groupId.toString), ) :: List() case _ => div(cls := "detail table-container")( "" ) :: List() def agendaView(tx: AgendaTx, result: Option[TransactionResult]) = tx match case tx: SuggestSimpleAgenda => div(cls := "detail table-container")( row("Title", tx.title.toString), row("Voting Token", tx.votingToken.toString), rowCustom("Vote Start", ParseHtml.fromDate(Some(tx.voteStart.getEpochSecond()))), rowCustom("Vote End", ParseHtml.fromDate(Some(tx.voteEnd.getEpochSecond()))), rowCustom("Vote End", p(tx.voteEnd.toString())), ) :: div(cls := "detail table-container")( row("vote option", "selected") :: tx.voteOptions .map(a => row(a._1.toString, a._2.toString)) .toList ) :: List() case tx: VoteSimpleAgenda => div(cls := "detail table-container")( rowCustom( "Agenda Tx Hash", ParseHtml.fromTxHash(tx.agendaTxHash.toUInt256Bytes.toHex), ), row("Selected Option", tx.selectedOption.toString), ) :: List() def votingView(tx: VotingTx) = tx match case tx: CreateVoteProposal => div(cls := "detail table-container")( row("Proposal ID", tx.proposalId.value.toString), row("Title", tx.title.value), row("Description", tx.description.value), rowCustom("Vote Start", ParseHtml.fromDate(Some(tx.voteStart.getEpochSecond()))), rowCustom("Vote End", ParseHtml.fromDate(Some(tx.voteEnd.getEpochSecond()))), rowCustom("Vote Type", p(tx.voteType.name)), rowCustom("Voting Power", div(cls := "inner")(tx.votingPower.keys.map(k => p(k.toString)).toList)), ) :: div(cls := "detail table-container")( rowCustom("Voting Option", div(cls := "inner")(tx.voteOptions.map(a => row(a._1.toString, a._2.toString)).toList)) ) :: List() case tx: CastVote => div(cls := "detail table-container")( row("Proposal Id", tx.proposalId.value.toString), row("Selected Option", tx.selectedOption.toString), ) :: List() case tx: TallyVotes => div(cls := "detail table-container")( row("Proposal Id", tx.proposalId.value.toString), ) :: List() def creatorDaoView(tx: CreatorDaoTx) = tx match case tx: CreateCreatorDao => div(cls := "detail table-container")( row("ID", tx.id.toString), row("Name", tx.name.toString), row("Description", tx.description.toString), row("Founder", tx.founder.toString), row("Coordinator", tx.coordinator.toString), ) :: List() case tx: UpdateCreatorDao => div(cls := "detail table-container")( row("ID", tx.id.toString), row("Name", tx.name.toString), row("Description", tx.description.toString), ) :: List() case tx: DisbandCreatorDao => div(cls := "detail table-container")( row("ID", tx.id.toString), ) :: List() case tx: ReplaceCoordinator => div(cls := "detail table-container")( row("ID", tx.id.toString), row("New Coordinator", tx.newCoordinator.utf8.value), ) :: List() case tx: AddMembers => div(cls := "detail table-container")( row("ID", tx.id.toString), rowCustom("Members", div(cls := "inner")(tx.members.map(a => div(a.toString)).toList)) ) :: List() case tx: RemoveMembers => div(cls := "detail table-container")( row("ID", tx.id.toString), rowCustom("Members", div(cls := "inner")(tx.members.map(a => div(a.toString)).toList)) ) :: List() case tx: PromoteModerators => div(cls := "detail table-container")( row("ID", tx.id.toString), rowCustom("Members", div(cls := "inner")(tx.members.map(a => div(a.toString)).toList)) ) :: List() case tx: DemoteModerators => div(cls := "detail table-container")( row("ID", tx.id.toString), rowCustom("Members", div(cls := "inner")(tx.members.map(a => div(a.toString)).toList)) ) :: List() ================================================ FILE: modules/lmscan-frontend/src/main/scala/io/leisuremeta/chain/lmscan/frontend/controllers/Model.scala ================================================ package io.leisuremeta.chain.lmscan package frontend import common.model.* import tyrian._ import cats.effect.IO import scalajs.js import io.circe.Decoder final case class GlobalModel( popup: Boolean = false, searchValue: String = "", current: js.Date = new js.Date(), ): def update(msg: GlobalMsg): GlobalModel = msg match case GlobalInput(s) => this.copy(searchValue = s) case UpdateTime(t) => this.copy(current = t) trait Model: val global: GlobalModel def view: Html[Msg] def url: String def update: Msg => (Model, Cmd[IO, Msg]) def toEmptyModel: EmptyModel = EmptyModel(global) trait PageModel extends Model with ApiModel: val page: Int val size: Int = 20 val searchPage: Int val data: Option[ApiModel] val pageToggle: Boolean case class EmptyModel( global: GlobalModel = GlobalModel(), ) extends Model: def view = DefaultLayout.view( this, LoaderView.view ) def url = "" def update: Msg => (Model, Cmd[IO, Msg]) = case ToPage(model) => model.update(Init) case NavigateToUrl(url) => (this, Nav.loadUrl(url)) case ErrorMsg => (ErrorModel(error = ""), Cmd.None) case GlobalSearch => (this, DataProcess.globalSearch(global.searchValue.toLowerCase)) case GlobalSearchResult(v) => (v, Nav.pushUrl(v.url)) case _ => (this, Cmd.None) case class IssueInfo(date: String, n: Int) given Decoder[IssueInfo] = Decoder.forProduct2( "Issue_date", "Issuance" )(IssueInfo.apply) case class NftJson( creatorDesc: String, collectionDesc: String, rarity: String, checksum: String, issue: IssueInfo, collection: String, creator: String, name: String, uri: String, ) given Decoder[NftJson] = Decoder.forProduct9( "Creator_description", "Collection_description", "Rarity", "NFT_checksum", "Issuance_info", "Collection_name", "Creator", "NFT_name", "NFT_URI" )(NftJson.apply) ================================================ FILE: modules/lmscan-frontend/src/main/scala/io/leisuremeta/chain/lmscan/frontend/controllers/Msg.scala ================================================ package io.leisuremeta.chain.lmscan package frontend import common.model._ import scalajs.js sealed trait Msg sealed trait GlobalMsg extends Msg case object ErrorMsg extends Msg case object RefreshData extends Msg case object NoneMsg extends Msg case class GlobalInput(s: String) extends GlobalMsg case class UpdateTime(t: js.Date) extends GlobalMsg case object GlobalSearch extends Msg case class GlobalSearchResult(v: Model) extends Msg case object ListSearch extends Msg case object Init extends Msg case class UpdateDetailPage(param: ApiModel) extends Msg case object DrawChart extends Msg case class UpdateChart(param: SummaryChart) extends Msg case class UpdateSearch(v: Int) extends Msg case class UpdateModel(model: ApiModel) extends Msg case class UpdateListModel[T](model: PageResponse[T]) extends Msg case class UpdateBlcs(model: PageResponse[BlockInfo]) extends Msg case class UpdateTxs(model: PageResponse[TxInfo]) extends Msg case class UpdateSample[T](model: PageResponse[T]) extends Msg case class GetDataFromApi(key: String) extends Msg case class SetLocal(key: String, d: String) extends Msg case class TogglePageInput(t: Boolean) extends Msg trait RouterMsg extends Msg case class ToPage(model: Model) extends RouterMsg case class NavigateToUrl(url: String) extends RouterMsg case object EmptyRoute extends RouterMsg ================================================ FILE: modules/lmscan-frontend/src/main/scala/io/leisuremeta/chain/lmscan/frontend/layouts/DefaultLayout.scala ================================================ package io.leisuremeta.chain.lmscan.frontend import tyrian.* import tyrian.Html.* object DefaultLayout: def view(model: Model, contents: List[Html[Msg]]): Html[Msg] = div(cls := "main")( header( NavBar.view(model), SearchView.view(model), ), section(cls := "con-wrap")(contents), footer(Footer.view()), ) def view(model: Model, contents: Html[Msg]): Html[Msg] = this.view(model, List(contents)) ================================================ FILE: modules/lmscan-frontend/src/main/scala/io/leisuremeta/chain/lmscan/frontend/pages/AccountDetailPage.scala ================================================ package io.leisuremeta.chain.lmscan package frontend import tyrian.* import cats.effect.IO import tyrian.Html.* import common.model._ object AccountDetailPage: def update(model: AccDetailModel): Msg => (Model, Cmd[IO, Msg]) = case Init => (model, DataProcess.getData(model)) case RefreshData => (model, Cmd.None) case UpdateDetailPage(d: AccountDetail) => (model, DataProcess.getData(model.copy(accDetail = d))) case UpdateModel(v: AccountDetail) => (model.set(v), Nav.pushUrl(model.url)) case UpdateSearch(v) => (model.copy(searchPage = v), Cmd.None) case ListSearch => (model.copy(page = model.searchPage, pageToggle = false), Cmd.emit(Init)) case TogglePageInput(t) => (model.copy(pageToggle = t), Cmd.None) case msg: GlobalMsg => (model.copy(global = model.global.update(msg)), Cmd.None) case msg => (model.toEmptyModel, Cmd.emit(msg)) def view(model: AccDetailModel): Html[Msg] = DefaultLayout.view( model, List( div(cls := "page-title")("Account"), model.data match case None => LoaderView.view case Some(_) => AccountDetailTable.view(model.accDetail) , div(cls := "page-title")("Transaction History"), model.data match case None => LoaderView.view case Some(_) => Table.view(model) ), ) final case class AccDetailModel( global: GlobalModel = GlobalModel(), accDetail: AccountDetail = AccountDetail(), page: Int = 1, searchPage: Int = 1, data: Option[PageResponse[TxInfo]] = None, pageToggle: Boolean = false, ) extends PageModel: def set(v: AccountDetail) = val data = if v.payload.length == 0 then Some(PageResponse()) else Some(PageResponse(v.totalCount, v.totalPages, v.payload)) this.copy( accDetail = v, data = data, ) def view: Html[Msg] = AccountDetailPage.view(this) def url = s"/acc/${accDetail.address.get}?p=${page}" def update: Msg => (Model, Cmd[IO, Msg]) = AccountDetailPage.update(this) ================================================ FILE: modules/lmscan-frontend/src/main/scala/io/leisuremeta/chain/lmscan/frontend/pages/AccountPage.scala ================================================ package io.leisuremeta.chain.lmscan package frontend import tyrian.* import cats.effect.IO import tyrian.Html.* import common.model._ object AccountPage: def update(model: AccModel): Msg => (Model, Cmd[IO, Msg]) = case Init => (model, DataProcess.getData(model)) case RefreshData => (model, Cmd.None) case UpdateListModel(v: PageResponse[AccountInfo]) => (model.copy(data = Some(v)), Nav.pushUrl(model.url)) case UpdateSearch(v) => (model.copy(searchPage = v), Cmd.None) case ListSearch => (AccModel(page = model.searchPage), Cmd.emit(Init)) case TogglePageInput(t) => (model.copy(pageToggle = t), Cmd.None) case msg: GlobalMsg => (model.copy(global = model.global.update(msg)), Cmd.None) case msg => (model.toEmptyModel, Cmd.emit(msg)) def view(model: AccModel): Html[Msg] = DefaultLayout.view( model, List( div(cls := "page-title")("Accounts"), Table.view(model), ), ) final case class AccModel( global: GlobalModel = GlobalModel(), page: Int = 1, searchPage: Int = 1, data: Option[PageResponse[AccountInfo]] = None, pageToggle: Boolean = false, ) extends PageModel: def view: Html[Msg] = AccountPage.view(this) def url = s"/accs/$page" def update: Msg => (Model, Cmd[IO, Msg]) = AccountPage.update(this) ================================================ FILE: modules/lmscan-frontend/src/main/scala/io/leisuremeta/chain/lmscan/frontend/pages/BlockDetailPage.scala ================================================ package io.leisuremeta.chain.lmscan package frontend import tyrian.* import cats.effect.IO import tyrian.Html.* import common.model._ object BlockDetailPage: def update(model: BlcDetailModel): Msg => (Model, Cmd[IO, Msg]) = case Init => (model, DataProcess.getData(model)) case RefreshData => (model, Cmd.None) case UpdateDetailPage(d: BlockDetail) => (model, DataProcess.getData(model.copy(blcDetail = d))) case UpdateModel(v: BlockDetail) => (model.set(v), Nav.pushUrl(model.url)) case UpdateSearch(v) => (model.copy(searchPage = v), Cmd.None) case ListSearch => (model.copy(page = model.searchPage, pageToggle = false), Cmd.emit(Init)) case TogglePageInput(t) => (model.copy(pageToggle = t), Cmd.None) case msg: GlobalMsg => (model.copy(global = model.global.update(msg)), Cmd.None) case msg => (model.toEmptyModel, Cmd.emit(msg)) def view(model: BlcDetailModel): Html[Msg] = DefaultLayout.view( model, List( div(cls := "page-title")("Block Details"), model.data match case None => LoaderView.view case Some(_) => BlockDetailTable.view(model.blcDetail) , div(cls := "page-title")("Transaction List"), model.data match case None => LoaderView.view case Some(_) => Table.view(model) ), ) final case class BlcDetailModel( global: GlobalModel = GlobalModel(), blcDetail: BlockDetail = BlockDetail(), page: Int = 1, searchPage: Int = 1, data: Option[PageResponse[TxInfo]] = None, pageToggle: Boolean = false, ) extends PageModel: def set(v: BlockDetail) = val data = if v.payload.length == 0 then Some(PageResponse()) else Some(PageResponse(v.totalCount, v.totalPages, v.payload)) this.copy( blcDetail = v, data = data, ) def view: Html[Msg] = BlockDetailPage.view(this) def url = s"/blc/${blcDetail.hash.get}?p=${page}" def update: Msg => (Model, Cmd[IO, Msg]) = BlockDetailPage.update(this) ================================================ FILE: modules/lmscan-frontend/src/main/scala/io/leisuremeta/chain/lmscan/frontend/pages/BlockPage.scala ================================================ package io.leisuremeta.chain.lmscan package frontend import tyrian.* import cats.effect.IO import tyrian.Html.* import common.model._ object BlockPage: def update(model: BlcModel): Msg => (Model, Cmd[IO, Msg]) = case Init => (model, DataProcess.getData(model)) case RefreshData => (model, DataProcess.getData(model)) case UpdateBlcs(v) => (model.copy(data = Some(v)), Nav.pushUrl(model.url)) case UpdateSearch(v) => (model.copy(searchPage = v), Cmd.None) case ListSearch => (BlcModel(page = model.searchPage), Cmd.emit(Init)) case TogglePageInput(t) => (model.copy(pageToggle = t), Cmd.None) case msg: GlobalMsg => (model.copy(global = model.global.update(msg)), Cmd.None) case msg => (model.toEmptyModel, Cmd.emit(msg)) def view(model: BlcModel): Html[Msg] = DefaultLayout.view( model, List( div(cls := "page-title")("Blocks"), Table.view(model), ), ) final case class BlcModel( global: GlobalModel = GlobalModel(), page: Int = 1, searchPage: Int = 1, data: Option[PageResponse[BlockInfo]] = None, pageToggle: Boolean = false, ) extends PageModel: def view: Html[Msg] = BlockPage.view(this) def url = s"/blcs/$page" def update: Msg => (Model, Cmd[IO, Msg]) = BlockPage.update(this) ================================================ FILE: modules/lmscan-frontend/src/main/scala/io/leisuremeta/chain/lmscan/frontend/pages/ErrorPage.scala ================================================ package io.leisuremeta.chain.lmscan.frontend import tyrian.* import cats.effect.IO import tyrian.Html.* object ErrorPage: def update(model: ErrorModel): Msg => (Model, Cmd[IO, Msg]) = case Init => (model, Cmd.None) case RefreshData => (model, Cmd.None) case msg: GlobalMsg => (model.copy(global = model.global.update(msg)), Cmd.None) case msg => (model.toEmptyModel, Cmd.emit(msg)) def view(model: ErrorModel): Html[Msg] = DefaultLayout.view( model, model.error match case "timeout" => timeout case _ => div(cls := "err-wrap")( p("THE PAGE YOU WERE LOOKING FOR DOESN’T EXIST."), p("You may have mistyped the information. Please check before searching."), div(cls := "cell type-button")( span( cls := "font-20px", onClick( ToPage(BaseModel()), ), )( "Back to Previous Page", ), ), ), ) val timeout = div(cls := "err-wrap")( p("TIME OUT! TRY AGAIN LATER.") ) final case class ErrorModel( global: GlobalModel = GlobalModel(), error: String ) extends Model: def view: Html[Msg] = ErrorPage.view(this) def url = "/error" def update: Msg => (Model, Cmd[IO, Msg]) = ErrorPage.update(this) ================================================ FILE: modules/lmscan-frontend/src/main/scala/io/leisuremeta/chain/lmscan/frontend/pages/MainPage.scala ================================================ package io.leisuremeta.chain.lmscan package frontend import tyrian.* import cats.effect.IO import tyrian.Html.* import common.model._ object MainPage: def update(model: BaseModel): Msg => (Model, Cmd[IO, Msg]) = case Init => (model, Cmd.Batch( DataProcess.getData(BlcModel()), DataProcess.getData(TxModel()), DataProcess.getLocal("board"), DataProcess.getLocal("chart"), Nav.pushUrl(model.url), )) case RefreshData => (model, Cmd.Batch( DataProcess.getData(BlcModel()), DataProcess.getData(TxModel()), DataProcess.getLocal("board"), DataProcess.getLocal("chart"), )) case UpdateModel(v: SummaryBoard) => (model.copy(summary = Some(v)), Cmd.None) case UpdateChart(v: SummaryChart) => (model.copy(chartData = Some(v)), Cmd.None) case UpdateBlcs(v) => (model.copy(blcs = Some(v)), Cmd.None) case UpdateTxs(v) => (model.copy(txs = Some(v)), Cmd.None) case GetDataFromApi(key) => (model, DataProcess.getDataAll(key)) case SetLocal(key, d) => (model, DataProcess.setLocal(key, d)) case msg: GlobalMsg => (model.copy(global = model.global.update(msg)), Cmd.None) case msg => (model.toEmptyModel, Cmd.emit(msg)) def view(model: BaseModel): Html[Msg] = DefaultLayout.view( model, List( BoardView.view(model), Table.mainView(model), ) ) final case class BaseModel( global: GlobalModel = GlobalModel(), summary: Option[SummaryBoard] = None, chartData: Option[SummaryChart] = None, blcs: Option[PageResponse[BlockInfo]] = None, txs: Option[PageResponse[TxInfo]] = None, ) extends Model: def view: Html[Msg] = MainPage.view(this) def url = "/" def update: Msg => (Model, Cmd[IO, Msg]) = MainPage.update(this) ================================================ FILE: modules/lmscan-frontend/src/main/scala/io/leisuremeta/chain/lmscan/frontend/pages/NftPage.scala ================================================ package io.leisuremeta.chain.lmscan package frontend import tyrian.* import cats.effect.IO import tyrian.Html.* import common.model._ object NftPage: def update(model: NftModel): Msg => (Model, Cmd[IO, Msg]) = case Init => (model, DataProcess.getData(model)) case RefreshData => (model, Cmd.None) case UpdateListModel(v: PageResponse[NftInfoModel]) => (model.copy(data = Some(v)), Nav.pushUrl(model.url)) case UpdateSearch(v) => (model.copy(searchPage = v), Cmd.None) case ListSearch => (NftModel(page = model.searchPage), Cmd.emit(Init)) case TogglePageInput(t) => (model.copy(pageToggle = t), Cmd.None) case msg: GlobalMsg => (model.copy(global = model.global.update(msg)), Cmd.None) case msg => (model.toEmptyModel, Cmd.emit(msg)) def view(model: NftModel): Html[Msg] = DefaultLayout.view( model, List( div(cls := "page-title")("NFTs"), Table.view(model), ), ) final case class NftModel( global: GlobalModel = GlobalModel(), page: Int = 1, searchPage: Int = 1, data: Option[PageResponse[NftInfoModel]] = None, pageToggle: Boolean = false, ) extends PageModel: def view: Html[Msg] = NftPage.view(this) def url = s"/nfts/$page" def update: Msg => (Model, Cmd[IO, Msg]) = NftPage.update(this) ================================================ FILE: modules/lmscan-frontend/src/main/scala/io/leisuremeta/chain/lmscan/frontend/pages/NftTokenPage.scala ================================================ package io.leisuremeta.chain.lmscan package frontend import tyrian.* import cats.effect.IO import tyrian.Html.* import common.model._ object NftTokenPage: def update(model: NftTokenModel): Msg => (Model, Cmd[IO, Msg]) = case Init => (model, DataProcess.getData(model)) case RefreshData => (model, Cmd.None) case UpdateListModel(v: PageResponse[NftSeasonModel]) => (model.copy(data = Some(v)), Nav.pushUrl(model.url)) case UpdateSearch(v) => (model.copy(searchPage = v), Cmd.None) case ListSearch => (NftTokenModel(page = model.searchPage, id = model.id), Cmd.emit(Init)) case TogglePageInput(t) => (model.copy(pageToggle = t), Cmd.None) case msg: GlobalMsg => (model.copy(global = model.global.update(msg)), Cmd.None) case msg => (model.toEmptyModel, Cmd.emit(msg)) def view(model: NftTokenModel): Html[Msg] = DefaultLayout.view( model, List( div(cls := "page-title")("NFTs Token"), Table.view(model), ), ) final case class NftTokenModel( global: GlobalModel = GlobalModel(), page: Int = 1, searchPage: Int = 1, id: String, data: Option[PageResponse[NftSeasonModel]] = None, pageToggle: Boolean = false, ) extends PageModel: def view: Html[Msg] = NftTokenPage.view(this) def url = s"/nft/$id/$page" def update: Msg => (Model, Cmd[IO, Msg]) = NftTokenPage.update(this) ================================================ FILE: modules/lmscan-frontend/src/main/scala/io/leisuremeta/chain/lmscan/frontend/pages/NtfDetailPage.scala ================================================ package io.leisuremeta.chain.lmscan package frontend import tyrian.* import cats.effect.IO import tyrian.Html.* import common.model._ object NftDetailPage: def update(model: NftDetailModel): Msg => (Model, Cmd[IO, Msg]) = case Init => (model, DataProcess.getData(model.nftDetail.nftFile.get)) case RefreshData => (model, Cmd.None) case UpdateDetailPage(d: NftDetail) => (model, DataProcess.getData(d.nftFile.get)) case UpdateModel(v: NftDetail) => (NftDetailModel(nftDetail = v), Nav.pushUrl(model.url)) case msg: GlobalMsg => (model.copy(global = model.global.update(msg)), Cmd.None) case msg => (model.toEmptyModel, Cmd.emit(msg)) def view(model: NftDetailModel): Html[Msg] = DefaultLayout.view( model, List( div(cls := "page-title")("NFT Details"), NftDetailTable.view(model.nftDetail), div(cls := "page-title")("History"), Table.view(model) ), ) final case class NftDetailModel( global: GlobalModel = GlobalModel(), nftDetail: NftDetail = NftDetail(), ) extends Model: def view: Html[Msg] = NftDetailPage.view(this) def url = s"/nft/${nftDetail.nftFile.get.tokenId.get}" def update: Msg => (Model, Cmd[IO, Msg]) = NftDetailPage.update(this) ================================================ FILE: modules/lmscan-frontend/src/main/scala/io/leisuremeta/chain/lmscan/frontend/pages/TxDetailPage.scala ================================================ package io.leisuremeta.chain package lmscan package frontend import tyrian.* import cats.effect.IO import tyrian.Html.* import common.model._ import io.circe.* import io.circe.parser.* import io.circe.generic.auto.* import api.model._ import api.model.Transaction.AccountTx import api.model.Transaction.TokenTx import api.model.Transaction.GroupTx import api.model.Transaction.RewardTx import api.model.Transaction.AgendaTx object TxDetailPage: def update(model: TxDetailModel): Msg => (Model, Cmd[IO, Msg]) = case Init => (model, DataProcess.getData(model.txDetail)) case RefreshData => (model, Cmd.None) case UpdateDetailPage(d: TxDetail) => (model, DataProcess.getData(d)) case UpdateModel(v: TxDetail) => (TxDetailModel(txDetail = v), Nav.pushUrl(model.url)) case msg: GlobalMsg => (model.copy(global = model.global.update(msg)), Cmd.None) case msg => (model.toEmptyModel, Cmd.emit(msg)) def view(model: TxDetailModel): Html[Msg] = DefaultLayout.view( model, div(cls := "page-title")("Transaction details") :: commonView(model.txDetail, model.getInfo) :: TxDetailTableCommon.view(model.getTxr), ) def commonView(data: TxDetail, info: (String, String, String)) = div(cls := "detail table-container")( div(cls := "row")( span("Transaction Hash"), span(data.hash.getOrElse("-")), ), div(cls := "row")( span("Created At"), ParseHtml.fromDate(data.createdAt), ), div(cls := "row")( span("Signer"), ParseHtml.fromAccHash(Some(info._1)), ), div(cls := "row")( span("Type"), span(info._2), ), div(cls := "row")( span("SubType"), span(info._3), ), ) final case class TxDetailModel( global: GlobalModel = GlobalModel(), txDetail: TxDetail = TxDetail(), ) extends Model: def view: Html[Msg] = TxDetailPage.view(this) def url = s"/tx/${txDetail.hash.get}" def update: Msg => (Model, Cmd[IO, Msg]) = TxDetailPage.update(this) def getTxr: Option[TransactionWithResult] = for json <- txDetail.json tx <- decode[TransactionWithResult](json).toOption yield tx def getInfo: (String, String, String) = extension (a: Transaction) def splitTx = a.toString.split("\\(").head this.getTxr match case Some(x) => val acc = x.signedTx.sig.account.toString val (tt, st) = x.signedTx.value match case tx: Transaction.TokenTx => ("TokenTx", tx.splitTx) case tx: Transaction.AccountTx => ("AccountTx", tx.splitTx) case tx: Transaction.GroupTx => ("GroupTx", tx.splitTx) case tx: Transaction.RewardTx => ("RewardTx", tx.splitTx) case tx: Transaction.AgendaTx => ("AgendaTx", tx.splitTx) case tx: Transaction.VotingTx => ("VotingTx", tx.splitTx) case tx: Transaction.CreatorDaoTx => ("CreatorDaoTx", tx.splitTx) (acc, tt, st) case None => ("", "", "") ================================================ FILE: modules/lmscan-frontend/src/main/scala/io/leisuremeta/chain/lmscan/frontend/pages/TxPage.scala ================================================ package io.leisuremeta.chain.lmscan package frontend import tyrian.* import cats.effect.IO import tyrian.Html.* import common.model._ object TxPage: def update(model: TxModel): Msg => (Model, Cmd[IO, Msg]) = case Init => (model, DataProcess.getData(model)) case RefreshData => if (model.page == 1) then (model, DataProcess.getData(model)) else (model, Cmd.None) case UpdateTxs(v) => (model.copy(data = Some(v)), Nav.pushUrl(model.url)) case UpdateSearch(v) => (model.copy(searchPage = v), Cmd.None) case ListSearch => (TxModel(page = model.searchPage), Cmd.emit(Init)) case TogglePageInput(t) => (model.copy(pageToggle = t), Cmd.None) case msg: GlobalMsg => (model.copy(global = model.global.update(msg)), Cmd.None) case msg => (model.toEmptyModel, Cmd.emit(msg)) def view(model: TxModel): Html[Msg] = DefaultLayout.view( model, List( div(cls := "page-title")("Transactions"), Table.view(model), ), ) final case class TxModel( global: GlobalModel = GlobalModel(), page: Int = 1, searchPage: Int = 1, data: Option[PageResponse[TxInfo]] = None, pageToggle: Boolean = false, ) extends PageModel: def view: Html[Msg] = TxPage.view(this) def url = s"/txs/$page" def update: Msg => (Model, Cmd[IO, Msg]) = TxPage.update(this) ================================================ FILE: modules/lmscan-frontend/src/main/scala/io/leisuremeta/chain/lmscan/frontend/pages/VdDetailPage.scala ================================================ package io.leisuremeta.chain.lmscan package frontend import tyrian.* import cats.effect.IO import tyrian.Html.* import common.model._ object VdDetailPage: def update(model: VdDetailModel): Msg => (Model, Cmd[IO, Msg]) = case Init => (model, DataProcess.getData(model)) case RefreshData => (model, DataProcess.getData(model)) case UpdateModel(v: NodeValidator.ValidatorDetail) => (model.copy(payload = v), Nav.pushUrl(model.url)) case msg: GlobalMsg => (model.copy(global = model.global.update(msg)), Cmd.None) case msg => (model.toEmptyModel, Cmd.emit(msg)) def view(model: VdDetailModel): Html[Msg] = DefaultLayout.view( model, List( div(cls := "page-title")("Validator Detail"), infoView(model.validator), div(cls := "page-title")("Proposed Blocks"), Table.view(model), ), ) def infoView(data: NodeValidator.Validator) = div(cls := "detail table-container")( div(cls := "row")( span("address"), span(data.address.getOrElse("")), ), div(cls := "row")( span("Total Proposed Blocks"), span(f"${data.cnt.getOrElse(0L)}%,d"), ), div(cls := "row")( span("Voting Power"), span(data.power.getOrElse(0.0).toString + "%"), ), ) final case class VdDetailModel( global: GlobalModel = GlobalModel(), address: String, page: Int = 1, searchPage: Int = 1, pageToggle: Boolean = false, payload: NodeValidator.ValidatorDetail = NodeValidator.ValidatorDetail() ) extends PageModel: def view: Html[Msg] = VdDetailPage.view(this) def url = s"/vds/$address?p=$page" def update: Msg => (Model, Cmd[IO, Msg]) = VdDetailPage.update(this) def validator = payload.validator def blcs = payload.page val data = Some(payload.page) ================================================ FILE: modules/lmscan-frontend/src/main/scala/io/leisuremeta/chain/lmscan/frontend/pages/VdPage.scala ================================================ package io.leisuremeta.chain.lmscan package frontend import tyrian.* import cats.effect.IO import tyrian.Html.* import common.model._ object VdPage: def update(model: VdModel): Msg => (Model, Cmd[IO, Msg]) = case Init => (model, DataProcess.getData(model)) case RefreshData => (model, DataProcess.getData(model)) case UpdateModel(v: NodeValidator.ValidatorList) => (model.copy(payload = v.payload.toList), Nav.pushUrl(model.url)) case msg: GlobalMsg => (model.copy(global = model.global.update(msg)), Cmd.None) case msg => (model.toEmptyModel, Cmd.emit(msg)) def view(model: VdModel): Html[Msg] = DefaultLayout.view( model, List( div(cls := "page-title")("Validators"), Table.view(model), ), ) final case class VdModel( global: GlobalModel = GlobalModel(), payload: List[NodeValidator.Validator] = List() ) extends Model: def view: Html[Msg] = VdPage.view(this) def url = s"/vds" def update: Msg => (Model, Cmd[IO, Msg]) = VdPage.update(this) ================================================ FILE: modules/lmscan-frontend/src/main/scala/io/leisuremeta/chain/lmscan/frontend/utils/Cell.scala ================================================ package io.leisuremeta.chain.lmscan.frontend import tyrian.Html.* import tyrian.* import V.* import io.leisuremeta.chain.lmscan.common.model.* import java.text.DecimalFormat import java.time.format.DateTimeFormatter import java.time._ import scalajs.js def toDT(t: Long, cur: js.Date): String = val offset = -cur.getTimezoneOffset().toInt / 60 LocalDateTime .ofEpochSecond(t, 0, ZoneOffset.ofHours(offset)) .toString .replace("T", " ") + s" (UTC${if offset < 0 then "-" else "+"}${offset.toString})" def timeAgo(t: Long, cur: js.Date): String = val now = LocalDateTime.now(ZoneId.ofOffset("UTC", ZoneOffset.ofHours(0))).toEpochSecond(ZoneOffset.UTC) val timeGap = now - t List( ((timeGap / 31536000).toInt, " year ago"), ((timeGap / 2592000).toInt, " month ago"), ((timeGap / 86400).toInt, " day ago"), ((timeGap / 3600).toInt, " hour ago"), ((timeGap / 60).toInt, " min ago"), ((timeGap / 1).toInt, "s ago"), ) .find((time, _) => time > 0) .map: case (time, msg) if time > 1 => time.toString + msg.replace(" a", "s a").replace("ss", "s") case (time, msg) => time.toString + msg .getOrElse("-") object ParseHtml: def fromDate(data: Option[Long]) = div( data match case Some(v) => toDT(v, new js.Date) case _ => "-" ) def fromAccHash(data: Option[String], filter: Boolean = true) = div( cls := s"acc-hash", onClick(ToPage(AccDetailModel(accDetail = AccountDetail(address = data)))), )(if filter then accountHash(data) else data.getOrElse("")) def fromTxHash(data: String) = span( cls := "tx-hash", onClick( ToPage(TxDetailModel(txDetail = TxDetail(hash = Some(data)))) ), )(data) def fromTokenId(value: String) = span( cls := "token-id", onClick( ToPage(NftDetailModel(nftDetail = NftDetail(nftFile = Some(NftFileModel(tokenId = Some(value)))))) ) )(value) def fromBal(data: Option[BigDecimal]) = div( span( data match case None => "- LM" case Some(v) => val a = v / BigDecimal("1E+18") DecimalFormat("#,###.####").format(a) + " LM", ), ) enum Cell: case ImageS(data: Option[String]) extends Cell case Image(data: Option[String]) extends Cell case Head(data: String, css: String = "cell") extends Cell case Any(data: String, css: String = "cell") extends Cell case PriceS( price: Option[BigDecimal], css: String = "cell", ) extends Cell case Price( price: Option[Double], balance: Option[BigDecimal], css: String = "cell", ) extends Cell case Balance(data: Option[BigDecimal], css: String = "cell") extends Cell case AGE(data: Option[Long], cur: js.Date) extends Cell case DateS(data: Option[Instant], css: String = "cell") extends Cell case DATE(data: Option[Long], css: String = "cell") extends Cell case BLOCK_NUMBER(data: (Option[String], Option[Long])) extends Cell case NftToken(data: NftInfoModel) extends Cell case NftDetail(data: NftSeasonModel, s: Option[String]) extends Cell case BLOCK_HASH(data: Option[String]) extends Cell case ACCOUNT_HASH(data: Option[String], css: String = "cell") extends Cell case TX_HASH(data: Option[String]) extends Cell case Tx_VALUE(data: (Option[String], Option[String])) extends Cell case PlainInt(data: Option[Int]) extends Cell case PlainLong(data: Option[Long]) extends Cell case PlainStr( data: Option[String] | Option[Int] | Option[Long] | Option[Double], css: String = "cell", ) extends Cell case AAA(data: String) extends Cell object gen: def cell(cells: Cell*) = cells .map(_ match case Cell.ImageS(nftUri) => img( cls := "thumb-img", src := s"${getOptionValue(nftUri, "-").toString}", ) case Cell.Image(nftUri) => List("mp3", "mp4") .find(data => plainStr(nftUri).contains(data)) match case Some(_) => // 비디오 포맷 video( cls := "nft-image p-10px", autoPlay, loop, name := "media", )( source( src := s"${getOptionValue(nftUri, "-").toString}", `type` := "video/mp4", ), ) case None => // 이미지 포맷 img( cls := "nft-image p-10px", src := s"${getOptionValue(nftUri, "-").toString}", ) case Cell.Head(data, css) => div(cls := s"$css")(span()(data)) case Cell.Any(data, css) => div(cls := s"$css")(span()(data)) case Cell.PriceS(price, css) => div(cls := s"$css")( span( price match case Some(p) => "$ " + DecimalFormat("#,###.0000").format(p) case _ => "$ 0", ), ) case Cell.Price(price, data, css) => div(cls := s"$css")( span( (price, data) match case (Some(p), Some(v)) => val a = v / BigDecimal("1E+18") * BigDecimal(p) "$ " + DecimalFormat("#,###.0000").format(a) case _ => "$ 0", ), ) case Cell.Balance(data, css) => div(cls := s"$css")( span( data match case None => "- LM" case Some(v) => val a = v / BigDecimal("1E+18") DecimalFormat("#,###.00").format(a) + " LM", ), ) case Cell.PlainStr(data, css) => div(cls := s"$css")(span()(plainStr(data))) case Cell.PlainInt(data) => div(cls := "cell")( span( )(plainStr(data)), ) case Cell.PlainLong(data) => div(cls := "cell")( span( )(plainStr(data)), ) case Cell.Tx_VALUE(subType, value) => div( cls := s"cell ${plainStr(subType).contains("Nft") match case true => "type-3" case _ => "" }", )( span( plainStr(subType).contains("Nft") match case true => onClick( ToPage(NftDetailModel(nftDetail = NftDetail(nftFile = Some(NftFileModel(tokenId = value))))) ) case _ => EmptyAttribute, )( plainStr(subType).contains("Nft") match case true => plainStr(value) case _ => value .map(s => s.forall(Character.isDigit)) .getOrElse(false) match case true => txValue(value) case false => plainStr(value), ), ) case Cell.ACCOUNT_HASH(hash, css) => div( cls := s"cell acc-hash $css", onClick(ToPage(AccDetailModel(accDetail = AccountDetail(address = hash)))), )( accountHash(hash), ) case Cell.AGE(data, cur) => div(cls := "cell", dataAttr( "tooltip-text", toDT(data.getOrElse(0L), cur), ), )( data match case Some(v) => timeAgo(v, cur) case _ => "-" ) case Cell.DateS(data, css) => data match case None => div() case Some(v) => div( cls := s"$css", dataAttr( "tooltip-text", v.toString ) )( DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm (O)") .withZone(ZoneId.of("+09:00")) .format(v) ) case Cell.DATE(data, css) => div(cls := s"$css")( { val date = toDT( plainStr(data).toInt, new js.Date ) + " +UTC" date match case x if x.contains("1970") => "-" case _ => date }, ) case Cell.BLOCK_NUMBER((hash, number)) => div(cls := "cell blc-num", onClick( ToPage(BlcDetailModel(blcDetail = BlockDetail(hash = hash))), ), )(plainStr(number)) case Cell.NftToken(nftInfo) => a( cls := "token-id", onClick( ToPage(NftTokenModel(id = nftInfo.season.get)), ), )( s"${plainSeason(nftInfo.season)}${plainStr(nftInfo.seasonName)}", ) case Cell.NftDetail(nftInfo, s) => div( cls := "token-id", onClick( ToPage(NftDetailModel(nftDetail = NftDetail(nftFile = Some(NftFileModel(tokenId = nftInfo.tokenId))))) ), )(plainStr(s)) case Cell.BLOCK_HASH(hash) => div( cls := "cell blc-hash", onClick( ToPage(BlcDetailModel(blcDetail = BlockDetail(hash = hash))), ), )(plainStr(hash)) case Cell.TX_HASH(hash) => div( cls := "cell tx-hash", onClick( ToPage(TxDetailModel(txDetail = TxDetail(hash = hash))) ), )( plainStr(hash), ) case _ => div(cls := "cell")(span()), ) .toList ================================================ FILE: modules/lmscan-frontend/src/main/scala/io/leisuremeta/chain/lmscan/frontend/utils/DataProcess.scala ================================================ package io.leisuremeta.chain.lmscan package frontend import cats.effect.IO import io.circe.parser.* import tyrian.* import tyrian.http.* import scala.scalajs.js import scala.concurrent.duration.* import common.model._ import tyrian.cmds.LocalStorage object Parse: import io.circe.*, io.circe.generic.semiauto.* given Decoder[SummaryBoard] = deriveDecoder[SummaryBoard] given Decoder[SummaryModel] = deriveDecoder[SummaryModel] given Decoder[SummaryChart] = deriveDecoder[SummaryChart] given Decoder[NftInfoModel] = deriveDecoder[NftInfoModel] given Decoder[BlockInfo] = deriveDecoder[BlockInfo] given Decoder[AccountInfo] = deriveDecoder[AccountInfo] given Decoder[NftSeasonModel] = deriveDecoder[NftSeasonModel] given a: Decoder[PageResponse[AccountInfo]] = deriveDecoder[PageResponse[AccountInfo]] given b: Decoder[PageResponse[BlockInfo]] = deriveDecoder[PageResponse[BlockInfo]] given c: Decoder[PageResponse[TxInfo]] = deriveDecoder[PageResponse[TxInfo]] given d: Decoder[PageResponse[NftInfoModel]] = deriveDecoder[PageResponse[NftInfoModel]] given e: Decoder[PageResponse[NftSeasonModel]] = deriveDecoder[PageResponse[NftSeasonModel]] def searchResponse(): Response => Msg = response => response.status match case Status(400, _) => ErrorMsg case Status(500, _) => ErrorMsg case _ => searchResult(response) def searchResult(response: Response): Msg = decode[SearchResult](response.body) match case Right(SearchResult.acc(x)) => GlobalSearchResult(AccDetailModel().set(x)) case Right(SearchResult.tx(x)) => GlobalSearchResult(TxDetailModel(txDetail = x)) case Right(SearchResult.blc(x)) => GlobalSearchResult(BlcDetailModel().set(x)) case Right(SearchResult.nft(x)) => GlobalSearchResult(NftDetailModel(nftDetail = x)) case _ => ErrorMsg def onResponse(model: ApiModel): Response => Msg = response => response.status match case Status(400, _) => ErrorMsg case Status(500, _) => ErrorMsg case _ => result(model, response) def result(model: ApiModel, response: Response): Msg = (model, response.body) match case (_: BlcModel, str) => UpdateBlcs(decode[PageResponse[BlockInfo]](str).getOrElse(PageResponse())) case (_: TxModel, str) => UpdateTxs(decode[PageResponse[TxInfo]](str).getOrElse(PageResponse())) case (_: AccModel, str) => UpdateListModel[AccountInfo](decode[PageResponse[AccountInfo]](str).getOrElse(PageResponse())) case (_: NftModel, str) => UpdateListModel(decode[PageResponse[NftInfoModel]](str).getOrElse(PageResponse())) case (_: NftTokenModel, str) => UpdateListModel(decode[PageResponse[NftSeasonModel]](str).getOrElse(PageResponse())) case (_: TxDetail, str) => UpdateModel(decode[TxDetail](str).getOrElse(TxDetail())) case (_: BlockDetail, str) => UpdateModel(decode[BlockDetail](str).getOrElse(BlockDetail())) case (_: AccountDetail, str) => UpdateModel(decode[AccountDetail](str).getOrElse(AccountDetail())) case (_: NftDetail, str) => UpdateModel(decode[NftDetail](str).getOrElse(NftDetail())) case (_: SummaryBoard, str) => SetLocal("board", str) case (_: SummaryChart, str) => SetLocal("chart", str) case (_: NodeValidator.ValidatorList, str) => val res = decode[List[NodeValidator.Validator]](str).map(NodeValidator.ValidatorList(_)) UpdateModel(res.getOrElse(NodeValidator.ValidatorList())) case (_: NodeValidator.ValidatorDetail, str) => UpdateModel(decode[NodeValidator.ValidatorDetail](str).getOrElse(NodeValidator.ValidatorDetail())) case (_, str) => ErrorMsg case (_, Left(json)) => ErrorMsg def parseFromString(k: String, s: String): Msg = k match case "board" => UpdateModel(decode[SummaryBoard](s).getOrElse(SummaryBoard())) case "chart" => UpdateChart(decode[SummaryChart](s).getOrElse(SummaryChart())) object DataProcess: val base = js.Dynamic.global.process.env.BASE_API_URL def onError(e: HttpError): Msg = ErrorMsg def getData(model: PageModel): Cmd[IO, Msg] = val url = model match case _: BlcModel => s"${base}block/list?pageNo=${model.page - 1}&sizePerRequest=${model.size}" case _: TxModel => s"${base}tx/list?pageNo=${model.page - 1}&sizePerRequest=${model.size}" case _: NftModel => s"${base}nft/list?pageNo=${model.page - 1}&sizePerRequest=${model.size}" case m: NftTokenModel => s"${base}nft/${m.id}?pageNo=${model.page - 1}&sizePerRequest=${model.size}" case _: AccModel => s"${base}account/list?pageNo=${model.page - 1}&sizePerRequest=${model.size}" Http.send( Request.get(url).withTimeout(20.seconds), Decoder[Msg](Parse.onResponse(model), onError) ) def getData(detail: TxDetail): Cmd[IO, Msg] = Http.send( Request.get(s"${base}tx/${detail.hash.getOrElse("")}/detail").withTimeout(10.seconds), Decoder[Msg](Parse.onResponse(detail), onError) ) def getData(model: BlcDetailModel): Cmd[IO, Msg] = Http.send( Request.get(s"${base}block/${model.blcDetail.hash.getOrElse("")}/detail?p=${model.page}").withTimeout(10.seconds), Decoder[Msg](Parse.onResponse(model.blcDetail), onError) ) def getData(model: AccDetailModel): Cmd[IO, Msg] = Http.send( Request.get(s"${base}account/${model.accDetail.address.getOrElse("")}/detail?p=${model.page}").withTimeout(10.seconds), Decoder[Msg](Parse.onResponse(model.accDetail), onError) ) def getData(model: NftFileModel): Cmd[IO, Msg] = Http.send( Request.get(s"${base}nft/${model.tokenId.getOrElse("")}/detail").withTimeout(10.seconds), Decoder[Msg](Parse.onResponse(NftDetail()), onError) ) def getData(model: VdModel): Cmd[IO, Msg] = Http.send( Request.get(s"${base}vds").withTimeout(10.seconds), Decoder[Msg](Parse.onResponse(NodeValidator.ValidatorList()), onError) ) def getData(model: VdDetailModel): Cmd[IO, Msg] = Http.send( Request.get(s"${base}vd/${model.address}?p=${model.page}").withTimeout(10.seconds), Decoder[Msg](Parse.onResponse(NodeValidator.ValidatorDetail()), onError) ) def getDataAll(key: String): Cmd[IO, Msg] = key match case "chart" => Http.send( Request.get(s"${base}summary/chart/balance"), Decoder[Msg](Parse.onResponse(SummaryChart()), onError) ) case "board" => Http.send( Request.get(s"${base}summary/main"), Decoder[Msg](Parse.onResponse(SummaryBoard()), onError) ) def globalSearch(v: String): Cmd[IO, Msg] = Http.send( Request.get(s"${base}search/${v}"), Decoder[Msg](Parse.searchResponse(), onError) ) def getLocal(key: String): Cmd[IO, Msg] = LocalStorage.getItem(key): case Right(LocalStorage.Result.Found(s)) => val (t, d) = s.splitAt(13) if js.Date.now() - t.toDouble < 60 * 10 * 1000 then Parse.parseFromString(key, d) else GetDataFromApi(key) case Left(LocalStorage.Result.NotFound(_)) => GetDataFromApi(key) def setLocal(k: String, d: String): Cmd[IO, Msg] = val now = js.Date.now().toString LocalStorage.setItem(k, now + d): case LocalStorage.Result.Success => Parse.parseFromString(k, d) case _ => Parse.parseFromString(k, d) ================================================ FILE: modules/lmscan-frontend/src/main/scala/io/leisuremeta/chain/lmscan/frontend/utils/ValidData.scala ================================================ package io.leisuremeta.chain.lmscan.frontend object V: def validNull = (value: Option[String]) => value match case Some("") => None case _ => value def commaNumber = (value: String) => String.format( "%,d", value.replace("-", "0"), ) def getOptionValue[T] = (field: Option[T], default: T) => field match case Some(value) => value case None => default def plainStr( data: Option[String] | Option[Int] | Option[Double] | Option[Long], ) = data match case Some(v) => v.toString case None => "-" def plainSeason(data: Option[String]) = data match case Some(v) => if("""\d+\.?\d*""".r.matches(v)) s"SEASON $v:" else s"$v:" case _ => "" def rarity(data: Option[String]) = getOptionValue(data, "-") match case "NRML" => "Normal" case "LGDY" => "Legendary" case "UNIQ" => "Unique" case "EPIC" => "Epic" case "RARE" => "Rare" case _ => getOptionValue(data, "-").toString() def txValue(data: Option[String]) = val res = String .format( "%.4f", (getOptionValue(data, "0.0") .asInstanceOf[String] .toDouble / Math.pow(10, 18).toDouble), ) val sosu = res.takeRight(5) val decimal = res.replace(sosu, "") val commaDecimal = String.format("%,d", decimal.toDouble) res == "0.0000" match case true => "-" case false => commaDecimal + sosu def accountHash(data: Option[String]) = data match case Some(str) => accountMatcher(str) case None => "" def accountMatcher(data: String) = data match case "playnomm" => "010cd45939f064fd82403754bada713e5a9563a1" case "reward-posting" => "d2c442e460e06d652f1d7c8706fd649306a5b9ce" case "reward-activity" => "43a57958149a577cd7528f6d79adbc5ba728c9f3" case "DAO-M" => "37cd3566cb27e40efdbdb8bf3e8264e7bd1ffffa" case "DAO-RWD" => "8010b03a46dd4519965796c011b36d37f841157d" case "DAO-REWARD" => "293b1e3cbb57ac8c354456d79a1e9675781650ed" case "creator-reward-posting" => "dad9764447ebe3e363cb383cb114aeedc442447c" case "creator-reward-activity" => "dca74dec332357ce717ba7702b8421edab2eaeee" case "reward-nft" => "2bc3ac647d09f47d1c733d28b4d151313d62864b" case "creator-rewar-fixqty" => "f7b90eed2a28d2d41a7ded5e66427b035be0fe9b" case "moonlabs" => "ec09ba30ac1038c91fa1ae587fbdf859557cbed1" case "eth-gateway" => "ca79f6fb199218fa681b8f441fefaac2e9a3ead3" case _ => data ================================================ FILE: modules/node/src/main/resources/application.conf.sample ================================================ local { network-id = 102 port = 8081 private = "", // local private key (Hex) } wire { time-window-millis = 1000, port = 11111, peers: [ { dest: "localhost: 8081", // "address:port" public-key-summary: "", // Peer Public Key Summary (Hex) }, ], } genesis { timestamp: "2020-05-20T09:00:00.00Z", } redis { host = "localhost" port = 6379 } ================================================ FILE: modules/node/src/main/scala/io/leisuremeta/chain/node/NodeApp.scala ================================================ package io.leisuremeta.chain package node import cats.effect.Async import cats.effect.kernel.Resource import cats.effect.std.{Dispatcher, Semaphore} import cats.syntax.flatMap.given import cats.syntax.functor.given import com.linecorp.armeria.server.Server import sttp.capabilities.fs2.Fs2Streams import sttp.tapir.server.ServerEndpoint import sttp.tapir.server.armeria.cats.{ ArmeriaCatsServerInterpreter, ArmeriaCatsServerOptions, } import sttp.tapir.server.interceptor.log.DefaultServerLog import api.{LeisureMetaChainApi as Api} import api.model.{ Account, GroupId, PublicKeySummary, Transaction, TransactionWithResult, } import api.model.account.EthAddress import api.model.creator_dao.CreatorDaoId import api.model.token.{SnapshotState, TokenDefinitionId, TokenId} import api.model.voting.ProposalId import dapp.{PlayNommDAppFailure, PlayNommState} import lib.crypto.{CryptoOps, KeyPair} import lib.crypto.Hash.ops.* import repository.{BlockRepository, StateRepository, TransactionRepository} import service.{ BlockService, LocalStatusService, NodeInitializationService, StateReadService, TransactionService, } final case class NodeApp[F[_] : Async: BlockRepository: StateRepository: TransactionRepository: PlayNommState]( config: NodeConfig, ): /** **************************************************************************** * Setup Endpoints * **************************************************************************** */ // import java.time.Instant import api.model.{Block, Signed} // , StateRoot} import lib.crypto.Hash // import lib.datatype.{BigNat, UInt256} val nodeAddresses: IndexedSeq[PublicKeySummary] = config.wire.peers.map { peer => PublicKeySummary .fromHex(peer.publicKeySummary) .getOrElse( throw new IllegalArgumentException( s"invalid pub key summary: ${peer.publicKeySummary}", ), ) } val localKeyPair: KeyPair = val privateKey = scala.sys.env .get("LMNODE_PRIVATE_KEY") .map(BigInt(_, 16)) .orElse(config.local.`private`) .get CryptoOps.fromPrivate(privateKey) def getAccountServerEndpoint = Api.getAccountEndpoint.serverLogic { (a: Account) => StateReadService.getAccountInfo(a).map { case Some(info) => Right(info) case None => Left(Right(Api.NotFound(s"account not found: $a"))) } } def getEthServerEndpoint = Api.getEthEndpoint.serverLogic { (ethAddress: EthAddress) => StateReadService.getEthAccount(ethAddress).map { case Some(account) => Right(account) case None => Left(Right(Api.NotFound(s"account not found: $ethAddress"))) } } def getGroupServerEndpoint = Api.getGroupEndpoint.serverLogic { (g: GroupId) => StateReadService.getGroupInfo(g).map { case Some(info) => Right(info) case None => Left(Right(Api.NotFound(s"group not found: $g"))) } } def getBlockListServerEndpoint = Api.getBlockListEndpoint.serverLogic { (fromOption, limitOption) => BlockService .index(fromOption, limitOption) .leftMap { (errorMsg: String) => Left(Api.ServerError(errorMsg)) } .value } def getBlockServerEndpoint = Api.getBlockEndpoint.serverLogic { (blockHash: Block.BlockHash) => val result = BlockService.get(blockHash).value result.map { case Right(Some(block)) => Right(block) case Right(None) => Left(Right(Api.NotFound(s"block not found: $blockHash"))) case Left(err) => Left(Left(Api.ServerError(err))) } } def getStatusServerEndpoint = Api.getStatusEndpoint.serverLogicSuccess { _ => LocalStatusService .status[F]( networkId = config.local.networkId, genesisTimestamp = config.genesis.timestamp, ) } def getTokenDefServerEndpoint = Api.getTokenDefinitionEndpoint.serverLogic { (tokenDefinitionId: TokenDefinitionId) => StateReadService.getTokenDef(tokenDefinitionId).map { case Some(tokenDef) => Right(tokenDef) case None => Left( Right( Api.NotFound(s"token definition not found: $tokenDefinitionId"), ), ) } } def getBalanceServerEndpoint = Api.getBalanceEndpoint.serverLogic { (account, movable) => StateReadService.getBalance(account, movable).map { balanceMap => Either.cond( balanceMap.nonEmpty, balanceMap, Right(Api.NotFound(s"balance not found: $account")), ) } } def getNftBalanceServerEndpoint = Api.getNftBalanceEndpoint.serverLogic { (account, movable) => StateReadService.getNftBalance(account, movable).map { nftBalanceMap => Either.cond( nftBalanceMap.nonEmpty, nftBalanceMap, Right(Api.NotFound(s"nft balance not found: $account")), ) } } def getTokenServerEndpoint = Api.getTokenEndpoint.serverLogic { (tokenId: TokenId) => StateReadService.getToken(tokenId).value.map { case Right(Some(nftState)) => Right(nftState) case Right(None) => Left(Right(Api.NotFound(s"token not found: $tokenId"))) case Left(err) => Left(Left(Api.ServerError(err))) } } def getTokenHistoryServerEndpoint = Api.getTokenHistoryEndpoint.serverLogic { (txHash: Hash.Value[TransactionWithResult]) => StateReadService.getTokenHistory(txHash).value.map { case Right(Some(nftState)) => Right(nftState) case Right(None) => Left(Right(Api.NotFound(s"token history not found: $txHash"))) case Left(err) => Left(Left(Api.ServerError(err))) } } def getOwnersServerEndpoint = Api.getOwnersEndpoint.serverLogic { (tokenDefinitionId: TokenDefinitionId) => StateReadService .getOwners(tokenDefinitionId) .leftMap { (errMsg) => Left(Api.ServerError(errMsg)) } .value } def getAccountActivityServerEndpoint = Api.getAccountActivityEndpoint.serverLogic { (account: Account) => StateReadService .getAccountActivity(account) .leftMap { case Right(msg) => Right(Api.BadRequest(msg)) case Left(msg) => Left(Api.ServerError(msg)) } .value } def getTokenActivityServerEndpoint = Api.getTokenActivityEndpoint.serverLogic { (tokenId: TokenId) => StateReadService .getTokenActivity(tokenId) .leftMap { case Right(msg) => Right(Api.BadRequest(msg)) case Left(msg) => Left(Api.ServerError(msg)) } .value } def getAccountSnapshotServerEndpoint = Api.getAccountSnapshotEndpoint.serverLogic { (account: Account) => StateReadService .getAccountSnapshot(account) .leftMap { case Right(msg) => Right(Api.BadRequest(msg)) case Left(msg) => Left(Api.ServerError(msg)) } .subflatMap { case Some(snapshot) => Right(snapshot) case None => Left(Right(Api.NotFound(s"No snapshot of account $account"))) } .value } def getTokenSnapshotServerEndpoint = Api.getTokenSnapshotEndpoint.serverLogic { (tokenId: TokenId) => StateReadService .getTokenSnapshot(tokenId) .leftMap { case Right(msg) => Right(Api.BadRequest(msg)) case Left(msg) => Left(Api.ServerError(msg)) } .subflatMap { case Some(snapshot) => Right(snapshot) case None => Left(Right(Api.NotFound(s"No snapshot of token $tokenId"))) } .value } def getOwnershipSnapshotServerEndpoint = Api.getOwnershipSnapshotEndpoint.serverLogic { (tokenId: TokenId) => StateReadService .getOwnershipSnapshot(tokenId) .leftMap { case Right(msg) => Right(Api.BadRequest(msg)) case Left(msg) => Left(Api.ServerError(msg)) } .subflatMap { case Some(snapshot) => Right(snapshot) case None => Left(Right(Api.NotFound(s"No snapshot of token $tokenId"))) } .value } def getOwnershipSnapshotMapServerEndpoint = Api.getOwnershipSnapshotMapEndpoint.serverLogic { (from: Option[TokenId], limit: Option[Int]) => StateReadService .getOwnershipSnapshotMap(from, limit.getOrElse(100)) .leftMap { case Right(msg) => Right(Api.BadRequest(msg)) case Left(msg) => Left(Api.ServerError(msg)) } .value } def getOwnershipRewardedServerEndpoint = Api.getOwnershipRewardedEndpoint.serverLogic { (tokenId: TokenId) => StateReadService .getOwnershipRewarded(tokenId) .leftMap { case Right(msg) => Right(Api.BadRequest(msg)) case Left(msg) => Left(Api.ServerError(msg)) } .subflatMap { case Some(log) => Right(log) case None => Left(Right(Api.NotFound(s"No rewarded log of token $tokenId"))) } .value } def getDaoServerEndpoint = Api.getDaoEndpoint.serverLogic: (groupId: GroupId) => StateReadService .getDaoInfo(groupId) .leftMap: case Right(msg) => Right(Api.BadRequest(msg)) case Left(msg) => Left(Api.ServerError(msg)) .subflatMap: case Some(daoInfo) => Right(daoInfo) case None => Left(Right(Api.NotFound(s"No DAO information of group $groupId"))) .value def getSnapshotStateServerEndpoint = Api.getSnapshotStateEndpoint.serverLogic: (defId: TokenDefinitionId) => StateReadService .getSnapshotState(defId) .leftMap: case Right(msg) => Right(Api.BadRequest(msg)) case Left(msg) => Left(Api.ServerError(msg)) .subflatMap: case Some(snapshotState) => Right(snapshotState) case None => Left(Right(Api.NotFound(s"No snapshot state of token $defId"))) .value def getFungibleSnapshotBalanceServerEndpoint = Api.getFungibleSnapshotBalanceEndpoint.serverLogic: ( account: Account, defId: TokenDefinitionId, snapshotId: SnapshotState.SnapshotId, ) => StateReadService .getFungibleSnapshotBalance(account, defId, snapshotId) .leftMap: case Right(msg) => Right(Api.BadRequest(msg)) case Left(msg) => Left(Api.ServerError(msg)) .value def getNftSnapshotBalanceServerEndpoint = Api.getNftSnapshotBalanceEndpoint.serverLogic: ( account: Account, defId: TokenDefinitionId, snapshotId: SnapshotState.SnapshotId, ) => StateReadService .getNftSnapshotBalance(account, defId, snapshotId) .leftMap: case Right(msg) => Right(Api.BadRequest(msg)) case Left(msg) => Left(Api.ServerError(msg)) .value def getVoteProposalServerEndpoint = Api.getVoteProposalEndpoint.serverLogic: (proposalId: ProposalId) => StateReadService .getVoteProposal(proposalId) .leftMap: case Right(msg) => Right(Api.BadRequest(msg)) case Left(msg) => Left(Api.ServerError(msg)) .subflatMap: case Some(proposal) => Right(proposal) case None => Left(Right(Api.NotFound(s"No proposal of vote $proposalId"))) .value def getAccountVotesServerEndpoint = Api.getAccountVotesEndpoint.serverLogic: (proposalId: ProposalId, account: Account) => StateReadService .getAccountVotes(proposalId, account) .leftMap: case Right(msg) => Right(Api.BadRequest(msg)) case Left(msg) => Left(Api.ServerError(msg)) .subflatMap: case Some(proposal) => Right(proposal) case None => Left(Right(Api.NotFound(s"No vote of $proposalId by $account"))) .value def getVoteCountServerEndpoint = Api.getVoteCountEndpoint.serverLogic: (proposalId: ProposalId) => StateReadService .getVoteCount(proposalId) .leftMap: case Right(msg) => Right(Api.BadRequest(msg)) case Left(msg) => Left(Api.ServerError(msg)) .value def getCreatorDaoInfoServerEndpoint = Api.getCreatorDaoInfoEndpoint.serverLogic: (daoId: CreatorDaoId) => StateReadService .getCreatorDaoInfo(daoId) .leftMap: case Right(msg) => Right(Api.BadRequest(msg)) case Left(msg) => Left(Api.ServerError(msg)) .subflatMap: case Some(daoInfo) => Right(daoInfo) case None => Left(Right(Api.NotFound(s"No creator DAO information of $daoId"))) .value def getCreatorDaoMemberServerEndpoint = Api.getCreatorDaoMemberEndpoint.serverLogic: (daoId: CreatorDaoId, from: Option[Account], limit: Option[Int]) => StateReadService .getCreatorDaoMember(daoId, from, limit.getOrElse(100)) .leftMap: case Right(msg) => Right(Api.BadRequest(msg)) case Left(msg) => Left(Api.ServerError(msg)) .value def postTxServerEndpoint(semaphore: Semaphore[F]) = Api.postTxEndpoint.serverLogic { (txs: Seq[Signed.Tx]) => scribe.info(s"received postTx request: $txs") val result = TransactionService.submit[F](semaphore, txs, localKeyPair).value result.map { case Left(PlayNommDAppFailure.External(msg)) => scribe.info(s"external error occured in tx $txs: $msg") Left(Right(Api.BadRequest(msg))) case Left(PlayNommDAppFailure.Internal(msg)) => scribe.error(s"internal error occured in tx $txs: $msg") Left(Left(Api.ServerError(msg))) case Right(txHashes) => scribe.info(s"submitted txs: $txHashes") Right(txHashes) } } def getTxSetServerEndpoint = Api.getTxSetEndpoint.serverLogic { (block: Block.BlockHash) => TransactionService .index(block) .leftMap { case Left(serverErrorMsg) => Left(Api.ServerError(serverErrorMsg)) case Right(errorMessage) => Right(Api.NotFound(errorMessage)) } .value } def getTxServerEndpoint = Api.getTxEndpoint.serverLogic { (txHash: Signed.TxHash) => TransactionService.get(txHash).value.map { case Right(Some(tx)) => Right(tx) case Right(None) => Left(Right(Api.NotFound(s"tx not found: $txHash"))) case Left(err) => Left(Left(Api.ServerError(err))) } } def postTxHashServerEndpoint = Api.postTxHashEndpoint.serverLogicPure[F] { (txs: Seq[Transaction]) => scribe.info(s"received postTxHash request: $txs") Right(txs.map(_.toHash)) } def leisuremetaEndpoints( semaphore: Semaphore[F], ): List[ServerEndpoint[Fs2Streams[F], F]] = List( getAccountServerEndpoint, getEthServerEndpoint, getBlockListServerEndpoint, getBlockServerEndpoint, getGroupServerEndpoint, getStatusServerEndpoint, getTxServerEndpoint, getTokenDefServerEndpoint, getBalanceServerEndpoint, getNftBalanceServerEndpoint, getTokenServerEndpoint, getTokenHistoryServerEndpoint, getOwnersServerEndpoint, getTxSetServerEndpoint, getAccountActivityServerEndpoint, getTokenActivityServerEndpoint, getAccountSnapshotServerEndpoint, getTokenSnapshotServerEndpoint, getOwnershipSnapshotServerEndpoint, getOwnershipSnapshotMapServerEndpoint, getOwnershipRewardedServerEndpoint, getDaoServerEndpoint, getSnapshotStateServerEndpoint, getFungibleSnapshotBalanceServerEndpoint, getNftSnapshotBalanceServerEndpoint, getVoteProposalServerEndpoint, getAccountVotesServerEndpoint, getCreatorDaoInfoServerEndpoint, getCreatorDaoMemberServerEndpoint, postTxServerEndpoint(semaphore), postTxHashServerEndpoint, ) val localPublicKeySummary: PublicKeySummary = PublicKeySummary.fromPublicKeyHash(localKeyPair.publicKey.toHash) val localNodeIndex: Int = config.wire.peers.map(_.publicKeySummary).indexOf(localPublicKeySummary) def getServer( dispatcher: Dispatcher[F], ): F[Server] = for initializeResult <- NodeInitializationService .initialize[F](config.genesis.timestamp) .value bestBlock <- initializeResult match case Left(err) => Async[F].raiseError(Exception(err)) case Right(block) => Async[F].pure(block) semaphore <- Semaphore[F](1) server <- Async[F].fromCompletableFuture: def log[F[_]: Async]( level: scribe.Level, )(msg: String, exOpt: Option[Throwable])(using mdc: scribe.mdc.MDC, ): F[Unit] = Async[F].delay(exOpt match case None => scribe.log(level, mdc, msg) case Some(ex) => scribe.log(level, mdc, msg, ex), ) val serverLog = DefaultServerLog( doLogWhenReceived = log(scribe.Level.Info)(_, None), doLogWhenHandled = log(scribe.Level.Info), doLogAllDecodeFailures = log(scribe.Level.Info), doLogExceptions = (msg: String, ex: Throwable) => Async[F].delay(scribe.warn(msg, ex)), noLog = Async[F].pure(()), ) val serverOptions = ArmeriaCatsServerOptions .customiseInterceptors[F](dispatcher) .serverLog(serverLog) .options val tapirService = ArmeriaCatsServerInterpreter[F](serverOptions) .toService(leisuremetaEndpoints(semaphore)) val server = Server.builder .maxRequestLength(128 * 1024 * 1024) .requestTimeout(java.time.Duration.ofMinutes(10)) .http(config.local.port.value) .service(tapirService) .build Async[F].delay: server.start().thenApply(_ => server) yield server def resource: Resource[F, Server] = for dispatcher <- Dispatcher.parallel[F] server <- Resource.fromAutoCloseable(getServer(dispatcher)) yield server ================================================ FILE: modules/node/src/main/scala/io/leisuremeta/chain/node/NodeConfig.scala ================================================ package io.leisuremeta.chain package node import scala.jdk.CollectionConverters.* import scala.util.Try import cats.data.EitherT import cats.effect.Async import cats.syntax.traverse.given import com.typesafe.config.{Config, ConfigException} import eu.timepit.refined.numeric.Interval import eu.timepit.refined.types.net.PortNumber import eu.timepit.refined.refineV import api.model.NetworkId import lib.datatype.{BigNat, UInt256, UInt256BigInt} import NodeConfig.* import java.time.Instant final case class NodeConfig( local: LocalConfig, wire: WireConfig, genesis: GenesisConfig, redis: RedisConfig, ) object NodeConfig: def load[F[_]: Async](getConf: F[Config]): EitherT[F, String, NodeConfig] = for config <- EitherT.right[String](getConf) local <- EitherT.fromEither[F](LocalConfig.load(config)) wire <- EitherT.fromEither[F](WireConfig.load(config)) genesis <- EitherT.fromEither[F](GenesisConfig.load(config)) redis <- EitherT.fromEither[F](RedisConfig.load(config)) yield NodeConfig(local, wire, genesis, redis) case class LocalConfig( networkId: NetworkId, port: PortNumber, `private`: Option[UInt256BigInt], ): override def toString: String = s"LocalConfig($networkId, $port, **hidden**)" object LocalConfig: def load(config: Config): Either[String, LocalConfig] = for networkIdLong <- either(config.getLong("local.network-id")) networkId <- BigNat.fromBigInt(BigInt(networkIdLong)) portInt <- either(config.getInt("local.port")) port <- refineV[Interval.Closed[0, 65535]](portInt) privStrOption <- eitherOption(config.getString("local.private")) privOption <- privStrOption match case None => Right(None) case Some(s) => UInt256.from(BigInt(s, 16)).map(Option(_)).left.map(_.msg) yield LocalConfig(NetworkId(networkId), port, privOption) case class WireConfig( timeWindowMillis: Long, port: PortNumber, peers: IndexedSeq[PeerConfig], ) object WireConfig: def load(config: Config): Either[String, WireConfig] = for timeWindowMillis <- either(config.getLong("wire.time-window-millis")) portInt <- either(config.getInt("wire.port")) port <- refineV[Interval.Closed[0, 65535]](portInt) peers <- either(config.getConfigList("wire.peers")) peerConfigs <- peers.asScala.toVector.traverse(PeerConfig.load) yield WireConfig(timeWindowMillis, port, peerConfigs) case class PeerConfig(dest: String, publicKeySummary: String) object PeerConfig: def load(config: Config): Either[String, PeerConfig] = for dest <- either(config.getString("dest")) publicKeySummary <- either(config.getString("public-key-summary")) yield PeerConfig(dest, publicKeySummary) case class GenesisConfig(timestamp: Instant) object GenesisConfig: def load(config: Config): Either[String, GenesisConfig] = for timestampString <- either(config.getString("genesis.timestamp")) timestamp <- either(Instant.parse(timestampString)) yield GenesisConfig(timestamp) case class RedisConfig(host: String, port: Int) object RedisConfig: def load(config: Config): Either[String, RedisConfig] = for host <- either(config.getString("redis.host")) port <- either(config.getInt("redis.port")) yield RedisConfig(host, port) private def either[A](action: => A): Either[String, A] = Try(action).toEither.left.map { case e: ConfigException.Missing => s"Missing config: ${e.getMessage}" case e: ConfigException.WrongType => s"Wrong type: ${e.getMessage}" case e: Exception => s"Exception: ${e.getMessage}" } private def eitherOption[A](action: => A): Either[String, Option[A]] = Try(action) .map(Some(_)) .recover { case e: ConfigException.Missing => None } .toEither .left .map { case e: ConfigException.WrongType => s"Wrong type: ${e.getMessage}" case e: Exception => s"Exception: ${e.getMessage}" } ================================================ FILE: modules/node/src/main/scala/io/leisuremeta/chain/node/NodeMain.scala ================================================ package io.leisuremeta.chain package node //import java.time.Instant import cats.effect.{ExitCode, Resource, IO, IOApp} import com.typesafe.config.{Config, ConfigFactory} import api.model.* import dapp.PlayNommState import lib.codec.byte.ByteCodec import lib.crypto.Hash import lib.datatype.{BigNat, UInt256Bytes} import lib.merkle.MerkleTrieNode import lib.merkle.MerkleTrieNode.MerkleHash import repository.{BlockRepository, StateRepository, TransactionRepository} import repository.StateRepository.given import store.* import store.interpreter._ object NodeMain extends IOApp: def multi[K: ByteCodec, V: ByteCodec]( config: NodeConfig, target: InterpreterTarget, ): Resource[IO, KeyValueStore[IO, K, V]] = SwayInterpreter[K, V](target.s) // MultiInterpreter[K, V](config.redis, target) def getBlockRepo(config: NodeConfig): Resource[IO, BlockRepository[IO]] = for bestBlockKVStore <- multi[UInt256Bytes, Block.Header](config, InterpreterTarget.BEST_NUM) given SingleValueStore[IO, Block.Header] = SingleValueStore .fromKeyValueStore[IO, Block.Header](using bestBlockKVStore) given KeyValueStore[IO, Block.BlockHash, Block] <- multi[Hash.Value[ Block, ], Block](config, InterpreterTarget.BLOCK) given KeyValueStore[IO, BigNat, Block.BlockHash] <- multi[ BigNat, Block.BlockHash, ](config, InterpreterTarget.BLOCK_NUM) given KeyValueStore[IO, Signed.TxHash, Block.BlockHash] <- multi[ Signed.TxHash, Block.BlockHash, ](config, InterpreterTarget.TX_BLOCK) yield BlockRepository.fromStores[IO] def getStateRepo(config: NodeConfig): Resource[IO, StateRepository[IO]] = for given KeyValueStore[ IO, MerkleHash, MerkleTrieNode, ] <- multi[MerkleHash, MerkleTrieNode](config, InterpreterTarget.MERKLE_TRIE) yield StateRepository.fromStores[IO] def getTransactionRepo(config: NodeConfig): Resource[IO, TransactionRepository[IO]] = for given KeyValueStore[IO, Hash.Value[ TransactionWithResult, ], TransactionWithResult] <- multi[Hash.Value[TransactionWithResult], TransactionWithResult](config, InterpreterTarget.TX) yield TransactionRepository.fromStores[IO] override def run(args: List[String]): IO[ExitCode] = val getConfig: IO[Config] = IO.blocking(ConfigFactory.load) NodeConfig.load[IO](getConfig).value.flatMap { case Right(config) => val program = for given BlockRepository[IO] <- getBlockRepo(config) given TransactionRepository[IO] <- getTransactionRepo(config) given StateRepository[IO] <- getStateRepo(config) given PlayNommState[IO] = PlayNommState.build[IO] server <- NodeApp[IO](config).resource yield server program.use(_ => IO.never).as(ExitCode.Success) case Left(err) => IO(println(err)).as(ExitCode.Error) } ================================================ FILE: modules/node/src/main/scala/io/leisuremeta/chain/node/dapp/PlayNommDApp.scala ================================================ package io.leisuremeta.chain package node package dapp import cats.effect.Concurrent import cats.data.{EitherT, StateT} import api.model.{Signed, Transaction, TransactionWithResult} import lib.merkle.MerkleTrieState import repository.TransactionRepository import submodule.* object PlayNommDApp: def apply[F[_]: Concurrent: TransactionRepository: PlayNommState]( signedTx: Signed.Tx, ): StateT[EitherT[F, PlayNommDAppFailure, *], MerkleTrieState, TransactionWithResult] = signedTx.value match case accountTx: Transaction.AccountTx => PlayNommDAppAccount(accountTx, signedTx.sig) case groupTx: Transaction.GroupTx => PlayNommDAppGroup(groupTx, signedTx.sig) case tokenTx: Transaction.TokenTx => PlayNommDAppToken(tokenTx, signedTx.sig) case rewardTx: Transaction.RewardTx => PlayNommDAppReward(rewardTx, signedTx.sig) case agendaTx: Transaction.AgendaTx => PlayNommDAppAgenda(agendaTx, signedTx.sig) case votingTx: Transaction.VotingTx => PlayNommDAppVoting(votingTx, signedTx.sig) case creatorDaoTx: Transaction.CreatorDaoTx => PlayNommDAppCreatorDao(creatorDaoTx, signedTx.sig) ================================================ FILE: modules/node/src/main/scala/io/leisuremeta/chain/node/dapp/PlayNommDAppFailure.scala ================================================ package io.leisuremeta.chain.node.dapp import cats.{~>, Functor} import cats.arrow.FunctionK import cats.data.EitherT import cats.syntax.bifunctor.* sealed trait PlayNommDAppFailure: def msg: String object PlayNommDAppFailure: final case class External(msg: String) extends PlayNommDAppFailure final case class Internal(msg: String) extends PlayNommDAppFailure def external(msg: String): PlayNommDAppFailure = External(msg) def internal(msg: String): PlayNommDAppFailure = Internal(msg) def mapExternal[F[_]: Functor]( msg: String, ): EitherT[F, String, *] ~> EitherT[F, PlayNommDAppFailure, *] = FunctionK.lift { [A] => (stringOr: EitherT[F, String, A]) => stringOr.leftMap(e => PlayNommDAppFailure.external(s"$msg: $e")) } def mapInternal[F[_]: Functor]( msg: String, ): EitherT[F, String, *] ~> EitherT[F, PlayNommDAppFailure, *] = FunctionK.lift { [A] => (stringOr: EitherT[F, String, A]) => stringOr.leftMap(e => PlayNommDAppFailure.internal(s"$msg: $e")) } ================================================ FILE: modules/node/src/main/scala/io/leisuremeta/chain/node/dapp/PlayNommState.scala ================================================ package io.leisuremeta.chain package node package dapp import cats.Monad import api.model.* import api.model.{Account as AccountM} import api.model.account.* import api.model.token.* import api.model.reward.* import api.model.voting.* import api.model.creator_dao.* import lib.crypto.Hash import lib.datatype.{BigNat, Utf8} import lib.merkle.MerkleTrie.NodeStore import lib.application.DAppState import java.time.Instant trait PlayNommState[F[_]]: def account: PlayNommState.Account[F] def group: PlayNommState.Group[F] def token: PlayNommState.Token[F] def reward: PlayNommState.Reward[F] def voting: PlayNommState.Voting[F] def creatorDao: PlayNommState.CreatorDao[F] object PlayNommState: def apply[F[_]: PlayNommState]: PlayNommState[F] = summon case class Account[F[_]]( name: DAppState[F, AccountM, AccountData], key: DAppState[F, (AccountM, PublicKeySummary), PublicKeySummary.Info], externalChainAddresses: DAppState[F, (ExternalChain, ExternalChainAddress), AccountM], ) case class Reward[F[_]]( dao: DAppState[F, GroupId, DaoInfo], accountActivity: DAppState[F, (AccountM, Instant), Seq[ActivityLog]], tokenReceived: DAppState[F, (TokenId, Instant), Seq[ActivityLog]], accountSnapshot: DAppState[F, AccountM, ActivitySnapshot], tokenSnapshot: DAppState[F, TokenId, ActivitySnapshot], ownershipSnapshot: DAppState[F, TokenId, OwnershipSnapshot], accountRewarded: DAppState[F, AccountM, ActivityRewardLog], tokenRewarded: DAppState[F, TokenId, ActivityRewardLog], ownershipRewarded: DAppState[F, TokenId, OwnershipRewardLog], ) case class Group[F[_]]( group: DAppState[F, GroupId, GroupData], groupAccount: DAppState[F, (GroupId, AccountM), Unit], ) type TxHash = Hash.Value[TransactionWithResult] type BalanceAmount = BigNat case class Token[F[_]]( definition: DAppState[F, TokenDefinitionId, TokenDefinition], fungibleBalance: DAppState[ F, (AccountM, TokenDefinitionId, TxHash), Unit, ], nftBalance: DAppState[F, (AccountM, TokenId, TxHash), Unit], nftState: DAppState[F, TokenId, NftState], nftHistory: DAppState[F, TxHash, NftState], rarityState: DAppState[F, (TokenDefinitionId, Rarity, TokenId), Unit], entrustFungibleBalance: DAppState[ F, (AccountM, AccountM, TokenDefinitionId, TxHash), Unit, ], entrustNftBalance: DAppState[ F, (AccountM, AccountM, TokenId, TxHash), Unit, ], snapshotState: DAppState[F, TokenDefinitionId, SnapshotState], fungibleSnapshot: DAppState[ F, (AccountM, TokenDefinitionId, SnapshotState.SnapshotId), Map[TxHash, BalanceAmount], ], nftSnapshot: DAppState[ F, (AccountM, TokenDefinitionId, SnapshotState.SnapshotId), Set[TokenId], ], totalSupplySnapshot: DAppState[ F, (TokenDefinitionId, SnapshotState.SnapshotId), BalanceAmount, ], ) case class Voting[F[_]]( proposal: DAppState[F, ProposalId, Proposal], votes: DAppState[F, (ProposalId, AccountM), (Utf8, BigNat)], counting: DAppState[F, ProposalId, Map[Utf8, BigNat]], ) case class CreatorDao[F[_]]( dao: DAppState[F, CreatorDaoId, CreatorDaoData], daoModerators: DAppState[F, (CreatorDaoId, AccountM), Unit], daoMembers: DAppState[F, (CreatorDaoId, AccountM), Unit], ) def build[F[_]: Monad: NodeStore]: PlayNommState[F] = scribe.info(s"Building PlayNommState... ") val playNommState = DAppState.WithCommonPrefix("playnomm") new PlayNommState: val account: Account[F] = Account[F]( name = playNommState.ofName("name"), key = playNommState.ofName("key"), externalChainAddresses = playNommState.ofName("pca"), ) val group: Group[F] = Group[F]( group = playNommState.ofName("group"), groupAccount = playNommState.ofName("group-account"), ) val token: Token[F] = Token[F]( definition = playNommState.ofName("token-def"), fungibleBalance = playNommState.ofName("fungible-balance"), nftBalance = playNommState.ofName("nft-balance"), nftState = playNommState.ofName("nft-state"), nftHistory = playNommState.ofName("nft-history"), rarityState = playNommState.ofName("rarity-state"), entrustFungibleBalance = playNommState.ofName("entrust-fungible-balance"), entrustNftBalance = playNommState.ofName("entrust-nft-balance"), snapshotState = playNommState.ofName("snapshot-state"), fungibleSnapshot = playNommState.ofName("fungible-snapshot"), nftSnapshot = playNommState.ofName("nft-snapshot"), totalSupplySnapshot = playNommState.ofName("total-supply-snapshot"), ) val reward: Reward[F] = Reward[F]( dao = playNommState.ofName("dao"), accountActivity = playNommState.ofName("account-activity"), tokenReceived = playNommState.ofName("token-received"), accountSnapshot = playNommState.ofName("account-snapshot"), tokenSnapshot = playNommState.ofName("token-snapshot"), ownershipSnapshot = playNommState.ofName("ownership-snapshot"), accountRewarded = playNommState.ofName("account-rewarded"), tokenRewarded = playNommState.ofName("token-rewarded"), ownershipRewarded = playNommState.ofName("ownership-rewarded"), ) val voting: Voting[F] = Voting[F]( proposal = playNommState.ofName("proposal"), votes = playNommState.ofName("votes"), counting = playNommState.ofName("counting"), ) val creatorDao: CreatorDao[F] = CreatorDao[F]( dao = playNommState.ofName("creator-dao"), daoModerators = playNommState.ofName("dao-moderators"), daoMembers = playNommState.ofName("dao-members"), ) ================================================ FILE: modules/node/src/main/scala/io/leisuremeta/chain/node/dapp/submodule/PlayNommDAppAccount.scala ================================================ package io.leisuremeta.chain package node package dapp package submodule import java.time.temporal.ChronoUnit import cats.Monad import cats.data.{EitherT, StateT} import cats.effect.Concurrent import cats.syntax.eq.* import cats.syntax.traverse.* import api.model.{ Account, AccountData, AccountSignature, PublicKeySummary, Signed, Transaction, TransactionWithResult, } import api.model.account.{ExternalChain, ExternalChainAddress} import lib.crypto.Hash.ops.* import lib.crypto.Recover.ops.* import lib.datatype.Utf8 import lib.merkle.MerkleTrieState object PlayNommDAppAccount: def apply[F[_]: Concurrent: PlayNommState]( tx: Transaction.AccountTx, sig: AccountSignature, ): StateT[ EitherT[F, PlayNommDAppFailure, *], MerkleTrieState, TransactionWithResult, ] = tx match case ca: Transaction.AccountTx.CreateAccount => for accountInfoOption <- getAccountInfo(ca.account) _ <- checkExternal( accountInfoOption.isEmpty, s"${ca.account} already exists", ) _ <- checkExternal( sig.account == ca.account || Some(sig.account) == ca.guardian, s"Signer ${sig.account} is neither ${ca.account} nor its guardian", ) initialPKS <- getPKS(sig, ca) keyInfo = PublicKeySummary.Info( addedAt = ca.createdAt, description = Utf8.unsafeFrom(s"automatically added at account creation"), expiresAt = Some(ca.createdAt.plus(40, ChronoUnit.DAYS)), ) _ <- if Option(sig.account) === ca.guardian then unit else PlayNommState[F].account.key .put((ca.account, initialPKS), keyInfo) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to put account key ${ca.account}" accountData = AccountData( guardian = ca.guardian, externalChainAddresses = ca.ethAddress.fold(Map.empty): ethAddress => Map(ExternalChain.ETH -> ExternalChainAddress(ethAddress.utf8)), lastChecked = ca.createdAt, memo = None, ) _ <- PlayNommState[F].account.name .put(ca.account, accountData) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to put account ${ca.account}" _ <- ca.ethAddress.fold(unit): ethAddress => PlayNommState[F].account.externalChainAddresses .put( (ExternalChain.ETH, ExternalChainAddress(ethAddress.utf8)), ca.account, ) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to update eth address ${ca.ethAddress}" yield TransactionWithResult(Signed(sig, ca))(None) case ca: Transaction.AccountTx.CreateAccountWithExternalChainAddresses => for accountInfoOption <- getAccountInfo(ca.account) _ <- checkExternal( accountInfoOption.isEmpty, s"${ca.account} already exists", ) _ <- checkExternal( sig.account == ca.account || Some(sig.account) == ca.guardian, s"Signer ${sig.account} is neither ${ca.account} nor its guardian", ) initialPKS <- getPKS(sig, ca) keyInfo = PublicKeySummary.Info( addedAt = ca.createdAt, description = Utf8.unsafeFrom(s"automatically added at account creation"), expiresAt = Some(ca.createdAt.plus(40, ChronoUnit.DAYS)), ) _ <- if Option(sig.account) === ca.guardian then unit else PlayNommState[F].account.key .put((ca.account, initialPKS), keyInfo) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to put account key ${ca.account}" accountData = AccountData( guardian = ca.guardian, externalChainAddresses = ca.externalChainAddresses, lastChecked = ca.createdAt, memo = ca.memo, ) _ <- PlayNommState[F].account.name .put(ca.account, accountData) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to put account ${ca.account}" _ <- ca.externalChainAddresses.toSeq.traverse: (ExternalChain, ExternalChainAddress) => PlayNommState[F].account.externalChainAddresses .put((ExternalChain, ExternalChainAddress), ca.account) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to put public chain address ${((ExternalChain, ExternalChainAddress), ca.account)}" yield TransactionWithResult(Signed(sig, ca))(None) case ua: Transaction.AccountTx.UpdateAccount => for _ <- verifySignature(sig, ua) accountDataOption <- getAccountInfo(ua.account) accountData <- fromOption( accountDataOption, s"${ua.account} does not exists", ) _ <- checkExternal( sig.account == ua.account || Some(sig.account) == accountData.guardian, s"Signer ${sig.account} is neither ${ua.account} nor its guardian", ) accountData1 = accountData.copy( guardian = ua.guardian, externalChainAddresses = ua.ethAddress.fold(Map.empty): ethAddress => Map(ExternalChain.ETH -> ExternalChainAddress(ethAddress.utf8)), lastChecked = ua.createdAt, memo = None, ) _ <- PlayNommState[F].account.name .put(ua.account, accountData1) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to put account ${ua.account}" _ <- ua.ethAddress.fold(unit): ethAddress => PlayNommState[F].account.externalChainAddresses .put( (ExternalChain.ETH, ExternalChainAddress(ethAddress.utf8)), ua.account, ) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to update eth address ${ua.ethAddress}" yield TransactionWithResult(Signed(sig, ua))(None) case ua: Transaction.AccountTx.UpdateAccountWithExternalChainAddresses => for _ <- verifySignature(sig, ua) accountDataOption <- getAccountInfo(ua.account) accountData <- fromOption( accountDataOption, s"${ua.account} does not exists", ) _ <- checkExternal( sig.account == ua.account || Some(sig.account) == accountData.guardian, s"Signer ${sig.account} is neither ${ua.account} nor its guardian", ) accountData1 = accountData.copy( guardian = ua.guardian, externalChainAddresses = ua.externalChainAddresses, lastChecked = ua.createdAt, memo = ua.memo, ) _ <- PlayNommState[F].account.name .put(ua.account, accountData1) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to put account ${ua.account}" _ <- ua.externalChainAddresses.toSeq.traverse: (ExternalChain, ExternalChainAddress) => PlayNommState[F].account.externalChainAddresses .put((ExternalChain, ExternalChainAddress), ua.account) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to update public chain address ${((ExternalChain, ExternalChainAddress), ua.account)}" yield TransactionWithResult(Signed(sig, ua))(None) case ap: Transaction.AccountTx.AddPublicKeySummaries => for _ <- verifySignature(sig, ap) accountDataOption <- getAccountInfo(ap.account) accountData <- fromOption( accountDataOption, s"${ap.account} does not exists", ) _ <- checkExternal( sig.account == ap.account || Some(sig.account) == accountData.guardian, s"Signer ${sig.account} is neither ${ap.account} nor its guardian ${accountData.guardian}", ) timeToCheck = accountData.lastChecked .plus(10, ChronoUnit.DAYS) .compareTo(ap.createdAt) < 0 // toRemove <- // if !timeToCheck then pure(Vector.empty) // else // PlayNommState[F].account.key // .from(ap.account.toBytes) // .flatMapF { stream => // stream // .filter { case (_, info) => // info.expiresAt match // case None => false // case Some(time) => // time // .plus(40, ChronoUnit.DAYS) // .compareTo(ap.createdAt) < 0 // } // .map { case ((account, pks), info) => // pks -> info.description // } // .compile // .toVector // } // .mapK(PlayNommDAppFailure.mapInternal { // s"Fail to get PKSes of account ${ap.account}" // }) // _ <- toRemove.traverse { case (pks, _) => // PlayNommState[F].account.key // .remove((ap.account, pks)) // .mapK(PlayNommDAppFailure.mapInternal { // s"Fail to remove old PKS $pks from account ${ap.account}" // }) // } _ <- ap.summaries.toSeq.traverse: (pks, description) => val expiresAt = if description === Utf8.unsafeFrom("permanent") then None else Some(ap.createdAt.plus(40, ChronoUnit.DAYS)) val keyInfo = PublicKeySummary.Info( addedAt = ap.createdAt, description = description, expiresAt = expiresAt, ) PlayNommState[F].account.key .put((ap.account, pks), keyInfo) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to put PKS $pks of account ${ap.account} with key info $PublicKeySummary.Info" txResult = Some: Transaction.AccountTx.AddPublicKeySummariesResult(Map.empty) yield TransactionWithResult(Signed(sig, ap))(txResult) def verifySignature[F[_]: Concurrent: PlayNommState]( sig: AccountSignature, tx: Transaction, ): StateT[EitherT[F, PlayNommDAppFailure, *], MerkleTrieState, Unit] = for pks <- getPKS(sig, tx) keyInfoOption <- PlayNommState[F].account.key .get((sig.account, pks)) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to decode key info of ${(sig.account, pks)}" // _ <- PlayNommState[F].account.key // .from(sig.account.toBytes) // .flatMapF: stream => // stream.compile.toList.map: list => // scribe.info(s"===> PKS: $list") // .mapK: // PlayNommDAppFailure.mapInternal: // s"Fail to get stream of PKSes of account ${sig.account}" keyInfo <- fromOption( keyInfoOption, s"There is no public key summary $pks from account ${sig.account}", ) newExpiresAt = keyInfo.expiresAt.map: instant => Seq(instant, tx.createdAt.plus(30, ChronoUnit.DAYS)) .maxBy(_.toEpochMilli()) _ <- if keyInfo.expiresAt.map(_.toEpochMilli()) === newExpiresAt.map(_.toEpochMilli()) then StateT .pure[EitherT[F, PlayNommDAppFailure, *], MerkleTrieState, Unit](()) else PlayNommState[F].account.key .put((sig.account, pks), keyInfo.copy(expiresAt = newExpiresAt)) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to update key info of ${(sig.account, pks)} with $keyInfo" yield () def getAccountInfo[F[_]: Monad: PlayNommState]( account: Account, ): StateT[EitherT[F, PlayNommDAppFailure, *], MerkleTrieState, Option[ AccountData, ]] = PlayNommState[F].account.name .get(account) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to decode account ${account}" def getPKS[F[_]: Monad: PlayNommState]( sig: AccountSignature, tx: Transaction, ): StateT[ EitherT[F, PlayNommDAppFailure, *], MerkleTrieState, PublicKeySummary, ] = fromOption( tx.toHash.recover(sig.sig), s"Fail to recover public key from $tx", ).map: pubKey => PublicKeySummary.fromPublicKeyHash(pubKey.toHash) ================================================ FILE: modules/node/src/main/scala/io/leisuremeta/chain/node/dapp/submodule/PlayNommDAppAgenda.scala ================================================ package io.leisuremeta.chain package node package dapp package submodule import cats.data.{EitherT, StateT} import cats.effect.Concurrent import cats.syntax.all.* import api.model.{ Account, AccountSignature, Signed, Transaction, TransactionWithResult, } import lib.codec.byte.ByteEncoder.ops.* import lib.crypto.Hash import lib.datatype.BigNat import lib.merkle.MerkleTrieState import repository.TransactionRepository object PlayNommDAppAgenda: def apply[F[_]: Concurrent: PlayNommState: TransactionRepository]( tx: Transaction.AgendaTx, sig: AccountSignature, ): StateT[ EitherT[F, PlayNommDAppFailure, *], MerkleTrieState, TransactionWithResult, ] = tx match case ssa: Transaction.AgendaTx.SuggestSimpleAgenda => PlayNommDAppAccount .verifySignature[F](sig, ssa) .map: _ => TransactionWithResult(Signed(sig, ssa), None) case vsa: Transaction.AgendaTx.VoteSimpleAgenda => for agendaTx <- StateT.liftF(getSuggestSimpleAgendaTx(vsa.agendaTxHash)) fungibleBalanceStream <- PlayNommState[F].token.fungibleBalance .streamWithPrefix((sig.account, agendaTx.votingToken).toBytes) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to get fungible balance stream of ${sig.account} ${agendaTx.votingToken}" fungibleBalanceTxs <- StateT .liftF: fungibleBalanceStream .map(_._1._3) .compile .toList .mapK: PlayNommDAppFailure.mapInternal: s"Fail to get fungible balance txs of ${sig.account} ${agendaTx.votingToken}" freeBalance <- PlayNommDAppToken.getFungibleBalanceTotalAmounts( fungibleBalanceTxs.toSet, sig.account, ) entrustBalanceStream <- PlayNommState[F].token.entrustFungibleBalance .streamWithPrefix(sig.account.toBytes) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to get entrust balance stream of ${sig.account} ${agendaTx.votingToken}" enturstBalanceAmounts <- StateT .liftF: entrustBalanceStream .filter(_._1._3 === agendaTx.votingToken) .map(_._1._4) .evalMap: txHash => TransactionRepository[F] .get(txHash) .leftMap(_.msg) .map: case Some(txWithResult) => txWithResult.signedTx.value match case ef: Transaction.TokenTx.EntrustFungibleToken => ef.amount case _ => BigNat.Zero case None => BigNat.Zero .compile .toList .mapK: PlayNommDAppFailure.mapInternal: s"Fail to get entrust balance txs of ${sig.account} ${agendaTx.votingToken}" entrustBalance = enturstBalanceAmounts.foldLeft(BigNat.Zero)(BigNat.add) votingAmount = BigNat.add(freeBalance, entrustBalance) txResult = Transaction.AgendaTx.VoteSimpleAgendaResult(votingAmount) yield TransactionWithResult(Signed(sig, vsa), Some(txResult)) def getSuggestSimpleAgendaTx[F[_]: Concurrent: TransactionRepository]( txHash: Hash.Value[TransactionWithResult], ): EitherT[F, PlayNommDAppFailure, Transaction.AgendaTx.SuggestSimpleAgenda] = for txWithResultOption <- TransactionRepository[F] .get(txHash) .leftMap: e => PlayNommDAppFailure.internal(s"Fail to get tx $txHash: ${e.msg}") txWithResult <- EitherT.fromOption( txWithResultOption, PlayNommDAppFailure.external(s"AgendaTx ${txHash} not found"), ) agendaTx <- txWithResult.signedTx.value match case ssa: Transaction.AgendaTx.SuggestSimpleAgenda => EitherT.pure(ssa) case _ => EitherT.leftT: PlayNommDAppFailure.external: s"AgendaTx ${txHash} is not SuggestSimpleAgenda" yield agendaTx ================================================ FILE: modules/node/src/main/scala/io/leisuremeta/chain/node/dapp/submodule/PlayNommDAppCreatorDao.scala ================================================ package io.leisuremeta.chain package node package dapp package submodule import cats.Functor import cats.data.{EitherT, StateT} import cats.effect.Concurrent import cats.syntax.all.* import api.model.{ Account, AccountSignature, Signed, Transaction, TransactionWithResult, } import api.model.creator_dao.* import lib.merkle.MerkleTrieState import repository.TransactionRepository object PlayNommDAppCreatorDao: def apply[F[_]: Concurrent: PlayNommState: TransactionRepository]( tx: Transaction.CreatorDaoTx, sig: AccountSignature, ): StateT[ EitherT[F, PlayNommDAppFailure, *], MerkleTrieState, TransactionWithResult, ] = tx match case cd: Transaction.CreatorDaoTx.CreateCreatorDao => for _ <- PlayNommDAppAccount.verifySignature(sig, tx) daoDataOption <- PlayNommState[F].creatorDao.dao .get(cd.id) .mapK: PlayNommDAppFailure.mapInternal: s"Failed to get CreatorDao with id ${cd.id}" _ <- checkExternal( daoDataOption.isEmpty, s"CreatorDao with id ${cd.id} already exists", ) daoData = CreatorDaoData( id = cd.id, name = cd.name, description = cd.description, founder = sig.account, coordinator = cd.coordinator, ) _ <- PlayNommState[F].creatorDao.dao .put(cd.id, daoData) .mapK: PlayNommDAppFailure.mapInternal: s"Failed to put CreatorDao with id ${cd.id}" yield TransactionWithResult(Signed(sig, cd))(None) case ud: Transaction.CreatorDaoTx.UpdateCreatorDao => for _ <- PlayNommDAppAccount.verifySignature(sig, tx) daoDataOption <- PlayNommState[F].creatorDao.dao .get(ud.id) .mapK: PlayNommDAppFailure.mapInternal: s"Failed to get CreatorDao with id ${ud.id}" daoData <- fromOption( daoDataOption, s"CreatorDao with id ${ud.id} does not exist", ) founderOrCoordinator = sig.account === daoData.founder || sig.account === daoData.coordinator hasAuth <- if founderOrCoordinator then pure(true) else isModerator(ud.id, sig.account) _ <- checkExternal( hasAuth, s"Account ${sig.account} is not authorized to update CreatorDao with id ${ud.id}", ) daoData1 = daoData.copy( name = ud.name, description = ud.description, ) _ <- PlayNommState[F].creatorDao.dao .put(ud.id, daoData1) .mapK: PlayNommDAppFailure.mapInternal: s"Failed to put CreatorDao with id ${ud.id}" yield TransactionWithResult(Signed(sig, ud))(None) case dd: Transaction.CreatorDaoTx.DisbandCreatorDao => for _ <- PlayNommDAppAccount.verifySignature(sig, tx) daoDataOption <- PlayNommState[F].creatorDao.dao .get(dd.id) .mapK: PlayNommDAppFailure.mapInternal: s"Failed to get CreatorDao with id ${dd.id}" daoData <- fromOption( daoDataOption, s"CreatorDao with id ${dd.id} does not exist", ) founderOrCoordinator = sig.account === daoData.founder || sig.account === daoData.coordinator _ <- checkExternal( founderOrCoordinator, s"Account ${sig.account} is not authorized to disband DAO ${dd.id}", ) _ <- PlayNommState[F].creatorDao.dao .remove(dd.id) .mapK: PlayNommDAppFailure.mapInternal: s"Failed to remove CreatorDao with id ${dd.id}" yield TransactionWithResult(Signed(sig, dd))(None) case rc: Transaction.CreatorDaoTx.ReplaceCoordinator => for _ <- PlayNommDAppAccount.verifySignature(sig, tx) daoDataOption <- PlayNommState[F].creatorDao.dao .get(rc.id) .mapK: PlayNommDAppFailure.mapInternal: s"Failed to get CreatorDao with id ${rc.id}" daoData <- fromOption( daoDataOption, s"CreatorDao with id ${rc.id} does not exist", ) isCurrentCoordinator = sig.account === daoData.coordinator _ <- checkExternal( isCurrentCoordinator, s"Only the current Coordinator can replace the Coordinator for DAO ${rc.id}", ) updatedDaoData = daoData.copy(coordinator = rc.newCoordinator) _ <- PlayNommState[F].creatorDao.dao .put(rc.id, updatedDaoData) .mapK: PlayNommDAppFailure.mapInternal: s"Failed to put CreatorDao with id ${rc.id}" yield TransactionWithResult(Signed(sig, rc))(None) case am: Transaction.CreatorDaoTx.AddMembers => for _ <- PlayNommDAppAccount.verifySignature(sig, tx) daoDataOption <- PlayNommState[F].creatorDao.dao .get(am.id) .mapK: PlayNommDAppFailure.mapInternal: s"Failed to get CreatorDao with id ${am.id}" daoData <- fromOption( daoDataOption, s"CreatorDao with id ${am.id} does not exist", ) founderOrCoordinator = sig.account === daoData.founder || sig.account === daoData.coordinator hasAuth <- if founderOrCoordinator then pure(true) else isModerator(am.id, sig.account) _ <- checkExternal( hasAuth, s"Account ${sig.account} is not authorized to add members to DAO ${am.id}", ) _ <- am.members.toSeq.traverse: member => PlayNommState[F].creatorDao.daoMembers .put((am.id, member), ()) .mapK: PlayNommDAppFailure.mapInternal: s"Failed to put CreatorDaoMember with id ${am.id} and member ${member}" yield TransactionWithResult(Signed(sig, am))(None) case rm: Transaction.CreatorDaoTx.RemoveMembers => for _ <- PlayNommDAppAccount.verifySignature(sig, tx) daoDataOption <- PlayNommState[F].creatorDao.dao .get(rm.id) .mapK: PlayNommDAppFailure.mapInternal: s"Failed to get CreatorDao with id ${rm.id}" daoData <- fromOption( daoDataOption, s"CreatorDao with id ${rm.id} does not exist", ) founderOrCoordinator = sig.account === daoData.founder || sig.account === daoData.coordinator hasAuth <- if founderOrCoordinator then pure(true) else isModerator(rm.id, sig.account) _ <- checkExternal( hasAuth, s"Account ${sig.account} is not authorized to remove members from DAO ${rm.id}", ) _ <- rm.members.toSeq.traverse: member => PlayNommState[F].creatorDao.daoMembers .remove((rm.id, member)) .mapK: PlayNommDAppFailure.mapInternal: s"Failed to remove CreatorDaoMember with id ${rm.id} and member ${member}" yield TransactionWithResult(Signed(sig, rm))(None) case pm: Transaction.CreatorDaoTx.PromoteModerators => for _ <- PlayNommDAppAccount.verifySignature(sig, tx) daoDataOption <- PlayNommState[F].creatorDao.dao .get(pm.id) .mapK: PlayNommDAppFailure.mapInternal: s"Failed to get CreatorDao with id ${pm.id}" daoData <- fromOption( daoDataOption, s"CreatorDao with id ${pm.id} does not exist", ) hasPermission = daoData.founder == sig.account || daoData.coordinator == sig.account _ <- checkExternal( hasPermission, s"Account ${sig.account} does not have permission to promote moderators in DAO ${pm.id}", ) _ <- pm.members.toSeq.traverse: member => PlayNommState[F].creatorDao.daoModerators .put((pm.id, member), ()) .mapK: PlayNommDAppFailure.mapInternal: s"Failed to put CreatorDaoModerator with id ${pm.id} and member ${member}" yield TransactionWithResult(Signed(sig, pm))(None) case dm: Transaction.CreatorDaoTx.DemoteModerators => for _ <- PlayNommDAppAccount.verifySignature(sig, tx) daoDataOption <- PlayNommState[F].creatorDao.dao .get(dm.id) .mapK: PlayNommDAppFailure.mapInternal: s"Failed to get CreatorDao with id ${dm.id}" daoData <- fromOption( daoDataOption, s"CreatorDao with id ${dm.id} does not exist", ) hasPermission = daoData.founder == sig.account || daoData.coordinator == sig.account _ <- checkExternal( hasPermission, s"Account ${sig.account} does not have permission to demote moderators in DAO ${dm.id}", ) _ <- dm.members.toSeq.traverse: member => PlayNommState[F].creatorDao.daoModerators .remove((dm.id, member)) .mapK: PlayNommDAppFailure.mapInternal: s"Failed to remove CreatorDaoModerator with id ${dm.id} and member ${member}" yield TransactionWithResult(Signed(sig, dm))(None) def isModerator[F[_]: Functor: PlayNommState](id: CreatorDaoId, account: Account): StateT[ EitherT[F, PlayNommDAppFailure, *], MerkleTrieState, Boolean, ] = PlayNommState[F].creatorDao.daoModerators .get((id, account)) .map(_.isDefined) .mapK: PlayNommDAppFailure.mapInternal: s"Failed to get CreatorDaoModerator with id ${id} and account ${account}" ================================================ FILE: modules/node/src/main/scala/io/leisuremeta/chain/node/dapp/submodule/PlayNommDAppGroup.scala ================================================ package io.leisuremeta.chain package node package dapp package submodule import cats.data.{EitherT, StateT} import cats.effect.Concurrent import cats.syntax.all.* import api.model.{ Account, AccountSignature, GroupData, Signed, Transaction, TransactionWithResult, } import lib.merkle.MerkleTrieState object PlayNommDAppGroup: def apply[F[_]: Concurrent: PlayNommState]( tx: Transaction.GroupTx, sig: AccountSignature, ): StateT[ EitherT[F, PlayNommDAppFailure, *], MerkleTrieState, TransactionWithResult, ] = tx match case cg: Transaction.GroupTx.CreateGroup => for accountData <- PlayNommDAppAccount.verifySignature[F](sig, cg) _ <- checkExternal[F]( cg.coordinator === sig.account, s"Account does not match signature: ${cg.coordinator} vs ${sig.account}", ) _ <- PlayNommState[F].group.group .put(cg.groupId, GroupData(cg.name, cg.coordinator)) .mapK: PlayNommDAppFailure .mapInternal[F](s"Failed to create group: ${cg.groupId}") yield TransactionWithResult(Signed(sig, cg), None) case aa: Transaction.GroupTx.AddAccounts => for groupDataOption <- PlayNommState[F].group.group .get(aa.groupId) .mapK: PlayNommDAppFailure .mapInternal[F](s"Failed to get group ${aa.groupId}") groupData <- fromOption( groupDataOption, s"Group does not exist: ${aa.groupId}", ) _ <- checkExternal[F]( groupData.coordinator === sig.account, s"Account does not match signature: ${groupData.coordinator} vs ${sig.account}", ) _ <- PlayNommDAppAccount.verifySignature[F](sig, aa) _ <- aa.accounts.toList.traverse: account => PlayNommState[F].group.groupAccount .put((aa.groupId, account), ()) .mapK: PlayNommDAppFailure.mapInternal[F]: s"Failed to add an account $account to group: ${aa.groupId}" yield TransactionWithResult(Signed(sig, aa), None) ================================================ FILE: modules/node/src/main/scala/io/leisuremeta/chain/node/dapp/submodule/PlayNommDAppReward.scala ================================================ package io.leisuremeta.chain package node package dapp package submodule import cats.data.{EitherT, StateT} import cats.effect.Concurrent import cats.syntax.traverse.* import api.model.{ AccountSignature, Signed, Transaction, TransactionWithResult, } import api.model.reward.{ ActivityLog, DaoInfo, } import api.model.TransactionWithResult.ops.* import lib.crypto.Hash import lib.crypto.Hash.ops.* import lib.datatype.BigNat import lib.merkle.MerkleTrieState import repository.TransactionRepository object PlayNommDAppReward: def apply[F[_]: Concurrent: TransactionRepository: PlayNommState]( tx: Transaction.RewardTx, sig: AccountSignature, ): StateT[ EitherT[F, PlayNommDAppFailure, *], MerkleTrieState, TransactionWithResult, ] = tx match case rd: Transaction.RewardTx.RegisterDao => for groupDataOption <- PlayNommState[F].group.group .get(rd.groupId) .mapK: PlayNommDAppFailure.mapInternal(s"Fail to get group ${rd.groupId}") _ <- checkExternal( groupDataOption.isDefined, s"Group ${rd.groupId} not found", ) daoInfoOption <- PlayNommState[F].reward.dao .get(rd.groupId) .mapK: PlayNommDAppFailure.mapInternal(s"Fail to get group ${rd.groupId}") _ <- checkExternal( daoInfoOption.isEmpty, s"Group ${rd.groupId} already has DAO", ) daoInfo = DaoInfo(moderators = rd.moderators) _ <- PlayNommState[F].reward.dao .put(rd.groupId, daoInfo) .mapK: PlayNommDAppFailure.mapInternal(s"Fail to put DAO ${rd.groupId}") yield TransactionWithResult(Signed(sig, tx), None) case or: Transaction.RewardTx.OfferReward => for _ <- PlayNommDAppAccount.verifySignature(sig, tx) tokenDef <- PlayNommDAppToken.getTokenDefinition(or.tokenDefinitionId) inputAmount <- PlayNommDAppToken.getFungibleBalanceTotalAmounts( or.inputs.map(_.toResultHashValue), sig.account, ) outputAmount = or.outputs.values.foldLeft(BigNat.Zero)(BigNat.add) diff <- fromEitherExternal: BigNat.tryToSubtract(inputAmount, outputAmount) txWithResult = TransactionWithResult(Signed(sig, or))(None) txHash = txWithResult.toHash _ <- PlayNommDAppToken.removeInputUtxos( sig.account, or.inputs.map(_.toResultHashValue), or.tokenDefinitionId, ) _ <- or.inputs.toList.traverse: inputTxHash => PlayNommDAppToken .removeFungibleSnapshot( sig.account, or.tokenDefinitionId, inputTxHash.toResultHashValue, ) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to remove fungible snapshot of $inputTxHash" _ <- or.outputs.toSeq.traverse: case (account, outputAmount) => for _ <- PlayNommDAppToken .putBalance(account, or.tokenDefinitionId, txHash) _ <- PlayNommDAppToken .addFungibleSnapshot( account, or.tokenDefinitionId, txHash, outputAmount, ) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to put fungible snapshot of $txHash" yield () totalAmount <- fromEitherInternal: BigNat.tryToSubtract(tokenDef.totalAmount, diff) _ <- PlayNommDAppToken.putTokenDefinition( or.tokenDefinitionId, tokenDef.copy(totalAmount = totalAmount), ) - <- PlayNommDAppToken .removeTotalSupplySnapshot(or.tokenDefinitionId, diff) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to remove total supply snapshot of ${or.tokenDefinitionId}" yield txWithResult case tx: Transaction.RewardTx.UpdateDao => ??? case tx: Transaction.RewardTx.RecordActivity => val txResult = TransactionWithResult(Signed(sig, tx), None) val txHash = txResult.toHash for _ <- tx.userActivity.toList.traverse { case (account, activities) => val logs = activities.map { a => ActivityLog(a.point, a.description, txHash) } PlayNommState[F].reward.accountActivity .put((account, tx.timestamp), logs) .mapK { PlayNommDAppFailure.mapInternal { s"Fail to put account activity in $txHash" } } } _ <- tx.tokenReceived.toList.traverse { case (account, activities) => val logs = activities.map { a => ActivityLog(a.point, a.description, txHash) } PlayNommState[F].reward.tokenReceived .put((account, tx.timestamp), logs) .mapK { PlayNommDAppFailure.mapInternal { s"Fail to put account activity in $txHash" } } } yield txResult case tx: Transaction.RewardTx.ExecuteReward => ??? case tx: Transaction.RewardTx.BuildSnapshot => ??? // val getNftStateStream = // StateT // .inspectF { (ms: MerkleState) => // GenericMerkleTrie // .from[F, TokenId, NftState](BitVector.empty) // .runA(ms.token.nftState) // .map { stream => // stream.evalMap { case (_, nftState) => // GenericMerkleTrie // .from[ // F, // (Account, TokenId, Hash.Value[TransactionWithResult]), // Unit, // ] { // (nftState.currentOwner, nftState.tokenId).toBytes.bits // } // .runA(ms.token.nftBalanceState) // .map { stream => // stream // .evalMap { case (keyBits, _) => // for // txHash <- EitherT.fromEither { // keyBits.bytes // .to[ // ( // Account, // TokenId, // Hash.Value[TransactionWithResult], // ), // ] // .map(_._3) // .leftMap(_.msg) // } // txOption <- TransactionRepository[F] // .get(txHash) // .leftMap(_.msg) // tx <- EitherT.fromOption( // txOption, // s"No transaction $txHash found", // ) // yield tx // } // .filter( // _.signedTx.value.createdAt.compareTo(tx.timestamp) < 0, // ) // .as(nftState) // } // }.flatten // } // } // .mapK(PlayNommDAppFailure.mapInternal(s"Fail to get NFT states")) // // val getWeightSum = getNftStateStream.flatMapF { stream => // stream // .map(_.weight) // .fold(BigNat.Zero)(BigNat.add) // .compile // .toList // .flatMap { list => EitherT.fromOption(list.headOption, "empty list") } // .leftMap { e => // PlayNommDAppFailure.internal(s"Fail to get weight sum: $e") // } // } // // for // nftStream <- getNftStateStream // weightSum <- getWeightSum // _ <- StateT.modifyF[EitherT[F, PlayNommDAppFailure, *], MerkleState] { // (ms: MerkleState) => // nftStream // .evalScan(ms) { (ms, nftState) => // val ownershipSnapshot = OwnershipSnapshot( // account = nftState.currentOwner, // timestamp = tx.timestamp, // point = nftState.weight, // definitionId = nftState.tokenDefinitionId, // amount = tx.ownershipAmount * nftState.weight / weightSum, // ) // PlayNommState[F].reward.ownershipSnapshot // .put(nftState.tokenId, ownershipSnapshot) // .transformS[MerkleState]( // _.main, // (ms, mts) => (ms.copy(main = mts)), // ) // .runS(ms) // } // .last // .compile // .toList // .flatMap { list => // EitherT.fromOption( // list.headOption.flatten, // "Fail to build final mekle state", // ) // } // .leftMap(e => PlayNommDAppFailure.internal(e)) // } // yield TransactionWithResult(Signed(sig, tx), None) case tx: Transaction.RewardTx.ExecuteOwnershipReward => ??? // val getInputAmount // : StateT[EitherT[F, PlayNommDAppFailure, *], MerkleState, BigNat] = // StateT.liftF { // tx.inputs.toList // .traverse { txHash => // for // txOption <- TransactionRepository[F].get(txHash).leftMap { e => // PlayNommDAppFailure.internal(s"fail to get tx $txHash") // } // txWithResult <- EitherT.fromOption( // txOption, // PlayNommDAppFailure.external(s"Tx input not found: $txHash"), // ) // amount <- txWithResult.signedTx.value match // case fb: Transaction.FungibleBalance => // EitherT.pure { // fb match // case mf: Transaction.TokenTx.MintFungibleToken => // mf.outputs.get(sig.account).getOrElse(BigNat.Zero) // case or: Transaction.TokenTx.TransferFungibleToken => // or.outputs.get(sig.account).getOrElse(BigNat.Zero) // case bf: Transaction.TokenTx.BurnFungibleToken => // txWithResult.result.fold(BigNat.Zero) { // case Transaction.TokenTx.BurnFungibleTokenResult( // outputAmount, // ) => // outputAmount // case _ => BigNat.Zero // } // case ef: Transaction.TokenTx.EntrustFungibleToken => // txWithResult.result.fold(BigNat.Zero) { // case Transaction.TokenTx.EntrustFungibleTokenResult( // remainder, // ) => // remainder // case _ => BigNat.Zero // } // case de: Transaction.TokenTx.DisposeEntrustedFungibleToken => // de.outputs.get(sig.account).getOrElse(BigNat.Zero) // case or: Transaction.RewardTx.OfferReward => // or.outputs.get(sig.account).getOrElse(BigNat.Zero) // case xr: Transaction.RewardTx.ExecuteReward => // txWithResult.result.fold(BigNat.Zero) { // case Transaction.RewardTx.ExecuteRewardResult( // outputs, // ) => // outputs.get(sig.account).getOrElse(BigNat.Zero) // case _ => BigNat.Zero // } // case xo: Transaction.RewardTx.ExecuteOwnershipReward => // txWithResult.result.fold(BigNat.Zero) { // case Transaction.RewardTx // .ExecuteOwnershipRewardResult( // outputs, // ) => // outputs.get(sig.account).getOrElse(BigNat.Zero) // case _ => BigNat.Zero // } // } // case _ => // EitherT.leftT(PlayNommDAppFailure.external { // s"Tx input is not a fungible balance: $txHash" // }) // yield amount // } // .map(_.foldLeft(BigNat.Zero)(BigNat.add)) // } // // def updateBalance( // outputs: Map[Account, BigNat], // outputTxHash: Hash.Value[TransactionWithResult], // ): StateT[EitherT[F, PlayNommDAppFailure, *], MerkleState, Unit] = // val program = // for // _ <- tx.inputs.toList.traverse { inputTxHash => // GenericMerkleTrie // .remove[ // F, // ( // Account, // TokenDefinitionId, // Hash.Value[TransactionWithResult], // ), // Unit, // ] { // (sig.account, tx.definitionId, inputTxHash).toBytes.bits // } // } // _ <- outputs.toList.traverse { (account, amount) => // GenericMerkleTrie.put[ // F, // (Account, TokenDefinitionId, Hash.Value[TransactionWithResult]), // Unit, // ]( // (account, tx.definitionId, outputTxHash).toBytes.bits, // (), // ) // } // yield () // // program // .transformS[MerkleState]( // _.token.fungibleBalanceState, // (ms, fbs) => // (ms.copy(token = ms.token.copy(fungibleBalanceState = fbs))), // ) // .mapK( // PlayNommDAppFailure.mapInternal("Fail to update fungible balance"), // ) // // def updateRewarded(txHash: Hash.Value[TransactionWithResult])( // ownershipSnapshot: (TokenId, OwnershipSnapshot), // ): StateT[EitherT[F, PlayNommDAppFailure, *], MerkleState, Unit] = // val program = // for _ <- PlayNommState[F].reward.ownershipRewarded.put( // ownershipSnapshot._1, // OwnershipRewardLog(ownershipSnapshot._2, txHash), // ) // yield () // // program // .mapK( // PlayNommDAppFailure.mapInternal("Fail to update rewarded state"), // ) // .transformS[MerkleState](_.main, (ms, mts) => (ms.copy(main = mts))) // // for // snapshots <- tx.targets.toList // .traverse { tokenId => // for // ownershipSnapshotOption <- // PlayNommState[F].reward.ownershipSnapshot // .get(tokenId) // .mapK( // PlayNommDAppFailure.mapInternal( // s"Fail to get snapshot data of token ${tokenId}", // ), // ) // ownershipSnapshot <- fromOption( // ownershipSnapshotOption, // s"No ownership snapshot for token $tokenId", // ) // yield ownershipSnapshot // } // .transformS[MerkleState](_.main, (ms, mts) => (ms.copy(main = mts))) // inputAmount <- getInputAmount // outputSum = snapshots.map(_.amount).foldLeft(BigNat.Zero)(BigNat.add) // // remainder <- StateT.liftF { // EitherT // .fromEither { // BigNat.fromBigInt(outputSum.toBigInt - inputAmount.toBigInt) // } // .leftMap(PlayNommDAppFailure.external) // } // outputs = snapshots.map { snapshot => // snapshot.account -> snapshot.amount // }.toMap + (sig.account -> remainder) // result = Transaction.RewardTx.ExecuteOwnershipRewardResult(outputs) // txWithResult = TransactionWithResult(Signed(sig, tx), Some(result)) // txHash = txWithResult.toHash // _ <- updateBalance(outputs, txHash) // _ <- (tx.targets.toList zip snapshots).traverse(updateRewarded(txHash)) // yield txWithResult ================================================ FILE: modules/node/src/main/scala/io/leisuremeta/chain/node/dapp/submodule/PlayNommDAppToken.scala ================================================ package io.leisuremeta.chain package node package dapp package submodule import cats.Monad import cats.data.{EitherT, StateT} import cats.effect.Concurrent import cats.syntax.all.* import api.model.{ Account, AccountSignature, Signed, Transaction, TransactionWithResult, } import api.model.TransactionWithResult.ops.* import api.model.token.* import api.model.token.SnapshotState.SnapshotId.* import lib.codec.byte.ByteEncoder.ops.* import lib.crypto.Hash import lib.crypto.Hash.ops.* import lib.datatype.BigNat import lib.merkle.MerkleTrieState import repository.TransactionRepository object PlayNommDAppToken: def apply[F[_]: Concurrent: PlayNommState: TransactionRepository]( tx: Transaction.TokenTx, sig: AccountSignature, ): StateT[ EitherT[F, PlayNommDAppFailure, *], MerkleTrieState, TransactionWithResult, ] = tx match case dt: Transaction.TokenTx.DefineToken => for _ <- PlayNommDAppAccount.verifySignature(sig, tx) tokenDefOption <- getTokenDefinitionOption(dt.definitionId) _ <- checkExternal( tokenDefOption.isEmpty, s"Token ${dt.definitionId} is already defined", ) tokenDefinition = TokenDefinition( id = dt.definitionId, name = dt.name, symbol = dt.symbol, adminGroup = dt.minterGroup, totalAmount = BigNat.Zero, nftInfo = dt.nftInfo.map(NftInfoWithPrecision.fromNftInfo), ) _ <- putTokenDefinition(dt.definitionId, tokenDefinition) yield TransactionWithResult(Signed(sig, dt))(None) case dp: Transaction.TokenTx.DefineTokenWithPrecision => for _ <- PlayNommDAppAccount.verifySignature(sig, tx) tokenDefOption <- getTokenDefinitionOption(dp.definitionId) _ <- checkExternal( tokenDefOption.isEmpty, s"Token ${dp.definitionId} is already defined", ) tokenDefinition = TokenDefinition( id = dp.definitionId, name = dp.name, symbol = dp.symbol, adminGroup = dp.minterGroup, totalAmount = BigNat.Zero, nftInfo = dp.nftInfo, ) _ <- putTokenDefinition(dp.definitionId, tokenDefinition) yield TransactionWithResult(Signed(sig, dp))(None) case mf: Transaction.TokenTx.MintFungibleToken => for _ <- PlayNommDAppAccount.verifySignature(sig, tx) tokenDef <- checkMinterAndGetTokenDefinition( sig.account, mf.definitionId, ) txWithResult = TransactionWithResult(Signed(sig, mf))(None) txHash = txWithResult.toHash _ <- mf.outputs.toSeq.traverse: (account, amount) => val partProgram = for _ <- PlayNommState[F].token.fungibleBalance .put((account, mf.definitionId, txHash), ()) _ <- addFungibleSnapshot(account, mf.definitionId, txHash, amount) yield () partProgram.mapK: PlayNommDAppFailure.mapInternal: s"Fail to put token balance ($account, ${mf.definitionId}, $txHash)" mintAmount = mf.outputs.values.foldLeft(BigNat.Zero)(BigNat.add) totalAmount = BigNat.add(tokenDef.totalAmount, mintAmount) _ <- putTokenDefinition( mf.definitionId, tokenDef.copy(totalAmount = totalAmount), ) _ <- addTotalSupplySnapshot(mf.definitionId, totalAmount) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to add total supply snapshot of ${mf.definitionId}" yield txWithResult case mn: Transaction.TokenTx.MintNFT => for _ <- PlayNommDAppAccount.verifySignature(sig, tx) tokenDef <- checkMinterAndGetTokenDefinition( sig.account, mn.tokenDefinitionId, ) nftStateOption <- PlayNommState[F].token.nftState .get(mn.tokenId) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to get nft state of ${mn.tokenId}" _ <- checkExternal( nftStateOption.isEmpty, s"NFT ${mn.tokenId} is already minted", ) txWithResult = TransactionWithResult(Signed(sig, mn))(None) txHash = txWithResult.toHash _ <- PlayNommState[F].token.nftBalance .put((mn.output, mn.tokenId, txHash), ()) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to put token balance (${mn.output}, ${mn.tokenId}, $txHash)" _ <- addNftSnapshot(mn.output, mn.tokenDefinitionId, mn.tokenId) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to add nft snapshot of ${mn.tokenId}" weight = tokenDef.nftInfo.flatMap(_.rarity.get(mn.rarity)) .getOrElse(BigNat.unsafeFromLong(2L)) nftState = NftState( tokenId = mn.tokenId, tokenDefinitionId = mn.tokenDefinitionId, rarity = mn.rarity, weight = weight, currentOwner = mn.output, memo = None, lastUpdateTx = txHash, previousState = None, ) _ <- PlayNommState[F].token.nftState .put(mn.tokenId, nftState) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to put nft state of ${mn.tokenId}" _ <- PlayNommState[F].token.nftHistory .put(txHash, nftState) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to put nft history of ${mn.tokenId} of $txHash" _ <- PlayNommState[F].token.rarityState .put((mn.tokenDefinitionId, mn.rarity, mn.tokenId), ()) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to put rarity state of ${mn.tokenDefinitionId}, ${mn.rarity}, ${mn.tokenId}" yield txWithResult case mn: Transaction.TokenTx.MintNFTWithMemo => for _ <- PlayNommDAppAccount.verifySignature(sig, tx) tokenDef <- checkMinterAndGetTokenDefinition( sig.account, mn.tokenDefinitionId, ) nftStateOption <- PlayNommState[F].token.nftState .get(mn.tokenId) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to get nft state of ${mn.tokenId}" _ <- checkExternal( nftStateOption.isEmpty, s"NFT ${mn.tokenId} is already minted", ) txWithResult = TransactionWithResult(Signed(sig, mn))(None) txHash = txWithResult.toHash _ <- PlayNommState[F].token.nftBalance .put((mn.output, mn.tokenId, txHash), ()) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to put token balance (${mn.output}, ${mn.tokenId}, $txHash)" _ <- addNftSnapshot(mn.output, mn.tokenDefinitionId, mn.tokenId) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to add nft snapshot of ${mn.tokenId}" weight = tokenDef.nftInfo.get.rarity .getOrElse(mn.rarity, BigNat.unsafeFromLong(2L)) nftState = NftState( tokenId = mn.tokenId, tokenDefinitionId = mn.tokenDefinitionId, rarity = mn.rarity, weight = weight, currentOwner = mn.output, memo = mn.memo, lastUpdateTx = txHash, previousState = None, ) _ <- PlayNommState[F].token.nftState .put(mn.tokenId, nftState) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to put nft state of ${mn.tokenId}" _ <- PlayNommState[F].token.nftHistory .put(txHash, nftState) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to put nft history of ${mn.tokenId} of $txHash" _ <- PlayNommState[F].token.rarityState .put((mn.tokenDefinitionId, mn.rarity, mn.tokenId), ()) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to put rarity state of ${mn.tokenDefinitionId}, ${mn.rarity}, ${mn.tokenId}" yield txWithResult case un: Transaction.TokenTx.UpdateNFT => for _ <- PlayNommDAppAccount.verifySignature(sig, tx) tokenDef <- checkMinterAndGetTokenDefinition( sig.account, un.tokenDefinitionId, ) nftStateOption <- PlayNommState[F].token.nftState .get(un.tokenId) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to get nft state of ${un.tokenId}" nftState <- fromOption( nftStateOption, s"Empty NFT State: ${un.tokenId}", ) txWithResult = TransactionWithResult(Signed(sig, un))(None) txHash = txWithResult.toHash weight = tokenDef.nftInfo.get.rarity .getOrElse(un.rarity, BigNat.unsafeFromLong(2L)) nftState1 = NftState( tokenId = un.tokenId, tokenDefinitionId = un.tokenDefinitionId, rarity = un.rarity, weight = weight, currentOwner = un.output, memo = un.memo, lastUpdateTx = txHash, previousState = Some(nftState.lastUpdateTx), ) _ <- PlayNommState[F].token.nftState .put(un.tokenId, nftState1) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to put nft state of ${un.tokenId}" _ <- PlayNommState[F].token.nftHistory .put(txHash, nftState1) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to put nft history of ${un.tokenId} of $txHash" yield txWithResult case tf: Transaction.TokenTx.TransferFungibleToken => for _ <- PlayNommDAppAccount.verifySignature(sig, tx) tokenDef <- getTokenDefinition(tf.tokenDefinitionId) inputAmount <- getFungibleBalanceTotalAmounts( tf.inputs.map(_.toResultHashValue), sig.account, ) outputAmount = tf.outputs.values.foldLeft(BigNat.Zero)(BigNat.add) diff <- fromEitherExternal: BigNat.tryToSubtract(inputAmount, outputAmount) txWithResult = TransactionWithResult(Signed(sig, tf))(None) txHash = txWithResult.toHash _ <- removeInputUtxos( sig.account, tf.inputs.map(_.toResultHashValue), tf.tokenDefinitionId, ) _ <- tf.inputs.toList.traverse: inputTxHash => removeFungibleSnapshot( sig.account, tf.tokenDefinitionId, inputTxHash.toResultHashValue, ) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to remove fungible snapshot of $inputTxHash" _ <- tf.outputs.toSeq.traverse: case (account, outputAmount) => putBalance(account, tf.tokenDefinitionId, txHash) *> addFungibleSnapshot( account, tf.tokenDefinitionId, txHash, outputAmount, ) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to add fungible snapshot of $account" totalAmount <- fromEitherInternal: BigNat.tryToSubtract(tokenDef.totalAmount, diff) _ <- putTokenDefinition( tf.tokenDefinitionId, tokenDef.copy(totalAmount = totalAmount), ) _ <- removeTotalSupplySnapshot(tf.tokenDefinitionId, diff) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to remove total supply snapshot of ${tf.tokenDefinitionId}" yield txWithResult case tn: Transaction.TokenTx.TransferNFT => for _ <- PlayNommDAppAccount.verifySignature(sig, tx) txWithResult = TransactionWithResult(Signed(sig, tn))(None) txHash = txWithResult.toHash utxoKey = (sig.account, tn.tokenId, tn.input.toResultHashValue) isRemoveSuccessful <- PlayNommState[F].token.nftBalance .remove(utxoKey) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to remove nft balance of $utxoKey" _ <- checkExternal(isRemoveSuccessful, s"No NFT Balance: ${utxoKey}") newUtxoKey = (tn.output, tn.tokenId, txHash) _ <- PlayNommState[F].token.nftBalance .put(newUtxoKey, ()) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to put nft balance of $newUtxoKey" nftStateOption <- PlayNommState[F].token.nftState .get(tn.tokenId) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to get nft state of ${tn.tokenId}" nftState <- fromOption( nftStateOption, s"Empty NFT State: ${tn.tokenId}", ) nftState1 = nftState.copy(currentOwner = tn.output) _ <- PlayNommState[F].token.nftState .put(tn.tokenId, nftState1) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to put nft state of ${tn.tokenId}" _ <- removeNftSnapshot[F](sig.account, tn.definitionId, tn.tokenId) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to remove nft snapshot of ${tn.tokenId}" _ <- addNftSnapshot[F](tn.output, tn.definitionId, tn.tokenId) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to add nft snapshot of ${tn.tokenId}" yield txWithResult case bf: Transaction.TokenTx.BurnFungibleToken => for _ <- PlayNommDAppAccount.verifySignature(sig, tx) tokenDef <- checkMinterAndGetTokenDefinition( sig.account, bf.definitionId, ) inputAmount <- getFungibleBalanceTotalAmounts( bf.inputs.map(_.toResultHashValue), sig.account, ) outputAmount <- fromEitherExternal: BigNat.tryToSubtract(inputAmount, bf.amount) result = Transaction.TokenTx.BurnFungibleTokenResult(outputAmount) txWithResult = TransactionWithResult(Signed(sig, bf))(Some(result)) txHash = txWithResult.toHash _ <- removeInputUtxos( sig.account, bf.inputs.map(_.toResultHashValue), bf.definitionId, ) _ <- bf.inputs.toList.traverse: inputTxHash => removeFungibleSnapshot( sig.account, bf.definitionId, inputTxHash.toResultHashValue, ) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to remove fungible snapshot of $inputTxHash" _ <- if outputAmount === BigNat.Zero then unit else putBalance(sig.account, bf.definitionId, txWithResult.toHash) *> addFungibleSnapshot( sig.account, bf.definitionId, txWithResult.toHash, outputAmount, ) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to add fungible snapshot of ${sig.account}" totalAmount <- fromEitherInternal: BigNat.tryToSubtract(tokenDef.totalAmount, bf.amount) _ <- putTokenDefinition( bf.definitionId, tokenDef.copy(totalAmount = totalAmount), ) _ <- removeTotalSupplySnapshot(bf.definitionId, bf.amount) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to remove total supply snapshot of ${bf.definitionId}" yield txWithResult case bn: Transaction.TokenTx.BurnNFT => for _ <- PlayNommDAppAccount.verifySignature(sig, tx) tokenId <- getNftTokenId(bn.input.toResultHashValue) txWithResult = TransactionWithResult(Signed(sig, bn))(None) txHash = txWithResult.toHash utxoKey = (sig.account, tokenId, bn.input.toResultHashValue) isRemoveSuccessful <- PlayNommState[F].token.nftBalance .remove(utxoKey) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to remove nft balance of $utxoKey" _ <- checkExternal(isRemoveSuccessful, s"No NFT Balance: ${utxoKey}") nftStateOption <- PlayNommState[F].token.nftState .get(tokenId) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to get nft state of ${tokenId}" _ <- checkInternal( nftStateOption.isDefined, s"Empty NFT State: ${tokenId}", ) nftState <- fromOption( nftStateOption, s"Empty NFT State: ${tokenId}", ) _ <- PlayNommState[F].token.nftState .remove(tokenId) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to remove nft state of ${tokenId}" _ <- PlayNommState[F].token.rarityState .remove((bn.definitionId, nftState.rarity, tokenId)) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to remove rarity state of ${tokenId}" _ <- removeNftSnapshot[F](sig.account, bn.definitionId, tokenId) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to remove nft snapshot of ${tokenId}" yield txWithResult case ef: Transaction.TokenTx.EntrustFungibleToken => for _ <- PlayNommDAppAccount.verifySignature(sig, tx) tokenDef <- getTokenDefinition(ef.definitionId) inputAmount <- getFungibleBalanceTotalAmounts( ef.inputs.map(_.toResultHashValue), sig.account, ) diff <- fromEitherExternal: BigNat.tryToSubtract(inputAmount, ef.amount) result = Transaction.TokenTx.EntrustFungibleTokenResult(diff) txWithResult = TransactionWithResult(Signed(sig, ef))(Some(result)) txHash = txWithResult.toHash _ <- removeInputUtxos( sig.account, ef.inputs.map(_.toResultHashValue), ef.definitionId, ) _ <- PlayNommState[F].token.entrustFungibleBalance .put((sig.account, ef.to, ef.definitionId, txHash), ()) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to put entrust fungible balance of (${sig.account}, ${ef.to}, ${ef.definitionId}, ${txHash})" _ <- putBalance(sig.account, ef.definitionId, txHash) yield txWithResult case de: Transaction.TokenTx.DisposeEntrustedFungibleToken => for _ <- PlayNommDAppAccount.verifySignature(sig, tx) tokenDef <- getTokenDefinition(de.definitionId) inputHashList = de.inputs.map(_.toResultHashValue) inputMap <- getEntrustedInputs(inputHashList, sig.account) inputAmount = inputMap.values.foldLeft(BigNat.Zero)(BigNat.add) outputAmount = de.outputs.values.foldLeft(BigNat.Zero)(BigNat.add) _ <- checkExternal( inputAmount === outputAmount, s"Output amount is not equal to input amount $inputAmount", ) txWithResult = TransactionWithResult(Signed(sig, de))(None) txHash = txWithResult.toHash _ <- inputMap.toList .map(_._1) .traverse: (account, txHash) => PlayNommState[F].token.entrustFungibleBalance .remove((account, sig.account, de.definitionId, txHash)) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to remove entrust fungible balance of (${account}, ${sig.account}, ${de.definitionId}, ${txHash})" *> removeFungibleSnapshot[F](account, de.definitionId, txHash) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to remove fungible snapshot of $txHash" _ <- de.outputs.toList.traverse: (account, amount) => PlayNommState[F].token.fungibleBalance .put((account, de.definitionId, txHash), ()) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to put fungible balance of (${account}, ${de.definitionId}, ${txHash})" *> addFungibleSnapshot[F](account, de.definitionId, txHash, amount) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to put fungible snapshot of $txHash" yield txWithResult case ef: Transaction.TokenTx.EntrustNFT => for _ <- PlayNommDAppAccount.verifySignature(sig, tx) txWithResult = TransactionWithResult(Signed(sig, ef))(None) txHash = txWithResult.toHash utxoKey = (sig.account, ef.tokenId, ef.input.toResultHashValue) isRemoveSuccessful <- PlayNommState[F].token.nftBalance .remove(utxoKey) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to remove nft balance of $utxoKey" _ <- checkExternal(isRemoveSuccessful, s"No NFT Balance: ${utxoKey}") newUtxoKey = (sig.account, ef.to, ef.tokenId, txHash) _ <- PlayNommState[F].token.entrustNftBalance .put(newUtxoKey, ()) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to put entrust nft balance of $newUtxoKey" yield txWithResult case de: Transaction.TokenTx.DisposeEntrustedNFT => for _ <- PlayNommDAppAccount.verifySignature(sig, tx) entrustedNFT <- getEntrustedNFT(de.input.toResultHashValue, sig.account) (fromAccount, entrustTx) = entrustedNFT txWithResult = TransactionWithResult(Signed(sig, de))(None) txHash = txWithResult.toHash utxoKey = ( fromAccount, sig.account, de.tokenId, de.input.toResultHashValue, ) isRemoveSuccessful <- PlayNommState[F].token.entrustNftBalance .remove(utxoKey) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to remove entrust nft balance of $utxoKey" _ <- checkExternal( isRemoveSuccessful, s"No Entrust NFT Balance: ${utxoKey}", ) _ <- removeNftSnapshot[F](fromAccount, de.definitionId, de.tokenId) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to remove nft snapshot of ${de.tokenId}" newUtxoKey = (de.output.getOrElse(fromAccount), de.tokenId, txHash) _ <- PlayNommState[F].token.nftBalance .put(newUtxoKey, ()) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to put nft balance of $newUtxoKey" _ <- addNftSnapshot[F]( de.output.getOrElse(fromAccount), de.definitionId, de.tokenId, ) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to add nft snapshot of ${de.tokenId}" _ <- de.output.fold(unit): toAddress => for nftStateOption <- PlayNommState[F].token.nftState .get(de.tokenId) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to get nft state of ${de.tokenId}" nftState <- fromOption( nftStateOption, s"Empty NFT State: ${de.tokenId}", ) nftState1 = nftState.copy(currentOwner = toAddress) _ <- PlayNommState[F].token.nftState .put(de.tokenId, nftState1) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to put nft state of ${de.tokenId}" yield () yield txWithResult case cs: Transaction.TokenTx.CreateSnapshots => for _ <- PlayNommDAppAccount.verifySignature(sig, tx) txWithResult = TransactionWithResult(Signed(sig, cs))(None) _ <- cs.definitionIds.toList.traverse: definitionId => for tokenDefOption <- getTokenDefinitionOption(definitionId) tokenDef <- fromOption( tokenDefOption, s"Token ${definitionId} is not defined", ) adminGroupId <- fromOption( tokenDef.adminGroup, s"No admin group in token ${definitionId}", ) _ <- PlayNommState[F].group.groupAccount .get((adminGroupId, sig.account)) .mapK: PlayNommDAppFailure.mapExternal: s"Not in admin group ${adminGroupId} of ${sig.account}" snapshotStateOption <- PlayNommState[F].token.snapshotState .get(definitionId) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to get snapshot state of ${definitionId}" lastSnapshotId = snapshotStateOption .fold(SnapshotState.SnapshotId.Zero)(_.snapshotId) snapshotState = SnapshotState( snapshotId = lastSnapshotId.increase, createdAt = cs.createdAt, txHash = txWithResult.toHash.toSignedTxHash, memo = cs.memo, ) _ <- PlayNommState[F].token.snapshotState .put(definitionId, snapshotState) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to put snapshot state of ${definitionId}" yield () yield txWithResult def getTokenDefinitionOption[F[_]: Monad: PlayNommState]( definitionId: TokenDefinitionId, ): StateT[EitherT[F, PlayNommDAppFailure, *], MerkleTrieState, Option[ TokenDefinition, ]] = PlayNommState[F].token.definition .get(definitionId) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to get token definition of ${definitionId}" def getTokenDefinition[F[_]: Monad: PlayNommState]( definitionId: TokenDefinitionId, ): StateT[ EitherT[F, PlayNommDAppFailure, *], MerkleTrieState, TokenDefinition, ] = for tokenDefOption <- getTokenDefinitionOption(definitionId) tokenDef <- fromOption( tokenDefOption, s"Token $definitionId is not defined", ) yield tokenDef def putTokenDefinition[F[_]: Monad: PlayNommState]( definitionId: TokenDefinitionId, definition: TokenDefinition, ): StateT[EitherT[F, PlayNommDAppFailure, *], MerkleTrieState, Unit] = PlayNommState[F].token.definition .put(definitionId, definition) .mapK(PlayNommDAppFailure.mapInternal { s"Fail to set token definition of $definitionId" }) def checkMinterAndGetTokenDefinition[F[_]: Monad: PlayNommState]( account: Account, definitionId: TokenDefinitionId, ): StateT[ EitherT[F, PlayNommDAppFailure, *], MerkleTrieState, TokenDefinition, ] = for tokenDef <- getTokenDefinition(definitionId) minterGroup <- fromOption( tokenDef.adminGroup, s"Token $definitionId does not have a minter group", ) groupAccountInfoOption <- PlayNommState[F].group.groupAccount .get((minterGroup, account)) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to get group account for ($minterGroup, $account)" _ <- checkExternal( groupAccountInfoOption.nonEmpty, s"Account $account is not a member of minter group $minterGroup", ) yield tokenDef def getFungibleBalanceTotalAmounts[F[_] : Monad: TransactionRepository: PlayNommState]( inputs: Set[Hash.Value[TransactionWithResult]], account: Account, ): StateT[EitherT[F, PlayNommDAppFailure, *], MerkleTrieState, BigNat] = StateT.liftF: inputs.toSeq .traverse: txHash => for txOption <- TransactionRepository[F] .get(txHash) .leftMap: e => PlayNommDAppFailure.internal( s"Fail to get tx $txHash: ${e.msg}", ) txWithResult <- EitherT.fromOption( txOption, PlayNommDAppFailure.internal(s"There is no tx of $txHash"), ) yield val amount = tokenBalanceAmount(account)(txWithResult) // scribe.info(s"Amount of ${txHash.toUInt256Bytes.toHex}: $amount") amount .map { _.foldLeft(BigNat.Zero)(BigNat.add) } def removeInputUtxos[F[_]: Monad: PlayNommState]( account: Account, inputs: Set[Hash.Value[TransactionWithResult]], definitionId: TokenDefinitionId, ): StateT[EitherT[F, PlayNommDAppFailure, *], MerkleTrieState, List[ Hash.Value[TransactionWithResult], ]] = val inputList = inputs.toList for removeResults <- inputList.traverse { txHash => PlayNommState[F].token.fungibleBalance .remove((account, definitionId, txHash)) .mapK(PlayNommDAppFailure.mapInternal { s"Fail to remove fingible balance ($account, $definitionId, $txHash)" }) } invalidUtxos = inputList.zip(removeResults).filterNot(_._2).map(_._1) _ <- checkExternal( invalidUtxos.isEmpty, s"These utxos are invalid: $invalidUtxos", ) yield invalidUtxos def tokenBalanceAmount(account: Account)( txWithResult: TransactionWithResult, ): BigNat = txWithResult.signedTx.value match case tb: Transaction.FungibleBalance => tb match case mt: Transaction.TokenTx.MintFungibleToken => mt.outputs.getOrElse(account, BigNat.Zero) case bt: Transaction.TokenTx.BurnFungibleToken => txWithResult.result.fold(BigNat.Zero): case Transaction.TokenTx.BurnFungibleTokenResult(amount) if txWithResult.signedTx.sig.account === account => amount case _ => scribe.error: s"Fail to get burn token result: $txWithResult" BigNat.Zero case tt: Transaction.TokenTx.TransferFungibleToken => tt.outputs.getOrElse(account, BigNat.Zero) case ef: Transaction.TokenTx.EntrustFungibleToken => txWithResult.result.fold(BigNat.Zero): case Transaction.TokenTx.EntrustFungibleTokenResult(remainder) => remainder case _ => BigNat.Zero case df: Transaction.TokenTx.DisposeEntrustedFungibleToken => df.outputs.getOrElse(account, BigNat.Zero) case or: Transaction.RewardTx.OfferReward => or.outputs.getOrElse(account, BigNat.Zero) case xr: Transaction.RewardTx.ExecuteReward => txWithResult.result.fold(BigNat.Zero): case Transaction.RewardTx.ExecuteRewardResult(outputs) => outputs.getOrElse(account, BigNat.Zero) case _ => BigNat.Zero case xo: Transaction.RewardTx.ExecuteOwnershipReward => txWithResult.result.fold(BigNat.Zero): case Transaction.RewardTx.ExecuteOwnershipRewardResult(outputs) => outputs.getOrElse(account, BigNat.Zero) case _ => BigNat.Zero case _ => scribe.error(s"Not a fungible balance: $txWithResult") BigNat.Zero def putBalance[F[_]: Monad: PlayNommState]( account: Account, definitionId: TokenDefinitionId, txHash: Hash.Value[TransactionWithResult], ): StateT[EitherT[F, PlayNommDAppFailure, *], MerkleTrieState, Unit] = PlayNommState[F].token.fungibleBalance .put((account, definitionId, txHash), ()) .mapK: PlayNommDAppFailure.mapInternal: s"Fail to put token balance ($account, $definitionId, $txHash)" def getNftTokenId[F[_]: Monad: TransactionRepository]( utxoHash: Hash.Value[TransactionWithResult], ): StateT[EitherT[F, PlayNommDAppFailure, *], MerkleTrieState, TokenId] = StateT.liftF: TransactionRepository[F] .get(utxoHash) .leftMap(e => PlayNommDAppFailure.internal(s"Fail to get tx: ${e.msg}")) .subflatMap: txOption => Either.fromOption( txOption, PlayNommDAppFailure.internal(s"There is no tx of $utxoHash"), ) .flatMap: txWithResult => txWithResult.signedTx.value match case nb: Transaction.NftBalance => EitherT.pure: nb match case mn: Transaction.TokenTx.MintNFT => mn.tokenId case mnm: Transaction.TokenTx.MintNFTWithMemo => mnm.tokenId case tn: Transaction.TokenTx.TransferNFT => tn.tokenId case den: Transaction.TokenTx.DisposeEntrustedNFT => den.tokenId case _ => EitherT.leftT: PlayNommDAppFailure.external: s"Tx $txWithResult is not a nft balance" def getEntrustedInputs[F[_]: Monad: TransactionRepository]( inputs: Set[Hash.Value[TransactionWithResult]], account: Account, ): StateT[EitherT[F, PlayNommDAppFailure, *], MerkleTrieState, Map[ (Account, Hash.Value[TransactionWithResult]), BigNat, ]] = StateT.liftF: inputs.toSeq .traverse: txHash => for txOption <- TransactionRepository[F] .get(txHash) .leftMap: e => PlayNommDAppFailure.internal: s"Fail to get tx $txHash: ${e.msg}" txWithResult <- EitherT.fromOption( txOption, PlayNommDAppFailure.internal(s"There is no tx of $txHash"), ) amount <- txWithResult.signedTx.value match case ef: Transaction.TokenTx.EntrustFungibleToken => EitherT.cond( ef.to === account, ef.amount, PlayNommDAppFailure.external: s"Entrust fungible token tx $txWithResult is not for $account", ) case _ => EitherT.leftT: PlayNommDAppFailure.external: s"Tx $txWithResult is not an entrust fungible token transaction" yield ((txWithResult.signedTx.sig.account, txHash), amount) .map(_.toMap) def getEntrustedNFT[F[_]: Monad: TransactionRepository]( input: Hash.Value[TransactionWithResult], account: Account, ): StateT[ EitherT[F, PlayNommDAppFailure, *], MerkleTrieState, (Account, Transaction.TokenTx.EntrustNFT), ] = StateT.liftF: TransactionRepository[F] .get(input) .leftMap: e => PlayNommDAppFailure.internal: s"Fail to get tx $input: ${e.msg}" .subflatMap: txOption => Either.fromOption( txOption, PlayNommDAppFailure.internal(s"There is no tx of $input"), ) .subflatMap: txWithResult => txWithResult.signedTx.value match case ef: Transaction.TokenTx.EntrustNFT if ef.to === account => Right((txWithResult.signedTx.sig.account, ef)) case _ => Left: PlayNommDAppFailure.external: s"Tx $txWithResult is not an entrust nft transaction" def addFungibleSnapshot[F[_]: Concurrent: PlayNommState]( account: Account, defId: TokenDefinitionId, input: Hash.Value[TransactionWithResult], amount: BigNat, ): StateT[EitherT[F, String, *], MerkleTrieState, Unit] = for snapshotStateOption <- PlayNommState[F].token.snapshotState.get(defId) snapshotId = snapshotStateOption .fold(SnapshotState.SnapshotId.Zero)(_.snapshotId) stream <- PlayNommState[F].token.fungibleSnapshot .reverseStreamFrom((account, defId).toBytes, None) lastSnapshotOption <- StateT.liftF: stream.head.compile.toList.flatMap: list => EitherT.pure(list.headOption) lastSnapshot = lastSnapshotOption .fold(Map.empty[Hash.Value[TransactionWithResult], BigNat])(_._2) _ <- PlayNommState[F].token.fungibleSnapshot.put( (account, defId, snapshotId), lastSnapshot + (input -> amount), ) yield () def addTotalSupplySnapshot[F[_]: Concurrent: PlayNommState]( defId: TokenDefinitionId, amount: BigNat, ): StateT[EitherT[F, String, *], MerkleTrieState, Unit] = for snapshotStateOption <- PlayNommState[F].token.snapshotState.get(defId) snapshotId = snapshotStateOption .fold(SnapshotState.SnapshotId.Zero)(_.snapshotId) stream <- PlayNommState[F].token.totalSupplySnapshot .reverseStreamFrom(defId.toBytes, None) lastSnapshotOption <- StateT.liftF: stream.head.compile.toList.flatMap: list => EitherT.pure(list.headOption) lastSnapshot = lastSnapshotOption.fold(BigNat.Zero)(_._2) _ <- PlayNommState[F].token.totalSupplySnapshot.put( (defId, snapshotId), BigNat.add(lastSnapshot, amount), ) yield () def addNftSnapshot[F[_]: Concurrent: PlayNommState]( account: Account, defId: TokenDefinitionId, tokenId: TokenId, ): StateT[EitherT[F, String, *], MerkleTrieState, Unit] = for snapshotStateOption <- PlayNommState[F].token.snapshotState.get(defId) snapshotId = snapshotStateOption .fold(SnapshotState.SnapshotId.Zero)(_.snapshotId) stream <- PlayNommState[F].token.nftSnapshot .reverseStreamFrom((account, defId).toBytes, None) lastSnapshotOption <- StateT.liftF: stream.head.compile.toList.flatMap: list => EitherT.pure(list.headOption) lastSnapshot = lastSnapshotOption .fold(Set.empty[TokenId])(_._2) _ <- PlayNommState[F].token.nftSnapshot.put( (account, defId, snapshotId), lastSnapshot + tokenId, ) yield () def removeFungibleSnapshot[F[_]: Concurrent: PlayNommState]( account: Account, defId: TokenDefinitionId, input: Hash.Value[TransactionWithResult], ): StateT[EitherT[F, String, *], MerkleTrieState, Unit] = for snapshotStateOption <- PlayNommState[F].token.snapshotState.get(defId) snapshotId = snapshotStateOption .fold(SnapshotState.SnapshotId.Zero)(_.snapshotId) stream <- PlayNommState[F].token.fungibleSnapshot .reverseStreamFrom((account, defId).toBytes, None) lastSnapshotOption <- StateT.liftF: stream.head.compile.toList.flatMap: list => EitherT.pure(list.headOption) lastSnapshot = lastSnapshotOption .fold(Map.empty[Hash.Value[TransactionWithResult], BigNat])(_._2) _ <- PlayNommState[F].token.fungibleSnapshot.put( (account, defId, snapshotId), lastSnapshot - input, ) yield () def removeTotalSupplySnapshot[F[_]: Concurrent: PlayNommState]( defId: TokenDefinitionId, amount: BigNat, ): StateT[EitherT[F, String, *], MerkleTrieState, Unit] = for snapshotStateOption <- PlayNommState[F].token.snapshotState.get(defId) snapshotId = snapshotStateOption .fold(SnapshotState.SnapshotId.Zero)(_.snapshotId) stream <- PlayNommState[F].token.totalSupplySnapshot .reverseStreamFrom(defId.toBytes, None) lastSnapshotOption <- StateT.liftF: stream.head.compile.toList.flatMap: list => EitherT.pure(list.headOption) lastSnapshot = lastSnapshotOption.fold(BigNat.Zero)(_._2) remainder <- StateT.liftF: EitherT.fromEither: BigNat.tryToSubtract(lastSnapshot, amount) _ <- PlayNommState[F].token.totalSupplySnapshot.put( (defId, snapshotId), remainder, ) yield () def removeNftSnapshot[F[_]: Concurrent: PlayNommState]( account: Account, defId: TokenDefinitionId, tokenId: TokenId, ): StateT[EitherT[F, String, *], MerkleTrieState, Unit] = for snapshotStateOption <- PlayNommState[F].token.snapshotState.get(defId) snapshotId = snapshotStateOption .fold(SnapshotState.SnapshotId.Zero)(_.snapshotId) stream <- PlayNommState[F].token.nftSnapshot .reverseStreamFrom((account, defId).toBytes, None) lastSnapshotOption <- StateT.liftF: stream.head.compile.toList.flatMap: list => EitherT.pure(list.headOption) lastSnapshot = lastSnapshotOption .fold(Set.empty[TokenId])(_._2) _ <- PlayNommState[F].token.nftSnapshot.put( (account, defId, snapshotId), lastSnapshot - tokenId, ) yield () ================================================ FILE: modules/node/src/main/scala/io/leisuremeta/chain/node/dapp/submodule/PlayNommDAppVoting.scala ================================================ package io.leisuremeta.chain package node package dapp package submodule //import cats.Monad import cats.data.{EitherT, StateT} import cats.effect.Concurrent import cats.syntax.all.* import api.model.{ // Account, AccountSignature, Signed, Transaction, TransactionWithResult, } //import api.model.TransactionWithResult.ops.* //import api.model.token.* import api.model.voting.* //import api.model.token.SnapshotState.SnapshotId.* import lib.codec.byte.ByteEncoder.ops.* //import lib.crypto.Hash //import lib.crypto.Hash.ops.* import lib.datatype.BigNat import lib.merkle.MerkleTrieState import repository.TransactionRepository object PlayNommDAppVoting: def apply[F[_]: Concurrent: PlayNommState: TransactionRepository]( tx: Transaction.VotingTx, sig: AccountSignature, ): StateT[ EitherT[F, PlayNommDAppFailure, *], MerkleTrieState, TransactionWithResult, ] = tx match case cp: Transaction.VotingTx.CreateVoteProposal => for _ <- PlayNommDAppAccount.verifySignature(sig, tx) _ <- cp.votingPower.toList.traverse: (defId, _) => PlayNommDAppToken.checkMinterAndGetTokenDefinition(sig.account, defId) proposal = Proposal( createdAt = cp.createdAt, proposalId = cp.proposalId, title = cp.title, description = cp.description, votingPower = cp.votingPower, voteStart = cp.voteStart, voteEnd = cp.voteEnd, voteType = cp.voteType, voteOptions = cp.voteOptions, quorum = cp.quorum, passThresholdNumer = cp.passThresholdNumer, passThresholdDenom = cp.passThresholdDenom, isActive = true, ) _ <- PlayNommState[F].voting.proposal .put(cp.proposalId, proposal) .mapK: PlayNommDAppFailure.mapInternal: s"Failed to save proposal: ${cp.proposalId}" yield TransactionWithResult(Signed(sig, cp))(None) case cv: Transaction.VotingTx.CastVote => for _ <- PlayNommDAppAccount.verifySignature(sig, tx) voteOption <- PlayNommState[F].voting.votes .get((cv.proposalId, sig.account)) .mapK: PlayNommDAppFailure.mapInternal: s"Failed to get vote: ${cv.proposalId} ${sig.account}" _ <- checkExternal( voteOption.isEmpty, s"Vote already casted: ${cv.proposalId} ${sig.account}", ) proposalOption <- PlayNommState[F].voting.proposal .get(cv.proposalId) .mapK: PlayNommDAppFailure.mapInternal: s"Failed to get proposal: ${cv.proposalId}" proposal <- fromOption( proposalOption, s"Proposal not found: ${cv.proposalId}", ) _ <- checkExternal( proposal.voteStart.compareTo(cv.createdAt) <= 0 && cv.createdAt.compareTo(proposal.voteEnd) <= 0, s"Vote outside of voting period: ${cv.proposalId} ${sig.account}", ) _ <- checkExternal( proposal.isActive, s"Proposal not active: ${cv.proposalId}", ) amountSeq <- proposal.voteType match case VoteType.ONE_PERSON_ONE_VOTE => pure(Seq(BigNat.One)) case VoteType.TOKEN_WEIGHTED => proposal.votingPower.toSeq.traverse: (defId, snapshotId) => for balance <- PlayNommState[F].token.fungibleSnapshot .reverseStreamFrom( (sig.account, defId).toBytes, Some(snapshotId.toBytes), ) .mapK: PlayNommDAppFailure.mapInternal: s"Failed to get balance stream of: ${sig.account} ${defId}" .flatMap: stream => StateT.liftF: stream.head.compile.toList .map: case Nil => Map.empty case (k, v) :: _ => v .leftMap: msg => PlayNommDAppFailure.internal: s"Failed to get balance of: ${sig.account} ${defId} ${snapshotId}: ${msg}" yield balance.values.foldLeft(BigNat.Zero)(BigNat.add) case VoteType.NFT_BASED => proposal.votingPower.toSeq.traverse: (defId, snapshotId) => for count <- PlayNommState[F].token.nftSnapshot .reverseStreamFrom( (sig.account, defId).toBytes, Some(snapshotId.toBytes), ) .mapK: PlayNommDAppFailure.mapInternal: s"Failed to get balance stream of: ${sig.account} ${defId}" .flatMap: stream => StateT.liftF: stream.head.compile.toList .map: tokenIds => tokenIds.size .leftMap: msg => PlayNommDAppFailure.internal: s"Failed to get balance of: ${sig.account} ${defId} ${snapshotId}: ${msg}" yield BigNat.unsafeFromBigInt(BigInt(count)) powerSum = amountSeq.foldLeft(BigNat.Zero)(BigNat.add) originalPowerOption <- PlayNommState[F].voting.votes .put((cv.proposalId, sig.account), (cv.selectedOption, powerSum)) .mapK: PlayNommDAppFailure.mapInternal: s"Failed to save vote: ${cv.proposalId} ${sig.account}" originalMapOption <- PlayNommState[F].voting.counting .get(cv.proposalId) .mapK: PlayNommDAppFailure.mapInternal: s"Failed to get counting: ${cv.proposalId}" originalMap = originalMapOption.getOrElse(Map.empty) newMap = originalMap.updated(cv.selectedOption, powerSum) _ <- PlayNommState[F].voting.counting .put(cv.proposalId, newMap) .mapK: PlayNommDAppFailure.mapInternal: s"Failed to save counting: ${cv.proposalId}" yield TransactionWithResult(Signed(sig, cv))(None) case tv: Transaction.VotingTx.TallyVotes => for _ <- PlayNommDAppAccount.verifySignature(sig, tx) proposalOption <- PlayNommState[F].voting.proposal .get(tv.proposalId) .mapK: PlayNommDAppFailure.mapInternal: s"Failed to get proposal: ${tv.proposalId}" proposal <- fromOption( proposalOption, s"Proposal not found: ${tv.proposalId}", ) _ <- proposal.votingPower.toList.traverse: (defId, _) => PlayNommDAppToken.checkMinterAndGetTokenDefinition(sig.account, defId) newProposal = proposal.copy(isActive = false) _ <- PlayNommState[F].voting.proposal .put(tv.proposalId, newProposal) .mapK: PlayNommDAppFailure.mapInternal: s"Failed to save proposal: ${tv.proposalId}" yield TransactionWithResult(Signed(sig, tv))(None) ================================================ FILE: modules/node/src/main/scala/io/leisuremeta/chain/node/dapp/submodule/package.scala ================================================ package io.leisuremeta.chain package node package dapp package submodule import cats.Monad import cats.data.{EitherT, StateT} import lib.merkle.MerkleTrieState def checkExternal[F[_]: Monad]( test: Boolean, errorMessage: String, ): StateT[EitherT[F, PlayNommDAppFailure, *], MerkleTrieState, Unit] = StateT.liftF { EitherT.cond( test, (), PlayNommDAppFailure.external(errorMessage), ) } def checkInternal[F[_]: Monad]( test: Boolean, errorMessage: String, ): StateT[EitherT[F, PlayNommDAppFailure, *], MerkleTrieState, Unit] = StateT.liftF: EitherT.cond( test, (), PlayNommDAppFailure.internal(errorMessage), ) def fromOption[F[_]: Monad, A]( option: Option[A], errorMessage: String, ): StateT[EitherT[F, PlayNommDAppFailure, *], MerkleTrieState, A] = StateT.liftF { EitherT.fromOption(option, PlayNommDAppFailure.external(errorMessage)) } def fromEitherExternal[F[_]: Monad, A]( either: Either[String, A] ): StateT[EitherT[F, PlayNommDAppFailure, *], MerkleTrieState, A] = StateT.liftF { EitherT.fromEither(either).leftMap(PlayNommDAppFailure.external) } def fromEitherInternal[F[_]: Monad, A]( either: Either[String, A] ): StateT[EitherT[F, PlayNommDAppFailure, *], MerkleTrieState, A] = StateT.liftF { EitherT.fromEither(either).leftMap(PlayNommDAppFailure.internal) } def pure[F[_]: Monad, A]( a: A, ): StateT[EitherT[F, PlayNommDAppFailure, *], MerkleTrieState, A] = StateT.liftF(EitherT.pure(a)) def unit[F[_]: Monad] = pure[F, Unit](()) ================================================ FILE: modules/node/src/main/scala/io/leisuremeta/chain/node/repository/BlockRepository.scala ================================================ package io.leisuremeta.chain package node package repository import cats.Monad import cats.data.EitherT import cats.implicits._ import api.model.{Block, Signed} import api.model.Block.BlockHash import lib.crypto.Hash import lib.crypto.Hash.ops._ import lib.datatype.BigNat import lib.failure.DecodingFailure import store.{HashStore, KeyValueStore, SingleValueStore} trait BlockRepository[F[_]] { def bestHeader: EitherT[F, DecodingFailure, Option[Block.Header]] def get(hash: Hash.Value[Block]): EitherT[F, DecodingFailure, Option[Block]] def put(block: Block): EitherT[F, DecodingFailure, Unit] def findByTransaction( txHash: Signed.TxHash ): EitherT[F, DecodingFailure, Option[BlockHash]] } object BlockRepository { def apply[F[_]: BlockRepository]: BlockRepository[F] = summon def fromStores[F[_]: Monad](using bestBlockHeaderStore: SingleValueStore[F, Block.Header], blockHashStore: HashStore[F, Block], blockNumberIndex: KeyValueStore[F, BigNat, BlockHash], txBlockIndex: KeyValueStore[F, Signed.TxHash, BlockHash], ): BlockRepository[F] = new BlockRepository[F] { def bestHeader: EitherT[F, DecodingFailure, Option[Block.Header]] = bestBlockHeaderStore.get() def get( blockHash: Hash.Value[Block] ): EitherT[F, DecodingFailure, Option[Block]] = blockHashStore.get(blockHash) def put(block: Block): EitherT[F, DecodingFailure, Unit] = for { _ <- EitherT.rightT[F, DecodingFailure]( scribe.debug(s"Putting block: $block") ) _ <- EitherT.right[DecodingFailure](blockHashStore.put(block)) _ <- EitherT.rightT[F, DecodingFailure](scribe.debug(s"block is put")) bestHeaderOption <- bestHeader _ <- EitherT.rightT[F, DecodingFailure]( scribe.debug(s"best header option: $bestHeaderOption") ) _ <- (bestHeaderOption match { case Some(best) if best.number.toBigInt >= block.header.number.toBigInt => EitherT.pure[F, DecodingFailure](()) case _ => val blockHash = block.toHash EitherT.right[DecodingFailure](for { _ <- Monad[F].pure(scribe.debug(s"putting best header")) _ <- bestBlockHeaderStore.put(block.header) _ <- blockNumberIndex.put(block.header.number, blockHash) _ <- block.transactionHashes.toList.traverse { txHash => txBlockIndex.put(txHash, blockHash) } _ <- Monad[F].pure(scribe.debug(s"putting best header is completed")) } yield ()) }) _ <- EitherT.rightT[F, DecodingFailure]( scribe.debug(s"Putting completed: $block") ) } yield () def findByTransaction( txHash: Signed.TxHash ): EitherT[F, DecodingFailure, Option[BlockHash]] = txBlockIndex.get(txHash) } } ================================================ FILE: modules/node/src/main/scala/io/leisuremeta/chain/node/repository/StateRepository.scala ================================================ package io.leisuremeta.chain package node package repository import cats.{Functor, Monad} import cats.data.{EitherT, Kleisli} import cats.syntax.traverse.* import lib.merkle.{MerkleTrie, MerkleTrieNode, MerkleTrieState} import lib.merkle.MerkleTrieNode.{MerkleHash, MerkleRoot} import lib.failure.DecodingFailure import store.KeyValueStore trait StateRepository[F[_]]: def get(merkleRoot: MerkleRoot): EitherT[F, DecodingFailure, Option[MerkleTrieNode]] def put(state: MerkleTrieState): EitherT[F, DecodingFailure, Unit] object StateRepository: def apply[F[_]: StateRepository]: StateRepository[F] = summon given nodeStore[F[_]: Functor: StateRepository]: MerkleTrie.NodeStore[F] = Kleisli(StateRepository[F].get(_).leftMap(_.msg)) given fromStores[F[_]: Monad](using stateKvStore: KeyValueStore[F, MerkleHash, MerkleTrieNode], ): StateRepository[F] with def get(merkleRoot: MerkleRoot): EitherT[F, DecodingFailure, Option[MerkleTrieNode]] = stateKvStore.get(merkleRoot) def put(state: MerkleTrieState): EitherT[F, DecodingFailure, Unit] = for // _ <- EitherT.pure(scribe.info(s"Putting state: $state")) _ <- state.diff.toList.traverse { case (hash, (node, count)) => if count <= 0 then EitherT.pure(()) else stateKvStore.get(hash).flatMap{ case None => EitherT.right(stateKvStore.put(hash, node)) case _ => EitherT.pure(()) } } // _ <- EitherT.pure(scribe.info(s"Putting completed: $state")) yield () ================================================ FILE: modules/node/src/main/scala/io/leisuremeta/chain/node/repository/TransactionRepository.scala ================================================ package io.leisuremeta.chain package node package repository import cats.data.EitherT import api.model.TransactionWithResult import lib.crypto.Hash import lib.failure.DecodingFailure import store.HashStore trait TransactionRepository[F[_]]: def get( transactionHash: Hash.Value[TransactionWithResult], ): EitherT[F, DecodingFailure, Option[TransactionWithResult]] def put(transaction: TransactionWithResult): F[Unit] object TransactionRepository: def apply[F[_]](implicit txRepo: TransactionRepository[F], ): TransactionRepository[F] = txRepo def fromStores[F[_]](implicit transctionHashStore: HashStore[F, TransactionWithResult], ): TransactionRepository[F] = new TransactionRepository[F]: def get( transactionHash: Hash.Value[TransactionWithResult], ): EitherT[F, DecodingFailure, Option[TransactionWithResult]] = transctionHashStore.get(transactionHash) def put(transaction: TransactionWithResult): F[Unit] = transctionHashStore.put(transaction) ================================================ FILE: modules/node/src/main/scala/io/leisuremeta/chain/node/service/BlockService.scala ================================================ package io.leisuremeta.chain package node package service import cats.{Functor, Monad} import cats.data.EitherT import cats.syntax.traverse.* import api.model.{Block, Signed, TransactionWithResult} import api.model.Block.ops.toBlockHash import api.model.api_model.BlockInfo import repository.{BlockRepository, TransactionRepository} import lib.crypto.Hash.ops.* object BlockService: def saveBlock[F[_]: Monad: BlockRepository: TransactionRepository]( block: Block, txs: Map[Signed.TxHash, TransactionWithResult], ): EitherT[F, String, Block.BlockHash] = for _ <- BlockRepository[F].put(block).leftMap(_.msg) _ <- EitherT.rightT[F, String](scribe.info(s"Saving txs: $txs")) _ <- block.transactionHashes.toList.traverse { (txHash: Signed.TxHash) => for tx <- EitherT .fromOption[F](txs.get(txHash), s"Missing transaction: $txHash") _ <- EitherT.right[String](TransactionRepository[F].put(tx)) yield () } _ <- EitherT.rightT[F, String](scribe.info(s"txs is saved successfully")) yield block.toHash def index[F[_]: Monad: BlockRepository]( fromOption: Option[Block.BlockHash], limitOption: Option[Int], ): EitherT[F, String, List[BlockInfo]] = def loop(from: Block.BlockHash, limit: Int, acc: List[BlockInfo]): EitherT[F, String, List[BlockInfo]] = if limit <= 0 then EitherT.pure(acc.reverse) else BlockRepository[F].get(from).leftMap(_.msg).flatMap { case None => EitherT.leftT(s"block not found: $from") case Some(block) => val info: BlockInfo = BlockInfo( blockNumber = block.header.number, timestamp = block.header.timestamp, blockHash = from, txCount = block.transactionHashes.size, ) if block.header.number.toBigInt <= BigInt(0) then EitherT.pure((info :: acc).reverse) else loop(block.header.parentHash, limit - 1, info :: acc) } for from <- fromOption match case Some(from) => EitherT.pure(from) case None => BlockRepository[F].bestHeader.leftMap(_.msg).flatMap{ case Some(blockHeader) => EitherT.pure(blockHeader.toHash.toBlockHash) case None => EitherT.leftT(s"Best header not found") } result <- loop(from, limitOption.getOrElse(50), Nil) yield result def get[F[_]: Functor: BlockRepository]( blockHash: Block.BlockHash, ): EitherT[F, String, Option[Block]] = BlockRepository[F].get(blockHash).leftMap(_.msg) ================================================ FILE: modules/node/src/main/scala/io/leisuremeta/chain/node/service/LocalStatusService.scala ================================================ package io.leisuremeta.chain package node package service import cats.MonadError import cats.syntax.flatMap.* import api.model.{Block, NetworkId, NodeStatus} import api.model.Block.ops.* import lib.crypto.Hash.ops.* import lib.datatype.BigNat import lib.failure.DecodingFailure import repository.BlockRepository import java.time.Instant object LocalStatusService: def status[F[_]: BlockRepository]( networkId: NetworkId, genesisTimestamp: Instant, )(using me: MonadError[F, Throwable]): F[NodeStatus] = BlockRepository[F].bestHeader.value.flatMap { case Left(err) => me.raiseError(err) case Right(bestBlockHeader) => val gHash = genesisHash(genesisTimestamp) me.pure { NodeStatus( networkId = networkId, genesisHash = gHash, bestHash = bestBlockHeader.fold(gHash)(_.toHash.toBlockHash), number = bestBlockHeader.fold(BigNat.Zero)(_.number), ) } } def genesisHash(genesisTimestamp: Instant): Block.BlockHash = NodeInitializationService.genesisBlock(genesisTimestamp).toHash ================================================ FILE: modules/node/src/main/scala/io/leisuremeta/chain/node/service/NodeInitializationService.scala ================================================ package io.leisuremeta.chain package node package service import java.time.Instant import cats.Monad import cats.data.EitherT import api.model.{Block, StateRoot} import api.model.Block.ops.* import lib.crypto.Hash import lib.crypto.Hash.ops.* import lib.datatype.{BigNat, UInt256} import repository.BlockRepository object NodeInitializationService: def initialize[F[_]: Monad: BlockRepository]( timestamp: Instant, ): EitherT[F, String, Block] = for _ <- EitherT.rightT[F, String](scribe.info(s"Initialize... ")) bestBlockHeaderOption <- BlockRepository[F].bestHeader.leftMap(_.msg) block <- bestBlockHeaderOption match case None => scribe.info( "No best block header found. Initializing genesis block.", ) val block = genesisBlock(timestamp) BlockRepository[F].put(genesisBlock(timestamp)).leftMap(_.msg).map(_ => block) case Some(header) => scribe.info("Best block header found. Skipping genesis block.") val blockHash = header.toHash.toBlockHash for blockOption <- BlockRepository[F].get(blockHash).leftMap(_.msg) block <- EitherT.fromOption[F]( blockOption, s"best block $blockHash not found in the block repository", ) yield block yield block def genesisBlock(genesisTimestamp: Instant): Block = Block( header = Block.Header( number = BigNat.Zero, parentHash = Hash.Value[Block](UInt256.EmptyBytes), stateRoot = StateRoot.empty, transactionsRoot = None, timestamp = genesisTimestamp, ), transactionHashes = Set.empty, votes = Set.empty, ) ================================================ FILE: modules/node/src/main/scala/io/leisuremeta/chain/node/service/RewardService.scala ================================================ package io.leisuremeta.chain package node package service //import java.time.{DayOfWeek, Instant, ZoneId}//, ZonedDateTime} //import java.time.temporal.{ChronoUnit, TemporalAdjusters} //import cats.{Monad, Monoid} //import cats.data.EitherT //import cats.effect.Concurrent //import cats.syntax.either.catsSyntaxEither //import cats.syntax.eq.catsSyntaxEq //import cats.syntax.foldable.toFoldableOps //import cats.syntax.functor.toFunctorOps //import cats.syntax.traverse.toTraverseOps //import fs2.Stream //import scodec.bits.BitVector //import api.model.{Block,TransactionWithResult}//{Account, Block, GroupId, StateRoot, TransactionWithResult} //import api.model.Block.ops.* //import api.model.TransactionWithResult.ops.* //import api.model.api_model.RewardInfo //import api.model.reward.{DaoActivity, DaoInfo} //import api.model.token.{ // NftState, // Rarity, // TokenDefinition, // TokenDefinitionId, // TokenId, //} //import dapp.PlayNommState //import lib.codec.byte.{ByteDecoder, DecodeResult} //import lib.codec.byte.ByteEncoder.ops.* //import lib.crypto.Hash //import lib.crypto.Hash.ops.* //import lib.datatype.{BigNat, Utf8} //import lib.merkle.{GenericMerkleTrie, GenericMerkleTrieState} //import lib.merkle.GenericMerkleTrie.NodeStore //import repository.{BlockRepository, TransactionRepository} //import repository.GenericStateRepository.given //import lib.merkle.{GenericMerkleTrie, GenericMerkleTrieState} object RewardService: // // def getRewardInfoFromBestHeader[F[_] // : Concurrent: BlockRepository: TransactionRepository: GenericStateRepository.RewardState: GenericStateRepository.TokenState]( // account: Account, // timestampOption: Option[Instant], // daoAccount: Option[Account], // rewardAmount: Option[BigNat], // ): EitherT[F, String, RewardInfo] = // for // bestHeaderOption <- BlockRepository[F].bestHeader.leftMap(_.msg) // bestHeader <- EitherT.fromOption( // bestHeaderOption, // s"Best header not found", // ) // stateRoot = GossipDomain.MerkleState.from(bestHeader) // info <- getRewardInfo[F]( // account, // timestampOption, // daoAccount, // rewardAmount, // stateRoot.reward, // stateRoot.token, // ) // yield info // // // def getRewardInfo[F[_] // : Concurrent: BlockRepository: TransactionRepository: GenericStateRepository.RewardState: GenericStateRepository.TokenState]( // account: Account, // timestampOption: Option[Instant], // daoAccount: Option[Account], // rewardAmount: Option[BigNat], // rewardMerkleState: GossipDomain.MerkleState.RewardMerkleState, // tokenMerkleState: GossipDomain.MerkleState.TokenMerkleState, // ): EitherT[F, String, RewardInfo] = // val timestamp = timestampOption.getOrElse(Instant.now()) // val canonicalTimestamp = getLatestRewardInstantBefore(timestamp) // val userActivityState = rewardMerkleState.userActivityState // val tokenReceivedState = rewardMerkleState.tokenReceivedState // // for // totalNumberOfDao <- countDao[F](rewardMerkleState.daoState) //// _ <- EitherT.pure(scribe.info(s"Total number of DAO: ${totalNumberOfDao}")) // userActivities <- getWeeklyUserActivities[F]( // timestamp, // account, // userActivityState, // ) // userActivityMilliPoint = userActivities.flatten // .map(dailyDaoActivityToMilliPoint) // .foldLeft(BigNat.Zero)(BigNat.add) //// _ <- EitherT.pure(scribe.info(s"userActivities: ${userActivities.flatten}")) // totalActivityMilliPoint <- getWeeklyTotalActivityPoint[F]( // timestamp, // userActivityState, // ) //// _ <- EitherT.pure(scribe.info(s"totalActivityMilliPoint: ${totalActivityMilliPoint}")) // userActivityReward = calculateUserActivityReward( // totalNumberOfDao, // userActivityMilliPoint, // totalActivityMilliPoint, // ) // stateRoot <- findStateRootAt(canonicalTimestamp) //// _ <- EitherT.pure(scribe.info(s"stateRoot: ${stateRoot}")) // tokens <- getNftOwned(account, stateRoot.token.nftBalanceState) //// _ <- EitherT.pure(scribe.info(s"tokens: ${tokens}")) // tokenReceivedDaoActivity <- getWeeklyTokenReceived( // tokens, // canonicalTimestamp, // tokenReceivedState, // ) //// _ <- EitherT.pure(scribe.info(s"tokenReceivedDaoActivity: ${tokenReceivedDaoActivity}")) // totalReceivedMilliPoint <- getWeeklyTotalReceivedPoint( // timestamp, // tokenReceivedState, // ) //// _ <- EitherT.pure(scribe.info(s"totalReceivedMilliPoint: ${totalReceivedMilliPoint}")) // tokenReceivedReward = calculateTokenReceivedReward( // totalNumberOfDao, // tokenReceivedDaoActivity, // totalReceivedMilliPoint, // ) // totalRarityRewardAmount <- getTotalRarityRewardAmount( // rewardAmount, // daoAccount, // ) // userRarityRewardItems <- getUserRarityItem(account, tokenMerkleState) //// _ <- EitherT.pure(scribe.info(s"userRarityRewardItems: ${userRarityRewardItems}")) // userRarityReward = rarityItemsToRewardDetailMap(userRarityRewardItems) // totalRarityRewardValue <- getTotalRarityRewardValue( // account, // tokenMerkleState, // ) //// _ <- EitherT.pure( //// scribe.info(s"totalRarityRewardValue: ${totalRarityRewardValue}"), //// ) // userRarityRewardValue = calculateUserRarityRewardValue( // totalRarityRewardAmount, // totalNumberOfDao, // userRarityRewardItems, // totalRarityRewardValue, // ) // isModerator <- isModerator(account, rewardMerkleState.daoState) // bonus = // if isModerator then userActivityReward + userActivityReward // else BigNat.Zero // total = // userActivityReward + tokenReceivedReward + userRarityRewardValue + bonus // yield RewardInfo( // account = account, // reward = RewardInfo.Reward( // total = total, // activity = userActivityReward, // token = tokenReceivedReward, // rarity = userRarityRewardValue, // bonus = bonus, // ), // point = RewardInfo.Point( // activity = userActivities.flatten.combineAll, // token = tokenReceivedDaoActivity, // rarity = userRarityReward, // ), // timestamp = timestamp, // totalNumberOfDao = BigNat.unsafeFromLong(totalNumberOfDao), // ) //*/ // /** count dao (max value 100) // * // * @return // * number of dao (if larger than 100, return 100) // */ // def countDao[F[_]: Concurrent: PlayNommState]( // daoState: GenericMerkleTrieState[GroupId, DaoInfo], // ): EitherT[F, String, Int] = // for // stream <- GenericMerkleTrie // .from[F, GroupId, DaoInfo](BitVector.empty) // .runA(daoState) // size <- stream.take(100).compile.count // yield size.toInt // def calculateUserActivityReward( // numberOfDao: Int, // userActivityMilliPoint: BigNat, // totalActivityMilliPoint: BigNat, // ): BigNat = // val limit = BigInt(120_000L) * 1000 * numberOfDao // val milliPoint: BigNat = // if totalActivityMilliPoint.toBigInt <= limit then userActivityMilliPoint // else // BigNat.unsafeFromBigInt( // limit, // ) * userActivityMilliPoint / totalActivityMilliPoint // milliPoint * BigNat.unsafeFromBigInt(BigInt(10).pow(15)) // // def getWeeklyUserActivities[F[_]: Concurrent: GenericStateRepository.RewardState]( // timestamp: Instant, // user: Account, // root: GenericMerkleTrieState[(Instant, Account), DaoActivity], // ): EitherT[F, String, Seq[Option[DaoActivity]]] = // val refInstants = getWeeklyRefTime( // getLatestRewardInstantBefore(timestamp), // ) // // refInstants // .traverse { (refInstant) => // GenericMerkleTrie.get[F, (Instant, Account), DaoActivity]( // (refInstant, user).toBytes.bits, // ) // } // .runA(root) // // def getLatestRewardInstantBefore(timestamp: Instant): Instant = // timestamp // .atZone(ZoneId.of("Asia/Seoul")) // .`with`(TemporalAdjusters.previous(DayOfWeek.MONDAY)) // .truncatedTo(ChronoUnit.DAYS) // .toInstant() // def getWeeklyRefTime(last: Instant): Seq[Instant] = // Seq.tabulate(7)(i => last.minus(7 - i, ChronoUnit.DAYS)) // // def dailyDaoActivityToMilliPoint(a: DaoActivity): BigNat = // BigNat // .fromBigInt { // daoActivityToMilliPoint(a).max(4000) // } // .toOption // .getOrElse(BigNat.Zero) // // def daoActivityToMilliPoint(a: DaoActivity): BigInt = // val weights = Seq(10, 10, 10, -10) // Seq(a.like, a.comment, a.share, a.report) // .map(_.toBigInt) // .zip(weights) // .map { case (number, weight) => number * weight } // .sum // // def getWeeklyTotalActivityPoint[F[_] // : Concurrent: GenericStateRepository.RewardState]( // timestamp: Instant, // root: GenericMerkleTrieState[(Instant, Account), DaoActivity], // ): EitherT[F, String, BigNat] = // val to: Instant = getLatestRewardInstantBefore(timestamp) // val from: Instant = getLatestRewardInstantBefore(to) // val timestampBits = from.toBytes.bits // // GenericMerkleTrie // .from[F, (Instant, Account), DaoActivity](timestampBits) // .runA(root) // .flatMap { stream => // stream // .evalMap { case (keyBits, daoActivity) => // EitherT.fromEither[F] { // ByteDecoder[(Instant, Account)] // .decode(keyBits.bytes) // .leftMap(_.msg) // .flatMap { case DecodeResult((instant, account), remainder) => // if remainder.isEmpty then Right((instant, daoActivity)) // else // Left( // s"Non-empty remainder: $remainder in account: ${keyBits.bytes}", // ) // } // } // } // .takeWhile(_._1.compareTo(to) <= 0) // .map(_._2) // .map(dailyDaoActivityToMilliPoint) // .fold(BigNat.Zero)(BigNat.add) // .compile // .toList // } // .map(_.head) // // def calculateWeeklyUserActivityReward( // weeklyUserActivity: Seq[DaoActivity], // ): BigNat = // // val weights = Seq(10, 10, 10, -10) // // val sumBigInt = weeklyUserActivity.map { (a: DaoActivity) => // Seq(a.like, a.comment, a.share, a.report) // .map(_.toBigInt) // .zip(weights) // .map { case (number, weight) => number * weight } // .sum // .max(4000) // Max daily millipoint // }.sum // // BigNat.fromBigInt(sumBigInt).toOption.getOrElse(BigNat.Zero) // // def getUserActivityTimeWindows(last: Instant): Seq[Instant] = // Seq.tabulate(8)(i => last.minus(7 - i, ChronoUnit.DAYS)) // // def findStateRootAt[F[_]: Monad: BlockRepository: TransactionRepository]( // instant: Instant, // ): EitherT[F, String, GossipDomain.MerkleState] = // // def loop( // blockHash: Block.BlockHash, // ): EitherT[F, String, GossipDomain.MerkleState] = // for // blockOption <- BlockRepository[F].get(blockHash).leftMap(_.msg) // block <- EitherT.fromOption(blockOption, s"Block not found: $blockHash") // txs <- block.transactionHashes.toList.traverse { txHash => // TransactionRepository[F] // .get(txHash.toResultHashValue) // .leftMap(_.msg) // .flatMap( // EitherT.fromOption( // _, // s"Transaction $txHash is not found in block $blockHash", // ), // ) // } // ans <- // if txs.exists(_.signedTx.value.createdAt.compareTo(instant) > 0) then // loop(block.header.parentHash) // else EitherT.pure(MerkleState.from(block.header)) // yield ans // // BlockRepository[F].bestHeader // .leftMap(_.msg) // .map(_.get.toHash.toBlockHash) // .flatMap(loop) // def getNftOwned[F[_]: Concurrent: GenericStateRepository.TokenState]( // user: Account, // state: GenericMerkleTrieState[ // (Account, TokenId, Hash.Value[TransactionWithResult]), // Unit, // ], // ): EitherT[F, String, List[TokenId]] = // for // stream <- GenericMerkleTrie // .from[F, (Account, TokenId, Hash.Value[TransactionWithResult]), Unit]( // user.toBytes.bits, // ) // .runA(state) // tokenIds <- stream // .evalMap { case (keyBits, ()) => // EitherT.fromEither[F] { // ByteDecoder[(Account, TokenId, Hash.Value[TransactionWithResult])] // .decode(keyBits.bytes) // .leftMap(_.msg) // .flatMap { // case DecodeResult((account, tokenId, txHash), remainder) // if remainder.isEmpty => // Right((account, tokenId)) // case _ => // Left( // s"fail to decode ${keyBits.bytes} in nft balance of account $user", // ) // } // } // } // .takeWhile(_._1 === user) // .map(_._2) // .compile // .toList // yield tokenIds // // def getWeeklyTokenReceived[F[_]: Monad: GenericStateRepository.RewardState]( // tokenList: List[TokenId], // canonicalTimestamp: Instant, // root: GenericMerkleTrieState[(Instant, TokenId), DaoActivity], // ): EitherT[F, String, DaoActivity] = // val refInstants = getWeeklyRefTime( // getLatestRewardInstantBefore(canonicalTimestamp), // ) // // val keyList = for // refInstant <- refInstants // tokenId <- tokenList // yield (refInstant, tokenId) // // keyList // .traverse { (refInstant, tokenId) => // GenericMerkleTrie // .get[F, (Instant, TokenId), DaoActivity]( // (refInstant, tokenId).toBytes.bits, // ) // .runA(root) // } // .map(_.flatten.combineAll) // // def getWeeklyTotalReceivedPoint[F[_] // : Concurrent: GenericStateRepository.RewardState]( // timestamp: Instant, // root: GenericMerkleTrieState[(Instant, TokenId), DaoActivity], // ): EitherT[F, String, BigNat] = // val to: Instant = getLatestRewardInstantBefore(timestamp) // val from: Instant = getLatestRewardInstantBefore(to) // val timestampBits = from.toBytes.bits // // GenericMerkleTrie // .from[F, (Instant, TokenId), DaoActivity](timestampBits) // .runA(root) // .flatMap { stream => // stream // .evalMap { case (keyBits, daoActivity) => // EitherT.fromEither[F] { // ByteDecoder[(Instant, TokenId)] // .decode(keyBits.bytes) // .leftMap(_.msg) // .flatMap { case DecodeResult((instant, tokenId), remainder) => // if remainder.isEmpty then Right((instant, daoActivity)) // else // Left( // s"Non-empty remainder: $remainder in token: ${keyBits.bytes}", // ) // } // } // } // .takeWhile(_._1.compareTo(to) <= 0) // .map(_._2) // .map(daoActivityToMilliPoint) // .foldMonoid // .compile // .toList // } // .map(_.head) // .map(BigNat.fromBigInt(_).toOption.getOrElse(BigNat.Zero)) // // def calculateTokenReceivedReward( // numberOfDao: Int, // tokenReceivedActivity: DaoActivity, // totalReceivedMilliPoint: BigNat, // ): BigNat = // if numberOfDao <= 0 then BigNat.Zero // else // val limit = BigInt(125_000L) * 1000 * numberOfDao - 50_000L // val tokenReceivedMilliPoint = BigNat // .fromBigInt(daoActivityToMilliPoint(tokenReceivedActivity)) // .toOption // .getOrElse(BigNat.Zero) // val milliPoint: BigNat = // if totalReceivedMilliPoint.toBigInt < limit then tokenReceivedMilliPoint // else // BigNat.unsafeFromBigInt( // limit, // ) * tokenReceivedMilliPoint / totalReceivedMilliPoint // milliPoint * BigNat.unsafeFromBigInt(BigInt(10).pow(15)) // // def getTotalRarityRewardAmount[F[_] // : Concurrent: BlockRepository: TransactionRepository: GenericStateRepository.TokenState]( // rewardAmount: Option[BigNat], // daoAccount: Option[Account], // ): EitherT[F, String, BigNat] = rewardAmount match // case Some(amount) => EitherT.pure(amount) // case None => // val targetAccount = // daoAccount.getOrElse(Account(Utf8.unsafeFrom("DAO-M"))) // EitherT.right { // StateReadService.getFreeBalance[F](targetAccount).map { balanceMap => // balanceMap // .get(TokenDefinitionId(Utf8.unsafeFrom("LM"))) // .fold(BigNat.Zero)(_.totalAmount) // } // } // // def getUserRarityReward[F[_] // : Concurrent: GenericStateRepository.TokenState: GenericStateRepository.RewardState]( // user: Account, // state: GossipDomain.MerkleState.TokenMerkleState, // ): EitherT[F, String, Map[Rarity, BigNat]] = // getUserRarityItem[F](user, state).map(rarityItemsToRewardDetailMap) // // def getUserRarityItem[F[_] // : Concurrent: GenericStateRepository.TokenState: GenericStateRepository.RewardState]( // user: Account, // state: GossipDomain.MerkleState.TokenMerkleState, // ): EitherT[F, String, List[(Rarity, BigNat)]] = // for // stream <- GenericMerkleTrie // .from[F, (Account, TokenId, Hash.Value[TransactionWithResult]), Unit]( // user.toBytes.bits, // ) // .runA(state.nftBalanceState) // result <- stream // .takeWhile(_._1.startsWith(user.toBytes.bits)) // .evalMap { (keyBits, _) => // for // decodeResult <- EitherT.fromEither { // ByteDecoder[ // (Account, TokenId, Hash.Value[TransactionWithResult]), // ] // .decode(keyBits.bytes) // .leftMap(_.msg) // } //// _ <- EitherT.pure{ //// scribe.info(s"Decode Result: $decodeResult") //// } // DecodeResult((_, tokenId, _), remainder) = decodeResult // _ <- EitherT.cond[F]( // remainder.isEmpty, // (), // s"Non-empty remainder: $remainder in nft-balance: ${keyBits.bytes}", // ) // nftStateOption <- GenericMerkleTrie // .get[F, TokenId, NftState](tokenId.toBytes.bits) // .runA(state.nftState) // nftState <- EitherT.fromOption( // nftStateOption, // s"Nft state not found: $tokenId", // ) // /* Set default rarity as 2 */ // weight = // if nftState.weight === BigNat.Zero then BigNat.unsafeFromLong(2) // else nftState.weight // yield (nftState.rarity, weight) // } // .compile // .toList // yield result // // def rarityItemsToRewardDetailMap( // items: List[(Rarity, BigNat)], // ): Map[Rarity, BigNat] = // items.groupMapReduce(_._1)(_._2)(_ + _) // // def getTotalRarityRewardValue[F[_]: Concurrent: GenericStateRepository.TokenState]( // user: Account, // state: GossipDomain.MerkleState.TokenMerkleState, // ): EitherT[F, String, BigNat] = // for // keyBitsStream <- GenericMerkleTrie // .from[F, (TokenDefinitionId, Rarity, TokenId), Unit](BitVector.empty) // .runA(state.rarityState) // stream = keyBitsStream // .evalMap { (keyBits, _) => // EitherT // .fromEither { // ByteDecoder[(TokenDefinitionId, Rarity, TokenId)] // .decode(keyBits.bytes) // .leftMap(_.msg) // .flatMap { // case DecodeResult((defId, rarity, tokenId), remainder) => // if remainder.isEmpty then Right((defId, rarity, tokenId)) // else // Left( // s"Non-empty remainder: $remainder in token: ${keyBits.bytes}", // ) // } // } // } // ansList <- stream // .evalMapAccumulate((Option.empty[TokenDefinition], BigNat.Zero)) { // case ((Some(tokenDef), acc), (defId, rarity, _)) // if tokenDef.id === defId => // val weight = getWeightfromTokenDef(tokenDef, rarity) // EitherT.rightT[F, String] { // ((Some(tokenDef), weight + acc), ()) // } // case ((_, acc), (defId, rarity, _)) => // for // tokenDefOption <- GenericMerkleTrie // .get[F, TokenDefinitionId, TokenDefinition](defId.toBytes.bits) // .runA(state.tokenDefinitionState) // tokenDef <- EitherT.fromOption( // tokenDefOption, // s"TokenDefinition not found: $defId", // ) // yield // val weight = getWeightfromTokenDef(tokenDef, rarity) // ((Some(tokenDef), weight + acc), ()) // } // .last // .compile // .toList // yield ansList.flatten.headOption.fold(BigNat.Zero)(_._1._2) // // def getWeightfromTokenDef( // tokenDef: TokenDefinition, // rarity: Rarity, // ): BigNat = { // for // nftInfo <- tokenDef.nftInfo // weight <- nftInfo.rarity.get(rarity) // yield weight // }.getOrElse(BigNat.Zero) // // def calculateUserRarityRewardValue( // totalRarityRewardAmount: BigNat, // totalNumberOfDao: Int, // userRarityRewardItems: List[(Rarity, BigNat)], // totalRarityRewardValue: BigNat, // ): BigNat = // val e18 = BigInt(10).pow(18) // val limit = // BigNat.unsafeFromBigInt(BigInt(250_000L) * e18 * totalNumberOfDao) // val totalAmount = BigNat.min(totalRarityRewardAmount, limit) // val userRarityReward = // userRarityRewardItems.map(_._2).foldLeft(BigNat.Zero)(BigNat.add) // // val ans = // (totalAmount * userRarityReward / totalRarityRewardValue).floorAt(16) // //// scribe.info(s"totalRarityRewardAmount: ${totalRarityRewardAmount}") //// scribe.info(s"limit: ${limit}") //// scribe.info(s"totalAmount: ${totalAmount}") //// scribe.info(s"userRarityReward: ${userRarityReward}") //// scribe.info(s"userRarityRewardValue: ${ans}") // // ans // // def isModerator[F[_]: Concurrent: GenericStateRepository.RewardState]( // user: Account, // root: GenericMerkleTrieState[GroupId, DaoInfo], // ): EitherT[F, String, Boolean] = // for // stream <- GenericMerkleTrie // .from[F, GroupId, DaoInfo](BitVector.empty) // .runA(root) // ansList <- stream // .exists { case (_, daoInfo) => daoInfo.moderators.contains(user) } // .compile // .toList // yield ansList.head end RewardService ================================================ FILE: modules/node/src/main/scala/io/leisuremeta/chain/node/service/StateReadService.scala ================================================ package io.leisuremeta.chain package node package service import cats.Monad import cats.data.{EitherT, StateT} import cats.effect.Concurrent import cats.syntax.eq.given import cats.syntax.either.* import cats.syntax.functor.* import cats.syntax.flatMap.* import cats.syntax.traverse.* import fs2.Stream import scodec.bits.ByteVector import api.{LeisureMetaChainApi as Api} import api.model.{ Account, AccountData, GroupId, GroupData, PublicKeySummary, Transaction, TransactionWithResult, } import api.model.account.{EthAddress, ExternalChain, ExternalChainAddress} import api.model.api_model.{ AccountInfo, ActivityInfo, BalanceInfo, CreatorDaoInfo, GroupInfo, NftBalanceInfo, } import api.model.creator_dao.CreatorDaoId import api.model.reward.{ ActivitySnapshot, DaoInfo, OwnershipSnapshot, OwnershipRewardLog, } import api.model.token.* import api.model.voting.* import dapp.PlayNommState import lib.codec.byte.ByteEncoder.ops.* import lib.crypto.Hash import lib.datatype.{BigNat, Utf8} import lib.merkle.MerkleTrieState import repository.{BlockRepository, TransactionRepository} object StateReadService: def getAccountInfo[F[_]: Concurrent: BlockRepository: PlayNommState]( account: Account, ): F[Option[AccountInfo]] = for bestHeaderEither <- BlockRepository[F].bestHeader.value bestHeader <- bestHeaderEither match case Left(err) => Concurrent[F].raiseError(err) case Right(None) => Concurrent[F].raiseError(new Exception("No best header")) case Right(Some(bestHeader)) => Concurrent[F].pure(bestHeader) merkleState = MerkleTrieState.fromRootOption(bestHeader.stateRoot.main) accountStateEither <- PlayNommState[F].account.name .get(account) .runA(merkleState) .value accountStateOption <- accountStateEither match case Left(err) => Concurrent[F].raiseError(new Exception(err)) case Right(accountStateOption) => Concurrent[F].pure(accountStateOption) keyListEither <- PlayNommState[F].account.key .streamWithPrefix(account.toBytes) .runA(merkleState) .flatMap(_.compile.toList) .map(_.map { case ((_, pks), info) => (pks, info) }) .value keyList <- keyListEither match case Left(err) => Concurrent[F].raiseError(new Exception(err)) case Right(keyList) => Concurrent[F].pure(keyList) yield accountStateOption.map: accountData => AccountInfo( externalChainAddresses = accountData.externalChainAddresses, ethAddress = accountData.externalChainAddresses .get(ExternalChain.ETH) .map: address => EthAddress(address.utf8), guardian = accountData.guardian, memo = accountData.memo, publicKeySummaries = keyList.toMap, ) def getEthAccount[F[_]: Concurrent: BlockRepository: PlayNommState]( ethAddress: EthAddress, ): F[Option[Account]] = for bestHeaderEither <- BlockRepository[F].bestHeader.value bestHeader <- bestHeaderEither match case Left(err) => Concurrent[F].raiseError(err) case Right(None) => Concurrent[F].raiseError(new Exception("No best header")) case Right(Some(bestHeader)) => Concurrent[F].pure(bestHeader) merkleState = MerkleTrieState.fromRootOption(bestHeader.stateRoot.main) ethStateEither <- PlayNommState[F].account.externalChainAddresses .get((ExternalChain.ETH, ExternalChainAddress(ethAddress.utf8))) .runA(merkleState) .value ethStateOption <- ethStateEither match case Left(err) => Concurrent[F].raiseError(new Exception(err)) case Right(ethStateOption) => Concurrent[F].pure(ethStateOption) yield ethStateOption def getGroupInfo[F[_]: Concurrent: BlockRepository: PlayNommState]( groupId: GroupId, ): F[Option[GroupInfo]] = for bestHeaderEither <- BlockRepository[F].bestHeader.value bestHeader <- bestHeaderEither match case Left(err) => Concurrent[F].raiseError(err) case Right(None) => Concurrent[F].raiseError(new Exception("No best header")) case Right(Some(bestHeader)) => Concurrent[F].pure(bestHeader) merkleState = MerkleTrieState.fromRootOption(bestHeader.stateRoot.main) groupDataEither <- PlayNommState[F].group.group .get(groupId) .runA(merkleState) .value groupDataOption <- groupDataEither match case Left(err) => Concurrent[F].raiseError(new Exception(err)) case Right(groupDataOption) => Concurrent[F].pure(groupDataOption) accountListEither <- PlayNommState[F].group.groupAccount .streamWithPrefix(groupId.toBytes) .runA(merkleState) .flatMap(_.compile.toList) .map(_.map(_._1._2)) .value accountList <- accountListEither match case Left(err) => Concurrent[F].raiseError(new Exception(err)) case Right(accountList) => Concurrent[F].pure(accountList) yield groupDataOption.map: groupData => GroupInfo(groupData, accountList.toSet) def getTokenDef[F[_]: Concurrent: BlockRepository: PlayNommState]( tokenDefinitionId: TokenDefinitionId, ): F[Option[TokenDefinition]] = for bestHeaderEither <- BlockRepository[F].bestHeader.value bestHeader <- bestHeaderEither match case Left(err) => Concurrent[F].raiseError(err) case Right(None) => Concurrent[F].raiseError(new Exception("No best header")) case Right(Some(bestHeader)) => Concurrent[F].pure(bestHeader) merkleState = MerkleTrieState.fromRootOption(bestHeader.stateRoot.main) tokenDefEither <- PlayNommState[F].token.definition .get(tokenDefinitionId) .runA(merkleState) .value tokenDefOption <- tokenDefEither match case Left(err) => Concurrent[F].raiseError(new Exception(err)) case Right(tokenDefOption) => Concurrent[F].pure(tokenDefOption) yield tokenDefOption def getBalance[F[_] : Concurrent: BlockRepository: TransactionRepository: PlayNommState]( account: Account, movable: Api.Movable, ): F[Map[TokenDefinitionId, BalanceInfo]] = movable match case Api.Movable.Free => getFreeBalance[F](account) case Api.Movable.Locked => getEntrustBalance[F](account) def getFreeBalance[F[_] : Concurrent: BlockRepository: TransactionRepository: PlayNommState]( account: Account, ): F[Map[TokenDefinitionId, BalanceInfo]] = for bestHeaderEither <- BlockRepository[F].bestHeader.value bestHeader <- bestHeaderEither match case Left(err) => Concurrent[F].raiseError(err) case Right(None) => Concurrent[F].raiseError(new Exception("No best header")) case Right(Some(bestHeader)) => Concurrent[F].pure(bestHeader) merkleState = MerkleTrieState.fromRootOption(bestHeader.stateRoot.main) // time0 = System.nanoTime() balanceListEither <- PlayNommState[F].token.fungibleBalance .streamWithPrefix(account.toBytes) .runA(merkleState) .flatMap: stream => stream .map: case ((account, defId, txHash), _) => (defId, txHash) .compile .toList .value // time1 = System.nanoTime() // _ = scribe.info(s"balanceList: ${(time1 - time0) / 1e6} ms") balanceList <- balanceListEither match case Left(err) => Concurrent[F].raiseError(new Exception(err)) case Right(balanceList) => Concurrent[F].pure(balanceList) balanceTxEither <- balanceList .traverse: (defId, txHash) => TransactionRepository[F] .get(txHash) .map: txWithResultOption => txWithResultOption.map(txWithResult => (defId, txHash, txWithResult), ) .value balanceTxList <- balanceTxEither match case Left(err) => Concurrent[F].raiseError(new Exception(err.msg)) case Right(balanceTxList) => Concurrent[F].pure(balanceTxList.flatten) yield balanceTxList .groupMapReduce(_._1): (_, txHash, txWithResult) => val info = txWithResult.signedTx.value match case fb: Transaction.FungibleBalance => fb match case mf: Transaction.TokenTx.MintFungibleToken => BalanceInfo( totalAmount = mf.outputs.get(account).getOrElse(BigNat.Zero), unused = Map(txHash -> txWithResult), ) case tf: Transaction.TokenTx.TransferFungibleToken => BalanceInfo( totalAmount = tf.outputs.get(account).getOrElse(BigNat.Zero), unused = Map(txHash -> txWithResult), ) case bf: Transaction.TokenTx.BurnFungibleToken => val amount = txWithResult.result match case Some( Transaction.TokenTx.BurnFungibleTokenResult( outputAmount, ), ) => outputAmount case _ => BigNat.Zero BalanceInfo( totalAmount = amount, unused = Map(txHash -> txWithResult), ) case ef: Transaction.TokenTx.EntrustFungibleToken => val amount = txWithResult.result.fold(BigNat.Zero) { case Transaction.TokenTx.EntrustFungibleTokenResult( remainder, ) => remainder case _ => BigNat.Zero } BalanceInfo( totalAmount = amount, unused = Map(txHash -> txWithResult), ) case de: Transaction.TokenTx.DisposeEntrustedFungibleToken => val amount = de.outputs.get(account).getOrElse(BigNat.Zero) BalanceInfo( totalAmount = amount, unused = Map(txHash -> txWithResult), ) case or: Transaction.RewardTx.OfferReward => BalanceInfo( totalAmount = or.outputs.get(account).getOrElse(BigNat.Zero), unused = Map(txHash -> txWithResult), ) case xr: Transaction.RewardTx.ExecuteReward => val amount = txWithResult.result.fold(BigNat.Zero) { case Transaction.RewardTx.ExecuteRewardResult(outputs) => outputs.get(account).getOrElse(BigNat.Zero) case _ => BigNat.Zero } BalanceInfo( totalAmount = amount, unused = Map(txHash -> txWithResult), ) case xo: Transaction.RewardTx.ExecuteOwnershipReward => val amount = txWithResult.result.fold(BigNat.Zero) { case Transaction.RewardTx.ExecuteOwnershipRewardResult( outputs, ) => outputs.get(account).getOrElse(BigNat.Zero) case _ => BigNat.Zero } BalanceInfo( totalAmount = amount, unused = Map(txHash -> txWithResult), ) case _ => BalanceInfo(totalAmount = BigNat.Zero, unused = Map.empty) // scribe.info(s"Amount of ${txHash.toUInt256Bytes.toHex}: ${info.totalAmount}") info .apply: (a, b) => BalanceInfo( totalAmount = BigNat.add(a.totalAmount, b.totalAmount), unused = a.unused ++ b.unused, ) def getEntrustBalance[F[_] : Concurrent: BlockRepository: TransactionRepository: PlayNommState]( account: Account, ): F[Map[TokenDefinitionId, BalanceInfo]] = for bestHeaderEither <- BlockRepository[F].bestHeader.value bestHeader <- bestHeaderEither match case Left(err) => Concurrent[F].raiseError(err) case Right(None) => Concurrent[F].raiseError(new Exception("No best header")) case Right(Some(bestHeader)) => Concurrent[F].pure(bestHeader) merkleState = MerkleTrieState.fromRootOption(bestHeader.stateRoot.main) balanceListEither <- PlayNommState[F].token.entrustFungibleBalance .streamWithPrefix(account.toBytes) .runA(merkleState) .flatMap: stream => stream .map: case ((account, toAccount, defId, txHash), _) => (defId, txHash) .compile .toList .value balanceList <- balanceListEither match case Left(err) => Concurrent[F].raiseError(new Exception(err)) case Right(balanceList) => Concurrent[F].pure(balanceList) balanceTxEither <- balanceList .traverse: (defId, txHash) => TransactionRepository[F] .get(txHash) .map: txWithResultOption => txWithResultOption.map(txWithResult => (defId, txHash, txWithResult), ) .value balanceTxList <- balanceTxEither match case Left(err) => Concurrent[F].raiseError(new Exception(err.msg)) case Right(balanceTxList) => Concurrent[F].pure(balanceTxList.flatten) yield balanceTxList .groupMapReduce(_._1): (_, txHash, txWithResult) => txWithResult.signedTx.value match case ef: Transaction.TokenTx.EntrustFungibleToken => BalanceInfo( totalAmount = ef.amount, unused = Map(txHash -> txWithResult), ) case _ => BalanceInfo(totalAmount = BigNat.Zero, unused = Map.empty) .apply: (a, b) => BalanceInfo( totalAmount = BigNat.add(a.totalAmount, b.totalAmount), unused = a.unused ++ b.unused, ) def getNftBalance[F[_] : Concurrent: BlockRepository: TransactionRepository: PlayNommState]( account: Account, movableOption: Option[Api.Movable], ): F[Map[TokenId, NftBalanceInfo]] = movableOption match case None => getAllNftBalance[F](account) case Some(Api.Movable.Free) => getFreeNftBalance[F](account) case Some(Api.Movable.Locked) => getEntrustedNftBalance[F](account) def getAllNftBalance[F[_] : Concurrent: BlockRepository: TransactionRepository: PlayNommState]( account: Account, ): F[Map[TokenId, NftBalanceInfo]] = for free <- getFreeNftBalance[F](account) entrusted <- getEntrustedNftBalance[F](account) yield free ++ entrusted def getFreeNftBalance[F[_] : Concurrent: BlockRepository: TransactionRepository: PlayNommState]( account: Account, ): F[Map[TokenId, NftBalanceInfo]] = for bestHeaderEither <- BlockRepository[F].bestHeader.value bestHeader <- bestHeaderEither match case Left(err) => Concurrent[F].raiseError(err) case Right(None) => Concurrent[F].raiseError(new Exception("No best header")) case Right(Some(bestHeader)) => Concurrent[F].pure(bestHeader) merkleState = MerkleTrieState.fromRootOption(bestHeader.stateRoot.main) nftBalanceMap <- getNftBalanceFromNftBalanceState[F]( account, merkleState, ) yield nftBalanceMap def getEntrustedNftBalance[F[_] : Concurrent: BlockRepository: TransactionRepository: PlayNommState]( account: Account, ): F[Map[TokenId, NftBalanceInfo]] = for bestHeaderEither <- BlockRepository[F].bestHeader.value bestHeader <- bestHeaderEither match case Left(err) => Concurrent[F].raiseError(err) case Right(None) => Concurrent[F].raiseError(new Exception("No best header")) case Right(Some(bestHeader)) => Concurrent[F].pure(bestHeader) merkleState = MerkleTrieState.fromRootOption(bestHeader.stateRoot.main) nftBalanceMap <- getEntrustedNftBalanceFromEntrustedNftBalanceState[F]( account, merkleState, ) yield nftBalanceMap def getNftBalanceFromNftBalanceState[F[_] : Concurrent: BlockRepository: TransactionRepository: PlayNommState]( account: Account, mts: MerkleTrieState, ): F[Map[TokenId, NftBalanceInfo]] = for balanceListEither <- PlayNommState[F].token.nftBalance .streamWithPrefix(account.toBytes) .runA(mts) .flatMap: stream => stream .map: case ((account, tokenId, txHash), _) => (tokenId, txHash) .compile .toList .value balanceList <- balanceListEither match case Left(err) => Concurrent[F].raiseError(new Exception(err)) case Right(balanceList) => Concurrent[F].pure(balanceList) balanceTxEither <- balanceList .traverse: (tokenId, txHash) => TransactionRepository[F] .get(txHash) .map: txWithResultOption => txWithResultOption.map: txWithResult => txWithResult.signedTx.value match case nb: Transaction.NftBalance => nb match case mf: Transaction.TokenTx.MintNFT => Map: tokenId -> NftBalanceInfo( mf.tokenDefinitionId, txHash, txWithResult, None, ) case mfm: Transaction.TokenTx.MintNFTWithMemo => Map: tokenId -> NftBalanceInfo( mfm.tokenDefinitionId, txHash, txWithResult, mfm.memo, ) case tn: Transaction.TokenTx.TransferNFT => Map: tokenId -> NftBalanceInfo( tn.definitionId, txHash, txWithResult, tn.memo, ) case de: Transaction.TokenTx.DisposeEntrustedNFT => Map: tokenId -> NftBalanceInfo( de.definitionId, txHash, txWithResult, None, ) case _ => Map.empty .value balanceTxList <- balanceTxEither match case Left(err) => Concurrent[F].raiseError(new Exception(err.msg)) case Right(balanceTxList) => Concurrent[F].pure(balanceTxList.flatten) yield balanceTxList.foldLeft(Map.empty)(_ ++ _) def getEntrustedNftBalanceFromEntrustedNftBalanceState[F[_] : Concurrent: BlockRepository: TransactionRepository: PlayNommState]( account: Account, mts: MerkleTrieState, ): F[Map[TokenId, NftBalanceInfo]] = for balanceListEither <- PlayNommState[F].token.entrustNftBalance .streamWithPrefix(account.toBytes) .runA(mts) .flatMap: stream => stream .map: case ((account, toAccount, tokenId, txHash), _) => (tokenId, txHash) .compile .toList .value balanceList <- balanceListEither match case Left(err) => Concurrent[F].raiseError(new Exception(err)) case Right(balanceList) => Concurrent[F].pure(balanceList) balanceTxEither <- balanceList .traverse: (tokenId, txHash) => TransactionRepository[F] .get(txHash) .map: txWithResultOption => txWithResultOption.map: txWithResult => txWithResult.signedTx.value match case en: Transaction.TokenTx.EntrustNFT => Map: tokenId -> NftBalanceInfo( en.definitionId, txHash, txWithResult, None, ) case _ => Map.empty .value balanceTxList <- balanceTxEither match case Left(err) => Concurrent[F].raiseError(new Exception(err.msg)) case Right(balanceTxList) => Concurrent[F].pure(balanceTxList.flatten) yield balanceTxList.foldLeft(Map.empty)(_ ++ _) def getToken[F[_]: Concurrent: BlockRepository: PlayNommState]( tokenId: TokenId, ): EitherT[F, String, Option[NftState]] = for bestHeaderOption <- BlockRepository[F].bestHeader.leftMap(_.msg) bestHeader <- EitherT.fromOption[F](bestHeaderOption, "No best header") merkleState = MerkleTrieState.fromRootOption(bestHeader.stateRoot.main) nftStateOption <- PlayNommState[F].token.nftState .get(tokenId) .runA(merkleState) yield nftStateOption def getTokenHistory[F[_]: Concurrent: BlockRepository: PlayNommState]( txHash: Hash.Value[TransactionWithResult], ): EitherT[F, String, Option[NftState]] = for bestHeaderOption <- BlockRepository[F].bestHeader.leftMap(_.msg) bestHeader <- EitherT.fromOption[F](bestHeaderOption, "No best header") merkleState = MerkleTrieState.fromRootOption(bestHeader.stateRoot.main) nftStateOption <- PlayNommState[F].token.nftHistory .get(txHash) .runA(merkleState) yield nftStateOption def getOwners[F[_]: Concurrent: BlockRepository: PlayNommState]( tokenDefinitionId: TokenDefinitionId, ): EitherT[F, String, Map[TokenId, Account]] = for bestHeaderOption <- BlockRepository[F].bestHeader.leftMap(_.msg) bestHeader <- EitherT.fromOption[F](bestHeaderOption, "No best header") merkleState = MerkleTrieState.fromRootOption(bestHeader.stateRoot.main) tokenIdList <- PlayNommState[F].token.rarityState .streamWithPrefix(tokenDefinitionId.toBytes) .runA(merkleState) .flatMap(stream => stream.map(_._1._3).compile.toList) ownerOptionList <- tokenIdList.traverse: (tokenId: TokenId) => PlayNommState[F].token.nftState .get(tokenId) .runA(merkleState) .map: nftStateOption => nftStateOption.map(state => (tokenId, state.currentOwner)) yield ownerOptionList.flatten.toMap def getAccountActivity[F[_]: Concurrent: BlockRepository: PlayNommState]( account: Account, ): EitherT[F, Either[String, String], Seq[ActivityInfo]] = val program = PlayNommState[F].reward.accountActivity .streamWithPrefix(account.toBytes) .map: stream => stream .takeWhile(_._1._1 === account) .flatMap: case ((account, instant), logs) => Stream.emits: logs.map: log => ActivityInfo( timestamp = instant, point = log.point, description = log.description, txHash = log.txHash, ) .compile .toList for bestHeaderOption <- BlockRepository[F].bestHeader.leftMap: e => Left(e.msg) bestHeader <- EitherT .fromOption[F](bestHeaderOption, Left("No best header")) merkleState = MerkleTrieState.fromRootOption(bestHeader.stateRoot.main) infosEitherT <- program.runA(merkleState).leftMap(_.asLeft[String]) infos <- infosEitherT.leftMap(_.asLeft[String]) yield infos.toSeq def getTokenActivity[F[_]: Concurrent: BlockRepository: PlayNommState]( tokenId: TokenId, ): EitherT[F, Either[String, String], Seq[ActivityInfo]] = val program = PlayNommState[F].reward.tokenReceived .streamWithPrefix(tokenId.toBytes) .map: stream => stream .takeWhile(_._1._1 === tokenId) .flatMap: case ((tokenId, instant), logs) => Stream.emits: logs.map: log => ActivityInfo( timestamp = instant, point = log.point, description = log.description, txHash = log.txHash, ) .compile .toList for bestHeaderOption <- BlockRepository[F].bestHeader.leftMap: e => Left(e.msg) bestHeader <- EitherT .fromOption[F](bestHeaderOption, Left("No best header")) merkleState = MerkleTrieState.fromRootOption(bestHeader.stateRoot.main) infosEitherT <- program.runA(merkleState).leftMap(_.asLeft[String]) infos <- infosEitherT.leftMap(_.asLeft[String]) yield infos.toSeq def getAccountSnapshot[F[_]: Concurrent: BlockRepository: PlayNommState]( account: Account, ): EitherT[F, Either[String, String], Option[ActivitySnapshot]] = val program = PlayNommState[F].reward.accountSnapshot.get(account) for bestHeaderOption <- BlockRepository[F].bestHeader.leftMap: e => Left(e.msg) bestHeader <- EitherT .fromOption[F](bestHeaderOption, Left("No best header")) merkleState = MerkleTrieState.fromRootOption(bestHeader.stateRoot.main) snapshotOption <- program.runA(merkleState).leftMap(_.asLeft[String]) yield snapshotOption def getTokenSnapshot[F[_]: Concurrent: BlockRepository: PlayNommState]( tokenId: TokenId, ): EitherT[F, Either[String, String], Option[ActivitySnapshot]] = val program = PlayNommState[F].reward.tokenSnapshot.get(tokenId) for bestHeaderOption <- BlockRepository[F].bestHeader.leftMap: e => Left(e.msg) bestHeader <- EitherT .fromOption[F](bestHeaderOption, Left("No best header")) merkleState = MerkleTrieState.fromRootOption(bestHeader.stateRoot.main) snapshotOption <- program.runA(merkleState).leftMap(_.asLeft[String]) yield snapshotOption def getOwnershipSnapshot[F[_]: Concurrent: BlockRepository: PlayNommState]( tokenId: TokenId, ): EitherT[F, Either[String, String], Option[OwnershipSnapshot]] = val program = PlayNommState[F].reward.ownershipSnapshot.get(tokenId) for bestHeaderOption <- BlockRepository[F].bestHeader.leftMap: e => Left(e.msg) bestHeader <- EitherT .fromOption[F](bestHeaderOption, Left("No best header")) merkleState = MerkleTrieState.fromRootOption(bestHeader.stateRoot.main) snapshotOption <- program.runA(merkleState).leftMap(_.asLeft[String]) yield snapshotOption def getOwnershipSnapshotMap[F[_]: Concurrent: BlockRepository: PlayNommState]( from: Option[TokenId], limit: Int, ): EitherT[F, Either[String, String], Map[TokenId, OwnershipSnapshot]] = val program = PlayNommState[F].reward.ownershipSnapshot .streamWithPrefix(from.fold(ByteVector.empty)(_.toBytes)) for bestHeaderOption <- BlockRepository[F].bestHeader.leftMap: e => Left(e.msg) bestHeader <- EitherT .fromOption[F](bestHeaderOption, Left("No best header")) merkleState = MerkleTrieState.fromRootOption(bestHeader.stateRoot.main) snapshotStream <- program.runA(merkleState).leftMap(_.asLeft[String]) snapshots <- snapshotStream .take(limit) .compile .toList .leftMap(_.asLeft[String]) yield snapshots.toMap def getOwnershipRewarded[F[_]: Concurrent: BlockRepository: PlayNommState]( tokenId: TokenId, ): EitherT[F, Either[String, String], Option[OwnershipRewardLog]] = val program = PlayNommState[F].reward.ownershipRewarded.get(tokenId) for bestHeaderOption <- BlockRepository[F].bestHeader.leftMap: e => Left(e.msg) bestHeader <- EitherT .fromOption[F](bestHeaderOption, Left("No best header")) merkleState = MerkleTrieState.fromRootOption(bestHeader.stateRoot.main) logOption <- program.runA(merkleState).leftMap(_.asLeft[String]) yield logOption def getDaoInfo[F[_]: Monad: BlockRepository: PlayNommState]( groupId: GroupId, ): EitherT[F, Either[String, String], Option[DaoInfo]] = val program = PlayNommState[F].reward.dao.get(groupId) for bestHeaderOption <- BlockRepository[F].bestHeader.leftMap: e => Left(e.msg) bestHeader <- EitherT .fromOption[F](bestHeaderOption, Left("No best header")) merkleState = MerkleTrieState.fromRootOption(bestHeader.stateRoot.main) infoOption <- program.runA(merkleState).leftMap(_.asLeft[String]) yield infoOption def getSnapshotState[F[_]: Monad: BlockRepository: PlayNommState]( defId: TokenDefinitionId, ): EitherT[F, Either[String, String], Option[SnapshotState]] = val program = PlayNommState[F].token.snapshotState.get(defId) for bestHeaderOption <- BlockRepository[F].bestHeader.leftMap: e => Left(e.msg) bestHeader <- EitherT .fromOption[F](bestHeaderOption, Left("No best header")) merkleState = MerkleTrieState.fromRootOption(bestHeader.stateRoot.main) stateOption <- program.runA(merkleState).leftMap(_.asLeft[String]) yield stateOption def getFungibleSnapshotBalance[F[_] : Concurrent: BlockRepository: PlayNommState]( account: Account, defId: TokenDefinitionId, snapshotId: SnapshotState.SnapshotId, ): EitherT[ F, Either[String, String], Map[Hash.Value[TransactionWithResult], BigNat], ] = val program = PlayNommState[F].token.fungibleSnapshot .reverseStreamFrom((account, defId).toBytes, Some(snapshotId.toBytes)) .flatMap: stream => StateT.liftF: stream.head.compile.toList.map: case Nil => Map.empty case (k, v) :: _ => v for bestHeaderOption <- BlockRepository[F].bestHeader.leftMap: e => Left(e.msg) bestHeader <- EitherT .fromOption[F](bestHeaderOption, Left("No best header")) merkleState = MerkleTrieState.fromRootOption(bestHeader.stateRoot.main) balanceMap <- program.runA(merkleState).leftMap(_.asLeft[String]) yield balanceMap def getNftSnapshotBalance[F[_]: Concurrent: BlockRepository: PlayNommState]( account: Account, defId: TokenDefinitionId, snapshotId: SnapshotState.SnapshotId, ): EitherT[F, Either[String, String], Set[TokenId]] = val program = PlayNommState[F].token.nftSnapshot .reverseStreamFrom((account, defId).toBytes, Some(snapshotId.toBytes)) .flatMap: stream => StateT.liftF: stream.head.compile.toList.map: case Nil => Set.empty case (_, tokens) :: _ => tokens for bestHeaderOption <- BlockRepository[F].bestHeader.leftMap: e => Left(e.msg) bestHeader <- EitherT .fromOption[F](bestHeaderOption, Left("No best header")) merkleState = MerkleTrieState.fromRootOption(bestHeader.stateRoot.main) balanceTokens <- program.runA(merkleState).leftMap(_.asLeft[String]) yield balanceTokens def getVoteProposal[F[_]: Concurrent: BlockRepository: PlayNommState]( proposalId: ProposalId, ): EitherT[F, Either[String, String], Option[Proposal]] = val program = PlayNommState[F].voting.proposal.get(proposalId) for bestHeaderOption <- BlockRepository[F].bestHeader.leftMap: e => Left(e.msg) bestHeader <- EitherT .fromOption[F](bestHeaderOption, Left("No best header")) merkleState = MerkleTrieState.fromRootOption(bestHeader.stateRoot.main) proposalOption <- program.runA(merkleState).leftMap(_.asLeft[String]) yield proposalOption def getAccountVotes[F[_]: Concurrent: BlockRepository: PlayNommState]( proposalId: ProposalId, voter: Account, ): EitherT[F, Either[String, String], Option[(Utf8, BigNat)]] = val program = PlayNommState[F].voting.votes.get((proposalId, voter)) for bestHeaderOption <- BlockRepository[F].bestHeader.leftMap: e => Left(e.msg) bestHeader <- EitherT .fromOption[F](bestHeaderOption, Left("No best header")) merkleState = MerkleTrieState.fromRootOption(bestHeader.stateRoot.main) voteOption <- program.runA(merkleState).leftMap(_.asLeft[String]) yield voteOption def getVoteCount[F[_]: Concurrent: BlockRepository: PlayNommState]( proposalId: ProposalId, ): EitherT[F, Either[String, String], Map[Utf8, BigNat]] = val program = PlayNommState[F].voting.counting.get(proposalId) for bestHeaderOption <- BlockRepository[F].bestHeader.leftMap: e => Left(e.msg) bestHeader <- EitherT .fromOption[F](bestHeaderOption, Left("No best header")) merkleState = MerkleTrieState.fromRootOption(bestHeader.stateRoot.main) countOption <- program.runA(merkleState).leftMap(_.asLeft[String]) yield countOption.getOrElse(Map.empty) def getCreatorDaoInfo[F[_]: Concurrent: BlockRepository: PlayNommState]( daoId: CreatorDaoId, ): EitherT[F, Either[String, String], Option[CreatorDaoInfo]] = val program = for daoInfoOption <- PlayNommState[F].creatorDao.dao.get(daoId) moderatorList <- PlayNommState[F].creatorDao.daoModerators .streamWithPrefix(daoId.toBytes) .flatMap: stream => StateT.liftF: stream .map(_._1._2) .compile .toList yield daoInfoOption.map: daoInfo => CreatorDaoInfo( id = daoId, name = daoInfo.name, description = daoInfo.description, founder = daoInfo.founder, coordinator = daoInfo.coordinator, moderators = moderatorList.toSet, ) for bestHeaderOption <- BlockRepository[F].bestHeader.leftMap: e => Left(e.msg) bestHeader <- EitherT .fromOption[F](bestHeaderOption, Left("No best header")) merkleState = MerkleTrieState.fromRootOption(bestHeader.stateRoot.main) daoInfoOption <- program.runA(merkleState).leftMap(_.asLeft[String]) yield daoInfoOption def getCreatorDaoMember[F[_]: Concurrent: BlockRepository: PlayNommState]( daoId: CreatorDaoId, from: Option[Account], limit: Int, ): EitherT[F, Either[String, String], Seq[Account]] = val program = PlayNommState[F].creatorDao.daoMembers .streamFrom(daoId.toBytes ++ from.fold(ByteVector.empty)(_.toBytes)) .flatMap: stream => StateT.liftF: stream .map(_._1._2) .limit(limit) .compile .toList for bestHeaderOption <- BlockRepository[F].bestHeader.leftMap: e => Left(e.msg) bestHeader <- EitherT .fromOption[F](bestHeaderOption, Left("No best header")) merkleState = MerkleTrieState.fromRootOption(bestHeader.stateRoot.main) memberList <- program.runA(merkleState).leftMap(_.asLeft[String]) yield memberList ================================================ FILE: modules/node/src/main/scala/io/leisuremeta/chain/node/service/TransactionService.scala ================================================ package io.leisuremeta.chain package node package service import cats.{Functor, Monad} import cats.data.{EitherT, Kleisli} import cats.effect.{Clock, Concurrent, Resource} import cats.effect.std.Semaphore import cats.syntax.functor.* import cats.syntax.traverse.* import scodec.bits.ByteVector import api.model.{Block, Signed, StateRoot, Transaction, TransactionWithResult} import api.model.Block.ops.* import api.model.TransactionWithResult.ops.* import api.model.api_model.TxInfo import dapp.{PlayNommDApp, PlayNommDAppFailure, PlayNommState} import lib.crypto.{Hash, KeyPair} import lib.crypto.Hash.ops.* import lib.crypto.Sign.ops.* import lib.datatype.BigNat import lib.merkle.* import lib.merkle.MerkleTrie.NodeStore import repository.{BlockRepository, StateRepository, TransactionRepository} object TransactionService: def submit[F[_] : Concurrent: Clock: BlockRepository: TransactionRepository: StateRepository: PlayNommState]( semaphore: Semaphore[F], txs: Seq[Signed.Tx], localKeyPair: KeyPair, ): EitherT[F, PlayNommDAppFailure, Seq[Hash.Value[TransactionWithResult]]] = Resource .make: EitherT .right[PlayNommDAppFailure](semaphore.acquire) .map: _ => scribe.info(s"Lock Acquired: $txs") () .apply: _ => EitherT.right[PlayNommDAppFailure]: semaphore.release.map: _ => scribe.info(s"Lock Released: $txs") () .use: _ => submit0[F](txs, localKeyPair) private def submit0[F[_] : Concurrent: Clock: BlockRepository: TransactionRepository: StateRepository: PlayNommState]( txs: Seq[Signed.Tx], localKeyPair: KeyPair, ): EitherT[F, PlayNommDAppFailure, Seq[Hash.Value[TransactionWithResult]]] = for time0 <- EitherT.liftF(Clock[F].realTime) bestBlockHeaderOption <- BlockRepository[F].bestHeader.leftMap: e => scribe.error(s"Best Header Error: $e") PlayNommDAppFailure.internal(s"Fail to get best header: ${e.msg}") bestBlockHeader <- EitherT.fromOption[F]( bestBlockHeaderOption, PlayNommDAppFailure.internal("No best Header Available"), ) baseState = MerkleTrieState.fromRootOption(bestBlockHeader.stateRoot.main) result <- txs .traverse(PlayNommDApp[F]) .run(baseState) (finalState, txWithResults) = result txHashes = txWithResults.map(_.toHash) txState = txs .map(_.toHash) .sortBy(_.toUInt256Bytes.toBytes) .foldLeft(MerkleTrieState.empty): (state, txHash) => given idNodeStore: NodeStore[cats.Id] = Kleisli.pure(None) MerkleTrie .put[cats.Id]( txHash.toUInt256Bytes.toBytes.toNibbles, ByteVector.empty, ) .runS(state) .value .getOrElse(state) stateRoot1 = StateRoot(finalState.root) now <- EitherT.right(Clock[F].realTimeInstant) header = Block.Header( number = BigNat.add(bestBlockHeader.number, BigNat.One), parentHash = bestBlockHeader.toHash.toBlockHash, stateRoot = stateRoot1, transactionsRoot = txState.root, timestamp = now, ) sig <- EitherT .fromEither(header.toHash.signBy(localKeyPair)) .leftMap: msg => scribe.error(s"Fail to sign header: $msg") PlayNommDAppFailure.internal(s"Fail to sign header: $msg") block = Block( header = header, transactionHashes = txHashes.toSet.map(_.toSignedTxHash), votes = Set(sig), ) _ <- BlockRepository[F] .put(block) .leftMap: e => scribe.error(s"Fail to put block: $e") PlayNommDAppFailure.internal(s"Fail to put block: ${e.msg}") _ <- StateRepository[F] .put(finalState) .leftMap: e => scribe.error(s"Fail to put state: $e") PlayNommDAppFailure.internal(s"Fail to put state: ${e.msg}") _ <- txWithResults.traverse: txWithResult => EitherT.liftF: TransactionRepository[F].put(txWithResult) time1 <- EitherT.right(Clock[F].realTime) yield scribe.info(s"total time consumed: ${time1 - time0}") txHashes def index[F[_]: Monad: BlockRepository: TransactionRepository]( blockHash: Block.BlockHash, ): EitherT[F, Either[String, String], Set[TxInfo]] = for blockOption <- BlockRepository[F].get(blockHash).leftMap(e => Left(e.msg)) block <- EitherT .fromOption[F](blockOption, Right(s"block not found: $blockHash")) txInfoSet <- block.transactionHashes.toList .traverse { (txHash) => for txOption <- TransactionRepository[F] .get(txHash.toResultHashValue) .leftMap(e => Left(e.msg)) tx <- EitherT.fromOption[F]( txOption, Left(s"tx not found: $txHash in block $blockHash"), ) yield val txType: String = tx.signedTx.value match case tx: Transaction.AccountTx => "Account" case tx: Transaction.GroupTx => "Group" case tx: Transaction.TokenTx => "Token" case tx: Transaction.RewardTx => "Reward" case tx: Transaction.AgendaTx => "Agenda" case tx: Transaction.VotingTx => "Voting" case tx: Transaction.CreatorDaoTx => "CreatorDao" TxInfo( txHash = txHash, createdAt = tx.signedTx.value.createdAt, account = tx.signedTx.sig.account, `type` = txType, ) } .map(_.toSet) yield txInfoSet def get[F[_]: Functor: TransactionRepository]( txHash: Signed.TxHash, ): EitherT[F, String, Option[TransactionWithResult]] = TransactionRepository[F].get(txHash.toResultHashValue).leftMap(_.msg) ================================================ FILE: modules/node/src/main/scala/io/leisuremeta/chain/node/store/HashStore.scala ================================================ package io.leisuremeta.chain package node.store import cats.data.EitherT import lib.crypto.Hash import lib.crypto.Hash.ops.* import lib.failure.DecodingFailure trait HashStore[F[_], A]: def get(hash: Hash.Value[A]): EitherT[F, DecodingFailure, Option[A]] def put(a: A): F[Unit] def remove(hash: Hash.Value[A]): F[Unit] object HashStore: given fromKeyValueStore[F[_], A: Hash](using kvStore: KeyValueStore[F, Hash.Value[A], A], ): HashStore[F, A] = new HashStore[F, A]: override def get( hash: Hash.Value[A], ): EitherT[F, DecodingFailure, Option[A]] = kvStore.get(hash) override def put(a: A): F[Unit] = kvStore.put(a.toHash, a) override def remove(hash: Hash.Value[A]): F[Unit] = kvStore.remove(hash) ================================================ FILE: modules/node/src/main/scala/io/leisuremeta/chain/node/store/KeyValueStore.scala ================================================ package io.leisuremeta.chain package node.store import cats.data.EitherT import lib.failure.DecodingFailure trait KeyValueStore[F[_], K, V]: def get(key: K): EitherT[F, DecodingFailure, Option[V]] def put(key: K, value: V): F[Unit] def remove(key: K): F[Unit] ================================================ FILE: modules/node/src/main/scala/io/leisuremeta/chain/node/store/SingleValueStore.scala ================================================ package io.leisuremeta.chain package node.store import cats.data.EitherT import lib.datatype.{UInt256, UInt256Bytes} import lib.failure.DecodingFailure trait SingleValueStore[F[_], A]: def get(): EitherT[F, DecodingFailure, Option[A]] def put(a: A): F[Unit] object SingleValueStore: def fromKeyValueStore[F[_], A](using kvStore: KeyValueStore[F, UInt256Bytes, A], ): SingleValueStore[F, A] = new SingleValueStore[F, A]: override def get(): EitherT[F, DecodingFailure, Option[A]] = kvStore.get(Key) override def put(a: A): F[Unit] = kvStore.put(Key, a) private val Key: UInt256Bytes = UInt256.EmptyBytes ================================================ FILE: modules/node/src/main/scala/io/leisuremeta/chain/node/store/interpreter/Bag.scala ================================================ package io.leisuremeta.chain.node.store.interpreter import cats.effect.IO import cats.effect.unsafe.IORuntime import swaydb.Bag.Async import swaydb.{IO as SwayIO} import scala.concurrent.{ExecutionContext, Future, Promise} import scala.util.Failure object Bag: /** Cats-effect 3 async bag implementation */ given swaydb.Bag.Async[IO] = new Async[IO]: self => given runtime: IORuntime = cats.effect.unsafe.implicits.global override def executionContext: ExecutionContext = runtime.compute override val unit: IO[Unit] = IO.unit override def none[A]: IO[Option[A]] = IO.pure(Option.empty) override def apply[A](a: => A): IO[A] = IO(a) override def map[A, B](a: IO[A])(f: A => B): IO[B] = a.map(f) override def transform[A, B](a: IO[A])(f: A => B): IO[B] = a.map(f) override def flatMap[A, B](fa: IO[A])(f: A => IO[B]): IO[B] = fa.flatMap(f) override def success[A](value: A): IO[A] = IO.pure(value) override def failure[A](exception: Throwable): IO[A] = IO.fromTry(Failure(exception)) override def foreach[A](a: IO[A])(f: A => Unit): Unit = f(a.unsafeRunSync()) def fromPromise[A](a: Promise[A]): IO[A] = IO.fromFuture(IO(a.future)) override def complete[A](promise: Promise[A], a: IO[A]): Unit = promise completeWith a.unsafeToFuture() override def fromIO[E: SwayIO.ExceptionHandler, A]( a: SwayIO[E, A], ): IO[A] = IO.fromTry(a.toTry) override def fromFuture[A](a: Future[A]): IO[A] = IO.fromFuture(IO(a)) override def suspend[B](f: => IO[B]): IO[B] = IO.defer(f) override def flatten[A](fa: IO[IO[A]]): IO[A] = fa.flatMap(io => io) ================================================ FILE: modules/node/src/main/scala/io/leisuremeta/chain/node/store/interpreter/MultiInterpreter.scala ================================================ package io.leisuremeta.chain package node package store package interpreter import java.nio.file.Path import cats.data.EitherT import cats.effect.{IO, Resource} import lib.codec.byte.ByteCodec import lib.failure.DecodingFailure import io.leisuremeta.chain.node.NodeConfig.RedisConfig import java.nio.file.Paths @SuppressWarnings(Array("org.wartremover.warts.ImplicitParameter")) class MultiInterpreter[K, V: ByteCodec]( redis: RedisInterpreter[K, V], sway: SwayInterpreter[K, V], ) extends KeyValueStore[IO, K, V]: def get(key: K): EitherT[IO, DecodingFailure, Option[V]] = for rRes <- redis.get(key) res <- rRes match case Some(v) => EitherT.pure[IO, DecodingFailure](Some(v)) case _ => sway.get(key) yield res def put(key: K, value: V): IO[Unit] = for _ <- redis.put(key, value) _ <- sway.put(key, value) yield () def remove(key: K): IO[Unit] = for _ <- redis.remove(key) _ <- sway.remove(key) yield () object MultiInterpreter: def apply[K: ByteCodec, V: ByteCodec](config: RedisConfig, dir: InterpreterTarget): Resource[IO, MultiInterpreter[K, V]] = for redis <- RedisInterpreter[K, V](config, dir.r) sway <- SwayInterpreter[K, V](dir.s) res = new MultiInterpreter(redis, sway) yield res case class InterpreterTarget(r: RedisPath, s: Path) object InterpreterTarget: val BEST_NUM = InterpreterTarget(RedisPath("node:best", 0), Paths.get("sway", "block", "best")) val BLOCK = InterpreterTarget(RedisPath("node:blc:", 0), Paths.get("sway", "block")) val BLOCK_NUM = InterpreterTarget(RedisPath("node:blc_num:", 0), Paths.get("sway", "block", "number")) val TX_BLOCK = InterpreterTarget(RedisPath("node:tx_blc:", 0), Paths.get("sway", "block", "tx")) val MERKLE_TRIE = InterpreterTarget(RedisPath("node:trie:", 0), Paths.get("sway", "state")) val TX = InterpreterTarget(RedisPath("node:tx:", 0), Paths.get("sway", "transaction")) ================================================ FILE: modules/node/src/main/scala/io/leisuremeta/chain/node/store/interpreter/RedisInterpreter.scala ================================================ package io.leisuremeta.chain package node package store package interpreter import cats.Monad import cats.data.EitherT import cats.effect.{IO, Resource} import scodec.bits.ByteVector import io.lettuce.core._ import lib.codec.byte.{ByteCodec, DecodeResult} import lib.failure.DecodingFailure import io.lettuce.core.api.async.RedisAsyncCommands import io.lettuce.core.codec.RedisCodec import java.nio.ByteBuffer import scala.util.Try import io.leisuremeta.chain.lib.datatype.Utf8 import scala.util.Success import io.leisuremeta.chain.lib.datatype.UInt256 import io.leisuremeta.chain.node.NodeConfig.RedisConfig @SuppressWarnings(Array("org.wartremover.warts.ImplicitParameter")) class RedisInterpreter[K, V: ByteCodec]( cmds: RedisAsyncCommands[K, V], dir: RedisPath, ) extends KeyValueStore[IO, K, V]: def get(key: K): EitherT[IO, DecodingFailure, Option[V]] = for _ <- EitherT.pure[IO, DecodingFailure]: scribe.debug(s"===> $dir: Geting with key: $key") v <- Try[V](cmds.get(key).get()) match case Success(value) if (value != null) => EitherT.right(IO.pure(Some(value))) case _ => EitherT.right(IO.pure(None)) _ <- EitherT.pure[IO, DecodingFailure]: scribe.debug(s"===> $dir: Got value: ${v}") yield v def put(key: K, value: V): IO[Unit] = for _ <- Monad[IO].pure(scribe.debug(s"===> $dir: Putting $key -> $value")) _ = cmds.set(key, value) yield () def remove(key: K): IO[Unit] = for _ <- IO.unit _ = cmds.del(key) yield () case class RedisPath(path: String, db: Int): def toByteVector = Utf8.unsafeFrom(path).bytes object RedisInterpreter: def apply[K: ByteCodec, V: ByteCodec](config: RedisConfig, dir: RedisPath): Resource[IO, RedisInterpreter[K, V]] = object CodecImpl extends RedisCodec[K, V]: @SuppressWarnings(Array("org.wartremover.warts.Throw")) def decodeKey(bytes: ByteBuffer): K = ByteCodec[K].decode(ByteVector(bytes)) match case Right(DecodeResult(v, r)) if r.isEmpty => v case _ => throw new Exception(s"Fail to decode $bytes") @SuppressWarnings(Array("org.wartremover.warts.Throw")) def decodeValue(bytes: ByteBuffer): V = ByteCodec[V].decode(ByteVector(bytes)) match case Right(DecodeResult(v, r)) if r.isEmpty => v case _ => throw new Exception(s"Fail to decode $bytes") def encodeKey(key: K): ByteBuffer = if (key != UInt256.EmptyBytes) (dir.toByteVector ++ ByteCodec[K].encode(key)).toByteBuffer else dir.toByteVector.toByteBuffer def encodeValue(value: V): ByteBuffer = ByteCodec[V].encode(value).toByteBuffer val uri = RedisURI.Builder.redis(config.host, config.port).withDatabase(dir.db).build() val client = IO(RedisClient.create(uri)) Resource .make(client)(c => IO(c.close())) .map(c => new RedisInterpreter[K, V](c.connect(CodecImpl).async(), dir)) ================================================ FILE: modules/node/src/main/scala/io/leisuremeta/chain/node/store/interpreter/SwayInterpreter.scala ================================================ package io.leisuremeta.chain package node package store package interpreter import java.nio.file.Path import scala.concurrent.ExecutionContext import cats.Monad import cats.data.EitherT import cats.effect.{IO, Resource} import cats.implicits._ import scodec.bits.ByteVector import swaydb.Map import swaydb.data.order.KeyOrder import swaydb.data.slice.Slice import swaydb.serializers.Serializer import swaydb.serializers.Default.ByteArraySerializer import lib.codec.byte.{ByteCodec, ByteDecoder, ByteEncoder, DecodeResult} import lib.datatype.BigNat import lib.failure.DecodingFailure import Bag.given @SuppressWarnings(Array("org.wartremover.warts.ImplicitParameter")) class SwayInterpreter[K, V: ByteCodec]( map: Map[K, Array[Byte], Nothing, IO], dir: Path, ) extends KeyValueStore[IO, K, V] { def get(key: K): EitherT[IO, DecodingFailure, Option[V]] = for { _ <- EitherT.pure[IO, DecodingFailure]( scribe.debug(s"===> $dir: Geting with key: $key") ) arrayOption <- EitherT.right(map.get(key)) _ <- EitherT.pure[IO, DecodingFailure]( scribe.debug( s"===> $dir: Got value: ${arrayOption.map(ByteVector.view).map(_.toHex)}" ) ) decodeResult <- arrayOption.traverse { array => EitherT.fromEither[IO]( ByteDecoder[V].decode(ByteVector.view(array)) ) } } yield decodeResult.map(_.value) def put(key: K, value: V): IO[Unit] = for { _ <- Monad[IO].pure(scribe.debug(s"===> $dir: Putting $key -> $value")) _ <- map.put(key, ByteEncoder[V].encode(value).toArray) } yield () def remove(key: K): IO[Unit] = map.remove(key).map(_ => ()) } object SwayInterpreter { def apply[K: ByteCodec, V: ByteCodec](dir: Path): Resource[IO, SwayInterpreter[K, V]] = { val map: IO[Map[K, Array[Byte], Nothing, IO]] = given KeyOrder[Slice[Byte]] = KeyOrder.default given KeyOrder[K] = null given ExecutionContext = swaydb.configs.level.DefaultExecutionContext.compactionEC swaydb.persistent.Map[K, Array[Byte], Nothing, IO](dir) Resource .make(map)(_.close()) .map(new SwayInterpreter[K, V](_, dir)) } def ensureNoRemainder[A]( decoded: DecodeResult[A], msg: String, ): Either[DecodingFailure, A] = Either.cond(decoded.remainder.isEmpty, decoded.value, DecodingFailure(msg)) given scala.reflect.ClassTag[Nothing] = scala.reflect.Manifest.Nothing given swaydb.data.sequencer.Sequencer[IO] = null given swaydb.core.build.BuildValidator = swaydb.core.build.BuildValidator.DisallowOlderVersions(swaydb.data.DataType.Map) def reverseBignatStoreIndex[A: ByteCodec](dir: Path): IO[SwayInterpreter[BigNat, A]] = { given KeyOrder[Slice[Byte]] = KeyOrder.reverseLexicographic val map = swaydb.persistent.Map[BigNat, Array[Byte], Nothing, IO](dir) map.map(new SwayInterpreter[BigNat, A](_, dir)) } @SuppressWarnings(Array("org.wartremover.warts.Throw")) implicit def byteCodecToSerialize[A: ByteCodec]: Serializer[A] = new Serializer[A] { override def write(data: A): Slice[Byte] = Slice[Byte](ByteEncoder[A].encode(data).toArray) override def read(data: Slice[Byte]): A = ByteDecoder[A].decode(ByteVector view data.toArray) match { case Right(DecodeResult(value, remainder)) if remainder.isEmpty => value case anything => throw new Exception(s"Fail to decode $data: $anything") } } } ================================================ FILE: modules/node/src/test/scala/io/leisuremeta/chain/node/dapp/PlayNommDAppTest.scala ================================================ package io.leisuremeta.chain package node package dapp import api.model.* import api.model.token.* import lib.codec.byte.ByteEncoder.ops.* import lib.crypto.{CryptoOps, KeyPair} import lib.crypto.Hash.ops.* import lib.crypto.Sign.ops.* import lib.datatype.{BigNat, Utf8} import lib.failure.DecodingFailure import lib.merkle.MerkleTrie.NodeStore import lib.merkle.MerkleTrieState import repository.TransactionRepository import store.KeyValueStore import cats.data.{EitherT, Kleisli, StateT} import cats.effect.IO import cats.syntax.all.* import munit.CatsEffectSuite class PlayNommDAppTest extends CatsEffectSuite: given emptyNodeStore: NodeStore[IO] = Kleisli.liftF(EitherT.pure(None)) given inMemoryStore[K, V]: KeyValueStore[IO, K, V] = new KeyValueStore[IO, K, V]: val store: collection.mutable.Map[K, V] = collection.mutable.Map.empty def get(key: K): EitherT[IO, DecodingFailure, Option[V]] = store.get(key).pure[EitherT[IO, DecodingFailure, *]] def put(key: K, value: V): IO[Unit] = IO.delay: store.put(key, value) () def remove(key: K): IO[Unit] = IO.delay: store.remove(key) () given txRepo: TransactionRepository[IO] = TransactionRepository.fromStores[IO] given initialPlayNommState: PlayNommState[IO] = PlayNommState.build[IO] val aliceKey = CryptoOps.fromPrivate: BigInt( "b229e76b742616db3ac2c5c2418f44063fcc5fcc52a08e05d4285bdb31acba06", 16, ) val alicePKS = PublicKeySummary.fromPublicKeyHash(aliceKey.publicKey.toHash) val alice = Account(Utf8.unsafeFrom("alice")) val bob = Account(Utf8.unsafeFrom("bob")) val carol = Account(Utf8.unsafeFrom("carol")) val networkId = NetworkId(BigNat.unsafeFromLong(2021L)) val mintGroup = GroupId(Utf8.unsafeFrom("mint-group")) val testToken = TokenDefinitionId(Utf8.unsafeFrom("test-token")) def sign(account: Account, key: KeyPair)(tx: Transaction): Signed.Tx = key.sign(tx).map(sig => Signed(AccountSignature(sig, account), tx)) match case Right(signedTx) => signedTx case Left(msg) => throw Exception(msg) def signAlice = sign(alice, aliceKey) def readFungibleSnapshotAndTotalSupplySnapshot( account: Account, ): StateT[ EitherT[IO, PlayNommDAppFailure, *], MerkleTrieState, (Option[BigNat], Option[BigNat]), ] = for snapshotStream <- PlayNommState[IO].token.fungibleSnapshot .reverseStreamFrom((alice, testToken).toBytes, None) .mapK: PlayNommDAppFailure.mapExternal: "Failed to get fungible snapshot stream" snapshotHeadList <- StateT .liftF: snapshotStream.head.compile.toList .mapK: PlayNommDAppFailure.mapExternal: "Failed to get snapshot stream head" totalAmountSnapshotStream <- PlayNommState[IO].token.totalSupplySnapshot .reverseStreamFrom(testToken.toBytes, None) .mapK: PlayNommDAppFailure.mapExternal: "Failed to get total supply snapshot stream" totalAmountSnapshotHeadList <- StateT .liftF: totalAmountSnapshotStream.head.compile.toList .mapK: PlayNommDAppFailure.mapExternal: "Failed to get total amount snapshot stream head" yield ( snapshotHeadList.headOption .map(_._2) .map(_.values.foldLeft(BigNat.Zero)(BigNat.add)), totalAmountSnapshotHeadList.headOption.map(_._2), ) val fixture: IO[MerkleTrieState] = val txs: Seq[Transaction] = IndexedSeq( Transaction.AccountTx.CreateAccount( networkId = networkId, createdAt = java.time.Instant.parse("2023-01-11T19:01:00.00Z"), account = alice, ethAddress = None, guardian = None, ), Transaction.GroupTx.CreateGroup( networkId = networkId, createdAt = java.time.Instant.parse("2023-01-11T19:02:00.00Z"), groupId = mintGroup, name = Utf8.unsafeFrom("Mint Group"), coordinator = alice, ), Transaction.GroupTx.AddAccounts( networkId = networkId, createdAt = java.time.Instant.parse("2023-01-11T19:03:00.00Z"), groupId = mintGroup, accounts = Set(alice), ), Transaction.TokenTx.DefineToken( networkId = networkId, createdAt = java.time.Instant.parse("2023-01-11T19:04:00.00Z"), definitionId = testToken, name = Utf8.unsafeFrom("Test Token"), symbol = Some(Utf8.unsafeFrom("TST")), minterGroup = Some(mintGroup), nftInfo = None, ), ) txs .map(signAlice(_)) .traverse(PlayNommDApp[IO](_)) .runS(MerkleTrieState.empty) .value .flatMap: case Right(state) => IO.pure(state) case Left(failure) => IO.raiseError(new Exception(failure.toString)) test("Account is added to group"): val program = for findOption <- PlayNommState[IO].group.groupAccount .get((mintGroup, alice)) yield findOption.nonEmpty for state <- fixture result <- program.runA(state).value yield assertEquals(result, Right(true)) test("Snapshot is created successfully"): val txs = Seq( Transaction.TokenTx.CreateSnapshots( networkId = networkId, createdAt = java.time.Instant.parse("2023-01-11T19:05:00.00Z"), definitionIds = Set(testToken), memo = Some(Utf8.unsafeFrom("Snapshot #0")), ), ) val program = for _ <- txs.map(signAlice(_)).traverse(PlayNommDApp[IO](_)) snapshotStateOption <- PlayNommState[IO].token.snapshotState .get(testToken) .mapK: PlayNommDAppFailure.mapExternal: "Failed to get snapshot state" yield snapshotStateOption for state <- fixture result <- program.runA(state).value snapshotStateOption <- IO.fromEither: result.leftMap(failure => new Exception(failure.msg)) yield assertEquals( snapshotStateOption.map(_.snapshotId), Some(SnapshotState.SnapshotId(BigNat.One)), ) test("Minting and snapshotting reflects in fungible snapshot balance"): val txs = Seq( Transaction.TokenTx.MintFungibleToken( networkId = networkId, createdAt = java.time.Instant.parse("2023-01-11T19:05:00.00Z"), definitionId = testToken, outputs = Map( alice -> BigNat.unsafeFromLong(100L), ), ), Transaction.TokenTx.CreateSnapshots( networkId = networkId, createdAt = java.time.Instant.parse("2023-01-11T19:06:00.00Z"), definitionIds = Set(testToken), memo = Some(Utf8.unsafeFrom("Snapshot #1")), ), ) val program = for _ <- txs.map(signAlice(_)).traverse(PlayNommDApp[IO](_)) snapshotTuple <- readFungibleSnapshotAndTotalSupplySnapshot(alice) yield snapshotTuple for state <- fixture result <- program.runA(state).value snapshotStateTuple <- IO.fromEither: result.leftMap(failure => new Exception(failure.msg)) (snapshotStateOption, totalAmountOption) = snapshotStateTuple snapShotSum = snapshotStateOption.map(_.toBigInt) totalAmount = totalAmountOption.map(_.toBigInt) yield assertEquals(snapShotSum, Some(BigInt(100L))) assertEquals(totalAmount, Some(BigInt(100L))) test("mint -> snapshot -> snapshot reflects last mint amount"): val txs = Seq( Transaction.TokenTx.MintFungibleToken( networkId = networkId, createdAt = java.time.Instant.parse("2023-01-11T19:05:00.00Z"), definitionId = testToken, outputs = Map( alice -> BigNat.unsafeFromLong(100L), ), ), Transaction.TokenTx.CreateSnapshots( networkId = networkId, createdAt = java.time.Instant.parse("2023-01-11T19:06:00.00Z"), definitionIds = Set(testToken), memo = Some(Utf8.unsafeFrom("Snapshot #1")), ), Transaction.TokenTx.CreateSnapshots( networkId = networkId, createdAt = java.time.Instant.parse("2023-01-11T19:07:00.00Z"), definitionIds = Set(testToken), memo = Some(Utf8.unsafeFrom("Snapshot #2")), ), ) val program = for _ <- txs.map(signAlice(_)).traverse(PlayNommDApp[IO](_)) snapshotTuple <- readFungibleSnapshotAndTotalSupplySnapshot(alice) yield snapshotTuple for state <- fixture result <- program.runA(state).value snapshotStateTuple <- IO.fromEither: result.leftMap(failure => new Exception(failure.msg)) (snapshotStateOption, totalAmountOption) = snapshotStateTuple snapShotSum = snapshotStateOption.map(_.toBigInt) totalAmount = totalAmountOption.map(_.toBigInt) yield assertEquals(snapShotSum, Some(BigInt(100L))) assertEquals(totalAmount, Some(BigInt(100L))) ================================================ FILE: modules/node-proxy/src/main/resources/migration-node.json ================================================ {} ================================================ FILE: modules/node-proxy/src/main/scala/io/leisuremeta/chain/node/proxy/NodeProxyApi.scala ================================================ package io.leisuremeta.chain package node package proxy import java.time.Instant import sttp.model.MediaType import sttp.tapir.* import api.model.* import api.model.account.EthAddress import api.model.token.* import api.model.creator_dao.* object NodeProxyApi: val jsonType = MediaType.ApplicationJson.toString @SuppressWarnings(Array("org.wartremover.warts.Any")) val getTxSetEndpoint = endpoint.get .in("tx" / query[String]("block")) .out(statusCode.and(stringJsonBody)) .out(header("Content-Type", jsonType)) @SuppressWarnings(Array("org.wartremover.warts.Any")) val getTxEndpoint = endpoint.get .in("tx" / path[String]) .out(statusCode.and(stringJsonBody)) .out(header("Content-Type", jsonType)) @SuppressWarnings(Array("org.wartremover.warts.Any")) val postTxEndpoint = endpoint.post .in("tx") .in(stringBody) .out(statusCode.and(stringJsonBody)) .out(header("Content-Type", jsonType)) @SuppressWarnings(Array("org.wartremover.warts.Any")) val postTxHashEndpoint = endpoint.post .in("txhash") .in(stringBody) .out(statusCode.and(stringJsonBody)) .out(header("Content-Type", jsonType)) @SuppressWarnings(Array("org.wartremover.warts.Any")) val getStatusEndpoint = endpoint.get.in("status") .out(statusCode.and(stringJsonBody)) .out(header("Content-Type", jsonType)) @SuppressWarnings(Array("org.wartremover.warts.Any")) val getAccountEndpoint = endpoint.get .in("account" / path[Account]) .out(statusCode.and(stringJsonBody)) .out(header("Content-Type", jsonType)) @SuppressWarnings(Array("org.wartremover.warts.Any")) val getEthEndpoint = endpoint.get .in("eth" / path[EthAddress]) .out(statusCode.and(stringJsonBody)) .out(header("Content-Type", jsonType)) @SuppressWarnings(Array("org.wartremover.warts.Any")) val getGroupEndpoint = endpoint.get .in("group" / path[GroupId]) .out(statusCode.and(stringJsonBody)) .out(header("Content-Type", jsonType)) @SuppressWarnings(Array("org.wartremover.warts.Any")) val getBlockListEndpoint = endpoint.get .in: "block" / query[Option[String]]("from") .and(query[Option[Int]]("limit")) .out(statusCode.and(stringJsonBody)) .out(header("Content-Type", jsonType)) @SuppressWarnings(Array("org.wartremover.warts.Any")) val getBlockEndpoint = endpoint.get .in("block" / path[String]) .out(statusCode.and(stringJsonBody)) .out(header("Content-Type", jsonType)) @SuppressWarnings(Array("org.wartremover.warts.Any")) val getTokenDefinitionEndpoint = endpoint.get .in("token-def" / path[TokenDefinitionId]) .out(statusCode.and(stringJsonBody)) .out(header("Content-Type", jsonType)) @SuppressWarnings(Array("org.wartremover.warts.Any")) val getBalanceEndpoint = endpoint.get .in("balance" / path[Account].and(query[String]("movable"))) .out(statusCode.and(stringJsonBody)) .out(header("Content-Type", jsonType)) @SuppressWarnings(Array("org.wartremover.warts.Any")) val getNftBalanceEndpoint = endpoint.get .in("nft-balance" / path[Account].and(query[Option[String]]("movable"))) .out(statusCode.and(stringJsonBody)) .out(header("Content-Type", jsonType)) @SuppressWarnings(Array("org.wartremover.warts.Any")) val getTokenEndpoint = endpoint.get .in("token" / path[TokenId]) .out(statusCode.and(stringJsonBody)) .out(header("Content-Type", jsonType)) @SuppressWarnings(Array("org.wartremover.warts.Any")) val getTokenHistoryEndpoint = endpoint.get .in("token-hist" / path[String]) .out(statusCode.and(stringJsonBody)) .out(header("Content-Type", jsonType)) @SuppressWarnings(Array("org.wartremover.warts.Any")) val getOwnersEndpoint = endpoint.get .in("owners" / path[TokenDefinitionId]) .out(statusCode.and(stringJsonBody)) .out(header("Content-Type", jsonType)) @SuppressWarnings(Array("org.wartremover.warts.Any")) val getAccountActivityEndpoint = endpoint.get .in("activity" / "account" / path[Account]) .out(statusCode.and(stringJsonBody)) .out(header("Content-Type", jsonType)) @SuppressWarnings(Array("org.wartremover.warts.Any")) val getTokenActivityEndpoint = endpoint.get .in("activity" / "token" / path[TokenId]) .out(statusCode.and(stringJsonBody)) .out(header("Content-Type", jsonType)) @SuppressWarnings(Array("org.wartremover.warts.Any")) val getAccountSnapshotEndpoint = endpoint.get .in("snapshot" / "account" / path[Account]) .out(statusCode.and(stringJsonBody)) .out(header("Content-Type", jsonType)) @SuppressWarnings(Array("org.wartremover.warts.Any")) val getTokenSnapshotEndpoint = endpoint.get .in("snapshot" / "token" / path[TokenId]) .out(statusCode.and(stringJsonBody)) .out(header("Content-Type", jsonType)) @SuppressWarnings(Array("org.wartremover.warts.Any")) val getOwnershipSnapshotEndpoint = endpoint.get .in("snapshot" / "ownership" / path[TokenId]) .out(statusCode.and(stringJsonBody)) .out(header("Content-Type", jsonType)) @SuppressWarnings(Array("org.wartremover.warts.Any")) val getOwnershipSnapshotMapEndpoint = endpoint.get .in: "snapshot" / "ownership" / query[Option[TokenId]]("from") .and(query[Option[Int]]("limit")) .out(statusCode.and(stringJsonBody)) .out(header("Content-Type", jsonType)) @SuppressWarnings(Array("org.wartremover.warts.Any")) val getOwnershipRewardedEndpoint = endpoint.get .in("rewarded" / "ownership" / path[TokenId]) .out(statusCode.and(stringJsonBody)) .out(header("Content-Type", jsonType)) @SuppressWarnings(Array("org.wartremover.warts.Any")) val getRewardEndpoint = endpoint.get .in: "reward" / path[Account] .and(query[Option[Instant]]("timestamp")) .and(query[Option[Account]]("dao-account")) .and(query[Option[String]]("reward-amount")) .out(statusCode.and(stringJsonBody)) .out(header("Content-Type", jsonType)) @SuppressWarnings(Array("org.wartremover.warts.Any")) val getDaoInfoEndpoint = endpoint.get .in("dao" / path[GroupId]) .out(statusCode.and(stringJsonBody)) .out(header("Content-Type", jsonType)) @SuppressWarnings(Array("org.wartremover.warts.Any")) val getSnapshotStateEndpoint = endpoint.get .in("snapshot-state" / path[TokenDefinitionId]) .out(statusCode.and(stringJsonBody)) .out(header("Content-Type", jsonType)) @SuppressWarnings(Array("org.wartremover.warts.Any")) val getFungibleSnapshotBalanceEndpoint = endpoint.get .in: "snapshot-balance" / path[Account] / path[TokenDefinitionId] / path[String] .out(statusCode.and(stringJsonBody)) .out(header("Content-Type", jsonType)) @SuppressWarnings(Array("org.wartremover.warts.Any")) val getNftSnapshotBalanceEndpoint = endpoint.get .in: "nft-snapshot-balance" / path[Account] / path[TokenDefinitionId] / path[String] .out(statusCode.and(stringJsonBody)) .out(header("Content-Type", jsonType)) @SuppressWarnings(Array("org.wartremover.warts.Any")) val getVoteProposalEndpoint = endpoint.get .in("vote" / "proposal" / path[String]) .out(statusCode.and(stringJsonBody)) .out(header("Content-Type", jsonType)) @SuppressWarnings(Array("org.wartremover.warts.Any")) val getAccountVotesEndpoint = endpoint.get .in("vote" / "account" / path[String] / path[Account]) .out(statusCode.and(stringJsonBody)) .out(header("Content-Type", jsonType)) @SuppressWarnings(Array("org.wartremover.warts.Any")) val getVoteCountEndpoint = endpoint.get .in("vote" / "count" / path[String]) .out(statusCode.and(stringJsonBody)) .out(header("Content-Type", jsonType)) @SuppressWarnings(Array("org.wartremover.warts.Any")) val getCreatorDaoInfoEndpoint = endpoint.get .in("creator-dao" / path[CreatorDaoId]) .out(statusCode.and(stringJsonBody)) .out(header("Content-Type", jsonType)) @SuppressWarnings(Array("org.wartremover.warts.Any")) val getCreatorDaoMemberEndpoint = endpoint.get .in: "creator-dao" / path[CreatorDaoId] / "member" .and(query[Option[Account]]("from")) .and(query[Option[Int]]("limit")) .out(statusCode.and(stringJsonBody)) .out(header("Content-Type", jsonType)) // enum Movable: // case Free, Locked // object Movable: // @SuppressWarnings(Array("org.wartremover.warts.ToString")) // given Codec[String, Movable, TextPlain] = Codec.string.mapDecode { // (s: String) => // s match // case "free" => DecodeResult.Value(Movable.Free) // case "locked" => DecodeResult.Value(Movable.Locked) // case _ => DecodeResult.Error(s, new Exception(s"invalid movable: $s")) // }(_.toString.toLowerCase(Locale.ENGLISH)) // @SuppressWarnings(Array("org.wartremover.warts.ToString")) // given Codec[String, Option[Movable], TextPlain] = Codec.string.mapDecode { // (s: String) => // s match // case "free" => DecodeResult.Value(Some(Movable.Free)) // case "locked" => DecodeResult.Value(Some(Movable.Locked)) // case "all" => DecodeResult.Value(None) // case _ => DecodeResult.Error(s, new Exception(s"invalid movable: $s")) // }(_.fold("")(_.toString.toLowerCase(Locale.ENGLISH))) ================================================ FILE: modules/node-proxy/src/main/scala/io/leisuremeta/chain/node/proxy/NodeProxyApp.scala ================================================ package io.leisuremeta.chain package node package proxy import cats.effect.std.Dispatcher import cats.effect.Async import cats.effect.kernel.Resource import com.linecorp.armeria.server.Server import sttp.tapir.server.interceptor.log.DefaultServerLog import sttp.tapir.server.armeria.cats.{ ArmeriaCatsServerInterpreter, ArmeriaCatsServerOptions, } import sttp.tapir.server.ServerEndpoint import sttp.capabilities.fs2.Fs2Streams import io.leisuremeta.chain.node.proxy.{NodeProxyApi as Api} import service.InternalApiService import api.model.creator_dao.CreatorDaoId import api.model.account.EthAddress import api.model.* import api.model.token.* final case class NodeProxyApp[F[_]: Async]( apiService: InternalApiService[F], ): def getBlockServerEndpoint = Api.getBlockEndpoint.serverLogicSuccess: (blockHash: String) => apiService.getBlock(blockHash) def getAccountServerEndpoint: ServerEndpoint[Fs2Streams[F], F] = Api.getAccountEndpoint.serverLogicSuccess: (a: Account) => apiService.getAccount(a) def getEthServerEndpoint = Api.getEthEndpoint.serverLogicSuccess: (ethAddress: EthAddress) => apiService.getEthAccount(ethAddress) def getGroupServerEndpoint = Api.getGroupEndpoint.serverLogicSuccess: (g: GroupId) => apiService.getGroupInfo(g) def getBlockListServerEndpoint = Api.getBlockListEndpoint.serverLogicSuccess: (fromOption, limitOption) => apiService.getBlockList(fromOption, limitOption) def getStatusServerEndpoint = Api.getStatusEndpoint.serverLogicSuccess: _ => apiService.getStatus def getTokenDefServerEndpoint = Api.getTokenDefinitionEndpoint.serverLogicSuccess: (tokenDefinitionId: TokenDefinitionId) => apiService.getTokenDef(tokenDefinitionId) def getBalanceServerEndpoint = Api.getBalanceEndpoint.serverLogicSuccess: (account, movable) => apiService.getBalance(account, movable) def getNftBalanceServerEndpoint = Api.getNftBalanceEndpoint.serverLogicSuccess: (account, movable) => apiService.getNftBalance(account, movable) def getTokenServerEndpoint = Api.getTokenEndpoint.serverLogicSuccess: (tokenId: TokenId) => apiService.getToken(tokenId) def getTokenHistoryServerEndpoint = Api.getTokenHistoryEndpoint.serverLogicSuccess: (txHash: String) => apiService.getTokenHistory(txHash) def getOwnersServerEndpoint = Api.getOwnersEndpoint.serverLogicSuccess: (tokenDefinitionId: TokenDefinitionId) => apiService.getOwners(tokenDefinitionId) def getAccountActivityServerEndpoint = Api.getAccountActivityEndpoint.serverLogicSuccess: (account: Account) => apiService.getAccountActivity(account) def getTokenActivityServerEndpoint = Api.getTokenActivityEndpoint.serverLogicSuccess: (tokenId: TokenId) => apiService.getTokenActivity(tokenId) def getAccountSnapshotServerEndpoint = Api.getAccountSnapshotEndpoint.serverLogicSuccess: (account: Account) => apiService.getAccountSnapshot(account) def getTokenSnapshotServerEndpoint = Api.getTokenSnapshotEndpoint.serverLogicSuccess: (tokenId: TokenId) => apiService.getTokenSnapshot(tokenId) def getOwnershipSnapshotServerEndpoint = Api.getOwnershipSnapshotEndpoint.serverLogicSuccess: (tokenId: TokenId) => apiService.getOwnershipSnapshot(tokenId) def getOwnershipSnapshotMapServerEndpoint = Api.getOwnershipSnapshotMapEndpoint.serverLogicSuccess: (from: Option[TokenId], limit: Option[Int]) => apiService.getOwnershipSnapshotMap(from, limit) def getOwnershipRewardedServerEndpoint = Api.getOwnershipRewardedEndpoint.serverLogicSuccess: (tokenId: TokenId) => apiService.getOwnershipRewarded(tokenId) def getDaoInfoServerEndpoint = Api.getDaoInfoEndpoint.serverLogicSuccess: (groupId: GroupId) => apiService.getDaoInfo(groupId) def getTxServerEndpoint = Api.getTxEndpoint.serverLogicSuccess: (txHash: String) => apiService.getTx(txHash) def getTxSetServerEndpoint = Api.getTxSetEndpoint.serverLogicSuccess: (block: String) => apiService.getTxSet(block) def postTxHashServerEndpoint = Api.postTxHashEndpoint.serverLogicSuccess: (txs: String) => scribe.info(s"received postTxHash request: $txs") apiService.postTxHash(txs) def postTxServerEndpoint = Api.postTxEndpoint.serverLogicSuccess: (txs: String) => scribe.info(s"received postTx request: $txs") apiService.postTx(txs) def getSnapshotStateServerEndpoint = Api.getSnapshotStateEndpoint.serverLogicSuccess: (tokenDefinitionId: TokenDefinitionId) => apiService.getSnapshotState(tokenDefinitionId) def getFungibleSnapshotBalanceServerEndpoint = Api.getFungibleSnapshotBalanceEndpoint.serverLogicSuccess: ( account: Account, tokenDefinitionId: TokenDefinitionId, snapshotId: String, ) => apiService .getFungibleSnapshotBalance(account, tokenDefinitionId, snapshotId) def getNftSnapshotBalanceServerEndpoint = Api.getNftSnapshotBalanceEndpoint.serverLogicSuccess: ( account: Account, tokenDefinitionId: TokenDefinitionId, snapshotId: String, ) => apiService .getNftSnapshotBalance(account, tokenDefinitionId, snapshotId) def getVoteProposalServerEndpoint = Api.getVoteProposalEndpoint.serverLogicSuccess: (proposalId: String) => apiService.getVoteProposal(proposalId) def getAccountVotesServerEndpoint = Api.getAccountVotesEndpoint.serverLogicSuccess: (proposalId: String, account: Account) => apiService.getAccountVotes(proposalId, account) def getVoteCountServerEndpoint = Api.getVoteCountEndpoint.serverLogicSuccess: (proposalId: String) => apiService.getVoteCount(proposalId) def getCreatorDaoInfoServerEndpoint = Api.getCreatorDaoInfoEndpoint.serverLogicSuccess: (creatorDaoId: CreatorDaoId) => apiService.getCreatorDaoInfo(creatorDaoId) def getCreatorDaoMemberServerEndpoint = Api.getCreatorDaoMemberEndpoint.serverLogicSuccess: (creatorDaoId: CreatorDaoId, from: Option[Account], limit: Option[Int]) => apiService.getCreatorDaoMember(creatorDaoId, from, limit) def proxyNodeEndpoints = List( getAccountServerEndpoint, getEthServerEndpoint, getBlockListServerEndpoint, getBlockServerEndpoint, getGroupServerEndpoint, getStatusServerEndpoint, getTxServerEndpoint, getTokenDefServerEndpoint, getBalanceServerEndpoint, getNftBalanceServerEndpoint, getTokenServerEndpoint, getTokenHistoryServerEndpoint, getOwnersServerEndpoint, getTxSetServerEndpoint, getAccountActivityServerEndpoint, getTokenActivityServerEndpoint, getAccountSnapshotServerEndpoint, getTokenSnapshotServerEndpoint, getOwnershipSnapshotServerEndpoint, getOwnershipSnapshotMapServerEndpoint, // getRewardServerEndpoint, getOwnershipRewardedServerEndpoint, getDaoInfoServerEndpoint, postTxServerEndpoint, postTxHashServerEndpoint, getSnapshotStateServerEndpoint, getFungibleSnapshotBalanceServerEndpoint, getNftSnapshotBalanceServerEndpoint, getVoteProposalServerEndpoint, getAccountVotesServerEndpoint, getVoteCountServerEndpoint, getCreatorDaoInfoServerEndpoint, getCreatorDaoMemberServerEndpoint, ) def getServer[IO]( dispatcher: Dispatcher[F], ): F[Server] = Async[F].fromCompletableFuture: def log[F[_]: Async]( level: scribe.Level, )(msg: String, exOpt: Option[Throwable])(using mdc: scribe.mdc.MDC, ): F[Unit] = Async[F].delay: exOpt match case None => scribe.log(level, mdc, msg) case Some(ex) => scribe.log(level, mdc, msg, ex) val serverLog = DefaultServerLog( doLogWhenReceived = log(scribe.Level.Info)(_, None), doLogWhenHandled = log(scribe.Level.Info), doLogAllDecodeFailures = log(scribe.Level.Info), doLogExceptions = (msg: String, ex: Throwable) => Async[F].delay(scribe.warn(msg, ex)), noLog = Async[F].pure(()), ) val serverOptions = ArmeriaCatsServerOptions .customiseInterceptors[F](dispatcher) .serverLog(serverLog) .options val tapirService = ArmeriaCatsServerInterpreter[F](serverOptions) .toService(proxyNodeEndpoints) val server = Server.builder .maxRequestLength(128 * 1024 * 1024) .requestTimeout(java.time.Duration.ofMinutes(10)) .http(8080) .service(tapirService) .build Async[F].delay: server.start().thenApply(_ => server) def resource: F[Resource[F, Server]] = Async[F].delay: for dispatcher <- Dispatcher.parallel[F] server <- Resource.fromAutoCloseable(getServer(dispatcher)) yield server ================================================ FILE: modules/node-proxy/src/main/scala/io/leisuremeta/chain/node/proxy/NodeProxyMain.scala ================================================ package io.leisuremeta.chain.node package proxy import cats.effect.{ExitCode, IO, IOApp} import cats.effect.Ref import sttp.client3.armeria.cats.ArmeriaCatsBackend import sttp.client3.* import cats.syntax.* import cats.syntax.all._ import com.linecorp.armeria.client.ClientFactory import com.linecorp.armeria.client.WebClient import com.linecorp.armeria.client.encoding.DecodingClient import cats.effect.kernel.Async import cats.syntax.flatMap.toFlatMapOps import model.NodeConfig import service.* object NodeProxyMain extends IOApp: def newClientFactory(options: SttpBackendOptions): ClientFactory = val builder = ClientFactory .builder() .connectTimeoutMillis(options.connectionTimeout.toMillis) options.proxy.fold(builder.build()) { proxy => builder .proxyConfig(proxy.asJavaProxySelector) .build() } def webClient(options: SttpBackendOptions): WebClient = WebClient .builder() .decorator( DecodingClient .builder() .autoFillAcceptEncoding(false) .strictContentEncoding(false) .newDecorator() ) .factory(newClientFactory(options)) .build() def run[F[_]: Async]: F[ExitCode] = ArmeriaCatsBackend .resourceUsingClient[F](webClient(SttpBackendOptions.Default)) .use { backend => for blocker <- Ref.of[F, Boolean](true) queue <- PostTxQueue[F] nodeConfg <- NodeWatchService.nodeConfig.flatMap ( _.fold(Async[F].raiseError[NodeConfig], Async[F].pure)) blockchainUrls <- Ref.of[F, List[String]](List(nodeConfg.oldNodeAddress)) internalApiSvc = InternalApiService[F](backend, blocker, blockchainUrls, queue) _ <- NodeWatchService.startOnNew(internalApiSvc, blockchainUrls, blocker, queue) appResource <- NodeProxyApp[F](internalApiSvc).resource exitcode <- appResource.useForever.as(ExitCode.Success) yield exitcode } override def run(args: List[String]): IO[ExitCode] = { run[IO] } ================================================ FILE: modules/node-proxy/src/main/scala/io/leisuremeta/chain/node/proxy/model/NodeConfig.scala ================================================ package io.leisuremeta.chain.node.proxy package model import io.leisuremeta.chain.lib.datatype.BigNat final case class NodeConfig( blockNumber: Option[BigNat], oldNodeAddress: String, newNodeAddress: Option[String], ) ================================================ FILE: modules/node-proxy/src/main/scala/io/leisuremeta/chain/node/proxy/model/TxModel.scala ================================================ package io.leisuremeta.chain.node.proxy.model import io.circe._ case class TxModel ( signedTx: String, result: Option[String] ) object TxModel { implicit val txModelDecoder: Decoder[TxModel] = new Decoder[TxModel] { final def apply(c: HCursor): Decoder.Result[TxModel] = { for { signedTxJson <- c.downField("signedTx").as[Json] signedTx = signedTxJson.noSpaces result <- c.downField("result").as[Option[String]].orElse(Right(None)) } yield { TxModel(signedTx, result) } } } } ================================================ FILE: modules/node-proxy/src/main/scala/io/leisuremeta/chain/node/proxy/service/InternalApiService.scala ================================================ package io.leisuremeta.chain package node.proxy package service import io.circe.parser.decode import sttp.client3.* import sttp.model.{Uri, StatusCode} import cats.implicits.* import cats.effect.* import cats.effect.Async import cats.effect.Ref import cats.effect.kernel.Async import scala.concurrent.duration.* import sttp.model.MediaType import io.leisuremeta.chain.node.proxy.model.TxModel import io.circe.generic.auto.* import api.model.* import api.model.account.EthAddress import api.model.creator_dao.CreatorDaoId import api.model.token.* import api.model.Block.* object InternalApiService: def apply[F[_]: Async]( backend: SttpBackend[F, Any], blocker: Ref[F, Boolean], baseUrlsLock: Ref[F, List[String]], queue: PostTxQueue[F], ): InternalApiService[F] = // Async[F].delay(new InternalApiService[F](backend, blocker, baseUrlsLock)) new InternalApiService[F](backend, blocker, baseUrlsLock, queue) class InternalApiService[F[_]: Async]( backend: SttpBackend[F, Any], blocker: Ref[F, Boolean], baseUrlsLock: Ref[F, List[String]], queue: PostTxQueue[F], ): // val baseUrl = "http://lmc.leisuremeta.io" // val baseUrl = "http://test.chain.leisuremeta.io" // def backend[F[_]: Async]: SttpBackend[F, Any] = // ArmeriaCatsBackend.usingClient[F](webClient(SttpBackendOptions.Default)) // def injectQueue(postQueue: PostTxQueue[F]) = // queue = Some(postQueue) def getAsString( uri: Uri, ): F[(StatusCode, String)] = for _ <- jobOrBlock result <- basicRequest .response(asStringAlways) .get(uri) .send(backend) .map(res => (res.code, res.body)) yield // scribe.info(s"getAsString result: $result") result def getAsResponse( uri: Uri, ): F[(StatusCode, Response[String])] = for _ <- jobOrBlock result <- basicRequest .response(asStringAlways) .get(uri) .send(backend) yield (result.code, result) def getTxAsResponse[A: io.circe.Decoder]( uri: Uri, ): F[(StatusCode, Either[String, A])] = for _ <- jobOrBlock result <- basicRequest .get(uri) .send(backend) .map { response => ( response.code, for body <- response.body a <- decode[A](body).leftMap(_.getMessage()) yield a, ) } yield // scribe.info(s"getAsString result: $result") result def postAsString( uri: Uri, body: String, ): F[(StatusCode, String)] = for _ <- jobOrBlock result <- basicRequest .response(asStringAlways) .post(uri) .body(body) .send(backend) .map(res => (res.code, res.body)) yield result def postAsResponse( uri: Uri, body: String, ): F[(StatusCode, Response[String])] = for _ <- jobOrBlock result <- basicRequest .response(asStringAlways) .post(uri) .contentType(MediaType.ApplicationJson) .body(body) .send(backend) yield (result.code, result) def getBlock( blockHash: String, ): F[(StatusCode, String)] = baseUrlsLock.get .flatMap { urls => urls.traverse { baseUrl => getAsString(uri"$baseUrl/block/$blockHash") } } .map(_.head) def getAccount( account: Account, ): F[(StatusCode, String)] = baseUrlsLock.get .flatMap { urls => urls.traverse { baseUrl => getAsString(uri"$baseUrl/account/${account.utf8.value}") } } .map(_.head) def getEthAccount( ethAddress: EthAddress, ): F[(StatusCode, String)] = baseUrlsLock.get .flatMap { urls => urls.traverse { baseUrl => getAsString(uri"$baseUrl/eth/$ethAddress") } } .map(_.head) def getGroupInfo( groupId: GroupId, ): F[(StatusCode, String)] = baseUrlsLock.get .flatMap { urls => urls.traverse { baseUrl => getAsString(uri"$baseUrl/group/$groupId") } } .map(_.head) def getBlockList( fromOption: Option[String], limitOption: Option[Int], ): F[(StatusCode, String)] = baseUrlsLock.get .flatMap { urls => urls.traverse { baseUrl => getAsString(uri"$baseUrl/block?from=${fromOption .getOrElse("")}&limit=${limitOption.getOrElse("")}") } } .map(_.head) def getStatus: F[(StatusCode, String)] = baseUrlsLock.get .flatMap { urls => urls.traverse { baseUrl => getAsString(uri"$baseUrl/status") } } .map(_.head) def getTokenDef( tokenDefinitionId: TokenDefinitionId, ): F[(StatusCode, String)] = baseUrlsLock.get .flatMap { urls => urls.traverse { baseUrl => getAsString(uri"$baseUrl/token-def/$tokenDefinitionId") } } .map(_.head) def getBalance( account: Account, movable: String, ): F[(StatusCode, String)] = baseUrlsLock.get .flatMap { urls => urls.traverse { baseUrl => getAsString(uri"$baseUrl/balance/$account?movable=${movable}") } } .map(_.head) def getNftBalance( account: Account, movable: Option[String], ): F[(StatusCode, String)] = baseUrlsLock.get .flatMap { urls => urls.traverse { baseUrl => getAsString(uri"$baseUrl/nft-balance/$account?movable=${movable}") } } .map(_.head) def getToken( tokenId: TokenId, ): F[(StatusCode, String)] = baseUrlsLock.get .flatMap { urls => urls.traverse { baseUrl => getAsString(uri"$baseUrl/token/$tokenId") } } .map(_.head) def getTokenHistory( txHash: String, ): F[(StatusCode, String)] = baseUrlsLock.get .flatMap { urls => urls.traverse { baseUrl => getAsString(uri"$baseUrl/token-hist/$txHash") } } .map(_.head) def getOwners( tokenDefinitionId: TokenDefinitionId, ): F[(StatusCode, String)] = baseUrlsLock.get .flatMap { urls => urls.traverse { baseUrl => getAsString(uri"$baseUrl/owners/$tokenDefinitionId") } } .map(_.head) def getAccountActivity( account: Account, ): F[(StatusCode, String)] = baseUrlsLock.get .flatMap { urls => urls.traverse { baseUrl => getAsString(uri"$baseUrl/activity/account/$account") } } .map(_.head) def getTokenActivity( tokenId: TokenId, ): F[(StatusCode, String)] = baseUrlsLock.get .flatMap { urls => urls.traverse { baseUrl => getAsString(uri"$baseUrl/activity/token/$tokenId") } } .map(_.head) def getAccountSnapshot( account: Account, ): F[(StatusCode, String)] = baseUrlsLock.get .flatMap { urls => urls.traverse { baseUrl => getAsString(uri"$baseUrl/snapshot/account/$account") } } .map(_.head) def getTokenSnapshot( tokenId: TokenId, ): F[(StatusCode, String)] = baseUrlsLock.get .flatMap { urls => urls.traverse { baseUrl => getAsString(uri"$baseUrl/snapshot/token/$tokenId") } } .map(_.head) def getStatusEndpoint: F[(StatusCode, String)] = baseUrlsLock.get .flatMap { urls => urls.traverse { baseUrl => getAsString(uri"$baseUrl/status") } } .map(_.head) def getOwnershipSnapshot( tokenId: TokenId, ): F[(StatusCode, String)] = baseUrlsLock.get .flatMap { urls => urls.traverse { baseUrl => getAsString(uri"$baseUrl/snapshot/ownership/$tokenId") } } .map(_.head) def getOwnershipSnapshotMap( tokenId: Option[TokenId], limit: Option[Int], ): F[(StatusCode, String)] = baseUrlsLock.get .flatMap { urls => urls.traverse { baseUrl => getAsString( uri"$baseUrl/snapshot/ownership?from=$tokenId&limit=$limit", ) } } .map(_.head) def getOwnershipRewarded( tokenId: TokenId, ): F[(StatusCode, String)] = baseUrlsLock.get .flatMap { urls => urls.traverse { baseUrl => getAsString(uri"$baseUrl/rewarded/ownership/$tokenId") } } .map(_.head) def getReward( tokenId: TokenId, ): F[(StatusCode, String)] = baseUrlsLock.get .flatMap { urls => urls.traverse { baseUrl => getAsString(uri"$baseUrl/rewarded/ownership/$tokenId") } } .map(_.head) def getDaoInfo(groupId: GroupId): F[(StatusCode, String)] = baseUrlsLock.get .flatMap: urls => urls.traverse: baseUrl => getAsString(uri"$baseUrl/dao/$groupId") .map(_.head) def getTx( txHash: String, ): F[(StatusCode, String)] = baseUrlsLock.get .flatMap { urls => urls.traverse { baseUrl => getAsString(uri"$baseUrl/tx/$txHash") } } .map(_.head) def getTxFromOld( txHash: String, ): F[(StatusCode, Either[String, TxModel])] = baseUrlsLock.get.map(_.head).flatMap { baseUrl => getTxAsResponse[TxModel](uri"$baseUrl/tx/$txHash") } def getTxSet( txHash: String, ): F[(StatusCode, String)] = baseUrlsLock.get .flatMap { urls => urls.traverse { baseUrl => getAsString(uri"$baseUrl/tx/$txHash") } } .map(_.head) def postTx( txs: String, ): F[(StatusCode, String)] = queue.push(txs) baseUrlsLock.get .flatMap { urls => urls.traverse { baseUrl => postAsString( uri"$baseUrl/tx", txs, ) } } .map(_.head) def postTx( baseUrl: String, txs: String, ): F[(StatusCode, Response[String])] = postAsResponse( uri"$baseUrl/tx", txs, ) def postTxHash( txs: String, ): F[(StatusCode, String)] = baseUrlsLock.get .flatMap { urls => urls.traverse { baseUrl => postAsString( uri"$baseUrl/txhash", txs, ) } } .map(_.head) def jobOrBlock: F[Unit] = blocker.get.flatMap { value => value match case false => Async[F].sleep(3.second) >> jobOrBlock case true => Async[F].unit } def bestBlock(baseUri: String): F[(StatusCode, Block)] = for res <- get[NodeStatus](uri"$baseUri/status") (_, nodeStatus) = res bestBlock <- block(baseUri, nodeStatus.bestHash.toUInt256Bytes.toHex) yield bestBlock def block(baseUri: String, blockHash: String): F[(StatusCode, Block)] = get[Block](uri"$baseUri/block/$blockHash") def postTxs(baseUri: String, body: String): F[(StatusCode, String)] = postAsString(uri"$baseUri/tx", body) def getAsOption[A: io.circe.Decoder](uri: Uri): F[Option[A]] = basicRequest .get(uri) .send(backend) .map { response => response.body.toOption.flatMap { body => decode[A](body).toOption } } def get[A: io.circe.Decoder](uri: Uri): F[(StatusCode, A)] = basicRequest .get(uri) .send(backend) .flatMap { response => response.body match case Right(body) => decode[A](body) match case Right(a) => Async[F].pure((response.code, a)) case Left(error) => Async[F].raiseError(error) case Left(error) => Async[F].raiseError( new Exception(s"Error in response body: $error"), ) } def getSnapshotState( tokenDefinitionId: TokenDefinitionId, ): F[(StatusCode, String)] = baseUrlsLock.get.flatMap { urls => urls.headOption match case Some(baseUrl) => val uri = uri"$baseUrl/snapshot-state/$tokenDefinitionId" getAsString(uri) case None => Async[F].pure( (StatusCode.ServiceUnavailable, "No base URL available"), ) } def getFungibleSnapshotBalance( account: Account, tokenDefinitionId: TokenDefinitionId, snapshotId: String, ): F[(StatusCode, String)] = baseUrlsLock.get.flatMap { urls => urls.headOption match case Some(baseUrl) => val uri = uri"$baseUrl/snapshot-balance/$account/$tokenDefinitionId/$snapshotId" getAsString(uri) case None => Async[F].pure( (StatusCode.ServiceUnavailable, "No base URL available"), ) } def getNftSnapshotBalance( account: Account, tokenDefinitionId: TokenDefinitionId, snapshotId: String, ): F[(StatusCode, String)] = baseUrlsLock.get.flatMap { urls => urls.headOption match case Some(baseUrl) => val uri = uri"$baseUrl/nft-snapshot-balance/$account/$tokenDefinitionId/$snapshotId" getAsString(uri) case None => Async[F].pure( (StatusCode.ServiceUnavailable, "No base URL available"), ) } def getVoteProposal( proposalId: String, ): F[(StatusCode, String)] = baseUrlsLock.get.flatMap { urls => urls.headOption match case Some(baseUrl) => val uri = uri"$baseUrl/vote/proposal/$proposalId" getAsString(uri) case None => Async[F].pure( (StatusCode.ServiceUnavailable, "No base URL available"), ) } def getAccountVotes( proposalId: String, account: Account, ): F[(StatusCode, String)] = baseUrlsLock.get.flatMap { urls => urls.headOption match case Some(baseUrl) => val uri = uri"$baseUrl/vote/account/$proposalId/$account" getAsString(uri) case None => Async[F].pure( (StatusCode.ServiceUnavailable, "No base URL available"), ) } def getVoteCount( proposalId: String, ): F[(StatusCode, String)] = baseUrlsLock.get.flatMap { urls => urls.headOption match case Some(baseUrl) => val uri = uri"$baseUrl/vote/count/$proposalId" getAsString(uri) case None => Async[F].pure( (StatusCode.ServiceUnavailable, "No base URL available"), ) } def getCreatorDaoInfo( creatorDaoId: CreatorDaoId, ): F[(StatusCode, String)] = baseUrlsLock.get.flatMap { urls => urls.headOption match case Some(baseUrl) => val uri = uri"$baseUrl/creator-dao/$creatorDaoId" getAsString(uri) case None => Async[F].pure( (StatusCode.ServiceUnavailable, "No base URL available"), ) } def getCreatorDaoMember( creatorDaoId: CreatorDaoId, from: Option[Account], limit: Option[Int], ): F[(StatusCode, String)] = baseUrlsLock.get.flatMap { urls => urls.headOption match case Some(baseUrl) => val uri = uri"$baseUrl/creator-dao/$creatorDaoId/member" .addParam("from", from.map(_.utf8.value).getOrElse("")) .addParam("limit", limit.map(_.toString).getOrElse("")) getAsString(uri) case None => Async[F].pure( (StatusCode.ServiceUnavailable, "No base URL available"), ) } ================================================ FILE: modules/node-proxy/src/main/scala/io/leisuremeta/chain/node/proxy/service/NodeBalancer.scala ================================================ package io.leisuremeta.chain.node.proxy package service import java.nio.file.{Files, Paths, StandardOpenOption} import cats.implicits.* import io.leisuremeta.chain.api.model.Block import cats.effect.Ref import cats.effect.kernel.Async import sttp.model.StatusCode import model.NodeConfig import fs2.io.file.Path import fs2.text.utf8 import scala.concurrent.duration._ import io.leisuremeta.chain.lib.datatype.BigNat import io.leisuremeta.chain.node.proxy.model.TxModel import fs2.Chunk import fs2.{Stream, text} import fs2.Pipe case class NodeBalancer[F[_]: Async] ( apiService: InternalApiService[F], blocker: Ref[F, Boolean], baseUrlsLock: Ref[F, List[String]], nodeConfig: NodeConfig, queue: PostTxQueue[F] ): // startBlock: bestBlock in old blockchain def logDiffTxsLoop(startBlock: Block, endBlockNumber: BigNat): F[Unit] = def getTxWithExponentialRetry(txHash: String, retries: Int, delay: FiniteDuration): F[Option[String]] = apiService.getTxFromOld(txHash).flatMap { (_, res) => res match case Right(txModel) => Async[F].pure(Some(txModel.signedTx)) case Left(err) if retries > 0 => Async[F].sleep(delay) *> getTxWithExponentialRetry(txHash, retries - 1, delay * 2) case _ => Async[F].pure(None) } def loop(currBlock: Block): F[Unit] = if (currBlock.header.number != endBlockNumber) { println(s"block download number: ${currBlock.header.number}") val parentHash = currBlock.header.parentHash.toUInt256Bytes.toBytes.toHex for txList <- currBlock.transactionHashes.toList.traverse { txHash => getTxWithExponentialRetry(txHash.toUInt256Bytes.toBytes.toHex, 5, 1.second) } filteredTxList = txList.flatMap(identity) _ <- filteredTxList match case head :: tail => appendLog("diff-txs.json", s"[${filteredTxList.mkString(",")}]") case Nil => Async[F].unit res <- apiService.block(nodeConfig.oldNodeAddress, parentHash) (_, prevBlock) = res _ <- loop(prevBlock) yield () } else { scribe.info("logDiffTxsLoop 종료") Async[F].unit } loop(startBlock) def appendLog(path: String, json: String): F[Unit] = Async[F].blocking: val _ = java.nio.file.Files.write( Paths.get(path), (json + "\n").getBytes, StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.APPEND, ) def postTxWithExponentialRetry(line: String, retries: Int, delay: FiniteDuration): F[String] = println(s"request txs: $line") apiService.postTx(nodeConfig.newNodeAddress.get, line).flatMap { (_, res) => //(statusCode, res) => println(s"generated hash: $res") val code = res.code if code.isSuccess then Async[F].pure(res.body) else if (code.isServerError || res.body.isEmpty) && retries > 0 then Async[F].sleep(delay) *> postTxWithExponentialRetry(line, retries - 1, delay * 2) else Async[F].pure("") } def createTxsToNewBlockchain: F[Option[String]] = val path = fs2.io.file.Path("diff-txs.json") def processLinesReversed(): F[Option[String]] = def reverseLines(): F[Option[String]] = def reversePipe: Pipe[F, String, String] = _.flatMap { s => Stream.chunk(Chunk.from(s.split('\n').toVector.reverse)) }.fold(Vector.empty[String]) { (acc, line) => line +: acc }.flatMap(Stream.emits) fs2.io.file.Files.forAsync[F] .readAll(path) .through(fs2.text.utf8.decode) .through(text.lines) .through(reversePipe) .scan((Option.empty[String], Option.empty[String])) { case ((_, prev), txs) => if (txs.isBlank) (None, prev) else (Some(txs), Some(txs)) } .evalMap { case (txsOpt, lastTxs) => txsOpt match case None => Async[F].pure(lastTxs) case Some(txs) => postTxWithExponentialRetry(txs, 5, 1.second).as(lastTxs) } .last .compile .lastOrError .map(_.flatten) .flatMap(Async[F].pure) reverseLines() processLinesReversed() // def createTxsToNewBlockchain1[F[_]: Async](implicit C: Concurrent[F], console: Console[F]): F[Unit] = // val path = Paths.get("diff-txs.json") // def reverseFile(): F[Unit] = // Stream.eval(fs2.io.file.Files[F].size(path)).flatMap { size => // Stream.unfoldLoopEval(size - 1L) { offset => // Async[F].uncancelable { _ => // fs2.io.file.Files[F].readRange(path, 1, offset, offset + 1) // .through(fs2.text.utf8.decode) // .compile // .string // .map { char => // (char, if (char.nonEmpty) Some(offset - 1L) else None) // } // } // } // } // .through(fs2.text.utf8.encode) // .through(fs2.text.utf8.decode) // .through(text.lines) // .evalMap { txs => // if (txs.isBlank) Async[F].unit // else postTxWithExponentialRetry(txs, 5, 1.second).as(()) // } // .last // .compile // .lastOrError // reverseFile() def deleteAllFiles: F[Unit] = Async[F].blocking: val diffTxs = Paths.get("diff-txs.json") val _ = Files.deleteIfExists(diffTxs) def run(): F[Unit] = def loop(endBlockNumber: BigNat, lastTxsOpt: Option[String]): F[Unit] = val newNodeAddr = nodeConfig.newNodeAddress.get val oldNodeAddr = nodeConfig.oldNodeAddress scribe.info(s"[oldNode: $oldNodeAddr]", s"-> [newNode: $newNodeAddr]") for response <- apiService.bestBlock(oldNodeAddr) (_, startBlock) = response _ <- if (startBlock.header.number == endBlockNumber) { for _ <- blocker.set(false) _ <- queue.pollsAfter(lastTxsOpt).flatMap { jsons => jsons.traverse { txs => postTxWithExponentialRetry(txs, 5, 1.second)} } _ <- baseUrlsLock.getAndUpdate{_.appended(nodeConfig.newNodeAddress.get)} _ <- blocker.set(true) // 양쪽 모두 릴레이 시작. _ <- Async[F].delay(scribe.info("마이그레이션 성공. 양쪽 모두 API 릴레이 시작")) newNodeCfg <- NodeWatchService.waitTerminateSig _ <- Async[F].delay(scribe.info(s"now api request only relayed to ${newNodeCfg.oldNodeAddress}")) _ <- baseUrlsLock.set(List(newNodeCfg.oldNodeAddress)) yield () } else { logDiffTxsLoop(startBlock, endBlockNumber) >> createTxsToNewBlockchain.flatMap { lastTxs => val validLastTxs = lastTxs match case Some(lastTxs) => Some(lastTxs) case None => lastTxsOpt deleteAllFiles >> loop(startBlock.header.number, validLastTxs) } } yield () loop(nodeConfig.blockNumber.getOrElse( throw new NoSuchElementException("local migration 이 완료된 blockNumber를 적어주세요.")), None) ================================================ FILE: modules/node-proxy/src/main/scala/io/leisuremeta/chain/node/proxy/service/NodeWatchService.scala ================================================ package io.leisuremeta.chain.node.proxy package service import java.nio.file.{Files, Paths} import scala.concurrent.duration._ import scala.jdk.CollectionConverters._ import scala.util.Try import cats.effect.{Async, Ref} import cats.syntax.all._ import io.circe.generic.auto._ import io.circe.parser.decode import model.NodeConfig object NodeWatchService: def nodeConfig[F[_]: Async]: F[Either[Throwable, NodeConfig]] = Async[F].blocking { // val path = Paths.get("/Users/jichangho/playnomm/leisuremeta-chain/migration-node.json") // val path = Paths.get("/Users/user/playnomm/source_code/leisuremeta-chain/migration-node.json") val path = Paths.get("/home/rocky/nodeproxy/migration-node.json") for json <- Try(Files.readAllLines(path).asScala.mkString("\n")).toEither nodeConfig <- decode[NodeConfig](json) yield nodeConfig } def newNodeWatchLoop[F[_]: Async]( apiService: InternalApiService[F], blcUrls: Ref[F, List[String]], blocker: Ref[F, Boolean], queue: PostTxQueue[F] ): F[Unit] = def loop: F[Option[Unit]] = nodeConfig.flatMap { nodeConfigEither => scribe.info(s"newNodeWatchLoop") nodeConfigEither match case Right(nodeConfig) => (nodeConfig.oldNodeAddress, nodeConfig.newNodeAddress) match case (oldNodeAddress, Some(newNodeAddress)) if !newNodeAddress.trim.isEmpty => scribe.info(s"nodeConfig: $nodeConfig") blcUrls.set(List(nodeConfig.oldNodeAddress)) *> NodeBalancer(apiService, blocker, blcUrls, nodeConfig, queue).run() *> Async[F].pure(Some(())) case (oldNodeAddress, _) => // newNodeAddress is None | isEmpty for urls <- blcUrls.get _ <- if urls.isEmpty then blcUrls.set(List(oldNodeAddress)) else if !urls.head.equals(oldNodeAddress) then blcUrls.set(List(oldNodeAddress)) else Async[F].unit yield Some(()) case Left(error) => scribe.error(s"Error decoding Node Config: $error\nreconfigure migration-node.json file.") Async[F].pure(Some(())) } loop.flatMap { case Some(_) => Async[F].sleep(5.seconds) >> newNodeWatchLoop(apiService, blcUrls, blocker, queue) case None => Async[F].unit } def startOnNew[F[_]: Async]( apiService: InternalApiService[F], blcUrls: Ref[F, List[String]], blocker: Ref[F, Boolean], queue: PostTxQueue[F] ): F[Unit] = Async[F].executionContext.flatMap { executionContext => Async[F].startOn( newNodeWatchLoop(apiService, blcUrls, blocker, queue), executionContext ) *> Async[F].unit } def waitTerminateSig[F[_]: Async]: F[NodeConfig] = def loop: F[NodeConfig] = nodeConfig.flatMap { nodeCfgEither => nodeCfgEither match case Right(nodeConfig) => nodeConfig.newNodeAddress match case Some(address) if address.isBlank() => Async[F].delay(scribe.info("마이그레이션 종료.")) >> Async[F].pure(nodeConfig) case _ => Async[F].delay(scribe.info("종료 시그널 기다리는 중..")) >> Async[F].sleep(10.second) >> loop case Left(error) => scribe.error(s"Error decoding Node Config: $error\nreconfigure migration-node.json file.") Async[F].sleep(5.second) loop } loop ================================================ FILE: modules/node-proxy/src/main/scala/io/leisuremeta/chain/node/proxy/service/PostTxQueue.scala ================================================ package io.leisuremeta.chain.node.proxy.service import cats.effect.kernel.Async import cats.syntax.all.* import cats.effect.std.Queue object PostTxQueue: def apply[F[_]: Async]: F[PostTxQueue[F]] = Queue.circularBuffer[F, String](5).map { queue => new PostTxQueue[F](queue) } class PostTxQueue[F[_]: Async]( queue: Queue[F, String] ): def push(txJson: String): F[Unit] = queue.offer(txJson) def pollsAfter(lastTxOpt: Option[String]): F[List[String]] = lastTxOpt match case None => Async[F].pure(List.empty) case Some(lastTx) => queue.tryTakeN(None).map { _.dropWhile(_ != lastTx) .drop(1) } def peek(): F[Unit] = for { items <- queue.tryTakeN(None) _ <- items.traverse { item => Async[F].delay(println(item)) } _ <- items.traverse(queue.offer(_)) } yield () ================================================ FILE: project/Settings.scala.sample ================================================ import sbt._ object Settings { val flywaySettings = new { lazy val url = "jdbc:postgresql://127.0.0.1:5432/postgres" lazy val user = "postgres" lazy val pwd = "1234" lazy val schemas = Seq("public") lazy val locations = Seq("db/test", "db/common") } } ================================================ FILE: project/build.properties ================================================ sbt.version=1.10.0 ================================================ FILE: project/plugins.sbt ================================================ addDependencyTreePlugin addSbtPlugin("org.wartremover" % "sbt-wartremover" % "3.4.3") addSbtPlugin("io.spray" % "sbt-revolver" % "0.10.0") addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.16.0") addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.3.2") addSbtPlugin("ch.epfl.scala" % "sbt-scalajs-bundler" % "0.21.1") addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.2") addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.12.1") addSbtPlugin("io.github.davidgregory084" % "sbt-tpolecat" % "0.4.4") addSbtPlugin("org.scalablytyped.converter" % "sbt-converter" % "1.0.0-beta44") addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "2.2.0") addSbtPlugin("io.github.davidmweber" % "flyway-sbt" % "7.4.0")