Skip to Content
Scalus Club is now open! Join us to get an early access to new features 🎉
DocumentationTransactionsFirst Contract Transaction

How to Use a Compiled Validator in Transactions

This guide shows how to lock funds at a script address and spend them using a compiled validator. It builds on the First Transaction guide, adding script interactions with datums and redeemers.

Compile the Validator

Start with a simple spending validator that checks whether the transaction is signed by the owner stored in the datum:

import scalus.Compiler.compile import scalus.uplc.builtin.Data import scalus.cardano.onchain.plutus.v3.* import scalus.cardano.onchain.plutus.prelude.* import scalus.uplc.Compiled.* object OwnerValidator { val validate = compile { (datum: Data, redeemer: Data, ctxData: Data) => val ctx = ctxData.to[ScriptContext] val owner = datum.to[PubKeyHash] val signatories = ctx.txInfo.signatories List.findOrFail(signatories) { sig => sig.hash == owner.hash } } }

Compile it to a Plutus V3 script:

import scalus.uplc.Compiled.* val compiled = PlutusV3.compile(OwnerValidator.validate)

PlutusV3.compile returns a PlutusV3[A] — a compiled script that carries both the on-chain Plutus script and the SIR (Scalus Intermediate Representation) used for diagnostic replay.

Lock Funds at the Script Address

Send ADA to the script address with an inline datum. The datum stores the owner’s public key hash — whoever holds the corresponding signing key can later unlock the funds:

import scalus.cardano.txbuilder.* import scalus.cardano.address.Address.addr given CardanoInfo = CardanoInfo.preprod val ownerPkh: PubKeyHash = // ... owner's public key hash val sponsorAddress = addr"addr_test1qz..." val lockTx = txBuilder .payTo(compiled, Value.ada(10), ownerPkh) // 10 ADA with inline datum .complete(provider, sponsorAddress) .await(30.seconds) .sign(signer) .transaction

The payTo(compiled, value, datum) overload derives the script address automatically and attaches the datum as an inline datum on the output. It also registers the debug script, enabling diagnostic replay if the script fails — the same behavior as spend(utxo, redeemer, compiled).

payTo also accepts a plain Address instead of a CompiledPlutus — use payTo(address, value, datum) when you need a manually constructed address or when sending to a non-script address.

Spend from the Script Address

To spend a UTxO locked at the script address, provide a redeemer and the compiled script. Pass the compiled object directly — not compiled.script:

val lockedUtxo: Utxo = // ... the UTxO locked at the script address val spendTx = txBuilder .spend(lockedUtxo, (), compiled, Set(ownerPkh.hash)) // redeemer is Unit, owner must sign .complete(provider, sponsorAddress) .await(30.seconds) .sign(signer) .transaction

Why pass compiled instead of compiled.script?

The spend overload accepting CompiledPlutus registers a debug script automatically. If the on-chain script was compiled with Options.release (no error traces) and fails during evaluation, TxBuilder recompiles from SIR with traces enabled and replays the execution — giving you a detailed error message instead of a cryptic failure.

You can use compiled.script and a raw script address, but you lose this diagnostic replay capability.

Complete Example

Here’s the full lock-then-spend flow:

import scalus.Compiler.compile import scalus.uplc.builtin.Data import scalus.cardano.address.Address.addr import scalus.cardano.ledger.* import scalus.cardano.node.BlockfrostProvider import scalus.cardano.txbuilder.* import scalus.cardano.onchain.plutus.v3.* import scalus.cardano.onchain.plutus.prelude.* import scalus.uplc.Compiled.* import scalus.utils.await import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.duration.* // 1. Compile the validator val compiled = PlutusV3.compile(compile { (datum: Data, redeemer: Data, ctxData: Data) => val ctx = ctxData.to[ScriptContext] val owner = datum.to[PubKeyHash] val signatories = ctx.txInfo.signatories List.findOrFail(signatories) { sig => sig.hash == owner.hash } }) // 2. Setup given CardanoInfo = CardanoInfo.preprod val provider = BlockfrostProvider.preprod("your-api-key").await(30.seconds) val sponsorAddress = addr"addr_test1qz..." val ownerPkh: PubKeyHash = // ... owner's public key hash // 3. Lock funds at the script address val lockTx = txBuilder .payTo(compiled, Value.ada(10), ownerPkh) .complete(provider, sponsorAddress) .await(30.seconds) .sign(signer) .transaction provider.submit(lockTx).await(30.seconds) // 4. Query the locked UTxO val scriptUtxos = provider.findUtxos(compiled.address(cardanoInfo.network)).await(30.seconds).getOrElse(Map.empty) val lockedUtxo = scriptUtxos.head // (Input, Output) pair // 5. Spend from the script address val spendTx = txBuilder .spend(lockedUtxo, (), compiled, Set(ownerPkh.hash)) .complete(provider, sponsorAddress) .await(30.seconds) .sign(signer) .transaction provider.submit(spendTx).await(30.seconds)

Next Steps

  • Spending UTxOs — Manual input selection and script UTxO patterns
  • Minting & Burning — Create and destroy native tokens using minting policies
  • HTLC Tutorial — End-to-end Hash Time-Locked Contract with transactions and tests
  • Debugging Guide — Deep dive into debugging failed script execution
Last updated on