diff --git a/build.sbt b/build.sbt index 1b10d6aa0..d5bf47888 100644 --- a/build.sbt +++ b/build.sbt @@ -451,6 +451,7 @@ lazy val scalusExamples = crossProject(JSPlatform, JVMPlatform) scalacOptions ++= commonScalacOptions, publish / skip := true, libraryDependencies += "io.bullet" %%% "borer-derivation" % "1.16.1", + libraryDependencies += "com.lihaoyi" %%% "pprint" % "0.9.4", libraryDependencies += "com.softwaremill.magnolia1_3" %%% "magnolia" % "1.3.18" % "test", libraryDependencies += "org.scalatest" %%% "scalatest" % "3.2.19" % "test", libraryDependencies += "org.scalatestplus" %%% "scalacheck-1-18" % "3.2.19.0" % "test" diff --git a/scalus-cardano-ledger/shared/src/main/scala/scalus/cardano/txbuilder/TransactionBuilder.scala b/scalus-cardano-ledger/shared/src/main/scala/scalus/cardano/txbuilder/TransactionBuilder.scala index 585be8d6a..b3ebe7aa7 100644 --- a/scalus-cardano-ledger/shared/src/main/scala/scalus/cardano/txbuilder/TransactionBuilder.scala +++ b/scalus-cardano-ledger/shared/src/main/scala/scalus/cardano/txbuilder/TransactionBuilder.scala @@ -29,14 +29,14 @@ import scalus.cardano.txbuilder.TransactionBuilder.{Operation, WitnessKind} import scalus.cardano.txbuilder.modifyWs import TransactionWitnessSet.given -// Type alias for compatibility - DiffHandler is now a function type in new Scalus API -type DiffHandler = (Long, Transaction) => Either[TxBalancingError, Transaction] - import scalus.|> import scala.annotation.tailrec import scala.collection.immutable.SortedMap +// Type alias for compatibility - DiffHandler is now a function type in new Scalus API +type DiffHandler = (Long, Transaction) => Either[TxBalancingError, Transaction] + // =================================== // Tx Builder steps // =================================== @@ -559,6 +559,14 @@ object TransactionBuilder: private val unsafeCtxWitnessL: Lens[Context, TransactionWitnessSet] = Focus[Context](_.transaction).refocus(_.witnessSet) + /** Modifications of tx's outputs (so far) is relatively "safe" operation in terms that it can't + * break the transaction validity as long as outputs are correct. Moreover, the DiffHandler to + * some extend does the same thing, so this lens is the only way to manually edit the tx' + * outputs in the context, which may be useful together with [[modify]]. + */ + val unsafeCtxTxOutputsL: Lens[Context, IndexedSeq[Sized[TransactionOutput]]] = + Focus[Context](_.transaction) >>> txOutputsL + /** Update the given transaction output to have the minimum required ada, only changing its * Coin. */ @@ -2172,6 +2180,10 @@ def txInputsL: Lens[Transaction, TaggedSortedSet[TransactionInput]] = { txBodyL.refocus(_.inputs) } +def txOutputsL: Lens[Transaction, IndexedSeq[Sized[TransactionOutput]]] = { + txBodyL.refocus(_.outputs) +} + def txReferenceInputsL: Lens[Transaction, TaggedSortedSet[TransactionInput]] = { txBodyL.refocus(_.referenceInputs) } diff --git a/scalus-examples/jvm/src/main/scala/scalus/examples/txbuilder/TransactionBuilderDemo.scala b/scalus-examples/jvm/src/main/scala/scalus/examples/txbuilder/TransactionBuilderDemo.scala new file mode 100644 index 000000000..a6fd9f836 --- /dev/null +++ b/scalus-examples/jvm/src/main/scala/scalus/examples/txbuilder/TransactionBuilderDemo.scala @@ -0,0 +1,186 @@ +package scalus.examples.txbuilder + +import com.bloxbean.cardano.client.util.HexUtil +import org.scalacheck.Arbitrary.arbitrary +import org.scalacheck.Gen +import org.scalatest.funsuite.AnyFunSuite +import scalus.cardano.address.Network.Mainnet +import scalus.cardano.address.{Address, Network, ShelleyAddress, ShelleyDelegationPart} +import scalus.cardano.ledger.* +import scalus.cardano.ledger.ArbitraryInstances.given +import scalus.cardano.ledger.rules.* +import scalus.cardano.ledger.rules.STS.Validator +import scalus.cardano.txbuilder.LowLevelTxBuilder.ChangeOutputDiffHandler +import scalus.cardano.txbuilder.TransactionBuilder.ensureMinAda +import scalus.cardano.txbuilder.TransactionBuilderStep.{Send, Spend} +import scalus.cardano.txbuilder.{PubKeyWitness, TransactionBuilder, TransactionUnspentOutput} +import scalus.examples.txbuilder.Generators.* +import scalus.uplc.eval.ExBudget + +import scala.sys.process.* + +class TransactionBuilderDemo extends AnyFunSuite { + + test("Empty tx") { + trace { + TransactionBuilder.build(Mainnet, List.empty) + } + } + + private def genUtxos = + val utxos = Gen.listOfN(2, genAdaUtxo()).sample.get + val additionalUtxo = genAdaUtxoAt(utxos.head.output.address).sample.get + utxos :+ additionalUtxo + + private def stage1 = { + val inputs = genUtxos + val output = TransactionOutput.apply( + address = genPubKeyAddr().sample.get, + value = inputs.foldLeft(Value.zero)((acc, o) => acc + o.output.value) + ) + TransactionBuilder.build(Mainnet, inputs.map(Spend(_, PubKeyWitness)) :+ Send(output)) + } + + test("Start building") { + trace { + stage1 + } + } + + private def stage2 = { + val ctx = stage1.getOrElse(???) + val feeUtxo = genAdaUtxo().sample.get + TransactionBuilder.modify(ctx, List(Spend(feeUtxo, PubKeyWitness), Send(feeUtxo.output))) + } + + test("Use modify for further building stages") { + trace { + stage2 + } + } + + private def balanced = { + val ctx: TransactionBuilder.Context = stage2.getOrElse(???) + ctx.balance( + ChangeOutputDiffHandler(testProtocolParams, 1).changeOutputDiffHandler, + testProtocolParams, + testEvaluator + ) + } + + test("Balancing") { + trace { + balanced + } + } + + val testValidators: Seq[Validator] = + // These validators are used to check an unsigned transaction + List( + EmptyInputsValidator, + InputsAndReferenceInputsDisjointValidator, + AllInputsMustBeInUtxoValidator, + ValueNotConservedUTxOValidator, + // VerifiedSignaturesInWitnessesValidator, + // MissingKeyHashesValidator, + // MissingOrExtraScriptHashesValidator, + TransactionSizeValidator, + FeesOkValidator, + OutputsHaveNotEnoughCoinsValidator, + OutputsHaveTooBigValueStorageSizeValidator, + OutsideValidityIntervalValidator, + OutsideForecastValidator + ) + + test("Validate balanced tx") { + trace { + val ctx = balanced.getOrElse(???) + ctx.validate(testValidators :+ MissingKeyHashesValidator, testProtocolParams) + + } + } + + // =================================== + // helpers + // =================================== + def trace(testFun: => Either[Any, TransactionBuilder.Context]): Any = { + testFun match { + case Left(any) => + pprint.pprintln(any) + assert(false) + case Right(ctx) => + dumpTx(ctx.transaction) + dumpCtx(ctx) + } + } + + private def dumpTx(transaction: Transaction): Unit = { + val cborHex = HexUtil.encodeHexString(transaction.toCbor) + println(s"CBOR Hex: $cborHex") + dumpCborDiag(cborHex) + } + + private def dumpCborDiag(cborHex: String): Unit = { + try { + val result = (s"echo $cborHex" #| "/home/euonymos/.cargo/bin/cbor-diag").!! + println + println("Diagnostic notation:") + println(result) + } catch { + case e: Exception => + println(s"Failed to run cbor-diag: ${e.getMessage}") + } + } + + private def dumpCtx(ctx: TransactionBuilder.Context): Unit = { + val indentedPrinter = pprint.PPrinter(defaultIndent = 4) + + println("Context.expectedSigners:") + indentedPrinter.pprintln(ctx.expectedSigners) + // println("Context.resolvedUtxos:") + // indentedPrinter.pprintln(ctx.resolvedUtxos) } + } +} + +object Generators { + + import scalus.cardano.address.ShelleyPaymentPart.Key + + def genAdaUtxo( + network: Network = Mainnet + ): Gen[TransactionUnspentOutput] = for { + address <- genPubKeyAddr(network) + res <- genAdaUtxoAt(address) + } yield res + + def genAdaUtxoAt(address: Address): Gen[TransactionUnspentOutput] = for { + utxoId <- arbitrary[TransactionInput] + coin <- arbitrary[Coin] + } yield TransactionUnspentOutput( + utxoId, + ensureMinAda( + TransactionOutput.apply( + address = address, + value = Value(coin) + ), + testProtocolParams + ) + ) + + def genPubKeyAddr( + network: Network = Mainnet, + delegation: ShelleyDelegationPart = ShelleyDelegationPart.Null + ): Gen[ShelleyAddress] = + arbitrary[AddrKeyHash].flatMap(akh => + ShelleyAddress(network = network, payment = Key(akh), delegation = delegation) + ) +} + +val testProtocolParams = CardanoInfo.mainnet.protocolParams + +val testEvaluator = PlutusScriptEvaluator( + slotConfig = SlotConfig.Mainnet, + initialBudget = ExBudget.enormous, + protocolMajorVersion = MajorProtocolVersion.plominPV, + costModels = testProtocolParams.costModels +)