All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.plasmalabs.sdk.builders.TransactionBuilderApi.scala Maven / Gradle / Ivy

The newest version!
package org.plasmalabs.sdk.builders

import cats.Monad
import cats.data.EitherT
import cats.implicits._
import org.plasmalabs.sdk.builders.UserInputValidations.TransactionBuilder._
import org.plasmalabs.sdk.codecs.AddressCodecs
import org.plasmalabs.sdk.common.ContainsEvidence.Ops
import org.plasmalabs.sdk.common.ContainsImmutable.instances._
import org.plasmalabs.sdk.models.{GroupPolicy, SeriesPolicy}
import org.plasmalabs.sdk.models._
import org.plasmalabs.sdk.models.box.Value.{Value => BoxValue}
import org.plasmalabs.sdk.models.box._
import org.plasmalabs.sdk.models.transaction.{IoTransaction, Schedule, SpentTransactionOutput, UnspentTransactionOutput}
import org.plasmalabs.sdk.syntax.{
  bigIntAsInt128,
  groupPolicyAsGroupPolicySyntaxOps,
  int128AsBigInt,
  longAsInt128,
  seriesPolicyAsSeriesPolicySyntaxOps,
  valueToQuantitySyntaxOps,
  valueToTypeIdentifierSyntaxOps,
  LvlType,
  UnknownType,
  ValueTypeIdentifier
}
import org.plasmalabs.indexer.services.Txo
import com.google.protobuf.ByteString
import com.google.protobuf.struct.Struct
import org.plasmalabs.quivr.models.{Int128, Proof, SmallData}
import scala.collection.immutable._
import scala.language.implicitConversions
import scala.util.{Failure, Success, Try}
import org.plasmalabs.sdk.models.LockAddress

/**
 * Defines a builder for IoTransaction
 */
trait TransactionBuilderApi[F[_]] {

  /**
   * Builds an unproven attestation for the given predicate
   *
   * @param lockPredicate The predicate to use to build the unproven attestation
   * @return An unproven attestation
   */
  def unprovenAttestation(lockPredicate: Lock.Predicate): F[Attestation]

  /**
   * Builds a lock address for the given lock
   *
   * @param lock The lock to use to build the lock address
   * @return A lock address
   */
  def lockAddress(lock: Lock): F[LockAddress]

  /**
   * Builds a lvl unspent transaction output for the given predicate lock and amount
   *
   * @param predicate The predicate to use to build the lvl output
   * @param amount The amount to use to build the lvl output
   * @return An unspent transaction output containing lvls
   */
  def lvlOutput(predicate: Lock.Predicate, amount: Int128): F[UnspentTransactionOutput]

  /**
   * Builds a lvl unspent transaction output for the given lock address and amount
   *
   * @param lockAddress The lock address to use to build the lvl output
   * @param amount The amount to use to build the lvl output
   * @return An unspent transaction output containing lvls
   */
  def lvlOutput(lockAddress: LockAddress, amount: Int128): F[UnspentTransactionOutput]

  /**
   * Builds a group constructor unspent transaction output for the given parameters
   * @param lockAddress The lock address to use to build the group constructor output
   * @param quantity The quantity to use to build the group constructor output
   * @param groupId The group id to use to build the group constructor output
   * @param fixedSeries The fixed series to use to build the group constructor output
   * @return An unspent transaction output containing group constructor tokens
   */
  def groupOutput(
    lockAddress: LockAddress,
    quantity:    Int128,
    groupId:     GroupId,
    fixedSeries: Option[SeriesId]
  ): F[UnspentTransactionOutput]

  /**
   * Builds a series constructor unspent transaction output for the given parameters
   * @param lockAddress The lock address to use to build the series constructor output
   * @param quantity The quantity to use to build the series constructor output
   * @param seriesId The series id to use to build the series constructor output
   * @param tokenSupply The token supply to use to build the series constructor output
   * @param fungibility The fungibility type to use to build the series constructor output
   * @param quantityDescriptor The quantity descriptor type to use to build the series constructor output
   * @return
   */
  def seriesOutput(
    lockAddress:        LockAddress,
    quantity:           Int128,
    seriesId:           SeriesId,
    tokenSupply:        Option[Int],
    fungibility:        FungibilityType,
    quantityDescriptor: QuantityDescriptorType
  ): F[UnspentTransactionOutput]

