Cardano Emulator for Fast Smart Contract Development
The Emulator is an in-memory Cardano node that validates transactions and executes Plutus scripts — just like a real node, but instantly. No Docker, no network, no waiting.
It performs both Phase 1 validation (transaction structure, signatures, fees, value conservation) and Phase 2 validation (Plutus script execution with cost tracking).
Quick Start
import scalus.cardano.node.Emulator
import scalus.cardano.ledger.rules.Context
// Create emulator with pre-funded addresses
val emulator = Emulator.withAddresses(Seq(Alice.address, Bob.address))
// Or with custom initial UTxOs
val emulator = Emulator(
initialUtxos = Map(
input(0) -> Output(Alice.address, Value.ada(1000)),
input(1) -> Output(Bob.address, Value.ada(500))
)
)Use it with TxBuilder just like any other Provider:
val tx = TxBuilder(testEnv)
.payTo(Bob.address, Value.ada(10))
.complete(emulator, Alice.address)
.await()
.sign(Alice.signer)
.transaction
emulator.submit(tx).await() match {
case Right(txHash) => println(s"Success: $txHash")
case Left(error) => println(s"Failed: $error")
}What It Does
The Emulator provides real transaction validation and real script execution:
| Feature | Description |
|---|---|
| Validates Transactions | Runs 20+ Cardano ledger rules — fees, signatures, value conservation, execution limits |
| Executes Plutus Scripts | Runs V1, V2, V3 scripts via the Scalus UPLC interpreter with full cost model evaluation |
| Tracks Execution Costs | Reports CPU and memory usage against protocol limits |
| Manages UTxO State | Updates inputs/outputs atomically on successful transactions |
| Handles Collateral | Processes collateral correctly when scripts fail (isValid=false) |
| Validates Native Scripts | Checks multisig and timelock native scripts |
Ledger Rules
The Emulator uses the Scalus Ledger Rules Framework — the same validators and mutators that implement Cardano’s UTXOW state transition rules.
Rules are auto-discovered at startup. You can customize which rules run by passing custom validators and mutators to the constructor.
API Reference
Constructor
class Emulator(
initialUtxos: Utxos = Map.empty,
initialContext: Context = Context.testMainnet(),
val validators: Iterable[STS.Validator] = Emulator.defaultValidators,
val mutators: Iterable[STS.Mutator] = Emulator.defaultMutators
) extends ProviderFactory Methods
// Quick setup with funded addresses (10,000 ADA each by default)
val emulator = Emulator.withAddresses(Seq(addr1, addr2))
// Custom initial value per address
val emulator = Emulator.withAddresses(Seq(addr1, addr2), Value.ada(50_000))Methods
| Method | Description |
|---|---|
submit(tx) | Submit transaction, returns Future[Either[SubmitError, TransactionHash]] |
findUtxo(input) | Look up single UTxO by transaction input |
findUtxos(address, ...) | Query UTxOs by address with optional filters |
setSlot(slot) | Advance the current slot (for time-based validation) |
snapshot() | Create a point-in-time copy of the emulator |
utxos | Get current UTxO set |
Error Handling
When a transaction fails validation, you get detailed error information:
emulator.submit(tx).await() match {
case Right(txHash) =>
println(s"Transaction submitted: $txHash")
case Left(NodeError(message, Some(exception: TransactionException))) =>
println(s"Validation failed: ${exception.explain}")
case Left(error) =>
println(s"Error: $error")
}Working with Time
Use setSlot() to test time-dependent validators:
// Set slot to test validity intervals
emulator.setSlot(SlotNo(1000))
// Transaction with validity range [500, 1500] will pass
val tx = TxBuilder(testEnv)
.validFrom(SlotNo(500))
.validTo(SlotNo(1500))
.payTo(Bob.address, Value.ada(10))
.complete(emulator, Alice.address)
.await()
.transaction
// Transaction with validity range [2000, 3000] will fail
val invalidTx = TxBuilder(testEnv)
.validFrom(SlotNo(2000))
.validTo(SlotNo(3000))
// ...Thread Safety
The Emulator is thread-safe using AtomicReference for state management. Concurrent transaction submissions use compare-and-swap for atomic updates:
// Safe to use from multiple threads
val futures = (1 to 10).map { i =>
Future {
val tx = buildTransaction(i)
emulator.submit(tx).await()
}
}Customizing Rules
Run with a subset of validators for specific test scenarios:
import scalus.cardano.ledger.rules.*
// Only run Plutus script execution, skip other validations
val minimalEmulator = Emulator(
initialUtxos = myUtxos,
validators = Set.empty, // No validators
mutators = Set(PlutusScriptsTransactionMutator) // Only script execution
)
// Add custom validators
val customEmulator = Emulator(
validators = Emulator.defaultValidators + MyCustomValidator,
mutators = Emulator.defaultMutators
)When to Use Emulator
Both Emulator and Yaci DevKit validate transactions and execute Plutus scripts. The difference is implementation:
| Scenario | Emulator | Yaci DevKit |
|---|---|---|
| Transaction validation | ✓ | ✓ |
| Plutus script execution | ✓ | ✓ |
| Unit tests | ✓ | |
| Rapid development iteration | ✓ | |
| CI/CD (speed matters) | ✓ | |
| Real Haskell Cardano node | ✓ | |
| Complete ledger rule set | ✓ | |
| Pre-deployment confidence | ✓ |
Use Emulator for fast feedback during development. Instant script execution, real validation, no setup overhead.
Use Local Devnet when you need the actual Haskell Cardano node for final validation before deployment.
Example: Testing a Minting Policy
import scalus.compiler.compile
import scalus.cardano.node.Emulator
import scalus.cardano.txbuilder.TxBuilder
test("minting policy validates token name") {
val emulator = Emulator.withAddresses(Seq(Alice.address))
// Compile minting policy
val policy = compile { (redeemer: Data, ctx: Data) =>
val sc = ctx.to[ScriptContext]
val tokenName = redeemer.to[TokenName]
// Validate only "MyToken" can be minted
require(tokenName == TokenName.fromString("MyToken"))
}
val script = Script.PlutusV3(policy.toUplc().plutusV3.cborByteString)
// Valid mint - should succeed
val validTx = TxBuilder(testEnv)
.mint(script, Map(AssetName.fromString("MyToken") -> 100L), TokenName.fromString("MyToken"))
.payTo(Alice.address, Value.asset(script.scriptHash, AssetName.fromString("MyToken"), 100))
.complete(emulator, Alice.address)
.await()
.sign(Alice.signer)
.transaction
emulator.submit(validTx).await() shouldBe a[Right[_, _]]
// Invalid mint - should fail
val invalidTx = TxBuilder(testEnv)
.mint(script, Map(AssetName.fromString("WrongToken") -> 100L), TokenName.fromString("WrongToken"))
// ...
emulator.submit(invalidTx).await() shouldBe a[Left[_, _]]
}See Also
- Local Devnet — Docker-based devnet for integration testing
- Provider — The Provider interface
- Ledger Rules — Transaction validation rules
- Transaction Builder — Building transactions