Skip to content

Commit

Permalink
Get confirmed + unconfirmed unspent boxes EP (#246)
Browse files Browse the repository at this point in the history
* Get confirmed + unconfirmed unspent boxes EP added

* refactoring

* review comments addressed

* review comments addressed

* review comments addressed

* logic correction
  • Loading branch information
semyonoskin authored Dec 19, 2022
1 parent d553381 commit cbd915f
Show file tree
Hide file tree
Showing 11 changed files with 355 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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}

Expand All @@ -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] =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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._
Expand Down Expand Up @@ -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]])
}
Original file line number Diff line number Diff line change
@@ -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
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ final class BoxesRoutes[
getOutputsByAddressR <+>
getUnspentOutputsByAddressR <+>
getOutputByIdR <+>
`getUnspent&UnconfirmedOutputsMergedByAddressR`
`getUnspent&UnconfirmedOutputsMergedByAddressR` <+>
getAllUnspentOutputsR

private def interpreter = Http4sServerInterpreter(opts)

Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand All @@ -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[
Expand All @@ -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)
Expand Down Expand Up @@ -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 <- _
Expand All @@ -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 <- _
Expand Down
Original file line number Diff line number Diff line change
@@ -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]
)

Original file line number Diff line number Diff line change
@@ -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]
)
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down
Loading

0 comments on commit cbd915f

Please sign in to comment.