Skip to main content

Tutorial

Basic workflow

The basic workflow is to write a Scala program and then compile it to a Plutus script, similar to how PlutuxTx works.

You can store the Plutus script in a *.plutus file and use it with the Cardano CLI. Or use one of the Java/JavaScript libraries to construct transactions with the script.

This example shows how to use the cardano-client-lib to send transactions.

You write a script using a small subset of Scala, which is then compiled to a Scalus Intermediate Representation (SIR) with compile function.

The SIR can be pretty-printed and reviewed.

The SIR is then compiled to Untyped Plutus Core (UPLC) that can be executed on the Cardano blockchain.

Simple validator

import scalus.Compiler.compile
import scalus.*
import scalus.builtin.Data
import scalus.uplc.Program

// Compile Scala code to Scalus Intermediate Representation (SIR)
val validator = compile {
// A simple validator that always succeeds
(context: Data) => ()
}
// validator: SIR = LamAbs(name = "context", term = Const(const = Unit))
// pretty print the SIR
validator.show
// res0: String = "{λ context -> () }"
// convert the SIR to UPLC and pretty print it with colorized syntax highlighting
validator.toUplc().showHighlighted
// res1: String = """(lam context
// (con unit ()))"""
// get a double CBOR encoded optimized UPLC program as HEX formatted string
Program(version = (1, 1, 0), term = validator.toUplcOptimized()).doubleCborHex
// res2: String = "46450101002499"

Constans and primitives

Plutus supports the following primitive types: unit, bool, integer, bytestring, string, data, list, pair. Those types are represented in Scalus as Unit, Boolean, BigInt, ByteString, String, Data, List, Pair respectively.

We use Scala native types to represent Unit, Boolean, BigInt, and String.

Here is an example of how to define constants and use built-in types.

import scalus.Compiler.compile
import scalus.*
import scalus.builtin.*
import scalus.builtin.ByteString.given

val constants = compile {
val unit = () // unit type
val bool = true || false // boolean type
val int = BigInt(123) // integer type
val bigint = BigInt("12345678901234567890") // large integer value
val implicitBigIng: BigInt = 123
val emptyByteString = ByteString.empty
val byteString = ByteString.fromHex("deadbeef")
val byteStringUtf8 = ByteString.fromString("hello") // from utf8 encoded string
val byteString2 = hex"deadbeef" // ByteString from hex string
val string = "Scalus Rocks!" // string type
val emptyList = List.empty[BigInt] // empty list
val list = List[BigInt](1, 2, 3) // list of integers
val pair = Pair(true, ()) // pair of boolean and unit
}

Builtin Functions

import scalus.builtin.*
import scalus.builtin.ByteString.given
import scalus.prelude.Prelude.{*, given}
compile {
// See scalus.builtin.Builtins for what is available
val data = Builtins.iData(123)
val eqData = data == Builtins.iData(123) || data != Builtins.iData(123)
val eq = Builtins.equalsByteString(hex"deadbeef", ByteString.empty)
val byteStringEq = hex"deadbeef" == ByteString.empty || hex"deadbeef" != ByteString.empty
val stringEq = "deadbeef" == "" || "deadbeef" != ""
val a = BigInt(1)
val sum = a + 1 - a * 3 / 4 // arithmetic operators
val intEquality = a == sum || a != sum
val bool = !true || (false == true) != false && true // boolean operators
val equals = a === sum // comparison operators
}

Data types

You can define your own data types using Scala case classes and enums.

import scalus.builtin.ByteString
import scalus.prelude.Prelude.{*, given}

case class Account(hash: ByteString, balance: BigInt)

enum State:
case Empty
case Active(account: Account)

import State.*
compile {
// Tuple2 literals are supported
val tuple = (true, BigInt(123))
val empty = State.Empty // simple constructor
// Use `new` to create an instance
val account = new Account(ByteString.empty, tuple._2) // access tuple fields
val active: State = new State.Active(account)
val hash = account.hash // access case class fields
// A simple pattern matching is supported
// no guards, no type ascriptions.
// Inner matches can be done only on single constructor case classes
// Wildcard patterns are supported
active match
case Empty => true
case Active(account @ Account(_, balance)) => balance == BigInt(123)
// all cases must be covered or there must be a default case
val isEmpty = active match
case Empty => true
case _ => false
}

Control flow

import scalus.prelude.Prelude.{*, given}
compile {
val a = BigInt(1)
// if-then-else
if a == BigInt(2) then ()
// throwing an exception compiles to Plutus ERROR,
// which aborts the evaluation of the script
// the exception message can be translated to a trace message
// using sir.toUplc(generateErrorTraces = true)
else throw new Exception("not 2")
}

Functions

compile {
val nonRecursiveLambda = (a: BigInt) => a + 1

def recursive(a: BigInt): BigInt =
if a == BigInt(0) then 0
else recursive(a - 1)
}

