Working with Contract
In this guide we cover how to compile your Scalus validators and integrate them with off-chain applications.
Compilation Pipeline
Scalus compiles your validator code through this pipeline:
Scala Source → SIR (compiler plugin) → UPLC → Plutus ScriptThe result is a PlutusV3[A] (or PlutusV1[A], PlutusV2[A]) object that wraps the compiled script and provides access to the program, script hash, and address.
Creating a Compiled Validator
Step 1: Define Your Types
import scalus.*
import scalus.uplc.builtin.{ByteString, Data}
import scalus.uplc.builtin.Data.{FromData, ToData}
import scalus.cardano.onchain.plutus.v3.*
import scalus.cardano.onchain.plutus.prelude.*
// Define the contract state (datum)
case class ContractDatum(
committer: ByteString,
receiver: ByteString,
image: ByteString,
timeout: PosixTime
) derives FromData, ToData
@Compile
object ContractDatum
// Define the actions (redeemer)
enum Action derives FromData, ToData:
case Timeout
case Reveal(preimage: ByteString)
@Compile
object ActionStep 2: Implement the Validator
import scalus.uplc.builtin.Builtins.sha3_256
@Compile
object HtlcValidator extends Validator:
inline override def spend(
datum: Option[Data],
redeemer: Data,
tx: TxInfo,
ownRef: TxOutRef
): Unit = {
val ContractDatum(committer, receiver, image, timeout) =
datum.map(_.to[ContractDatum]).getOrFail("Missing datum")
redeemer.to[Action] match
case Action.Timeout =>
require(tx.isSignedBy(committer), "Must be signed by committer")
require(
tx.validRange.isEntirelyAfter(timeout),
"Can only timeout after deadline"
)
case Action.Reveal(preimage) =>
require(tx.isSignedBy(receiver), "Must be signed by receiver")
require(
!tx.validRange.isEntirelyAfter(timeout),
"Must reveal before deadline"
)
require(sha3_256(preimage) === image, "Invalid preimage")
}
end HtlcValidatorStep 3: Compile the Validator
Use PlutusV3.compile to compile your validator into a PlutusV3 object:
import scalus.compiler.Options
import scalus.uplc.PlutusV3
private given Options = Options.release
lazy val HtlcContract = PlutusV3.compile(HtlcValidator.validate)Compiler Options
The Options control how SIR is lowered to UPLC.
Presets:
| Preset | Error Traces | Optimization | Use Case |
|---|---|---|---|
Options.release | removed | yes | Production deployment, minimal script size |
Options.default | kept | no | Development |
Options.debug | yes | no | Debugging with detailed error messages |
Key fields:
case class Options(
targetLoweringBackend: TargetLoweringBackend, // UPLC lowering strategy
targetLanguage: Language, // PlutusV1, PlutusV2, PlutusV3
targetProtocolVersion: MajorProtocolVersion, // default: vanRossemPV (11)
generateErrorTraces: Boolean, // include error trace messages
removeTraces: Boolean, // strip all trace calls
optimizeUplc: Boolean, // apply UPLC optimizer
)Target protocol version controls which UPLC features are available during lowering. The default is vanRossemPV (protocol version 11, the van Rossem hard fork, live on mainnet since 2026-07-18). This enables case-on-builtins, dropList field access, arrays, and the other batch6 builtins.
Compiling for an older protocol version changes the generated code (and therefore the script hash). To reproduce pre-van-Rossem output – for example, to verify the hash of a contract deployed before the switch – pin the target:
// Target protocol version 10 (Plomin) – disables PV11-only lowering
private given Options = Options.release.copy(
targetProtocolVersion = MajorProtocolVersion.plominPV
)
// Or use the plomin preset
private given Options = Options.plominThe Scalus marker. Options.release marks the compiled program as Scalus-generated, so
explorers and analytics tools can attribute it (the same idea as Aiken’s and plu-ts’s markers).
The marker is [(error) (con integer 3)], placed on the first (error) node of the optimised
program – 3 is Scalus’s compiler id in CIP-171 .
An (error) node is only reached when the script is about to fail, so on every successful run
the marker is never evaluated: it costs no execution budget, only two or three bytes of script
size. A program with no (error) node at all (a computation that cannot fail) is left unmarked.
The marker is part of the script bytes, so it changes the script hash. Use
Options.releaseUntagged to compile without it – for example, to reproduce the hash of a
contract deployed from a version of Scalus that did not add one.
private given Options = Options.releaseUntaggedScalus 1.0 and 1.1 used a different marker, [(lam _ program) (con string "S")] wrapped around
the whole program. It was evaluated on every run and cost 300 memory and 48,000 CPU steps, which
is why it was replaced. Tooling that recognises Scalus scripts should accept both shapes:
contracts compiled with those versions are deployed on mainnet.
Using Compiled Contracts
Access Script and Address
import scalus.cardano.address.{Address, Network}
import scalus.cardano.ledger.*
// The compiled Plutus script
val script: Script.PlutusV3 = HtlcContract.script
// Script hash
val scriptHash = script.scriptHash
// Script address for a network
val mainnetAddress: Address = HtlcContract.address(Network.Mainnet)
val testnetAddress: Address = HtlcContract.address(Network.Testnet)
// The UPLC program
val program: Program = HtlcContract.program
val scriptHex: String = program.doubleCborHexEnable Error Traces for Debugging
Use .withErrorTraces to get a version with detailed error messages:
// For testing/debugging — includes error traces
val debugContract = HtlcContract.withErrorTraces
// Use in tests
val result = debugContract.program.runWithDebug(scriptContext)Build Transactions
Locking funds at the script:
import scalus.cardano.txbuilder.TxBuilder
def lock(
env: CardanoInfo,
inputUtxo: Utxo,
changeAddress: Address,
value: Value,
datum: ContractDatum
): Transaction = {
TxBuilder(env)
.spend(inputUtxo)
.payTo(HtlcContract, value, datum)
.build(changeTo = changeAddress)
.sign(signer)
.transaction
}Unlocking funds with a redeemer:
def unlock(
env: CardanoInfo,
lockedUtxo: Utxo,
collateralUtxo: Utxo,
redeemer: Action.Reveal,
recipientAddress: Address
): Transaction = {
TxBuilder(env)
.spend(lockedUtxo, redeemer.toData, HtlcContract)
.payTo(recipientAddress, lockedUtxo.output.value)
.collaterals(collateralUtxo)
.build(changeTo = recipientAddress)
.sign(signer)
.transaction
}Parameterized Validators
For validators that need runtime parameters (e.g., a one-shot UTxO reference), use DataParameterizedValidator:
@Compile
object AuctionValidator extends DataParameterizedValidator {
inline override def spend(
oneShotData: Data,
datum: Option[Data],
redeemer: Data,
txInfo: TxInfo,
txOutRef: TxOutRef
): Unit = {
val oneShot = oneShotData.to[TxOutRef]
// ... validator logic
}
}
private given Options = Options.release
lazy val AuctionContract: PlutusV3[Data => Data => Unit] =
PlutusV3.compile(AuctionValidator.validate)Apply the parameter at runtime to get a concrete contract instance:
// Apply the one-shot UTxO reference to get a concrete contract
val appliedContract: PlutusV3[Data => Unit] =
AuctionContract.apply(Data.toData(oneShot))
// Now use it like a regular contract
val scriptAddress = appliedContract.address(network)
val script = appliedContract.scriptBlueprints (CIP-57)
What are Blueprints?
Blueprints are JSON documents that describe your validator’s interface (CIP-57). They include:
- Contract metadata (title, description, version)
- Datum and redeemer type schemas
- Compiled script code and hash
- Parameter definitions
Generating Blueprints
Use Blueprint.plutusV3 to create a blueprint from a compiled contract:
import scalus.cardano.blueprint.Blueprint
lazy val HtlcBlueprint = Blueprint.plutusV3[ContractDatum, Action](
title = "Hashed Time-Locked Contract",
description = "Releases funds when recipient reveals hash preimage before deadline, " +
"otherwise refunds to sender.",
version = "1.0.0",
license = None,
compiled = HtlcContract
)For parameterized validators, include the parameter type:
lazy val AuctionBlueprint = Blueprint.plutusV3[Data, Datum, Action](
title = "Auction",
description = "Parameterized auction validator",
version = "1.0.0",
license = None,
compiled = AuctionContract
)Blueprint Structure
A blueprint JSON contains:
{
"preamble": {
"title": "Hashed Time-Locked Contract",
"description": "Releases funds when...",
"version": "1.0.0",
"plutusVersion": "v3"
},
"validators": [
{
"title": "HtlcValidator",
"datum": {
"title": "ContractDatum",
"schema": {
"dataType": "constructor",
"fields": [
{"title": "committer", "dataType": "bytes"},
{"title": "receiver", "dataType": "bytes"},
{"title": "image", "dataType": "bytes"},
{"title": "timeout", "dataType": "integer"}
]
}
},
"redeemer": {
"title": "Action",
"schema": {
"anyOf": [
{"title": "Timeout", "index": 0, "fields": []},
{"title": "Reveal", "index": 1, "fields": [
{"title": "preimage", "dataType": "bytes"}
]}
]
}
},
"compiledCode": "590b2a590b27010000...",
"hash": "a3b5c8d9..."
}
]
}Schemas are automatically derived from your Scala types using compile-time reflection.
Exporting Blueprints
Save the blueprint to a JSON file:
import java.io.File
val blueprintFile = File("htlc-contract-blueprint.json")
HtlcBlueprint.writeToFile(blueprintFile)Use cases:
- Documentation for contract users
- Input for code generation tools
- Contract verification and auditing
- Automated testing tools