Skip to content

Commit

Permalink
Pretty print ergo tree (#243)
Browse files Browse the repository at this point in the history
  • Loading branch information
jozanek authored Oct 17, 2022
1 parent 97bb05b commit 44c1acb
Show file tree
Hide file tree
Showing 9 changed files with 166 additions and 6 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package org.ergoplatform.explorer.http.api.v1.defs

import org.ergoplatform.explorer.http.api.ApiErr
import org.ergoplatform.explorer.http.api.commonDirectives._
import org.ergoplatform.explorer.http.api.v1.models.{ErgoTreeConversionRequest, ErgoTreeHuman}
import sttp.tapir._
import sttp.tapir.json.circe._

final class ErgoTreeEndpointDefs[F[_]]() {

private val PathPrefix = "ergotree"

def endpoints: List[Endpoint[_, _, _, _]] = List(convertErgoTreeDef)

def convertErgoTreeDef: Endpoint[ErgoTreeConversionRequest, ApiErr, ErgoTreeHuman, Any] =
baseEndpointDef.post
.in(PathPrefix / "convert")
.in(jsonBody[ErgoTreeConversionRequest])
.out(jsonBody[ErgoTreeHuman])
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package org.ergoplatform.explorer.http.api.v1.models

import derevo.circe.{decoder, encoder}
import derevo.derive
import sttp.tapir.{Schema, Validator}

@derive(encoder, decoder)
final case class ErgoTreeHuman(constants: String, script: String)

object ErgoTreeHuman {

implicit val schema: Schema[ErgoTreeHuman] =
Schema
.derived[ErgoTreeHuman]
.modify(_.constants)(_.description("Constants use in ergo script"))
.modify(_.script)(_.description("Human readable ergo script"))

implicit val validator: Validator[ErgoTreeHuman] = schema.validator
}

@derive(encoder, decoder)
final case class ErgoTreeConversionRequest(hashed: String)

object ErgoTreeConversionRequest {

implicit val schema: Schema[ErgoTreeConversionRequest] =
Schema
.derived[ErgoTreeConversionRequest]
.modify(_.hashed)(_.description("Hashed value of ergo script"))

implicit val validator: Validator[ErgoTreeConversionRequest] = schema.validator
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ final case class InputInfo(
outputCreatedAt: Int,
outputSettledAt: Int,
ergoTree: HexString,
ergoTreeConstants: String,
ergoTreeScript: String,
address: Address,
assets: List[AssetInstanceInfo],
additionalRegisters: Json
Expand Down Expand Up @@ -53,7 +55,11 @@ object InputInfo {
)(_ => Map.empty)
)

def apply(i: FullInput, assets: List[ExtendedAsset]): InputInfo =
def apply(i: FullInput, assets: List[ExtendedAsset]): InputInfo = {
val (ergoTreeConstants, ergoTreeScript) = PrettyErgoTree.fromHexString(i.ergoTree).fold(
_ => ("", ""),
tree => (tree.constants, tree.script)
)
InputInfo(
i.input.boxId,
i.value,
Expand All @@ -66,10 +72,13 @@ object InputInfo {
i.outputCreatedAt,
i.outputSettledAt,
i.ergoTree,
ergoTreeConstants,
ergoTreeScript,
i.address,
assets.sortBy(_.index).map(AssetInstanceInfo(_)),
i.additionalRegisters
)
}

def batch(ins: List[FullInput], assets: List[ExtendedAsset]): List[InputInfo] = {
val groupedAssets = assets.groupBy(_.boxId)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ final case class OutputInfo(
creationHeight: Int,
settlementHeight: Int,
ergoTree: HexString,
ergoTreeConstants: String,
ergoTreeScript: String,
address: Address,
assets: List[AssetInstanceInfo],
additionalRegisters: Json,
Expand Down Expand Up @@ -56,7 +58,11 @@ object OutputInfo {
def apply(
o: ExtendedOutput,
assets: List[ExtendedAsset]
): OutputInfo =
): OutputInfo = {
val (ergoTreeConstants, ergoTreeScript) = PrettyErgoTree.fromHexString(o.output.ergoTree).fold(
_ => ("", ""),
tree => (tree.constants, tree.script)
)
OutputInfo(
o.output.boxId,
o.output.txId,
Expand All @@ -67,17 +73,24 @@ object OutputInfo {
o.output.creationHeight,
o.output.settlementHeight,
o.output.ergoTree,
ergoTreeConstants,
ergoTreeScript,
o.output.address,
assets.sortBy(_.index).map(AssetInstanceInfo(_)),
o.output.additionalRegisters,
o.spentByOpt,
o.output.mainChain
)
}

def unspent(
o: Output,
assets: List[ExtendedAsset]
): OutputInfo =
): OutputInfo = {
val (ergoTreeConstants, ergoTreeScript) = PrettyErgoTree.fromHexString(o.ergoTree).fold(
_ => ("", ""),
tree => (tree.constants, tree.script)
)
OutputInfo(
o.boxId,
o.txId,
Expand All @@ -88,10 +101,13 @@ object OutputInfo {
o.creationHeight,
o.settlementHeight,
o.ergoTree,
ergoTreeConstants,
ergoTreeScript,
o.address,
assets.sortBy(_.index).map(AssetInstanceInfo(_)),
o.additionalRegisters,
None,
o.mainChain
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package org.ergoplatform.explorer.http.api.v1.models

import org.ergoplatform.explorer.HexString
import sigmastate.PrettyPrintErgoTree
import sigmastate.lang.exceptions.SerializerException
import sigmastate.serialization.ErgoTreeSerializer.DefaultSerializer

object PrettyErgoTree {
def fromString(s: String) : Either[PrettyErgoTreeError, ErgoTreeHuman] = {
HexString.fromString[Either[Throwable, *]](s) match {
case Left(_) => Left(PrettyErgoTreeError.BadEncoding)
case Right(hexString) => fromHexString(hexString)
}
}

def fromHexString(h: HexString): Either[PrettyErgoTreeError, ErgoTreeHuman] = {
try {
val ergoTree = DefaultSerializer.deserializeErgoTree(h.bytes)
ergoTree.root match {
case Left(_) => Left(PrettyErgoTreeError.UnparsedErgoTree)
case Right(value) =>
val script = PrettyPrintErgoTree.prettyPrint(value, width = 160)
val constants = ergoTree.constants.zipWithIndex.map { case (c, i) => s"$i: ${c.value}" }.mkString("\n")
Right(ErgoTreeHuman(constants, script))
}
} catch {
case se: SerializerException => Left(PrettyErgoTreeError.DeserializeException(se.message))
}
}
}

sealed trait PrettyErgoTreeError
object PrettyErgoTreeError {
case object BadEncoding extends PrettyErgoTreeError
case class DeserializeException(msg: String) extends PrettyErgoTreeError
case object UnparsedErgoTree extends PrettyErgoTreeError
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ final class DocsRoutes[F[_]: Concurrent: ContextShift: Timer](settings: Requests
new AddressesEndpointDefs(settings).endpoints ++
new BlocksEndpointDefs(settings).endpoints ++
new MempoolEndpointDefs().endpoints ++
new StatsEndpointsDefs().endpoints
new StatsEndpointsDefs().endpoints ++
new ErgoTreeEndpointDefs().endpoints

private def tags =
Tag("transactions", "Transactions methods".some) ::
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package org.ergoplatform.explorer.http.api.v1.routes

import cats.MonadError
import cats.effect.{Concurrent, ContextShift, Timer}
import cats.syntax.semigroupk._
import cats.syntax.either._
import io.chrisdavenport.log4cats.Logger
import org.ergoplatform.explorer.HexString
import org.ergoplatform.explorer.http.api.ApiErr
import org.ergoplatform.explorer.http.api.algebra.AdaptThrowable.AdaptThrowableEitherT
import org.ergoplatform.explorer.http.api.syntax.adaptThrowable._
import org.ergoplatform.explorer.http.api.syntax.routes._
import org.ergoplatform.explorer.http.api.v1.defs.ErgoTreeEndpointDefs
import org.ergoplatform.explorer.http.api.v1.models.{ErgoTreeHuman, PrettyErgoTree, PrettyErgoTreeError}
import org.http4s.HttpRoutes
import sttp.tapir.server.http4s._


final class ErgoTreeRoutes[
F[_]: Concurrent: ContextShift: Timer: AdaptThrowableEitherT[*[_], ApiErr]
]()(implicit opts: Http4sServerOptions[F, F], F: MonadError[F, Throwable]) {

val defs = new ErgoTreeEndpointDefs

val routes: HttpRoutes[F] = convertErgoTreeR

private def interpreter = Http4sServerInterpreter(opts)

private def convertErgoTreeR: HttpRoutes[F] =
interpreter.toRoutes(defs.convertErgoTreeDef) { req => {
val maybeTree = PrettyErgoTree.fromString(req.hashed).leftMap{
case PrettyErgoTreeError.BadEncoding => ApiErr.badRequest(s"Could not decode hex string: ${req.hashed}")
case PrettyErgoTreeError.UnparsedErgoTree => ApiErr.badRequest(s"Could not parse ergo tree")
case PrettyErgoTreeError.DeserializeException(msg) => ApiErr.unknownErr(msg)
}
F.pure(maybeTree)
}}
}

object ErgoTreeRoutes {
def apply[F[_]: Concurrent: ContextShift: Timer: Logger]()(implicit opts: Http4sServerOptions[F, F]): HttpRoutes[F] =
new ErgoTreeRoutes[F]().routes
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,9 @@ object RoutesV1Bundle {
assetsRoutes = AssetsRoutes(requestsSettings, assets, tokens)
txsRoutes = TransactionsRoutes(requestsSettings, transactions)
addressesRoutes = AddressesRoutes(requestsSettings, transactions, addresses)
ergoTreeRoutes = ErgoTreeRoutes()
docs = DocsRoutes(requestsSettings)
routes =
infoRoutes <+> txsRoutes <+> boxesRoutes <+> epochsRoutes <+> tokensRoutes <+> assetsRoutes <+> addressesRoutes <+> blocksRoutes <+> mempoolRoutes <+> docs
infoRoutes <+> txsRoutes <+> boxesRoutes <+> epochsRoutes <+> tokensRoutes <+> assetsRoutes <+> addressesRoutes <+> blocksRoutes <+> mempoolRoutes <+> ergoTreeRoutes <+> docs
} yield RoutesV1Bundle(routes)
}
3 changes: 2 additions & 1 deletion project/dependencies.scala
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,8 @@ object dependencies {
Redis ++
Enums

lazy val api: List[ModuleID] = Monix
lazy val api: List[ModuleID] = Monix ++
List("org.scorexfoundation" %% "sigma-state" % "4.0.6-31-e2e0ffa1-SNAPSHOT")

lazy val grabber: List[ModuleID] = Monix

Expand Down

0 comments on commit 44c1acb

Please sign in to comment.