Modules and reusable code

You can define reusable code in a Scala object annotated with @Compile. Scalus will compile the code to *.sir files and include them in the jar file. This way you can distribute your code as a library.

Use @Ignore to exclude a definition from the compilation.

The compile will link the modules together and compile them to a single script.


@Compile
object ReusableCode {
val constant = BigInt(1)
def usefulFunction(a: BigInt): BigInt = a + 1
@Ignore // this function is not compiled to UPLC
def shouldNotBeInUplc() = ???
}

val modules = compile {
ReusableCode.usefulFunction(ReusableCode.constant)
}

FromData

FromData type class is used to convert a Data value to a Scalus value.

import scalus.builtin.*, Builtins.*, Data.*
import scalus.builtin.FromDataInstances.given

case class Account(hash: ByteString, balance: BigInt)

enum State:
case Empty
case Active(account: Account)


val fromDataExample = compile {
// The `fromData` function is used to convert a `Data` value to a Scalus value.
val data = iData(123)
// fromData is a summoner method for the `FromData` type class
// there are instances for all built-in types
val a = fromData[BigInt](data)
// also you can use extension method `to` on Data
val b = data.to[BigInt]

// you can define your own `FromData` instances
{
given FromData[Account] = (d: Data) => {
val args = unConstrData(d).snd
new Account(args.head.to[ByteString], args.tail.head.to[BigInt])
}
val account = data.to[Account]
}

// or your can you a macro to derive the FromData instance
{
given FromData[Account] = FromData.deriveCaseClass
given FromData[State] = FromData.deriveEnum[State] {
case 0 => d => State.Empty
case 1 => FromData.deriveConstructor[State.Active]
}
}
}

Writing a validator

Here is a simple example of a PlutusV2 validator written in Scalus.

import scalus.ledger.api.v1.PubKeyHash
import scalus.ledger.api.v3.*
import scalus.ledger.api.v3.FromDataInstances.given
import scalus.builtin.ByteString.given
import scalus.prelude.List

// Use Scala 3 indentation syntax. Look ma, no braces! Like Python!
val pubKeyValidator = compile:
def validator(ctxData: Data) = {
val ctx = ctxData.to[ScriptContext]
List.findOrFail[PubKeyHash](ctx.txInfo.signatories): sig =>
sig.hash == hex"deadbeef"
}

Troubleshooting

Firstly, you can use a debugger and debug the Scala code.

You can use log and trace functions to log messages to the execution log.

And there is a ? operator that can be used to log the value of a boolean expression when it is false.

import scalus.builtin.Builtins.trace
import scalus.prelude.*
import scalus.prelude.Prelude.log
val sir = compile {
val a = trace("a")(BigInt(1))
val b = BigInt(2)
log("Checking if a == b")
val areEqual = a == b
areEqual.? // logs "areEqual ? False"
}.toUplc().evalDebug.toString
// sir: String = """Success executing script:
// term: (con bool False)
// budget: { mem: 0.004298, cpu: 0.962976 }
// costs:
// Startup: 1 { mem: 0.000100, cpu: 0.000100 }
// Const: 7 { mem: 0.000700, cpu: 0.112000 }
// Force: 5 { mem: 0.000500, cpu: 0.080000 }
// Var: 3 { mem: 0.000300, cpu: 0.048000 }
// LamAbs: 4 { mem: 0.000400, cpu: 0.064000 }
// Builtin: 5 { mem: 0.000500, cpu: 0.080000 }
// Apply: 15 { mem: 0.001500, cpu: 0.240000 }
// Delay: 2 { mem: 0.000200, cpu: 0.032000 }
// EqualsInteger: 1 { mem: 0.000001, cpu: 0.052333 }
// IfThenElse: 1 { mem: 0.000001, cpu: 0.076049 }
// Trace: 3 { mem: 0.000096, cpu: 0.178494 }
// logs: a: { mem: 0.000932, cpu: 0.187598 }
// Checking if a == b: { mem: 0.002064, cpu: 0.423096 }
// areEqual ? False: { mem: 0.004298, cpu: 0.962976 }"""

Converting the Scalus code to Flat/CBOR encoded UPLC

The compile function converts the Scalus code to a SIR value, Scalus Intermediate Representation. You then need to convert the SIR value to a UPLC value and encode it to Flat and then to CBOR.

Many APIs require the HEX encoded string of double CBOR encoded Flat encoded UPLC program, like Hex(CborEncode(CborEncode(FlatEncode(Program(version, uplc))))).

import scalus.*
import scalus.builtin.ByteString.given
import scalus.ledger.api.PlutusLedgerLanguage
import scalus.ledger.api.v1.PubKeyHash
import scalus.ledger.api.v3.*
import scalus.ledger.api.v3.FromDataInstances.given
import scalus.prelude.List
import scalus.uplc.Program