  /**
   * Builds an asset unspent transaction output for the given parameters
   * @param lockAddress The lock address to use to build the asset output
   * @param quantity The quantity to use to build the asset output
   * @param groupId The group id to use to build the asset output
   * @param seriesId The series id to use to build the asset output
   * @param fungibilityType The fungibility type to use to build the asset output
   * @param quantityDescriptorType The quantity descriptor type to use to build the asset output
   * @param metadata The metadata to use to build the asset output
   * @param commitment The commitment to use to build the asset output
   * @return An unspent transaction output containing asset tokens
   */
  def assetOutput(
    lockAddress:            LockAddress,
    quantity:               Int128,
    groupId:                GroupId,
    seriesId:               SeriesId,
    fungibilityType:        FungibilityType,
    quantityDescriptorType: QuantityDescriptorType,
    metadata:               Option[Struct],
    commitment:             Option[ByteString]
  ): F[UnspentTransactionOutput]

  /**
   * Builds a datum with default values for a transaction. The schedule is defaulted to use the current timestamp, with
   * min and max slot being 0 and Long.MaxValue respectively.
   *
   * @return A transaction datum
   */
  def datum(
    groupPolicies:       Seq[GroupPolicy] = Seq.empty,
    seriesPolicies:      Seq[SeriesPolicy] = Seq.empty,
    mintingStatements:   Seq[AssetMintingStatement] = Seq.empty,
    mergingStatements:   Seq[AssetMergingStatement] = Seq.empty,
    splittingStatements: Seq[AssetSplittingStatement] = Seq.empty
  ): F[Datum.IoTransaction]

  /**
   * Builds a simple lvl transaction with the given parameters
   *
   * @param lvlTxos The lvl transaction outputs that are able to be spent in the transaction
   * @param lockPredicateFrom The predicate to use to build the transaction input
   * @param lockPredicateForChange The predicate to use to build the transaction change output
   * @param recipientLockAddress The lock address to use to build the transaction recipient output
   * @param amount The amount to use to build the transaction recipient output
   * @return A simple lvl transaction
   */
  def buildSimpleLvlTransaction(
    lvlTxos:                Seq[Txo],
    lockPredicateFrom:      Lock.Predicate,
    lockPredicateForChange: Lock.Predicate,
    recipientLockAddress:   LockAddress,
    amount:                 Long
  ): F[IoTransaction]

  /**
   * Builds a transaction to transfer the ownership of tokens (optionally identified by tokenIdentifier). If
   * tokenIdentifier is provided, only the TXOs matching the identifier will go to the recipient. If it is None, then
   * all tokens provided in txos will go to the recipient. Any remaining tokens in txos that are not transferred to the
   * recipient will be transferred to the change address.
   *
   * @param txos All the TXOs encumbered by the Lock given by lockPredicateFrom. These TXOs must contain some token
   *             matching tokenIdentifier (if it is provided) and at least the quantity of LVLs to satisfy the fee. Else
   *             an error will be returned. Any TXOs that contain values of an invalid type, such as UnknownType, will be
   *             filtered out and won't be included in the inputs.
   * @param lockPredicateFrom The Lock Predicate encumbering the txos
   * @param recipientLockAddress The LockAddress of the recipient
   * @param changeLockAddress A LockAddress to send the tokens that are not going to the recipient
   * @param fee The fee to pay for the transaction. The txos must contain enough LVLs to satisfy this fee
   * @param tokenIdentifier An optional token identifier to denote the type of token to transfer to the recipient. If
   *                        None, all tokens in txos will be transferred to the recipient and changeLockAddress will be
   *                        ignored. This must not be UnknownType.
   * @return An unproven transaction
   */
  def buildTransferAllTransaction(
    txos:                 Seq[Txo],
    lockPredicateFrom:    Lock.Predicate,
    recipientLockAddress: LockAddress,
    changeLockAddress:    LockAddress,
    fee:                  Long,
    tokenIdentifier:      Option[ValueTypeIdentifier] = None
  ): F[Either[BuilderError, IoTransaction]]

  /**
   * Builds a transaction to transfer a certain amount of a specified Token (given by tokenIdentifier). The transaction
   * will also transfer any other tokens (in the txos) that are encumbered by the same predicate to the change address.
   *
   * @note This function only supports transferring a specific amount of assets (via tokenIdentifier) if their quantity
   *       descriptor type is LIQUID.
   * @note This function only support transferring a specific amount of TOPLs (via tokenIdentifier) if their staking
   *       registration is None.
   * @param tokenIdentifier The Token Identifier denoting the type of token to transfer to the recipient. If this denotes
   *                        an Asset Token, the referenced asset's quantity descriptor type must be LIQUID, else an error
   *                        will be returned. This must not be UnknownType.
   * @param txos All the TXOs encumbered by the Lock given by lockPredicateFrom. These TXOs must contain at least the
   *             necessary quantity (given by amount) of the identified Token and at least the quantity of LVLs to
   *             satisfy the fee. Else an error will be returned. Any TXOs that contain values of an invalid type, such
   *             as UnknownType, will be filtered out and won't be included in the inputs.
   * @param lockPredicateFrom The Lock Predicate encumbering the txos
   * @param amount The amount of identified Token to transfer to the recipient
   * @param recipientLockAddress The LockAddress of the recipient
   * @param changeLockAddress A LockAddress to send the tokens that are not going to the recipient
   * @param fee              The transaction fee. The txos must contain enough LVLs to satisfy this fee
   * @return An unproven transaction
   */
  def buildTransferAmountTransaction(
    tokenIdentifier:      ValueTypeIdentifier,
    txos:                 Seq[Txo],
    lockPredicateFrom:    Lock.Predicate,
    amount:               Long,
    recipientLockAddress: LockAddress,
    changeLockAddress:    LockAddress,
    fee:                  Long
  ): F[Either[BuilderError, IoTransaction]]

