diff --git a/build.sbt b/build.sbt index 357aa9cf0..018a3ccd0 100644 --- a/build.sbt +++ b/build.sbt @@ -2,7 +2,7 @@ lazy val commonSettings = Seq( scalacOptions ++= commonScalacOptions, scalaVersion := "2.12.15", organization := "org.ergoplatform", - version := "9.17.4", + version := "9.17.5", resolvers += Resolver.sonatypeRepo("public"), resolvers += Resolver.sonatypeRepo("snapshots"), libraryDependencies ++= dependencies.Testing ++ dependencies.CompilerPlugins, diff --git a/modules/chain-grabber/src/main/scala/org/ergoplatform/explorer/indexer/Application.scala b/modules/chain-grabber/src/main/scala/org/ergoplatform/explorer/indexer/Application.scala index ebcf67cc9..4dcfef46f 100644 --- a/modules/chain-grabber/src/main/scala/org/ergoplatform/explorer/indexer/Application.scala +++ b/modules/chain-grabber/src/main/scala/org/ergoplatform/explorer/indexer/Application.scala @@ -19,6 +19,7 @@ import org.http4s.client.Client import pureconfig.generic.auto._ import tofu.concurrent.MakeRef import tofu.logging.Logs +import cats.effect.IO import scala.concurrent.ExecutionContext.global diff --git a/modules/chain-grabber/src/main/scala/org/ergoplatform/explorer/indexer/extractors/BlockInfoBuildFrom.scala b/modules/chain-grabber/src/main/scala/org/ergoplatform/explorer/indexer/extractors/BlockInfoBuildFrom.scala index df0432f92..ea36a5a3a 100644 --- a/modules/chain-grabber/src/main/scala/org/ergoplatform/explorer/indexer/extractors/BlockInfoBuildFrom.scala +++ b/modules/chain-grabber/src/main/scala/org/ergoplatform/explorer/indexer/extractors/BlockInfoBuildFrom.scala @@ -8,7 +8,7 @@ import org.ergoplatform.explorer.protocol.constants import org.ergoplatform.explorer.protocol.models.ApiFullBlock import org.ergoplatform.explorer.settings.ProtocolSettings import org.ergoplatform.explorer.{Address, BuildFrom, CRaise} -import org.ergoplatform.{ErgoAddressEncoder, ErgoScriptPredef, Pay2SAddress} +import org.ergoplatform.{ErgoScriptPredef, Pay2SAddress} import scorex.util.encode.Base16 import sigmastate.basics.DLogProtocol.ProveDlog import sigmastate.interpreter.CryptoConstants.EcPointType @@ -106,26 +106,13 @@ final class BlockInfoBuildFrom[ private def minerRewardAndFee( apiBlock: ApiFullBlock )(protocolSettings: ProtocolSettings): (Long, Long) = { - val emission = protocolSettings.emission.emissionAtHeight(apiBlock.header.height.toLong) + val emission = protocolSettings.emission.emissionAt(apiBlock.header.height.toLong) val reward = math.min(constants.TeamTreasuryThreshold, emission) - val eip27Reward = - if (reward >= constants.Eip27UpperPoint) reward - constants.Eip27DefaultReEmission - else if (constants.Eip27LowerPoint < reward) reward - (reward - constants.Eip27ResidualEmission) - else reward val fee = apiBlock.transactions.transactions .flatMap(_.outputs.toList) .filter(_.ergoTree.unwrapped == constants.FeePropositionScriptHex) .map(_.value) .sum - protocolSettings.networkPrefix.value.toByte match { - case ErgoAddressEncoder.MainnetNetworkPrefix - if apiBlock.header.height >= constants.MainnetEip27ActivationHeight => - (eip27Reward, fee) - case ErgoAddressEncoder.TestnetNetworkPrefix - if apiBlock.header.height >= constants.TestnetEip27ActivationHeight => - (eip27Reward, fee) - case _ => - (reward, fee) - } + (reward, fee) } } diff --git a/modules/explorer-api/src/main/scala/org/ergoplatform/explorer/http/api/models/AssetInstanceInfo.scala b/modules/explorer-api/src/main/scala/org/ergoplatform/explorer/http/api/models/AssetInstanceInfo.scala index 8e2e89fee..8e2c0ce5c 100644 --- a/modules/explorer-api/src/main/scala/org/ergoplatform/explorer/http/api/models/AssetInstanceInfo.scala +++ b/modules/explorer-api/src/main/scala/org/ergoplatform/explorer/http/api/models/AssetInstanceInfo.scala @@ -3,9 +3,7 @@ package org.ergoplatform.explorer.http.api.models import io.circe.Codec import io.circe.magnolia.derivation.decoder.semiauto.deriveMagnoliaDecoder import io.circe.magnolia.derivation.encoder.semiauto.deriveMagnoliaEncoder -import io.circe.magnolia.derivation.decoder.semiauto.deriveMagnoliaDecoder -import io.circe.magnolia.derivation.encoder.semiauto.deriveMagnoliaEncoder -import org.ergoplatform.explorer.db.models.aggregates.{ExtendedAsset, ExtendedUAsset} +import org.ergoplatform.explorer.db.models.aggregates.{AnyAsset, ExtendedAsset, ExtendedUAsset} import org.ergoplatform.explorer.{TokenId, TokenType} import sttp.tapir.{Schema, Validator} @@ -26,6 +24,9 @@ object AssetInstanceInfo { def apply(asset: ExtendedAsset): AssetInstanceInfo = AssetInstanceInfo(asset.tokenId, asset.index, asset.amount, asset.name, asset.decimals, asset.`type`) + def apply(asset: AnyAsset): AssetInstanceInfo = + AssetInstanceInfo(asset.tokenId, asset.index, asset.amount, asset.name, asset.decimals, asset.`type`) + implicit val codec: Codec[AssetInstanceInfo] = Codec.from(deriveMagnoliaDecoder, deriveMagnoliaEncoder) implicit val schema: Schema[AssetInstanceInfo] = diff --git a/modules/explorer-api/src/main/scala/org/ergoplatform/explorer/http/api/v1/defs/BoxesEndpointDefs.scala b/modules/explorer-api/src/main/scala/org/ergoplatform/explorer/http/api/v1/defs/BoxesEndpointDefs.scala index 5d405e912..910be24f9 100644 --- a/modules/explorer-api/src/main/scala/org/ergoplatform/explorer/http/api/v1/defs/BoxesEndpointDefs.scala +++ b/modules/explorer-api/src/main/scala/org/ergoplatform/explorer/http/api/v1/defs/BoxesEndpointDefs.scala @@ -5,7 +5,13 @@ import org.ergoplatform.explorer.http.api.ApiErr import org.ergoplatform.explorer.http.api.commonDirectives._ import org.ergoplatform.explorer.http.api.models.Sorting.SortOrder import org.ergoplatform.explorer.http.api.models.{HeightRange, Items, Paging} -import org.ergoplatform.explorer.http.api.v1.models.{BoxAssetsQuery, BoxQuery, MOutputInfo, OutputInfo} +import org.ergoplatform.explorer.http.api.v1.models.{ + AnyOutputInfo, + BoxAssetsQuery, + BoxQuery, + MOutputInfo, + OutputInfo +} import org.ergoplatform.explorer.settings.RequestsSettings import sttp.capabilities.fs2.Fs2Streams import sttp.tapir._ @@ -170,4 +176,11 @@ final class BoxesEndpointDefs[F[_]](settings: RequestsSettings) { "Search among UTXO set by ergoTreeTemplateHash and tokens. " + "The resulted UTXOs will contain at lest one of the given tokens." ) + + def getAllUnspentOutputsByAddressDef: Endpoint[(Address, Paging, SortOrder), ApiErr, Items[AnyOutputInfo], Any] = + baseEndpointDef.get + .in(PathPrefix / "unspent" / "all" / "byAddress" / path[Address]) + .in(paging(settings.maxEntitiesPerRequest)) + .in(ordering) + .out(jsonBody[Items[AnyOutputInfo]]) } diff --git a/modules/explorer-api/src/main/scala/org/ergoplatform/explorer/http/api/v1/models/AnyOutputInfo.scala b/modules/explorer-api/src/main/scala/org/ergoplatform/explorer/http/api/v1/models/AnyOutputInfo.scala new file mode 100644 index 000000000..f79869d77 --- /dev/null +++ b/modules/explorer-api/src/main/scala/org/ergoplatform/explorer/http/api/v1/models/AnyOutputInfo.scala @@ -0,0 +1,87 @@ +package org.ergoplatform.explorer.http.api.v1.models + +import derevo.circe.{decoder, encoder} +import derevo.derive +import io.circe.Json +import org.ergoplatform.explorer._ +import org.ergoplatform.explorer.db.models.AnyOutput +import org.ergoplatform.explorer.db.models.aggregates.AnyAsset +import org.ergoplatform.explorer.http.api.models.AssetInstanceInfo +import sttp.tapir.{Schema, SchemaType, Validator} + +@derive(encoder, decoder) +final case class AnyOutputInfo( + boxId: BoxId, + transactionId: TxId, + headerId: Option[BlockId], + value: Long, + index: Int, + globalIndex: Option[Long], + creationHeight: Int, + settlementHeight: Option[Int], + ergoTree: HexString, + ergoTreeConstants: String, + ergoTreeScript: String, + address: Address, + assets: List[AssetInstanceInfo], + additionalRegisters: Json, + spentTransactionId: Option[TxId], + mainChain: Option[Boolean] +) + +object AnyOutputInfo { + + implicit val schema: Schema[AnyOutputInfo] = + Schema + .derived[AnyOutputInfo] + .modify(_.boxId)(_.description("Id of the box")) + .modify(_.transactionId)(_.description("Id of the transaction that created the box")) + .modify(_.headerId)(_.description("Id of the block a box included in")) + .modify(_.value)(_.description("Value of the box in nanoERG")) + .modify(_.index)(_.description("Index of the output in a transaction")) + .modify(_.globalIndex)(_.description("Global index of the output in the blockchain")) + .modify(_.creationHeight)(_.description("Height at which the box was created")) + .modify(_.settlementHeight)(_.description("Height at which the box got fixed in blockchain")) + .modify(_.ergoTree)(_.description("Serialized ergo tree")) + .modify(_.address)(_.description("An address derived from ergo tree")) + .modify(_.spentTransactionId)(_.description("Id of the transaction this output was spent by")) + + implicit val validator: Validator[AnyOutputInfo] = schema.validator + + implicit private def registersSchema: Schema[Json] = + Schema( + SchemaType.SOpenProduct( + Schema(SchemaType.SString[Json]()) + )(_ => Map.empty) + ) + + def apply( + o: AnyOutput, + assets: List[AnyAsset] + ): AnyOutputInfo = { + val (ergoTreeConstants, ergoTreeScript) = PrettyErgoTree + .fromHexString(o.ergoTree) + .fold( + _ => ("", ""), + tree => (tree.constants, tree.script) + ) + AnyOutputInfo( + o.boxId, + o.txId, + o.headerId, + o.value, + o.index, + o.globalIndex, + o.creationHeight, + o.settlementHeight, + o.ergoTree, + ergoTreeConstants, + ergoTreeScript, + o.address, + assets.sortBy(_.index).map(AssetInstanceInfo(_)), + o.additionalRegisters, + o.spendingTxId, + o.mainChain + ) + } +} diff --git a/modules/explorer-api/src/main/scala/org/ergoplatform/explorer/http/api/v1/routes/BoxesRoutes.scala b/modules/explorer-api/src/main/scala/org/ergoplatform/explorer/http/api/v1/routes/BoxesRoutes.scala index 385eebfbe..37c9cca68 100644 --- a/modules/explorer-api/src/main/scala/org/ergoplatform/explorer/http/api/v1/routes/BoxesRoutes.scala +++ b/modules/explorer-api/src/main/scala/org/ergoplatform/explorer/http/api/v1/routes/BoxesRoutes.scala @@ -42,7 +42,8 @@ final class BoxesRoutes[ getOutputsByAddressR <+> getUnspentOutputsByAddressR <+> getOutputByIdR <+> - `getUnspent&UnconfirmedOutputsMergedByAddressR` + `getUnspent&UnconfirmedOutputsMergedByAddressR` <+> + getAllUnspentOutputsR private def interpreter = Http4sServerInterpreter(opts) @@ -150,6 +151,11 @@ final class BoxesRoutes[ interpreter.toRoutes(defs.searchUnspentOutputsByTokensUnionDef) { case (query, paging) => service.searchUnspentByAssetsUnion(query, paging).adaptThrowable.value } + + private def getAllUnspentOutputsR: HttpRoutes[F] = + interpreter.toRoutes(defs.getAllUnspentOutputsByAddressDef) { case (address, paging, ord) => + service.getAllUnspentOutputs(address, paging, ord).adaptThrowable.value + } } object BoxesRoutes { diff --git a/modules/explorer-api/src/main/scala/org/ergoplatform/explorer/http/api/v1/services/Boxes.scala b/modules/explorer-api/src/main/scala/org/ergoplatform/explorer/http/api/v1/services/Boxes.scala index 9a480611d..789bc623b 100644 --- a/modules/explorer-api/src/main/scala/org/ergoplatform/explorer/http/api/v1/services/Boxes.scala +++ b/modules/explorer-api/src/main/scala/org/ergoplatform/explorer/http/api/v1/services/Boxes.scala @@ -12,13 +12,27 @@ import org.ergoplatform.explorer.Err.{RefinementFailed, RequestProcessingErr} import org.ergoplatform.explorer._ import org.ergoplatform.explorer.db.Trans import org.ergoplatform.explorer.db.algebra.LiftConnectionIO -import org.ergoplatform.explorer.db.models.Output -import org.ergoplatform.explorer.db.models.aggregates.ExtendedOutput -import org.ergoplatform.explorer.db.repositories.{AssetRepo, HeaderRepo, OutputRepo, UInputRepo, UOutputRepo} +import org.ergoplatform.explorer.db.models.{AnyOutput, Output, UOutput} +import org.ergoplatform.explorer.db.models.aggregates.{ExtendedOutput, ExtendedUOutput} +import org.ergoplatform.explorer.db.repositories.{ + AssetRepo, + HeaderRepo, + OutputRepo, + UAssetRepo, + UInputRepo, + UOutputRepo +} import org.ergoplatform.explorer.http.api.models.Sorting.SortOrder import org.ergoplatform.explorer.http.api.models.{HeightRange, Items, Paging} import org.ergoplatform.explorer.http.api.streaming.CompileStream -import org.ergoplatform.explorer.http.api.v1.models.{BoxAssetsQuery, BoxQuery, MOutputInfo, OutputInfo, UOutputInfo} +import org.ergoplatform.explorer.http.api.v1.models.{ + AnyOutputInfo, + BoxAssetsQuery, + BoxQuery, + MOutputInfo, + OutputInfo, + UOutputInfo +} import org.ergoplatform.explorer.http.api.v1.shared.MempoolProps import org.ergoplatform.explorer.protocol.sigma._ import org.ergoplatform.explorer.settings.ServiceSettings @@ -109,6 +123,10 @@ trait Boxes[F[_]] { /** Get unspent outputs matching a given `boxQuery`. */ def searchUnspentByAssetsUnion(boxQuery: BoxAssetsQuery, paging: Paging): F[Items[OutputInfo]] + + /** Get both confirmed & unconfirmed outputs with the given `address` in proposition. + */ + def getAllUnspentOutputs(address: Address, paging: Paging, ord: SortOrder): F[Items[AnyOutputInfo]] } object Boxes { @@ -119,8 +137,8 @@ object Boxes { ](serviceSettings: ServiceSettings, memprops: MempoolProps[F, D])(trans: D Trans F)(implicit e: ErgoAddressEncoder ): F[Boxes[F]] = - (HeaderRepo[F, D], OutputRepo[F, D], AssetRepo[F, D], UOutputRepo[F, D], UInputRepo[F, D]).mapN( - new Live(serviceSettings, memprops, _, _, _, _, _)(trans) + (HeaderRepo[F, D], OutputRepo[F, D], AssetRepo[F, D], UAssetRepo[F, D], UOutputRepo[F, D], UInputRepo[F, D]).mapN( + new Live(serviceSettings, memprops, _, _, _, _, _, _)(trans) ) final private class Live[ @@ -132,6 +150,7 @@ object Boxes { headers: HeaderRepo[D, Stream], outputs: OutputRepo[D, Stream], assets: AssetRepo[D, Stream], + uassets: UAssetRepo[D], uoutputs: UOutputRepo[D, Stream], uinputs: UInputRepo[D, Stream] )(trans: D Trans F)(implicit e: ErgoAddressEncoder) @@ -379,6 +398,18 @@ object Boxes { .thrushK(trans.xa) } + def getAllUnspentOutputs(address: Address, paging: Paging, ord: SortOrder): F[Items[AnyOutputInfo]] = { + val ergoTree = addressToErgoTreeHex(address) + (for { + nUnspent <- uoutputs.countAllByErgoTree(ergoTree) + boxes <- uoutputs + .streamAllUnspentByErgoTree(ergoTree, paging.offset, paging.limit, ord.value) + .chunkN(serviceSettings.chunkSize) + .through(toAnyOutputInfo) + .to[List] + } yield Items(boxes, nUnspent)).thrushK(trans.xa) + } + private def toOutputInfo: Pipe[D, Chunk[ExtendedOutput], OutputInfo] = for { outs <- _ @@ -388,6 +419,15 @@ object Boxes { flattened <- Stream.emits(outsInfo.toList) } yield flattened + private def toAnyOutputInfo: Pipe[D, Chunk[AnyOutput], AnyOutputInfo] = + for { + outs <- _ + outIds <- Stream.emit(outs.toList.map(_.boxId).toNel).unNone + assets <- uassets.getConfirmedAndUnconfirmed(outIds).map(_.groupBy(_.boxId)).asStream + outsInfo = outs.map(out => AnyOutputInfo(out, assets.getOrElse(out.boxId, Nil))) + flattened <- Stream.emits(outsInfo.toList) + } yield flattened + private def toUnspentOutputInfo: Pipe[D, Chunk[Output], OutputInfo] = for { outs <- _ diff --git a/modules/explorer-core/src/main/scala/org/ergoplatform/explorer/db/models/AnyOutput.scala b/modules/explorer-core/src/main/scala/org/ergoplatform/explorer/db/models/AnyOutput.scala new file mode 100644 index 000000000..e184a5dac --- /dev/null +++ b/modules/explorer-core/src/main/scala/org/ergoplatform/explorer/db/models/AnyOutput.scala @@ -0,0 +1,23 @@ +package org.ergoplatform.explorer.db.models + +import io.circe.Json +import org.ergoplatform.explorer._ + +final case class AnyOutput( + boxId: BoxId, + txId: TxId, + headerId: Option[BlockId], + value: Long, + creationHeight: Int, + settlementHeight: Option[Int], + index: Int, + globalIndex: Option[Long], + ergoTree: HexString, + ergoTreeTemplateHash: ErgoTreeTemplateHash, + address: Address, + additionalRegisters: Json, + timestamp: Option[Long], + mainChain: Option[Boolean], + spendingTxId: Option[TxId] +) + diff --git a/modules/explorer-core/src/main/scala/org/ergoplatform/explorer/db/models/aggregates/AnyAsset.scala b/modules/explorer-core/src/main/scala/org/ergoplatform/explorer/db/models/aggregates/AnyAsset.scala new file mode 100644 index 000000000..35bd451df --- /dev/null +++ b/modules/explorer-core/src/main/scala/org/ergoplatform/explorer/db/models/aggregates/AnyAsset.scala @@ -0,0 +1,14 @@ +package org.ergoplatform.explorer.db.models.aggregates + +import org.ergoplatform.explorer.{BlockId, BoxId, TokenId, TokenType} + +final case class AnyAsset( + tokenId: TokenId, + boxId: BoxId, + headerId: Option[BlockId], + index: Int, + amount: Long, + name: Option[String], + decimals: Option[Int], + `type`: Option[TokenType] +) diff --git a/modules/explorer-core/src/main/scala/org/ergoplatform/explorer/db/queries/UAssetQuerySet.scala b/modules/explorer-core/src/main/scala/org/ergoplatform/explorer/db/queries/UAssetQuerySet.scala index e770414eb..37aaecf14 100644 --- a/modules/explorer-core/src/main/scala/org/ergoplatform/explorer/db/queries/UAssetQuerySet.scala +++ b/modules/explorer-core/src/main/scala/org/ergoplatform/explorer/db/queries/UAssetQuerySet.scala @@ -4,7 +4,7 @@ import cats.data.NonEmptyList import doobie.implicits._ import doobie.util.query.Query0 import doobie.{Fragments, LogHandler} -import org.ergoplatform.explorer.db.models.aggregates.{AggregatedAsset, ExtendedUAsset} +import org.ergoplatform.explorer.db.models.aggregates.{AggregatedAsset, AnyAsset, ExtendedUAsset} import org.ergoplatform.explorer.{BoxId, HexString} object UAssetQuerySet extends QuerySet { @@ -49,6 +49,36 @@ object UAssetQuerySet extends QuerySet { Fragments.in(fr"where a.box_id", boxIds)) .query[ExtendedUAsset] + def getConfirmedAndUnconfirmed(boxIds: NonEmptyList[BoxId])(implicit lh: LogHandler): Query0[AnyAsset] = + sql""" + |select distinct + | a.token_id, + | a.box_id, + | null, + | a.index, + | a.value, + | t.name, + | t.decimals, + | t.type + |from node_u_assets a + |left join tokens t on a.token_id = t.token_id + |${Fragments.in(fr"where a.box_id", boxIds)} + |union + |select distinct on (a.index, a.token_id, a.box_id) + | a.token_id, + | a.box_id, + | a.header_id, + | a.index, + | a.value, + | t.name, + | t.decimals, + | t.type + |from node_assets a + |left join tokens t on a.token_id = t.token_id + |${Fragments.in(fr"where a.box_id", boxIds)} + |""".stripMargin + .query[AnyAsset] + def getAllUnspentByErgoTree(ergoTree: HexString)(implicit lh: LogHandler): Query0[ExtendedUAsset] = sql""" |select diff --git a/modules/explorer-core/src/main/scala/org/ergoplatform/explorer/db/queries/UOutputQuerySet.scala b/modules/explorer-core/src/main/scala/org/ergoplatform/explorer/db/queries/UOutputQuerySet.scala index aa33af006..6eb82c050 100644 --- a/modules/explorer-core/src/main/scala/org/ergoplatform/explorer/db/queries/UOutputQuerySet.scala +++ b/modules/explorer-core/src/main/scala/org/ergoplatform/explorer/db/queries/UOutputQuerySet.scala @@ -6,8 +6,9 @@ import doobie._ import doobie.implicits._ import doobie.refined.implicits._ import doobie.util.query.Query0 -import org.ergoplatform.explorer.db.models.UOutput +import org.ergoplatform.explorer.constraints.OrderingString import org.ergoplatform.explorer.db.models.aggregates.ExtendedUOutput +import org.ergoplatform.explorer.db.models.{AnyOutput, UOutput} import org.ergoplatform.explorer.{BoxId, HexString, TxId} object UOutputQuerySet extends QuerySet { @@ -184,6 +185,59 @@ object UOutputQuerySet extends QuerySet { |where i.box_id is null and o.ergo_tree = $ergoTree |""".stripMargin.query[ExtendedUOutput] + def streamAllUnspentByErgoTree(ergoTree: HexString, offset: Int, limit: Int, ordering: OrderingString)(implicit + lh: LogHandler + ): Query0[AnyOutput] = { + val q = sql""" + |select * from ( + |select distinct on (o.box_id) + |o.box_id, + |o.tx_id, + |null, + |o.value, + |o.creation_height, + |null, + |o.index, + |null, + |o.ergo_tree, + |o.ergo_tree_template_hash, + |o.address, + |o.additional_registers, + |null, + |null, + |null + |from node_u_outputs o + |left join node_u_inputs i on i.box_id = o.box_id + |where i.box_id is null and o.ergo_tree = $ergoTree + |union all + |select distinct on (o.box_id, o.global_index) + |o.box_id, + |o.tx_id, + |o.header_id, + |o.value, + |o.creation_height, + |o.settlement_height, + |o.index, + |o.global_index, + |o.ergo_tree, + |o.ergo_tree_template_hash, + |o.address, + |o.additional_registers, + |o.timestamp, + |o.main_chain, + |null + |from node_outputs o + |left join node_inputs i on o.box_id = i.box_id and i.main_chain = true + |where o.main_chain = true + |and i.box_id is null + |and o.ergo_tree = $ergoTree + |) sub + |""".stripMargin + val ord = Fragment.const(s"order by creation_height $ordering") + val lim = Fragment.const(s"offset $offset limit $limit") + (q ++ ord ++ lim).query[AnyOutput] + } + def sumUnspentByErgoTree( ergoTree: HexString )(implicit lh: LogHandler): Query0[Long] = @@ -192,4 +246,35 @@ object UOutputQuerySet extends QuerySet { |left join node_u_inputs i on i.box_id = o.box_id |where i.box_id is null and o.ergo_tree = $ergoTree |""".stripMargin.query[Long] + + def countUnspentByErgoTree( + ergoTree: HexString + )(implicit lh: LogHandler): Query0[Int] = + sql""" + |select count(distinct o.box_id) + |from node_u_outputs o + |left join node_u_inputs i on o.box_id = i.box_id + |where i.box_id is null + |and o.ergo_tree = $ergoTree + |""".stripMargin.query[Int] + + def countAllByErgoTree( + ergoTree: HexString + )(implicit lh: LogHandler): Query0[Int] = + sql""" + |SELECT sum(count) from ( + |select count(distinct o.box_id) + |from node_u_outputs o + |left join node_u_inputs i on o.box_id = i.box_id + |where i.box_id is null + |and o.ergo_tree = $ergoTree + |union + |select count(distinct o.box_id) + |from node_outputs o + |left join node_inputs i on o.box_id = i.box_id and i.main_chain = true + |where o.main_chain = true + |and i.box_id is null + |and o.ergo_tree = $ergoTree + |) sub + |""".stripMargin.query[Int] } diff --git a/modules/explorer-core/src/main/scala/org/ergoplatform/explorer/db/repositories/UAssetRepo.scala b/modules/explorer-core/src/main/scala/org/ergoplatform/explorer/db/repositories/UAssetRepo.scala index 47f41791d..c61a9129d 100644 --- a/modules/explorer-core/src/main/scala/org/ergoplatform/explorer/db/repositories/UAssetRepo.scala +++ b/modules/explorer-core/src/main/scala/org/ergoplatform/explorer/db/repositories/UAssetRepo.scala @@ -8,7 +8,7 @@ import doobie.util.log.LogHandler import org.ergoplatform.explorer.db.DoobieLogHandler import org.ergoplatform.explorer.db.algebra.LiftConnectionIO import org.ergoplatform.explorer.db.models.UAsset -import org.ergoplatform.explorer.db.models.aggregates.{AggregatedAsset, ExtendedUAsset} +import org.ergoplatform.explorer.db.models.aggregates.{AggregatedAsset, AnyAsset, ExtendedUAsset} import org.ergoplatform.explorer.db.syntax.liftConnectionIO._ import org.ergoplatform.explorer.{BoxId, HexString} @@ -32,6 +32,10 @@ trait UAssetRepo[D[_]] { */ def getAllByBoxIds(boxIds: NonEmptyList[BoxId]): D[List[ExtendedUAsset]] + /** Get confirmed + unconfirmed assets belonging to a given list of `boxId`. + */ + def getConfirmedAndUnconfirmed(boxIds: NonEmptyList[BoxId]): D[List[AnyAsset]] + /** Get all unspent assets belonging to a given `address`. */ def getAllUnspentByErgoTree(ergoTree: HexString): D[List[ExtendedUAsset]] @@ -62,6 +66,9 @@ object UAssetRepo { def getAllByBoxIds(boxIds: NonEmptyList[BoxId]): D[List[ExtendedUAsset]] = QS.getAllByBoxIds(boxIds).to[List].liftConnectionIO + def getConfirmedAndUnconfirmed(boxIds: NonEmptyList[BoxId]): D[List[AnyAsset]] = + QS.getConfirmedAndUnconfirmed(boxIds).to[List].liftConnectionIO + def getAllUnspentByErgoTree(ergoTree: HexString): D[List[ExtendedUAsset]] = QS.getAllUnspentByErgoTree(ergoTree).to[List].liftConnectionIO diff --git a/modules/explorer-core/src/main/scala/org/ergoplatform/explorer/db/repositories/UOutputRepo.scala b/modules/explorer-core/src/main/scala/org/ergoplatform/explorer/db/repositories/UOutputRepo.scala index a4d7c771d..94286694f 100644 --- a/modules/explorer-core/src/main/scala/org/ergoplatform/explorer/db/repositories/UOutputRepo.scala +++ b/modules/explorer-core/src/main/scala/org/ergoplatform/explorer/db/repositories/UOutputRepo.scala @@ -7,12 +7,13 @@ import cats.implicits._ import doobie.free.implicits._ import doobie.refined.implicits._ import doobie.util.log.LogHandler +import org.ergoplatform.explorer.constraints.OrderingString import org.ergoplatform.explorer.db.DoobieLogHandler import org.ergoplatform.explorer.{BoxId, ErgoTree, HexString, TxId} import org.ergoplatform.explorer.db.algebra.LiftConnectionIO import org.ergoplatform.explorer.db.syntax.liftConnectionIO._ import org.ergoplatform.explorer.db.doobieInstances._ -import org.ergoplatform.explorer.db.models.UOutput +import org.ergoplatform.explorer.db.models.{AnyOutput, UOutput} import org.ergoplatform.explorer.db.models.aggregates.ExtendedUOutput /** [[ExtendedUOutput]] data access operations. @@ -59,9 +60,26 @@ trait UOutputRepo[D[_], S[_[_], _]] { */ def getAllUnspentByErgoTree(ergoTree: HexString): D[List[ExtendedUOutput]] + /** Get confirmed + unconfirmed unspent main-chain outputs with a given `ergoTree`. + */ + def streamAllUnspentByErgoTree( + ergoTree: HexString, + offset: Int, + limit: Int, + ordering: OrderingString + ): S[D, AnyOutput] + /** Get total amount of all unspent main-chain outputs with a given `ergoTree`. */ def sumUnspentByErgoTree(ergoTree: HexString): D[Long] + + /** Count unspent main-chain outputs with a given `ergoTree`. + */ + def countUnspentByErgoTree(ergoTree: HexString): D[Int] + + /** Count confirmed + unconfirmed unspent main-chain outputs with a given `ergoTree`. + */ + def countAllByErgoTree(ergoTree: HexString): D[Int] } object UOutputRepo { @@ -107,7 +125,23 @@ object UOutputRepo { def getAllUnspentByErgoTree(ergoTree: HexString): D[List[ExtendedUOutput]] = QS.getAllUnspentByErgoTree(ergoTree).to[List].liftConnectionIO + def streamAllUnspentByErgoTree( + ergoTree: HexString, + offset: Int, + limit: Int, + ordering: OrderingString + ): Stream[D, AnyOutput] = + QS.streamAllUnspentByErgoTree(ergoTree, offset, limit, ordering) + .stream + .translate(LiftConnectionIO[D].liftConnectionIOK) + def sumUnspentByErgoTree(ergoTree: HexString): D[Long] = QS.sumUnspentByErgoTree(ergoTree).unique.liftConnectionIO + + def countUnspentByErgoTree(ergoTree: HexString): D[Int] = + QS.countUnspentByErgoTree(ergoTree).unique.liftConnectionIO + + def countAllByErgoTree(ergoTree: HexString): D[Int] = + QS.countAllByErgoTree(ergoTree).unique.liftConnectionIO } } diff --git a/modules/explorer-core/src/main/scala/org/ergoplatform/explorer/protocol/Emission.scala b/modules/explorer-core/src/main/scala/org/ergoplatform/explorer/protocol/Emission.scala new file mode 100644 index 000000000..9e42726f3 --- /dev/null +++ b/modules/explorer-core/src/main/scala/org/ergoplatform/explorer/protocol/Emission.scala @@ -0,0 +1,96 @@ +package org.ergoplatform.explorer.protocol + +import org.ergoplatform.settings.MonetarySettings + +final case class ReemissionSettings( + applyReemissionRules: Boolean = true, + activationHeight: Int = 777217, + reemissionStartHeight: Int = 2080800 +) + +final class Emission(settings: MonetarySettings, reemission: ReemissionSettings) { + + private lazy val reemissionLen: Long = (reemission.activationHeight until reemission.reemissionStartHeight).map { h => + val defaultReward = math.max(settings.fixedRate - settings.oneEpochReduction * epoch(h), 0) + getReemission(defaultReward) + }.sum / constants.Eip27ResidualEmission + + def issuedCoinsAfterHeight(h: Long): Long = + if (h < settings.fixedRatePeriod) { + settings.fixedRate * h + } else if (!reemission.applyReemissionRules || h < reemission.activationHeight) { + val fixedRateEmission: Long = settings.fixedRate * (settings.fixedRatePeriod - 1) + val currentEpoch = epoch(h) + val completeNonFixedRateEpochsEmission: Long = (1 to currentEpoch.toInt).map { e => + math.max(settings.fixedRate - settings.oneEpochReduction * e, 0) * settings.epochLength + }.sum + val heightInThisEpoch = (h - settings.fixedRatePeriod) % settings.epochLength + 1 + val rateThisEpoch = math.max(settings.fixedRate - settings.oneEpochReduction * (currentEpoch + 1), 0) + val incompleteEpochEmission = heightInThisEpoch * rateThisEpoch + + completeNonFixedRateEpochsEmission + fixedRateEmission + incompleteEpochEmission + } else { + val emissionBeforeEip27 = issuedCoinsAfterHeight(reemission.activationHeight - 1) + val firstEpochAfterActivation = + (reemission.activationHeight - settings.fixedRatePeriod) / settings.epochLength + 1 + val firstReductionAfterActivation = firstEpochAfterActivation * settings.epochLength + val currentEpoch = epoch(h) + val defaultRewardPerBlockInCurrentEpoch = + math.max(settings.fixedRate - settings.oneEpochReduction * currentEpoch, 0) + val adjustedReward = defaultRewardPerBlockInCurrentEpoch - getReemission( + defaultRewardPerBlockInCurrentEpoch + ) + if (h < firstReductionAfterActivation) { + val blocksSinceActivation = h - reemission.activationHeight + val accumulatedEmissionSinceActivation = blocksSinceActivation * adjustedReward + emissionBeforeEip27 + accumulatedEmissionSinceActivation + } else { + val accumulatedEmissionSinceActivationBeforeFirstReduction = + (firstReductionAfterActivation - reemission.activationHeight) * adjustedReward + if (h < reemission.reemissionStartHeight) { + val accumulatedEmissionSinceFirstReduction = + (firstEpochAfterActivation to currentEpoch.toInt).map(e => getEpochEmissionAfterEip27(e)).sum + val heightInThisEpoch = (h - settings.fixedRatePeriod) % settings.epochLength + 1 + val rateThisEpoch = math.max(settings.fixedRate - settings.oneEpochReduction * (currentEpoch + 1), 0) + val rateThisEpochWithReemission = rateThisEpoch - getReemission(rateThisEpoch) + val incompleteEpochEmission = heightInThisEpoch * rateThisEpochWithReemission + emissionBeforeEip27 + accumulatedEmissionSinceActivationBeforeFirstReduction + accumulatedEmissionSinceFirstReduction + incompleteEpochEmission + } else { + val lastEmissionEpoch = + (reemission.reemissionStartHeight - settings.fixedRatePeriod) / settings.epochLength + 1 + val accumulatedEmissionSinceFirstReductionUntilReemission = + (firstEpochAfterActivation to lastEmissionEpoch).map(e => getEpochEmissionAfterEip27(e)).sum + val reemissionTail = + math.min(h - reemission.reemissionStartHeight, reemissionLen) * constants.Eip27ResidualEmission + emissionBeforeEip27 + accumulatedEmissionSinceActivationBeforeFirstReduction + accumulatedEmissionSinceFirstReductionUntilReemission + reemissionTail + } + } + } + + def emissionAt(h: Long): Long = { + val defaultReward = math.max(settings.fixedRate - settings.oneEpochReduction * epoch(h), 0) + if (h < settings.fixedRatePeriod) { + settings.fixedRate + } else if (h < reemission.activationHeight || !reemission.applyReemissionRules) { + defaultReward + } else if (h < reemission.reemissionStartHeight && reemission.applyReemissionRules) { + defaultReward - getReemission(defaultReward) + } else if (h < reemission.reemissionStartHeight + reemissionLen && reemission.applyReemissionRules) { + constants.Eip27ResidualEmission + } else { + 0 + } + } + + private def epoch(h: Long): Long = + 1 + (h - settings.fixedRatePeriod) / settings.epochLength + + private def getEpochEmissionAfterEip27(e: Int): Long = { + val defaultRewardInEpoch = math.max(settings.fixedRate - settings.oneEpochReduction * e, 0) * settings.epochLength + defaultRewardInEpoch - getReemission(defaultRewardInEpoch) * settings.epochLength + } + + private def getReemission(reward: Long): Long = + if (reward >= constants.Eip27UpperPoint) constants.Eip27DefaultReEmission + else math.max(reward - constants.Eip27ResidualEmission, 0) +} diff --git a/modules/explorer-core/src/main/scala/org/ergoplatform/explorer/settings/ProtocolSettings.scala b/modules/explorer-core/src/main/scala/org/ergoplatform/explorer/settings/ProtocolSettings.scala index d1819649b..f6db14803 100644 --- a/modules/explorer-core/src/main/scala/org/ergoplatform/explorer/settings/ProtocolSettings.scala +++ b/modules/explorer-core/src/main/scala/org/ergoplatform/explorer/settings/ProtocolSettings.scala @@ -4,6 +4,7 @@ import eu.timepit.refined.api.Refined import eu.timepit.refined.string._ import org.ergoplatform.ErgoAddressEncoder import org.ergoplatform.explorer.Address +import org.ergoplatform.explorer.protocol.{Emission, ReemissionSettings} import org.ergoplatform.mining.emission.EmissionRules import org.ergoplatform.settings.MonetarySettings @@ -13,7 +14,7 @@ final case class ProtocolSettings( monetary: MonetarySettings ) { - val emission = new EmissionRules(monetary) + val emission = new Emission(monetary, ReemissionSettings()) val addressEncoder: ErgoAddressEncoder = ErgoAddressEncoder(networkPrefix.value.toByte) diff --git a/modules/explorer-core/src/test/scala/org/ergoplatform/explorer/protocol/EmissionSpec.scala b/modules/explorer-core/src/test/scala/org/ergoplatform/explorer/protocol/EmissionSpec.scala new file mode 100644 index 000000000..c7a3323a6 --- /dev/null +++ b/modules/explorer-core/src/test/scala/org/ergoplatform/explorer/protocol/EmissionSpec.scala @@ -0,0 +1,12 @@ +package org.ergoplatform.explorer.protocol + +import org.ergoplatform.settings.MonetarySettings +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should + +class EmissionSpec extends AnyFlatSpec with should.Matchers { + val emission = new Emission(MonetarySettings(), ReemissionSettings()) + "Emission.issuedCoinsAfterHeight" should "compute correct total supply" in { + emission.issuedCoinsAfterHeight(6647136L) shouldBe 102624741000000000L + } +}