val serializeToDoubleCborHex = {
val pubKeyValidator = compile {
def validator(datum: Data, redeamder: Data, ctxData: Data) = {
val ctx = ctxData.to[ScriptContext]
List.findOrFail[PubKeyHash](ctx.txInfo.signatories)(sig => sig.hash == hex"deadbeef")
}
}
// convert to UPLC
// generateErrorTraces = true will add trace messages to the UPLC program
val uplc = pubKeyValidator.toUplc(generateErrorTraces = true)
val program = Program((1, 1, 0), uplc)
val flatEncoded = program.flatEncoded // if needed
val cbor = program.cborEncoded // if needed
val doubleEncoded = program.doubleCborEncoded // if needed
// in most cases you want to use the hex representation of the double CBOR encoded program
program.doubleCborHex
// also you can produce a pubKeyValidator.plutus file for use with cardano-cli
import scalus.utils.Utils
Utils.writePlutusFile("pubKeyValidator.plutus", program, PlutusLedgerLanguage.PlutusV2)
// or simply
program.writePlutusFile("pubKeyValidator.plutus", PlutusLedgerLanguage.PlutusV2)
}

Evaluating scripts

Scalus provides a high-level API to evaluate UPLC scripts.

compile(BigInt(2) + 2).toUplc().evalDebug.toString
// res11: String = """Success executing script:
// term: (con integer 4)
// budget: { mem: 0.000602, cpu: 0.181308 }
// costs:
// Startup: 1 { mem: 0.000100, cpu: 0.000100 }
// Const: 2 { mem: 0.000200, cpu: 0.032000 }
// Builtin: 1 { mem: 0.000100, cpu: 0.016000 }
// Apply: 2 { mem: 0.000200, cpu: 0.032000 }
// AddInteger: 1 { mem: 0.000002, cpu: 0.101208 }
// logs: """

You get a Result object that contains the result of the evaluation, the execution budget, the execution costs, and the logs.

You can also use the low-level API to evaluate scripts.

import scalus.builtin.{*, given}
import scalus.ledger.api.*
import scalus.uplc.*, eval.*

def evaluation() = {
val sir = compile {
def usefulFunction(a: BigInt): BigInt = a + 1
usefulFunction(1)
}
val term = sir.toUplc()
// simply evaluate the term
VM.evaluateTerm(term).show // (con integer 2)
// get default MachineParams for PlutusV3 in Conway era
val defaultMachineParams = MachineParams.defaultPlutusV3Params
// or
term.eval.show // (con integer 2)

// evaluate a flat encoded script and calculate the execution budget and logs
val result =
VM.evaluateScriptCounting(defaultMachineParams, Program((1, 1, 0), term).flatEncoded)
println(s"Execution budget: ${result.budget}")
println(s"Evaluated term: ${result.term.show}")
println(s"Logs: ${result.logs.mkString("\n")}")

// you can get the actual execution costs from protocol parameters JSON from cardano-cli
lazy val machineParams = MachineParams.fromCardanoCliProtocolParamsJson(
"JSON with protocol parameters",
PlutusLedgerLanguage.PlutusV3
)
// or from blockfrost API
lazy val machineParams2 = MachineParams.fromBlockfrostProtocolParamsJson(
"JSON with protocol parameters",
PlutusLedgerLanguage.PlutusV3
)

// evaluate the term with debug information
// the `Result` type has a readable `toString` method
VM.evaluateDebug(term) match
case r @ Result.Success(evaled, budget, costs, logs) =>
println(r)
case r @ Result.Failure(exception, budget, costs, logs) =>
println(r)

// Low-level evaluation API:

// CountingBudgetSpender is a budget spender that counts the total cost of the evaluation
val countingBudgetSpender = CountingBudgetSpender()
// TallyingBudgetSpender is a budget spender that counts the costs of each operation
val tallyingBudgetSpender = TallyingBudgetSpender(countingBudgetSpender)
val logger = Log()
// use NoLogger to disable logging
val noopLogger = NoLogger
val cekMachine = CekMachine(
defaultMachineParams, // or use default params
tallyingBudgetSpender,
logger,
JVMPlatformSpecific // platform specific functions. Use JSPlatformSpecific for Scala.js
)
val debruijnedTerm = DeBruijn.deBruijnTerm(term)
try {
cekMachine.evaluateTerm(debruijnedTerm)
} catch {
case e: StackTraceMachineError =>
println(s"Error: ${e.getMessage}")
println(s"Stacktrace: ${e.getCekStack}")
println(s"Env: ${e.env}")
}
println(s"Execution budget: ${tallyingBudgetSpender.budgetSpender.getSpentBudget}")
println(s"Logs: ${logger.getLogs.mkString("\n")}")
println(
s"Execution stats:\n${tallyingBudgetSpender.costs.toArray.sortBy(_._1.toString()).map {
case (k, v) => s"$k: $v"
}.mkString("\n")}"
)
}