  /**
   * Builds a simple transaction to mint Group Constructor tokens.
   * If successful, the transaction will have one or more inputs (at least the registrationUtxo) and one or more
   * outputs (at least the minted group constructor tokens). There can be more inputs and outputs if the supplied txos
   * contain more tokens.
   *
   * @param txos All the TXOs encumbered by the Lock given by lockPredicateFrom. These TXOs must contain some LVLs (as
   *             specified in the policy), to satisfy the registration fee. Else an error will be returned. Any TXOs
   *             that contain values of an invalid type, such as UnknownType, will be filtered out and won't be included
   *             in the inputs.
   * @param lockPredicateFrom The Predicate Lock that encumbers the funds in the txos. This will be used in
   *                         the attestations of the inputs.
   * @param groupPolicy The group policy for which we are minting constructor tokens. This group policy specifies a
   *                    registrationUtxo to be used as an input in this transaction.
   * @param quantityToMint The quantity of constructor tokens to mint
   * @param mintedAddress The LockAddress to send the minted constructor tokens to.
   * @param changeAddress The LockAddress to send the change to.
   * @param fee              The transaction fee. The txos must contain enough LVLs to satisfy this fee
   * @return An unproven Group Constructor minting transaction if possible. Else, an error
   */
  def buildGroupMintingTransaction(
    txos:              Seq[Txo],
    lockPredicateFrom: Lock.Predicate,
    groupPolicy:       GroupPolicy,
    quantityToMint:    Long,
    mintedAddress:     LockAddress,
    changeAddress:     LockAddress,
    fee:               Long
  ): F[Either[BuilderError, IoTransaction]]

  /**
   * Builds a simple transaction to mint Series Constructor tokens.
   * If successful, the transaction will have one or more inputs (at least the registrationUtxo) and one or more
   * outputs (at least the minted series constructor tokens). There can be more inputs and outputs if the supplied txos
   * contain more tokens.
   *
   * @param txos              All the TXOs encumbered by the Lock given by lockPredicateFrom. These TXOs must contain
   *                          some LVLs (as specified in the policy), to satisfy the registration fee. Else an error will
   *                          be returned. Any TXOs that contain values of an invalid type, such as UnknownType, will be
   *                          filtered out and won't be included in the inputs.
   * @param lockPredicateFrom The Predicate Lock that encumbers the funds in the txos. This will be used in
   *                          the attestations of the inputs.
   * @param seriesPolicy The series policy for which we are minting constructor tokens. This series policy specifies a
   *                    registrationUtxo to be used as an input in this transaction.
   * @param quantityToMint The quantity of constructor tokens to mint
   * @param mintedAddress The LockAddress to send the minted constructor tokens to.
   * @param changeAddress The LockAddress to send the change to.
   * @param fee           The transaction fee. The txos must contain enough LVLs to satisfy this fee
   * @return An unproven Series Constructor minting transaction if possible. Else, an error
   */
  def buildSeriesMintingTransaction(
    txos:              Seq[Txo],
    lockPredicateFrom: Lock.Predicate,
    seriesPolicy:      SeriesPolicy,
    quantityToMint:    Long,
    mintedAddress:     LockAddress,
    changeAddress:     LockAddress,
    fee:               Long
  ): F[Either[BuilderError, IoTransaction]]

