Skip to content

Commit

Permalink
TSDK-663 Transaction Pretty Print / Display (#167)
Browse files Browse the repository at this point in the history
* Updated Readme

* Finished UTXO display

* Finished Transaction display

* Added Txo display

* Added blockId display

* Parameterized the label padding

* Ensured tests pass with padding
  • Loading branch information
DiademShoukralla committed Dec 20, 2023
1 parent 58bc32d commit 27cdf2d
Show file tree
Hide file tree
Showing 12 changed files with 470 additions and 51 deletions.
52 changes: 1 addition & 51 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

# BramblSc

Topl's Brambl SDK implemented in Scala
Topl's Brambl SDK implemented in Scala. [Read the docs to get started](https://topl.github.io/BramblSc/docs/current/reference/getting-started).

Multiple artifacts will be built from this repo. Some will be just for Topl clients and some will be shared.

Expand All @@ -12,53 +12,3 @@ The artifacts generated from this repo are:
- crypto
- service-kit
- quivr4s

## Consume with JitPack

This repo can be consumed using jitpack. Here is how:

First, be sure to add jitpack to the end of the resolvers list in build.sbt. It should look like this:
```sbt
resolvers ++= Seq(
"Typesafe Repository" at "https://repo.typesafe.com/typesafe/releases/",
"Sonatype Staging" at "https://s01.oss.sonatype.org/content/repositories/staging",
"Sonatype Snapshots" at "https://s01.oss.sonatype.org/content/repositories/snapshots/",
"Bintray" at "https://jcenter.bintray.com/",
"jitpack" at "https://jitpack.io"
)
```

Then just add the dependency like this:
```sbt
val bramblSc =
"com.github.Topl" % "BramblSc" % "1bdc895"
```
Where `1bdc895` refers to a commit on this repo's main branch. This will add the artifacts for both `brambl-sdk` and `crypto`.
Then just use the dependencies like you would any other.

## Consume Maven Release

First, be sure to add Sonatype s01 releases to the end of the resolvers list in build.sbt. It should look like this:
```sbt
resolvers ++= Seq(
"Typesafe Repository" at "https://repo.typesafe.com/typesafe/releases/",
"Sonatype Staging" at "https://s01.oss.sonatype.org/content/repositories/staging",
"Sonatype Snapshots" at "https://s01.oss.sonatype.org/content/repositories/snapshots/",
"Bintray" at "https://jcenter.bintray.com/",
"jitpack" at "https://jitpack.io",
"Sonatype Releases" at "https://s01.oss.sonatype.org/content/repositories/releases/"
)
```

Then just add the dependencies for `brambl-sdk` and `crypto` like this:
```sbt
val brambl-sdk =
"co.topl" %% "brambl-sdk" % "2.0.0-alpha1"
```

```sbt
val crypto =
"co.topl" %% "crypto" % "2.0.0-alpha1"
```

Replace `2.0.0-alpha1` with the latest released version. Then just use the dependencies like you would any other.
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package co.topl.brambl.display

import co.topl.brambl.display.DisplayOps.DisplayTOps
import co.topl.brambl.models.box.{AssetMintingStatement, Value}
import co.topl.brambl.utils.Encoding
import co.topl.brambl.syntax.int128AsBigInt

trait AssetDisplayOps {

implicit val assetDisplay: DisplayOps[Value.Asset] = (asset: Value.Asset) =>
Seq(
"Asset",
padLabel("GroupId") + asset.groupId.map(gId => gId.display).getOrElse("N/A"),
padLabel("SeriesId") + asset.seriesId.map(sId => sId.display).getOrElse("N/A"),
padLabel("Commitment") + asset.commitment
.map(x => Encoding.encodeToHex(x.toByteArray()))
.getOrElse("No commitment"),
padLabel("Ephemeral-Metadata"),
asset.ephemeralMetadata.map(meta => meta.display).getOrElse("No ephemeral metadata")
).mkString("\n")

implicit val assetMintingStatementDisplay: DisplayOps[AssetMintingStatement] = (ams: AssetMintingStatement) =>
Seq(
padLabel("Group-Token-Utxo") + ams.groupTokenUtxo.display,
padLabel("Series-Token-Utxo") + ams.seriesTokenUtxo.display,
padLabel("Quantity") + (ams.quantity: BigInt).toString,
padLabel("Permanent-Metadata"),
ams.permanentMetadata.map(meta => meta.display).getOrElse("No permanent metadata")
).mkString("\n")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package co.topl.brambl.display

import co.topl.brambl.utils.Encoding
import co.topl.consensus.models.BlockId

trait BlockDisplayOps {

implicit val blockIdDisplay: DisplayOps[BlockId] = (blockId: BlockId) =>
Encoding.encodeToBase58(blockId.value.toByteArray())
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package co.topl.brambl.display

import co.topl.brambl.display.DisplayOps.DisplayTOps
import co.topl.brambl.models.{Datum, GroupId, SeriesId}
import co.topl.brambl.utils.Encoding
import co.topl.brambl.models.box.Value

trait GroupDisplayOps {
implicit val groupIdDisplay: DisplayOps[GroupId] = (id: GroupId) => Encoding.encodeToHex(id.value.toByteArray())

implicit val groupPolicyDisplay: DisplayOps[Datum.GroupPolicy] = (gp: Datum.GroupPolicy) =>
Seq(
padLabel("Label") + gp.event.label,
padLabel("Registration-Utxo") + gp.event.registrationUtxo.display,
padLabel("Fixed-Series") + displayFixedSeries(gp.event.fixedSeries)
).mkString("\n")

implicit val groupDisplay: DisplayOps[Value.Group] = (group: Value.Group) =>
Seq(
"Group Constructor",
padLabel("Id") + group.groupId.display,
padLabel("Fixed-Series") + displayFixedSeries(group.fixedSeries)
).mkString("\n")

private def displayFixedSeries(fixedSeries: Option[SeriesId]): String =
fixedSeries.map(sId => sId.display).getOrElse("NO FIXED SERIES")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package co.topl.brambl.display

import co.topl.brambl.display.DisplayOps.DisplayTOps
import co.topl.brambl.models.{Datum, SeriesId}
import co.topl.brambl.models.box.{FungibilityType, QuantityDescriptorType}
import co.topl.brambl.utils.Encoding
import co.topl.brambl.models.box.Value

trait SeriesDisplayOps {

implicit val seriesIdDisplay: DisplayOps[SeriesId] = (id: SeriesId) => Encoding.encodeToHex(id.value.toByteArray())

implicit val fungibilityDisplay: DisplayOps[FungibilityType] = {
case FungibilityType.GROUP_AND_SERIES => "group-and-series"
case FungibilityType.GROUP => "group"
case FungibilityType.SERIES => "series"
case _ => throw new Exception("Unknown fungibility type") // this should not happen
}

implicit val quantityDescriptorDisplay: DisplayOps[QuantityDescriptorType] = {
case QuantityDescriptorType.LIQUID => "liquid"
case QuantityDescriptorType.ACCUMULATOR => "accumulator"
case QuantityDescriptorType.FRACTIONABLE => "fractionable"
case QuantityDescriptorType.IMMUTABLE => "immutable"
case _ => throw new Exception("Unknown quantity descriptor type") // should not happen
}

implicit val seriesPolicyDisplay: DisplayOps[Datum.SeriesPolicy] = (sp: Datum.SeriesPolicy) =>
Seq(
padLabel("Label") + sp.event.label,
padLabel("Registration-Utxo") + sp.event.registrationUtxo.display,
padLabel("Fungibility") + sp.event.fungibility.display,
padLabel("Quantity-Descriptor") + sp.event.quantityDescriptor.display,
padLabel("Token-Supply") + displayTokenSupply(sp.event.tokenSupply),
padLabel("Permanent-Metadata-Scheme"),
sp.event.permanentMetadataScheme.map(meta => meta.display).getOrElse("No permanent metadata"),
padLabel("Ephemeral-Metadata-Scheme"),
sp.event.ephemeralMetadataScheme.map(meta => meta.display).getOrElse("No ephemeral metadata")
).mkString("\n")

implicit val seriesDisplay: DisplayOps[Value.Series] = (series: Value.Series) =>
Seq(
"Series Constructor",
padLabel("Id") + series.seriesId.display,
padLabel("Fungibility") + series.fungibility.display,
padLabel("Token-Supply") + displayTokenSupply(series.tokenSupply),
padLabel("Quant-Descr.") + series.quantityDescriptor.display
).mkString("\n")

private def displayTokenSupply(tokenSupply: Option[Int]): String =
tokenSupply.map(_.toString).getOrElse("UNLIMITED")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package co.topl.brambl.display

import com.google.protobuf.struct.{Struct, Value}
import com.google.protobuf.struct.Value.Kind.{
BoolValue,
Empty,
ListValue,
NullValue,
NumberValue,
StringValue,
StructValue
}

trait StructDisplayOps {
private val Indent = 2
private val InitialIndent = Indent

implicit val structDisplay: DisplayOps[Struct] = (struct: Struct) =>
" " * InitialIndent + display(struct, InitialIndent)

private def display(struct: Struct, indent: Int): String =
struct.fields.view.keys
.map({ key =>
struct.fields(key).kind match {
case StructValue(s) =>
s"$key:\n" + " " * (indent + Indent) + s"${display(s, indent + Indent)}"
case ListValue(l) =>
s"$key:\n" + l.values
.map(s => " " * (indent + Indent) + s"-${display(s, 0)}")
.mkString("\n")
case _ => s"$key: ${display(struct.fields(key), indent)}"
}
})
.mkString("\n" + " " * indent)

private def display(v: Value, indent: Int): String = v match {
case Value(NullValue(_), _) => "null"
case Value(Empty, _) => "empty"
case Value(BoolValue(b), _) => b.toString()
case Value(NumberValue(n), _) => n.toString()
case Value(StringValue(s), _) => s
case Value(ListValue(l), _) =>
l.values.map(s => " " * indent + s"- ${display(s, 0)}").mkString("\n")
case Value(StructValue(s), _) =>
" " * indent + display(s, indent)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package co.topl.brambl.display

import co.topl.brambl.display.DisplayOps.DisplayTOps
import co.topl.brambl.models.TransactionOutputAddress
import co.topl.brambl.models.transaction.SpentTransactionOutput
import co.topl.brambl.utils.Encoding
import co.topl.genus.services.Txo

trait StxoDisplayOps {

implicit val stxoDisplay: DisplayOps[SpentTransactionOutput] = (stxo: SpentTransactionOutput) =>
Seq(
padLabel("TxoAddress") + stxo.address.display,
padLabel("Attestation") + "Not implemented",
stxo.value.value.display
).mkString("\n")

implicit val txoAddressDisplay: DisplayOps[TransactionOutputAddress] = (txoAddress: TransactionOutputAddress) =>
s"${Encoding.encodeToBase58(txoAddress.id.value.toByteArray())}#${txoAddress.index}"

implicit val txoDisplay: DisplayOps[Txo] = (txo: Txo) =>
Seq(
padLabel("TxoAddress") + txo.outputAddress.display,
padLabel("LockAddress") + txo.transactionOutput.address.display,
txo.transactionOutput.value.value.display
).mkString("\n")

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package co.topl.brambl.display

import co.topl.brambl.display.DisplayOps.DisplayTOps
import co.topl.brambl.models.TransactionId
import co.topl.brambl.models.transaction.IoTransaction
import co.topl.brambl.syntax.ioTransactionAsTransactionSyntaxOps
import co.topl.brambl.utils.Encoding

trait TransactionDisplayOps {

implicit val transactionIdDisplay: DisplayOps[TransactionId] = (id: TransactionId) =>
Encoding.encodeToBase58(id.value.toByteArray())

implicit val transactionDisplay: DisplayOps[IoTransaction] = (tx: IoTransaction) =>
s"""
${padLabel("TransactionId")}${tx.transactionId.getOrElse(tx.computeId).display}

Group Policies
==============
${tx.groupPolicies.map(gp => gp.display).mkString("\n-----------\n")}

Series Policies
===============
${tx.seriesPolicies.map(sp => sp.display).mkString("\n-----------\n")}

Asset Minting Statements
========================
${tx.mintingStatements.map(ams => ams.display).mkString("\n-----------\n")}

Inputs
======
${if (tx.inputs.isEmpty) ("No inputs")
else tx.inputs.map(stxo => stxo.display).mkString("\n-----------\n")}

Outputs
=======
${if (tx.outputs.isEmpty) ("No outputs")
else tx.outputs.map(utxo => utxo.display).mkString("\n-----------\n")}

Datum
=====
${padLabel("Value")}${Encoding.encodeToBase58(tx.datum.event.metadata.value.toByteArray())}
"""
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package co.topl.brambl.display

import co.topl.brambl.codecs.AddressCodecs
import co.topl.brambl.display.DisplayOps.DisplayTOps
import co.topl.brambl.models.LockAddress
import co.topl.brambl.models.transaction.UnspentTransactionOutput

trait UtxoDisplayOps {

implicit val utxoDisplay: DisplayOps[UnspentTransactionOutput] = (utxo: UnspentTransactionOutput) =>
Seq(
padLabel("LockAddress") + utxo.address.display,
utxo.value.value.display
).mkString("\n")

implicit val lockAddressDisplay: DisplayOps[LockAddress] = (lockAddress: LockAddress) =>
AddressCodecs.encodeAddress(lockAddress)

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package co.topl.brambl.display

import co.topl.brambl.display.DisplayOps.DisplayTOps
import co.topl.brambl.models.box.Value
import co.topl.brambl.models.box.Value.Value._
import co.topl.brambl.syntax.{int128AsBigInt, valueToQuantitySyntaxOps}

import scala.util.{Failure, Success, Try}

trait ValueDisplayOps {

implicit val valueDisplay: DisplayOps[Value.Value] = (value: Value.Value) =>
Seq(typeDisplay(value), quantityDisplay(value)).mkString("\n")

def typeDisplay(value: Value.Value): String = {
val vType = value match {
case Lvl(_) => "LVL"
case Group(g) => g.display
case Series(s) => s.display
case Asset(a) => a.display
case Topl(_) => "TOPL"
case _ => "Unknown txo type"
}
padLabel("Type") + vType
}

def quantityDisplay(value: Value.Value): String = {
val quantity = Try {
value.quantity
} match {
case Success(asInt128) => (asInt128: BigInt).toString()
case Failure(_) => "Undefine type"
}
padLabel("Value") + quantity
}
}
32 changes: 32 additions & 0 deletions brambl-sdk/src/main/scala/co/topl/brambl/display/package.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package co.topl.brambl

package object display
extends UtxoDisplayOps
with StxoDisplayOps
with ValueDisplayOps
with StructDisplayOps
with AssetDisplayOps
with GroupDisplayOps
with SeriesDisplayOps
with TransactionDisplayOps
with BlockDisplayOps {

val LabelLength = 27

def padLabel(label: String): String = {
val padding = " " * (LabelLength - label.length).max(0)
s"${label}${padding}: "
}

trait DisplayOps[T] {
def display(t: T): String
}

object DisplayOps {
def apply[T](implicit ev: DisplayOps[T]): DisplayOps[T] = ev

implicit class DisplayTOps[T: DisplayOps](t: T) {
def display: String = DisplayOps[T].display(t)
}
}
}
Loading

0 comments on commit 27cdf2d

Please sign in to comment.