Developing Your First Cardano Smart Contract
In this tutorial, you’ll write, test, and debug your first Cardano smart contract using Scalus. You’ll learn the fundamentals of spending validators, datums, and redeemers while experiencing Scalus’s unique advantage: debugging smart contracts as regular Scala code with your favorite IDE.
By the end, you’ll have a working validator that locks funds and only unlocks them when specific conditions are met—all tested locally before deploying on-chain.
Creating a new validator from the template
Let’s seed a new Scalus validator:
sbt new scalus3/validator.g8When prompted, name the application hello cardano. This command scaffolds a simple Scalus validator project.
sbt new scalus3/validator.g8
name [MyValidator]: hello cardano
Template applied in ./hello-cardanoProject structure
Let’s take a look at what just got generated:
hello-cardano/
├── HelloCardano.scala # Simple validator
├── HelloCardano.test.scala # Simple tests
├── project.scala # Project configuration
└── README.mdLet’s test our validator
scala-cli test hello-cardanoIf you see this message, everything is going fine!
HelloCardanoTest:
- HelloCardano should work correctly *** FAILED ***It’s time to implement the spending logic, but before that - let’s learn the key concepts.
Key concepts
-
A spending validator is a program that controls when funds can be unlocked from a UTxO (Unspent Transaction Output) on the Cardano blockchain. Think of it as a lock that requires specific conditions to be met before allowing access.
-
Datum - Configuration data attached when locking funds in the contract. It’s like setting the combination on a lock. In our example, the datum stores the public key hash of the authorized owner.
-
Redeemer - Data provided when spending the funds. It’s like entering the combination to open the lock. Our example requires the redeemer to contain the message “Hello, Cardano!”
Writing Your First Validator
Let’s write a simple validator that demonstrates these concepts, update the spending validator:
@Compile
object HelloCardano extends Validator:
inline override def spend(
datum: Option[Data],
redeemer: Data,
tx: TxInfo,
outRef: TxOutRef
): Unit =
// Extract the owner's public key hash from the datum
val owner = datum.getOrFail("Datum not found").to[PubKeyHash]
// Check that the transaction is signed by the owner
val signed = tx.signatories.contains(owner)
require(signed, "Must be signed")
// Verify the redeemer contains the correct message
val saysHello = redeemer.to[String] == "Hello, Cardano!"
require(saysHello, "Invalid redeemer")How It Works in Practice
To spend a UTxO locked by this validator:
- Lock: Create a UTxO with funds and attach a datum containing the owner’s public key hash
- Unlock: Provide a redeemer with “Hello, Cardano!”, sign the transaction, and both checks must pass
Learn more about Cardano Validators.
Let’s test our Validator
Now that we’ve written our validator, let’s test it to ensure it works correctly.
Scalus provides the ScalusTest trait with helper methods to create test scenarios.
Test 1: Success case - Validates that when both conditions are met (correct message and owner signature), the validator succeeds.
class HelloCardanoTest extends AnyFunSuite with ScalusTest:
test("HelloCardano validates correct message and signature") {
// 1. Setup: Create a public key hash for the owner
val ownerPubKey = PubKeyHash(hex"1234567890abcdef1234567890abcdef1234567890abcdef12345678")
// 2. Prepare the redeemer with the correct message
val message = "Hello, Cardano!".toData
// 3. Create a script context with the owner in signatories
val context = makeSpendingScriptContext(
datum = ownerPubKey.toData,
redeemer = message,
signatories = List(ownerPubKey)
)
// 4. Compile and run the validator
val result = compile(HelloCardano.spend).runScript(context)
// 5. Assert the validation succeeded
assert(result.isSuccess)
}How it works?
compile()- Transforms your Scala validator code into Plutus UPLC (Untyped Plutus Core), the low-level language that runs on Cardano. This happens at compile-time using the Scalus compiler plugin.runScript()- Executes the compiled validator with the providedScriptContext, simulating on-chain execution. Returns aResultindicating success or failure with execution costs.assert()- A standard Scala test assertion that verifies the condition is true. If false, the test fails. Here we check if the validator execution succeeded (result.isSuccess).makeSpendingScriptContext()- A helper fromScalusTestthat creates a properly structuredScriptContextfor spending validators, so you don’t need to manually construct complex context objects.
What about negative scenario?
Test 2: Wrong message - Ensures the validator fails when the redeemer contains an incorrect message, even if signed by the owner.
test("HelloCardano fails with wrong message") {
val ownerPubKey = PubKeyHash(hex"1234567890abcdef1234567890abcdef1234567890abcdef12345678")
val wrongMessage = "Wrong message".toData
val context = makeSpendingScriptContext(
datum = ownerPubKey.toData,
redeemer = wrongMessage,
signatories = List(ownerPubKey)
)
val result = compile(HelloCardano.spend).runScript(context)
// Validator should fail due to incorrect message
assert(result.isFailure)
}Running the Tests
scala-cli test hello-cardanoYou should see output showing all tests passing:
HelloCardanoTest:
- HelloCardano validates correct message and signature
- HelloCardano fails with wrong messageLearn more about Testing Smart Contracts.
Debugging
One of Scalus’s biggest advantages is that you can debug validators as regular Scala code before deploying them on-chain.
Debug as Regular Scala Code
Your validator is just Scala code until it’s compiled to UPLC. This means you can:
- Use your IDE’s debugger - Set breakpoints, step through execution, inspect variables
- Call validators directly from the test - Run in the debug mode.
Look how simple it is!
Learn more about Debugging Smart Contracts.
Compiling into Plutus script
Use the compile function to transform your Scala validator into a Plutus script:
import scalus.Compiler.compile
import scalus.ledger.api.PlutusLedgerLanguage
// Compile your validator
val compiled = compile {
def myValidator(datum: Data, redeemer: Data, ctx: Data): Unit = {
val context = ctx.to[ScriptContext]
require(context.txInfo.signatories.nonEmpty, "No signatories")
}
myValidator
}
// Convert to UPLC and encode for Cardano
val program = compiled.toUplc(generateErrorTraces = true).plutusV3
val scriptHex = program.doubleCborHexUsing Validators in Transactions
Once compiled and encoded, use the script hex in your transactions:
val compiled = compile(MyValidator)
val program = compiled.toUplc().plutusV3
val scriptHex = program.doubleCborHex
// Use scriptHex when building transactions
// with your transaction builder of choiceLearn more about Compiling Smart Contracts.
What’s Next?
Congratulations! You’ve successfully created, tested, and debugged your first Cardano smart contract. You now understand:
- How spending validators control access to locked funds
- The role of datums, redeemers, and script context
- How to write type-safe validators in Scala
- How to test validators with different scenarios
- How to debug validators using standard Scala tooling
- How to compile validator into Plutus script
- How to use the compiled validator in transactions
Continue Your Journey
- Validators in Depth - Learn about different validator types and patterns
- Testing Smart Contracts - Advanced testing techniques and property-based testing
- Debugging and Troubleshooting - Deep dive into debugging strategies
- Compiling Smart Contracts - Generate plutus script and use in your transactions
- Examples Repository - Real-world validator examples