  /**
   * Builds a simple transaction to mint asset tokens.
   * If successful, the transaction will have two or more inputs (at least the group and series registration tokens) and
   * two or more outputs (at least the minted asset tokens and the input group constructor token). There can be more
   * inputs and outputs if the supplied txos contain more tokens.
   *
   * @note If the "tokenSupply" field in the registration series constructor tokens is present, then the quantity of
   *       asset tokens to mint (defined in the AMS) has to be a multiple of this field, else an error will be returned.
   *       In this case, minting each multiple of "tokenSupply" quantity of assets will burn a single series constructor token.
   * @param mintingStatement      The minting statement that specifies the asset to mint.
   * @param txos                  All the TXOs encumbered by the Locks given by locks. These TXOs must contain some
   *                              group and series constructors (as referenced in the AMS) to satisfy the minting
   *                              requirements. Else an error will be returned. Any TXOs that contain values of an invalid
   *                              type, such as UnknownType, will be filtered out and won't be included in the inputs.
   * @param locks             A mapping of Predicate Locks that encumbers the funds in the txos. This will be used in the
   *                              attestations of the txos' inputs.
   * @param fee The transaction fee. The txos must contain enough LVLs to satisfy this fee
   * @param mintedAssetLockAddress The LockAddress to send the minted asset tokens to.
   * @param changeAddress The LockAddress to send the change to.
   * @param ephemeralMetadata     Optional ephemeral metadata to include in the minted asset tokens.
   * @param commitment            Optional commitment to include in the minted asset tokens.
   * @return An unproven asset minting transaction if possible. Else, an error
   */
  def buildAssetMintingTransaction(
    mintingStatement:       AssetMintingStatement,
    txos:                   Seq[Txo],
    locks:                  Map[LockAddress, Lock.Predicate],
    fee:                    Long,
    mintedAssetLockAddress: LockAddress,
    changeAddress:          LockAddress,
    ephemeralMetadata:      Option[Struct] = None,
    commitment:             Option[ByteString] = None
  ): F[Either[BuilderError, IoTransaction]]

  /**
   * Builds a transaction to merge distinct, but compatible, assets. If successful, the transaction will have one or more
   * outputs; the merged asset and, optionally, the change. The merged asset will contain the sum of the quantities of the
   * merged inputs. The change will contain the remaining tokens that were not merged into the merged asset.
   *
   * @note The assets to merge must be valid. To be valid, the assets must have the same fungibility type and quantity descriptor
   *       type. The fungibility type must be one of "GROUP" or "SERIES". If "GROUP", then the assets must share the same Group ID.
   *       If "SERIES", then the assets must share the same Series ID. Fields such as "commitment" and "ephermeralMetadata" do not
   *       carryover; if desired, these fields in the merged output can be specified using the "ephemeralMetadata" and "commitment"
   *       arguments.
   *
   * @param utxosToMerge The UTXOs to merge. These UTXOs must contain assets that are compatible to merge.
   * @param txos All the TXOs encumbered by the Locks given by locks. These represent the inputs of the transaction.
   * @param locks A mapping of Predicate Locks that encumbers the funds in the txos. This will be used in the attestations of the txos' inputs.
   * @param fee The transaction fee. The txos must contain enough LVLs to satisfy this fee
   * @param mergedAssetLockAddress The LockAddress to send the merged asset tokens to.
   * @param changeAddress The LockAddress to send any change to.
   * @param ephemeralMetadata Optional ephemeral metadata to include in the merged asset token.
   * @param commitment Optional commitment to include in the merged asset token.
   * @return An unproven asset merge transaction if possible. Else, an error
   */
  def buildAssetMergeTransaction(
    utxosToMerge:           Seq[TransactionOutputAddress],
    txos:                   Seq[Txo],
    locks:                  Map[LockAddress, Lock.Predicate],
    fee:                    Long,
    mergedAssetLockAddress: LockAddress,
    changeAddress:          LockAddress,
    ephemeralMetadata:      Option[Struct] = None,
    commitment:             Option[ByteString] = None
  ): F[Either[BuilderError, IoTransaction]]
}

object TransactionBuilderApi {

  object implicits {

    case class LockAddressOps(
      lockAddress: LockAddress
    ) {
      def toBase58(): String = AddressCodecs.encodeAddress(lockAddress)
    }

    implicit def lockAddressOps(
      lockAddress: LockAddress
    ): LockAddressOps = LockAddressOps(lockAddress)

  }

