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

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:

FeatureDescription
Validates TransactionsRuns 20+ Cardano ledger rules — fees, signatures, value conservation, execution limits
Executes Plutus ScriptsRuns V1, V2, V3 scripts via the Scalus UPLC interpreter with full cost model evaluation
Tracks Execution CostsReports CPU and memory usage against protocol limits
Manages UTxO StateUpdates inputs/outputs atomically on successful transactions
Handles CollateralProcesses collateral correctly when scripts fail (isValid=false)
Validates Native ScriptsChecks 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 Provider

Factory 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

MethodDescription
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
utxosGet 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:

ScenarioEmulatorYaci 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

Last updated on