  def make[F[_]: Monad](
    networkId: Int,
    ledgerId:  Int
  ): TransactionBuilderApi[F] =
    new TransactionBuilderApi[F] {

      override def buildSimpleLvlTransaction(
        lvlTxos:                Seq[Txo],
        lockPredicateFrom:      Lock.Predicate,
        lockPredicateForChange: Lock.Predicate,
        recipientLockAddress:   LockAddress,
        amount:                 Long
      ): F[IoTransaction] = for {
        unprovenAttestationToProve <- unprovenAttestation(lockPredicateFrom)
        totalValues =
          lvlTxos
            .foldLeft(
              BigInt(0)
            )((acc, x) => acc + x.transactionOutput.value.value.quantity)
        datum                 <- datum()
        lvlOutputForChange    <- lvlOutput(lockPredicateForChange, totalValues - amount)
        lvlOutputForRecipient <- lvlOutput(recipientLockAddress, amount)
        ioTransaction = IoTransaction.defaultInstance
          .withInputs(
            lvlTxos.map(x =>
              SpentTransactionOutput(
                x.outputAddress,
                unprovenAttestationToProve,
                x.transactionOutput.value
              )
            )
          )
          .withOutputs(
            // If there is no change, we don't need to add it to the outputs
            if (totalValues - amount > 0)
              Seq(lvlOutputForRecipient) :+ lvlOutputForChange
            else
              Seq(lvlOutputForRecipient)
          )
          .withDatum(datum)
      } yield ioTransaction

      override def buildTransferAllTransaction(
        txos:              Seq[Txo],
        lockPredicateFrom: Lock.Predicate,
        recipientLockAddr: LockAddress,
        changeLockAddr:    LockAddress,
        fee:               Long,
        tokenIdentifier:   Option[ValueTypeIdentifier]
      ): F[Either[BuilderError, IoTransaction]] = (
        for {
          fromLockAddr <- EitherT.right(lockAddress(Lock().withPredicate(lockPredicateFrom)))
          filteredTxos = txos.filter(_.transactionOutput.value.value.typeIdentifier != UnknownType)
          _ <- EitherT
            .fromEither[F](validateTransferAllParams(filteredTxos, fromLockAddr, fee, tokenIdentifier))
            .leftMap(errs => UserInputErrors(errs.toList))
          stxoAttestation <- EitherT.right(unprovenAttestation(lockPredicateFrom))
          datum           <- EitherT.right(datum())
          stxos           <- buildStxos(filteredTxos, stxoAttestation)
          utxos           <- buildUtxos(filteredTxos, tokenIdentifier, None, recipientLockAddr, changeLockAddr, fee)
        } yield IoTransaction(inputs = stxos, outputs = utxos, datum = datum)
      ).value

      override def buildTransferAmountTransaction(
        transferType:      ValueTypeIdentifier,
        txos:              Seq[Txo],
        lockPredicateFrom: Lock.Predicate,
        amount:            Long,
        recipientLockAddr: LockAddress,
        changeLockAddr:    LockAddress,
        fee:               Long
      ): F[Either[BuilderError, IoTransaction]] = (
        for {
          fromLockAddr <- EitherT.right(lockAddress(Lock().withPredicate(lockPredicateFrom)))
          filteredTxos = txos.filter(_.transactionOutput.value.value.typeIdentifier != UnknownType)
          _ <- EitherT
            .fromEither[F](validateTransferAmountParams(filteredTxos, fromLockAddr, amount, transferType, fee))
            .leftMap(errs => UserInputErrors(errs.toList))
          stxoAttestation <- EitherT.right(unprovenAttestation(lockPredicateFrom))
          datum           <- EitherT.right(datum())
          stxos           <- buildStxos(filteredTxos, stxoAttestation)
          utxos <- buildUtxos(
            filteredTxos,
            transferType.some,
            BigInt(amount).some,
            recipientLockAddr,
            changeLockAddr,
            fee
          )
        } yield IoTransaction(inputs = stxos, outputs = utxos, datum = datum)
      ).value

      private def buildStxos(
        txos: Seq[Txo],
        att:  Attestation
      ): EitherT[F, BuilderError, Seq[SpentTransactionOutput]] =
        EitherT.rightT(txos.map(x => SpentTransactionOutput(x.outputAddress, att, x.transactionOutput.value)))

      private def buildUtxos(
        txos:             Seq[Txo],
        transferTypeOpt:  Option[ValueTypeIdentifier], // If not provided, then we are transferring all
        amount:           Option[BigInt], // If not provided, then we are transferring all
        recipientAddress: LockAddress,
        changeAddress:    LockAddress,
        fee:              Long
      ): EitherT[F, BuilderError, Seq[UnspentTransactionOutput]] = Try {
        val groupedValues = applyFee(fee, txos.map(_.transactionOutput.value.value).groupBy(_.typeIdentifier))
        val otherVals = (groupedValues -- transferTypeOpt).values.toSeq.flatMap(DefaultAggregationOps.aggregate)
        val (transferVals, changeVals) = transferTypeOpt match {
          // If transferTypeOpt is provided, then we need to calculate what goes to the recipient vs to change
          case Some(transferType) =>
            DefaultAggregationOps
              .aggregateWithChange(groupedValues.getOrElse(transferType, Seq.empty), amount)
              .map(_ ++ otherVals) // otherVals, in addition to the change, goes to the change address
          // If transferTypeOpt is not provided, then all of otherVals goes to the recipient
          case _ => (otherVals, Seq.empty)
        }

        val toRecipient = transferVals
          .map(Value.defaultInstance.withValue)
          .map(UnspentTransactionOutput(recipientAddress, _))
        val toChange = changeVals.map(Value.defaultInstance.withValue).map(UnspentTransactionOutput(changeAddress, _))
        toRecipient ++ toChange
      } match {
        case Success(utxos) => EitherT.rightT(utxos)
        case Failure(err) => EitherT.leftT(BuilderRuntimeError(s"Failed to build utxos. cause: ${err.getMessage}", err))
      }

      /**
       * Apply the fee to the LVL values.
       * Due to validation, we know that there are enough LVLs in the values to satisfy the fee.
       * If there are no LVLs, then we don't need to apply the fee.
       *
       * @param fee The fee to apply to the LVLs
       * @param values The values of the transaction's inputs.
       * @return The values with the LVLs aggregated together and reduced by the fee amount. If there are no LVLs, then
       *         the values are returned unchanged. In this case, we know that the fee is 0.
       */
      private def applyFee(
        fee:    Long,
        values: Map[ValueTypeIdentifier, Seq[BoxValue]]
      ): Map[ValueTypeIdentifier, Seq[BoxValue]] = values.get(LvlType) match {
        case Some(lvlVals) =>
          val newLvlVal = DefaultAggregationOps.aggregateWithChange(lvlVals, BigInt(fee).some)._2
          if (newLvlVal.isEmpty) values - LvlType
          else values + (LvlType -> newLvlVal)
        case _ => values
      }

      override def buildGroupMintingTransaction(
        txos:              Seq[Txo],
        lockPredicateFrom: Lock.Predicate,
        groupPolicy:       GroupPolicy,
        quantityToMint:    Long,
        mintedAddress:     LockAddress,
        changeAddress:     LockAddress,
        fee:               Long
      ): F[Either[BuilderError, IoTransaction]] = (
        for {
          registrationLockAddr <- EitherT.right[BuilderError](lockAddress(Lock().withPredicate(lockPredicateFrom)))
          filteredTxos = txos.filter(_.transactionOutput.value.value.typeIdentifier != UnknownType)
          _ <- EitherT
            .fromEither[F](
              validateConstructorMintingParams(
                filteredTxos,
                registrationLockAddr,
                groupPolicy.registrationUtxo,
                quantityToMint,
                fee
              )
            )
            .leftMap(errs => UserInputErrors(errs.toList))
          stxoAttestation <- EitherT.right[BuilderError](unprovenAttestation(lockPredicateFrom))
          stxos           <- buildStxos(filteredTxos, stxoAttestation)
          datum           <- EitherT.right[BuilderError](datum(groupPolicies = Seq(groupPolicy)))
          utxoMinted <- EitherT.right[BuilderError](
            groupOutput(mintedAddress, quantityToMint, groupPolicy.computeId, groupPolicy.fixedSeries)
          )
          utxoChange <- buildUtxos(filteredTxos, None, None, changeAddress, changeAddress, fee)
        } yield IoTransaction(
          inputs = stxos,
          outputs = utxoChange :+ utxoMinted,
          datum = datum
        )
      ).value

      override def buildSeriesMintingTransaction(
        txos:              Seq[Txo],
        lockPredicateFrom: Lock.Predicate,
        seriesPolicy:      SeriesPolicy,
        quantityToMint:    Long,
        mintedAddress:     LockAddress,
        changeAddress:     LockAddress,
        fee:               Long
      ): F[Either[BuilderError, IoTransaction]] = (
        for {
          registrationLockAddr <- EitherT.right[BuilderError](lockAddress(Lock().withPredicate(lockPredicateFrom)))
          filteredTxos = txos.filter(_.transactionOutput.value.value.typeIdentifier != UnknownType)
          _ <- EitherT
            .fromEither[F](
              validateConstructorMintingParams(
                filteredTxos,
                registrationLockAddr,
                seriesPolicy.registrationUtxo,
                quantityToMint,
                fee
              )
            )
            .leftMap(errs => UserInputErrors(errs.toList))
          stxoAttestation <- EitherT.right[BuilderError](unprovenAttestation(lockPredicateFrom))
          stxos           <- buildStxos(filteredTxos, stxoAttestation)
          datum           <- EitherT.right[BuilderError](datum(seriesPolicies = Seq(seriesPolicy)))
          utxoMinted <- EitherT.right[BuilderError](
            seriesOutput(
              mintedAddress,
              quantityToMint,
              seriesPolicy.computeId,
              seriesPolicy.tokenSupply,
              seriesPolicy.fungibility,
              seriesPolicy.quantityDescriptor
            )
          )
          utxoChange <- buildUtxos(filteredTxos, None, None, changeAddress, changeAddress, fee)
        } yield IoTransaction(
          inputs = stxos,
          outputs = utxoChange :+ utxoMinted,
          datum = datum
        )
      ).value

      private def toAttestationMap(
        txos:  Seq[Txo],
        locks: Map[LockAddress, Lock.Predicate]
      ): EitherT[F, BuilderError, Map[Seq[Txo], Attestation]] = {
        val txoMap = txos.groupBy(_.transactionOutput.address)
        // Per validation, we know that all txos have a lock within locks and all locks have a txo corresponding to them
        EitherT.right[BuilderError](
          locks.toSeq.map(el => (txoMap(el._1), unprovenAttestation(el._2)).sequence).sequence.map(_.toMap)
        )
      }

      override def buildAssetMintingTransaction(
        mintingStatement:       AssetMintingStatement,
        txos:                   Seq[Txo],
        locks:                  Map[LockAddress, Lock.Predicate],
        fee:                    Long,
        mintedAssetLockAddress: LockAddress,
        changeAddress:          LockAddress,
        ephemeralMetadata:      Option[Struct] = None,
        commitment:             Option[ByteString] = None
      ): F[Either[BuilderError, IoTransaction]] = (
        for {
          datum <- EitherT.right[BuilderError](datum(mintingStatements = Seq(mintingStatement)))
          filteredTxos = txos.filter(_.transactionOutput.value.value.typeIdentifier != UnknownType)
          _ <- EitherT
            .fromEither[F](validateAssetMintingParams(mintingStatement, filteredTxos, locks.keySet, fee))
            .leftMap(errs => UserInputErrors(errs.toList))
          attestations <- toAttestationMap(filteredTxos, locks)
          stxos        <- attestations.map(el => buildStxos(el._1, el._2)).toSeq.sequence.map(_.flatten)
          // Per validation, there is exactly one series token in txos
          (seriesTxo, nonSeriesTxo) = filteredTxos
            .partition(_.outputAddress == mintingStatement.seriesTokenUtxo)
            .leftMap(_.head)
          seriesUtxo = seriesTxo.transactionOutput
          seriesToken = seriesUtxo.value.getSeries
          // Per validation, there is exactly one group token in txos
          groupToken = filteredTxos
            .filter(_.outputAddress == mintingStatement.groupTokenUtxo)
            .head
            .transactionOutput
            .value
            .getGroup
          utxoMinted <- EitherT.right[BuilderError](
            assetOutput(
              mintedAssetLockAddress,
              mintingStatement.quantity,
              groupToken.groupId,
              seriesToken.seriesId,
              seriesToken.fungibility,
              seriesToken.quantityDescriptor,
              ephemeralMetadata,
              commitment
            )
          )
          seriesTxoAdjusted = {
            val inputQuantity = seriesToken.quantity
            val outputQuantity: BigInt =
              if (seriesToken.tokenSupply.isEmpty) inputQuantity
              else inputQuantity - (mintingStatement.quantity / seriesToken.tokenSupply.get)
            if (outputQuantity > 0)
              Seq(
                seriesTxo
                  .withTransactionOutput(
                    seriesUtxo
                      .withValue(
                        Value.defaultInstance
                          .withSeries(seriesToken.withQuantity(outputQuantity))
                      )
                  ) // Only the quantity changes
              )
            else Seq.empty[Txo]
          }
          changeOutputs <- buildUtxos(nonSeriesTxo ++ seriesTxoAdjusted, None, None, changeAddress, changeAddress, fee)
        } yield IoTransaction(
          inputs = stxos,
          outputs = changeOutputs :+ utxoMinted,
          datum = datum
        )
      ).value

      override def groupOutput(
        lockAddress: LockAddress,
        quantity:    Int128,
        groupId:     GroupId,
        fixedSeries: Option[SeriesId]
      ): F[UnspentTransactionOutput] =
        UnspentTransactionOutput(
          lockAddress,
          Value.defaultInstance.withGroup(
            Value.Group(groupId = groupId, quantity = quantity, fixedSeries = fixedSeries)
          )
        ).pure[F]

      override def seriesOutput(
        lockAddress:        LockAddress,
        quantity:           Int128,
        seriesId:           SeriesId,
        tokenSupply:        Option[Int],
        fungibility:        FungibilityType,
        quantityDescriptor: QuantityDescriptorType
      ): F[UnspentTransactionOutput] =
        UnspentTransactionOutput(
          lockAddress,
          Value.defaultInstance.withSeries(
            Value.Series(
              seriesId = seriesId,
              quantity = quantity,
              tokenSupply = tokenSupply,
              quantityDescriptor = quantityDescriptor,
              fungibility = fungibility
            )
          )
        ).pure[F]

      override def assetOutput(
        lockAddress:            LockAddress,
        quantity:               Int128,
        groupId:                GroupId,
        seriesId:               SeriesId,
        fungibilityType:        FungibilityType,
        quantityDescriptorType: QuantityDescriptorType,
        metadata:               Option[Struct],
        commitment:             Option[ByteString]
      ): F[UnspentTransactionOutput] =
        UnspentTransactionOutput(
          lockAddress,
          Value.defaultInstance.withAsset(
            Value.Asset(
              groupId = groupId.some,
              seriesId = seriesId.some,
              groupAlloy = None,
              seriesAlloy = None,
              quantity = quantity,
              fungibility = fungibilityType,
              quantityDescriptor = quantityDescriptorType,
              ephemeralMetadata = metadata,
              commitment = commitment
            )
          )
        ).pure[F]

      override def lvlOutput(
        lockAddress: LockAddress,
        amount:      Int128
      ): F[UnspentTransactionOutput] =
        UnspentTransactionOutput(
          lockAddress,
          Value.defaultInstance.withLvl(Value.LVL(amount))
        ).pure[F]

      override def lockAddress(
        lock: Lock
      ): F[LockAddress] =
        LockAddress(
          networkId,
          ledgerId,
          LockId(lock.sizedEvidence.digest.value)
        ).pure[F]

      override def lvlOutput(
        predicate: Lock.Predicate,
        amount:    Int128
      ): F[UnspentTransactionOutput] =
        UnspentTransactionOutput(
          LockAddress(
            networkId,
            ledgerId,
            LockId(Lock().withPredicate(predicate).sizedEvidence.digest.value)
          ),
          Value.defaultInstance.withLvl(Value.LVL(amount))
        ).pure[F]

      override def datum(
        groupPolicies:       Seq[GroupPolicy] = Seq.empty,
        seriesPolicies:      Seq[SeriesPolicy] = Seq.empty,
        mintingStatements:   Seq[AssetMintingStatement] = Seq.empty,
        mergingStatements:   Seq[AssetMergingStatement] = Seq.empty,
        splittingStatements: Seq[AssetSplittingStatement] = Seq.empty
      ): F[Datum.IoTransaction] =
        Datum
          .IoTransaction(
            Event.IoTransaction.defaultInstance
              .withSchedule(Schedule(0, Long.MaxValue, System.currentTimeMillis))
              .withMetadata(SmallData.defaultInstance)
              .withGroupPolicies(groupPolicies)
              .withSeriesPolicies(seriesPolicies)
              .withMintingStatements(mintingStatements)
              .withMergingStatements(mergingStatements)
              .withSplittingStatements(splittingStatements)
          )
          .pure[F]

      override def unprovenAttestation(
        predicate: Lock.Predicate
      ): F[Attestation] =
        Attestation(
          Attestation.Value.Predicate(
            Attestation.Predicate(
              predicate,
              List.fill(predicate.challenges.length)(Proof())
            )
          )
        ).pure[F]

      override def buildAssetMergeTransaction(
        utxosToMerge:           Seq[TransactionOutputAddress],
        txos:                   Seq[Txo],
        locks:                  Map[LockAddress, Lock.Predicate],
        fee:                    Long,
        mergedAssetLockAddress: LockAddress,
        changeAddress:          LockAddress,
        ephemeralMetadata:      Option[Struct],
        commitment:             Option[ByteString]
      ): F[Either[BuilderError, IoTransaction]] = (
        for {
          datum <- EitherT.right[BuilderError](datum())
          filteredTxos = txos.filter(_.transactionOutput.value.value.typeIdentifier != UnknownType)
          _ <- EitherT
            .fromEither[F](validateAssetMergingParams(utxosToMerge, filteredTxos, locks.keySet, fee))
            .leftMap(errs => UserInputErrors(errs.toList))
          attestations <- toAttestationMap(filteredTxos, locks)
          stxos        <- attestations.map(el => buildStxos(el._1, el._2)).toSeq.sequence.map(_.flatten)
          (txosToMerge, otherTxos) = filteredTxos.partition(txo => utxosToMerge.contains(txo.outputAddress))
          utxosChange <- buildUtxos(otherTxos, None, None, changeAddress, changeAddress, fee)
          mergedUtxo = MergingOps.merge(txosToMerge, mergedAssetLockAddress, ephemeralMetadata, commitment)
          asm = AssetMergingStatement(utxosToMerge, utxosChange.length)
          datumWithAssetMintingStatement = datum.copy(event = datum.event.copy(mergingStatements = Seq(asm)))
        } yield IoTransaction(
          inputs = stxos,
          outputs = utxosChange :+ mergedUtxo,
          datum = datumWithAssetMintingStatement
        )
      ).value
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy