# Scalus documentation (full) Generated from https://scalus.org. One section per page; each section header is the canonical URL. --- Source: https://scalus.org/docs --- # Scalus Introduction **Scalus** is a development platform for building smart contracts and decentralized applications (dApps) on the Cardano blockchain. It provides a unified environment where developers can write both on-chain smart contracts and off-chain logic using [Scala 3](/docs/language-guide/scala3) - a modern, expressive, and type-safe functional programming language. ## Get Started with Scalus icon="🚀" title="Quick Start Guide" href="/docs/get-started" description="Install Scalus and create your first validator in 5 minutes" /> icon="⚙️" title="Build a Smart Contract" href="/docs/smart-contracts/developing-smart-contracts" description="Learn to write, compile, and test Cardano validators" /> icon="🏗️" title="Build Transactions" href="/docs/transactions/building-first-transaction" description="Construct and submit Cardano transactions with TxBuilder" /> icon="💡" title="DApp Starter Tutorial" href="/docs/dapp-development/dapp-starter-tutorial" description="Build a complete native token minting service from scratch" /> **New to Cardano or Scala?** Check out our [onboarding guides](/docs/get-started) for developers coming from either ecosystem. ## Why Choose Scalus for Cardano Development? ### One Language, Full Stack Write smart contracts and application logic in Scala 3. No context switching between Plutus/Aiken for contracts and JavaScript/Python for frontends and backends. ### Type-Safe by Design Catch errors at compile time, not on-chain. Leverage Scala's powerful type system to prevent entire classes of bugs before deployment. ### Professional Tooling Debug validators with breakpoints in IntelliJ IDEA or VS Code. Step through execution, inspect variables, and use the tools you already know. ### Battle-Tested Ecosystem Built on the JVM with access to thousands of proven libraries. Use ScalaTest for testing, Akka for concurrency, and the entire Scala ecosystem. ### Fine-Grained Control Advanced optimizations give you control over generated UPLC code. Reduce script size and execution costs with macros, inline optimizations, and direct Script Context access. ## Development Workflow - **Write** - [Smart contracts](/docs/smart-contracts/developing-smart-contracts) and off-chain logic in Scala 3 - **Test** - [Unit, integration, and property-based tests](/docs/testing) with ScalaTest and ScalaCheck - **Debug** - [Step through validators](/docs/testing/debugging) with breakpoints in your IDE - **Optimise** - [Reduce script size and costs](/docs/smart-contract-optimisations) with advanced techniques - **Build** - [Construct transactions](/docs/transactions) with automatic fee calculation and balancing - **Deploy** - Test on [local devnet](/docs/testing/local-devnet), then ship to mainnet ## Modern Development Experience Built for professional developers and teams, Scalus provides: - **Industry-Standard IDEs** - Full support for IntelliJ IDEA and VS Code with intelligent code completion - **Comprehensive Testing** - Unit tests, integration tests, and property-based testing with ScalaTest and ScalaCheck - **Rich Ecosystem** - Access to thousands of JVM libraries and the entire [Scala ecosystem](https://index.scala-lang.org/) - **One Language** - Write everything in Scala 3—no juggling multiple languages, tools, or paradigms Scalus is a development platform made for professionals and businesses who value productivity, type safety, and code quality. --- Source: https://scalus.org/docs/cardano-smart-contract-development-platform --- # Cardano Smart Contract Development Platform Scalus is a **JVM-based Cardano development platform** built around Scala 3. It combines smart contracts, transaction building, local emulator testing, debugging, profiling, blueprints, examples, and multiplatform tooling in one stack. It is not only a smart-contract language or a transaction SDK. Scalus connects smart-contract development, the compiler pipeline, optimization controls, transaction building, ledger/emulator workflows, testing, debugging, profiling, CIP-57 blueprints, example contracts, and multiplatform JVM/JavaScript/Native tooling. This page is a positioning guide, not a claim that one tool is universally better than another. Aiken, Plutarch, Yaci DevKit, Balius, Hardhat, and Foundry solve different problems with different tradeoffs. ## In One Sentence Scalus is for teams that want a coherent Cardano stack where contracts, off-chain code, transaction flows, tests, blueprints, and local execution environments can evolve together. ## Smart Contract Development Cardano teams are not only choosing a contract syntax. They are choosing how quickly they can write, understand, optimize, test, debug, and maintain protocols over time. | Capability | Scalus | Aiken | Plutarch | |---|---|---|---| | Primary model | JVM-based Scala 3 platform | Cardano-focused smart-contract language | Haskell eDSL for Plutus | | Contract authoring | Scala 3 with `@Compile` | Aiken language | Haskell / Plutarch DSL | | Plutus support | V1, V2, V3 | V3-focused | V1, V2, V3 | | Standard library | Yes | Yes | Yes | | Low-level UPLC control | Yes | Compiler-driven | Yes | | Custom optimizations | Compiler and UPLC-level options | Mostly compiler-driven | Manual / DSL-level | | Off-chain type/code reuse | Native Scala/JVM model | Code generation and external integrations | External off-chain stack | | Developer ecosystem | Scala/JVM ecosystem | Cardano-native ecosystem | Haskell ecosystem | | Custom data representation | Annotations and compiler support | Limited compared with low-level DSLs | Manual control | | Debugging | IDE breakpoints, source positions, traces | Logs and toolchain support | Haskell/toolchain support | | Transaction building | Native JVM and JS/TS APIs | External SDKs such as Lucid or Mesh | External SDKs | | Testing | ScalaTest, ScalaCheck, emulator, boundary/scenario/state-machine testing | Built-in test tooling | Haskell ecosystem tooling | **Aiken** is an excellent focused smart-contract language for Cardano. **Plutarch** is powerful when a Haskell team wants precise low-level control. **Scalus** is different: it aims to connect the contract language, compiler, off-chain transactions, local execution, testing, blueprints, and examples into one Scala/JVM-centered development platform. ## Development Environment and Testing Scalus is designed to shorten the local development loop. Its in-memory emulator gives developers a fast, in-process Cardano execution environment for transaction and script validation. It can be used from the JVM and exposed to JavaScript/TypeScript through the `scalus` npm package. This gives Cardano developers something closer to the local execution loops common in Ethereum tooling: build a transaction, evaluate scripts, inspect failures, advance time, and iterate without always stitching together a separate network, provider, and testing stack. | Capability | Scalus | Yaci DevKit | Hardhat | Foundry | |---|---|---|---|---| | Ecosystem | Cardano | Cardano | Ethereum | Ethereum | | In-memory blockchain | Yes, in-process emulator | No, local devnet process | Yes, Hardhat Network | Yes, Anvil | | Local devnet | Yes, via Yaci DevKit integration | Yes | Yes | Yes | | Slot/block/time control | Slot and epoch workflows | Devnet configuration | Block/time controls | Block/time controls | | Test language | Scala / JVM types | Java / external clients | JavaScript / TypeScript | Solidity | | Property-based testing | ScalaCheck | Depends on caller stack | Available through ecosystem | Forge fuzzing | | Multi-party / wallet testing | Native helpers | Devnet accounts and clients | Yes | Yes | | Smart-contract debugging | Source positions, IDE debugging, traces | Transaction-level logs | Stack traces and console logs | Stack traces | | Typical use | Fast local Cardano protocol tests and emulator workflows | Real local Cardano network and integration testing | Ethereum dApp testing | Ethereum contract testing | Yaci DevKit remains valuable when a real local Cardano node/devnet is needed. Scalus complements that with an in-process emulator and integrated testing tools for faster inner-loop development. ## Application Framework The application layer is where Scalus moves beyond contract tooling. The goal is to support complete Cardano applications and protocol backends: transaction building, typed UTxO indexing, reactive workers, scheduling, persistence, and crash recovery. In the Cardano ecosystem, **Balius** is a close conceptual neighbor because it focuses on headless dApps and workers. Scalus approaches the problem from the Scala/JVM side and connects that runtime direction to its existing compiler, transaction builder, emulator, testing, and blueprint tooling. | Capability | Scalus | Balius | |---|---|---| | Primary focus | JVM-based application platform plus contracts, transactions, emulator, testing, and blueprints | Headless Cardano dApps and workers | | Runtime model | Scala/JVM-oriented, with Native and JS/TS tooling in the wider stack | Rust/WASM-oriented | | Reactive workers | In development | Yes | | Persistence layer | In development, crash-consistent direction | Runtime-managed | | Job / time scheduling | In development | Runtime-managed | | Crash recovery | In development, event-log replay direction | Runtime-managed | | L1 access | External node/provider integrations | External node/provider integrations | | Production maturity | Core compiler/transaction/testing stack available; application runtime in development | In development | Scalus's compiler, transaction builder, emulator, testing stack, blueprint tooling, examples, and JS package are available today. The application runtime layer is under active development. ## What This Means for Teams For small contracts, a focused contract language can be enough. For larger protocols, the hard part is keeping all moving pieces aligned: - Smart-contract source code - Generated UPLC and optimization choices - Datum, redeemer, and transaction schemas - Off-chain transaction builders - Local emulator and devnet tests - Scenario, property, and boundary tests - Deployment blueprints and reference scripts - Long-term maintenance and operations Scalus is designed around that larger workflow. It gives teams a single JVM-based foundation for building, testing, and evolving Cardano protocols while still supporting JavaScript/TypeScript and Native tooling where those platforms make sense. ## References - [Aiken](https://aiken-lang.org/) - [Plutarch](https://plutarch-plutus.org/) - [Yaci DevKit](https://devkit.yaci.xyz/) - [Hardhat](https://hardhat.org/) - [Foundry](https://www.getfoundry.sh/) - [Balius](https://docs.txpipe.io/balius) --- Source: https://scalus.org/docs/get-started/project-commands --- # Project Templates & Commands Scalus ships two [Giter8](https://www.foundweekends.org/giter8/) templates for scaffolding a new project, and the generated projects come pre-wired with commands for building, testing, profiling, generating CIP-57 blueprints, and deploying contracts on-chain. Every generated project is **dual-build**: it works with both [sbt](https://www.scala-sbt.org/) and [Scala CLI](https://scala-cli.virtuslab.org/), sharing one flat source layout (sources at the project root, test sources using the `.test.scala` suffix). ## Templates | Template | Command | What you get | |----------|---------|--------------| | `hello.g8` | `sbt new scalus3/hello.g8` | Minimal "Hello, Cardano!" spending validator with unit and integration tests. | | `validator.g8` | `sbt new scalus3/validator.g8` | Full DApp starter: validator, `Contract` (blueprint + deploy), unit and integration tests. | Start with **`hello.g8`** to learn the basics; reach for **`validator.g8`** when you want the full toolchain (CIP-57 blueprint generation and on-chain deployment) ready to go. ### `hello.g8` layout ```ansi hello-cardano/ ├── HelloCardano.scala # Plutus V3 spending validator ├── HelloCardano.test.scala # Unit tests (ScalaTest + scalus-testkit) ├── HelloCardanoIntegration.test.scala # Submit-based tests (emulator / Yaci DevKit) ├── HelloCardanoContract.scala # Contract: compiled script + CIP-57 blueprint ├── project.scala # Scala CLI build configuration ├── build.sbt # sbt build configuration └── Readme.md ``` ### `validator.g8` layout The `validator.g8` template prompts for a `name` (default `My Validator`) and names the files after it: ```ansi my-validator/ ├── MyValidator.scala # On-chain validator — replace its body with your logic ├── MyValidator.test.scala # Unit tests using scalus-testkit's ScalusTest ├── MyValidatorIntegration.test.scala # Submit-based tests (emulator / Yaci DevKit) ├── MyValidatorContract.scala # Contract: compiled script + CIP-57 blueprint ├── project.scala # Scala CLI build configuration ├── build.sbt # sbt build configuration └── Readme.md ``` ## Build & test commands The standard commands behave exactly as they do in any sbt or Scala CLI project. From the project directory: | Command | Purpose | |---------|---------| | `sbt compile` | Compile the on-chain and off-chain sources. | | `sbt test` | Run all tests (unit + integration; uses the emulator backend). | | `sbt "testOnly *HelloCardanoTest"` | Run a single test suite by name (wildcards allowed). | | `sbt clean` | Remove `target/` build artifacts. Use it to force a full rebuild if you hit stale-class issues. | Quotes are required around `testOnly` (and any command with arguments) so sbt receives the whole string as one command. | Command | Purpose | |---------|---------| | `scala-cli compile .` | Compile the project. | | `scala-cli test .` | Run all tests. | | `scala-cli test . --test-only 'HelloCardanoTest'` | Run a single test suite. | | `scala-cli fmt .` | Format sources with Scalafmt. | | `scala-cli clean .` | Clear the Scala CLI build cache. | Scala CLI reads its configuration from `project.scala` rather than `build.sbt`. ### Speeding up sbt Each `sbt ` boots a fresh JVM. Two ways to avoid paying that startup cost every time: - **`sbtn`** — a thin native client that talks to a persistent sbt server, keeping the JVM warm between invocations. Use it as a drop-in replacement: `sbtn compile`, `sbtn test`, `sbtn "testOnly *HelloCardanoTest"`. Run `sbtn shutdown` to stop the background server. - **The sbt shell** — run `sbt` with no arguments to drop into its interactive prompt, then type `compile`, `test`, `testOnly …` directly (no `sbt` prefix, no surrounding quotes). The JVM is reused for the whole session. One caveat: a warm server or shell reads environment variables like `SCALUS_TEST_ENV` and `SCALUS_PROFILE` once, at startup, and reuses them for its whole life — so setting one on a later command has no effect. To switch backends or toggle profiling, set the variable on a plain `sbt ` (fresh JVM each time), or restart the server (`sbtn shutdown`) or shell first. ## Test backends The integration tests (`*Integration.test.scala`) run against a backend selected by the `SCALUS_TEST_ENV` environment variable — `emulator` (the default) or `yaci`: ```sh copy sbt test # in-memory emulator (the default) SCALUS_TEST_ENV=emulator sbt test # in-memory emulator, named explicitly SCALUS_TEST_ENV=yaci sbt test # a local Yaci DevKit node (auto-started; requires Docker) ``` The same variable works with Scala CLI (`SCALUS_TEST_ENV=yaci scala-cli test .`), since the test code reads it directly rather than relying on an sbt-only command. See [Emulator](/docs/testing/emulator) and [Local Devnet](/docs/testing/local-devnet) for what each backend does and when to use it. ## Profiling Set `SCALUS_PROFILE=1` to run the tests with on-chain execution profiling enabled. It writes an interactive HTML report (CPU and memory budget per source line) to `target/profile.html`: ```sh copy SCALUS_PROFILE=1 sbt test ``` The same variable works with Scala CLI (`SCALUS_PROFILE=1 scala-cli test .`). Open `target/profile.html` in a browser to see where your validator spends its execution budget. See [Profiling](/docs/testing/profiling) for how to read the report and optimise from it. ## Blueprint & deploy The sbt build enables the `ScalusSbtPlugin`, which adds two tasks backed by the `Contract` object in your project: ```sh copy # Generate CIP-57 blueprints under # target/scala-3.3.7/resource_managed/main/META-INF/scalus/blueprints// # plus an aggregate plutus.json at the resource root # (sbt package / publish also embed them in the JAR automatically) sbt blueprint # Deploy the validator as a reference-script UTxO # (needs a Blockfrost key and a funded wallet) export BLOCKFROST_API_KEY=... # or pass --blockfrost-key export CARDANO_MNEMONIC="word1 ..." # or pass --mnemonic sbt "deploy MyValidatorContract --network preview" ``` These two tasks are the heart of the Scalus sbt plugin. The [**SBT Plugin**](/docs/dapp-development/sbt-plugin) page documents blueprint verification, the full set of `deploy` flags, and how to wire the plugin into an existing (non-template) project. ## Next steps - [Write Smart Contracts](/docs/smart-contracts/developing-smart-contracts) — validators, data conversion, compilation - [SBT Plugin](/docs/dapp-development/sbt-plugin) — blueprint verification and contract deployment in depth - [Testing](/docs/testing) — unit, property-based, and emulator-based testing - [Build Transactions](/docs/transactions/building-first-transaction) — construct and submit transactions --- Source: https://scalus.org/docs/get-started/ai-assisted-development --- # AI-Assisted Development Scalus ships first-class support for LLM coding agents (Claude Code, Cursor, Codex, Copilot, and friends). Three layers work together: fetchable context artifacts on this site, AI-ready project templates, and a Claude Code plugin with task skills. ## Context artifacts (llms.txt) LLMs trained before Scalus 1.0 hallucinate outdated APIs. These plain-text artifacts give any agent the current ground truth: | URL | Content | When an agent should fetch it | |---|---|---| | [/llms.txt](https://scalus.org/llms.txt) | Index of everything below | First contact | | [/llms-api.txt](https://scalus.org/llms-api.txt) | Version-pinned public API signatures: prelude, builtins, validator traits, ledger, TxBuilder, Emulator, testkit | Before writing any Scalus code | | [/llms-examples.txt](https://scalus.org/llms-examples.txt) | 21 complete example validators with their tests | When writing a new validator or test | | [/llms-full.txt](https://scalus.org/llms-full.txt) | All documentation pages as one markdown file | Deep dives | Every documentation page is also available as plain markdown: append `.md` to its URL, e.g. [/docs/smart-contracts/validators.md](https://scalus.org/docs/smart-contracts/validators.md). All artifacts are generated from the source of truth on every release – the API cheatsheet comes from the compiled code itself, so it cannot drift from the published library. ## AI-ready project templates Projects scaffolded from the [hello.g8 or validator.g8 templates](/docs/get-started/project-commands) are AI-ready out of the box: - **`AGENTS.md`** – the cross-tool agent instruction file: build/test commands, the on-chain subset rules (`@Compile` restrictions, `toData` comparisons, prelude-only imports), common pitfalls, and the artifact URLs above. Claude Code, Cursor, and most agents read it automatically. `CLAUDE.md` points at it. - **`.claude/skills/`** – the five Scalus task skills (below), pre-installed. ## Claude Code plugin For existing projects, install the skills with the Scalus plugin: ``` /plugin marketplace add scalus3/scalus /plugin install scalus@scalus ``` The plugin ships five skills that load on demand when the task matches: | Skill | Use | |---|---| | `contract` | Writing validators | | `contract-test` | Testing validators | | `local-development` | Emulator + TxBuilder development loop | | `optimize-contract` | Execution-budget optimization review | | `smart-contract-security-review` | Pre-deploy security audit | Skills are plain markdown in [`scalus-skills/`](https://github.com/scalus3/scalus/tree/master/scalus-skills) – agents other than Claude Code can read the `SKILL.md` files directly. ## Tips for best results - Tell your agent to **fetch `/llms-api.txt` before writing code** and check every signature it plans to use. This is the single highest-impact instruction. - Point it at `/llms-examples.txt` and ask it to imitate the HTLC example – agents follow working code better than prose. - Have it **measure execution budgets** with the testkit assertions (`assertBudgetWithin`) instead of guessing. - Ask for negative tests: every validator test suite should prove the failure cases fail. --- Source: https://scalus.org/docs/get-started/for-scala-developers --- # Scalus for Scala Developers Build Cardano smart contracts using the Scala you already know. Get your first validator running in 30 minutes with familiar tools like IntelliJ IDEA, ScalaTest, and SBT. ## Why Scalus Works for Scala Developers You already understand: - ✓ Scala 3 syntax and type system - ✓ Functional programming patterns - ✓ Case classes, pattern matching, and collections - ✓ Testing with ScalaTest/ScalaCheck You'll learn: - **Cardano's eUTxO model** - How blockchain state works - **Plutus validators** - On-chain code that controls funds - **Scalus constraints** - Which Scala features compile to UPLC - **Blockchain-specific patterns** - Datums, redeemers, script context **Time Investment:** ~2-3 hours to first working validator, ~1 day to proficiency --- ## Your Learning Path ### Step 1: Quick Start (30 minutes) **Goal:** Get your first validator running and debugged in your IDE #### Check Hello Cardano Contract - [ ] **[First Scalus smart contract](/docs/get-started#get-your-first-scalus-validator)** - Explore Hello Cardano validator #### Write you First Smart Contract - [ ] **[Write your first smart contract](/docs/smart-contracts/developing-smart-contracts)** - Build a spending validator - [ ] **[Debug in your IDE](/docs/testing/debugging)** - Set breakpoints and inspect execution **🎯 Quick Win:** You'll have a working Cardano validator and debug it with IntelliJ IDEA's debugger. Yes, **real breakpoints on blockchain code!** **What You Just Learned:** - Validators are just Scala objects with an `@Compile` annotation - You can debug them before deploying to blockchain - The development experience feels like regular Scala development --- ### Step 2: Understand Cardano Blockchain (1-2 hours) **Goal:** Understand how Cardano's eUTxO model and validators work #### Blockchain Concepts (New to You) **Coming from Web2?** Think of validators as smart locks on digital assets. They define rules for who can unlock/spend funds. **Essential Reading:** - [ ] **[Understanding eUTxO Model](https://docs.cardano.org/about-cardano/learn/eutxo-explainer)** - How Cardano manages state (15 min) - [ ] **[The EUTxO-Model - Plutus Pioneer Program](https://www.youtube.com/watch?v=ulYDNaEKf4g)** - Learning details (25 min) - [ ] **[Validators in Depth](/docs/smart-contracts/validators)** - Spending, minting, rewarding, certifying validators (20 min) - [ ] **[HTLC Tutorial](/docs/smart-contracts/htlc-tutorial)** - Complete Hash Time-Locked Contract example (45 min) #### Scalus-Specific Concepts - [ ] **[Supported Scala Features](/docs/language-guide/support)** - **CRITICAL:** Not all Scala works on-chain! - [ ] **[Primitive Types](/docs/language-guide/constants-primitives)** - `BigInt`, `ByteString`, `Data` instead of `Int`, `Array[Byte]`, etc. - [ ] **[Plutus Data](/docs/smart-contracts/plutus-data)** - Automatic serialization for case classes **🎯 Quick Win:** Build a multi-signature wallet validator using pattern matching and Scala collections. **Important Constraint:** Not all Scala features compile to Plutus Core (UPLC): ✅ **Supported:** vals, defs, case classes, enums, pattern matching, lambdas, recursion, `inline` ❌ **Not Supported:** `var`, try-catch (except `throw`), mutable collections, arbitrary type classes Always check [Supported Features](/docs/language-guide/support)! --- ### Step 3: Development Lifecycle (2-3 hours) **Goal:** Build tested, optimized validators ready for testnet/mainnet #### Testing & Quality - [ ] **[Testing Smart Contracts](/docs/testing/unit-testing)** - Use ScalaCheck for property-based testing - [ ] **[Custom Data Types](/docs/language-guide/data-types)** - Define datums and redeemers with case classes - [ ] **[Collections](/docs/language-guide/collections)** - `List`, `AssocMap` for on-chain data **Testing Example:** ```scala class MyValidatorTest extends AnyFunSuite with ScalusTest: test("validator accepts valid signature") { val context = makeSpendingScriptContext( datum = ownerPkh.toData, redeemer = signatureData, signatories = List(ownerPkh) ) val compiled = PlutusV3.compile(MyValidator.validate) val result = compiled.runScript(context) assert(result.isSuccess) // Familiar ScalaTest! } ``` #### Build & Deploy - [ ] **[Compiling Validators](/docs/smart-contracts/compiling)** - Generate UPLC bytecode and contract blueprints - [ ] **Evaluating Scripts (In progress)** - Run validators locally, check execution costs - [ ] **[Builtin Functions](/docs/language-guide/builtin-functions)** - Cryptographic operations and blockchain primitives #### Off-Chain Transaction Building - [ ] **[Transaction Builder](/docs/transactions)** - Build complete Cardano transactions in Scala - [ ] **[Building Your First Transaction](/docs/transactions/building-first-transaction)** - Send ADA and native tokens - [ ] **[Spending UTxOs](/docs/transactions/spending-utxos)** - Spend from validator scripts with redeemers - [ ] **[Minting & Burning Assets](/docs/transactions/minting-burning-assets)** - Create and manage native tokens - [ ] **[Staking & Rewards](/docs/transactions/staking-rewards)** - Stake delegation and reward withdrawal - [ ] **[Governance](/docs/transactions/governance)** - DRep registration and voting delegation **🎯 Quick Win:** Deploy a fully tested validator to Cardano testnet and build a complete off-chain application to interact with it. --- ### Step 4: Master Advanced Patterns **Goal:** Master optimizations and advanced validator patterns #### Optimisations - [ ] **[Smart Contract Optimisations](/docs/smart-contract-optimisations)** - Reduce script size and execution costs - [ ] **[Modules](/docs/language-guide/modules)** - Build reusable validator libraries - [ ] **[Real-World Examples](https://github.com/scalus3/scalus/tree/master/scalus-examples)** - Study production patterns - [ ] **[Transaction Builder](/docs/transactions)** - Build complete off-chain applications #### Design Patterns & Security - [ ] **[Design Patterns](/docs/design-patterns)** - Proven patterns for efficient multi-input validators - [ ] **[Security Guide](/docs/security)** - Common security issues and mitigations **🎯 Quick Win:** Optimize a validator to reduce execution units by 20-30% using inlining and macros. --- ## Common Questions from Scala Devs ### "What Scala features can I use?" **The Good News:** Most functional Scala works! ✅ **Fully Supported:** - `val`, `def`, case classes, enums - Lambdas and higher-order functions - Pattern matching on case classes/enums - Recursion and tail recursion - `given` parameters and `using` clauses - `inline` and compile-time macros - Extension methods ❌ **Not Supported:** - `var` (mutable variables) - `try-catch` (but `throw` works) - Mutable collections - Complex type class derivation - Effects (IO, Future, etc.) See the complete list: [Supported Features](/docs/language-guide/support) ### "Can I use my favorite Scala libraries?" **For on-chain code:** No. Validators compile to UPLC and can only use `scalus.*` libraries. **For off-chain code:** Yes! Use any JVM library for: - Building transactions - Testing validators - Backend services - Frontend integration Think of it like Scala.js: different target platform = different available libraries. ### "How does compilation work?" ``` Your Scala Code (@Compile annotation) ↓ Scalus Compiler Plugin ↓ SIR (Scalus Intermediate Representation) ↓ UPLC (Untyped Plutus Core) ↓ Plutus Script (runs on Cardano) ``` **Two modes:** 1. **JVM execution** - For testing and debugging (fast, debuggable) 2. **UPLC compilation** - For on-chain deployment (verifiable, immutable) ### "What about performance?" **On-chain execution is metered:** - Memory usage (bytes) - CPU steps (execution units) - Script size (affects fees) Scalus provides: - **Built-in cost estimation** - See execution units before deploying - **Optimization passes** - Reduce script size and costs - **Profiling tools** - Identify expensive operations See: [Smart Contract Optimisations](/docs/smart-contract-optimisations) ### "How does error handling work?" **On-chain:** No try-catch, but you can use `require` and `fail`: ```scala inline def spend(datum: Option[Data], redeemer: Data, tx: TxInfo, ownRef: TxOutRef): Unit = val owner = datum.getOrFail("Missing datum").to[PubKeyHash] // This throws if condition is false require(tx.signatories.contains(owner), "Not signed by owner") // Explicit fail also works if !validCondition then fail("Invalid condition") ``` **Off-chain (testing):** Full Scala error handling: ```scala test("validator rejects invalid signature") { val compiled = PlutusV3.compile(MyValidator.validate) val result = compiled.runScript(context) assert(result.isFailure) // Check it failed assert(result.logs.exists(_.contains("Invalid signature"))) // Check error message } ``` --- ## Common Pitfalls & Solutions **Pitfall #1: Using Scala stdlib types on-chain** **Problem:** ```scala @Compile object Bad extends Validator: def spend(...) = val data = scala.collection.immutable.List(1, 2, 3) // ❌ Wrong! ``` **Solution:** ```scala @Compile object Good extends Validator: def spend(...) = val data = scalus.cardano.onchain.plutus.prelude.List(1, 2, 3) // ✅ Correct! ``` Use `scalus.cardano.onchain.plutus.prelude.List`, not `scala.collection.List`. **Pitfall #2: Forgetting `@Compile` annotation** **Problem:** Code runs in tests but doesn't compile to UPLC. **Solution:** Always annotate validators: ```scala @Compile // ← Essential! object MyValidator extends Validator ``` **Pitfall #3: Using unsupported Scala features** **Problem:** Code compiles in Scala but fails in Scalus plugin. **Solution:** Check [Supported Features](/docs/language-guide/support) before using advanced features. Stick to simple `val`, `def`, case classes, and pattern matching. --- ## Essential Cardano Resources ### Blockchain Fundamentals - **[Cardano eUTxO Model](https://docs.cardano.org/learn/eutxo-explainer)** - How state works on Cardano - **[Plutus Overview](https://docs.cardano.org/developer-resources/smart-contracts/plutus)** - Cardano's smart contract platform - **[Cardano Developer Portal](https://developers.cardano.org/)** - Official developer resources ### Scalus-Specific - **[Scalus Examples](https://github.com/scalus3/scalus/tree/master/scalus-examples)** - Real validator code - **[API Documentation](https://scalus.org/api/index.html)** - Complete API reference - **[Language Guide](/docs/language-guide)** - Scalus language features --- ## Get Help Need assistance? Connect with the Scalus community: - **Join Discord:** [Ask questions](https://discord.gg/B6tXmBzhTn) - **Join Scalus Club:** [Check new features & discuss](https://luma.com/scalus) - **Examples:** [Real-world validator code](https://github.com/scalus3/scalus/tree/master/scalus-examples) Happy building! 🚀 --- Source: https://scalus.org/docs/get-started/for-cardano-developers --- # Scalus for Cardano Developers Welcome! You know Cardano's eUTxO model, Plutus, and blockchain fundamentals. Now learn how to build validators using Scala 3's powerful type system and professional tooling. ## Why This Path Works for You You already understand: - ✓ UTxO model, datums, and redeemers - ✓ Validator logic and on-chain execution - ✓ Plutus Core and script evaluation You'll learn: - **Scala 3 fundamentals** - Just enough to write validators - **Type-safe development** - Catch errors at compile time - **Professional debugging** - Use IDE breakpoints, not trace logs - **Familiar concepts** - Datums, redeemers, and script context in Scala **Time Investment:** ~4-6 hours to productivity, ~2-3 days to proficiency --- ## Your Journey in 4 Steps ### Step 1: Quick Start (30-45 minutes) **Goal:** Get your first Scalus validator running and see it work in your IDE #### Setup - [ ] **[Install Scalus](/docs/get-started)** - Set up Scala, SBT, and your development environment #### First Contract - [ ] **[Write your first smart contract](/docs/smart-contracts/developing-smart-contracts)** - Build a "Hello, Cardano!" spending validator - [ ] **[Debug in your IDE](/docs/testing/debugging)** - Set breakpoints and step through validator execution **🎯 Quick Win:** You'll have a working validator and see it execute step-by-step in IntelliJ IDEA or VS Code. No more blind `trace` debugging! **What You Just Learned:** - Scalus validators look familiar (datums, redeemers, script context) - But you can debug them like regular code with breakpoints - The `@Compile` annotation marks code for on-chain compilation --- ### Step 2: Learn Scala Essentials (2-3 hours) **Goal:** Understand enough Scala to write and read validators confidently #### Scala 3 Crash Course for Cardano Devs **Don't Skip This:** You need Scala basics to be productive. These resources are curated for speed. **Option A: Quick Video Course (Recommended)** - [ ] **[Scala 3 for Beginners](https://www.youtube.com/watch?v=-8V6bMjThNo)** by Rock the JVM (2 hours) - Focus on: vals, defs, case classes, pattern matching, collections - Skip: implicits, type classes, advanced features (you won't need them initially) **Option B: Interactive Tutorial** - [ ] **[Scala Exercises - Std Lib](https://www.scala-exercises.org/std_lib)** - Hands-on practice - [ ] **[Tour of Scala](https://docs.scala-lang.org/tour/tour-of-scala.html)** - Official quick tour - Focus on: Basics, Classes, Pattern Matching, Collections **Option C: Quick Reference (If you're in a hurry)** - [ ] **[Scala 3 Syntax Summary](https://docs.scala-lang.org/scala3/book/taste-intro.html)** - 15-minute overview - [ ] Then jump straight to writing code and learn by doing #### Scalus-Specific Scala Features - [ ] **[Why Scala 3?](/docs/language-guide/scala3)** - Why it's great for blockchain development - [ ] **[Supported Scala Features](/docs/language-guide/support)** - What works in Scalus (CRITICAL to read!) - [ ] **[Primitive Types](/docs/language-guide/constants-primitives)** - `BigInt`, `ByteString`, `Data`, etc. - [ ] **[Functions](/docs/language-guide/functions)** - Defining functions, lambdas, higher-order functions - [ ] **[Pattern Matching](/docs/language-guide/control-flow)** - `if-then-else`, `match` expressions **🎯 Quick Win:** Write a simple token minting policy using pattern matching and Scala collections. --- ### Step 3: Cardano-Specific Development (2-3 hours) **Goal:** Build production-ready validators using Cardano-specific features #### Core Validator Concepts - [ ] **[Validators in Depth](/docs/smart-contracts/validators)** - Spending, minting, rewarding, certifying validators in Scalus - [ ] **[HTLC Tutorial](/docs/smart-contracts/htlc-tutorial)** - Complete Hash Time-Locked Contract with transactions and tests - [ ] **[Plutus Data](/docs/smart-contracts/plutus-data)** - Automatic conversion between Scala types and Plutus `Data` - [ ] **[Custom Data Types](/docs/language-guide/data-types)** - Define datums and redeemers with case classes #### Testing & Quality - [ ] **[Testing Smart Contracts](/docs/testing/unit-testing)** - Property-based testing with ScalaCheck - [ ] **[Builtin Functions](/docs/language-guide/builtin-functions)** - Cryptographic operations, hashing, serialization #### Build & Deploy - [ ] **[Compiling Validators](/docs/smart-contracts/compiling)** - Generate UPLC, create blueprints, attach to transactions - [ ] **Evaluating Scripts (In progress)** - Run validators locally, check execution costs #### Off-Chain Transaction Building - [ ] **[Transaction Builder](/docs/transactions)** - Build complete Cardano transactions in Scala - [ ] **[Building Your First Transaction](/docs/transactions/building-first-transaction)** - Send ADA and native tokens - [ ] **[Spending UTxOs](/docs/transactions/spending-utxos)** - Spend from validator scripts - [ ] **[Minting & Burning Assets](/docs/transactions/minting-burning-assets)** - Create and burn native tokens - [ ] **[Staking & Rewards](/docs/transactions/staking-rewards)** - Register, delegate, and withdraw rewards - [ ] **[Governance](/docs/transactions/governance)** - Participate in Cardano governance with DReps **🎯 Quick Win:** Build a tested, optimized validator and deploy it to testnet with a complete off-chain application that interacts with it. --- ### Step 4: Production Ready (Ongoing) **Goal:** Master advanced patterns and optimizations #### Optimisations - [ ] **[Smart Contract Optimisations](/docs/smart-contract-optimisations)** - Reduce script size and execution costs - [ ] **[Collections](/docs/language-guide/collections)** - Efficient `List` and `AssocMap` operations - [ ] **[Modules](/docs/language-guide/modules)** - Organize code into reusable libraries - [ ] **[Real-World Examples](https://github.com/scalus3/scalus/tree/master/scalus-examples)** - Study production validator patterns #### Design Patterns & Security - [ ] **[Design Patterns](/docs/design-patterns)** - Proven patterns for efficient multi-input validators - [ ] **[Security Guide](/docs/security)** - Common security issues and mitigations **🎯 Quick Win:** Optimize a validator to reduce execution units by 20-30% using advanced techniques. --- ## Common Questions from Cardano Devs ### "Do I really need to learn Scala?" **Short answer:** Yes, but not all of it. You need ~10% of Scala to be productive: - Basic syntax (vals, defs, case classes) - Pattern matching (you already know this concept from Plutus) - Collections (List, Map) - Type annotations That's it. You can learn this in 2-3 hours and be writing validators. ### "How different is Scalus from Aiken/Plutarch?" **Conceptually:** Almost identical. Datums, redeemers, script context, validators - all the same. **Syntactically:** Different, but familiar patterns: | Concept | Aiken | Scalus | |---------|-------|--------| | Type annotation | `value: Int` | `value: BigInt` | | Pattern matching | `when` | `match` | | List operations | `list.filter(fn)` | `list.filter(fn)` | | Require check | `expect True = ...` | `require(condition, "msg")` | ### "What about debugging?" This is where Scalus shines: **Aiken/Plutarch:** ``` trace @"checkpoint 1" // Hope you see this in logs trace @"value" value // Print debugging ``` **Scalus:** ```scala // Set breakpoint in IDE val owner = datum.to[PubKeyHash] // ← Breakpoint here // Step through, inspect variables, see call stack ``` You get **real debugging** with IntelliJ IDEA or VS Code. ### "Is it production-ready?" Yes. Scalus validators compile to standard Plutus Core and run on Cardano mainnet just like any other Plutus script. --- ## Common Pitfalls & Solutions **Pitfall #1: Trying to use unsupported Scala features** **Problem:** Not all Scala features compile to UPLC (e.g., mutable vars, try-catch, complex type classes). **Solution:** Always check [Supported Features](/docs/language-guide/support) before using advanced Scala features. **Pitfall #2: Forgetting the `@Compile` annotation** **Problem:** Your validator runs in Scala but doesn't compile to UPLC. **Solution:** Always annotate validator objects with `@Compile`: ```scala @Compile // ← Don't forget this! object MyValidator extends Validator ``` **Pitfall #3: Using Scala standard library directly** **Problem:** Scala's `scala.collection.List` doesn't exist on-chain. **Solution:** Use Scalus types: `scalus.cardano.onchain.plutus.prelude.List`, `scalus.uplc.builtin.ByteString`, etc. --- ## Essential Scala Resources ### Quick References - **[Scala 3 Cheat Sheet](https://docs.scala-lang.org/cheatsheets/index.html)** - Syntax at a glance - **[Scala 3 Book](https://docs.scala-lang.org/scala3/book/introduction.html)** - Comprehensive but readable guide ### Video Courses - **[Scala at Light Speed](https://www.youtube.com/watch?v=-8V6bMjThNo)** - Best crash course (2 hours) ### Interactive Learning - **[Scala Exercises](https://www.scala-exercises.org/)** - Hands-on practice - **[Scastie](https://scastie.scala-lang.org/)** - Online Scala playground ### When You're Stuck - **[Scalus Examples](https://github.com/scalus3/scalus/tree/master/scalus-examples)** - Real validator code - **[Scalus API Docs](https://javadoc.io/doc/org.scalus/scalus_3/latest/index.html)** - Complete API reference --- ## Get Help Need assistance? Connect with the Scalus community: - **Join Discord:** [Ask questions](https://discord.gg/B6tXmBzhTn) - **Join Scalus Club:** [Check new features & discuss](https://luma.com/scalus) - **Examples:** [Real-world validator code](https://github.com/scalus3/scalus/tree/master/scalus-examples) Happy building! 🚀 --- Source: https://scalus.org/docs/get-started/migrating-to-1.0 --- # Migrating from 0.18 to 1.0 Scalus 1.0.0-M1 is the first milestone of the 1.0 line. Artifact coordinates are unchanged (`org.scalus:scalus_3`, `org.scalus:scalus-cardano-ledger_3`, ...) – update the version and the compiler plugin together: ```scala libraryDependencies += "org.scalus" %% "scalus" % "1.0.0" addCompilerPlugin("org.scalus" %% "scalus-plugin" % "1.0.0") ``` ## Stability promise From 1.0.0-M1 onward, `scalus-core`, `scalus-cardano-ledger` and `scalus-bloxbean-cardano-client-lib` are the stable surface, checked with MiMa on every build. Most APIs will stay binary compatible across the 1.x line; some parts will likely still have breaking changes in 1.x releases, always behind a deprecation cycle. `scalus-testkit` is best-effort; `*.internal` packages and compiler internals carry no compatibility promise. ## On-chain behavior changes These change generated code, so **every script hash changes** when you recompile. - **Protocol version 11 (van Rossem) is the default** compile and evaluation target. Budgets drop roughly 30% and scripts shrink roughly 20%. To reproduce pre-PV11 output for an already deployed contract, use `Options.plomin` or set `targetProtocolVersion = MajorProtocolVersion.plominPV`. - **`BigInt./` and `BigInt.%` now match Scala semantics** (truncated division, `quotientInteger`/`remainderInteger`). Previously `/` compiled to floor division (`divideInteger`), which differs for negative operands. - **Hand-written `Eq` instances are rejected** in on-chain code: `===` always compiles to structural equality and never calls the instance body. Use `Eq.derived` for case classes, enums and sealed traits, or wrap a structural comparison in `Eq.structural(...)`. ## Removed API | Removed | Use instead | |---|---| | `scalus.compiler.intrinsics.ReprTag` | `scalus.compiler.UplcRepresentation` | | `scalus.ScalusDebug` | `scalus.compiler.ScalusDebug` | | `scalus.CompileDerivations` | `scalus.compiler.CompileDerivations` | | `scalus.serialization.flat` `w7l` | `word7Bytes` | | `scalus.bloxbean.TxEvaluator` | `ScalusTransactionEvaluator` or `scalus.cardano.ledger.PlutusScriptEvaluator` | | `Builtins.multiIndexArray` | `Builtins.indexArray` per element (it was a Scalus invention no Plutus release implements) | ## Binary compatibility notes - **`SIRVersion` is 6.0**: precompiled `.sir` artifacts must be recompiled with the 1.0 compiler plugin. - **`scalus.serialization.flat`** was converted from a package object to top-level definitions. Scala sources compile unchanged; recompile anything that linked against the old binary names. - **Provider traits changed**: `BlockchainReader.findUtxo`/`findUtxos` moved onto the effect-polymorphic `BlockchainReaderTF`, which gained an abstract `mapF`; `EmulatorBase` gained the applied-transaction log (`appliedTxLog`, `appliedTxIndex`, `clearAppliedTxs`). Custom provider or emulator implementations must implement the new members; users of the built-in providers are unaffected. - `ImmutableEmulator` and `Emulator` constructors gained parameters (evaluator mode, transaction log) – use the factory methods rather than the constructors. --- Source: https://scalus.org/docs/get-started --- # Getting Started with Scalus This guide will help you set up your Scalus development environment and create your first Cardano validator in minutes. You'll install Scala 3, generate a starter project, and run your first smart contract tests. ## Install Scala 3 Development Environment We recommend using [Coursier](https://get-coursier.io/docs/cli-overview), the official Scala installer that sets up your complete development environment including JVM, Scala compiler, build tools, and code formatters. Homebrew based installation: ```sh copy brew install coursier && coursier setup ``` On the Apple Silicon (M1, M2, …) architecture: ```sh copy curl -fL https://github.com/VirtusLab/coursier-m1/releases/latest/download/cs-aarch64-apple-darwin.gz | gzip -d > cs && chmod +x cs && (xattr -d com.apple.quarantine cs || true) && ./cs setup ``` Otherwise, on the x86-64 architecture: ```sh copy curl -fL https://github.com/coursier/coursier/releases/latest/download/cs-x86_64-apple-darwin.gz | gzip -d > cs && chmod +x cs && (xattr -d com.apple.quarantine cs || true) && ./cs setup ``` On the x86-64 architecture: ```sh copy curl -fL https://github.com/coursier/coursier/releases/latest/download/cs-x86_64-pc-linux.gz | gzip -d > cs && chmod +x cs && ./cs setup ``` Otherwise, on the ARM64 architecture: ```sh copy curl -fL https://github.com/VirtusLab/coursier-m1/releases/latest/download/cs-aarch64-pc-linux.gz | gzip -d > cs && chmod +x cs && ./cs setup ``` Download and execute [the Scala installer for Windows](https://github.com/coursier/launchers/raw/master/cs-x86_64-pc-win32.zip) based on Coursier, and follow the on-screen instructions. Coursier will install Scala compiler, Command line tool: [Scala CLI](https://scala-cli.virtuslab.org/), Build tool: [sbt](https://www.scala-sbt.org/), REPL: [Ammonite](https://ammonite.io/) and Code formatter: [Scalafmt](https://scalameta.org/scalafmt/). ## Get Your First Cardano Validator ```sh copy sbt new scalus3/hello.g8 ``` This ran the template `scalus3/hello.g8` using [Giter8](https://www.foundweekends.org/giter8/). Let’s take a look at what just got generated: ```ansi hello-cardano/ ├── HelloCardano.scala # Plutus V3 spending validator ├── HelloCardano.test.scala # Unit tests ├── HelloCardanoIntegration.test.scala # Submit-based tests ├── HelloCardanoContract.scala # Contract: compiled script + CIP-57 blueprint ├── project.scala # Scala CLI build configuration ├── build.sbt # sbt build configuration └── Readme.md ``` Want the full DApp toolchain (CIP-57 blueprint generation and on-chain deployment) ready to go? Generate the richer starter instead with `sbt new scalus3/validator.g8`. Both templates, and all the commands their projects support, are covered in [Project Commands](/docs/get-started/project-commands). ## Run Your First Validator Tests Run unit tests (ScalaTest and ScalaCheck): ```sh copy cd hello-cardano && sbt test ``` All is good if you see the following output: ```sh HelloCardanoTest: - Hello Cardano message is signed by the owner HelloCardanoIntegrationTest: - [emulator] owner can spend with the Hello redeemer - [emulator] a non-owner cannot spend ``` ## Set Up Your IDE for Cardano Development Setting up a productive development environment will significantly improve your Scala/Scalus development experience. Scala offers wide range of IDE support, the most popular are IntelliJ and VSCode. 1. Install IntelliJ IDEA (Community or Ultimate edition) from the [JetBrains website](https://www.jetbrains.com/idea/download/) 2. Install the Scala plugin: - Go to **Settings/Preferences → Plugins → Marketplace** - Search for "Scala" and install the plugin - Restart IntelliJ IDEA when prompted {/*(https://www.jetbrains.com/help/idea/discover-intellij-idea-for-scala.html) */} 3. Open your Scalus project: - Select **File → Open** and navigate to your project directory - Choose Import as sbt project when prompted 1. Install [Visual Studio Code](https://code.visualstudio.com/Download) from the official website. 2. Install the Metals extension from [the Marketplace](https://marketplace.visualstudio.com/items?itemName=scalameta.metals) 3. Open your Scalus project folder: - Select **File → Open Folder** and navigate to your project directory. 4. Metals should activate and begin importing the project automatically. Metals is most commonly used with VS Code, but it’s also available for Emacs, Vim, Sublime Text, Helix as documented [here](https://scalameta.org/metals/docs/#editor-support). ## You are good to go! Start exploring. ```scala filename="HelloCardano.scala" copy showLineNumbers package hello import scalus.compiler.Compile import scalus.uplc.builtin.Data import scalus.cardano.onchain.plutus.v3.* import scalus.cardano.onchain.plutus.prelude.* /** This validator demonstrates two key validation checks: * 1. It verifies that the transaction is signed by the owner's public key * hash (stored in the datum). * 1. It confirms that the redeemer contains the exact string "Hello, * Cardano!". * * Both conditions must be met for the validator to approve spending the UTxO. */ @Compile object HelloCardano extends Validator { inline override def spend( datum: Option[Data], redeemer: Data, tx: TxInfo, ownRef: TxOutRef ): Unit = { val owner = datum.getOrFail("Datum not found").to[PubKeyHash] require(tx.isSignedBy(owner), "Must be signed") val saysHello = redeemer.to[String] == "Hello, Cardano!" require(saysHello, "Invalid redeemer") } } ``` ## Next Steps Now that your environment is ready, choose your path: **Learn the fundamentals:** - [Project Commands](/docs/get-started/project-commands) — Templates plus every sbt/Scala CLI command: test, profile, blueprint, deploy - [Write Smart Contracts](/docs/smart-contracts/developing-smart-contracts) — Validators, data conversion, compilation - [Build Transactions](/docs/transactions/building-first-transaction) — Construct and submit transactions - [Test Locally](/docs/testing/unit-testing) — Unit testing and debugging **Go deeper:** - [DApp Starter Tutorial](/docs/dapp-development/dapp-starter-tutorial) — Build a complete token minting service - [Design Patterns](/docs/design-patterns) — Optimization patterns for efficient contracts - [Language Guide](/docs/language-guide) — Scala 3 features supported in Scalus **New to Cardano or Scala?** - [For Scala Developers](/docs/get-started/for-scala-developers) — Cardano concepts - [For Cardano Developers](/docs/get-started/for-cardano-developers) — Scala basics --- Source: https://scalus.org/docs/multiplatform --- # Scalus Multiplatform — Cardano on JVM, JavaScript, and Native Scalus compiles to **JVM**, **JavaScript**/**TypeScript**, and **Native** from the same Scala 3 codebase. Write once, deploy anywhere — from server-side backends to browser wallets to CLI tools. ## Platform Support Matrix | Feature | JVM | JS/TS | Native | |---------|:---:|:-----:|:------:| | Smart contracts / UPLC | ✓ (Scala) | — | — | | [SIR compiler](/docs/smart-contract-optimisations/lowering-backends) | ✓ (Scala) | — | — | | [Standard library](/docs/language-guide) | ✓ | ✓ (Off-chain) | ✓ (Off-chain) | | [Transaction builder](/docs/transactions) | ✓ | ✓ | — | | Script evaluation / Cost calculation | ✓ | ✓ | — | | Plutus VM | ✓ | ✓ | ✓ | | [Ledger Rules / Framework](/docs/ledger/ledger-rules) | ✓ | ✓ | — | | [Testing utilities ](/docs/testing) | ✓ | ✓ | ✓ | | [Emulator](/docs/testing/emulator) | ✓ | [✓](/docs/testing/js-emulator) | — | | [Yaci DevKit](/docs/testing/local-devnet) | ✓ | — | — | | Ed25519 signatures | ✓ | ✓ | ✓ | | ECDSA secp256k1 | ✓ | ✓ | ✓ | | Schnorr signatures | ✓ | ✓ | ✓ | | BLS12-381 | ✓ | ✓ | ✓ | ## When to Use Which Platform ### JVM — Full DApp Development The JVM platform provides the complete Scalus stack: - **Smart contract development** with the compiler plugin (Scala 3) - **Transaction building** with TxBuilder - **Local testing** with Emulator and Yaci DevKit - **Integration** with Bloxbean Cardano Client Library - **Production backends** with mature JVM ecosystem **Best for:** Backend services, full DApp development, integration testing, production deployments. ### JavaScript/TypeScript — Offchain & Browser JavaScript/TypeScript enables Scalus in Node.js and browser environments: - **Script evaluation** — Run Plutus V1/V2/V3 scripts - **Transaction building** — Construct and sign transactions - **Wallet integration** — Works with Lucid Evolution - **Cost estimation** — Calculate execution units offchain - **Browser DApps** — Full offchain logic in the browser See the [`scalus` npm package](https://www.npmjs.com/package/scalus) for JavaScript/TypeScript API, installation, and usage examples. **Best for:** Browser wallets, Node.js tooling, Lucid-based DApps, offchain computation. ### Native — CLI & Embedded Scala Native compiles to native binaries: - **Fast startup** — No JVM warmup - **Small footprint** — Standalone executables - **System integration** — FFI to C libraries **Best for:** CLI tools, embedded systems, static libraries. ## Get in Touch **Building a JavaScript/TypeScript or Native integration?** We're actively looking for feedback and early adopters. [Open an issue](https://github.com/scalus3/scalus/issues) or reach out — we'd love to help with your integration. ## See Also - [Getting Started](/docs/get-started) — Set up JVM development environment - [Emulator](/docs/testing/emulator) — In-memory testing (JVM) - [JS/TS Emulator](/docs/testing/js-emulator) — In-memory testing (JavaScript/TypeScript) - [Transaction Builder](/docs/transactions) — Build transactions (JVM/JS) - [Local Devnet](/docs/testing/local-devnet) — Integration testing (JVM only) --- Source: https://scalus.org/docs/language-guide/scala3 --- # Why Scala 3 for Cardano? Scala 3 is a powerful, modern programming language that combines object-oriented and functional programming paradigms. It offers cleaner syntax, better type inference, and enhanced metaprogramming capabilities—making it ideal for writing secure, type-safe smart contracts. ## What Makes Scala 3 Exceptional for Blockchain? 1. **Multi-platform versatility**: Runs on JVM, compiles to JavaScript, and supports native compilation via LLVM, giving developers flexibility across environments. 2. **Industry-proven reliability**: Major financial institutions and tech companies rely on Scala for mission-critical systems: - Payment processors like Klarna and Stripe use it for high-volume transaction systems - Investment banks including Morgan Stanley and Deutsche Bank leverage it for time-sensitive trading platforms and analytics - Tech giants such as Twitter, LinkedIn, and Netflix use Scala for their data processing pipelines 3. **Perfect for blockchain**: Scala's immutable data structures, strong type system, and pattern matching make it ideal for writing secure, predictable smart contracts and blockchain applications. The language's widespread adoption in finance and enterprise environments demonstrates its capability to handle complex, high-stakes applications—exactly what's needed for blockchain development. --- Source: https://scalus.org/docs/language-guide/support --- # Supported Scala Features Scalus compiles Scala code to Untyped Plutus Core (UPLC), a minimalist lambda calculus designed for secure blockchain execution. This constraint means only a subset of Scala's feature set is supported—those that can be efficiently and deterministically translated to UPLC. The compilation process transforms your Scala code into an intermediate representation before generating the final UPLC that runs on Cardano. UPLC's deliberately limited nature (optimized for security and determinism) excludes many high-level language features. ## Supported Features Below is a comprehensive list of what Scala features are currently supported in Scalus: * simple `val`s and `def`s of supported built-in types or case classes/enums * lambda expressions * recursive functions * passing/returning functions as arguments (higher-order functions) * `if-then-else` expressions * `match` expressions on case classes and enums with: * nested patterns: `case List.Cons(Option.Some(v), _) => ...` * wildcard patterns: `case _ => ...` * constant patterns for booleans and strings: `case true => ...`, `case "hello" => ...` * guard clauses: `case Option.Some(v) if v > 0 => ...` * variable bindings with `@`: `case some @ Option.Some(v) => ...` * non-exhaustive matching with `@unchecked` * `given` arguments and `using` clauses * `throw` expressions but no `try-catch` expressions * built-in functions and operators * simple data types: case classes and enums * `inline` vals, functions and macros in general * implicit conversions * opaque types (non top-level) and type aliases * extension methods * tuples * value destructuring in vals: `val Some((a, b)) = optionOfTuple` ## Unsupported Features * `var`s and `lazy val`s - use immutable `val` declarations instead * by-name parameters (`b: => T`) - rejected at compile time, because Scalus would evaluate the argument strictly, unlike Scala; use an `inline` method with an `inline` parameter, or an explicit function parameter `b: () => T` * `while` loops - use recursion or higher-order functions like `fold` * classes, inheritance and polymorphism aka virtual dispatch * you can't use `isInstanceOf` or runtime type checks * use sealed traits with enums or case classes for polymorphic data * `try-catch` expressions - use `Option`, `Either`, or error handling patterns instead * overloaded functions - each function must have a unique name * mutually recursive functions - functions cannot call each other recursively --- Source: https://scalus.org/docs/language-guide/constants-primitives --- # Primitive Types Scalus provides full support for Plutus V3 primitive types, enabling you to write Cardano smart contracts using familiar Scala syntax. These types map directly to Plutus Core primitives for efficient blockchain execution. ## Type Correspondence The following table shows how primitive types map between Plutus, Scalus, and Aiken: | Plutus V3 | Scalus | Aiken | |------------------------|------------------------|--------------------| | `unit` | `Unit` | `Void` | | `bool` | `Boolean` | `Bool` | | `integer` | `BigInt` | `Int` | | `bytestring` | `ByteString` | `ByteArray` | | `string` | `String` | `String` | | `data` | `Data` | `Data` | | `list` | `List[A]` | `List` | | `pair` | `Pair[A, B]` | `Pair` | | `BLS12_381_G1_Element` | `G1Element` | `G1Element` | | `BLS12_381_G2_Element` | `G2Element` | `G2Element` | | `BLS12_381_MlResult` | `MLResult` | `MillerLoopResult` | Scalus leverages Scala's native types for `Unit`, `Boolean`, `BigInt`, and `String`, providing a familiar programming experience. For blockchain-specific types, Scalus offers custom implementations with convenient constructors and utility methods. ## Creating Values ### Unit The unit type represents the absence of a meaningful value. It's similar to `void` in other languages but is an actual value. ```scala val unit: Unit = () ``` ### Boolean Boolean values are either `true` or `false`. Scalus supports all standard boolean operators. ```scala val bool = true val and = true && false // logical AND val or = true || false // logical OR val not = !true // logical NOT val eq = true == false // equality val neq = true != false // inequality ``` ### BigInt Arbitrary precision integers are the primary numeric type in Plutus. Scalus uses Scala's `BigInt`. ```scala val small = BigInt(123) val large = BigInt("123456789012345678901234567890") val fromInt: BigInt = 42 // automatic conversion from Int // Arithmetic operations val sum = BigInt(10) + BigInt(20) val diff = BigInt(100) - BigInt(30) val product = BigInt(5) * BigInt(7) val quotient = BigInt(20) / BigInt(4) val remainder = BigInt(17) % BigInt(5) // Comparison val greater = BigInt(10) > BigInt(5) val less = BigInt(3) < BigInt(7) val greaterOrEqual = BigInt(10) >= BigInt(10) val lessOrEqual = BigInt(5) <= BigInt(10) val equals = BigInt(42) == BigInt(42) ``` `/` and `%` follow Scala's semantics on-chain: truncated division (rounding toward zero), compiled to the `quotientInteger` and `remainderInteger` builtins. They satisfy `(a / b) * b + a % b == a`. If you need floor division (rounding toward negative infinity), use `Builtins.divideInteger` and `Builtins.modInteger` explicitly. ### ByteString ByteStrings are immutable byte arrays, commonly used for hashes, cryptographic keys, and binary data. ```scala import scalus.uplc.builtin.ByteString import scalus.uplc.builtin.ByteString.* // Creating ByteStrings val empty = ByteString.empty val fromHex = ByteString.fromHex("deadbeef") val fromArray = ByteString.fromArray(Array[Byte](0xde.toByte, 0xad.toByte)) val fromString = ByteString.fromString("Hello") // UTF-8 encoded val utf8Literal = utf8"Привіт світ" // using utf8 string interpolator val hexLiteral = hex"deadbeef" // using hex string interpolator // Operations val concat = hex"dead" ++ hex"beef" val length = fromHex.length val take = fromHex.take(2) // first 2 bytes val drop = fromHex.drop(2) // skip first 2 bytes val slice = fromHex.slice(1, 3) // bytes from index 1 to 3 // Comparison val eq = fromHex == hex"deadbeef" val compare = fromHex < hex"ffffff" ``` ### String Strings are Unicode text values. ```scala val greeting = "Hello, Cardano!" val concat = "Hello" ++ " " ++ "World" val eq = "test" == "test" ``` ### Data `Data` is Plutus's universal representation type, used for serialization and interoperability. Any value can be encoded as `Data`, making it essential for on-chain communication. **What is Data?** `Data` is a tree-like structure that can represent: - **Integers** (`I` constructor) - **ByteStrings** (`B` constructor) - **Lists** of Data (`List` constructor) - **Maps** from Data to Data (`Map` constructor) - **Constructors** with an integer tag and list of Data fields (`Constr` constructor) Think of `Data` as a universal serialization format similar to JSON, but optimized for blockchain use. ```scala import scalus.uplc.builtin.Data import scalus.uplc.builtin.Builtins // Creating Data values val intData = Builtins.iData(42) val bytesData = Builtins.bData(hex"deadbeef") val listData = Builtins.listData(List(intData, bytesData)) val mapData = Builtins.mapData(List((intData, bytesData))) val constrData = Builtins.constrData(0, List(intData, bytesData)) // Deconstructing Data val extractedInt: BigInt = Builtins.unIData(intData) val extractedBytes: ByteString = Builtins.unBData(bytesData) val extractedList: List[Data] = Builtins.unListData(listData) val extractedMap: List[(Data, Data)] = Builtins.unMapData(mapData) val (tag, fields) = Builtins.unConstrData(constrData) // Comparison val dataEq = intData == Builtins.iData(42) ``` **Converting to/from Data** Scalus automatically generates `ToData` and `FromData` instances for case classes and enums, enabling seamless conversion: ```scala import scalus.uplc.builtin.Data.* case class Account(owner: ByteString, balance: BigInt) val account = Account(hex"abc123", 1000) val accountData: Data = account.toData val recovered: Account = FromData.fromData(accountData) val recovered2: Account = accountData.to[Account] ``` ### List Immutable linked lists are the primary collection type. ```scala // Creating lists val empty = List.empty[BigInt] val numbers = List(1, 2, 3, 4, 5) val cons = 0 :: List(1, 2, 3) // prepend element // Operations val head = numbers.head // first element (1) val tail = numbers.tail // rest of list val isEmpty = numbers.isEmpty val length = numbers.length // Higher-order functions val doubled = numbers.map(_ * 2) val evens = numbers.filter(_ % 2 == 0) val sum = numbers.foldLeft(0)(_ + _) ``` ### Pair Pairs (2-tuples) hold exactly two values of potentially different types. ```scala import scalus.uplc.builtin.BuiltinPair // Creating pairs val pair = BuiltinPair(BigInt(42), ByteString.fromHex("deadbeef")) val tuple: (Boolean, Unit) = (true, ()) // Accessing elements val first = pair.fst val second = pair.snd val (a, b) = tuple // destructuring ``` ### BLS12-381 Types Cryptographic elliptic curve types for advanced cryptography and zero-knowledge proofs. ```scala import scalus.uplc.builtin.Builtins import scalus.uplc.builtin.bls12_381.{G1Element, G2Element, MLResult} val g1Point: G1Element = ??? // from bytestring val g2Point: G2Element = ??? // from bytestring val mlResult: MLResult = Builtins.bls12_381_millerLoop(g1Point, g2Point) ``` ## Type Safety Scalus ensures type safety at compile time. Type mismatches are caught before deployment: ```scala val valid: BigInt = 42 val invalid: BigInt = "not a number" // Compile error! ``` This prevents many common smart contract vulnerabilities by catching errors early in the development process. --- Source: https://scalus.org/docs/language-guide/builtin-functions --- # Builtin Functions Cardano Plutus provides built-in functions optimized for blockchain execution. Scalus exposes these functions through the [`Builtins`](https://scalus.org/api/scalus/uplc/builtin/Builtins$.html) object with familiar Scala syntax. These builtin functions are automatically recognized by the Scalus compiler and compiled to their corresponding Plutus operations for efficient on-chain execution. ## Plutus Versions Builtin functions are available in different Plutus versions: - **Plutus V1** - Original builtins (integers, bytestrings, lists, data, basic crypto) - **Plutus V2** - Added `serialiseData`, SECP256k1 signatures - **Plutus V3** - Added BLS12-381, Keccak-256, bitwise operations - **Plutus V3 (Protocol Version 11+)** - Added array operations, `dropList`, `ripemd_160`, BuiltinValue operations ## Integer Operations Arithmetic and comparison operations on arbitrary-precision integers (`BigInt`). ```scala import scalus.uplc.builtin.Builtins // Arithmetic val sum = Builtins.addInteger(BigInt(10), BigInt(20)) val diff = Builtins.subtractInteger(BigInt(100), BigInt(30)) val product = Builtins.multiplyInteger(BigInt(5), BigInt(7)) val quotient = Builtins.divideInteger(BigInt(20), BigInt(4)) // floor division val quot = Builtins.quotientInteger(BigInt(20), BigInt(4)) // truncated division val remainder = Builtins.remainderInteger(BigInt(17), BigInt(5)) val modulo = Builtins.modInteger(BigInt(17), BigInt(5)) // Comparison val eq = Builtins.equalsInteger(BigInt(42), BigInt(42)) val lt = Builtins.lessThanInteger(BigInt(3), BigInt(7)) val lte = Builtins.lessThanEqualsInteger(BigInt(5), BigInt(5)) // Or use operators val sumOp = BigInt(10) + BigInt(20) val eqOp = BigInt(42) == BigInt(42) ``` `divideInteger` uses floor division (rounds toward negative infinity), while `quotientInteger` uses truncated division (rounds toward zero). The difference matters for negative numbers. ## ByteString Operations Operations on immutable byte arrays, organized by category. ### Constructing ```scala import scalus.uplc.builtin.{Builtins, ByteString} // Append two ByteStrings val concatenated = Builtins.appendByteString( ByteString.fromHex("dead"), ByteString.fromHex("beef") ) // Prepend a byte (0-255) val withPrefix = Builtins.consByteString(BigInt(0xFF), ByteString.fromHex("1234")) ``` ### Inspecting ```scala // Length of ByteString val length = Builtins.lengthOfByteString(ByteString.fromHex("deadbeef")) // 4 // Get byte at index val byte = Builtins.indexByteString(ByteString.fromHex("deadbeef"), BigInt(0)) // 0xDE // Slice ByteString val slice = Builtins.sliceByteString(BigInt(1), BigInt(2), ByteString.fromHex("deadbeef")) // hex"adbe" ``` ### Comparing ```scala // Equality val eq = Builtins.equalsByteString( ByteString.fromHex("deadbeef"), ByteString.fromHex("deadbeef") ) // true // Lexicographic comparison val lt = Builtins.lessThanByteString( ByteString.fromHex("1234"), ByteString.fromHex("5678") ) // true val lte = Builtins.lessThanEqualsByteString( ByteString.fromHex("1234"), ByteString.fromHex("1234") ) // true ``` ### Bitwise Operations (Plutus V3+) Perform bitwise operations on ByteStrings. The `shouldPad` parameter controls behavior when ByteStrings have different lengths. See [CIP-122](https://github.com/cardano-foundation/CIPs/tree/master/CIP-0122) for specification. ```scala // Bitwise AND val and = Builtins.andByteString(false, ByteString.fromHex("0FFF"), ByteString.fromHex("FF")) // hex"0F" val andPad = Builtins.andByteString(true, ByteString.fromHex("0FFF"), ByteString.fromHex("FF")) // hex"0FFF" // Bitwise OR val or = Builtins.orByteString(false, ByteString.fromHex("0FFF"), ByteString.fromHex("FF")) // hex"FF" // Bitwise XOR val xor = Builtins.xorByteString(false, ByteString.fromHex("0FFF"), ByteString.fromHex("FF")) // hex"F0" // Bitwise complement (NOT) val complement = Builtins.complementByteString(ByteString.fromHex("FF")) // hex"00" ``` ### Bit Manipulation (Plutus V3+) See [CIP-122](https://github.com/cardano-foundation/CIPs/tree/master/CIP-0122) and [CIP-123](https://github.com/cardano-foundation/CIPs/tree/master/CIP-0123) for specifications. Bit indexing uses LSB (Least Significant Bit) ordering, meaning index 0 refers to the rightmost bit. For example, in `0x04` (binary `00000100`), bit 2 is set. ```scala // Read a bit at index (index 0 is the rightmost/least significant bit) // Example: 0x0004 = binary 0000000000000100, so bit at index 2 is set val bit = Builtins.readBit(ByteString.fromHex("0004"), BigInt(2)) // true // Write bits at indices val modified = Builtins.writeBits(ByteString.fromHex("0000"), BuiltinList(BigInt(0), BigInt(1)), true) // hex"0003" // Replicate a byte n times val repeated = Builtins.replicateByte(BigInt(4), BigInt(0xFF)) // hex"FFFFFFFF" ``` ### Shifting and Rotating (Plutus V3+) See [CIP-123](https://github.com/cardano-foundation/CIPs/tree/master/CIP-0123) for specification. ```scala // Shift bits left (positive) or right (negative) val shiftLeft = Builtins.shiftByteString(ByteString.fromHex("000F"), BigInt(4)) // hex"00F0" val shiftRight = Builtins.shiftByteString(ByteString.fromHex("000F"), BigInt(-4)) // hex"0000" // Rotate bits val rotateLeft = Builtins.rotateByteString(ByteString.fromHex("000F"), BigInt(4)) // hex"00F0" val rotateRight = Builtins.rotateByteString(ByteString.fromHex("000F"), BigInt(-4)) // hex"F000" // Count set bits (population count) val popcount = Builtins.countSetBits(ByteString.fromHex("000F")) // 4 // Find first set bit index (-1 if none) val firstSet = Builtins.findFirstSetBit(ByteString.fromHex("0002")) // 1 ``` ### Integer/ByteString Conversion (Plutus V2+) See [CIP-121](https://github.com/cardano-foundation/CIPs/tree/master/CIP-0121) for specification. ```scala // Convert ByteString to integer (big-endian or little-endian) val toIntBE = Builtins.byteStringToInteger(true, ByteString.fromHex("1234")) // 4660 (big-endian) val toIntLE = Builtins.byteStringToInteger(false, ByteString.fromHex("3412")) // 4660 (little-endian) // Convert integer to ByteString with specified length val fromIntBE = Builtins.integerToByteString(true, BigInt(2), BigInt(4660)) // hex"1234" val fromIntLE = Builtins.integerToByteString(false, BigInt(2), BigInt(4660)) // hex"3412" val minimal = Builtins.integerToByteString(true, BigInt(0), BigInt(4660)) // hex"1234" (minimal length) ``` ## String Operations UTF-8 string operations. ```scala // Concatenate strings val greeting = Builtins.appendString("Hello, ", "Cardano!") // Equality val eq = Builtins.equalsString("test", "test") // Convert to/from UTF-8 ByteString val encoded = Builtins.encodeUtf8("Hello") val decoded = Builtins.decodeUtf8(encoded) ``` ## List Operations Operations on immutable linked lists (`BuiltinList`). ```scala import scalus.uplc.builtin.{Builtins, BuiltinList} // Choose based on empty/non-empty val result = Builtins.chooseList(BuiltinList(1, 2, 3), "empty", "not empty") // "not empty" // Construct list val list = Builtins.mkCons(BigInt(0), BuiltinList(BigInt(1), BigInt(2), BigInt(3))) // Deconstruct list val head = Builtins.headList(list) // 0 val tail = Builtins.tailList(list) // [1, 2, 3] val isEmpty = Builtins.nullList(list) // false // Drop first n elements (Plutus V3, PV 11+) val dropped = Builtins.dropList(BigInt(2), list) // [2, 3] ``` ## Array Operations (Plutus V3, PV 11+) O(1) indexed access to elements. See [CIP-156](https://github.com/cardano-foundation/CIPs/tree/master/CIP-0156) for specification. Arrays are created from lists and provide efficient random access, unlike linked lists which require O(n) traversal. ```scala import scalus.uplc.builtin.{Builtins, BuiltinList, BuiltinArray} // Convert list to array val list = BuiltinList(Builtins.iData(BigInt(10)), Builtins.iData(BigInt(20)), Builtins.iData(BigInt(30))) val array = Builtins.listToArray(list) // Get array length val len = Builtins.lengthOfArray(array) // 3 // Access element by index (O(1)) val first = Builtins.indexArray(array, BigInt(0)) // iData(10) val second = Builtins.indexArray(array, BigInt(1)) // iData(20) ``` ## Pair Operations Operations on pairs (2-tuples). ```scala import scalus.uplc.builtin.{Builtins, BuiltinPair} val pair = BuiltinPair(BigInt(42), ByteString.fromHex("deadbeef")) // Extract elements val first = Builtins.fstPair(pair) // 42 val second = Builtins.sndPair(pair) // hex"deadbeef" ``` ## Data Operations Operations for Plutus's universal `Data` type, used for serialization and interoperability. ### Creating Data ```scala import scalus.uplc.builtin.{Builtins, Data} // Create Data from primitives val intData = Builtins.iData(BigInt(42)) val bytesData = Builtins.bData(ByteString.fromHex("deadbeef")) val listData = Builtins.listData(BuiltinList(intData, bytesData)) val mapData = Builtins.mapData(BuiltinList(Builtins.mkPairData(intData, bytesData))) val constrData = Builtins.constrData(BigInt(0), BuiltinList(intData, bytesData)) ``` ### Deconstructing Data ```scala // Extract Data values val int = Builtins.unIData(intData) val bytes = Builtins.unBData(bytesData) val list = Builtins.unListData(listData) val map = Builtins.unMapData(mapData) val (tag, fields) = Builtins.unConstrData(constrData) ``` ### Data Operations ```scala // Choose based on Data variant val result = Builtins.chooseData( someData, constrCase = "constructor", mapCase = "map", listCase = "list", iCase = "integer", bCase = "bytestring" ) // Compare Data values val eq = Builtins.equalsData(data1, data2) // Serialize Data to CBOR (Plutus V2+) val serialized = Builtins.serialiseData(someData) ``` ### Monomorphic Constructors ```scala // Create pairs and lists of Data val pairData = Builtins.mkPairData(intData, bytesData) val emptyList = Builtins.mkNilData() val emptyPairList = Builtins.mkNilPairData() ``` ## Cryptographic Hash Functions ### Hashing ```scala // SHA-2 and SHA-3 (Plutus V1+) val sha2 = Builtins.sha2_256(ByteString.fromString("message")) // 32 bytes val sha3 = Builtins.sha3_256(ByteString.fromString("message")) // 32 bytes // BLAKE2b (Plutus V1+) - used extensively in Cardano val blake256 = Builtins.blake2b_256(ByteString.fromString("message")) // 32 bytes val blake224 = Builtins.blake2b_224(ByteString.fromString("message")) // 28 bytes // Keccak-256 (Plutus V3+) - Ethereum compatible // Note: This is original Keccak, not NIST SHA-3 val keccak = Builtins.keccak_256(ByteString.fromString("message")) // 32 bytes // RIPEMD-160 (Plutus V3, PV 11+) - Bitcoin compatible val ripemd = Builtins.ripemd_160(ByteString.fromString("message")) // 20 bytes ``` See [CIP-158](https://github.com/cardano-foundation/CIPs/tree/master/CIP-0158) for Keccak-256 and [CIP-127](https://github.com/cardano-foundation/CIPs/tree/master/CIP-0127) for RIPEMD-160. ### Digital Signatures ```scala // Ed25519 signature verification (Plutus V1+) // publicKey: 32 bytes, message: any length, signature: 64 bytes val validEd = Builtins.verifyEd25519Signature(publicKey, message, signature) // ECDSA SECP256k1 signature verification (Plutus V2+) // publicKey: 33 bytes (compressed), messageHash: 32 bytes (pre-hashed!), signature: 64 bytes val validEcdsa = Builtins.verifyEcdsaSecp256k1Signature(publicKey, messageHash, signature) // Schnorr SECP256k1 signature verification (Plutus V2+) // publicKey: 32 bytes (x-only), message: any length, signature: 64 bytes val validSchnorr = Builtins.verifySchnorrSecp256k1Signature(publicKey, message, signature) ``` For `verifyEcdsaSecp256k1Signature`, the message must be pre-hashed (32 bytes). For Ed25519 and Schnorr, pass the original message. ## BLS12-381 Pairing Operations (Plutus V3+) Advanced cryptographic operations for zero-knowledge proofs and pairing-based cryptography. See [CIP-381](https://github.com/cardano-foundation/CIPs/tree/master/CIP-0381) for specification. Scalus provides convenient wrappers in `scalus.cardano.onchain.plutus.prelude.bls12_381`: ```scala import scalus.cardano.onchain.plutus.prelude.bls12_381.{G1, G2, Scalar} // G1 operations (48-byte compressed points) val g1Zero = G1.zero val g1Gen = G1.generator val g1Sum = g1Point1 + g1Point2 // addition val g1Neg = -g1Point1 // negation val g1Scaled = g1Point1.scale(scalar) // scalar multiplication val g1Compressed = g1Point1.compress // to ByteString val g1FromHash = G1.hashToGroup(message, dst) // G2 operations (96-byte compressed points) val g2Zero = G2.zero val g2Gen = G2.generator val g2Sum = g2Point1 + g2Point2 val g2Neg = -g2Point1 val g2Scaled = g2Point1.scale(scalar) val g2Compressed = g2Point1.compress val g2FromHash = G2.hashToGroup(message, dst) ``` ### Low-level Builtin Functions ```scala import scalus.uplc.builtin.Builtins // G1 Group operations val g1Sum = Builtins.bls12_381_G1_add(g1Point1, g1Point2) val g1Neg = Builtins.bls12_381_G1_neg(g1Point1) val g1Scaled = Builtins.bls12_381_G1_scalarMul(scalar, g1Point1) val g1Equal = Builtins.bls12_381_G1_equal(g1Point1, g1Point2) // G1 Serialization val g1Compressed = Builtins.bls12_381_G1_compress(g1Point) // 48 bytes val g1Uncompressed = Builtins.bls12_381_G1_uncompress(bytes) // G1 Hash to curve val g1FromHash = Builtins.bls12_381_G1_hashToGroup(message, dst) // G2 Group operations (same pattern as G1) val g2Sum = Builtins.bls12_381_G2_add(g2Point1, g2Point2) val g2Neg = Builtins.bls12_381_G2_neg(g2Point1) val g2Scaled = Builtins.bls12_381_G2_scalarMul(scalar, g2Point1) val g2Equal = Builtins.bls12_381_G2_equal(g2Point1, g2Point2) // G2 Serialization val g2Compressed = Builtins.bls12_381_G2_compress(g2Point) // 96 bytes val g2Uncompressed = Builtins.bls12_381_G2_uncompress(bytes) // G2 Hash to curve val g2FromHash = Builtins.bls12_381_G2_hashToGroup(message, dst) ``` ### Pairing Operations Used for verifying pairing equations like `e(P1, Q1) == e(P2, Q2)`: ```scala // Miller loop computes intermediate pairing result val ml1 = Builtins.bls12_381_millerLoop(g1Point1, g2Point1) val ml2 = Builtins.bls12_381_millerLoop(g1Point2, g2Point2) // Multiply Miller loop results (for multi-pairing) val mlProduct = Builtins.bls12_381_mulMlResult(ml1, ml2) // Final verification: checks if e(P1,Q1) == e(P2,Q2) val isValid = Builtins.bls12_381_finalVerify(ml1, ml2) ``` ## Control Flow ```scala // Conditional execution val result = Builtins.ifThenElse(condition, thenValue, elseValue) // Force evaluation after unit val forced = Builtins.chooseUnit()(computeValue) ``` ## Debugging ```scala // Trace a message (collected during evaluation, doesn't affect result) val result = Builtins.trace("Debug message")(computeValue) ``` Trace messages are collected during script execution and can be viewed in transaction evaluation logs. They don't affect the script result but do consume execution units. ## Practical Examples This section shows how builtin functions can be used in smart contracts. ### Bit Manipulation for Compact State Using bitwise operations for space-efficient on-chain state: ```scala import scalus.* import scalus.uplc.builtin.Builtins.* import scalus.uplc.builtin.ByteString @Compile object BitFlags { // Store up to 256 boolean flags in a 32-byte ByteString /** Check if flag at position is set */ def isSet(flags: ByteString, position: BigInt): Boolean = readBit(flags, position) /** Set a flag at position */ def setFlag(flags: ByteString, position: BigInt): ByteString = writeBits(flags, BuiltinList(position), true) /** Clear a flag at position */ def clearFlag(flags: ByteString, position: BigInt): ByteString = writeBits(flags, BuiltinList(position), false) /** Toggle a flag at position using XOR */ def toggleFlag(flags: ByteString, position: BigInt): ByteString = { // Create a mask with only the target bit set val byteIndex = position / BigInt(8) val bitIndex = position % BigInt(8) val mask = replicateByte(byteIndex, BigInt(0)) ++ ByteString.fromArray(Array((1 << bitIndex.toInt).toByte)) ++ replicateByte(BigInt(32) - byteIndex - BigInt(1), BigInt(0)) xorByteString(false, flags, mask) } /** Count how many flags are set */ def countFlags(flags: ByteString): BigInt = countSetBits(flags) } ``` ## Usage Notes - All builtin functions are type-safe and validated at compile time - Most builtins have convenient operator syntax (e.g., `+`, `==`, `++`) - Builtin functions compile directly to Plutus opcodes for maximum efficiency - Check Plutus version compatibility when using newer builtins - See the [Builtins API documentation](https://scalus.org/api/scalus/uplc/builtin/Builtins$.html) for complete details - Refer to the [Plutus specification](https://plutus.cardano.intersectmbo.org/resources/plutus-core-spec.pdf) for formal semantics ## Related CIPs - [CIP-121](https://github.com/cardano-foundation/CIPs/tree/master/CIP-0121) - Integer/ByteString conversions - [CIP-122](https://github.com/cardano-foundation/CIPs/tree/master/CIP-0122) - Bitwise primitives - [CIP-123](https://github.com/cardano-foundation/CIPs/tree/master/CIP-0123) - Bitwise shifts and rotations - [CIP-127](https://github.com/cardano-foundation/CIPs/tree/master/CIP-0127) - RIPEMD-160 hash - [CIP-156](https://github.com/cardano-foundation/CIPs/tree/master/CIP-0156) - Array builtins - [CIP-158](https://github.com/cardano-foundation/CIPs/tree/master/CIP-0158) - Keccak-256 hash - [CIP-381](https://github.com/cardano-foundation/CIPs/tree/master/CIP-0381) - BLS12-381 primitives --- Source: https://scalus.org/docs/language-guide/collections --- # Collections Scalus provides immutable collection types for organizing and manipulating data in smart contracts. These collections—List and AssocMap—are optimized for blockchain execution and compile efficiently to Plutus Core. ## List Immutable singly-linked lists are the primary collection type in Plutus. Lists are recursive data structures ideal for sequential processing. ```scala import scalus.cardano.onchain.plutus.prelude.List // Creating lists val empty = List.empty[BigInt] val numbers = List(1, 2, 3, 4, 5) val range = List.range(0, 10) // [0, 1, 2, ..., 10] (inclusive) // Prepending (O(1)) val withZero = 0 :: numbers // [0, 1, 2, 3, 4, 5] // Pattern matching numbers match case Nil => "empty" case head :: tail => s"head: $head, tail: $tail" // Common operations val doubled = numbers.map(_ * 2) val evens = numbers.filter(_ % 2 == 0) val sum = numbers.foldLeft(0)(_ + _) val sum2 = numbers.foldRight(0)(_ + _) val reversed = numbers.reverse val length = numbers.length val head = numbers.head val tail = numbers.tail val contains = numbers.contains(3) // List comprehensions val pairs = for x <- List(1, 2, 3) y <- List(10, 20) yield (x, y) // [(1,10), (1,20), (2,10), (2,20), (3,10), (3,20)] // Flattening val nested = List(List(1, 2), List(3, 4)) val flat = nested.flatten // [1, 2, 3, 4] // Zipping val letters = List("a", "b", "c") val zipped = numbers.zip(letters) // [(1,"a"), (2,"b"), (3,"c")] ``` **Performance Notes:** - Prepending (`::`) is O(1) - Appending is O(n) - avoid in favor of prepending + reversing - Random access is O(n) - use lists for sequential processing - Pattern matching on lists is efficient and idiomatic ## Tuple Tuples are fixed-size heterogeneous collections. Scalus supports tuples up to Tuple22. ```scala // Creating tuples val pair: (BigInt, ByteString) = (42, ByteString.fromHex("deadbeef")) val triple = (true, 123, "hello") val nested = ((1, 2), (3, 4)) // Accessing elements val first = pair._1 // 42 val second = pair._2 // hex"deadbeef" // Destructuring val (a, b) = pair val (x, y, z) = triple // Pattern matching pair match case (num, bytes) => s"Number: $num, Bytes: $bytes" // Converting val list = triple.toList // requires all elements to have same type ``` **Use Cases:** - Returning multiple values from functions - Grouping related data without defining a case class - Temporary data structures ## BuiltinPair `BuiltinPair` is Plutus's builtin pair type, similar to tuples but with named accessors. ```scala import scalus.uplc.builtin.BuiltinPair // Creating pairs val pair = BuiltinPair(BigInt(42), ByteString.fromHex("deadbeef")) // Accessing elements val first = pair.fst val second = pair.snd // Using with Builtins import scalus.uplc.builtin.Builtins val extractFirst = Builtins.fstPair(pair) val extractSecond = Builtins.sndPair(pair) // Pattern matching pair match case BuiltinPair(num, bytes) => s"Number: $num, Bytes: $bytes" ``` ## Option `Option` represents an optional value - either `Some(value)` or `None`. ```scala import scalus.cardano.onchain.plutus.prelude.Option import scalus.cardano.onchain.plutus.prelude.Option.* // Creating Options val some: Option[BigInt] = Some(42) val none: Option[BigInt] = None // Pattern matching some match case Some(value) => value case None => 0 // Operations val mapped = some.map(_ * 2) // Some(84) val flatMapped = some.flatMap(x => Some(x + 1)) // Some(43) val filtered = some.filter(_ > 40) // Some(42) val getOrElse = some.getOrElse(0) // 42 val orElse = none.orElse(Some(100)) // Some(100) // Combining options val opt1 = Some(10) val opt2 = Some(20) val combined = for x <- opt1 y <- opt2 yield x + y // Some(30) ``` **Use Cases:** - Representing values that might be absent - Avoiding null references - Safe operations that might fail ## Error Handling Scalus does not provide an `Either` type on-chain. Instead, use `require` for validation and `Option` for values that might be absent: ```scala // Use require for validation require(amount > 0, "Amount must be positive") // Use Option for values that might be absent val value: Option[BigInt] = map.get(key) val result = value.getOrFail("Key not found") ``` ## AssocMap `AssocMap` is an association list - a list of key-value pairs. It's an ordered map with O(n) lookup. ```scala import scalus.cardano.onchain.plutus.prelude.AssocMap // Creating association maps val empty = AssocMap.empty[ByteString, BigInt] val map = AssocMap.fromList(List( (ByteString.fromHex("01"), BigInt(100)), (ByteString.fromHex("02"), BigInt(200)) )) // Operations val lookup = map.get(ByteString.fromHex("01")) // Some(100) val insert = map.insert(ByteString.fromHex("03"), BigInt(300)) val delete = map.delete(ByteString.fromHex("01")) val member = map.contains(ByteString.fromHex("02")) // true // Keys and values val keys = map.keys val values = map.values // Mapping over values val doubled = map.mapValues(_ * 2) // Filtering val filtered = map.filter { case (k, v) => v > 150 } ``` **Use Cases:** - Small maps where ordering matters - Sequential processing of key-value pairs - When insertion order needs to be preserved ## Map (PlutusData) Plutus `Data.Map` is the builtin map type for on-chain data, represented as a list of `(Data, Data)` pairs. ```scala import scalus.uplc.builtin.Data import scalus.uplc.builtin.Builtins // Creating maps val pairs = List( (Builtins.iData(BigInt(1)), Builtins.bData(ByteString.fromHex("aa"))), (Builtins.iData(BigInt(2)), Builtins.bData(ByteString.fromHex("bb"))) ) val mapData = Builtins.mapData(pairs) // Extracting map val extractedPairs = Builtins.unMapData(mapData) // Working with map entries extractedPairs.foreach { case (keyData, valueData) => val key = Builtins.unIData(keyData) val value = Builtins.unBData(valueData) // process key-value pair } ``` **Use Cases:** - Storing data on-chain in datum or redeemer - Interoperability with other Plutus scripts - Serialization and deserialization ## Choosing the Right Collection | Collection | Use When | Performance | |------------|----------|-------------| | **List** | Sequential processing, functional operations | Prepend O(1), access O(n) | | **Tuple** | Fixed number of heterogeneous values | Access O(1) | | **BuiltinPair** | Two related values, interop with builtins | Access O(1) | | **Option** | Optional values, avoiding errors | Pattern match O(1) | | **AssocMap** | Small ordered maps, preserved insertion order | Lookup O(n) | | **Map (Data)** | On-chain data storage, script interop | Lookup O(n) | ## Collection Conversions ```scala // List to Tuple (up to 22 elements) val list = List(1, 2, 3) // Manual conversion needed // Tuple to List (requires same types) val tuple = (1, 2, 3) // Use productIterator for generic approach // List to AssocMap val keyValues = List(("a", 1), ("b", 2)) val assocMap = AssocMap.fromList(keyValues) // AssocMap to List val backToList = assocMap.toList // Data conversions val listData = Builtins.listData(List(Builtins.iData(1), Builtins.iData(2))) val mapData = Builtins.mapData(List( Builtins.mkPairData(Builtins.iData(1), Builtins.iData(100)) )) ``` ## Best Practices 1. **Prefer immutable operations** - All collection operations return new collections 2. **Use List for sequential processing** - Most efficient for functional operations 3. **Avoid expensive operations** - Random access, appending, and large maps are costly 4. **Pattern match for safety** - Handle all cases (empty/non-empty, Some/None) 5. **Use AssocMap for small maps** - For larger maps, consider alternative data structures 6. **Minimize on-chain data** - Keep collections small to reduce transaction costs 7. **Leverage type safety** - Use appropriate collection types to catch errors at compile time ## Common Patterns ### Processing Lists ```scala // Sum all elements val sum = numbers.foldLeft(0)(_ + _) // Find maximum val max = numbers.foldLeft(numbers.head)((a, b) => if a > b then a else b) // Group by predicate (using two filters) val evens = numbers.filter(_ % 2 == 0) val odds = numbers.filter(_ % 2 != 0) // Take while condition holds val lessThanFive = numbers.takeWhile(_ < 5) // Drop while condition holds val fiveAndAbove = numbers.dropWhile(_ < 5) ``` ### Safe Map Access ```scala // Using Option for safe access def safeLookup(map: AssocMap[String, Int], key: String): Option[Int] = map.get(key) // With default value def lookupOrDefault(map: AssocMap[String, Int], key: String, default: Int): Int = map.get(key).getOrElse(default) // Chaining lookups val result = for value1 <- map1.get(key1) value2 <- map2.get(key2) yield value1 + value2 ``` ### Validation with require ```scala // Validating input on-chain def validatePositive(n: BigInt): Unit = require(n > 0, "Number must be positive") // Chain validations require(amount > 0, "Amount must be positive") require(deadline > currentTime, "Deadline must be in the future") ``` --- Source: https://scalus.org/docs/language-guide/data-types --- # Custom Data Types Define custom data types using Scala's case classes and enums, which compile to Plutus structures. These types automatically serialize to and from Plutus `Data`, enabling type-safe smart contract development on Cardano. ## Defining Data Types ### Case Classes Case classes define product types - structures with named fields. ```scala import scalus.uplc.builtin.ByteString // Simple case class case class Account(owner: ByteString, balance: BigInt) // Nested case classes case class Token(policyId: ByteString, assetName: ByteString) case class TokenAmount(token: Token, amount: BigInt) // Case class with multiple fields case class Transaction( from: Account, to: Account, amount: BigInt, timestamp: BigInt ) ``` ### Enums (Sum Types) Enums define sum types - values that can be one of several variants. ```scala // Simple enum enum Color: case Red case Green case Blue // Enum with associated data enum State: case Empty case Active(account: Account) case Locked(account: Account, until: BigInt) // Enum with multiple constructors enum Result: case Success(value: BigInt) case Failure(error: String) case Pending ``` ### Recursive Types You can define recursive data structures: ```scala // Binary tree enum Tree: case Leaf(value: BigInt) case Node(left: Tree, right: Tree) // Linked list (though List is built-in) enum MyList: case Nil case Cons(head: BigInt, tail: MyList) ``` ## Creating and Using Custom Types ```scala import scalus.cardano.onchain.plutus.prelude.{*, given} compile { // Create instances using constructors val account = Account(ByteString.fromHex("abc123"), BigInt(1000)) // Using new keyword val account2 = new Account(ByteString.fromHex("def456"), BigInt(2000)) // Accessing fields val owner = account.owner val balance = account.balance // Creating enum values val empty: State = State.Empty val active = State.Active(account) val locked = State.Locked(account, BigInt(1000000)) // Tuples val pair = (true, BigInt(123)) val (flag, number) = pair // destructuring } ``` ## ToData and FromData Scalus automatically generates `ToData` and `FromData` instances for your custom types, enabling seamless conversion to/from Plutus `Data`. ### Automatic Derivation ```scala import scalus.uplc.builtin.{Data, ToData, FromData} case class Account(owner: ByteString, balance: BigInt) // Automatically available val account = Account(ByteString.fromHex("abc123"), BigInt(1000)) val accountData: Data = account.toData // Convert to Data val recovered: Account = FromData.fromData(accountData) // Convert from Data ``` ### How It Works Case classes are encoded as Plutus constructors: - Constructor tag (field index in enum, 0 for single case class) - List of fields encoded as Data ```scala // Account(owner, balance) becomes: // Constr(0, [B(owner), I(balance)]) enum State: case Empty // Constr(0, []) case Active(account) // Constr(1, [account.toData]) case Locked(acc, until) // Constr(2, [acc.toData, I(until)]) ``` ### Manual ToData/FromData For custom encoding logic: ```scala case class CustomType(value: BigInt) given ToData[CustomType] with def toData(v: CustomType): Data = Builtins.iData(v.value * 2) // Custom encoding given FromData[CustomType] with def fromData(d: Data): CustomType = CustomType(Builtins.unIData(d) / 2) // Custom decoding ``` ## Extension Methods Add methods to existing types without modifying them: ```scala extension (account: Account) def hasBalance(amount: BigInt): Boolean = account.balance >= amount def deposit(amount: BigInt): Account = Account(account.owner, account.balance + amount) def withdraw(amount: BigInt): Account = if account.balance >= amount then Account(account.owner, account.balance - amount) else throw new Exception("Insufficient balance") // Usage compile { val account = Account(ByteString.fromHex("abc"), BigInt(1000)) val canAfford = account.hasBalance(BigInt(500)) // true val newAccount = account.deposit(BigInt(500)) // balance: 1500 } ``` ### Extension Methods for Enums ```scala extension (state: State) def isActive: Boolean = state match case State.Active(_) => true case _ => false def getAccount: Option[Account] = state match case State.Active(acc) => Some(acc) case State.Locked(acc, _) => Some(acc) case State.Empty => None // Usage compile { val state = State.Active(account) if state.isActive then state.getAccount match case Some(acc) => acc.balance case None => BigInt(0) else BigInt(0) } ``` ## Inline Methods Inline methods are expanded at compile time, reducing function call overhead: ```scala case class Point(x: BigInt, y: BigInt) extension (p: Point) inline def distanceSquared(other: Point): BigInt = val dx = p.x - other.x val dy = p.y - other.y dx * dx + dy * dy // The inline method will be expanded at the call site compile { val p1 = Point(BigInt(0), BigInt(0)) val p2 = Point(BigInt(3), BigInt(4)) val dist = p1.distanceSquared(p2) // Expanded inline } ``` ## Pattern Matching and Deconstructing ### Basic Pattern Matching ```scala compile { val state: State = State.Active(account) // Match on enum variants state match case State.Empty => BigInt(0) case State.Active(account) => account.balance case State.Locked(account, until) => if until < currentTime then account.balance else BigInt(0) } ``` ### Nested Pattern Matching ```scala case class Token(policy: ByteString, name: ByteString) case class Balance(token: Token, amount: BigInt) enum Wallet: case Empty case Single(balance: Balance) case Multiple(balances: List[Balance]) compile { val wallet: Wallet = Wallet.Single( Balance(Token(hex"abc", hex"def"), BigInt(1000)) ) // Nested pattern matching wallet match case Wallet.Empty => BigInt(0) case Wallet.Single(Balance(Token(policy, name), amount)) => amount // Destructure nested structures case Wallet.Multiple(balances) => balances.map(_.amount).sum } ``` ### Pattern Matching with Bindings ```scala compile { state match case State.Empty => BigInt(0) case State.Active(acc @ Account(owner, balance)) => // Both `acc` and destructured `owner`, `balance` available if balance > BigInt(1000) then balance else BigInt(0) case _ => BigInt(0) // Wildcard pattern } ``` ### Deconstructing in Val Declarations ```scala compile { // Destructure tuple val (x, y) = (BigInt(10), BigInt(20)) // Destructure case class val Account(owner, balance) = account // Destructure Option val Some((a, b)) = optionOfTuple // Partial destructuring val State.Active(acc) = activeState // Assumes it's Active } ``` ### List Pattern Matching ```scala compile { val numbers = List(1, 2, 3, 4, 5) numbers match case Nil => BigInt(0) case head :: Nil => head // Single element case first :: second :: rest => first + second // At least two elements case _ => BigInt(0) } ``` ## Type Aliases and Opaque Types ### Type Aliases Create alternative names for existing types: ```scala type UserId = ByteString type Balance = BigInt type Timestamp = BigInt case class User(id: UserId, balance: Balance, created: Timestamp) // Usage val user = User( ByteString.fromHex("abc123"), BigInt(1000), BigInt(1640000000) ) ``` ### Opaque Types (Non Top-Level) Opaque types provide type safety without runtime overhead: ```scala object Types: opaque type PositiveInt = BigInt object PositiveInt: def apply(n: BigInt): Option[PositiveInt] = if n > 0 then Some(n) else None extension (p: PositiveInt) def value: BigInt = p def add(other: PositiveInt): PositiveInt = p + other // Usage import Types.* compile { val maybePos = PositiveInt(BigInt(10)) maybePos match case Some(p) => val doubled = p.add(p) doubled.value case None => BigInt(0) } ``` ## Best Practices 1. **Keep types simple** - Complex nested structures increase execution costs 2. **Use enums for alternatives** - More efficient than multiple case classes 3. **Leverage pattern matching** - Safe and expressive way to handle variants 4. **Use extension methods** - Add functionality without inheritance 5. **Inline small methods** - Reduce function call overhead 6. **Validate at boundaries** - Check data validity when converting from Data 7. **Use opaque types for safety** - Prevent mixing up similar types (e.g., different IDs) 8. **Minimize Data conversions** - Only convert when necessary for on-chain communication ## Common Patterns ### Smart Constructor Pattern ```scala case class PositiveAmount private (value: BigInt) object PositiveAmount: def create(value: BigInt): Either[String, PositiveAmount] = if value > 0 then Right(PositiveAmount(value)) else Left("Amount must be positive") // Usage compile { PositiveAmount.create(BigInt(100)) match case Right(amount) => amount.value case Left(error) => throw new Exception(error) } ``` ### Builder Pattern ```scala case class Config( timeout: BigInt, maxRetries: BigInt, enableLogging: Boolean ) object Config: def default: Config = Config( timeout = BigInt(30000), maxRetries = BigInt(3), enableLogging = false ) extension (c: Config) def withTimeout(t: BigInt): Config = c.copy(timeout = t) def withMaxRetries(r: BigInt): Config = c.copy(maxRetries = r) def withLogging(enabled: Boolean): Config = c.copy(enableLogging = enabled) // Usage compile { val config = Config.default .withTimeout(BigInt(60000)) .withLogging(true) } ``` ### State Machine Pattern ```scala enum StateMachine: case Initial case Processing(data: ByteString) case Completed(result: BigInt) case Failed(error: String) extension (sm: StateMachine) def transition(input: ByteString): StateMachine = sm match case StateMachine.Initial => StateMachine.Processing(input) case StateMachine.Processing(data) => // Process data if isValid(data) then StateMachine.Completed(computeResult(data)) else StateMachine.Failed("Invalid data") case _ => sm // No transition ``` ## Error Handling with Custom Types ```scala enum ValidationError: case InvalidAmount(got: BigInt, expected: BigInt) case InsufficientBalance(required: BigInt, available: BigInt) case Unauthorized(user: ByteString) type ValidationResult[A] = Either[ValidationError, A] def validateTransfer( from: Account, to: Account, amount: BigInt ): ValidationResult[Transaction] = if amount <= 0 then Left(ValidationError.InvalidAmount(amount, BigInt(0))) else if from.balance < amount then Left(ValidationError.InsufficientBalance(amount, from.balance)) else Right(Transaction(from, to, amount, currentTimestamp)) ``` --- Source: https://scalus.org/docs/language-guide/control-flow --- # Control Flow Scalus provides control flow constructs for directing program execution in smart contracts. These constructs—if-then-else and pattern matching—are sufficient for most validator logic and compile efficiently to Plutus Core. ## If-Then-Else The `if-then-else` expression is the primary conditional construct. ```scala import scalus.cardano.onchain.plutus.prelude.{*, given} compile { val balance = BigInt(1000) val threshold = BigInt(500) // Basic if-then-else val status = if balance >= threshold then "sufficient" else "insufficient" // Nested conditionals val category = if balance == BigInt(0) then "empty" else if balance < BigInt(100) then "low" else if balance < BigInt(1000) then "medium" else "high" // Multi-line blocks val result = if balance > threshold then val fee = BigInt(10) balance - fee else balance // Expression result val doubled = if balance > BigInt(0) then balance * 2 else BigInt(0) } ``` **Key Points:** - Both branches must return the same type - Can be used as an expression (returns a value) - Supports nested conditions ## Pattern Matching Pattern matching is a powerful feature for deconstructing and analyzing data structures. ### Basic Pattern Matching ```scala enum State: case Empty case Active(balance: BigInt) case Locked(balance: BigInt, until: BigInt) compile { val state: State = State.Active(BigInt(1000)) // Match on enum variants state match case State.Empty => BigInt(0) case State.Active(balance) => balance case State.Locked(balance, until) => if until < currentTime then balance else BigInt(0) // All cases must be covered or use wildcard state match case State.Active(balance) => balance case _ => BigInt(0) // Wildcard catches all other cases } ``` ### Nested Patterns You can pattern match on nested structures: ```scala case class Token(policy: ByteString, name: ByteString) case class Balance(token: Token, amount: BigInt) enum Wallet: case Empty case Single(balance: Balance) case Multiple(balances: List[Balance]) compile { val wallet: Wallet = Wallet.Single( Balance(Token(hex"abc", hex"def"), BigInt(1000)) ) // Nested pattern matching val totalAmount = wallet match case Wallet.Empty => BigInt(0) case Wallet.Single(Balance(Token(policy, _), amount)) => amount // Extract nested fields case Wallet.Multiple(balances) => balances.map(_.amount).foldLeft(BigInt(0))(_ + _) } ``` ### Pattern Bindings Use `@` to bind a pattern to a variable while also destructuring it: ```scala case class Account(owner: ByteString, balance: BigInt) compile { val state = State.Active(Account(hex"abc", BigInt(1000))) state match case State.Active(acc @ Account(owner, balance)) => // Both `acc` and individual fields available if balance > BigInt(500) then acc.balance else BigInt(0) case _ => BigInt(0) } ``` ### List Patterns ```scala compile { val numbers = List(1, 2, 3, 4, 5) numbers match case Nil => "empty list" case head :: Nil => s"single element: $head" case first :: second :: rest => s"at least two elements: $first and $second" case _ => "other" } ``` ### Tuple Patterns ```scala compile { val pair = (BigInt(10), hex"abc") pair match case (amount, hash) => if amount > BigInt(0) then hash else hex"" } ``` ### Guard Clauses Guards add conditions to patterns using `if`: ```scala import scalus.cardano.onchain.plutus.prelude.* compile { def categorize(opt: Option[BigInt]): String = opt match case Option.Some(v) if v > BigInt(100) => "large" case Option.Some(v) if v > BigInt(0) => "positive" case Option.Some(v) if v < BigInt(0) => "negative" case Option.Some(_) => "zero" case Option.None => "none" // Complex guards with multiple conditions def processItem(list: List[BigInt]): String = list match case List.Cons(h, t) if h > BigInt(0) && t.length > 0 => "positive with tail" case List.Cons(h, _) if h > BigInt(0) => "positive no tail" case List.Cons(_, t) if t.length > 0 => "has tail" case _ => "other" } ``` ### Pattern Matching with Constants Pattern matching on boolean and string constants is supported: ```scala compile { // Boolean patterns val flag = true flag match case true => "yes" case false => "no" // String patterns val cmd = "start" cmd match case "start" => BigInt(1) case "stop" => BigInt(0) case _ => BigInt(-1) } ``` Integer constant patterns like `case BigInt(0) =>` are not yet supported. Use guards instead: `case n if n == BigInt(0) =>`. ## Exhaustiveness Checking Scala's compiler ensures all cases are covered: ```scala enum Color: case Red case Green case Blue compile { val color: Color = Color.Red // Compiler ensures all cases are handled color match case Color.Red => "red" case Color.Green => "green" case Color.Blue => "blue" // No wildcard needed - all cases covered // Or use wildcard for remaining cases color match case Color.Red => "red" case _ => "not red" } ``` ## Error Handling ### Using throw `throw` terminates execution immediately, compiling to Plutus ERROR: ```scala compile { def validateAmount(amount: BigInt): BigInt = if amount <= BigInt(0) then throw new Exception("Amount must be positive") else amount val validated = validateAmount(BigInt(100)) // OK // validateAmount(BigInt(-1)) // Throws error } ``` **Error Messages:** - Error messages can be traced using `sir.toUplc(generateErrorTraces = true)` - Useful for debugging offchain - Keep messages concise to reduce script size ### Using Option ```scala compile { def safeDivide(a: BigInt, b: BigInt): Option[BigInt] = if b == BigInt(0) then None else Some(a / b) safeDivide(BigInt(10), BigInt(2)) match case Some(result) => result case None => BigInt(0) } ``` ### Using Either ```scala compile { def validateTransfer(from: Account, amount: BigInt): Either[String, Account] = if amount <= BigInt(0) then Left("Invalid amount") else if from.balance < amount then Left("Insufficient balance") else Right(Account(from.owner, from.balance - amount)) validateTransfer(account, BigInt(500)) match case Right(newAccount) => newAccount.balance case Left(error) => throw new Exception(error) } ``` ## Loops (Not Supported - Use Recursion) Scalus doesn't support `while` or `for` loops. Use recursion or higher-order functions instead: ### Recursion Instead of Loops ```scala compile { // Sum using recursion def sum(list: List[BigInt]): BigInt = list match case Nil => BigInt(0) case head :: tail => head + sum(tail) // Factorial using recursion def factorial(n: BigInt): BigInt = if n <= BigInt(1) then BigInt(1) else n * factorial(n - 1) // Find element using recursion def find(list: List[BigInt], target: BigInt): Boolean = list match case Nil => false case head :: tail => if head == target then true else find(tail, target) } ``` ### Higher-Order Functions Instead of Loops ```scala compile { val numbers = List(1, 2, 3, 4, 5) // Sum - instead of for loop val sum = numbers.foldLeft(BigInt(0))(_ + _) // Filter - instead of filtering loop val evens = numbers.filter(_ % 2 == 0) // Transform - instead of transformation loop val doubled = numbers.map(_ * 2) // Count - instead of counting loop val countLarge = numbers.filter(_ > 3).length // All/Any - instead of validation loop val allPositive = numbers.forall(_ > 0) val anyLarge = numbers.exists(_ > 10) } ``` ## Early Returns (Not Supported) Scalus doesn't support early returns. Use pattern matching or conditionals instead: ### Instead of Early Return ```scala // NOT SUPPORTED def process(value: BigInt): BigInt = { if value < 0 then return 0 // Not supported if value > 100 then return 100 // Not supported value * 2 } ``` ### Use Pattern Matching or Nested Ifs ```scala compile { def process(value: BigInt): BigInt = if value < BigInt(0) then BigInt(0) else if value > BigInt(100) then BigInt(100) else value * 2 // Or use match with guards def processWithMatch(value: BigInt): BigInt = value match case v if v < BigInt(0) => BigInt(0) case v if v > BigInt(100) => BigInt(100) case v => v * 2 } ``` ## Best Practices 1. **Prefer pattern matching over nested ifs** - More readable and safer 2. **Always handle all cases** - Use wildcard for catch-all 3. **Keep conditions simple** - Complex logic should be extracted to functions 4. **Use meaningful patterns** - Destructure to give names to values 5. **Avoid deep nesting** - Extract to helper functions 6. **Use guards for filtering** - `case Some(v) if v > 0 => ...` is cleaner than nested ifs 7. **Use recursion judiciously** - Be aware of stack depth 8. **Leverage type system** - Let compiler check exhaustiveness ## Common Patterns ### Validation Pattern ```scala compile { def validate(input: BigInt): Either[String, BigInt] = if input < BigInt(0) then Left("Negative value") else if input > BigInt(1000000) then Left("Value too large") else Right(input) validate(BigInt(500)) match case Right(value) => value case Left(error) => throw new Exception(error) } ``` ### State Machine Pattern ```scala enum State: case Initial case Processing(step: BigInt) case Complete(result: BigInt) case Failed(reason: String) compile { def transition(state: State, input: BigInt): State = state match case State.Initial => if input > BigInt(0) then State.Processing(BigInt(1)) else State.Failed("Invalid input") case State.Processing(step) => if step >= BigInt(10) then State.Complete(step * input) else State.Processing(step + 1) case State.Complete(_) | State.Failed(_) => state // Terminal states } ``` ### Safe Unwrapping Pattern ```scala compile { def getBalance(maybeAccount: Option[Account]): BigInt = maybeAccount match case Some(account) => account.balance case None => BigInt(0) // Or use getOrElse val balance = maybeAccount.map(_.balance).getOrElse(BigInt(0)) } ``` ### List Processing Pattern ```scala compile { def processAll(items: List[BigInt]): BigInt = items match case Nil => BigInt(0) case head :: tail => val processed = head * 2 processed + processAll(tail) // Or use fold val result = items.foldLeft(BigInt(0))((acc, item) => acc + item * 2) } ``` --- Source: https://scalus.org/docs/language-guide/functions --- # Functions Functions are first-class values in Scalus, enabling functional programming patterns essential for smart contract development. Scalus supports named functions (`def`), anonymous functions (lambdas), and higher-order functions. ## Defining Functions ### Basic Function Definition ```scala import scalus.cardano.onchain.plutus.prelude.{*, given} compile { // Simple function def add(a: BigInt, b: BigInt): BigInt = a + b // Function with explicit return type def multiply(x: BigInt, y: BigInt): BigInt = x * y // Multi-line function def calculate(a: BigInt, b: BigInt): BigInt = val sum = a + b val doubled = sum * 2 doubled + 1 // Call functions val result = add(BigInt(10), BigInt(20)) // 30 } ``` ### Type Inference Return types can often be inferred, but explicit types are recommended for clarity: ```scala compile { // Type inferred def double(x: BigInt) = x * 2 // Explicit return type (recommended) def triple(x: BigInt): BigInt = x * 3 } ``` ## Lambda Functions (Anonymous Functions) Lambdas are functions without names, useful for passing as arguments: ```scala compile { // Lambda syntax val increment = (x: BigInt) => x + 1 // Lambda with multiple parameters val add = (a: BigInt, b: BigInt) => a + b // Lambda with explicit type val multiply: (BigInt, BigInt) => BigInt = (x, y) => x * y // Multi-line lambda val complexCalc = (x: BigInt) => { val doubled = x * 2 val squared = doubled * doubled squared + 1 } // Using lambdas val result = increment(BigInt(41)) // 42 } ``` ### Shorthand Syntax Use underscore for concise lambdas: ```scala compile { val numbers = List(1, 2, 3, 4, 5) // Verbose val doubled = numbers.map((x: BigInt) => x * 2) // Shorthand val tripled = numbers.map(_ * 3) // Multiple underscores represent different arguments val sum = numbers.foldLeft(BigInt(0))(_ + _) // Equivalent to: (acc, x) => acc + x } ``` ## Higher-Order Functions Functions that take other functions as parameters or return functions: ```scala compile { // Function taking a function as parameter def apply(x: BigInt, f: BigInt => BigInt): BigInt = f(x) // Usage val result = apply(BigInt(10), x => x * 2) // 20 // Function returning a function def makeMultiplier(factor: BigInt): BigInt => BigInt = (x: BigInt) => x * factor val double = makeMultiplier(BigInt(2)) val triple = makeMultiplier(BigInt(3)) double(BigInt(10)) // 20 triple(BigInt(10)) // 30 } ``` ### Common Higher-Order Functions ```scala compile { val numbers = List(1, 2, 3, 4, 5) // map: transform each element val doubled = numbers.map(_ * 2) // [2, 4, 6, 8, 10] // filter: select elements matching condition val evens = numbers.filter(_ % 2 == 0) // [2, 4] // foldLeft: reduce from left val sum = numbers.foldLeft(BigInt(0))(_ + _) // 15 // foldRight: reduce from right val product = numbers.foldRight(BigInt(1))(_ * _) // 120 // find: first element matching condition val firstEven = numbers.find(_ % 2 == 0) // Some(2) // exists: check if any element matches val hasLarge = numbers.exists(_ > 10) // false // forall: check if all elements match val allPositive = numbers.forall(_ > 0) // true } ``` ## Recursive Functions Recursion is the primary iteration mechanism in Scalus: ```scala compile { // Simple recursion def factorial(n: BigInt): BigInt = if n <= BigInt(1) then BigInt(1) else n * factorial(n - 1) // Tail recursion (more efficient) def factorialTail(n: BigInt, acc: BigInt = BigInt(1)): BigInt = if n <= BigInt(1) then acc else factorialTail(n - 1, n * acc) // List recursion def sum(list: List[BigInt]): BigInt = list match case Nil => BigInt(0) case head :: tail => head + sum(tail) // Finding in list def contains(list: List[BigInt], target: BigInt): Boolean = list match case Nil => false case head :: tail => if head == target then true else contains(tail, target) } ``` **Important:** - Scalus supports recursive functions - Consider tail recursion for efficiency - Be aware of recursion depth limits - Use higher-order functions when possible ## Default Parameters Scalus supports default parameter values: ```scala def greet(name: String, greeting: String = "Hello"): String = appendString(name, greeting) ``` ## Named Arguments Scalus supports named arguments: ```scala def transfer(from: Account, to: Account, amount: BigInt): Transaction = ??? // This works as expected: transfer(from = alice, to = bob, amount = BigInt(100)) ``` ## Variable Arguments (Varargs) Scalus supports variable argument lists (vargs) with a little caveat: ```scala def sum(numbers: BigInt*): BigInt = numbers.list.foldLeft(BigInt(0))(_ + _) ``` You must convert a sequence to a `List` using the `.list` extension method before performing list operations. ## Function Overloading **NOT SUPPORTED** - Scalus doesn't support function overloading (multiple functions with the same name): ```scala // NOT SUPPORTED def add(a: BigInt, b: BigInt): BigInt = a + b def add(a: BigInt, b: BigInt, c: BigInt): BigInt = a + b + c ``` **Workaround:** Use different function names or a single function with List: ```scala compile { // Different names def add2(a: BigInt, b: BigInt): BigInt = a + b def add3(a: BigInt, b: BigInt, c: BigInt): BigInt = a + b + c // Or use a List def addAll(numbers: List[BigInt]): BigInt = numbers.foldLeft(BigInt(0))(_ + _) addAll(List(1, 2)) // 3 addAll(List(1, 2, 3)) // 6 addAll(List(1, 2, 3, 4)) // 10 } ``` ## Mutually Recursive Functions **NOT SUPPORTED** - Functions cannot call each other recursively: ```scala // NOT SUPPORTED def isEven(n: BigInt): Boolean = if n == BigInt(0) then true else isOdd(n - 1) def isOdd(n: BigInt): Boolean = if n == BigInt(0) then false else isEven(n - 1) ``` **Workaround:** Combine into a single function or use a helper enum: ```scala compile { // Combine into one function def isEven(n: BigInt): Boolean = (n % BigInt(2)) == BigInt(0) def isOdd(n: BigInt): Boolean = !isEven(n) // Or use helper data structure enum Parity: case Even case Odd def checkParity(n: BigInt, current: Parity): Boolean = if n == BigInt(0) then current match case Parity.Even => true case Parity.Odd => false else val nextParity = current match case Parity.Even => Parity.Odd case Parity.Odd => Parity.Even checkParity(n - 1, nextParity) checkParity(BigInt(10), Parity.Even) // true (10 is even) } ``` ## Closures Lambdas can capture variables from their enclosing scope: ```scala compile { val multiplier = BigInt(10) // Lambda captures 'multiplier' val multiplyBy10 = (x: BigInt) => x * multiplier multiplyBy10(BigInt(5)) // 50 // Function returning closure def makeAdder(x: BigInt): BigInt => BigInt = (y: BigInt) => x + y val add5 = makeAdder(BigInt(5)) add5(BigInt(10)) // 15 } ``` ## Partial Application Create new functions by fixing some arguments: ```scala compile { def add3(a: BigInt, b: BigInt, c: BigInt): BigInt = a + b + c // Create a partially applied function def addWith10And20(c: BigInt): BigInt = add3(10, 20, c) addWith10And20(30) // 60 // Using closures for partial application def partial2(f: (BigInt, BigInt, BigInt) => BigInt, a: BigInt): (BigInt, BigInt) => BigInt = (b: BigInt, c: BigInt) => f(a, b, c) val addWith10 = partial2(add3, BigInt(10)) addWith10(20, 30) // 60 } ``` ## Function Composition Combine functions to create new ones: ```scala compile { def double(x: BigInt): BigInt = x * 2 def increment(x: BigInt): BigInt = x + 1 // Manual composition def doubleAndIncrement(x: BigInt): BigInt = increment(double(x)) doubleAndIncrement(5) // 11 // Composition helper def compose[A, B, C](f: B => C, g: A => B): A => C = (x: A) => f(g(x)) val composed = compose(increment, double) composed(5) // 11 } ``` ## Best Practices 1. **Use descriptive names** - Function names should clearly indicate purpose 2. **Keep functions small** - Single responsibility principle 3. **Prefer immutability** - Don't modify parameters, return new values 4. **Use type annotations** - Explicit return types improve clarity 5. **Leverage higher-order functions** - More concise than explicit recursion 6. **Avoid deep recursion** - Be mindful of stack depth 7. **Use tail recursion** - More efficient for deep recursion 8. **Document complex functions** - Add comments for non-obvious logic ## Common Patterns ### Validation Function ```scala compile { def validate(amount: BigInt): Either[String, BigInt] = if amount < BigInt(0) then Left("Amount cannot be negative") else if amount > BigInt(1000000) then Left("Amount too large") else Right(amount) validate(BigInt(500)) match case Right(value) => value case Left(error) => throw new Exception(error) } ``` ### Transformation Pipeline ```scala compile { val numbers = List(1, 2, 3, 4, 5) val result = numbers .filter(_ % 2 == 0) // Keep evens .map(_ * 2) // Double each .foldLeft(BigInt(0))(_ + _) // Sum all // result: 12 (2*2 + 4*2 = 4 + 8 = 12) } ``` ### Conditional Execution ```scala compile { def conditionalExecute( condition: Boolean, onTrue: () => BigInt, onFalse: () => BigInt ): BigInt = if condition then onTrue() else onFalse() conditionalExecute( balance > BigInt(1000), () => processLargeBalance(balance), () => processSmallBalance(balance) ) } ``` ### Memoization Pattern (Using Map) ```scala compile { // Simple memoization using a map def fibonacci(n: BigInt, memo: Map[BigInt, BigInt]): (BigInt, Map[BigInt, BigInt]) = if n <= BigInt(1) then (n, memo) else memo.get(n) match case Some(result) => (result, memo) case None => val (fib1, memo1) = fibonacci(n - 1, memo) val (fib2, memo2) = fibonacci(n - 2, memo1) val result = fib1 + fib2 (result, memo2 + (n -> result)) val (result, _) = fibonacci(BigInt(10), Map.empty) } ``` ### Error Handling Wrapper ```scala compile { def tryExecute[A](f: () => A, onError: () => A): A = try f() catch case _: Exception => onError() val result = tryExecute( () => riskyOperation(), () => fallbackValue ) } ``` ## Performance Considerations 1. **Function calls have overhead** - Consider inlining small functions 2. **Recursion depth** - Stack depth is limited, use iteration (fold) when possible 3. **Closure captures** - Capturing variables adds memory overhead 4. **Tail recursion** - More efficient than general recursion 5. **Higher-order functions** - May be more expensive than direct code ## Using `inline` Mark functions as `inline` to have them expanded at compile time: ```scala compile { // Inline function - no call overhead inline def square(x: BigInt): BigInt = x * x // Usage - will be expanded to: val result = 5 * 5 val result = square(BigInt(5)) } ``` **Benefits:** - Zero function call overhead - Can enable further optimizations - Useful for small, frequently called functions **Use inline for:** - Simple calculations - Frequently called helpers - Performance-critical code ## Summary - Functions are first-class values - Lambdas provide concise function syntax - Higher-order functions enable functional patterns - Recursion replaces loops - No default parameters, named arguments, varargs, or overloading - No mutually recursive functions - Use `inline` for performance-critical small functions --- Source: https://scalus.org/docs/language-guide/modules --- # Modules Modules in Scalus enable code organization, reusability, and distribution. Use the `@Compile` annotation to create reusable libraries that compile to Plutus Core and can be shared across multiple smart contracts. ## What are Modules? A module is a Scala object annotated with `@Compile` that contains definitions (values, functions, types) that are compiled to Scalus Intermediate Representation (SIR) and can be reused across multiple contracts. ### Key Benefits 1. **Code Reusability** - Write once, use in multiple contracts 2. **Library Distribution** - Package modules as JAR files for sharing 3. **Modular Design** - Separate concerns into logical units 4. **Type Safety** - Full Scala type checking across modules 5. **Optimized Compilation** - Scalus optimizes and inlines module code ## The @Compile Annotation The `@Compile` annotation marks an object for compilation to Plutus Core. ### Basic Usage ```scala import scalus.Compile @Compile object MathUtils: val pi = BigInt(314159) // Approximation * 100000 def square(x: BigInt): BigInt = x * x def abs(x: BigInt): BigInt = if x < BigInt(0) then -x else x ``` ### What @Compile Does When you annotate an object with `@Compile`: 1. **Compilation to SIR** - Scalus compiler plugin transforms the code to Scalus Intermediate Representation 2. ***.sir File Generation** - SIR is serialized to `.sir` files included in your JAR 3. **Cross-Contract Reuse** - Other contracts can import and use the compiled code 4. **Separate Compilation** - Modules compile independently of contracts that use them ### Where to Use @Compile ```scala // Good: Utility functions @Compile object ValidationUtils: def isPositive(n: BigInt): Boolean = n > BigInt(0) def inRange(n: BigInt, min: BigInt, max: BigInt): Boolean = n >= min && n <= max // Good: Domain logic @Compile object TokenLogic: def calculateFee(amount: BigInt, feeRate: BigInt): BigInt = (amount * feeRate) / BigInt(1000000) // Good: Shared constants @Compile object Constants: val minStake = BigInt(2000000) // 2 ADA in Lovelace val maxSupply = BigInt(1000000000) // Bad: Don't use @Compile on validators themselves // Validators should use compile { ... } instead ``` ## The @Ignore Annotation The `@Ignore` annotation excludes definitions from Plutus compilation while keeping them available for off-chain code. ### When to Use @Ignore ```scala import scalus.{Compile, Ignore} @Compile object DataProcessor: // Compiled to Plutus def processValue(x: BigInt): BigInt = x * 2 // NOT compiled to Plutus - only available off-chain @Ignore def debugInfo(x: BigInt): String = s"Processing value: $x" // NOT compiled - used only in tests @Ignore def testHelper(): Unit = println("This is for testing only") // NOT compiled - platform-specific code @Ignore def logToFile(msg: String): Unit = // File I/O not supported in Plutus java.nio.file.Files.writeString(???, msg) ``` ### Common @Ignore Use Cases 1. **Debugging utilities** - Logging, tracing, debug output 2. **Test helpers** - Setup, assertions, test data generation 3. **Documentation** - Example code that shouldn't be in the contract 4. **Platform-specific code** - JVM/JS-specific implementations 5. **Expensive computations** - Operations that should only run off-chain ## Imports and Module Linking ### Importing Modules Simply import the module object and use its definitions: ```scala import scalus.cardano.onchain.plutus.prelude.{*, given} @Compile object Validation: def isPositive(n: BigInt): Boolean = n > BigInt(0) def isNotEmpty(bs: ByteString): Boolean = Builtins.lengthOfByteString(bs) > BigInt(0) // Use the module in a validator import Validation.* val validator = compile { def validate(amount: BigInt, signature: ByteString): Boolean = // Use imported functions isPositive(amount) && isNotEmpty(signature) validate(BigInt(100), ByteString.fromHex("deadbeef")) } ``` ### Nested Module Imports ```scala @Compile object Core: val maxValue = BigInt(1000000) object Nested: def isValid(n: BigInt): Boolean = n <= maxValue // Import nested object import Core.Nested.* val result = compile { isValid(BigInt(500)) // Uses Core.Nested.isValid } ``` ### Wildcard Imports ```scala @Compile object Utils: def add(a: BigInt, b: BigInt): BigInt = a + b def multiply(a: BigInt, b: BigInt): BigInt = a * b def divide(a: BigInt, b: BigInt): BigInt = a / b // Import all functions import Utils.* val calculation = compile { val sum = add(BigInt(10), BigInt(20)) val product = multiply(sum, BigInt(2)) divide(product, BigInt(3)) } ``` ## Linking Modules with compile The `compile { ... }` macro links modules together and compiles them to a single Plutus script. ### How Linking Works ```scala @Compile object ModuleA: val constant = BigInt(100) def helper(x: BigInt): BigInt = x + constant @Compile object ModuleB: def process(x: BigInt): BigInt = x * 2 // compile links both modules into one script val linked = compile { import ModuleA.* import ModuleB.* val value = BigInt(10) val processed = process(value) // From ModuleB val result = helper(processed) // From ModuleA result } ``` ### Compilation Process 1. **Parse** - Scala compiler parses your code 2. **Type Check** - Full Scala type checking 3. **Transform to SIR** - Scalus plugin transforms to SIR 4. **Link** - Modules are linked together 5. **Optimize** - Dead code elimination, inlining 6. **Lower to UPLC** - SIR is compiled to Untyped Plutus Core ### Multiple Modules Example ```scala @Compile object Constants: val minAmount = BigInt(1000000) val feeRate = BigInt(1000) // 0.1% @Compile object Validation: import Constants.* def validateAmount(amount: BigInt): Boolean = amount >= minAmount def calculateFee(amount: BigInt): BigInt = (amount * feeRate) / BigInt(1000000) @Compile object Logic: import Validation.* def processPayment(amount: BigInt): Either[String, BigInt] = if validateAmount(amount) then val fee = calculateFee(amount) Right(amount - fee) else Left("Amount too small") // Link all three modules val validator = compile { import Logic.* processPayment(BigInt(2000000)) match case Right(finalAmount) => finalAmount case Left(error) => throw new Exception(error) } ``` ## Inline Values Inline values are evaluated at compile time and substituted directly into the code, eliminating runtime overhead. ### Defining Inline Values ```scala @Compile object Config: // Inline constant - substituted at compile time inline val minStake = BigInt(2000000) // Inline function - expanded at call site inline def square(x: BigInt): BigInt = x * x // Regular value - evaluated at runtime val dynamicThreshold = BigInt(1000000) ``` ### Benefits of Inline ```scala @Compile object Math: // Without inline val two = BigInt(2) def double(x: BigInt): BigInt = x * two // With inline - no function call overhead inline val twoInline = BigInt(2) inline def doubleInline(x: BigInt): BigInt = x * twoInline val usage = compile { import Math.* // double(5) compiles to: function call to multiply 5 by 'two' val result1 = double(BigInt(5)) // doubleInline(5) compiles to: 5 * 2 (directly) val result2 = doubleInline(BigInt(5)) result2 // More efficient } ``` ### When to Use Inline **Use inline for:** - Mathematical constants (π, e, conversion factors) - Small utility functions called frequently - Configuration values known at compile time - Performance-critical calculations **Don't use inline for:** - Large functions (increases code size) - Values that might change between compilations - Complex logic that benefits from being a separate function ### Inline Examples ```scala @Compile object Constants: // Currency conversion inline val lovelacePerAda = BigInt(1000000) // Time constants inline val secondsPerDay = BigInt(86400) inline val slotsPerEpoch = BigInt(432000) // Inline helper functions inline def adaToLovelace(ada: BigInt): BigInt = ada * lovelacePerAda inline def lovelaceToAda(lovelace: BigInt): BigInt = lovelace / lovelacePerAda val conversion = compile { import Constants.* val ada = BigInt(10) // adaToLovelace(10) expands to: 10 * 1000000 val lovelace = adaToLovelace(ada) lovelace } ``` ## Function Overloading (Not Supported) **IMPORTANT:** Scalus does not support function overloading. Each function must have a unique name. ### What Doesn't Work ```scala @Compile object Overloaded: // NOT SUPPORTED - Compilation error def add(a: BigInt, b: BigInt): BigInt = a + b def add(a: BigInt, b: BigInt, c: BigInt): BigInt = a + b + c // Error! // NOT SUPPORTED - Compilation error def process(x: BigInt): BigInt = x * 2 def process(x: ByteString): ByteString = x // Error! ``` ### Workarounds #### 1. Different Function Names ```scala @Compile object Math: def add2(a: BigInt, b: BigInt): BigInt = a + b def add3(a: BigInt, b: BigInt, c: BigInt): BigInt = a + b + c def add4(a: BigInt, b: BigInt, c: BigInt, d: BigInt): BigInt = a + b + c + d ``` #### 2. Use List for Variable Arguments ```scala @Compile object Math: def addAll(numbers: List[BigInt]): BigInt = numbers.foldLeft(BigInt(0))(_ + _) val result = compile { import Math.* addAll(List(1, 2)) // 3 addAll(List(1, 2, 3)) // 6 addAll(List(1, 2, 3, 4)) // 10 } ``` #### 3. Use Type-Specific Names ```scala @Compile object Processor: def processInt(x: BigInt): BigInt = x * 2 def processBytes(x: ByteString): ByteString = Builtins.appendByteString(x, x) def processList(xs: List[BigInt]): List[BigInt] = xs.map(_ * 2) ``` #### 4. Use Sum Types (Enums) ```scala enum Input: case Single(value: BigInt) case Double(a: BigInt, b: BigInt) case Multiple(values: List[BigInt]) @Compile object Processor: def process(input: Input): BigInt = input match case Input.Single(v) => v case Input.Double(a, b) => a + b case Input.Multiple(vs) => vs.foldLeft(BigInt(0))(_ + _) val result = compile { import Processor.* process(Input.Single(BigInt(10))) // 10 process(Input.Double(BigInt(5), BigInt(7))) // 12 process(Input.Multiple(List(1, 2, 3, 4))) // 10 } ``` ## Distributing Code as a Library Modules can be packaged and distributed as JAR files for reuse across projects. ### Creating a Library Module ```scala // In your library project: mylib/src/main/scala/mylib/Utils.scala package mylib import scalus.Compile import scalus.uplc.builtin.{Builtins, ByteString} @Compile object Validation: inline val minAmount = BigInt(1000000) def isValidAmount(amount: BigInt): Boolean = amount >= minAmount def isValidSignature(pubKey: ByteString, msg: ByteString, sig: ByteString): Boolean = Builtins.verifyEd25519Signature(pubKey, msg, sig) @Compile object TokenUtils: def calculateFee(amount: BigInt, basisPoints: BigInt): BigInt = (amount * basisPoints) / BigInt(10000) def applyFee(amount: BigInt, basisPoints: BigInt): BigInt = amount - calculateFee(amount, basisPoints) ``` ### Publishing the Library ```scala // In your library's build.sbt name := "mylib" organization := "com.example" version := "1.0.0" libraryDependencies += "org.scalus" %% "scalus" % "0.8.0" // Publish to Maven Central or your repository ``` ### Using the Library ```scala // In your contract project's build.sbt libraryDependencies += "com.example" %% "mylib" % "1.0.0" // In your validator code import scalus.cardano.onchain.plutus.prelude.{*, given} import mylib.{Validation, TokenUtils} val validator = compile { val amount = BigInt(5000000) val fee = TokenUtils.calculateFee(amount, BigInt(250)) // 2.5% if Validation.isValidAmount(amount) then TokenUtils.applyFee(amount, BigInt(250)) else throw new Exception("Invalid amount") } ``` ## Best Practices ### 1. Module Organization Organize modules by domain or functionality: ```scala // Good: Organized by domain @Compile object Validation: def validateAmount(amount: BigInt): Boolean = ??? def validateSignature(sig: ByteString): Boolean = ??? @Compile object Calculation: def calculateFee(amount: BigInt): BigInt = ??? def calculateReward(stake: BigInt): BigInt = ??? @Compile object Constants: val minStake = BigInt(2000000) val maxSupply = BigInt(1000000000) // Bad: Mixing unrelated concerns @Compile object Everything: val minStake = BigInt(2000000) def validateAmount(amount: BigInt): Boolean = ??? def calculateFee(amount: BigInt): BigInt = ??? def processTransaction(tx: Transaction): Boolean = ??? ``` ### 2. Use Inline for Constants ```scala @Compile object Config: // Good: Inline for compile-time constants inline val protocolVersion = BigInt(3) inline val minUtxoValue = BigInt(1000000) // Good: Regular val for values that might change val currentEpoch = BigInt(450) ``` ### 3. Keep Modules Focused ```scala // Good: Single responsibility @Compile object TimeUtils: inline val secondsPerDay = BigInt(86400) def daysSince(timestamp: BigInt, reference: BigInt): BigInt = (timestamp - reference) / secondsPerDay // Bad: Too many responsibilities @Compile object Utils: // Time functions def daysSince(timestamp: BigInt, reference: BigInt): BigInt = ??? // String functions def concatenate(a: String, b: String): String = ??? // Crypto functions def hash(data: ByteString): ByteString = ??? ``` ### 4. Document Public APIs ```scala @Compile object TokenLogic: /** * Calculate transaction fee based on amount and fee rate. * * @param amount Transaction amount in Lovelace * @param feeRate Fee rate in basis points (1 bp = 0.01%) * @return Fee amount in Lovelace */ def calculateFee(amount: BigInt, feeRate: BigInt): BigInt = (amount * feeRate) / BigInt(10000) ``` ### 5. Minimize Module Dependencies ```scala // Good: Independent modules @Compile object Math: def square(x: BigInt): BigInt = x * x @Compile object Validation: // Self-contained, no dependencies on Math def isPositive(n: BigInt): Boolean = n > BigInt(0) // Acceptable: Clear dependency chain @Compile object Advanced: import Math.* def sumOfSquares(a: BigInt, b: BigInt): BigInt = square(a) + square(b) ``` ### 6. Use @Ignore Liberally ```scala @Compile object DataProcessor: def processValue(x: BigInt): BigInt = x * 2 // Test helpers - not in Plutus @Ignore def testWithRandomValue(): BigInt = processValue(scala.util.Random.nextInt(100)) // Debugging - not in Plutus @Ignore def debugProcess(x: BigInt): Unit = println(s"Processing $x -> ${processValue(x)}") ``` ## Common Patterns ### Validation Module Pattern ```scala @Compile object Validation: import scalus.uplc.builtin.Builtins inline val minAmount = BigInt(1000000) inline val maxAmount = BigInt(1000000000) def isValidAmount(amount: BigInt): Boolean = amount >= minAmount && amount <= maxAmount def isValidHash(hash: ByteString): Boolean = Builtins.lengthOfByteString(hash) == BigInt(32) def isValidPubKey(pubKey: ByteString): Boolean = Builtins.lengthOfByteString(pubKey) == BigInt(32) ``` ### Constants Module Pattern ```scala @Compile object ProtocolConstants: // Network parameters inline val slotsPerEpoch = BigInt(432000) inline val slotDuration = BigInt(1) // 1 second // Economic parameters inline val minUtxoValue = BigInt(1000000) inline val minPoolCost = BigInt(340000000) // Conversion helpers inline def epochToSlot(epoch: BigInt): BigInt = epoch * slotsPerEpoch inline def slotToEpoch(slot: BigInt): BigInt = slot / slotsPerEpoch ``` ### Helper Functions Pattern ```scala @Compile object Helpers: // List operations def sumList(numbers: List[BigInt]): BigInt = numbers.foldLeft(BigInt(0))(_ + _) def maxList(numbers: List[BigInt]): Option[BigInt] = numbers match case Nil => None case head :: tail => Some(tail.foldLeft(head)((a, b) => if a > b then a else b)) // ByteString operations def concat(strings: List[ByteString]): ByteString = strings.foldLeft(ByteString.empty)(Builtins.appendByteString) ``` ### State Machine Module Pattern ```scala enum State: case Initial case Active(value: BigInt) case Locked(value: BigInt, until: BigInt) case Finalized(result: BigInt) @Compile object StateMachine: def transition(state: State, action: Action, currentTime: BigInt): State = (state, action) match case (State.Initial, Action.Start(value)) => State.Active(value) case (State.Active(value), Action.Lock(until)) => State.Locked(value, until) case (State.Locked(value, until), Action.Unlock) => if currentTime >= until then State.Active(value) else state case (State.Active(value), Action.Finalize) => State.Finalized(value) case _ => state // Invalid transition, keep current state ``` ## Summary - **@Compile** marks objects for compilation to Plutus Core - **@Ignore** excludes definitions from Plutus compilation - **Modules** enable code reusability and library distribution - **Import** brings module definitions into scope - **compile { }** links modules into a single script - **inline** values are substituted at compile time for efficiency - **Function overloading is NOT supported** - use different names or workarounds - Organize modules by domain for maintainability - Use inline for constants and small functions - Document public APIs for library consumers --- Source: https://scalus.org/docs/language-guide --- # Scalus Language Guide Learn how to write smart contracts for Cardano using Scala 3. This guide covers Scalus language features, syntax, and programming patterns for building efficient, type-safe validators that compile to Plutus Core. ## Introduction - **[Why Scala 3?](/docs/language-guide/scala3)** - Discover why Scala 3 is ideal for blockchain development. Learn about its multi-platform versatility, industry-proven reliability, and how it's used by major financial institutions and tech companies. - **[Supported Scala Features](/docs/language-guide/support)** - Understand which Scala features are supported when compiling to Plutus Core, including lambdas, pattern matching, higher-order functions, and more. ## Fundamental Types - **[Primitive Types](/docs/language-guide/constants-primitives)** - Work with Plutus primitive types using familiar Scala syntax: `Unit`, `Boolean`, `BigInt`, `ByteString`, `String`, and `Data`. - **[Custom Data Types](/docs/language-guide/data-types)** - Define smart contract data structures using case classes and enums that automatically serialize to Plutus `Data`. - **[Collections](/docs/language-guide/collections)** - Use immutable `List`, `AssocMap` (key-value pairs), and efficient collection operations for on-chain data manipulation. ## Programming Constructs - **[Functions](/docs/language-guide/functions)** - Write named functions, lambdas, recursive functions, and higher-order functions. Learn about function composition and currying for functional programming patterns. - **[Control Flow](/docs/language-guide/control-flow)** - Control program execution with `if-then-else`, pattern matching on case classes and enums, and handling conditional logic efficiently. - **[Builtin Functions](/docs/language-guide/builtin-functions)** - Access Plutus builtin functions for cryptographic operations, byte string manipulation, list operations, and blockchain-specific utilities. ## Advanced Topics - **[Modules](/docs/language-guide/modules)** - Organize code into reusable modules with `@Compile`, create shareable libraries, and build modular smart contract architectures. ## Language Constraints Scalus compiles to Untyped Plutus Core (UPLC), a minimalist lambda calculus optimized for blockchain execution. This means only a **subset of Scala features** is supported - those that can be efficiently translated to UPLC while maintaining security and determinism. **Supported**: Functions, pattern matching, case classes, enums, higher-order functions, recursion, `given`/`using`, inline macros **Not Supported**: Try-catch (except `throw`), mutable variables (`var`), complex pattern matching, JVM-specific features, effects, arbitrary type class instances See [Supported Scala Features](/docs/language-guide/support) for the complete list. ## Getting Started New to Scalus? Start with: 1. [Why Scala 3?](/docs/language-guide/scala3) - Understand the language choice 2. [Primitive Types](/docs/language-guide/constants-primitives) - Learn the basic building blocks 3. [Your First Smart Contract](/docs/smart-contracts/developing-smart-contracts) - Write your first validator Already familiar with Scala? Jump to [Supported Features](/docs/language-guide/support) to understand the constraints, then explore [Builtin Functions](/docs/language-guide/builtin-functions) for blockchain-specific operations. --- Source: https://scalus.org/docs/smart-contracts/developing-smart-contracts --- # Build 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: ```sh copy sbt new scalus3/validator.g8 ``` When prompted, name the application `hello cardano`. This command scaffolds a simple Scalus validator project. ```console sbt new scalus3/validator.g8 name [MyValidator]: hello cardano Template applied in ./hello-cardano ``` ## Project structure Let’s take a look at what just got generated: ```ansi hello-cardano/ ├── HelloCardano.scala # Simple validator ├── HelloCardano.test.scala # Simple tests ├── project.scala # Project configuration └── README.md ``` Let's test our validator ```sh copy cd hello-cardano sbt test ``` If you see this message, everything is going fine! ```sh {2} 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 1. 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. 2. **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. 3. **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: ```scala copy {4-9, 11, 14-15, 18-19} @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: 1. **Lock**: Create a UTxO with funds and attach a datum containing the owner's public key hash 2. **Unlock**: Provide a redeemer with "Hello, Cardano!", sign the transaction, and both checks must pass Learn more about [Cardano Validators](/docs/smart-contracts/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. ```scala copy lines {1, 3, 5, 7, 10, 18, 21} 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?** 1. **`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. 2. **`runScript()`** - Executes the compiled validator with the provided `ScriptContext`, simulating on-chain execution. Returns a `Result` indicating success or failure with execution costs. 3. **`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`). 4. **`makeSpendingScriptContext()`** - A helper from `ScalusTest` that creates a properly structured `ScriptContext` for 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. ```scala copy lines {1, 3, 14} 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 ```sh copy scala-cli test hello-cardano ``` You should see output showing all tests passing: ``` HelloCardanoTest: - HelloCardano validates correct message and signature - HelloCardano fails with wrong message ``` Learn more about [Testing Smart Contracts](/docs/testing/unit-testing). ## 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: 1. **Use your IDE's debugger** - Set breakpoints, step through execution, inspect variables 2. **Call validators directly from the test** - Run in the debug mode. Look how simple it is! Learn more about [Debugging Smart Contracts](/docs/testing/debugging). ## Compiling into Plutus script Use `PlutusV3.compile` to transform your Scala validator into a Plutus script: ```scala import scalus.compiler.Options import scalus.uplc.PlutusV3 // Compile your validator to Plutus V3 private given Options = Options.release val compiled = PlutusV3.compile(HelloCardano.validate) // Get the script hex for use in transactions val scriptHex = compiled.script.doubleCborHex ``` Learn more about [Compiling Smart Contracts](/docs/smart-contracts/compiling). ## 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 - **[HTLC Tutorial](/docs/smart-contracts/htlc-tutorial)** - Build a complete Hash Time-Locked Contract with transactions and tests - **[Validators in Depth](/docs/smart-contracts/validators)** - Learn about different validator types and patterns - **[Testing Smart Contracts](/docs/testing/unit-testing)** - Advanced testing techniques and property-based testing - **[Debugging and Troubleshooting](/docs/testing/debugging)** - Deep dive into debugging strategies - **[Compiling Smart Contracts](/docs/smart-contracts/compiling)** - Generate Plutus script and configure compilation options - **[Working with Contract](/docs/dapp-development/working-with-contract)** - Set options, generate blueprints, and get script addresses - **[First Contract Transaction](/docs/transactions/first-contract-transaction)** - Lock funds and spend from a script address - **[Building Transactions](/docs/transactions/building-first-transaction)** - Build and submit simple ADA transfers - **[Examples Repository](https://github.com/scalus3/scalus/tree/master/scalus-examples)** - Real-world validator examples --- Source: https://scalus.org/docs/smart-contracts/htlc-tutorial --- # Build an HTLC: From Contract to Mainnet This tutorial takes you through the complete lifecycle of a Cardano smart contract — from design thinking to mainnet deployment. You'll build a Hash Time-Locked Contract (HTLC) while experiencing what makes Scalus different: **one language for everything**, **real debugging**, and **professional testing**. **Prerequisites:** This tutorial assumes you've completed the [Getting Started](/docs/get-started) guide and have a Scalus project set up. If not, start there first. ## The Problem: Trustless Conditional Payments Imagine Alice wants to pay Bob, but only if Bob reveals a secret within 24 hours. If Bob doesn't act in time, Alice gets her money back. Neither party should be able to cheat. This is a **Hash Time-Locked Contract** — the building block for: - **Atomic swaps** between blockchains (swap ADA for BTC without trusting an exchange) - **Payment channels** for instant off-chain payments - **Escrow** with automatic refunds The challenge: how do you enforce "reveal secret OR timeout" without a trusted third party? That's what smart contracts do. ## Designing the Contract Before writing code, let's think through what we need. ### What Data Lives On-Chain? (The Datum) When Alice locks funds, the contract needs to remember: | Field | Purpose | |-------|---------| | `committer` | Alice's public key hash — can reclaim after timeout | | `receiver` | Bob's public key hash — can claim with secret | | `image` | Hash of the secret (Bob must reveal the preimage) | | `timeout` | Deadline as POSIX time | This is our **datum** — configuration stored with the locked UTxO. ### What Actions Are Possible? (The Redeemer) Two ways to spend the locked funds: 1. **Reveal** — Bob provides the secret (preimage) before timeout 2. **Timeout** — Alice reclaims after the deadline passes This is our **redeemer** — the action being validated. ### Time Handling on Cardano Cardano validators can't read the current time directly. Instead, transactions declare a **validity interval** — the blockchain only accepts the transaction within that window. - **Reveal**: Transaction must be valid only before timeout (`validTo <= timeout`) - **Timeout**: Transaction must be valid only after timeout (`validFrom >= timeout`) The blockchain enforces these bounds, and our validator checks they're set correctly. ## Writing the Validator Now let's implement. In Scalus, we define types with automatic serialization: ```scala import scalus.* import scalus.uplc.builtin.Builtins.sha3_256 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.* // Datum — stored when locking funds case class Config( committer: PubKeyHash, receiver: PubKeyHash, image: ByteString, // sha3_256(secret) timeout: PosixTime ) derives FromData, ToData @Compile object Config // Redeemer — action when spending enum Action derives FromData, ToData: case Timeout case Reveal(preimage: ByteString) @Compile object Action ``` **Why `derives FromData, ToData`?** Cardano stores all on-chain data in a universal `Data` format. Scalus automatically generates serialization code, so you work with typed Scala values while the blockchain sees `Data`. Type errors are caught at compile time, not on-chain. ### The Validator Logic The complete validator with entry point and error messages: ```scala @Compile object HtlcValidator { // Entry point called by Cardano inline def validate(scData: Data): Unit = { val ctx = scData.to[ScriptContext] ctx.scriptInfo match case ScriptInfo.SpendingScript(txOutRef, datum) => spend(datum, ctx.redeemer, ctx.txInfo, txOutRef) case _ => fail(MustBeSpending) } // Spending validation logic inline def spend( datum: Option[Data], redeemer: Data, tx: TxInfo, ownRef: TxOutRef ): Unit = { val config = datum.getOrFail(InvalidDatum).to[Config] redeemer.to[Action] match case Action.Timeout => // COMMITTER RECLAIMS: must be after timeout, signed by committer val validFrom = tx.validRange.from.finite(0) require(config.timeout <= validFrom, InvalidCommitterTimePoint) require(tx.isSignedBy(config.committer), UnsignedCommitterTransaction) case Action.Reveal(preimage) => // RECEIVER CLAIMS: must be before timeout, correct secret, signed val validTo = tx.validRange.to.finiteOrFail(ValidRangeMustBeBound) require(validTo <= config.timeout, InvalidReceiverTimePoint) require(tx.isSignedBy(config.receiver), UnsignedReceiverTransaction) require(sha3_256(preimage) == config.image, InvalidReceiverPreimage) } // Error messages — inline vals compile to string constants inline val MustBeSpending = "Must be a spending script" inline val InvalidDatum = "Invalid Datum" inline val ValidRangeMustBeBound = "ValidTo must be set" inline val UnsignedCommitterTransaction = "Must be signed by committer" inline val UnsignedReceiverTransaction = "Must be signed by receiver" inline val InvalidCommitterTimePoint = "Must be after timeout" inline val InvalidReceiverTimePoint = "Must be before timeout" inline val InvalidReceiverPreimage = "Invalid preimage" } ``` **Why `inline val` for error messages?** They compile to string constants in UPLC, making errors readable in block explorers. These same constants are used in tests to verify the correct error is thrown. **What each check does:** | Check | Prevents | |-------|----------| | `config.timeout <= validFrom` | Committer reclaiming before timeout | | `validTo <= config.timeout` | Receiver claiming after timeout | | `sha3_256(preimage) == config.image` | Receiver claiming with wrong secret | | `tx.isSignedBy(...)` | Anyone else stealing funds | The validator is 84 lines total. It compiles to **569 bytes** of Plutus Core. ## The Scalus Advantage: Debugging Here's something you can't do in other Cardano languages: **set a breakpoint and step through your validator**. In Scalus, your validator is regular Scala code. Before it's compiled to Plutus Core, you can: 1. Write a test that calls your validator directly 2. Set a breakpoint on any line 3. Step through execution, inspect variables, see exactly why validation fails ```scala test("debug why receiver fails") { // Call validator directly — it's just Scala HtlcValidator.spend( datum = Some(config.toData), redeemer = Action.Reveal(wrongPreimage).toData, tx = mockTxInfo, ownRef = mockRef ) // Set breakpoint above, step through, see sha3_256(wrongPreimage) != image } ``` **This is impossible in Aiken or Plutus.** When your validator fails on-chain, you get an error code. In Scalus, you debug it like any Scala application. ## Testing Like a Professional Scalus integrates with ScalaTest and ScalaCheck — tools Scala developers already know. ### Unit Tests with ScalusTest The `ScalusTest` trait provides helpers for creating script contexts and asserting failures: ```scala import org.scalatest.funsuite.AnyFunSuite import scalus.uplc.builtin.Builtins.sha3_256 import scalus.cardano.ledger.* import scalus.cardano.node.Emulator import scalus.testing.kit.Party.{Alice, Bob, Eve} import scalus.testing.kit.{ScalusTest, TestUtil} import java.time.Instant class HtlcTest extends AnyFunSuite with ScalusTest { private given env: CardanoInfo = TestUtil.testEnvironment private val contract = HtlcContract.compiled.withErrorTraces private val txCreator = HtlcTransactions(env = env, contract = contract) // Test data val validPreimage: Preimage = genByteStringOfN(32).sample.get val wrongPreimage: Preimage = genByteStringOfN(12).sample.get private val image: Image = sha3_256(validPreimage) // Time setup private val slot: SlotNo = 10 private val timeout: Instant = env.slotConfig.slotToInstant(slot) private val beforeTimeout: Instant = env.slotConfig.slotToInstant(slot - 1) private val afterTimeout: Instant = env.slotConfig.slotToInstant(slot + 1) private def createProvider: Emulator = Emulator.withAddresses(Seq(Alice.address, Bob.address, Eve.address)) test("receiver reveals preimage before timeout") { val provider = createProvider val lockedUtxo = lock(provider) val utxos = provider.findUtxos(Bob.address).await().toOption.get val revealTx = txCreator.reveal( utxos = utxos, lockedUtxo = lockedUtxo, payeeAddress = Bob.address, sponsor = Bob.address, preimage = validPreimage, receiverPkh = Bob.addrKeyHash, validTo = timeout, signer = Bob.signer ) provider.setSlot(slot - 1) val result = provider.submit(revealTx).await() assert(result.isRight, s"Should succeed: $result") } test("receiver fails with wrong preimage") { val provider = createProvider val lockedUtxo = lock(provider) val utxos = provider.findUtxos(Bob.address).await().toOption.get // assertScriptFail checks the error message matches assertScriptFail(HtlcValidator.InvalidReceiverPreimage) { txCreator.reveal( utxos = utxos, lockedUtxo = lockedUtxo, payeeAddress = Bob.address, sponsor = Bob.address, preimage = wrongPreimage, // Wrong! receiverPkh = Bob.addrKeyHash, validTo = timeout, signer = Bob.signer ) } } test("committer reclaims after timeout") { val provider = createProvider val lockedUtxo = lock(provider) val utxos = provider.findUtxos(Alice.address).await().toOption.get val timeoutTx = txCreator.timeout( utxos = utxos, lockedUtxo = lockedUtxo, payeeAddress = Alice.address, sponsor = Alice.address, committerPkh = Alice.addrKeyHash, validFrom = afterTimeout, signer = Alice.signer ) provider.setSlot(slot + 1) val result = provider.submit(timeoutTx).await() assert(result.isRight, s"Should succeed: $result") } } ``` **`assertScriptFail`** checks that the script fails with the expected error message. It uses the same error constants defined in the validator, ensuring tests and contract stay in sync. ### Property-Based Testing With ScalaCheck, generate hundreds of test cases automatically: ```scala test("any random preimage fails except the correct one") { forAll(genByteStringOfN(32)) { randomPreimage => whenever(randomPreimage != validPreimage) { assertScriptFail("Wrong secret") { txCreator.reveal(preimage = randomPreimage, ...) } } } } ``` This found a real bug in an early version: preimages of different lengths weren't handled correctly. ### What to Test | Scenario | Expected | |----------|----------| | Correct preimage, before timeout | Success | | Wrong preimage | Fail: "Wrong secret" | | Correct preimage, after timeout | Fail: "Too late" | | No signature from receiver | Fail: "Only receiver can claim" | | Committer reclaims after timeout | Success | | Committer reclaims before timeout | Fail: "Too early" | | Random attacker tries either path | Fail: signature check | ## End-to-End: Same Language for Transactions In most Cardano development, you write contracts in one language (Aiken, Plutus) and transactions in another (JavaScript, Python). Context switching slows you down. **In Scalus, it's all Scala.** Your transaction builder uses the same types as your validator. ### Why Transactions Matter for Testing Here's a key insight: **you build transactions to test your validator**. The transaction builder creates the `ScriptContext` that your validator receives. This means: 1. Build a transaction with `TxBuilder` 2. Extract the `ScriptContext` for a specific input 3. Run your validator against it 4. Check if it passes or fails with the expected error This is how the tests work — they build real transactions, extract the script context, and verify the validator behavior. No mocking required. Learn more: [Building Transactions](/docs/transactions/building-first-transaction) | [Transaction Builder API](/docs/transactions) ### The Transaction Builder ```scala import scalus.uplc.builtin.Data import scalus.cardano.address.Address import scalus.cardano.ledger.* import scalus.cardano.txbuilder.* import scalus.cardano.onchain.plutus.v1.PubKeyHash import scalus.uplc.PlutusV3 import java.time.Instant case class HtlcTransactions( env: CardanoInfo, contract: PlutusV3[Data => Unit] ) { private val script: Script.PlutusV3 = contract.script private val scriptAddress: Address = contract.address(env.network) private val builder = TxBuilder(env) /** Lock funds in the HTLC */ def lock( utxos: Utxos, value: Value, sponsor: Address, committer: AddrKeyHash, receiver: AddrKeyHash, image: Image, timeout: Instant, signer: TransactionSigner ): Transaction = { // Same Config type as the validator — shared between on-chain and off-chain val datum = Config(PubKeyHash(committer), PubKeyHash(receiver), image, timeout.toEpochMilli) builder .payTo(scriptAddress, value, datum) .complete(availableUtxos = utxos, sponsor = sponsor) .sign(signer) .transaction } /** Receiver claims with preimage (before timeout) */ def reveal( utxos: Utxos, lockedUtxo: Utxo, payeeAddress: Address, sponsor: Address, preimage: Preimage, receiverPkh: AddrKeyHash, validTo: Instant, signer: TransactionSigner ): Transaction = { // Same Action type — compiler ensures correct redeemer val redeemer = Action.Reveal(preimage) builder .spend(lockedUtxo, redeemer, script, Set(receiverPkh)) .payTo(payeeAddress, lockedUtxo.output.value) .validTo(validTo) // Must be before timeout .complete(availableUtxos = utxos, sponsor) .sign(signer) .transaction } /** Committer reclaims (after timeout) */ def timeout( utxos: Utxos, lockedUtxo: Utxo, payeeAddress: Address, sponsor: Address, committerPkh: AddrKeyHash, validFrom: Instant, signer: TransactionSigner ): Transaction = { val redeemer = Action.Timeout builder .spend(lockedUtxo, redeemer, script, Set(committerPkh)) .payTo(payeeAddress, lockedUtxo.output.value) .validFrom(validFrom) // Must be after timeout .complete(availableUtxos = utxos, sponsor) .sign(signer) .transaction } } ``` If you accidentally use `Action.Timeout` when you meant `Action.Reveal`, the compiler tells you — not the blockchain. ### Usage Example ```scala // Setup val contract = HtlcContract.compiled.withErrorTraces val txCreator = HtlcTransactions(cardanoInfo, contract) val preimage = generateRandomBytes(32) val image = sha3_256(preimage) val timeout = Instant.now().plusHours(24) // 1. Alice locks 100 ADA for Bob val lockTx = txCreator.lock( utxos = aliceUtxos, value = Value.ada(100), sponsor = alice.address, committer = alice.addrKeyHash, receiver = bob.addrKeyHash, image = image, timeout = timeout, signer = alice.signer ) provider.submit(lockTx) // 2a. Bob claims with preimage (happy path) val revealTx = txCreator.reveal( utxos = bobUtxos, lockedUtxo = lockedUtxo, payeeAddress = bob.address, sponsor = bob.address, preimage = preimage, receiverPkh = bob.addrKeyHash, validTo = timeout, signer = bob.signer ) provider.submit(revealTx) // 2b. OR Alice reclaims after timeout (if Bob didn't claim) val timeoutTx = txCreator.timeout( utxos = aliceUtxos, lockedUtxo = lockedUtxo, payeeAddress = alice.address, sponsor = alice.address, committerPkh = alice.addrKeyHash, validFrom = timeout.plusSeconds(1), signer = alice.signer ) provider.submit(timeoutTx) ``` ## From Emulator to Mainnet Scalus provides a progression of testing environments — same code, increasing realism: ### Emulator (Instant Feedback) The **Emulator** is an in-memory Cardano simulation. It: - Runs instantly (no waiting for blocks) - Validates transactions against ledger rules - Evaluates scripts with the real Plutus VM - Doesn't require Docker or external services Use it for rapid iteration — run hundreds of tests in seconds. ### YaciDevKit (Local Devnet) **YaciDevKit** runs a real Cardano node in Docker. It: - Has actual block production and consensus - Produces real transaction IDs - Simulates network delays and slot timing - Catches issues the emulator might miss (timing edge cases, serialization) Use it before deploying to testnet — it's the closest to production without spending real ADA. Learn more: [Local Devnet Setup](/docs/testing/local-devnet) ### Testnet and Mainnet Finally, deploy to **Preprod** (testnet) with real network conditions, then **Mainnet**. ### Same Test Code, Different Backends ```scala class HtlcIntegrationTest extends AnyFunSuite with IntegrationTest { test(s"[${testEnvName}] receiver reveals preimage") { val lockedUtxo = lock(...) val revealTx = txCreator.reveal(preimage = validPreimage, ...) val result = ctx.submit(revealTx).await() assert(result.isRight) } } ``` Run with different environments: ```sh # Emulator (instant, in-memory) sbtn scalusCardanoLedgerIt/testOnly *HtlcIntegrationTest # YaciDevKit (local devnet with Docker) SCALUS_TEST_ENV=yaci sbtn scalusCardanoLedgerIt/testOnly *HtlcIntegrationTest # Preprod (real testnet via Blockfrost) SCALUS_TEST_ENV=preprod \ BLOCKFROST_API_KEY=your_key \ WALLET_MNEMONIC_PREPROD="your mnemonic words..." \ sbtn scalusCardanoLedgerIt/testOnly *HtlcIntegrationTest ``` **The workflow:** Start with emulator for fast iteration, validate on YaciDevKit for confidence, deploy to testnet for final verification, then mainnet. ## Compiling and Blueprint Generation The contract compilation and CIP-57 blueprint in one file: ```scala import scalus.cardano.blueprint.Blueprint import scalus.compiler.Options import scalus.uplc.PlutusV3 object HtlcContract { // Release mode for smaller script size private given Options = Options.release // Compile validator to Plutus V3 lazy val compiled = PlutusV3.compile(HtlcValidator.validate) // Generate CIP-57 blueprint for wallets and explorers lazy val blueprint = Blueprint.plutusV3[Config, Action]( title = "Hash Time-Locked Contract", description = "Releases funds when recipient reveals hash preimage before deadline, otherwise refunds to sender.", version = "1.0.0", license = Some("Apache License Version 2.0"), compiled = compiled ) @main def main(): Unit = { println(s"Script size: ${compiled.script.script.size} bytes") println(blueprint.toJson()) } } ``` Run to see the script size and blueprint: ```sh scala-cli run HtlcContract.scala # Script size: 569 bytes # { "preamble": { "title": "Hash Time-Locked Contract", ... }, "validators": [...] } ``` ## Summary: The Scalus Development Experience | Step | What Scalus Gives You | |------|----------------------| | **Design** | Scala types with automatic serialization | | **Implement** | Familiar language, IDE support, type safety | | **Debug** | Breakpoints, step-through, variable inspection | | **Test** | ScalaTest + ScalaCheck, property-based testing | | **Transactions** | Same types, compiler-checked correctness | | **Deploy** | Emulator (fast) → YaciDevKit (realistic) → Testnet → Mainnet | ## Full Source Code - [HtlcValidator.scala](https://github.com/nau/scalus/blob/master/scalus-examples/shared/src/main/scala/scalus/examples/htlc/HtlcValidator.scala) — Smart contract - [HtlcTransactions.scala](https://github.com/nau/scalus/blob/master/scalus-examples/shared/src/main/scala/scalus/examples/htlc/HtlcTransactions.scala) — Transaction building - [HtlcTest.scala](https://github.com/nau/scalus/blob/master/scalus-examples/jvm/src/test/scala/scalus/examples/htlc/HtlcTest.scala) — Unit tests - [HtlcIntegrationTest.scala](https://github.com/nau/scalus/blob/master/scalus-cardano-ledger-it/src/test/scala/scalus/testing/integration/HtlcIntegrationTest.scala) — Integration tests Based on the [Rosetta Smart Contracts HTLC specification](https://github.com/blockchain-unica/rosetta-smart-contracts/tree/main/contracts/htlc). ## Next Steps - **[Working with Contract](/docs/dapp-development/working-with-contract)** — Compilation options, blueprints, and script addresses - **[First Contract Transaction](/docs/transactions/first-contract-transaction)** — Lock and spend from a script address - **[Building Transactions](/docs/transactions/building-first-transaction)** — Transaction builder API in depth - **[Debugging Guide](/docs/testing/debugging)** — Deep dive into IDE debugging - **[Testing Guide](/docs/testing/unit-testing)** — Property-based testing patterns - **[Parameterized Validators](/docs/smart-contracts/parameterized-validators)** — Make HTLC reusable - **[Security Guide](/docs/security)** — Common vulnerabilities to avoid --- Source: https://scalus.org/docs/smart-contracts/validators --- # Cardano Validator Types A Cardano validator is a predicate function that approves or rejects transactions. It either succeeds (returns `Unit`) or fails (throws an exception). Scalus compiles Scala validators to Plutus Core bytecode. ## Quick Reference | Purpose | Method | Validates | Common Use Cases | |---------|--------|-----------|------------------| | Spending | `spend(...)` | UTxO consumption | Escrow, vesting, DEX orders | | Minting | `mint(...)` | Token creation/burning | NFT collections, fungible tokens | | Rewarding | `reward(...)` | Stake reward withdrawal | DAO treasuries | | Certifying | `certify(...)` | Delegation certificates | Controlled delegation | | Voting | `vote(...)` | Governance votes | DAO voting | | Proposing | `propose(...)` | Governance proposals | Treasury limits | ## Creating a Validator To create a validator, extend the `Validator` trait and annotate with `@Compile`: ```scala copy @Compile object MyValidator extends Validator: inline override def spend( datum: Option[Data], redeemer: Data, tx: TxInfo, ownRef: TxOutRef ): Unit = { // validation logic here } ``` The `@Compile` annotation tells Scalus to compile your Scala code into Plutus Core bytecode. ## The Validator Trait The `Validator` trait is the foundation for all smart contracts. It provides methods for all six Plutus V3 script purposes: ```scala copy @Compile trait Validator { inline def validate(scData: Data): Unit inline def validateScriptContext(sc: ScriptContext): Unit = { sc.scriptInfo match case ScriptInfo.MintingScript(policyId) => mint(sc.redeemer, policyId, sc.txInfo) case ScriptInfo.SpendingScript(txOutRef, datum) => spend(datum, sc.redeemer, sc.txInfo, txOutRef) case ScriptInfo.RewardingScript(credential) => reward(sc.redeemer, credential, sc.txInfo) case ScriptInfo.CertifyingScript(index, cert) => certify(sc.redeemer, cert, sc.txInfo) case ScriptInfo.VotingScript(voter) => vote(sc.redeemer, voter, sc.txInfo) case ScriptInfo.ProposingScript(index, procedure) => propose(procedure, sc.txInfo) } // Override the methods you need inline def spend(datum: Option[Data], redeemer: Data, tx: TxInfo, ownRef: TxOutRef): Unit = ??? inline def mint(redeemer: Data, policyId: PolicyId, tx: TxInfo): Unit = ??? inline def reward(redeemer: Data, stakingKey: Credential, tx: TxInfo): Unit = ??? inline def certify(redeemer: Data, cert: TxCert, tx: TxInfo): Unit = ??? inline def vote(redeemer: Data, voter: Voter, tx: TxInfo): Unit = ??? inline def propose(proposalProcedure: ProposalProcedure, tx: TxInfo): Unit = ??? } ``` The `validate` method is the entry point called by Cardano. It deserializes the script context and routes to the appropriate handler method. ## Script Purposes ### Spending Scripts **Most common validator type.** Controls whether a UTxO can be spent. ```scala copy inline override def spend( datum: Option[Data], // Data attached to the UTxO redeemer: Data, // Data provided by the spender tx: TxInfo, // Transaction script execution context ownRef: TxOutRef // Reference to the UTxO being spent ): Unit ``` **Use cases:** Escrow, vesting, multi-signature wallets, DEX order books, NFT marketplaces ### Minting Policies Governs creation and destruction of native tokens. ```scala copy inline override def mint( redeemer: Data, // Data provided by the minter policyId: PolicyId, // The policy ID of tokens being minted/burned tx: TxInfo // Transaction script execution context ): Unit ``` **Use cases:** NFT collections, fungible tokens, access tokens, time-locked minting ### Rewarding Scripts Validates withdrawal of staking rewards. ```scala copy inline override def reward( redeemer: Data, // Data provided by the withdrawer stakingKey: Credential, // The stake credential tx: TxInfo // Transaction script execution context ): Unit ``` **Use cases:** DAO treasury withdrawals, controlled reward distribution ### Certifying Scripts Controls publication of delegation certificates. ```scala copy inline override def certify( redeemer: Data, // Data provided by the certificate publisher cert: TxCert, // The certificate being published tx: TxInfo // Transaction script execution context ): Unit ``` **Use cases:** Controlled stake delegation, DAO-managed stake pools ### Voting Scripts Validates governance votes (CIP-1694). ```scala copy inline override def vote( redeemer: Data, // Data provided by the voter voter: Voter, // The voter identity tx: TxInfo // Transaction script execution context ): Unit ``` **Use cases:** DAO voting, delegated voting rights, quadratic voting ### Proposing Scripts Constitution guardrails for governance proposals. ```scala copy inline override def propose( proposalProcedure: ProposalProcedure, // The proposal being submitted tx: TxInfo // Transaction script execution context ): Unit ``` **Use cases:** Treasury spending limits, parameter change constraints, protocol upgrade requirements ## Next Steps - **[Parameterized Validators](/docs/smart-contracts/parameterized-validators)** — Reusable validators with compile-time configuration - **[Testing Validators](/docs/testing/unit-testing)** — Test your validators thoroughly - **[Debugging](/docs/testing/debugging)** — Debug validators in your IDE - **[Building Transactions](/docs/transactions/building-first-transaction)** — Use validators in transactions --- Source: https://scalus.org/docs/smart-contracts/parameterized-validators --- # Parameterized Validators Parameterized validators let you write validator logic once and deploy it multiple times with different configurations. Each parameter combination produces a unique script with its own address. ## How It Works When you compile a validator with specific values, those values are permanently embedded into the UPLC bytecode as constants. The compiled script doesn't receive these values at runtime—they're hardcoded during compilation. ```scala // Define a parameterized validator @Compile object TimeLock: inline def validate(deadline: PosixTime)(datum: Option[Data], redeemer: Data, tx: TxInfo, outRef: TxOutRef): Unit = require(tx.validRange.isEntirelyAfter(deadline), "Too early") // ... rest of validation // Compile with a specific deadline val compiled = compile(TimeLock.validate(PosixTime(1735689600000L))) val program = compiled.toUplc().plutusV3 ``` Each different `deadline` value produces a completely different script with a different script hash and address. ## Use Cases ### Multi-Signature Wallets Write one multisig logic, deploy with different signer sets: ```scala @Compile object MultiSig: inline def validate(requiredSigners: List[PubKeyHash], threshold: BigInt)( datum: Option[Data], redeemer: Data, tx: TxInfo, outRef: TxOutRef ): Unit = val signatureCount = requiredSigners.count(tx.signatories.contains) require(signatureCount >= threshold, "Not enough signatures") // Deploy for different teams val teamA = compile(MultiSig.validate(List(alice, bob, carol), BigInt(2))) val teamB = compile(MultiSig.validate(List(dave, eve), BigInt(2))) ``` ### Time-Locked Contracts Same vesting logic, different unlock dates: ```scala @Compile object Vesting: inline def validate(beneficiary: PubKeyHash, unlockTime: PosixTime)( datum: Option[Data], redeemer: Data, tx: TxInfo, outRef: TxOutRef ): Unit = require(tx.signatories.contains(beneficiary), "Not beneficiary") require(tx.validRange.isEntirelyAfter(unlockTime), "Still locked") // Different vesting schedules val q1Vesting = compile(Vesting.validate(employee, PosixTime(1711929600000L))) val q2Vesting = compile(Vesting.validate(employee, PosixTime(1719792000000L))) ``` ### Token Policies One minting logic, different configurations: ```scala @Compile object TokenPolicy: inline def validate(maxSupply: BigInt, admin: PubKeyHash)( redeemer: Data, policyId: PolicyId, tx: TxInfo ): Unit = require(tx.signatories.contains(admin), "Admin signature required") val minted = tx.mint.filter(_._1 == policyId).map(_._2).sum require(minted <= maxSupply, "Exceeds max supply") // Different token configurations val goldToken = compile(TokenPolicy.validate(BigInt(1000000), treasury)) val silverToken = compile(TokenPolicy.validate(BigInt(10000000), treasury)) ``` ## Key Points **Each parameter set = unique script hash.** Two validators with different parameters have different addresses, even if the logic is identical. - Parameters become constants in the compiled bytecode - No runtime overhead—values are inlined - Script hash changes when parameters change - Useful for deploying the same logic with different configs ## Verifying Parameters On-Chain When one script needs to verify another script's parameters (e.g., a minting policy ensuring tokens go to a correctly parameterized marketplace), use the [Parameter Validation Pattern](/docs/design-patterns/parameter-validation). ## Related - **[Parameter Validation Pattern](/docs/design-patterns/parameter-validation)** — Verify script parameters on-chain - **[Compiling Smart Contracts](/docs/smart-contracts/compiling)** — Compilation options and encoding - **[Validator Types](/docs/smart-contracts/validators)** — All six validator purposes --- Source: https://scalus.org/docs/smart-contracts/plutus-data --- # FromData and ToData On-chain, all data is stored in a universal format called `Data` (Plutus Core Data). Datums, redeemers, and script context are all encoded as `Data`. Scalus provides type-safe conversions to work with your Scala types instead of raw `Data`. ## Quick Usage ```scala // Convert Data to a typed value val owner = datum.to[PubKeyHash] // Convert a typed value to Data val data = myValue.toData ``` ## Using Data in Validators In validators, you receive `Data` and convert it to typed values: ```scala @Compile object MyValidator extends Validator: inline override def spend(datum: Option[Data], redeemer: Data, tx: TxInfo, outRef: TxOutRef): Unit = // Extract typed values from Data val owner = datum.getOrFail("Datum not found").to[PubKeyHash] val action = redeemer.to[MyRedeemer] // ... validation logic ``` Two equivalent ways to convert: ```scala // Using .to[T] extension method (preferred) val myDatum = datumData.to[MyDatumType] // Using fromData function import scalus.uplc.builtin.Data.fromData val myDatum = fromData[MyDatumType](datumData) ``` ## Deriving Instances Scalus provides type classes `FromData` and `ToData` to convert between user-defined types and `Data`. Derive instances automatically using Scala 3's `derives` syntax. One caveat - you must always supply a companion object with @Compile annotation on it even if it is empty. This is because Scalus needs to compile the companion object to include the `FromData` instance in the UPLC script. ```scala mdoc:compile-only import scalus.uplc.builtin.*, Builtins.*, Data.* case class Account(hash: ByteString, balance: BigInt) derives FromData, ToData @Compile object Account enum State derives FromData, ToData: case Empty case Active(account: Account) @Compile object State ``` Here's an example of converting standard and user-defined types to and from `Data`. ```scala mdoc:compile-only import scalus.uplc.builtin.*, Builtins.*, Data.* 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 typed Scala value. val data = iData(123) // fromData is a method that looks up the appropriate `FromData` instance for the type // 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 Account(args.head.to[ByteString], args.tail.head.to[BigInt]) } val account = data.to[Account] } // or you can use a macro to derive the FromData instance { given FromData[Account] = FromData.derived given FromData[State] = FromData.derived } } ``` You can derive `FromData` instances for your case classes and enums via standard Scala 3 `derives` syntax. --- Source: https://scalus.org/docs/smart-contracts/compiling --- # Compiling Smart Contracts ## Basic Compilation Use `PlutusV3.compile` to transform your Scala validator into a Plutus script: ```scala import scalus.compiler.Options import scalus.uplc.PlutusV3 // Compile your validator to Plutus V3 val compiled = PlutusV3.compile(MyValidator.validate) // Get the script for use in transactions val script = compiled.script val scriptHex = compiled.program.doubleCborHex ``` The compilation flow: `PlutusV3.compile` takes a validator entry point and produces a `PlutusV3[A]` object containing the script, its hash, and address. ## Choosing a Plutus Version Select the appropriate version for your use case: ```scala import scalus.uplc.{PlutusV1, PlutusV2, PlutusV3} // Plutus V3 (recommended for new validators) val compiledV3 = PlutusV3.compile(MyValidator.validate) // Plutus V2 (for reference inputs, inline datums) val compiledV2 = PlutusV2.compile(MyValidator.validate) // Plutus V1 (legacy) val compiledV1 = PlutusV1.compile(MyValidator.validate) ``` ## Compilation Options Control error traces and optimizations via `Options`: ```scala import scalus.compiler.Options // Development: Include error traces for debugging given Options = Options.debug // Production: Minimal script size given Options = Options.release ``` - `Options.debug`: Adds error location information (easier debugging, larger script) - `Options.release`: Minimal script size (for production) You can also enable error traces on a compiled validator: ```scala val compiled = PlutusV3.compile(MyValidator.validate) // Add error traces for debugging val withTraces = compiled.withErrorTraces ``` ## Complete Example ```scala import scalus.* import scalus.uplc.builtin.Data import scalus.compiler.Options import scalus.cardano.onchain.plutus.v3.* import scalus.uplc.PlutusV3 @Compile object MyValidator { inline def validate(scData: Data): Unit = { val ctx = scData.to[ScriptContext] ctx.scriptInfo match case ScriptInfo.SpendingScript(_, datum) => require(ctx.txInfo.signatories.nonEmpty, "No signatories") case _ => fail("Must be spending") } } object MyContract { private given Options = Options.release lazy val compiled = PlutusV3.compile(MyValidator.validate) } // Get hex for use in transactions val scriptHex = MyContract.compiled.program.doubleCborHex ``` ## Encoding Options UPLC programs can be encoded in several formats: ```scala val program = compiled.program // Flat encoding (binary format) val flatEncoded = program.flatEncoded // CBOR encoding (wraps Flat) val cborEncoded = program.cborEncoded // Double CBOR encoding (standard for Cardano transactions) val doubleCborEncoded = program.doubleCborEncoded // Hex string of double CBOR (most commonly used in APIs) val hexString = program.doubleCborHex ``` ## What's Next? Once your validator compiles, you're ready to publish and use it: - **[Blueprint Generation](/docs/dapp-development/sbt-plugin#blueprint-generation)** — Produce CIP-57 blueprints with the Scalus sbt plugin and verify script hashes - **[Deploying Contracts](/docs/dapp-development/sbt-plugin#deploying-contracts)** — Publish your compiled contract as a reference script UTxO on preview, preprod, or mainnet - **[Working with Contract](/docs/dapp-development/working-with-contract)** — Set compilation options, generate CIP-57 blueprints, and get script addresses - **[First Contract Transaction](/docs/transactions/first-contract-transaction)** — Lock funds at a script address and spend them with a redeemer - **[Building Transactions](/docs/transactions/building-first-transaction)** — Build and submit simple ADA transfers using TxBuilder - **[HTLC Tutorial](/docs/smart-contracts/htlc-tutorial)** — End-to-end example: contract, transactions, testing, and deployment --- Source: https://scalus.org/docs/smart-contracts/evaluating-script --- # Evaluating script Scalus provides a high-level API to evaluate UPLC scripts. ```scala mdoc compile(BigInt(2) + 2).toUplc().evaluateDebug.toString ``` 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. ```scala mdoc:compile-only import scalus.cardano.ledger.Language import scalus.cardano.onchain.plutus.* import scalus.uplc.*, eval.* def evaluation() = { import scalus.* import scalus.uplc.eval.PlutusVM val sir = compile { def usefulFunction(a: BigInt): BigInt = a + 1 usefulFunction(1) } val term = sir.toUplc() // setup a given PlutusVM for the PlutusV2 language and default parameters given v2VM: PlutusVM = PlutusVM.makePlutusV2VM() // simply evaluate the term with CEK machine term.evaluate.show // (con integer 2) // you can get the actual execution costs from protocol parameters JSON from cardano-cli lazy val machineParams = MachineParams.fromCardanoCliProtocolParamsJson( "JSON with protocol parameters", Language.PlutusV3 ) // or from blockfrost API lazy val machineParams2 = MachineParams.fromBlockfrostProtocolParamsJson( "JSON with protocol parameters", Language.PlutusV3 ) // use latest PlutusV3 VM with explicit machine parameters val v3vm: PlutusVM = PlutusVM.makePlutusV3VM(machineParams) // evaluate a Plutus V3 script considering CIP-117 // calculate the execution budget, all builtins costs, and collect logs val script = term.plutusV3 script.evaluateDebug(using v3vm) match case r @ Result.Success(evaled, budget, costs, logs) => println(r) case Result.Failure(exception, budget, costs, logs) => println(s"Exception: $exception, logs: $logs") // evaluate a flat encoded script and calculate the execution budget and logs // 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 try { v3vm.evaluateScript(script, tallyingBudgetSpender, logger) } 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")}" ) } ``` --- Source: https://scalus.org/docs/smart-contracts/in-depth-validator-look --- ## Overview A Scalus validator is an object that extends the `Validator` trait, annotated with `@Compile` to compile your Scala code to Plutus Core bytecode. When defining a contract, you can define logic for 6 distinct purposes by overriding a corresponding method. Each method corresponds to a respective Plutus Redeemer Tag type: - `spend` - `mint` - `reward` - `certify` - `vote` - `propose` A typical contract defines these functions, often just one. A successful contract returns `Unit`, while errors are indicated by throwing exceptions (we'll explore this in more detail below). ## Sample Validator Let's take a look at an example contract, `HTLCValidator`, to illustrate the concepts above. ```scala import scalus.Compile import scalus.uplc.builtin.Builtins.sha3_256 import scalus.uplc.builtin.Data.{FromData, ToData} import scalus.uplc.builtin.{ByteString, Data, FromData, ToData} import scalus.cardano.onchain.plutus.v3.* import scalus.cardano.onchain.plutus.prelude.* type Preimage = ByteString type Image = ByteString // Contract Datum case class Config( committer: PubKeyHash, receiver: PubKeyHash, image: Image, timeout: PosixTime ) derives FromData, ToData // Redeemer enum Action derives FromData, ToData: case Timeout case Reveal(preimage: Preimage) @Compile object HtlcValidator { inline def validate(scData: Data): Unit = { val ctx = scData.to[ScriptContext] ctx.scriptInfo match case ScriptInfo.SpendingScript(txOutRef, datum) => spend(datum, ctx.redeemer, ctx.txInfo, txOutRef) case _ => fail(MustBeSpending) } /** Spending script purpose validation */ inline def spend(datum: Option[Data], redeemer: Data, tx: TxInfo, ownRef: TxOutRef): Unit = { val config = datum.getOrFail(InvalidDatum).to[Config] redeemer.to[Action] match case Action.Timeout => val validFrom = tx.validRange.from.finite(0) require(config.timeout <= validFrom, InvalidCommitterTimePoint) require(tx.isSignedBy(config.committer), UnsignedCommitterTransaction) case Action.Reveal(preimage) => val validTo = tx.validRange.to.finiteOrFail(ValidRangeMustBeBound) require(validTo <= config.timeout, InvalidReceiverTimePoint) require(tx.isSignedBy(config.receiver), UnsignedReceiverTransaction) require(sha3_256(preimage) == config.image, InvalidReceiverPreimage) } // Error messages inline val MustBeSpending = "Must be a spending script" inline val InvalidDatum = "Invalid Datum" inline val ValidRangeMustBeBound = "ValidTo must be set" inline val UnsignedCommitterTransaction = "Must be signed by a committer" inline val UnsignedReceiverTransaction = "Must be signed by a receiver" inline val InvalidCommitterTimePoint = "Must be exclusively after timeout" inline val InvalidReceiverTimePoint = "Must be inclusively before timeout" inline val InvalidReceiverPreimage = "Invalid preimage" } ``` ### Import section and type definitions At the very top of the file, you can see imports, which are present in most Scala programs. Then, we declare *type aliases*, which enhance our types with semantics relevant to the contract. After that, we define the Datum and Redeemer types. `Derives` syntax allows us to convert the `Data` values from the script context into `Config` and `Action` types to utilize the Scala type system. ### Validator code In the body of the `object HtlcValidator`, where `object` is a Scala keyword that defines a singleton instance, we can see the `inline def spend` declaration, which contains the logic of the contract. ###### Decoding First, we turn the redeemer Data instance into the type that we're going to be working with: `Action`. This is available thanks to the `FromData` that we derived automatically. Scalus can automatically derive `FromData` instances for most types that you're going to use for Datums and Redeemers. The behavior of the validator then branches based on the redeemer type. To implement the branching, we use pattern matching. ###### Pattern matching Pattern matching is a Scala language feature that, for enumerable types with known variants, such as `Action`, allows to handle every possible option. In this case, it's `Timeout` and `Reveal`. ##### `require()` For each of the two redeemers, we define the requirements necessary for the `Spend` transaction to be successful. If any of the required conditions don't hold true, the validator execution ends with an error. The list of all errors is enumerated at the bottom of the contract, each containing a message describing why the spending was forbidden. > [!NOTE] > > `require` is an inline function, meaning that the compiler will not generate a method call and instead insert > the body of `require` directly into validator body. #### Summary Each validator is, in essence, a boolean function — it either allows performing the desired action (e.g., spending the script-locked funds) or forbids it. Thus, the logic is just a sequence of binary checks. In Scalus, this usually means a series of `require` calls, where the first parameter is the boolean invariant to check, and the second parameter is the error that is thrown if the condition is false. In the `HtlcValidator`, you can see this clearly: the timeout is only valid if the initiating transaction is signed by the correct key and the necessary amount of time has passed. This is neatly expressed in 2 lines of Scala code: ```scala require(config.timeout <= validFrom, InvalidCommitterTimePoint) require(tx.isSignedBy(config.committer), UnsignedCommitterTransaction) ``` --- Source: https://scalus.org/docs/smart-contracts --- # Cardano Smart Contract Development Write type-safe smart contracts for Cardano using Scala. Debug validators with breakpoints in your IDE, test with ScalaCheck, and compile to Plutus Core. ```scala @Compile object MyValidator extends Validator: inline override def spend( datum: Option[Data], redeemer: Data, tx: TxInfo, outRef: TxOutRef ): Unit = val owner = datum.getOrFail("No datum").to[PubKeyHash] require(tx.signatories.contains(owner), "Not signed by owner") ``` ## Tutorials - **[Build Your First Validator](/docs/smart-contracts/developing-smart-contracts)** — Create a spending validator, write tests, and debug step-by-step - **[HTLC Tutorial](/docs/smart-contracts/htlc-tutorial)** — Complete Hash Time-Locked Contract with tests and transactions - **[Compiling to Plutus](/docs/smart-contracts/compiling)** — Compile validators, configure options, and prepare for deployment ## Validators - **[Validator Types](/docs/smart-contracts/validators)** — Spending, minting, rewarding, and certifying validators - **[Parameterized Validators](/docs/smart-contracts/parameterized-validators)** — Reusable validators with compile-time configuration - **[Plutus Data](/docs/smart-contracts/plutus-data)** — Type-safe conversion between Scala and Plutus Data ## Testing & Debugging - **[Unit Testing](/docs/testing/unit-testing)** — ScalusTest trait and property-based testing with ScalaCheck - **[Debugging](/docs/testing/debugging)** — Breakpoints, logging, and IDE debugger integration ## Production - **[Security](/docs/security)** — Common vulnerabilities and how to avoid them - **[Optimisations](/docs/smart-contract-optimisations)** — Reduce script size and execution costs - **[Design Patterns](/docs/design-patterns)** — Patterns for multi-input validators ## Deploying - **[Blueprint Generation](/docs/dapp-development/sbt-plugin#blueprint-generation)** — Generate CIP-57 blueprints with the sbt plugin and verify on-chain script hashes - **[Deploying Contracts](/docs/dapp-development/sbt-plugin#deploying-contracts)** — Publish compiled contracts as reference script UTxOs via Blockfrost - **[Working with Contract](/docs/dapp-development/working-with-contract)** — Compilation options, blueprints, and script addresses - **[First Contract Transaction](/docs/transactions/first-contract-transaction)** — Lock funds and spend from a script address with TxBuilder - **[Building Transactions](/docs/transactions/building-first-transaction)** — Build and submit simple ADA transfers - **[DApp Development](/docs/dapp-development)** — Full application development --- Source: https://scalus.org/docs/smart-contract-optimisations/measuring-performance --- # Measuring Performance Always measure before and after optimization. ## Measuring script execution directly For quick comparisons of script variants, you can evaluate the UPLC directly and get CPU steps, memory, and flat-encoded size: ```scala mdoc:compile-only copy showLineNumbers import scalus.*, scalus.compiler.{compile, Options} import scalus.uplc.*, scalus.uplc.eval.* import scalus.cardano.ledger.{ExUnitPrices, ExUnits, NonNegativeInterval} given Options = Options.release given PlutusVM = PlutusVM.makePlutusV3VM() val sir = compile { /* your validator */ ??? } val program = sir.toUplcOptimized().plutusV3 // Apply arguments and evaluate val applied = program // $ datum $ redeemer $ ctxData val result = applied.deBruijnedProgram.evaluateDebug result match case Result.Success(_, budget, _, _) => val flatSize = applied.flatEncoded.length println(s"Flat size: $flatSize bytes") println(s"CPU steps: ${budget.steps}") println(s"Memory: ${budget.memory}") // Compute execution fee with mainnet prices val exPrices = ExUnitPrices( priceMemory = NonNegativeInterval(0.0577, precision = 15), priceSteps = NonNegativeInterval(0.0000721, precision = 15) ) val execFee = ExUnits(budget.memory, budget.steps).fee(exPrices) println(s"Exec fee: ${execFee.value} lovelace") // Approximate total: exec fee + size fee (44 lovelace/byte) println(s"Size fee: ${flatSize * 44} lovelace") case Result.Failure(err, _, _, logs) => println(s"Failed: ${err.getMessage}") ``` However, measuring script execution alone can be misleading. The actual transaction fee includes a size component (44 lovelace per byte of transaction), so a script that saves CPU but grows in size may cost more overall. If you have a working smart contract, the most accurate way to measure is to build a complete transaction via the Emulator and check the final fee. ## Measuring via the Emulator Build a real transaction with `TxBuilder` and the `Emulator`, then inspect the fee and execution units: ```scala mdoc:compile-only copy showLineNumbers import scalus.cardano.ledger.* import scalus.cardano.node.Emulator import scalus.cardano.txbuilder.TxBuilder import scalus.uplc.PlutusV3 import scalus.compiler.Options import scalus.utils.await given CardanoInfo = CardanoInfo.mainnet given Options = Options.release val compiled = PlutusV3.compile(MyValidator.validate) val emulator = Emulator(initialUtxos) // Build and complete the transaction val tx = TxBuilder(summon[CardanoInfo]) .spend(scriptUtxo, redeemer, compiled) .payTo(recipientAddress, outputValue) .complete(emulator, changeAddress) .await() .sign(signer) .transaction // The transaction fee includes both size and execution costs val fee: Coin = tx.body.value.fee println(s"Transaction fee: ${fee.value} lovelace") // Inspect execution units per redeemer val redeemers = tx.witnessSet.redeemers.toSeq.flatMap(_.value.toSeq) redeemers.foreach { r => println(s" ${r.tag}: CPU=${r.exUnits.steps}, mem=${r.exUnits.memory}") } ``` See the [Emulator](/docs/testing/emulator) page for full setup details. ## What's Next? - **[Algorithmic Optimisations](/docs/smart-contract-optimisations/algorithmic-optimisations)** — start here for the biggest wins; design patterns that reduce on-chain work asymptotically. - **[Scala Metaprogramming](/docs/smart-contract-optimisations/scala-metaprogramming)** — once you've measured a hot path, use compile-time evaluation to eliminate it. --- Source: https://scalus.org/docs/smart-contract-optimisations/algorithmic-optimisations --- # Algorithmic Optimisations Before diving into compiler-level techniques, apply algorithmic optimizations. These give the biggest wins and are backend-agnostic. Scalus provides a library of [Design Patterns](/docs/design-patterns) for common cases: - **[Withdraw Zero](/docs/design-patterns/withdraw-zero)** -- run heavy logic once via a stake validator instead of per-UTxO (O(N) instead of O(N²)) - **[UTxO Indexer](/docs/design-patterns/utxo-indexer)** -- pass indexes in the redeemer for O(1) lookup instead of linear search - **[Transaction Level Minting](/docs/design-patterns/transaction-level-minting)** -- batch minting validation into a single check - **[Merkelized Validator](/docs/design-patterns/merkelized-validator)** -- split large validators into smaller pieces, include only the branch you need See the full `OptimizedPaymentSplitterValidator` example in `scalus-examples/.../paymentsplitter/` for a real-world application of the withdraw-zero pattern. ## What's Next? - **[Design Patterns](/docs/design-patterns)** — full reference for every pattern listed above, with implementations and trade-offs. - **[Scala Metaprogramming](/docs/smart-contract-optimisations/scala-metaprogramming)** — once the algorithm is right, eliminate per-call overhead with `inline` and compile-time evaluation. - **[Measuring Performance](/docs/smart-contract-optimisations/measuring-performance)** — quantify the impact of each pattern on real transaction fees. --- Source: https://scalus.org/docs/smart-contract-optimisations/scala-metaprogramming --- # Scala Metaprogramming Scala 3 `inline` keyword is a powerful tool for on-chain code optimization. The Scala compiler evaluates `inline` expressions at compile time, before the Scalus compiler plugin sees the code. This lets you control exactly what gets compiled to UPLC. ## Inlining The `inline` keyword tells the Scala compiler to substitute the expression at its usage site. This eliminates a `let` binding in UPLC, saving CPU on the lambda application. But inlining is a trade-off -- it can help or hurt depending on how many times the expression is used. ## When inlining helps: single use site In a recursive hash chain, the combiner function has one call site per recursion step. Inlining eliminates the lambda overhead without duplicating code: ```scala mdoc:compile-only copy showLineNumbers import scalus.*, scalus.compiler.compile import scalus.uplc.builtin.Builtins.* import scalus.uplc.builtin.{BuiltinList, ByteString, Data} @Compile object Checksum { // inline: body substituted at the single call site in checksum inline def combine(acc: ByteString, elem: ByteString): ByteString = blake2b_256(appendByteString(acc, elem)) def checksum(list: BuiltinList[Data], acc: ByteString): ByteString = if list.isEmpty then acc else checksum(list.tail, combine(acc, list.head.toByteString)) } ``` Here `combine` appears once in the recursive body -- `inline` saves CPU with no size penalty. The optimizer may also inline single-use functions automatically, but marking them `inline` guarantees it. ## When inlining hurts If the same function is called from many places, `inline` duplicates the body at every call site. The script gets bigger: ```scala mdoc:compile-only copy showLineNumbers import scalus.*, scalus.compiler.compile import scalus.uplc.builtin.Builtins.* import scalus.uplc.builtin.{ByteString, Data} @Compile object FieldValidator { // inline: body duplicated 3 times → bigger script inline def checkNonEmpty(bs: ByteString): Unit = lengthOfByteString(bs) > BigInt(0) || (throw new RuntimeException("empty")) def validate(datum: Data, redeemer: Data, ctx: Data): Unit = { val fields = datum.toConstr.snd checkNonEmpty(fields.head.toByteString) // body copied here checkNonEmpty(fields.tail.head.toByteString) // body copied here checkNonEmpty(fields.tail.tail.head.toByteString) // body copied here } } ``` Compare with a regular `def` (single lambda, called 3 times): | | `inline def` | `def` | |---|---|---| | Flat size | 126 B | 103 B | | CPU steps | 2,815,862 | 3,007,862 | | Memory | 10,888 | 12,088 | | Execution fee | 832 lovelace | 915 lovelace | | Script size fee (44 lovelace/byte) | 5,544 lovelace | 4,532 lovelace | | **Total transaction fee** | **6,376 lovelace** | **5,447 lovelace** | `inline` wins on execution (-7% CPU) but the 23 extra bytes of script size cost 1,012 lovelace more. The total transaction fee is **17% higher** with `inline` -- the size component dominates. Execution fee is only part of the transaction fee. Cardano charges 44 lovelace per byte of transaction size (`txFeePerByte`), and the flat-encoded script is part of the transaction. Inlining that saves a few thousand CPU steps but adds bytes to the script can easily cost more overall. Always measure the full transaction fee, not just execution. ## Loop Unrolling When the number of iterations is known at compile time, you can use `inline` to unroll a loop. The Scala compiler expands the recursive `inline` calls, producing straight-line code with no lambda overhead per iteration: ```scala mdoc:compile-only copy showLineNumbers import scalus.*, scalus.compiler.compile import scalus.uplc.builtin.Builtins.* import scalus.uplc.builtin.{BuiltinList, Data} @Compile object Unrolled { // The Scala compiler unrolls this at compile time: // checkN(3, sigs, expected) becomes: // sigs.head == expected.head; checkN(2, sigs.tail, expected.tail) // sigs.head == expected.head; checkN(1, sigs.tail, expected.tail) // sigs.head == expected.head; checkN(0, sigs.tail, expected.tail) // () inline def checkN(inline n: Int, sigs: BuiltinList[Data], expected: BuiltinList[Data]): Unit = inline if n <= 0 then () else { sigs.head == expected.head || (throw new RuntimeException("missing sig")) checkN(n - 1, sigs.tail, expected.tail) } def validator(datum: Data, redeemer: Data, ctx: Data): Unit = { val sigs = ctx.toConstr.snd.head.toList val expected = datum.toList checkN(3, sigs, expected) // unrolled to 3 direct checks at compile time } } ``` The Scala compiler resolves `inline if n <= 0` at compile time, recursively expanding `checkN` until `n` reaches 0. The Scalus compiler plugin only sees the final straight-line code — no recursion, no lambda calls per iteration. | | Unrolled | Recursive | |---|---|---| | Flat size | 137 B | 116 B | | CPU steps | 6,212,620 | 8,062,548 | | Fee | 1,144 lovelace | 1,735 lovelace | Unrolling saves **23% CPU** and **34% fee** for 3 iterations, at the cost of 21 extra bytes of script size. ## Inlining Constants When you mark a parameter as `inline`, its value is directly embedded in the compiled code. This is particularly useful for configuration like public key hashes: ```scala mdoc:compile-only copy showLineNumbers import scalus.*, scalus.compiler.*, uplc.builtin.{Data, Builtins, ByteString}, Builtins.*, ByteString.* inline def validator(inline pubKeyHash: ByteString)(datum: Data, redeemer: Data, ctxData: Data) = verifyEd25519Signature(pubKeyHash, datum.toByteString, redeemer.toByteString) val script = compile: validator(hex"deadbeef") ``` This generates SIR with the constant `#deadbeef` directly embedded -- no runtime parameter passing: ```ocaml {λ datum redeemer ctxData -> verifyEd25519Signature(#deadbeef, unBData(datum), unBData(redeemer)) } ``` ## Compile-Time Evaluation When a closed function (no free variables) is applied to constants inside `compile`, the entire computation is evaluated at compile time by the optimizer's partial evaluator: ```scala mdoc:compile-only copy showLineNumbers {5-9,12} import scalus.*, scalus.compiler.* import scala.annotation.tailrec @Compile object Fibonacci { def fib(n: BigInt): BigInt = @tailrec def f(n: BigInt, x: BigInt, y: BigInt): BigInt = if n > 1 then f(n - 1, y, x + y) else y f(n, 0, 1) } // fib(100) is fully evaluated at compile time! val sir = compile(Fibonacci.fib(100)) val uplc = sir.toUplcOptimized() // uplc is now just: (Const Integer 354224848179261915075) ``` The recursive loop runs during compilation. The final script contains only the constant. This works for any closed function applied to constant arguments -- precomputed lookup tables, derived configuration, hash preimages, mathematical constants. Constant folding is also implemented in the [UPLC Optimiser Pipeline](/docs/smart-contract-optimisations/uplc-optimiser-pipeline), which can fold closed subexpressions at the UPLC level. ## Conditional Code Generation Using `inline if` with a compile-time parameter, you can generate different code at compile time. This is useful for creating separate debug and production versions of your validators: ```scala mdoc:compile-only copy showLineNumbers import scalus.*, scalus.compiler.*, uplc.builtin.Data, uplc.builtin.Builtins inline def dbg[A](msg: String)(a: A)(using debug: Boolean): A = inline if debug then Builtins.trace(msg)(a) else a inline def validator(using debug: Boolean)(datum: Data, redeemer: Data, ctxData: Data) = dbg("datum")(datum) val releaseScript = compile(validator(using false)) // {λ datum redeemer ctxData -> datum } val debugScript = compile(validator(using true)) // {λ datum redeemer ctxData -> trace("datum", datum) } ``` The `releaseScript` contains no `trace` calls at all -- the conditional code is evaluated during Scala compilation, so there's zero runtime overhead for disabled features. ## What's Next? - **[Low-Level Builtins](/docs/smart-contract-optimisations/low-level-builtins)** — once the high-level structure is tight, drop to UPLC builtins and raw Plutus Data for further savings. - **[UPLC Optimiser Pipeline](/docs/smart-contract-optimisations/uplc-optimiser-pipeline)** — see exactly which optimiser passes pick up the patterns produced by `inline`. --- Source: https://scalus.org/docs/smart-contract-optimisations/low-level-builtins --- # Low-Level Builtins Instead of using high-level prelude types like `ScriptContext` and `List[A]`, you can use builtins and primitive data types (`Data`, `BuiltinList`, `ByteString`) directly inside `compile {}`. This gives you manual control over Data access and makes the resulting Scala code effectively a "compiled assembler" -- it maps almost 1:1 to UPLC builtins. Let's see the difference on a real validator. Here's a high-level `PreimageValidator` that checks a hash preimage and verifies a signatory: ```scala mdoc:compile-only copy showLineNumbers import scalus.*, scalus.compiler.compile import scalus.uplc.builtin.{ByteString, Data} import scalus.uplc.builtin.Builtins.* import scalus.cardano.onchain.plutus.v2.* import scalus.cardano.onchain.plutus.prelude.* @Compile object PreimageValidator { def preimageValidator(datum: Data, redeemer: Data, ctxData: Data): Unit = { val (hash, pkh) = datum.to[(ByteString, ByteString)] val preimage = redeemer.toByteString val ctx = ctxData.to[ScriptContext] ctx.txInfo.signatories.find(_.hash == pkh).orFail("Not signed") require(sha2_256(preimage) == hash, "Wrong preimage") } } ``` Where does the cost come from? - **V3 Lowering backend**: types stay as `Data` internally, so `.to[ScriptContext]` is cheap. The cost is in navigating nested `Data` constructors to reach the `signatories` field -- chained `tailList` calls through 8 unused fields. - **Template-based backends**: `.to[ScriptContext]` Scott-encodes or reconstructs the entire structure including all unused fields -- significantly more expensive. Now the same logic using direct builtins: ```scala mdoc:compile-only copy showLineNumbers import scalus.*, scalus.compiler.compile import scalus.uplc.builtin.Builtins.* import scalus.uplc.builtin.{BuiltinList, ByteString, Data} import scalus.cardano.onchain.plutus.v2.ScriptContext import scalus.cardano.onchain.plutus.prelude.require @Compile object OptimizedPreimageValidator { def preimageValidator(datum: Data, redeemer: Data, ctxData: Data): Unit = { // Manual Data deconstruction instead of .to[T] val pair = datum.toConstr.snd inline def hash = pair.head.toByteString val pkh = pair.tail.head // kept as Data for equalsData inline def preimage = redeemer.toByteString // Walk signatories with BuiltinList[Data] def checkSignatories(sigs: BuiltinList[Data]): Unit = if sigs.head == pkh then () else checkSignatories(sigs.tail) // Direct field access -- skip unused TxInfo fields inline def sigs = ctxData.field[ScriptContext](_.txInfo.signatories).toList checkSignatories(sigs) require(sha2_256(preimage) == hash) } } ``` ## Direct Script Context Access In this example we can see the use of `datum.toConstr.snd` -- deconstructing `Data` manually instead of `.to[T]`. Since we know the Data representation (a `Constr` with fields as a list), we can use builtins directly: `unConstrData` returns a pair of (constructor tag, fields list), and `.snd` gives us the fields. Scalus provides two macros for type-safe field access without hardcoding offsets: **`fieldAsData` macro** (and its shorthand `.field`) -- generates a chain of `headList`/`tailList` calls to extract exactly the field you need. Works with any protocol version: ```scala mdoc:compile-only copy showLineNumbers import scalus.compiler.fieldAsData import scalus.uplc.builtin.Data // Both forms are equivalent: val signatories = fieldAsData[ScriptContext](_.txInfo.signatories)(ctxData) val signatories2 = ctxData.field[ScriptContext](_.txInfo.signatories) ``` **`offsetOf` macro** (PV11+) -- returns the 0-based field index as `BigInt` at compile time. Use it with the `dropList` builtin to skip N fields in one call instead of chaining `tailList`: ```scala mdoc:compile-only copy showLineNumbers import scalus.compiler.offsetOf import scalus.uplc.builtin.Builtins.* val txInfoFields = ctxData.toConstr.snd.head.toConstr.snd // offsetOf[TxInfo](_.signatories) expands to BigInt(8) at compile time val sigs = dropList(offsetOf[TxInfo](_.signatories), txInfoFields).head.toList ``` Using direct builtins you also have control over Plutus version details -- like using `dropList` (PV11) instead of chained `tailList`, or `Case` on `BuiltinList` instead of `chooseList`. Here's what the same validator looks like with PV11 features: ```scala mdoc:compile-only copy showLineNumbers import scalus.*, scalus.compiler.{compile, offsetOf} import scalus.uplc.builtin.Builtins.* import scalus.uplc.builtin.{BuiltinList, Data} import scalus.cardano.onchain.plutus.v2.TxInfo import scalus.cardano.onchain.plutus.prelude.require @Compile object OptimizedPreimageValidatorV4 { def preimageValidator(datum: Data, redeemer: Data, ctxData: Data): Unit = { val pair = datum.toConstr.snd inline def hash = pair.head.toByteString val pkh = pair.tail.head inline def preimage = redeemer.toByteString // Pattern match on BuiltinList -- compiles to Case on List (PV11) def checkSignatories(sigs: BuiltinList[Data]): Unit = (sigs: @unchecked) match case BuiltinList.Cons(h, t) => if h == pkh then () else checkSignatories(t) // dropList + offsetOf: skip directly to signatories field val txInfoFields = ctxData.toConstr.snd.head.toConstr.snd inline def sigs = dropList(offsetOf[TxInfo](_.signatories), txInfoFields).head.toList checkSignatories(sigs) require(sha2_256(preimage) == hash) } } ``` `@unchecked` on the `BuiltinList` match omits the `Nil` branch. If the list is empty, the VM throws `CaseListBranchError`. Use this when you know the list is non-empty or when failure on empty is acceptable. ## Budget Comparison Here's the same PreimageValidator across styles and protocol versions (1 signatory): | Variant | Flat Size | CPU | Memory | Exec Fee | Tx Fee (44 lovelace/byte) | |---------|-----------|-----|--------|----------|---------------------------| | High-level, PV9 | 496 B | 6,594,493 | 21,962 | 1,743 | 23,567 lovelace | | High-level, PV11 | 448 B | 4,821,782 | 17,410 | 1,353 | 21,065 lovelace | | Direct builtins, PV9 | 426 B | 5,529,726 | 13,908 | 1,202 | 19,946 lovelace | | Direct builtins, PV11 | 374 B | 4,161,695 | 9,990 | 877 | 17,333 lovelace | Key observations: - **PV11 with zero code changes** (high-level PV9 vs PV11): 27% CPU savings -- this is the default `targetProtocolVersion` since the van Rossem hard fork (use `Options.plomin` to target PV10) - **Direct builtins** (PV11 builtins vs PV11 high-level): further 14% CPU, 43% memory savings from manual Data access - **Transaction fee** difference is smaller than execution fee difference because the size component (44 lovelace/byte) dominates. Still, direct builtins on PV11 save **26%** on total transaction fee compared to high-level PV9. The `PreimageBudgetComparisonTest` includes additional variants -- signatory index lookup (O(1) instead of linear search) and direct UPLC construction. Run it yourself: ```bash sbtn "scalusExamplesJVM/testOnly scalus.examples.PreimageBudgetComparisonTest" ``` ## What's Next? - **[Lowering Backends](/docs/smart-contract-optimisations/lowering-backends)** — control how your remaining high-level types are encoded in UPLC with `@UplcRepr`. - **[UPLC Term DSL](/docs/smart-contract-optimisations/uplc-term-dsl)** — go one level deeper and hand-craft the UPLC AST directly when builtins still aren't enough. --- Source: https://scalus.org/docs/smart-contract-optimisations/lowering-backends --- # Lowering Backends The Scalus compiler ships with three backends for lowering SIR (Scalus Intermediate Representation) to UPLC. Each one makes a different trade-off between code size, execution cost, and Plutus protocol-version compatibility. The default is **V3 Lowering**; the other two exist mostly for legacy code and for cases where a different on-chain representation is preferred. ## Backend Comparison | | **V3 Lowering** (default) | **Scott Encoding** | **Sum of Products** | |---|---|---|---| | Enum value | `SirToUplcV3Lowering` | `ScottEncodingLowering` | `SumOfProductsLowering` | | Architecture | Sea-of-nodes, data-flow based | Template-based | Template-based | | Data handling | Types stay as `Data` internally (`toData` is a NOOP in most cases) | Scott-encodes constructors as lambdas | Uses PlutusV3 `Constr`/`Case` nodes | | Protocol version | PlutusV3+ (Chang) | PlutusV1, V2, V3 | PlutusV3+ | | Best for | Most validators (default choice) | Legacy code, PlutusV1/V2 scripts | When `Constr`/`Case` encoding is preferred over `Data` | ## Selecting a Backend Backend selection happens through the `Options` given: ```scala mdoc:compile-only copy showLineNumbers import scalus.compiler.Options import scalus.compiler.sir.TargetLoweringBackend import scalus.cardano.ledger.MajorProtocolVersion given Options = Options( targetLoweringBackend = TargetLoweringBackend.SirToUplcV3Lowering, targetProtocolVersion = MajorProtocolVersion.vanRossemPV, generateErrorTraces = true, optimizeUplc = true ) ``` The `Options.default`, `Options.debug`, `Options.release`, and `Options.plomin` presets all use V3 Lowering. To switch to a different backend, build `Options` directly with the `targetLoweringBackend` you want. ## V3 Lowering Backend (Default) The V3 Lowering Backend uses a sea-of-nodes, data-flow-based architecture. Its defining property is that **most types stay as `Data` internally** -- so `toData` and `fromData` are NOOPs in most cases, and the code never spends CPU on round-tripping between Scala types and Plutus Data. By default, the backend infers the UPLC representation from the type structure: case classes become `Constr(tag, fields)`, enums use different constructor tags per variant, and so on. Use the `@UplcRepr` annotation to override the default representation when it isn't optimal for your use case. This backend produces the smallest, fastest scripts for most validators, especially anything that touches the Cardano `ScriptContext` (which is already supplied as `Data` by the ledger). ## Scott Encoding Backend The Scott Encoding Backend is a **template-based** backend that encodes algebraic data types using Scott encoding -- each constructor becomes a lambda that takes one continuation per branch. A pattern match becomes an application of those continuations. Unlike V3 Lowering, Scott Encoding does not keep types as `Data` internally. Conversions in and out of `Data` (via `toData`/`fromData`) carry real cost because the entire structure must be reconstructed. When to use it: - **Legacy validators on PlutusV1 or V2.** These protocol versions don't support the `Constr`/`Case` UPLC nodes that the V3 backend relies on. - **Maintaining existing scripts** whose hash must be preserved on-chain. - **Code that doesn't go through `Data`** -- pure computational helpers where the Scott form happens to be smaller than the Constr form. ```scala mdoc:compile-only copy showLineNumbers import scalus.compiler.Options import scalus.compiler.sir.TargetLoweringBackend import scalus.cardano.ledger.MajorProtocolVersion // Targeting PlutusV2 with Scott encoding given Options = Options( targetLoweringBackend = TargetLoweringBackend.ScottEncodingLowering, targetProtocolVersion = MajorProtocolVersion.vasilPV ) ``` ## Sum of Products Backend The Sum of Products Backend is the other **template-based** backend. It encodes ADTs using PlutusV3's native `Constr` (constructor application) and `Case` (pattern dispatch) UPLC nodes -- the same primitives the on-chain ledger uses for `Data`. Like Scott Encoding, it doesn't keep types as `Data` internally, so `toData`/`fromData` conversions cost CPU. Unlike Scott Encoding, the resulting UPLC tends to be more compact because `Constr`/`Case` are denser than chained lambdas. When to use it: - You want the structural `Constr`/`Case` representation for ADTs but don't want the data-flow rewriting that V3 Lowering applies. - You're targeting PlutusV3 and have a measurable workload where Sum of Products produces smaller scripts than V3 Lowering after optimisation. (Always [measure](/docs/smart-contract-optimisations/measuring-performance) -- this is the exception, not the rule.) ```scala mdoc:compile-only copy showLineNumbers given Options = Options( targetLoweringBackend = TargetLoweringBackend.SumOfProductsLowering, targetProtocolVersion = MajorProtocolVersion.vanRossemPV ) ``` ## Controlling Type Representation with `@UplcRepr` Independent of which backend is chosen, the `@UplcRepr` annotation overrides how a particular type is encoded at the UPLC level: | `UplcRepresentation` | Effect | |---|---| | `ProductCase` | Multi-field case class as `Constr(tag, [field1, field2, ...])` (default for case classes) | | `SumCase` | Enum variants with different constructor tags (default for enums) | | `ProductCaseOneElement` | Single-field wrapper unwrapped to just the inner type (eliminates `Constr` overhead) | | `Map` | Key-value pairs as a Plutus map | | `PackedDataMap` | Compact packed-data map representation | | `Data` | Keep as raw `Data`, no structural encoding | | `BuiltinArray` | Array-like structure | ```scala mdoc:compile-only copy showLineNumbers import scalus.compiler.annotations.{UplcRepr, UplcRepresentation} import scalus.uplc.builtin.ByteString // Single-field wrapper: unwrapped to just ByteString (no Constr overhead) @UplcRepr(UplcRepresentation.ProductCaseOneElement) case class PubKeyHash(hash: ByteString) // Map representation instead of List of pairs @UplcRepr(UplcRepresentation.PackedDataMap) case class AssocMap[K, V](inner: scalus.cardano.onchain.plutus.prelude.List[(K, V)]) ``` `@UplcRepr` is most useful with the template-based backends (Scott Encoding and Sum of Products), where structural decisions affect every conversion. With V3 Lowering, types already stay as `Data` internally, so `@UplcRepr(UplcRepresentation.Data)` is the de-facto default. On the template-based backends, applying `@UplcRepr(UplcRepresentation.Data)` to types that don't need structural access avoids `toData`/`fromData` overhead. ## Choosing a Backend in Practice 1. **Start with V3 Lowering.** The default works for the vast majority of validators on PlutusV3. 2. **Drop to Scott Encoding** only when you need PlutusV1/V2 compatibility or are maintaining legacy scripts whose hash must not change. 3. **Try Sum of Products** only after measuring: if your workload spends a lot of CPU on `Constr`/`Case` operations and V3 Lowering's data-flow rewriting isn't paying off, the template backend may produce a smaller script. 4. **Apply `@UplcRepr`** per-type once you've chosen a backend, especially on the template-based backends. ## What's Next? - **[UPLC Term DSL](/docs/smart-contract-optimisations/uplc-term-dsl)** — drop below the lowering backend entirely and hand-craft UPLC validators with full AST control. - **[UPLC Optimiser Pipeline](/docs/smart-contract-optimisations/uplc-optimiser-pipeline)** — the optimiser passes that run on whatever the backend produces. - **[Measuring Performance](/docs/smart-contract-optimisations/measuring-performance)** — measure the impact of switching backends on real transactions. --- Source: https://scalus.org/docs/smart-contract-optimisations/uplc-term-dsl --- # UPLC Term DSL For maximum control over your on-chain code, Scalus lets you build UPLC terms directly using a Scala-based DSL. This is analogous to [Plutarch](https://github.com/Plutonomicon/plutarch-plutus) in the Haskell ecosystem -- you construct the UPLC AST by hand, with full control over every lambda, application, and builtin call. ## When to Use the Term DSL - You've exhausted high-level and compiled-assembler optimizations and need to squeeze out more - You want to apply specific UPLC patterns that the compiler doesn't generate - You're building reusable UPLC combinators - You want to combine hand-crafted UPLC fragments with compiler-generated code ## DSL Cheat Sheet ```scala import scalus.uplc.Term, scalus.uplc.Term.{asTerm, λ, lam, vr} import scalus.uplc.TermDSL.given import scalus.uplc.DefaultFun.* ``` | Scala DSL | UPLC | Description | |-----------|------|-------------| | `f $ x` | `Apply(f, x)` | Function application | | `!t` | `Force(t)` | Force a delayed term | | `~t` | `Delay(t)` | Delay evaluation | | `λ("x")(body)` | `LamAbs("x", body)` | Lambda with named parameter | | `λ { x => body }` | `LamAbs("x", body)` | Lambda macro (extracts param name) | | `lam("x", "y")(body)` | `LamAbs("x", LamAbs("y", body))` | Multi-parameter lambda | | `vr"x"` | `Var(NamedDeBruijn("x"))` | Variable reference | | `42.asTerm` | `Const(Integer(42))` | Lift Scala value to Term | | `true.asTerm` | `Const(Bool(True))` | Lift boolean | | `AddInteger` | `Builtin(AddInteger)` | Builtin (implicit conversion) | | `Term.Case(arg, branches)` | `Case(arg, [...])` | V4 Case dispatch | | `Term.Const(Constant.Unit)` | `Const(Unit)` | Unit constant | | `Term.Error()` | `Error` | Abort execution | All 100+ Plutus builtins from `scalus.uplc.DefaultFun` are available via implicit conversion -- just import `TermDSL.given` and write the builtin name directly. ## Example: Factorial A simple example to show the DSL mechanics -- computing factorial using a fixpoint combinator: ```scala mdoc:compile-only copy showLineNumbers import scalus.uplc.Term, scalus.uplc.Term.{asTerm, λ, vr} import scalus.uplc.TermDSL.given import scalus.uplc.DefaultFun.* // Y-combinator (strict fixpoint) def pfix(f: Term => Term): Term = λ { r => r $ r } $ λ { r => f(r $ r) } val factorial = pfix { recur => λ { n => // Force(Force(IfThenElse) $ condition $ ~thenBranch $ ~elseBranch) !(!IfThenElse $ (LessThanEqualsInteger $ n $ 0.asTerm) $ ~(1.asTerm) $ ~(MultiplyInteger $ n $ (recur $ (SubtractInteger $ n $ 1.asTerm)))) } } // Wrap as a Plutus program and evaluate import scalus.uplc.eval.PlutusVM given PlutusVM = PlutusVM.makePlutusV3VM() val result = (factorial $ 10.asTerm).plutusV3.deBruijnedProgram.evaluateDebug // Result: 3628800 ``` Note how `IfThenElse` is a polymorphic builtin that needs two `Force` applications (`!!`), and both branches must be `Delay`ed (`~`) to prevent eager evaluation. ## Example: PreimageValidator in Term DSL Here's the same PreimageValidator from the [Low-Level Builtins](/docs/smart-contract-optimisations/low-level-builtins) chapter, built entirely with the Term DSL. This targets PlutusV4 and uses `Case` on List and `Case` on Bool for maximum efficiency: ```scala mdoc:compile-only copy showLineNumbers import scalus.uplc.Term, scalus.uplc.Term.{asTerm, λ} import scalus.uplc.TermDSL.given import scalus.uplc.DefaultFun.* import scalus.uplc.Constant as C // Strict fixpoint combinator def pfix(f: Term => Term): Term = λ { r => r $ r } $ λ { r => f(r $ r) } val preimageValidator: Term = λ { datum => λ { redeemer => λ { ctxData => // let pair = snd(unConstrData(datum)) (λ { pair => // let pkh = head(tail(pair)) (λ { pkh => // Extract signatories: // snd(unConstrData(head(snd(unConstrData(ctxData))))) // -> txInfoFields, then dropList(8, ...) for signatories val txInfoFields = !(!SndPair) $ (UnConstrData $ (!HeadList $ (!(!SndPair) $ (UnConstrData $ ctxData)))) val sigs = UnListData $ (!HeadList $ (!DropList $ BigInt(8).asTerm $ txInfoFields)) // Recursive signatory check using Case on List (Cons-only) val checkSigs = pfix { recur => λ { s => Term.Case( s, scala.List( // Cons(h, t) branch λ { h => λ { t => // Case on Bool: equalsData(h, pkh) Term.Case( EqualsData $ h $ pkh, scala.List( recur $ t, // False(0): keep looking Term.Const(C.Unit) // True(1): found ) ) } } ) ) } } // Check signatories, then verify hash (λ { _ => Term.Case( EqualsByteString $ (Sha2_256 $ (UnBData $ redeemer)) $ (UnBData $ (!HeadList $ pair)), scala.List( Term.Error(), // False(0): wrong preimage Term.Const(C.Unit) // True(1): success ) ) }) $ (checkSigs $ sigs) }) $ (!HeadList $ (!TailList $ pair)) // pkh = head(tail(pair)) }) $ (!(!SndPair) $ (UnConstrData $ datum)) // pair = snd(unConstrData(datum)) } } } ``` Key patterns: - **Let-binding via lambda**: `(λ { x => body }) $ value` simulates `let x = value in body` - **`Term.Case` on Bool**: branches are `scala.List(falseBranch, trueBranch)` (False = constructor 0, True = constructor 1) - **`Term.Case` on List**: Cons-only branch means the VM errors on empty list - **`!(!SndPair)`**: `SndPair` is polymorphic, needs two `Force` applications - **No `Delay`** needed with `Case` on Bool (unlike `IfThenElse` which evaluates eagerly) ## Wrapping as a Plutus Script Once you have a `Term`, wrap it as a versioned Plutus program: ```scala mdoc:compile-only copy showLineNumbers import scalus.uplc.Term val validator: Term = ??? // your hand-crafted term // Wrap as PlutusV3 (version 1.1.0) val program = validator.plutusV3 // Flat-encode for deployment val flatBytes = program.flatEncoded val cborHex = program.doubleCborHex ``` ## Applying UPLC Optimizer Passes Hand-crafted UPLC can benefit from the same optimizer passes that the compiler uses. For example, `CaseConstrApply` rewrites multi-argument `Apply` chains as `Case`/`Constr` nodes: ```scala mdoc:compile-only copy showLineNumbers import scalus.uplc.Term import scalus.uplc.transform.* val handCrafted: Term = ??? // your DSL-built term // Apply individual passes val optimized = handCrafted |> EtaReduce.apply |> Inliner.apply |> CaseConstrApply.apply // Or apply the full pipeline val fullyOptimized = scalus.uplc.transform.UplcOptimizer.optimize(handCrafted) ``` ## Writing Your Own Optimizer The `scalus.uplc.transform` package contains all built-in optimizer passes. You can write your own by implementing the `Optimizer` trait: ```scala mdoc:compile-only copy showLineNumbers import scalus.uplc.Term import scalus.uplc.transform.Optimizer object MyOptimizer extends Optimizer { def apply(term: Term): Term = { // Transform the term tree term } } ``` Custom optimizers can be applied to both compiler-generated and hand-crafted UPLC. You can also register them in the `Options.uplcOptimizers` list to run them as part of the standard pipeline. See `scalus.uplc.transform.Inliner`, `EtaReduce`, and `CaseConstrApply` for examples of real optimizer implementations. ## Mixing DSL with Compiled Code You can combine hand-crafted UPLC fragments with compiler-generated code: ```scala mdoc:compile-only copy showLineNumbers import scalus.*, scalus.compiler.compile import scalus.uplc.Term, scalus.uplc.Term.{asTerm, λ} import scalus.uplc.TermDSL.given import scalus.uplc.DefaultFun.* // Compile part of the logic with the plugin val compiledFragment = compile { (x: BigInt, y: BigInt) => x + y }.toUplcOptimized() // Use it in a hand-crafted validator val validator = λ { datum => λ { redeemer => λ { ctx => compiledFragment $ datum $ redeemer } } } ``` This lets you hand-optimize hot paths while keeping the rest of your validator in high-level Scala. ## What's Next? - **[UPLC Optimiser Pipeline](/docs/smart-contract-optimisations/uplc-optimiser-pipeline)** — the optimiser passes that run on hand-crafted UPLC fragments, same as compiler-generated ones. - **[Measuring Performance](/docs/smart-contract-optimisations/measuring-performance)** — confirm that each hand-rolled term actually beats the compiled version on CPU, memory, and total fee. --- Source: https://scalus.org/docs/smart-contract-optimisations/uplc-optimiser-pipeline --- # UPLC Optimiser Pipeline Scalus optimizes Untyped Plutus Core (UPLC) scripts to reduce script size and execution costs. By default, `toUplcOptimized()` and `Options.default` apply the full optimization pipeline automatically. ## Optimization Pipeline The optimizer runs several passes in sequence: 1. **EtaReduce** — eliminates redundant lambda wrappers (`λx. f x` → `f`) 2. **Inliner** — beta-reduction, dead code elimination, constant folding via `PartialEvaluator` (runs 3 times) 3. **StrictIf** — converts eligible `if/then/else` to strict evaluation 4. **ForcedBuiltinsExtractor** — hoists shared `Force(Builtin(...))` subexpressions 5. **CaseConstrApply** — rewrites multi-argument application as `Case`/`Constr` nodes Steps 1–2 repeat three times to handle patterns created by earlier inlining. ```scala mdoc:compile-only copy showLineNumbers import scalus.*, scalus.compiler.*, scalus.uplc.Term val sir = compile: (x: BigInt) => x + BigInt(1) // Option 1: use toUplcOptimized() — optimization always on val optimized: Term = sir.toUplcOptimized() // Option 2: use toUplc() with explicit control val unoptimized: Term = sir.toUplc(optimizeUplc = false) // Option 3: apply individual passes manually import scalus.uplc.transform.* val manual: Term = sir.toUplc(optimizeUplc = false) |> EtaReduce.apply |> Inliner.apply ``` ## EtaReduce Eliminates unnecessary lambda abstractions. Transforms `λx. f x` into `f` when `x` appears only as a direct argument and `f` is pure (won't crash if evaluated eagerly). ## Inliner The Inliner is the core optimization pass. It performs: - **Beta-reduction** — `(λx. body) arg` → `body[x := arg]` when safe - **Identity elimination** — `(λx. x) t` → `t` - **Dead code elimination** — `(λx. body) arg` → `body` when `x` is unused and `arg` is pure - **Force/Delay cancellation** — `Force(Delay(t))` → `t` - **Constant folding** — closed subexpressions are evaluated at compile time via the CEK machine ### Inlining Safety The Inliner uses occurrence analysis to decide when inlining is safe: | Occurrences | Action | |---|---| | **Zero** (unused) | Eliminate the argument if it's pure (can't crash) | | **Once, direct** | Always inline — evaluation timing is unchanged | | **Once, guarded** (under lambda/delay) | Inline only if the argument is a value (no side effects to defer) | | **Multiple** | Inline only small/cheap terms: variables, small constants (≤64 bits), builtins | ### Compile-time Partial Evaluation When the Inliner encounters a closed subexpression (no free variables) that contains a reducible operation (function application, `Force`, or `Case`), it runs the CEK machine to evaluate it at compile time. If the result is a constant, the original expression is replaced with that constant. This means arithmetic on constants, pattern matching on known constructors, and even multi-step computations are all folded away during compilation: ```scala mdoc:compile-only copy showLineNumbers import scalus.*, scalus.compiler.* // addInteger(2, 3) is folded to 5 at compile time val sir1 = compile: BigInt(2) + BigInt(3) // Case/Constr on known tag is eliminated at compile time val sir2 = compile: val pair = (BigInt(1), BigInt(2)) pair._1 + pair._2 // folded to 3 ``` Partial evaluation has a budget cap to prevent slow compilation. If evaluation exceeds the budget or fails, the original term is kept unchanged. Expressions containing `Trace` are never folded because trace has a logging side effect. ## Closed Functions as Compile-time Macros Any function that doesn't depend on external (runtime) variables is effectively a **compile-time macro**. When such a function is applied to constant arguments inside `compile`, the entire computation — including recursion — is evaluated at compile time by the Inliner's partial evaluator. The result is a single constant embedded in the final script. This is powerful for precomputing values that would be expensive to calculate on-chain: ```scala mdoc:compile-only copy showLineNumbers {5-9,12} import scalus.*, scalus.compiler.* import scala.annotation.tailrec @Compile object Fibonacci { def fib(n: BigInt): BigInt = @tailrec def f(n: BigInt, x: BigInt, y: BigInt): BigInt = if n > 1 then f(n - 1, y, x + y) else y f(n, 0, 1) } // fib(100) is fully evaluated at compile time! // The recursive computation runs during compilation, // and the final script contains just the constant. val sir = compile(Fibonacci.fib(100)) val uplc = sir.toUplcOptimized() // uplc is now: (Const Integer 354224848179261915075) ``` Here `fib` is a closed function — it only uses its parameters and local bindings, with no references to runtime state. When called with `100`, the Inliner: 1. Beta-reduces the function application, substituting `100` for `n` 2. Recognizes the result as a closed, reducible expression 3. Runs the CEK machine to fully evaluate the tail-recursive loop 4. Replaces the entire expression with the constant `354224848179261915075` ### Practical Uses This pattern works for any closed computation over constants: - **Precomputed lookup tables** — encode Fibonacci numbers, CRC tables, or fee schedules as a ByteString at compile time, then slice at runtime for O(1) lookups - **Derived configuration** — compute thresholds, epoch boundaries, or protocol parameters from base constants - **Hash preimages** — precompute hashes of known values so the on-chain code only needs to compare - **Mathematical constants** — compute precision values, powers, or factorials once at compile time The key insight: if you can express the computation in Scalus (`@Compile` annotated code) and all inputs are constants, it runs at compile time for free. ## What's Next? - **[Measuring Performance](/docs/smart-contract-optimisations/measuring-performance)** — quantify the impact of each optimiser pass on real transaction fees. - **[Deploying Contracts](/docs/dapp-development/sbt-plugin#deploying-contracts)** — once the optimised script is final, publish it as a reference script UTxO. - **[Smart Contract Optimisations](/docs/smart-contract-optimisations)** — back to the index for the full set of techniques. --- Source: https://scalus.org/docs/smart-contract-optimisations --- # Advanced Smart Contract Optimisations So you have a working smart contract and now want to make it efficient. First, let's understand the compilation pipeline: **Scala Source -> SIR -> (lowering) -> UPLC -> (optimize) -> UPLC -> Plutus Script** In most cases the default pipeline works well out of the box. But when you're working on a highly specific protocol or hitting transaction budget limits, understanding the low-level details lets you take control and squeeze out the performance you need. ## Compilation Backends Scalus has three backends for lowering SIR to UPLC: | | **V3 Lowering** (default) | **Scott Encoding** | **Sum of Products** | |---|---|---|---| | Enum value | `SirToUplcV3Lowering` | `ScottEncodingLowering` | `SumOfProductsLowering` | | Architecture | Sea-of-nodes, data-flow based | Template-based | Template-based | | Data handling | Types stay as `Data` internally (`toData` is a NOOP in most cases) | Scott-encodes constructors as lambdas | Uses PlutusV3 `Constr`/`Case` nodes | | Best for | Most validators (default choice) | Legacy code, PlutusV1/V2 scripts | When `Constr`/`Case` encoding is preferred | How your code is compiled depends on the `given Options` in scope. The `Options` case class controls the backend, target protocol version, optimization, and error traces: ```scala mdoc:compile-only copy showLineNumbers import scalus.compiler.Options import scalus.compiler.sir.TargetLoweringBackend import scalus.cardano.ledger.MajorProtocolVersion // Use Options to select the backend and protocol version given Options = Options( targetLoweringBackend = TargetLoweringBackend.SirToUplcV3Lowering, // default targetProtocolVersion = MajorProtocolVersion.vanRossemPV, // default (PV11) generateErrorTraces = true, optimizeUplc = true ) ``` There are also convenient presets: `Options.default`, `Options.debug`, `Options.release`, and `Options.plomin` (targets protocol version 10, reproducing pre-van-Rossem output and script hashes). The same `given Options` is picked up by `PlutusV3.compile`: ```scala mdoc:compile-only copy showLineNumbers import scalus.compiler.Options import scalus.uplc.PlutusV3 // Options in scope control how the validator is compiled given Options = Options.release val compiled = PlutusV3.compile(MyValidator.validate) ``` ## Inspecting Your Compiled Output Before optimizing, learn to read what the compiler produces. There are three stages in the pipeline you can inspect. Let's use a simple validator as an example: ```scala mdoc:compile-only copy showLineNumbers import scalus.*, scalus.compiler.{compile, Options} import scalus.uplc.builtin.{ByteString, Data} import scalus.uplc.builtin.Builtins.* @Compile object SimpleValidator { def validator(datum: Data, redeemer: Data, ctx: Data): Unit = { val hash = datum.toByteString val preimage = redeemer.toByteString sha2_256(preimage) == hash || (throw new RuntimeException("Wrong")) } } given Options = Options(generateErrorTraces = true) val sir = compile(SimpleValidator.validator) ``` ### SIR (Scalus Intermediate Representation) SIR is a typed lambda calculus that closely mirrors your Scala code. Print it with `sir.show` (or `sir.showHighlighted` for terminal colors): ```scala mdoc:compile-only copy showLineNumbers println(sir.show) ``` ```ocaml let SimpleValidator$.validator = {λ datum redeemer ctx -> let hash: ByteString = unBData(datum) in let preimage: ByteString = unBData(redeemer) in let _: Boolean = equalsByteString(sha2_256(preimage), hash) or ERROR "Wrong" in () } in {λ datum redeemer ctx -> SimpleValidator$.validator(datum, redeemer, ctx) } ``` You can see the structure: `let` bindings, builtin calls (`unBData`, `sha2_256`, `equalsByteString`), and the `or ERROR` short-circuit. SIR-level optimizations are applied automatically during the pipeline: `RemoveRecursivity` runs during linking, `RemoveTraces` strips trace calls when `Options.removeTraces = true` (e.g. `Options.release`), and `LetFloating` is used by the template-based backends. ### Lowered Value (V3 Backend intermediate form) The V3 Lowering Backend has an intermediate representation between SIR and UPLC. Inspect it with `sir.toLoweredValue().show`: ```scala mdoc:compile-only copy showLineNumbers val lowered = sir.toLoweredValue() println(lowered.show) ``` ``` let SimpleValidator$.validator = (lam datum: Data -> Data -> Data -> Unit. (lam redeemer: Data -> Data -> Unit. (lam ctx: Data -> Unit. let hash = App((builtin unBData) datum) in let preimage = App((builtin unBData) redeemer) in let _ = if App(App((builtin equalsByteString) App((builtin sha2_256) preimage)) hash) then (con bool True) else force(Trace("Wrong" (delay (error)))) in (con unit ())))) in ... ``` This is the V3 Lowering Backend's intermediate form. You can see how it maps closely to the final UPLC but still retains type annotations and named bindings. Notice that `datum` and `redeemer` stay as `Data` — no conversion happens. ### Final UPLC The final output -- Untyped Plutus Core. This is what runs on-chain: ```scala mdoc:compile-only copy showLineNumbers // Unoptimized -- see the full structure val uplc = sir.toUplc(optimizeUplc = false) println(uplc.show) ``` ``` [(lam validator20 (lam datum21 (lam redeemer22 (lam ctx23 [validator20 datum21 redeemer22 ctx23])))) (lam datum13 (lam redeemer14 (lam ctx15 [(lam hash16 [(lam preimage17 [(lam _19 (con unit ())) (force [(force (builtin ifThenElse)) [(builtin equalsByteString) [(builtin sha2_256) preimage17] hash16] (delay (con bool True)) (delay (force [(force (builtin trace)) (con string "Wrong") (delay (error))]))])]) [(builtin unBData) redeemer14]]) [(builtin unBData) datum13]])))] ``` After optimization (`sir.toUplc(optimizeUplc = true)`), the optimizer inlines the outer let-binding, hoists common builtins, and applies `CaseConstrApply`: ``` [(lam __IfThenElse (lam __Trace (lam datum21 (lam redeemer22 (lam ctx23 [(lam _19 (con unit ())) (force (case (constr 0 [(builtin equalsByteString) [(builtin sha2_256) [(builtin unBData) redeemer22]] [(builtin unBData) datum21]] (delay (con bool True)) (delay (force [__Trace (con string "Wrong") (delay (error))]))) __IfThenElse))]))))) (force (builtin ifThenElse)) (force (builtin trace))] ``` Notice how the optimized version: - Inlined the `validator` let-binding and the `hash`/`preimage` bindings - Hoisted `(force (builtin ifThenElse))` and `(force (builtin trace))` as shared lambdas - Converted `ifThenElse` application to a `case`/`constr` node (via `CaseConstrApply`) ## Optimization Techniques - **[Measuring Performance](/docs/smart-contract-optimisations/measuring-performance)** -- script evaluation, emulator-based transaction fees - **[Algorithmic Optimisations](/docs/smart-contract-optimisations/algorithmic-optimisations)** -- design patterns for the biggest wins - **[Scala Metaprogramming](/docs/smart-contract-optimisations/scala-metaprogramming)** -- inlining, loop unrolling, constants, compile-time evaluation, conditional compilation - **[Low-Level Builtins](/docs/smart-contract-optimisations/low-level-builtins)** -- manual Data access, script context macros, budget comparison - **[Lowering Backends](/docs/smart-contract-optimisations/lowering-backends)** -- `@UplcRepr`, V3 Lowering vs template-based backends - **[UPLC Term DSL](/docs/smart-contract-optimisations/uplc-term-dsl)** -- hand-craft validators with full AST control (Plutarch-style) - **[UPLC Optimiser Pipeline](/docs/smart-contract-optimisations/uplc-optimiser-pipeline)** -- apply low-level optimiser passes to reduce execution costs ## When to Optimize 1. **After functionality is correct** -- get it working first, then optimize 2. **When hitting limits** -- transaction size or execution budget constraints 3. **For production deployment** -- reduce user costs and improve UX 4. **Iteratively** -- inspect, optimize, measure, repeat --- Source: https://scalus.org/docs/transactions/building-first-transaction --- # How to Build Your First Cardano Transaction This guide walks you through building a simple Cardano transaction using Scalus TxBuilder. You'll learn the fundamental workflow: setting up the environment, building, and signing a transaction. ## Set Up the `CardanoInfo` `CardanoInfo` encapsulates everything TxBuilder needs to construct valid transactions: - **Protocol parameters** for fee calculation, execution limits, and deposits - **Network** identifier (Mainnet or Testnet) for address validation - **Slot configuration** for time-based validity constraints ### Pre-built Configurations Scalus provides ready-to-use configurations with embedded protocol parameters: ```scala copy import scalus.cardano.ledger.* import scalus.cardano.txbuilder.* // Mainnet (production) val cardanoInfo = CardanoInfo.mainnet // Preprod testnet val preprodInfo = CardanoInfo.preprod // Preview testnet val previewInfo = CardanoInfo.preview ``` ### Custom Configuration (Yaci DevKit Example) For local development with Yaci DevKit or other custom environments, load protocol parameters from JSON and configure the slot timing: ```scala copy // Load params from Blockfrost API or cardano-cli val params = ProtocolParams.fromBlockfrostJson(blockfrostJsonString) // Or: ProtocolParams.fromCardanoCliJson(cliJsonString) // Yaci DevKit uses slot length of 1 second and start time of 0 val yaciSlotConfig = SlotConfig( zeroTime = 0L, zeroSlot = 0L, slotLength = 1000 ) val yaciDevKit = CardanoInfo( protocolParams = params, network = Network.Testnet, slotConfig = yaciSlotConfig ) ``` The pre-built configurations use embedded protocol parameters that are updated periodically. For production use, consider querying current parameters from a node or API. ## Working with Addresses Scalus provides convenient string interpolators for parsing Cardano bech32 addresses: ```scala copy import scalus.cardano.address.Address.{addr, stake} import scalus.cardano.address.StakeAddress // Parse any Cardano address (Shelley, Stake, or Byron) val recipient = addr"addr1qx2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3n0d3vllmyqwsx5wktcd8cc3sq835lu7drv2xwl2wywfgse35a3x" val testnet = addr"addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3n0d3vllmyqwsx5wktcd8cc3sq835lu7drv2xwl2wywfgs3xyj0d" // Parse stake addresses specifically (returns StakeAddress type) val stakeAddr: StakeAddress = stake"stake1uyehkck0lajq8gr28t9uxnuvgcqrc6070x3k9r8048z8y5gh6ffgw" ``` These interpolators validate the address format at runtime and throw an exception for invalid addresses. The `addr` interpolator returns `Address` (the base trait), while `stake` returns `StakeAddress` specifically. Use `stake` when you need the more specific type. ## Connect to the Blockchain and Query UTXOs To interact with the Cardano blockchain, you need a `BlockchainProvider`. Connect to a Blockfrost-compatible API and query UTXOs to spend: ```scala copy import scalus.cardano.node.BlockfrostProvider import scalus.utils.await import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.duration.* // Connect to preprod testnet (get API key at blockfrost.io) val provider = BlockfrostProvider.preprod("your-blockfrost-api-key").await(30.seconds) // Query UTXOs at your address val myAddress = addr"addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer..." val utxos = provider.findUtxos(myAddress).await(30.seconds).getOrElse(Map.empty) ``` See [Blockchain Providers](/docs/dapp-development/blockchain-providers) for more details on providers and advanced UTXO queries. ## Build the Transaction Use TxBuilder's fluent API to specify inputs, outputs, and change handling: ```scala copy import scalus.cardano.address.Address.addr import scalus.cardano.ledger.Value // Your UTxOs and addresses val myUtxo: Utxo = // ... UTxO to spend val recipientAddress = addr"addr1qx2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer..." val changeAddress = addr"addr1qy..." val builder = TxBuilder(cardanoInfo) .spend(myUtxo) // Add input .payTo(recipientAddress, Value.ada(10)) // Send 10 ADA .build(changeTo = changeAddress) // Finalize transaction with change ``` The `build()` method: - Calculates the transaction fee based on size - Creates a change output with the remaining value - Validates that the transaction is balanced - Returns a new builder with the finalized transaction `Value.ada(10)` creates a Value with 10 million lovelace (10 ADA) and no extra tokens. ## Sign the Transaction Add signatures to authorize spending the inputs. You can create a `TransactionSigner` in two ways: **From a mnemonic phrase and derivation path:** ```scala copy import scalus.cardano.wallet.BloxbeanAccount val mnemonic = "test " * 23 + "sauce" // 24-word test mnemonic val derivationPath = "m/1852'/1815'/0'/0/0" // Standard Cardano derivation path for the first ADA wallet val signer = BloxbeanAccount(network, mnemonic, derivationPath).signerForUtxos ``` **From a specific keypair:** ```scala copy val keyPair: KeyPair = // ... your keypair val signer = TransactionSigner(Set(keyPair)) ``` Then sign the transaction: ```scala copy val signedBuilder = builder.sign(signer) val transaction = signedBuilder.transaction ``` The `sign()` method adds the signature to the transaction's witness set. You can chain multiple `sign()` calls if multiple signatures are needed. ## Submit the Transaction Finally, submit the signed transaction to the Cardano network: ```scala copy import scalus.cardano.node.BlockchainProvider import scalus.utils.await import scala.concurrent.duration.* val provider: BlockchainProvider = // ... blockfrost, emulator, etc. provider.submit(transaction).await(30.seconds) match { case Right(txHash) => println(s"Transaction submitted: ${txHash.toHex}") case Left(error) => println(s"Submission failed: $error") } ``` ## Complete Example Here's the full workflow in one place: ```scala copy import scalus.cardano.ledger.* import scalus.cardano.txbuilder.* import scalus.cardano.address.Address import scalus.cardano.node.BlockchainProvider import scalus.utils.await import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.duration.* // Setup - use given CardanoInfo for cleaner txBuilder calls given CardanoInfo = CardanoInfo.mainnet val myUtxo: Utxo = // ... val recipientAddress: Address = // ... val changeAddress: Address = // ... val signer: TransactionSigner = // ... val provider: BlockchainProvider = // ... // Build, sign, and submit using txBuilder with context parameter val transaction = txBuilder .spend(myUtxo) .payTo(recipientAddress, Value.ada(10)) .build(changeTo = changeAddress) .sign(signer) .transaction provider.submit(transaction).await(30.seconds) ``` ## Using Automatic Completion For simpler cases, use `complete()` to let TxBuilder handle input selection automatically: ```scala copy val transaction = TxBuilder(cardanoInfo) .payTo(recipientAddress, Value.ada(10)) .complete(provider, sponsorAddress) // Automatic input selection and balancing .await(30.seconds) .sign(signer) .transaction ``` The `complete()` method: - Queries the provider for UTxOs at the sponsor address - Selects inputs to cover the payment and fees - Adds collateral if the transaction includes script execution - Creates change outputs to return excess value - Balances the transaction (no need to call `.build()` afterward) ## Cross-Platform Async Completion The `complete()` method returns a `Future[TxBuilder]` and works on both JVM and JavaScript platforms: ```scala copy import scala.concurrent.ExecutionContext.Implicits.global val futureTransaction: Future[Transaction] = TxBuilder(cardanoInfo) .payTo(recipientAddress, Value.ada(10)) .complete(asyncProvider, sponsorAddress) .map(_.sign(signer).transaction) ``` This is the recommended approach for cross-platform code and async workflows. ### Completion with Pre-fetched UTXOs If you already have UTXOs available in memory (e.g., from a previous query or a custom source), you can use the synchronous `complete()` overload that accepts UTXOs directly: ```scala copy // Pre-fetched or cached UTXOs from any source val availableUtxos: Utxos = Map( Input(txHash, 0) -> Output(sponsorAddress, Value.ada(100)), Input(txHash, 1) -> Output(sponsorAddress, Value.ada(50)) ) // Synchronous - no Future, no async provider query val transaction = TxBuilder(cardanoInfo) .payTo(recipientAddress, Value.ada(10)) .complete(availableUtxos, sponsorAddress) // Immediate completion .sign(signer) .transaction ``` This variant is useful when: - You've already queried UTXOs and want to avoid redundant network calls - You're building transactions in a batch with shared UTXO state - You're working with a custom UTXO source (e.g., in-memory ledger state) The behavior is identical to the provider-based `complete()` - it selects inputs, adds collateral if needed, and balances the transaction. ## Next Steps - **[First Contract Transaction](/docs/transactions/first-contract-transaction)** - Lock funds and spend from a script address using a compiled validator - **[Payment Methods](/docs/transactions/payment-methods)** - Different ways to send ADA and tokens - **[Spending UTxOs](/docs/transactions/spending-utxos)** - Manual input selection and spending from scripts - **[Minting & Burning](/docs/transactions/minting-burning-assets)** - Create and destroy native tokens - **[Staking & Rewards](/docs/transactions/staking-rewards)** - Register stake keys, delegate to pools, and withdraw rewards - **[Governance](/docs/transactions/governance)** - Participate in Cardano governance through DRep delegation --- Source: https://scalus.org/docs/transactions/first-contract-transaction --- # How to Use a Compiled Validator in Transactions This guide shows how to lock funds at a script address and spend them using a compiled validator. It builds on the [First Transaction](/docs/transactions/building-first-transaction) guide, adding script interactions with datums and redeemers. ## Compile the Validator Start with a simple spending validator that checks whether the transaction is signed by the owner stored in the datum: ```scala copy import scalus.Compiler.compile import scalus.uplc.builtin.Data import scalus.cardano.onchain.plutus.v3.* import scalus.cardano.onchain.plutus.prelude.* import scalus.uplc.Compiled.* object OwnerValidator { val validate = compile { (datum: Data, redeemer: Data, ctxData: Data) => val ctx = ctxData.to[ScriptContext] val owner = datum.to[PubKeyHash] val signatories = ctx.txInfo.signatories List.findOrFail(signatories) { sig => sig.hash == owner.hash } } } ``` Compile it to a Plutus V3 script: ```scala copy import scalus.uplc.Compiled.* val compiled = PlutusV3.compile(OwnerValidator.validate) ``` `PlutusV3.compile` returns a `PlutusV3[A]` — a compiled script that carries both the on-chain Plutus script and the SIR (Scalus Intermediate Representation) used for diagnostic replay. ## Lock Funds at the Script Address Send ADA to the script address with an inline datum. The datum stores the owner's public key hash — whoever holds the corresponding signing key can later unlock the funds: ```scala copy import scalus.cardano.txbuilder.* import scalus.cardano.address.Address.addr given CardanoInfo = CardanoInfo.preprod val ownerPkh: PubKeyHash = // ... owner's public key hash val sponsorAddress = addr"addr_test1qz..." val lockTx = txBuilder .payTo(compiled, Value.ada(10), ownerPkh) // 10 ADA with inline datum .complete(provider, sponsorAddress) .await(30.seconds) .sign(signer) .transaction ``` The `payTo(compiled, value, datum)` overload derives the script address automatically and attaches the datum as an inline datum on the output. It also registers the debug script, enabling diagnostic replay if the script fails — the same behavior as `spend(utxo, redeemer, compiled)`. `payTo` also accepts a plain `Address` instead of a `CompiledPlutus` — use `payTo(address, value, datum)` when you need a manually constructed address or when sending to a non-script address. ## Spend from the Script Address To spend a UTxO locked at the script address, provide a redeemer and the compiled script. Pass the `compiled` object directly — not `compiled.script`: ```scala copy val lockedUtxo: Utxo = // ... the UTxO locked at the script address val spendTx = txBuilder .spend(lockedUtxo, (), compiled, Set(ownerPkh.hash)) // redeemer is Unit, owner must sign .complete(provider, sponsorAddress) .await(30.seconds) .sign(signer) .transaction ``` **Why pass `compiled` instead of `compiled.script`?** The `spend` overload accepting `CompiledPlutus` registers a debug script automatically. If the on-chain script was compiled with `Options.release` (no error traces) and fails during evaluation, TxBuilder recompiles from SIR with traces enabled and replays the execution — giving you a detailed error message instead of a cryptic failure. You *can* use `compiled.script` and a raw script address, but you lose this diagnostic replay capability. ## Complete Example Here's the full lock-then-spend flow: ```scala copy import scalus.Compiler.compile import scalus.uplc.builtin.Data import scalus.cardano.address.Address.addr import scalus.cardano.ledger.* import scalus.cardano.node.BlockfrostProvider import scalus.cardano.txbuilder.* import scalus.cardano.onchain.plutus.v3.* import scalus.cardano.onchain.plutus.prelude.* import scalus.uplc.Compiled.* import scalus.utils.await import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.duration.* // 1. Compile the validator val compiled = PlutusV3.compile(compile { (datum: Data, redeemer: Data, ctxData: Data) => val ctx = ctxData.to[ScriptContext] val owner = datum.to[PubKeyHash] val signatories = ctx.txInfo.signatories List.findOrFail(signatories) { sig => sig.hash == owner.hash } }) // 2. Setup given CardanoInfo = CardanoInfo.preprod val provider = BlockfrostProvider.preprod("your-api-key").await(30.seconds) val sponsorAddress = addr"addr_test1qz..." val ownerPkh: PubKeyHash = // ... owner's public key hash // 3. Lock funds at the script address val lockTx = txBuilder .payTo(compiled, Value.ada(10), ownerPkh) .complete(provider, sponsorAddress) .await(30.seconds) .sign(signer) .transaction provider.submit(lockTx).await(30.seconds) // 4. Query the locked UTxO val scriptUtxos = provider.findUtxos(compiled.address(cardanoInfo.network)).await(30.seconds).getOrElse(Map.empty) val lockedUtxo = scriptUtxos.head // (Input, Output) pair // 5. Spend from the script address val spendTx = txBuilder .spend(lockedUtxo, (), compiled, Set(ownerPkh.hash)) .complete(provider, sponsorAddress) .await(30.seconds) .sign(signer) .transaction provider.submit(spendTx).await(30.seconds) ``` ## Next Steps - **[Spending UTxOs](/docs/transactions/spending-utxos)** — Manual input selection and script UTxO patterns - **[Minting & Burning](/docs/transactions/minting-burning-assets)** — Create and destroy native tokens using minting policies - **[HTLC Tutorial](/docs/smart-contracts/htlc-tutorial)** — End-to-end Hash Time-Locked Contract with transactions and tests - **[Debugging Guide](/docs/testing/debugging)** — Deep dive into debugging failed script execution --- Source: https://scalus.org/docs/transactions/payment-methods --- # Payment Methods: Send ADA and Native Tokens TxBuilder provides several methods for creating transaction outputs with different levels of control. This guide covers the various ways to send ADA and native tokens. ## How to Make Simple ADA Payment The most basic payment sends ADA to an address without any datum: ```scala copy TxBuilder(env) .spend(utxo) .payTo(recipientAddress, Value.ada(10)) // Send 10 ADA .build(changeTo = changeAddress) ``` ## Sending Native Tokens on Cardano Send native tokens along with ADA: ```scala copy import scalus.cardano.ledger.{AssetName, PolicyId, MultiAsset, Value} import scalus.uplc.builtin.ByteString.hex import scala.collection.immutable.SortedMap val policyId = PolicyId(hex"...") val assetName = AssetName(hex"...") val tokenValue = Value.asset( policyId, assetName, amount = 100L, // 100 tokens lovelace = Coin.ada(2) // Plus 2 ADA ) TxBuilder(env) .spend(utxo) .payTo(recipientAddress, tokenValue) .build(changeTo = changeAddress) ``` TxBuilder automatically ensures outputs meet the minimum ADA requirement based on the protocol parameters. ## Payment with Inline Datum Send funds to a script address with an inline datum: ```scala copy import scalus.uplc.builtin.Data import scalus.uplc.builtin.ToData case class MyDatum(owner: PubKeyHash, amount: Long) derives ToData val datum = MyDatum(ownerPubKeyHash, 1000) TxBuilder(env) .spend(utxo) .payTo(scriptAddress, Value.ada(10), datum) // Datum is inlined .build(changeTo = changeAddress) ``` The datum is automatically serialized to `Data` and included inline in the output. ## Payment with Datum Hash Send funds with a datum hash and attach the datum to the witness set: ```scala copy val datum = MyDatum(ownerPubKeyHash, 1000) TxBuilder(env) .spend(utxo) .attach(datum.toData) // Add datum to witness set .payTo(scriptAddress, Value.ada(10), datumHash) .build(changeTo = changeAddress) ``` The `attach()` method computes the datum hash and stores the datum for inclusion in the witness set. Use a precomputed datum hash: ```scala copy import scalus.cardano.ledger.DataHash import scalus.uplc.builtin.Builtins.{blake2b_256, serialiseData} val datum = MyDatum(ownerPubKeyHash, 1000) val datumHash = DataHash.fromByteString( blake2b_256(serialiseData(datum.toData)) ) TxBuilder(env) .spend(utxo) .attach(datum.toData) .payTo(scriptAddress, Value.ada(10), datumHash) .build(changeTo = changeAddress) ``` ## Multiple Payments Chain multiple `payTo()` calls to create multiple outputs: ```scala copy TxBuilder(env) .spend(utxo) .payTo(recipient1, Value.ada(5)) .payTo(recipient2, Value.ada(3)) .payTo(recipient3, Value.ada(2)) .build(changeTo = changeAddress) ``` Each call creates a separate UTxO at the recipient address. ## Custom Transaction Output For full control, use the `output()` method with a complete `TransactionOutput`: ```scala copy import scalus.cardano.ledger.{Output, DatumOption, Script} val customOutput = Output( address = recipientAddress, value = Value.ada(10), datumOption = Some(DatumOption.Inline(myDatum.toData)), scriptRef = Some(Script.PlutusV3(myScript)) // Attach reference script ) TxBuilder(env) .spend(utxo) .output(customOutput) .build(changeTo = changeAddress) ``` This gives you complete control over all output fields, including attaching reference scripts. ## Token Change Handling When spending UTxOs with tokens, TxBuilder automatically returns unused tokens to the change address: ```scala copy // UTxO contains 10 ADA + 1000 tokens val utxoWithTokens: Utxo = // ... TxBuilder(env) .spend(utxoWithTokens) .payTo(recipient, Value.ada(5)) // Only send ADA .build(changeTo = changeAddress) // Tokens automatically returned here ``` The change output will contain the remaining ADA and all 1000 tokens. ## Next Steps - **[Spending UTxOs](/docs/transactions/spending-utxos)** - Learn about input selection and spending from scripts - **[Minting & Burning](/docs/transactions/minting-burning-assets)** - Create and destroy native tokens - **[Validator Interactions](/docs/transactions/validator-interactions)** - Work with Plutus scripts --- Source: https://scalus.org/docs/transactions/spending-utxos --- # Spending UTxOs: Inputs, Scripts, and Collateral This guide covers how to spend different types of UTxOs: from regular addresses, from script addresses, and using reference inputs. ## Spending from Wallet / Public Key Address The simplest case is spending a UTxO locked by a public key: ```scala copy val utxo: Utxo = // ... UTxO from a regular address TxBuilder(env) .spend(utxo) // Add as input .payTo(recipient, Value.ada(10)) .build(changeTo = changeAddress) .sign(signer) // Must be signed by the address owner ``` You can also spend multiple UTxOs at once: ```scala copy val utxos: Utxos = Map( input1 -> output1, input2 -> output2 ) TxBuilder(env) .spend(utxos) // Spend all UTxOs .payTo(recipient, Value.ada(20)) .build(changeTo = changeAddress) ``` ## Spending from Script Address When spending UTxOs locked by a validator script, you must provide a redeemer and the script itself: ```scala copy import scalus.uplc.builtin.Data val scriptUtxo: Utxo = // ... UTxO at script address val redeemer = MyRedeemer(...) // Your redeemer type // Spend with inline script TxBuilder(env) .spend(scriptUtxo, redeemer, script) .collaterals(collateralUtxo) // Required for script execution .payTo(recipient, scriptUtxo.output.value) .build(changeTo = changeAddress) ``` Script transactions require collateral inputs. These are only consumed if the script fails validation. ### With Required Signers If your validator requires additional signatures: ```scala copy TxBuilder(env) .spend(scriptUtxo, redeemer, script) .requireSignature(pubKeyHash1) .requireSignature(pubKeyHash2) .collaterals(collateralUtxo) .payTo(recipient, scriptUtxo.output.value) .build(changeTo = changeAddress) .sign(signer1) .sign(signer2) // Must provide all required signatures ``` Required signers appear in `TxInfo.signatories`, so on-chain validators can verify these signatures. ## Spending with Attached Scripts You can attach a script multiple times, it will be reused for all inputs that reference it: ```scala copy TxBuilder(env) .spend(scriptUtxo1, redeemer1, script) // attach a script .spend(scriptUtxo2, redeemer2, script) // Can reuse for multiple inputs .collaterals(collateralUtxo) .payTo(recipient, totalValue) .build(changeTo = changeAddress) ``` TxBuilder will automatically find the script in the attached scripts map and use it for validation. ## Delayed Redeemers For advanced cases where the redeemer depends on the final transaction structure, you can provide a function that computes the redeemer from the assembled transaction: ```scala copy def buildRedeemer(tx: Transaction): Data = { // Compute redeemer based on final transaction val outputCount = tx.body.value.outputs.size MyRedeemer(outputCount).toData } TxBuilder(env) .spend(scriptUtxo, buildRedeemer, script) // Redeemer computed after assembly .collaterals(collateralUtxo) .payTo(recipient, scriptUtxo.output.value) .build(changeTo = changeAddress) ``` The redeemer function is called after the transaction is assembled but before script evaluation, allowing self-referential validation logic. ### Use Cases for Delayed Redeemers Delayed redeemers are useful when: 1. **Input index validation**: Your validator needs to verify its position in the transaction inputs 2. **Output verification**: The redeemer contains indices of outputs to validate 3. **Self-referencing logic**: The script needs to know properties of the final transaction ```scala copy // Example: Validator that checks its input index val buildIndexRedeemer: Transaction => Data = { tx => val myInputIndex = tx.body.value.inputs.indexWhere(_.transactionId == scriptUtxo.input.transactionId) Data.I(myInputIndex) } TxBuilder(env) .spend(scriptUtxo, buildIndexRedeemer, script) .collaterals(collateralUtxo) .payTo(recipient, scriptUtxo.output.value) .build(changeTo = changeAddress) ``` When using `complete()`, delayed redeemers are automatically recomputed after inputs are selected and the transaction is balanced. ## Reference Inputs Reference inputs allow scripts to read UTxOs without consuming them: ```scala copy val referenceUtxo: Utxo = // ... UTxO with reference data TxBuilder(env) .spend(myUtxo) .references(referenceUtxo) // Add as reference input .payTo(recipient, Value.ada(10)) .build(changeTo = changeAddress) ``` Multiple reference inputs can be added: ```scala copy TxBuilder(env) .spend(myUtxo) .references(refUtxo1, refUtxo2, refUtxo3) .payTo(recipient, Value.ada(10)) .build(changeTo = changeAddress) ``` Reference inputs are visible in the script context but are not spent. ## Collateral Inputs Script transactions require collateral to cover fees if validation fails: ```scala copy val collateral: Utxo = // ... Pure ADA UTxO TxBuilder(env) .spend(scriptUtxo, redeemer, script) .collaterals(collateral) // Add single collateral .payTo(recipient, scriptUtxo.output.value) .build(changeTo = changeAddress) ``` Multiple collateral inputs: ```scala copy TxBuilder(env) .spend(scriptUtxo, redeemer, script) .collaterals(collateral1, collateral2) .payTo(recipient, scriptUtxo.output.value) .build(changeTo = changeAddress) ``` When using `complete()`, collateral inputs are automatically selected and added if the transaction includes script execution. TxBuilder also automatically creates collateral return outputs when needed and respects the protocol's collateral requirements. ## Reference Scripts CIP-33 reference scripts allow you to store a script in a UTxO and reference it in transactions, reducing transaction size and fees. ### Using a Reference Script When spending a script UTxO, you can reference a script stored in another UTxO instead of including it in the transaction: ```scala copy // UTxO containing the reference script val refScriptUtxo: Utxo = // ... UTxO with scriptRef field // UTxO locked by the script val scriptUtxo: Utxo = // ... UTxO to spend TxBuilder(env) .references(refScriptUtxo) // Add as reference input .spend(scriptUtxo, redeemer) // Script resolved from reference .collaterals(collateralUtxo) .payTo(recipient, scriptUtxo.output.value) .build(changeTo = changeAddress) ``` ### Creating Outputs with Reference Scripts You can attach a script to an output for others to reference: ```scala copy import scalus.cardano.ledger.{Output, Script} val outputWithScript = Output( address = scriptAddress, value = Value.ada(10), datumOption = None, scriptRef = Some(Script.PlutusV3(myScript)) // Attach reference script ) TxBuilder(env) .spend(utxo) .output(outputWithScript) .build(changeTo = changeAddress) ``` Reference scripts are particularly useful for complex validators, as they significantly reduce transaction fees by not including the full script in each transaction. ## Unified Spend API with Script Witness For consistency with other script operations (minting, staking, governance), you can use the unified spend API with explicit script witnesses using factory methods: ```scala copy import scalus.cardano.txbuilder.ThreeArgumentPlutusScriptWitness.* import scalus.cardano.txbuilder.Datum.DatumInlined // With attached script and immediate redeemer val tx = TxBuilder(env) .spend(scriptUtxo, attached(validatorScript, redeemer, DatumInlined)) .collaterals(collateralUtxo) .payTo(recipient, scriptUtxo.output.value) .build(changeTo = changeAddress) // With reference script and immediate redeemer val tx = TxBuilder(env) .references(refScriptUtxo) .spend(scriptUtxo, reference(redeemer, DatumInlined)) .collaterals(collateralUtxo) .payTo(recipient, scriptUtxo.output.value) .build(changeTo = changeAddress) // With delayed redeemer (computed from final transaction) val tx = TxBuilder(env) .spend(scriptUtxo, attached(validatorScript, tx => computeRedeemer(tx), DatumInlined)) .collaterals(collateralUtxo) .payTo(recipient, scriptUtxo.output.value) .build(changeTo = changeAddress) ``` ### Factory Method Reference The `ThreeArgumentPlutusScriptWitness` companion object provides these factory methods: | Method | Description | |--------|-------------| | `attached(script, redeemer, datum)` | Attach script with immediate redeemer | | `attached(script, redeemerBuilder, datum)` | Attach script with delayed redeemer | | `reference(redeemer, datum)` | Use reference script with immediate redeemer | | `reference(redeemerBuilder, datum)` | Use reference script with delayed redeemer | The `datum` parameter specifies how the datum is provided: - `DatumInlined` - The datum is stored inline in the UTxO (most common) - `DatumValue(data)` - Provide the datum value explicitly (for UTxOs with datum hashes) The factory methods automatically convert your redeemer type to `Data` using the `ToData` typeclass. ## Next Steps - **[Minting & Burning](/docs/transactions/mint-burn-assets)** - Create and destroy native tokens - **[Validator Interactions](/docs/transactions/validator-interactions)** - Detailed guide on working with validators - **[Advanced Features](/docs/transactions/advanced-features)** - Learn about `complete()` and automatic input selection --- Source: https://scalus.org/docs/transactions/minting-burning-assets --- # Minting and Burning Native Tokens on Cardano This guide covers how to create (mint) and destroy (burn) native tokens on Cardano using TxBuilder. ## Understanding Cardano Minting Policies A minting policy is a Plutus script that controls when tokens can be created or destroyed. The policy ID (the script hash) becomes part of the token identifier. Minting creates tokens but doesn't automatically send them anywhere. You must explicitly add outputs to receive the newly minted tokens. ## Minting Native Tokens To mint tokens, use the `mint()` method with your minting policy script: ```scala copy import scalus.uplc.builtin.ByteString.hex import scalus.cardano.ledger.{AssetName, PolicyId} val mintingPolicy: Script.PlutusV3 = // ... your minting policy script val policyId = mintingPolicy.scriptHash val assetName = AssetName(hex"4d795368696e79546f6b656e") // "MyShinyToken" in hex val redeemer = MintRedeemer(...) val assets = Map( assetName -> 1000L // Mint 1000 tokens ) TxBuilder(env) .spend(utxo) .mint(mintingPolicy, assets, redeemer) .payTo(recipient, Value.asset(policyId, assetName, 1000L, Coin.ada(2))) .build(changeTo = changeAddress) ``` This mints 1000 tokens and sends them to the recipient along with 2 ADA. ## Minting Multiple Native Token Types Mint different tokens under the same policy: ```scala copy val tokenA = AssetName(hex"546f6b656e41") // "TokenA" val tokenB = AssetName(hex"546f6b656e42") // "TokenB" val assets = SortedMap( tokenA -> 500L, tokenB -> 300L ) TxBuilder(env) .spend(utxo) .mint(mintingPolicy, assets, redeemer) .payTo( recipient, Value( Coin.ada(2), MultiAsset(SortedMap(policyId -> assets)) ) ) .build(changeTo = changeAddress) ``` ## Burning Native Tokens To burn tokens, use negative amounts: ```scala copy val assets = Map( assetName -> -100L // Burn 100 tokens ) TxBuilder(env) .spend(utxoWithTokens) // Must contain the tokens being burned .mint(mintingPolicy, assets, redeemer) .payTo(recipient, Value.ada(5)) .build(changeTo = changeAddress) ``` The tokens are removed from circulation and the UTxO containing them is consumed. ## Using Reference Scripts If your minting policy is stored as a reference script, use `mint()` with the policy ID: ```scala copy val policyId = mintingPolicy.scriptHash TxBuilder(env) .spend(utxo) .references(mintingPolicyReferenceUtxo) .mint(policyId, assets, redeemer) .payTo(recipient, Value.asset(policyId, assetName, 1000L, Coin.ada(2))) .build(changeTo = changeAddress) ``` This avoids including the script in the transaction, reducing transaction size. ## Minting with Required Signers If your minting policy requires specific signatures: ```scala copy val assets = Map(assetName -> 1000L) TxBuilder(env) .spend(utxo) .mint(mintingPolicy, assets, redeemer) .requireSignature(pubKeyHash) .payTo(recipient, Value.asset(mintingPolicy.scriptHash, assetName, 1000L, Coin.ada(2))) .build(changeTo = changeAddress) .sign(signer) // Must provide required signature ``` ## How to Mint NFTs on Cardano Mint a unique NFT (1 token, typically with amount = 1): ```scala copy val nftName = AssetName(hex"4d794e4654") // "MyNFT" val nftAssets = Map(nftName -> 1L) TxBuilder(env) .spend(utxo) .mint(mintingPolicy, nftAssets, redeemer) .payTo( recipient, Value.asset(mintingPolicy.scriptHash, nftName, 1L, Coin.ada(2)) ) .build(changeTo = changeAddress) ``` The minting policy typically ensures uniqueness by checking for a specific UTxO or enforcing a one-time mint. ## Minting and Burning in One Transaction You can mint and burn in the same transaction: ```scala copy val assets = Map( tokenA -> 500L, // Mint 500 of tokenA tokenB -> -100L // Burn 100 of tokenB ) TxBuilder(env) .spend(utxoWithTokenB) // Must contain tokenB for burning .mint(mintingPolicy, assets, redeemer) .payTo( recipient, Value.asset(mintingPolicy.scriptHash, tokenA, 500L, Coin.ada(2)) ) .build(changeTo = changeAddress) ``` ## Multiple Policies Mint tokens under different policies by chaining multiple `mint()` calls: ```scala copy TxBuilder(env) .spend(utxo) .mint(policy1, assets1, redeemer1) .mint(policy2, assets2, redeemer2) .payTo(recipient, combinedValue) .build(changeTo = changeAddress) ``` Each policy is evaluated independently with its own redeemer. ## Delayed Redeemers for Minting When the redeemer depends on the final transaction structure (e.g., for self-referential scripts), use a delayed redeemer: ```scala copy TxBuilder(env) .spend(utxo) .mint(mintingPolicy, assets, (tx: Transaction) => computeRedeemer(tx)) .payTo(recipient, Value.asset(policyId, assetName, 1000L, Coin.ada(2))) .build(changeTo = changeAddress) ``` The redeemer function receives the assembled transaction and computes the appropriate redeemer data. ## Unified Mint API with Script Witness For consistency with other script operations (staking, governance), you can use the unified `mint()` API with explicit script witnesses: ```scala copy import scalus.cardano.txbuilder.TwoArgumentPlutusScriptWitness.* // With attached script val tx = TxBuilder(env) .spend(utxo) .collaterals(collateralUtxo) .mint(policyId, assets, attached(mintingPolicy, redeemer)) .payTo(recipient, Value.asset(policyId, assetName, 1000L, Coin.ada(2))) .build(changeTo = changeAddress) // With reference script val tx = TxBuilder(env) .spend(utxo) .collaterals(collateralUtxo) .references(mintingPolicyRefUtxo) .mint(policyId, assets, reference(redeemer)) .payTo(recipient, Value.asset(policyId, assetName, 1000L, Coin.ada(2))) .build(changeTo = changeAddress) // With delayed redeemer val tx = TxBuilder(env) .spend(utxo) .collaterals(collateralUtxo) .mint(policyId, assets, attached(mintingPolicy, tx => computeRedeemer(tx))) .payTo(recipient, Value.asset(policyId, assetName, 1000L, Coin.ada(2))) .build(changeTo = changeAddress) ``` The unified API always uses `policyId` as the first parameter, with the witness determining whether the script is attached or referenced. ### Native Script Minting For native script minting policies: ```scala copy import scalus.cardano.txbuilder.NativeScriptWitness.* val tx = TxBuilder(env) .spend(utxo) .mint(policyId, assets, attached(nativeScript)) .payTo(recipient, Value.asset(policyId, assetName, 1000L, Coin.ada(2))) .build(changeTo = changeAddress) ``` ## Next Steps - **[Validator Interactions](/docs/transactions/validator-interactions)** - Work with spending validators and minting policies - **[Advanced Features](/docs/transactions/advanced-features)** - Learn about reference scripts and advanced patterns --- Source: https://scalus.org/docs/transactions/staking-rewards --- # Cardano Staking: Delegate to Stake Pool, Withdraw Rewards Scalus TxBuilder provides a type-safe API for managing Cardano staking operations. This guide covers the four core staking operations: registering stake keys, delegating to pools, withdrawing rewards, and deregistering stake keys. Staking operations require interaction with Cardano's stake address system. All operations support both Byron-era and Conway-era protocol parameters. ## Registering Stake Keys Registering a stake key is the first step to participate in Cardano staking. This operation requires a **2 ADA deposit** that is refundable when you deregister the stake key. ```scala copy val tx = TxBuilder(env) .registerStake(stakeAddress) .complete(provider, sponsorAddress) ``` The deposit amount is automatically taken from protocol parameters (`env.protocolParams.stakeAddressDeposit`). The 2 ADA deposit is held by the protocol and will be returned when you deregister your stake key. ## Delegating to Stake Pools After registering a stake key, you can delegate it to a stake pool using the pool's unique Pool ID. Your delegated ADA helps secure the network and earns staking rewards. ```scala copy val poolId = PoolKeyHash.fromHex("pool1...") val tx = TxBuilder(env) .delegateTo(stakeAddress, poolId) .complete(provider, sponsorAddress) ``` ## Registering and Delegating in One Transaction For efficiency, you can combine registration and delegation into a single transaction: ```scala copy val poolId = PoolKeyHash.fromHex("pool1...") val tx = TxBuilder(env) .stakeAndDelegate(stakeAddress, poolId) .complete(provider, sponsorAddress) ``` The deposit is automatically taken from protocol parameters. Combining operations saves transaction fees and reduces the number of blockchain interactions required. ## Withdrawing Staking Rewards Claim your accumulated staking rewards to your wallet. ```scala copy val tx = TxBuilder(env) .withdrawRewards(stakeAddress, rewardAmount) .complete(provider, sponsorAddress) ``` **Post-Vasil Era**: Stake rewards must be withdrawn fully. Partial withdrawals are not supported. **Post-Chang Era**: Rewards are only withdrawable if you have delegated your voting power to a DRep (Delegated Representative). See the [Governance](/docs/transactions/governance) guide for more information. ## Deregistering Stake Keys Deregistering a stake key returns your initial 2 ADA deposit and removes your stake key from the blockchain. ```scala copy val tx = TxBuilder(env) .deregisterStake(stakeAddress) .complete(provider, sponsorAddress) ``` With explicit refund: ```scala copy val tx = TxBuilder(env) .deregisterStake(stakeAddress, Coin.ada(2)) .complete(provider, sponsorAddress) ``` ## Working with Stake Addresses ### Creating Stake Addresses from Key Hash To perform staking operations, you need a properly formatted stake address. Here's how to create one from a stake key hash: ```scala copy import scalus.cardano.address.{StakeAddress, StakePayload} val stakeAddress = StakeAddress( Network.Mainnet, StakePayload.Stake(stakeKeyHash) ) ``` ### Extracting Stake Address from Payment Address You can extract the stake address from a Shelley payment address: ```scala copy val stakeAddress = shelleyAddress.delegation match { case ShelleyDelegationPart.Key(keyHash) => StakeAddress(shelleyAddress.network, StakePayload.Stake(keyHash)) case _ => throw new Exception("No stake key") } ``` ## Script-Based Staking Operations Scalus supports script-based stake credentials (post-Conway). Use the `ScriptWitness` factory methods to authorize operations from script-controlled stake addresses. ### Creating Script Stake Addresses ```scala copy import scalus.cardano.address.{StakeAddress, StakePayload} // Create a stake address controlled by a script val scriptStakeAddress = StakeAddress( Network.Mainnet, StakePayload.Script(stakingScript.scriptHash) ) ``` ### Withdrawing Rewards from Script Stake Address ```scala copy import scalus.cardano.txbuilder.TwoArgumentPlutusScriptWitness.* // With attached script (included in transaction) val tx = TxBuilder(env) .spend(utxo) .collaterals(collateralUtxo) .withdrawRewards(scriptStakeAddress, rewardAmount, attached(stakingScript, redeemer)) .complete(provider, sponsorAddress) // With reference script (script already deployed on-chain) val tx = TxBuilder(env) .spend(utxo) .collaterals(collateralUtxo) .references(scriptRefUtxo) .withdrawRewards(scriptStakeAddress, rewardAmount, reference(redeemer)) .complete(provider, sponsorAddress) ``` ### Delegating Script Stake Address ```scala copy import scalus.cardano.txbuilder.TwoArgumentPlutusScriptWitness.* val tx = TxBuilder(env) .spend(utxo) .collaterals(collateralUtxo) .delegateTo(scriptStakeAddress, poolId, attached(stakingScript, redeemer)) .complete(provider, sponsorAddress) ``` ### Registering Script Stake Key ```scala copy import scalus.cardano.txbuilder.TwoArgumentPlutusScriptWitness.* val tx = TxBuilder(env) .spend(utxo) .collaterals(collateralUtxo) .registerStake(scriptStakeAddress, attached(stakingScript, redeemer)) .complete(provider, sponsorAddress) ``` ### Deregistering Script Stake Key ```scala copy import scalus.cardano.txbuilder.TwoArgumentPlutusScriptWitness.* val tx = TxBuilder(env) .spend(utxo) .collaterals(collateralUtxo) .deregisterStake(scriptStakeAddress, attached(stakingScript, redeemer)) .complete(provider, sponsorAddress) // With explicit refund amount val tx = TxBuilder(env) .spend(utxo) .collaterals(collateralUtxo) .deregisterStake(scriptStakeAddress, Some(Coin.ada(2)), attached(stakingScript, redeemer)) .complete(provider, sponsorAddress) ``` For delayed redeemers (computed from the final transaction), pass a lambda: `attached(script, tx => computeRedeemer(tx))` ### Native Script Staking You can also use native scripts for staking operations: ```scala copy import scalus.cardano.txbuilder.NativeScriptWitness.* val tx = TxBuilder(env) .spend(utxo) .withdrawRewards(nativeScriptStakeAddress, rewardAmount, attached(nativeScript)) .complete(provider, sponsorAddress) ``` ## See Also - [Governance](/docs/transactions/governance) - [Building Your First Transaction](/docs/transactions/building-first-transaction) --- Source: https://scalus.org/docs/transactions/governance --- # Cardano Governance: Delegate to DRep, Vote Scalus TxBuilder provides comprehensive support for Cardano's Conway era governance features. ADA holders can participate in on-chain governance by delegating voting power to Delegated Representatives (DReps) or by becoming DReps themselves. Governance operations are part of the Conway era upgrade. These features enable decentralized decision-making for the Cardano protocol. ## Delegating Voting Power to DReps Delegate your voting power to a Delegated Representative (DRep) to participate in Cardano governance decisions. ```scala copy val drep = DRep.KeyHash(drepKeyHash) val tx = TxBuilder(env) .delegateVoteToDRep(stakeAddress, drep) .complete(provider, sponsorAddress) ``` ### DRep Types Scalus supports multiple DRep delegation options: ```scala copy DRep.KeyHash(keyHash) // Specific DRep DRep.ScriptHash(scriptHash) // Script-based DRep DRep.Abstain // Abstain from voting DRep.NoConfidence // Vote no confidence ``` **Script-based DReps** enable programmatic voting aligned with community policies or institutional strategies, allowing automated governance participation. ## Registering Stake Keys and Delegating to DRep Combine stake registration and DRep delegation in a single transaction: ```scala copy val drep = DRep.KeyHash(drepKeyHash) val tx = TxBuilder(env) .registerAndDelegateVoteToDRep(stakeAddress, drep) .complete(provider, sponsorAddress) ``` This requires the standard 2 ADA stake registration deposit, which is refundable upon deregistration. ## Delegating to Stake Pool and DRep Delegate both your staking power (to a stake pool) and voting power (to a DRep) simultaneously: ```scala copy val poolId = PoolKeyHash.fromHex("pool1...") val drep = DRep.KeyHash(drepKeyHash) val tx = TxBuilder(env) .delegateToPoolAndDRep(stakeAddress, poolId, drep) .complete(provider, sponsorAddress) ``` **Post-Chang Era**: Delegating to a DRep is required to withdraw staking rewards. See [Staking & Rewards](/docs/transactions/staking-rewards) for more details. ## Registering and Delegating to Both Register your stake key and delegate to both a stake pool and DRep in one efficient transaction: ```scala copy val poolId = PoolKeyHash.fromHex("pool1...") val drep = DRep.KeyHash(drepKeyHash) val tx = TxBuilder(env) .registerAndDelegateToPoolAndDRep(stakeAddress, poolId, drep) .complete(provider, sponsorAddress) ``` Combining operations saves transaction fees and simplifies the user experience. ## Becoming a Delegated Representative (DRep) ### Registering as a DRep To become a DRep and accept voting delegation, you need to register with a credential: ```scala copy val drepCredential = Credential.KeyHash(yourKeyHash) val anchor = Some(Anchor( url = "https://example.com/drep-metadata.json", dataHash = metadataHash )) val tx = TxBuilder(env) .registerDRep(drepCredential, anchor) .complete(provider, sponsorAddress) ``` DRep registration requires a deposit (currently 500 ADA), which is automatically taken from protocol parameters. This deposit is refundable upon deregistration. The `anchor` parameter is optional but recommended. It should point to metadata describing your governance positions, credentials, and voting philosophy. ### Updating DRep Metadata Update your DRep metadata to reflect changes in your governance positions or credentials: ```scala copy val newAnchor = Some(Anchor( url = "https://example.com/updated-metadata.json", dataHash = newMetadataHash )) val tx = TxBuilder(env) .updateDRep(drepCredential, newAnchor) .complete(provider, sponsorAddress) ``` ### Unregistering as DRep Unregister your DRep credential and reclaim your 500 ADA deposit: ```scala copy val tx = TxBuilder(env) .unregisterDRep(drepCredential, Coin.ada(500)) .complete(provider, sponsorAddress) ``` ## Script-Based Governance Operations Scalus supports script-based stake credentials and DRep credentials for governance operations. Use the `ScriptWitness` factory methods to authorize operations from script-controlled addresses. ### Delegating Voting Power from Script Stake Address ```scala copy import scalus.cardano.txbuilder.TwoArgumentPlutusScriptWitness.* // With attached script val tx = TxBuilder(env) .spend(utxo) .collaterals(collateralUtxo) .delegateVoteToDRep(scriptStakeAddress, drep, attached(stakingScript, redeemer)) .complete(provider, sponsorAddress) // With reference script val tx = TxBuilder(env) .spend(utxo) .collaterals(collateralUtxo) .references(scriptRefUtxo) .delegateVoteToDRep(scriptStakeAddress, drep, reference(redeemer)) .complete(provider, sponsorAddress) ``` ### Combined Registration and Delegation with Script ```scala copy import scalus.cardano.txbuilder.TwoArgumentPlutusScriptWitness.* // Register stake and delegate to DRep val tx = TxBuilder(env) .spend(utxo) .collaterals(collateralUtxo) .registerAndDelegateVoteToDRep(scriptStakeAddress, drep, attached(stakingScript, redeemer)) .complete(provider, sponsorAddress) // Delegate to both pool and DRep val tx = TxBuilder(env) .spend(utxo) .collaterals(collateralUtxo) .delegateToPoolAndDRep(scriptStakeAddress, poolId, drep, attached(stakingScript, redeemer)) .complete(provider, sponsorAddress) // Register and delegate to both pool and DRep val tx = TxBuilder(env) .spend(utxo) .collaterals(collateralUtxo) .registerAndDelegateToPoolAndDRep(scriptStakeAddress, poolId, drep, attached(stakingScript, redeemer)) .complete(provider, sponsorAddress) ``` ### Script-Based DRep Operations Script-based DReps enable programmatic, automated governance participation: ```scala copy import scalus.cardano.txbuilder.TwoArgumentPlutusScriptWitness.* val scriptDrepCredential = Credential.ScriptHash(drepScript.scriptHash) // Register script-based DRep val tx = TxBuilder(env) .spend(utxo) .collaterals(collateralUtxo) .registerDRep(scriptDrepCredential, anchor, attached(drepScript, redeemer)) .complete(provider, sponsorAddress) // Update script-based DRep metadata val tx = TxBuilder(env) .spend(utxo) .collaterals(collateralUtxo) .updateDRep(scriptDrepCredential, newAnchor, attached(drepScript, redeemer)) .complete(provider, sponsorAddress) // Unregister script-based DRep val tx = TxBuilder(env) .spend(utxo) .collaterals(collateralUtxo) .unregisterDRep(scriptDrepCredential, Coin.ada(500), attached(drepScript, redeemer)) .complete(provider, sponsorAddress) ``` For delayed redeemers (computed from the final transaction), pass a lambda: `attached(script, tx => computeRedeemer(tx))` ## See Also - [Staking & Rewards](/docs/transactions/staking-rewards) - [Building Your First Transaction](/docs/transactions/building-first-transaction) --- Source: https://scalus.org/docs/transactions/validator-interactions --- # Validator Interactions This guide demonstrates how to build transactions that interact with Plutus validators, using a real-world example: a Hash Time Locked Contract (HTLC). ## HTLC Overview An HTLC is a smart contract that locks funds until either: 1. The receiver reveals a secret (preimage) before a timeout, or 2. The timeout expires and the original sender can reclaim the funds This pattern is useful for atomic swaps, payment channels, and escrow services. ## Sharing Code Between Validator and Transaction Builder One of TxBuilder's key advantages is code reuse. The same data types used in your validator can be used when building transactions: ```scala copy import scalus.uplc.builtin.{ByteString, ToData} // Shared data types used in both validator and transaction building case class Config( committer: PubKeyHash, receiver: PubKeyHash, image: ByteString, // Hash of the secret timeout: Long // Deadline (slot number) ) derives ToData enum Action derives ToData { case Reveal(preimage: ByteString) case Timeout } ``` These types are used in the validator logic and when constructing transactions, ensuring type safety across your entire application. ## Lock Transaction Lock funds in the HTLC by sending them to the script address with the configuration as datum: ```scala copy def lock( utxos: Utxos, value: Value, changeAddress: Address, committer: AddrKeyHash, receiver: AddrKeyHash, image: ByteString, timeout: Long ): Transaction = { val datum = Config( PubKeyHash(committer), PubKeyHash(receiver), image, timeout ) TxBuilder(env, evaluator) .spend(utxos) .payTo(scriptAddress, value, datum) // Lock funds with config .build(changeTo = changeAddress) .sign(signer) .transaction } ``` The funds are now locked at the script address with the HTLC configuration stored in the datum. ## Reveal Transaction The receiver can claim the funds by revealing the preimage: ```scala copy def reveal( utxos: Utxos, collateralUtxos: Utxos, lockedUtxo: Utxo, payeeAddress: Address, changeAddress: Address, preimage: ByteString, receiverPkh: AddrKeyHash, time: Long ): Transaction = { val redeemer = Action.Reveal(preimage) TxBuilder(env, evaluator) .spend(utxos) .collaterals(collateralUtxos) .spend(lockedUtxo, redeemer, script, Set(receiverPkh)) // Spend with redeemer .payTo(payeeAddress, lockedUtxo.output.value) .validFrom(java.time.Instant.ofEpochMilli(time)) .build(changeTo = changeAddress) .sign(signer) .transaction } ``` Key points: - The redeemer contains the preimage that hashes to the image in the datum - The receiver's signature is required (`Set(receiverPkh)`) - The validator checks that the hash matches and the receiver signed ## Timeout Transaction After the timeout, the committer can reclaim the funds: ```scala copy def timeout( utxos: Utxos, collateralUtxos: Utxos, lockedUtxo: Utxo, payeeAddress: Address, changeAddress: Address, committerPkh: AddrKeyHash, time: Long ): Transaction = { val redeemer = Action.Timeout TxBuilder(env, evaluator) .spend(utxos) .collaterals(collateralUtxos) .spend(lockedUtxo, redeemer, script, Set(committerPkh)) .payTo(payeeAddress, lockedUtxo.output.value) .validFrom(java.time.Instant.ofEpochMilli(time)) // Must be after timeout .build(changeTo = changeAddress) .sign(signer) .transaction } ``` Key points: - The validator checks that the timeout has passed - The committer's signature is required - The `validFrom` ensures the transaction is only valid after the timeout ## Script Evaluation TxBuilder automatically evaluates the validator during `build()`: ```scala copy val evaluator = PlutusScriptEvaluator( env, EvaluatorMode.EvaluateAndComputeCost ) TxBuilder(env, evaluator) .spend(lockedUtxo, redeemer, script) // ... rest of transaction .build(changeTo = changeAddress) // Validator is evaluated here ``` If the validator fails (wrong preimage, wrong signer, wrong time, etc.), `build()` throws an exception with the error message from the validator. Script evaluation happens locally during transaction building, allowing you to catch errors before submitting to the network. ## Type-Safe Data Serialization Because both the validator and transaction builder use the same Scala types, serialization is automatic and type-safe: ```scala copy // In validator val config = datum.to[Config] // Deserialize from Data // In transaction builder val datum = Config(...).toData // Serialize to Data ``` This eliminates a whole class of serialization errors common in other ecosystems. ## Testing the Full Flow You can test the complete flow in a single test: ```scala copy test("HTLC full flow") { val preimage = hex"deadbeef" val image = blake2b_256(preimage) val timeout = currentSlot + 100 // 1. Lock val lockTx = lock(utxos, Value.ada(100), changeAddr, committer, receiver, image, timeout) // 2. Extract locked UTxO val lockedUtxo = findLockedUtxo(lockTx, scriptAddress) // 3. Reveal before timeout val revealTx = reveal( utxos, collaterals, lockedUtxo, receiverAddr, changeAddr, preimage, receiver, currentSlot + 50 ) assert(revealTx.body.value.inputs.contains(lockedUtxo.input)) } ``` ## Next Steps - **[Advanced Features](/docs/transactions/advanced-features)** - Learn about `complete()`, reference scripts, and more - **[Smart Contract Examples](https://github.com/scalus3/scalus/tree/master/scalus-examples)** - More validator examples --- Source: https://scalus.org/docs/transactions/advanced-features --- # Advanced Features ## Testing Validators with Custom Evaluators One of the challenges we encounter when testing Cardano smart contracts is testing negative cases-scenarios where your validator *should* reject a transaction. TxBuilder's enables the developer to do it via custom evaluators. ### The Problem with Negative Testing When building a transaction with scripts, TxBuilder evaluates them during `build()` to: 1. Verify the script passes validation with the provided redeemer 2. Calculate execution costs to fill out the `Redeemer` This creates a dilemma for negative testing: ```scala copy // A redeemer that is known to fail with our validator val invalidRedeemer = Data(...) val tx = TxBuilder(env) .spend(scriptUtxo, invalidRedeemer, validator) .payTo(attacker, scriptUtxo.output.value) .build(changeTo = changeAddress) // Throws `PlutusScriptEvaluationError` by default ``` The validator correctly rejects the invalid redeemer during local evaluation, but this means `build()` throws an exception and you never get a transaction to test. You can't submit it to verify the on-chain behavior. ### The Solution: Constant Budget Evaluator ```scala copy // Use constant budget evaluator for negative tests val tx = TxBuilder.withConstMaxBudgetEvaluator(env) .spend(scriptUtxo, invalidRedeemer, validator) .payTo(attacker, scriptUtxo.output.value) .build(changeTo = changeAddress) // The previous exception is gone, as the scripts were never actually evaluated .sign(signer) .transaction // Now you can submit and verify it fails on-chain import scalus.utils.await import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.duration.* provider.submit(tx).await(30.seconds) match { case Left(error) => assert(error.message.contains("MyValidator logic error")) case Right(_) => fail("Transaction should have been rejected!") } ``` This technique bridges unit testing (validator in isolation) and integration testing (full transaction flow including on-chain rejection). ### Testing Pattern: Positive and Negative Cases Here's a complete testing pattern using both evaluators: ```scala copy import scalus.utils.await import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.duration.* class ValidatorIntegrationTest extends AnyFunSuite { val defaultEvaluator = PlutusScriptEvaluator( cardanoInfo, EvaluatorMode.EvaluateAndComputeCost ) test("valid redeemer is accepted on-chain") { val validRedeemer = Action.Reveal(correctPreimage) // Use default evaluator - should pass locally val tx = TxBuilder(env, defaultEvaluator) .spend(collateralUtxos) .spend(scriptUtxo, validRedeemer, validator, Set(receiverPkh)) .payTo(receiver, scriptUtxo.output.value) .build(changeTo = changeAddress) .sign(signer) .transaction // Should succeed on-chain provider.submit(tx).await(30.seconds) match { case Right(txHash) => println(s"Transaction accepted: ${txHash.toHex}") case Left(error) => fail(s"Valid transaction was rejected: $error") } } test("invalid redeemer is rejected on-chain") { val invalidRedeemer = Action.Reveal(wrongPreimage) // Use constant budget evaluator - bypasses local validation val tx = TxBuilder.withConstMaxBudgetEvaluator(env) .spend(collateralUtxos) .spend(scriptUtxo, invalidRedeemer, validator, Set(receiverPkh)) .payTo(attacker, scriptUtxo.output.value) .build(changeTo = changeAddress) .sign(signer) .transaction // Should fail on-chain provider.submit(tx).await(30.seconds) match { case Left(error) => assert(error.message.contains("preimage hash mismatch")) case Right(txHash) => fail(s"Invalid transaction was accepted: ${txHash.toHex}") } } } ``` ## Transaction Chaining Transaction chaining allows building multiple transactions where each uses outputs from previous ones without waiting for on-chain confirmation. This is useful for complex workflows that require multiple sequential transactions. ### Using Transaction.utxos Every `Transaction` has a `utxos` method that returns the UTXOs it would create: ```scala copy // Build the first transaction val lockTx = TxBuilder(env) .payTo(scriptAddress, Value.ada(10), lockDatum) .complete(emulator, sponsorAddress) .sign(signer) .transaction // Get UTXOs from lockTx directly (no provider query needed) val scriptUtxo = Utxo(lockTx.utxos.find(_._2.address == scriptAddress).get) // Build second transaction using the output from first val unlockTx = TxBuilder(env) .spend(scriptUtxo, unlockRedeemer, validator) .payTo(recipientAddress, scriptUtxo.output.value) .complete(emulator, sponsorAddress) .sign(signer) .transaction // Submit both transactions emulator.submit(lockTx) emulator.submit(unlockTx) ``` ### Emulator for Testing The `Emulator` supports transaction chaining naturally by tracking UTXOs across submissions: ```scala copy val emulator = Emulator(initialUtxos) // Submit first transaction emulator.submit(lockTx) // Can also query UTXOs from emulator val utxo = emulator.findUtxo( address = scriptAddress, transactionId = Some(lockTx.id) ).toOption.get // Submit second transaction emulator.submit(unlockTx) ``` The `Transaction.utxos` method is particularly useful when you need to reference specific outputs immediately after building a transaction, without querying a provider. ## Draft Transactions for ScriptContext Testing The `draft` method assembles a transaction without balancing or fee calculation. This is useful for testing validators by deriving a `ScriptContext` from the transaction structure. ### Why Use `draft`? When testing validators, you often need to: 1. Create a transaction with specific inputs and outputs 2. Derive the `ScriptContext` that would be passed to your validator 3. Run the validator against that context The normal `build()` method requires fee calculation and balancing, which adds complexity when you just want to test validator logic. The `draft` method skips these steps. ### Basic Usage ```scala copy import scalus.cardano.txbuilder.{txBuilder, RedeemerPurpose} import scalus.cardano.txbuilder.RedeemerPurpose.ForSpend given CardanoInfo = TestUtil.testEnvironment val tx = txBuilder .spend(lockedUtxo, redeemer, validatorScript, Set(signerPkh)) .payTo(recipient, Value.ada(10)) .validTo(deadline) .draft // No balancing, no fee calculation // Derive ScriptContext for the spend val scriptContext = tx.getScriptContextV3(utxos, ForSpend(lockedUtxo.input)) // Test your validator validator.code(scriptContext.toData) ``` ### Complete Testing Example ```scala copy import scalus.uplc.builtin.Data.toData import scalus.cardano.txbuilder.{txBuilder, RedeemerPurpose} import scalus.cardano.txbuilder.RedeemerPurpose.ForSpend import scalus.testing.kit.{ScalusTest, EvalTestKit} class MyValidatorTest extends AnyFunSuite, ScalusTest, EvalTestKit { given CardanoInfo = TestUtil.testEnvironment test("validator accepts valid action") { // Setup: create UTXOs and locked output val utxos: Utxos = ... val lockedUtxo: Utxo = ... val validRedeemer = MyAction.Unlock(secretValue) // Build draft transaction val tx = txBuilder .spend(lockedUtxo, validRedeemer, myValidator.script, Set(ownerPkh)) .payTo(owner, lockedUtxo.output.value) .validTo(deadline) .draft // Derive ScriptContext val sc = tx.getScriptContextV3(utxos, ForSpend(lockedUtxo.input)) // Test validator - should succeed without throwing myValidator.code(sc.toData) // Or test with the compiled program myValidator.program $ sc.toData } } ``` The `draft` method is designed for testing. For production transactions, use `build()` or `complete()` which handle fee calculation and balancing. --- Source: https://scalus.org/docs/transactions --- # Building Cardano Transactions Scalus provides a fluent API for constructing Cardano transactions — payments, token minting, staking, governance, and smart contract interactions. ## What You Can Build - **[Payments](/docs/transactions/payment-methods)** — Send ADA and native tokens - **[Spending UTxOs](/docs/transactions/spending-utxos)** — Spending UTxOs from wallet and script addresses - **[Token Minting](/docs/transactions/minting-burning-assets)** — Create and burn native assets - **[Staking](/docs/transactions/staking-rewards)** — Delegate stake and withdraw rewards - **[Governance](/docs/transactions/governance)** — DRep registration and voting - **[Smart Contract Interactions](/docs/transactions/spending-utxos)** — Spend script UTxOs with Plutus validators ## Transaction Builder TransactionBuilder (`TxBuilder`) handles the complexity of Cardano transactions — UTxO selection, fee calculation, script evaluation, and balancing: ```scala copy val tx = TxBuilder(env) .payTo(recipient, Value.ada(10)) .complete(provider, myAddress) .await() .sign(signer) .transaction ``` Since validators are written in Scala, you can share code between on-chain and off-chain: - **Code Reuse** — Share data types and validation logic - **Type Safety** — Scala's type system across your entire stack - **Integrated Testing** — Test validators and transactions together ## Quick Example ```scala copy val tx = TxBuilder(env) .spend(utxo) // Add UTxO as input .payTo(recipientAddress, Value.ada(10)) // Send 10 ADA to recipient .build(changeTo = changeAddress) // Finalize: calculate fees, handle change .sign(signer) // Add signature .transaction // Get the final transaction ``` TxBuilder is purely functional — each method returns a new immutable instance. Only `build()` can throw while evaluating scripts and balancing. ## Automatic Completion The `complete()` method handles everything that can be determined programmatically: ```scala copy TxBuilder(env) .payTo(recipient, Value.ada(10)) .complete(provider, sponsorAddress) // Automatically complete using funds from sponsorAddress ``` It automatically: - Selects UTxOs from the sponsor address to cover outputs and fees - Adds collateral inputs for script execution - Calculates fees based on transaction size and execution costs - Creates change outputs and iteratively rebalances ## Script Evaluation TxBuilder evaluates Plutus scripts during build to catch errors before submission: ```scala copy TxBuilder(env, evaluator) .spend(scriptUtxo, redeemer, script) .payTo(recipient, scriptUtxo.output.value) .build(changeTo = changeAddress) // Scripts evaluated here ``` ## Next Steps - **[First Transaction](/docs/transactions/building-first-transaction)** — Step-by-step guide - **[First Contract Transaction](/docs/transactions/first-contract-transaction)** — Lock and spend from a script address - **[Payment Methods](/docs/transactions/payment-methods)** — Send ADA and tokens - **[Spending UTxOs](/docs/transactions/spending-utxos)** — Input selection and script UTxOs - **[Minting & Burning](/docs/transactions/minting-burning-assets)** — Create and destroy native tokens - **[Staking & Rewards](/docs/transactions/staking-rewards)** — Delegation and reward withdrawal - **[Governance](/docs/transactions/governance)** — DRep registration and voting ## Related - [Smart Contracts](/docs/smart-contracts) — Write validators to use in transactions - [Testing with Emulator](/docs/testing/emulator) — Test transactions locally - [DApp Starter Tutorial](/docs/dapp-development/dapp-starter-tutorial) — Complete example with transactions --- Source: https://scalus.org/docs/testing/tdd-atdd-workflow --- # TDD & ATDD Workflow AI coding assistants have made writing code dramatically faster. But the bottleneck was never typing speed — it was knowing what to build and verifying it's correct. When AI writes most of the code, tests shift from verification tool to **specification language**: the test defines intent, the implementation is disposable. This applies doubly to smart contracts, where bugs are expensive and irreversible. On Cardano, a deployed validator cannot be patched — if it's wrong, funds may be locked or stolen. Tests are your last line of defense before mainnet. The workflow below works whether you write code manually or use AI assistants. The key insight: **invest time in test quality, not implementation quality**. A precise test catches bad implementations automatically — whether written by a human or generated by AI. ## TDD: Test-Driven Development Write a failing test first, then make it pass. For smart contracts, this means defining validator behavior before writing validator logic. ### Write a failing test Define what your validator should accept and reject: ```scala class AuctionValidatorTest extends AnyFunSuite with ScalusTest { test("reject bid below current highest") { val state = AuctionState(currentBid = 100_000_000, deadline = 1000) val lowBid = 50_000_000 val result = Try(evaluateValidator( datum = state.toData, redeemer = BidRedeemer(lowBid).toData, context = buildContext(bidAmount = lowBid, slot = 500) )) assert(result.isFailure, "Validator should reject bids below current highest") } test("accept bid above current highest") { val state = AuctionState(currentBid = 100_000_000, deadline = 1000) val highBid = 150_000_000 val result = Try(evaluateValidator( datum = state.toData, redeemer = BidRedeemer(highBid).toData, context = buildContext(bidAmount = highBid, slot = 500) )) assert(result.isSuccess, "Validator should accept bids above current highest") } } ``` These tests fail because the validator doesn't exist yet. That's the point — the test is the specification. ### Implement the validator Now write (or let AI generate) the validator that makes the tests pass: ```scala @Compile object AuctionValidator extends Validator: inline override def spend( datum: Option[AuctionDatum], redeemer: AuctionRedeemer, ctx: ScriptContext ): Unit = val d = datum.getOrFail("no datum") redeemer match case BidRedeemer(amount) => require(amount > d.currentBid, "Bid must exceed current highest") require(ctx.txInfo.validRange.from < d.deadline, "Auction has ended") // ... output checks ``` ### Run tests, iterate If tests pass, you're done. If they fail, fix the implementation — not the tests. The tests encode your intent; the implementation serves the tests. ### Property-based TDD For stronger guarantees, express properties instead of individual examples: ```scala test("any bid at or below current highest is rejected") { forAll(Gen.choose(0L, state.currentBid)) { lowBid => val result = Try(evaluateValidator( datum = state.toData, redeemer = BidRedeemer(lowBid).toData, context = buildContext(bidAmount = lowBid, slot = 500) )) assert(result.isFailure) } } ``` ScalaCheck generates hundreds of random values within the range, catching edge cases that hand-picked examples miss. See [Unit Testing](/docs/testing/unit-testing) for the full guide. ## ATDD: Acceptance Test-Driven Development TDD works at the validator level: does this function behave correctly? ATDD works at the **transaction level**: does this contract do what stakeholders need in a real Cardano transaction? With Scalus, ATDD means writing full transaction scenarios against the [Emulator](/docs/testing/emulator) before building the contract logic. ### Define acceptance criteria as scenarios Before writing any validator code, define what a complete interaction looks like: ```scala test("full auction lifecycle") { val emulator = Emulator.withAddresses(Seq(Alice.address, Bob.address)) // 1. Alice creates auction with 100 ADA starting bid val createTx = TxBuilder(emulator.cardanoInfo) .payToScript(auctionAddress, AuctionDatum(100_000_000, deadline).toData, Value.ada(100)) .complete(emulator, Alice.address).await() .sign(Alice.signer).transaction emulator.submit(createTx).await() // 2. Bob bids 150 ADA — should succeed val bidTx = TxBuilder(emulator.cardanoInfo) .spend(auctionUtxo, BidRedeemer(150_000_000).toData, auctionScript) .payToScript(auctionAddress, updatedDatum(150_000_000, Bob.pkh).toData, Value.ada(150)) .payTo(Alice.address, Value.ada(100)) // refund previous bidder .complete(emulator, Bob.address).await() .sign(Bob.signer).transaction emulator.submit(bidTx).await() // 3. Verify final state val finalUtxo = emulator.findUtxos(auctionAddress).head val finalDatum = AuctionDatum.fromData(finalUtxo.output.requireInlineDatum) assert(finalDatum.currentBid == 150_000_000) assert(finalDatum.topBidder == Bob.pkh) } ``` This scenario fails initially — the validator and transaction builder helpers don't exist yet. But it captures the full acceptance criteria: create, bid, refund, state transition. ### Implement to satisfy scenarios Build the validator, datum types, and transaction builders until the acceptance scenarios pass. Each passing scenario is a verified feature. ### Add boundary testing Once the happy path works, use [Boundary Testing](/docs/testing/boundary-testing) to automatically explore edge cases and attack vectors: ```scala test("auction resists attack variations") { val commands = ContractScalaCheckCommands(emulator, AuctionStep) { (reader, state) => Future.successful( Prop(state.currentBid >= 0) :| "bid non-negative" && Prop(state.topBidder != PubKeyHash.empty) :| "has bidder" ) } commands.property().check() } ``` The boundary testing toolkit generates steal attempts, corrupted datums, double satisfaction attacks, and boundary value variations — all derived from your contract's structure. ## The Workflow ``` ┌─────────────────────────────────────────────────┐ │ 1. Write acceptance test (ATDD) │ │ Full transaction scenario against Emulator │ │ → defines WHAT the contract should do │ └─────────────────┬───────────────────────────────┘ │ ┌─────────────────▼───────────────────────────────┐ │ 2. Write unit tests (TDD) │ │ Validator behavior for each redeemer action │ │ → defines HOW the validator should behave │ └─────────────────┬───────────────────────────────┘ │ ┌─────────────────▼───────────────────────────────┐ │ 3. Implement validator │ │ Write code (or let AI generate it) │ │ → iterate until all tests pass │ └─────────────────┬───────────────────────────────┘ │ ┌─────────────────▼───────────────────────────────┐ │ 4. Add boundary & attack testing │ │ Boundary testing explores edge cases │ │ → catches what you didn't think of │ └─────────────────┬───────────────────────────────┘ │ ┌─────────────────▼───────────────────────────────┐ │ 5. Deploy with confidence │ │ Tests → Emulator → Devnet → Testnet → Main │ └─────────────────────────────────────────────────┘ ``` ## Further Reading - [TDD in the AI Coding Era](https://lantr.io/blog/tdd-atdd-ai-coding-era/) — the full argument for why TDD matters more, not less, when AI writes your code - [Unit Testing](/docs/testing/unit-testing) — property-based testing with ScalaCheck - [Boundary Testing](/docs/testing/boundary-testing) — automated edge case and attack exploration - [Emulator](/docs/testing/emulator) — in-memory Cardano node for fast iteration - [Debugging](/docs/testing/debugging) — IDE debugging and logging for validators --- Source: https://scalus.org/docs/testing/unit-testing --- # Unit Testing ## Setup Mix `ScalusTest` into your ScalaTest suite: ```scala import org.scalatest.funsuite.AnyFunSuite import scalus.testing.kit.ScalusTest import scalus.testing.kit.Party.{Alice, Bob} class MyValidatorTest extends AnyFunSuite, ScalusTest { test("validator accepts valid input") { // ... } } ``` `ScalusTest` provides: - A default PlutusV3 VM (overridable via `plutusVM`) - Script context builders - Result checking helpers (`checkResult`, `assertScriptFail`) - `random[A]` for generating test data via ScalaCheck - `Arbitrary` instances for all Cardano types - SIR and Program evaluation extensions ## Parties Scalus provides a `Party` enum with pre-derived HD wallets for test participants: ```scala import scalus.testing.kit.Party import scalus.testing.kit.Party.{Alice, Bob, Charles, Eve} import scalus.cardano.address.Network val aliceAddr = Alice.address(Network.Mainnet) val alicePkh = Alice.addrKeyHash val signer = Alice.signer ``` Available parties: Alice, Bob, Charles, Dave, Eve, Faith, Grace, Hal, Ivan, Judy, Kevin, Laura, Mallory, Nick, Oracle, Peggy, Sybil, Trent, Victor, Wendy. Each party has: - `address(network)` — Shelley address for a given network - `addrKeyHash` — payment key hash - `signer` — `TransactionSigner` for signing transactions - `account` — full `HdAccount` (CIP-1852 HD wallet) ## Creating an Emulator The `Emulator` is an in-memory Cardano node for fast, deterministic testing. Fund participants with initial UTxOs: ```scala import scalus.cardano.node.Emulator import scalus.cardano.ledger.* import scalus.cardano.ledger.rules.PlutusScriptsTransactionMutator import scalus.testing.kit.TestUtil.genesisHash val emulator = Emulator( initialUtxos = Map( Input(genesisHash, 0) -> TransactionOutput.Babbage( address = Alice.address(Network.Mainnet), value = Value.lovelace(100_000_000L) ), Input(genesisHash, 1) -> TransactionOutput.Babbage( address = Bob.address(Network.Mainnet), value = Value.lovelace(100_000_000L) ) ), initialContext = Context.testMainnet(), mutators = Set(PlutusScriptsTransactionMutator) ) ``` Or use the convenience factory: ```scala val emulator = Emulator.withAddresses( Seq(Alice.address(Network.Mainnet), Bob.address(Network.Mainnet)), Value.lovelace(100_000_000L) ) ``` See [Emulator](/docs/testing/emulator) for the full guide. ## Testing with Transactions The most common pattern: build a transaction, submit it to the emulator, and verify the result. ### Happy Path ```scala test("receiver reveals preimage before timeout") { // Build and submit the lock transaction first val lockTx = txCreator.lock(/* ... */) emulator.submit(lockTx).await() // Build the reveal transaction val revealTx = txCreator.reveal( utxos = utxos, lockedUtxo = lockedUtxo, payeeAddress = Bob.address, sponsor = Bob.address, preimage = validPreimage, receiverPkh = Bob.addrKeyHash, validTo = timeout, signer = Bob.signer ) val result = emulator.submit(revealTx).await() assert(result.isRight) } ``` ### Testing Failures with assertScriptFail `assertScriptFail` verifies that a transaction fails with a specific error message from the validator's trace logs: ```scala test("reject reveal with wrong preimage") { assertScriptFail("Invalid receiver preimage") { txCreator.reveal( utxos = utxos, lockedUtxo = lockedUtxo, payeeAddress = Bob.address, sponsor = Bob.address, preimage = wrongPreimage, receiverPkh = Bob.addrKeyHash, validTo = timeout, signer = Bob.signer ) } } test("reject timeout before deadline") { assertScriptFail("Timeout not reached") { txCreator.timeout( utxos = utxos, lockedUtxo = lockedUtxo, payeeAddress = Alice.address, sponsor = Alice.address, signer = Alice.signer, validFrom = beforeTimeout // too early ) } } ``` `assertScriptFail` catches `TxBuilderException.BalancingException` (thrown during transaction building when script evaluation fails) and checks that the script logs contain the expected error substring. For `assertScriptFail` to show meaningful error messages, compile your validator with error traces enabled. Most validators do this via a `withErrorTraces` variant. ## Script Context Testing For lower-level testing, construct a `ScriptContext` directly and evaluate the validator program against it. ### Using TxBuilder.draft Build a transaction without submitting, extract the script context, and evaluate: ```scala import scalus.testing.kit.TestUtil.getScriptContextV3 import scalus.cardano.txbuilder.RedeemerPurpose.ForSpend test("validator budget for reveal") { val scriptCtx = TxBuilder(emulator.cardanoInfo) .spend(lockedUtxo, redeemer = Action.Reveal(validPreimage), script = contract.script) .requireSignature(Bob.addrKeyHash) .payTo(Bob.address, Value.ada(10)) .validTo(timeout) .draft // build without submitting .getScriptContextV3(utxos, ForSpend(lockedUtxo.input)) // extract context val result = contract(scriptCtx.toData).program.evaluateDebug assert(result.isSuccess) assert(result.budget == ExUnits(memory = 42970, steps = 16_307848)) } ``` ### Using makeSpendingScriptContext For quick tests that don't need a full transaction, build a minimal script context: ```scala test("donate fails after deadline") { val datum = CampaignDatum( totalSum = BigInt(0), goal = BigInt(10_000_000), recipient = recipientPkh, deadline = BigInt(1000), withdrawn = BigInt(0), donationPolicyId = donationPolicyId ) val context = ScriptContext( txInfo = TxInfo( inputs = List(TxInInfo(outRef = txOutRef, resolved = /* ... */)), outputs = List.Nil, mint = Value.zero, signatories = List.Nil, validRange = Interval.after(deadline + 1000), // after deadline id = random[TxId] ), redeemer = redeemer.toData, scriptInfo = ScriptInfo.SpendingScript(txOutRef, Option.None) ) val program = crowdfundingContract.program $ context.toData val result = program.evaluateDebug assert(result.isFailure) assert(result.logs.exists(_.contains("before deadline"))) } ``` ### Evaluating SIR and Programs `ScalusTest` adds extension methods for evaluation: ```scala // From SIR (intermediate representation) val result = mySIR.runScript(scriptContext) val result = mySIR.runScript(scriptContext, param = Some(paramData)) // From Program (compiled UPLC) val result = myProgram.runWithDebug(scriptContext) ``` Both return a `Result` — either `Result.Success(term, budget, costs, logs)` or `Result.Failure(exception, budget, costs, logs)`. ## Checking Results ### checkResult For structured result checking with optional budget verification: ```scala test("validator accepts valid bid") { val result = program.runWithDebug(scriptContext) checkResult(success, result) } test("validator rejects low bid") { val result = program.runWithDebug(scriptContext) checkResult(failure("Bid must exceed current highest"), result) } test("validator accepts with expected budget") { val result = program.runWithDebug(scriptContext) checkResult(success(ExUnits(memory = 314279, steps = 94_609535)), result) } ``` ### Direct Result Inspection ```scala val result = program.evaluateDebug // Success checks assert(result.isSuccess) assert(result.budget == ExUnits(memory = 314279, steps = 94_609535)) assert(result.budget.fee == Coin(3656)) // Failure checks assert(result.isFailure) assert(result.logs.exists(_.contains("expected error message"))) ``` ## Budget Testing Track execution budgets to catch regressions when the validator or compiler changes: ```scala test("budget: first bid") { val budget = TestCase( action = TestAction.Bid(bidAmount = 3_000_000L), expected = Expected.Success ).runWithBudget() assert(budget == ExUnits(memory = 314279, steps = 94_609535)) } test("budget: outbid with refund") { val budget = TestCase( action = TestAction.Outbid(newBidAmount = 5_000_000L), expected = Expected.Success ).runWithBudget() assert(budget == ExUnits(memory = 410558, steps = 122_052616)) } ``` `ExUnits` has a `.fee` method that computes the Cardano transaction fee from CPU and memory costs — useful for estimating real costs. Budget values are deterministic for a given validator and transaction. Pin exact values in tests to detect unintended changes from compiler upgrades or code refactoring. ## Random Data Generation `ScalusTest` provides `random[A]` for generating test data via ScalaCheck's `Arbitrary`: ```scala test("example with random data") { val txId = random[TxId] val txInfo = random[TxInfo] val address = random[Address] val value = random[Value] val pkh = random[PubKeyHash] } ``` Available `Arbitrary` instances cover all Cardano types: - **Plutus types:** `TxInfo`, `TxOut`, `TxInInfo`, `ScriptContext`, `Value`, `Address`, etc. - **Primitives:** `PubKeyHash`, `TxId`, `TxOutRef`, `POSIXTime`, `ValidatorHash`, etc. - **Custom types** with `derives FromData, ToData` For property-based testing with `forAll`, see [Property-Based Testing](/docs/testing/property-based-testing). ## TestUtil Helpers `TestUtil` provides mock data generators and script context extraction: ```scala import scalus.testing.kit.TestUtil // Mock hashes for test data val pkh = TestUtil.mockPubKeyHash(variation = 1) val scriptHash = TestUtil.mockScriptHash(variation = 1) val txOutRef = TestUtil.mockTxOutRef(variation = 1, idx = 0) // Genesis hash for emulator initial UTxOs val genesisHash = TestUtil.genesisHash // Extract script context from a built transaction val ctx = tx.getScriptContextV3(utxos, RedeemerPurpose.ForSpend(input)) val ctx = tx.getScriptContextV2(utxos, RedeemerPurpose.ForMint(policyId)) ``` ## Examples - [AuctionValidatorTest](https://github.com/scalus3/scalus/blob/master/scalus-examples/jvm/src/test/scala/scalus/examples/auction/AuctionValidatorTest.scala) — Unit tests and budget assertions for an auction validator - [HtlcTest](https://github.com/scalus3/scalus/blob/master/scalus-examples/jvm/src/test/scala/scalus/examples/htlc/HtlcTest.scala) — HTLC validator with assertScriptFail and budget tracking ## Related - [Property-Based Testing](/docs/testing/property-based-testing) — ScalaCheck forAll, random sequences, invariant checking - [Boundary Testing](/docs/testing/boundary-testing) — Attack patterns, transaction variations, state-space exploration - [Emulator](/docs/testing/emulator) — In-memory Cardano node for fast iteration - [TDD & ATDD Workflow](/docs/testing/tdd-atdd-workflow) — Test-first development for smart contracts - [Debugging](/docs/testing/debugging) — IDE debugging and logging for validators --- Source: https://scalus.org/docs/testing/property-based-testing --- # Property-Based Testing Example-based tests check specific scenarios you think of. Property-based tests check invariants across hundreds of randomly generated inputs — catching edge cases you didn't anticipate. For smart contracts, this is critical: a validator that passes 10 hand-picked test cases can still fail on the 11th combination of amount, timing, and signatories. ScalaCheck generates those combinations for you. ## Setup Scalus integrates with [ScalaCheck](https://www.scalacheck.org/) and provides `Arbitrary` instances for all Cardano types: ```scala {2,8-10} import org.scalatest.funsuite.AnyFunSuite import org.scalatestplus.scalacheck.ScalaCheckPropertyChecks import scalus.testing.kit.ScalusTest class MyValidatorTest extends AnyFunSuite, ScalusTest, ScalaCheckPropertyChecks { test("value is always non-negative") { forAll { (txIn: TxInInfo) => assert(txIn.resolved.value.lovelace >= 0) } } } ``` `forAll` generates random `TxInInfo` values and checks the property for each. If it fails, ScalaCheck shrinks the failing input to the minimal reproduction case. Available `Arbitrary` instances: - **Plutus types:** `TxInfo`, `TxOut`, `TxInInfo`, `ScriptContext`, `Value`, `Address` - **Primitives:** `PubKeyHash`, `TxId`, `TxOutRef`, `POSIXTime`, `ValidatorHash` - **Custom types** with `derives FromData, ToData` ## Custom Generators Use `Gen` to constrain random values to your domain: ```scala {3-4,7,14} import org.scalacheck.Gen val validBidGen: Gen[Long] = Gen.choose(2_000_000L, 100_000_000L) val invalidBidGen: Gen[Long] = Gen.choose(0L, 1_999_999L) test("bids above minimum are accepted") { forAll(validBidGen) { bidAmount => val result = submitBid(bidAmount) assert(result.isRight) } } test("bids below minimum are rejected") { forAll(invalidBidGen) { bidAmount => val result = submitBid(bidAmount) assert(result.isLeft) } } ``` ### Combining Generators Compose generators for complex test inputs: ```scala {1-5,8} val auctionScenarioGen: Gen[(Long, Long, Boolean)] = for currentBid <- Gen.choose(2_000_000L, 50_000_000L) newBid <- Gen.choose(0L, 100_000_000L) afterDeadline <- Gen.oneOf(true, false) yield (currentBid, newBid, afterDeadline) test("bid validation is consistent") { forAll(auctionScenarioGen) { (currentBid, newBid, afterDeadline) => val result = validateBid(currentBid, newBid, afterDeadline) if afterDeadline then assert(result.isFailure, "Should reject all bids after deadline") else if newBid <= currentBid then assert(result.isFailure, "Should reject bids at or below current") else assert(result.isSuccess, "Should accept valid bids before deadline") } } ``` ## Testing Validator Properties The key insight: express what your validator **should guarantee** as a property, then let ScalaCheck find counterexamples. ### Crowdfunding: Donation Validates Correctly ```scala {2-4,6,28-35} test("donate fails after deadline") { val deadlineGen = Gen.choose(1000L, 100_000L) val slotGen = Gen.choose(0L, 200_000L) val amountGen = Gen.choose(1_000_000L, 50_000_000L) forAll(deadlineGen, slotGen, amountGen) { (deadline, currentSlot, amount) => val datum = CampaignDatum( totalSum = BigInt(0), goal = BigInt(10_000_000), recipient = random[PubKeyHash], deadline = BigInt(deadline), withdrawn = BigInt(0), donationPolicyId = donationPolicyId ) val context = ScriptContext( txInfo = TxInfo( inputs = List(campaignInput(datum)), outputs = List.Nil, mint = Value.zero, signatories = List.Nil, validRange = Interval.after(currentSlot), id = random[TxId] ), redeemer = Action.Donate(BigInt(amount), BigInt(0), BigInt(0), BigInt(1)).toData, scriptInfo = ScriptInfo.SpendingScript(txOutRef, Option.None) ) val result = crowdfundingContract.program $ context.toData val evalResult = result.evaluateDebug if currentSlot > deadline then assert(evalResult.isFailure, "Should reject donations after deadline") else // May fail for other reasons (missing outputs, etc.) but not for deadline evalResult match case Result.Failure(_, _, _, logs) => assert(!logs.exists(_.contains("before deadline")), "Should not fail with deadline error when before deadline") case _ => () // success is fine } } ``` ### Auction: Bid Amount Invariant ```scala {2-3,5,18-22} test("bid must exceed current highest") { val bidGen = Gen.choose(0L, 200_000_000L) val currentBidGen = Gen.choose(2_000_000L, 100_000_000L) forAll(bidGen, currentBidGen) { (newBid, currentBid) => val datum = AuctionDatum( seller = random[PubKeyHash], highestBidder = random[PubKeyHash], highestBid = BigInt(currentBid), auctionEndTime = BigInt(999_999), itemId = utf8"test-item" ) val context = buildBidContext(datum, newBid, slot = 500) val result = auctionContract.program $ context.toData val evalResult = result.evaluateDebug if newBid > currentBid then // Valid bid — may still fail for other reasons (outputs, etc.) () else assert(evalResult.isFailure, s"Should reject bid $newBid <= current $currentBid") } } ``` ## Labeled Properties Use labeled props for clear failure messages when testing multiple properties at once: ```scala {4-8} import org.scalacheck.Prop test("campaign datum invariants") { forAll { (datum: CampaignDatum) => Prop(datum.totalSum >= 0) :| "totalSum non-negative" && Prop(datum.goal > 0) :| "goal positive" && Prop(datum.withdrawn >= 0) :| "withdrawn non-negative" && Prop(datum.withdrawn <= datum.totalSum) :| "withdrawn <= totalSum" } } ``` When a property fails, ScalaCheck reports which label broke — instead of a generic assertion error. ## Beyond Single-Step Testing `forAll` tests properties against a fixed state. But smart contracts evolve through sequences of actions — donate, wait, withdraw, reclaim — where bugs emerge from specific orderings. Scalus provides two tools for multi-step property testing: - **`ContractScalaCheckCommands`** — generates random sequences of actions, checks invariants after each step, and shrinks to minimal failing sequences. Scales to hundreds of participants. - **`Scenario.explore`** — exhaustive exploration of all action combinations at bounded depth. Full coverage for small state spaces. Both use the same `ContractStepVariations` interface and can be combined with [attack patterns](/docs/testing/boundary-testing#standard-variations) for security testing. See [Boundary Testing](/docs/testing/boundary-testing) for the full guide on multi-step testing, transaction variations, and attack simulation. ## Examples - [CrowdfundingValidatorTest](https://github.com/scalus3/scalus/blob/master/scalus-examples/jvm/src/test/scala/scalus/examples/crowdfunding/CrowdfundingValidatorTest.scala) — Property-based script context testing with random data and error log verification ## Related - [Unit Testing](/docs/testing/unit-testing) — ScalusTest trait, assertScriptFail, budget testing - [Boundary Testing](/docs/testing/boundary-testing) — Multi-step testing, attack patterns, and state-space exploration - [TDD & ATDD Workflow](/docs/testing/tdd-atdd-workflow) — Test-first development for smart contracts - [Emulator](/docs/testing/emulator) — In-memory Cardano node for fast iteration --- Source: https://scalus.org/docs/testing/boundary-testing --- # Boundary Testing ## The Idea Smart contract bugs often hide at boundary values — an amount just below the minimum bid, a deadline off by one slot, a missing output in a multi-UTXO transaction. Manual tests check the happy path and a few error cases, but miss the combinatorial explosion of edge cases. Boundary testing automates this. The core idea is **state-space exploration**: 1. **Observe** the current blockchain state (UTXOs, datums, balances, slot) 2. **Generate a set of transactions** — each representing a possible interaction with the contract 3. **Apply each transaction in its own copy of the world**, checking invariants on the resulting state 4. **Repeat** from step 1 on each new state — building a tree of reachable states, checking at every node How do you generate the transaction sets in step 2? The generic interface is a **step function** `BlockchainReader => Scenario[Unit]` — you read the current state and produce any set of transactions you like using the `Scenario` monad. The most concise way to generate transaction sets is the `ContractStepVariations` helper: define a base transaction (the normal, valid interaction) and then apply **variations** that systematically change amounts, timings, outputs, and recipients around boundary values. The testkit also includes pre-built [attack patterns](/docs/security/common-vulnerabilities) (steal outputs, corrupt datums, double satisfaction) so you get broad security coverage with minimal setup. You can run boundary tests in three modes: - **ScalaCheck forAll** — random sampling with shrinking, single step - **ScalaCheck Commands** — random multi-step sequences, scales to large state spaces - **Scenario** — exhaustive exploration at bounded depth, full coverage of small state spaces ## Quick Start ### 1. Define Observable State Define a case class capturing the blockchain state relevant to your test. This is **not** just the contract's datum — it's everything you need to observe from the blockchain to build and verify transactions. In the simplest case it mirrors the contract datum plus the UTXO reference, but it can also include multiple UTXOs at the script address, balances at other addresses, current slot, etc. Simple case — single UTXO contract: ```scala case class AuctionState( currentBid: Coin, deadline: SlotNo, topBidder: PubKeyHash, auctionUtxo: Utxo ) ``` General case — multiple UTXOs (needed for double satisfaction testing, etc.): ```scala case class MultiUtxoState( openUtxos: Seq[Utxo], datums: Seq[MyDatum] ) ``` The richer your state, the more attack vectors you can explore. For example, you can only test double satisfaction if your state tracks all UTXOs at the script address, not just one. ### 2. Implement ContractStepVariations To explore the state space you need to implement `ContractStepVariations[S]` — the interface that tells the testkit how to interact with your contract: ```scala {2-4,7,10,13} trait ContractStepVariations[S] { def extractState(reader: BlockchainReader)(using ExecutionContext): Future[S] def makeBaseTx(reader: BlockchainReader, state: S)(using ExecutionContext): Future[TxTemplate] def variations: TxVariations[S] // convenience — builds template and enumerates variations def allVariations(reader: BlockchainReader, state: S)(using ExecutionContext): Future[Seq[Transaction]] // slot delays to explore at this step (default: empty) def slotDelays(state: S): Seq[Long] = Seq.empty // all actions — combines Submit(tx) for each variation + Wait(slots) for each delay def allActions(reader: BlockchainReader, state: S)(using ExecutionContext): Future[Seq[StepAction]] } ``` The three core methods you implement: - **`extractState`** — read the blockchain and build your observable state `S` - **`makeBaseTx`** — build a base transaction template that variations will modify (usually a correct happy-path transaction, but can be any starting point) - **`variations`** — generate a set of transactions by varying the base template (amounts, timings, recipients, etc.) The rest is derived: `allVariations` builds the template and enumerates variations, `allActions` combines `Submit(tx)` for each variation with `Wait(slots)` for each delay. Each `Submit` is applied in its own copy of the world; each `Wait` advances the slot clock. Here's a complete implementation for the auction example: ```scala {3,16,29} object AuctionStep extends ContractStepVariations[AuctionState] { def extractState(reader: BlockchainReader)(using ExecutionContext): Future[AuctionState] = reader.findUtxos(auctionScriptAddress).map { result => val utxo = result.getOrThrow.head val datum = AuctionDatum.fromData(utxo.output.requireInlineDatum) AuctionState( currentBid = datum.topBid, deadline = datum.deadline, topBidder = datum.topBidder, auctionUtxo = utxo ) } def makeBaseTx(reader: BlockchainReader, state: AuctionState)(using ExecutionContext): Future[TxTemplate] = Future.successful( TxTemplate( builder = TxBuilder(reader.cardanoInfo) .spend(state.auctionUtxo, BidRedeemer(state.currentBid + 1).toData, auctionScript) .payToScript(auctionScriptAddress, updatedDatum(state.currentBid + 1, Alice), auctionValue) .payTo(previousBidder(state), refundValue(state)) .validFrom(state.deadline - 10), sponsor = Alice.address, signer = Alice.signer ) ) def variations: TxVariations[AuctionState] = TxVariations.standard.default[AuctionState]( extractUtxo = _.auctionUtxo, extractDatum = s => updatedDatum(s.currentBid + 1, Alice), redeemer = _ => BidRedeemer.toData, script = auctionScript ) } ``` `makeBaseTx` returns a `TxTemplate` which bundles the builder with sponsor and signer. `variations` here uses `TxVariations.standard.default` — pre-built [attack patterns](/docs/security/common-vulnerabilities) that give broad coverage with minimal setup. See [Defining Variations](#defining-variations) for custom variations. For time-dependent contracts, override `slotDelays` to explore behavior at different points in time: ```scala {12} object HtlcStep extends ContractStepVariations[HtlcState] { def extractState(reader: BlockchainReader)(using ExecutionContext) = ... def makeBaseTx(reader: BlockchainReader, state: HtlcState)(using ExecutionContext) = ... def variations = TxVariations.standard.default[HtlcState]( extractUtxo = _.utxo, extractDatum = s => s.utxo.output.requireInlineDatum, redeemer = _ => HtlcRedeemer.Unlock.toData, script = htlcScript ) // Time-dependent: explore advancing 10 and 100 slots override def slotDelays(state: HtlcState) = Seq(10L, 100L) } ``` Override individual methods via anonymous refinement: ```scala // Extend with custom attack variation new HtlcStep { override def variations = super.variations ++ customAttackVariation } ``` ### 3. Define Variations The `variations` field above used `TxVariations.standard.default` — the quickest way to get broad coverage. You can also write custom variations or compose them: ```scala {14,27} import scalus.testing.TxVariations // Pre-built attack patterns val standardVariations = TxVariations.standard.default[AuctionState]( extractUtxo = _.auctionUtxo, extractDatum = s => updatedDatum(s.currentBid + 1), redeemer = _ => BidRedeemer.toData, script = auctionScript ) // Custom boundary testing val customVariation: TxVariations[AuctionState] = new TxVariations[AuctionState] { override def enumerate(reader, state, txTemplate)(using ExecutionContext) = { val txs = for amount <- TxVariations.standard.valuesAround(state.currentBid) yield TxBuilder(reader.cardanoInfo) .spend(state.auctionUtxo, BidRedeemer(amount).toData, auctionScript) .payToScript(auctionScriptAddress, updatedDatum(amount), auctionValue) .payTo(previousBidder(state), refundValue(state)) .validFrom(state.deadline - 10) Future.sequence(txs.map(_.complete(reader, txTemplate.sponsor) .map(_.sign(txTemplate.signer).transaction))) } } // Compose via ++ val allVariations = standardVariations ++ customVariation ``` ### 4. Run Tests Three modes are available, trading off between completeness and scalability: | Mode | Exploration | Shrinking | Best for | |------|-------------|-----------|----------| | **ScalaCheck forAll** | Random sampling, fixed state | Yes | Single-step boundary testing | | **ScalaCheck Commands** | Random N sequences of random actions | Yes | Large state spaces, many participants | | **Scenario** | Exhaustive at bounded depth | No | Full coverage of small state spaces | #### ScalaCheck forAll Simplest mode — random sampling with shrinking against a fixed state. ```scala test("bid boundary values") { val provider: BlockchainProvider = emulator val state = AuctionStep.extractState(provider).await() given Arbitrary[TxBuilder] = Arbitrary(AuctionStep.allVariationsGen(provider, state)) forAll { (incompleteTx: TxBuilder) => val result = Try(incompleteTx.signAndComplete(emulator).await()) // test-specific assertions val amount = extractAmount(incompleteTx) if amount > state.currentBid then assert(result.isSuccess) else assert(result.isFailure) } } ``` #### ScalaCheck Commands Stateful property testing — generates random sequences of commands, with automatic shrinking on failure. Uses `step.allActions` to generate both transaction submissions (`SubmitTxCommand`) and slot advancements (`AdvanceSlotCommand`). This is the right choice when the exploration space is too large for exhaustive Scenario exploration. Instead of checking every combination, ScalaCheck randomly samples N command sequences (controlled by `withMinSuccessfulTests`), each time picking a random action from `allActions`. If a violation is found, ScalaCheck automatically shrinks the sequence to the minimal failing case. This trades completeness for scalability — you can test contracts with hundreds of participants, dozens of actions, and complex time-dependent logic where exhaustive exploration would be infeasible. **How it works:** ScalaCheck's `Commands` framework maintains two parallel states: | | Abstract Model (State) | System Under Test (Sut) | |-|------------------------|-------------------------| | Type | `ImmutableEmulator` | Mutable `Emulator` | | Purpose | Predict behavior, generate next commands | Execute actual transactions | | Updates | New immutable copy on each change | Mutates in-place | On each step, `genCommand` calls `step.allActions(reader, state)` to get all possible actions, then ScalaCheck randomly picks one. On failure, ScalaCheck shrinks the command sequence to find the minimal failing case. - **Successful transaction:** updates both model and Sut, then runs `checkInvariants` - **Rejected transaction:** passes — rejection is expected for attack variations - **Exception:** fails the property **Basic usage:** ```scala test("auction command sequence") { val emulator = Emulator( initialUtxos = Map( Input(genesisHash, 0) -> Output(Alice.address, Value.ada(1000)), Input(genesisHash, 1) -> Output(Bob.address, Value.ada(1000)) ) ) // Setup auction... // Pass Emulator directly — conversion to ImmutableEmulator happens internally val commands = ContractScalaCheckCommands(emulator, AuctionStep) { (reader, state) => Future.successful(Prop(state.currentBid >= 0)) } commands.property().check() } ``` **Invariant checking:** the second parameter group receives `(BlockchainReader, S) => Future[Prop]` — checked after every successful transaction. Use labeled props for clear failure messages: ```scala {3-5} val commands = ContractScalaCheckCommands(emulator, step) { (reader, state) => Future.successful { Prop(state.totalSum >= 0) :| "totalSum non-negative" && Prop(state.goal == expectedGoal) :| "goal unchanged" && Prop(state.withdrawn <= state.totalSum) :| "withdrawn <= totalSum" } } ``` **Configuring test parameters:** ```scala val result = org.scalacheck.Test.check( org.scalacheck.Test.Parameters.default .withMinSuccessfulTests(15) // number of successful command sequences .withMaxDiscardRatio(20), // allow more discards for complex setups commands.property() ) assert(result.passed, s"Property test failed: $result") ``` **Slot advancement** is controlled by overriding `slotDelays` on the step. These are included as `StepAction.Wait` actions alongside transaction submissions — no need to pass a slot generator to `ContractScalaCheckCommands`. **Overriding `allVariations` for complex logic:** when the available actions depend on the current blockchain state (e.g., different actions before vs after a deadline), override `allVariations` directly: ```scala {11-12,16,20,23,29} class CrowdfundingStep(campaignId: ByteString) extends ContractStepVariations[CrowdfundingState] { override def allVariations( reader: BlockchainReader, state: CrowdfundingState )(using ExecutionContext): Future[Seq[Transaction]] = reader.currentSlot.flatMap { currentSlot => val slotTime = reader.cardanoInfo.slotConfig.slotToTime(currentSlot) val beforeDeadline = slotTime < state.datum.deadline val goalReached = state.datum.totalSum >= state.datum.goal val txFutures = Seq.newBuilder[Future[Option[Transaction]]] if beforeDeadline then // generate donate transactions for rotating donors donors.foreach(d => txFutures += buildDonateTx(reader, state, d).map(Some(_)).recover { case _ => None }) if !beforeDeadline && goalReached then txFutures += buildWithdrawTx(reader, state).map(Some(_)).recover { case _ => None } if !beforeDeadline && !goalReached then txFutures += buildReclaimTx(reader, state).map(Some(_)).recover { case _ => None } Future.sequence(txFutures.result()).map(_.flatten) } override def slotDelays(state: CrowdfundingState): Seq[Long] = Seq(20L, 50L) // makeBaseTx and variations can return empty defaults since allVariations is overridden override def makeBaseTx(...) = Future.successful(TxTemplate(...)) override def variations = TxVariations.empty } ``` This pattern is useful when different contract phases (before/after deadline, goal reached/not reached) offer fundamentally different actions. #### Scenario — Exhaustive Exploration Explores all combinations of boundary values at bounded depth using a logic monad. The step function receives a `BlockchainReader` and performs one interaction using normal Scenario operations. Actions (`submit`, `sleep`) are automatically logged. ```scala {6,11-14,22} test("auction exhaustive boundaries") { val emulator = Emulator(...) // Setup auction... val scenario = ScenarioExplorer.explore(maxDepth = 4) { reader => async[Scenario] { // Future.await works inside async[Scenario] via futureToScenarioConversion val currentSlot = reader.currentSlot.await val state = AuctionStep.extractState(reader).await Scenario.check(state.currentBid >= 0, "negative bid").await val txs = AuctionStep.allVariations(reader, state).await val tx = Scenario.fromCollection(txs).await val result = Scenario.submit(tx).await result match case Right(_) => () case Left(_) => Scenario.fail[Unit].await } } // Pass Emulator directly — conversion to ImmutableEmulator happens internally val results = Await.result(Scenario.runAll(emulator)(scenario), Duration.Inf) val violations = results.flatMap(_._2) assert(violations.isEmpty, s"Found violations: ${violations.mkString("\n")}") } ``` If a `Scenario.check` fails, `ScenarioExplorer` returns a `Violation` containing the action path (all `StepAction.Submit` and `StepAction.Wait` entries) that led to the failure. ## Multi-Actor Testing The Quick Start above uses a single `ContractStepVariations` — one interaction type with variations. In practice, contracts are tested by multiple **actors** (participants) doing different things — a donor donating, a recipient withdrawing, an attacker trying to steal funds. The actor model makes this explicit: 1. **Contract under test** — the smart contract validator 2. **Actors** — participants that interact with the contract, each generates actions based on state 3. **Test configuration** = state extraction + set of actors + slot delays We use "actor" in the sense of "one who acts" — a participant performing actions — not in the sense of the Actor concurrency model (Akka, Erlang). There is no message passing or mailbox here; actors simply generate transaction actions given the current blockchain state. Each actor is a `ContractTestActor[S]` that, given the current blockchain state, returns the actions it can take: ```scala trait ContractTestActor[S] { def name: String def actions(reader: BlockchainReader, state: S)(using ExecutionContext): Future[Seq[StepAction]] } ``` ### Creating Actors Three factory methods cover common patterns: ```scala {2,8,15} // Simple actor — builds one transaction (or none if it can't act) val bidder = ContractTestActor.simple[AuctionState]( "bidder-alice", (reader, state) => buildBidTx(reader, state).map(Some(_)) ) // Multi-action actor — builds multiple transactions val donorGroup = ContractTestActor.multi[CampaignState]( "donor-group", (reader, state) => buildDonateTxs(reader, state) ) // Actor with attack variations — base tx + standard attack patterns val attacker = ContractTestActor.withVariations[AuctionState]( "attacker", baseTx = (reader, state) => buildAttackerBaseTx(reader, state).map(Some(_)), txVariations = TxVariations.standard.default( extractUtxo = _.auctionUtxo, extractDatum = s => updatedDatum(s), redeemer = _ => BidRedeemer.toData, script = auctionScript ) ) ``` ### Composing Actors into a Step Use `ContractStepVariations.fromActors` to combine actors into a single step: ```scala val step = ContractStepVariations.fromActors[AuctionState]( extract = reader => extractAuctionState(reader), actors = Seq( HonestBidderActor(alice), HonestBidderActor(bob), attackerActor // attack txs should be rejected by the contract ), delays = _ => Seq(10L, 50L) ) // Use with ScalaCheck Commands or Scenario as usual val commands = ContractScalaCheckCommands(emulator, step) { (reader, state) => Future.successful(Prop(state.currentBid >= 0) :| "bid non-negative") } ``` The key pattern: mix honest actors with attacker actors. The contract should reject all attack transactions while accepting honest ones. If an attacker's transaction gets accepted, the invariant check fails and ScalaCheck reports the minimal failing sequence. ### Two Paths - **Simple (single interaction type):** Implement `ContractStepVariations` directly with `makeBaseTx` + `variations` (as shown in [Quick Start](#quick-start)) - **Multi-actor:** Define actors, compose with `ContractStepVariations.fromActors` Both produce a `ContractStepVariations[S]` that works with ScalaCheck Commands and Scenario exploration. ## Defining Variations The `variations` field of `ContractStepVariations` returns a `TxVariations[S]` — this is how you define the set of transactions to generate from the base template. ### TxVariations — Enumerate-First (Boundary Testing) The primary method is `enumerate` returning `Future[Seq[Transaction]]` — fully completed, signed transactions ready to submit. ```scala trait TxVariations[S] { def enumerate( reader: BlockchainReader, state: S, txTemplate: TxTemplate )(using ExecutionContext): Future[Seq[Transaction]] def ++(other: TxVariations[S]): TxVariations[S] // compose } ``` - **reader** — query blockchain state (UTxOs, slot, params) — read-only, no submit - **state** — observable blockchain state `S` extracted by `extractState` (may include multiple UTXOs, balances, etc.) - **txTemplate** — bundles sponsor (pays fees/collateral) and signer ### TxSamplingVariations — Gen-First (Fuzz Testing) For large/continuous domains that can't be enumerated (e.g., arbitrary `Value` with random token bundles): ```scala trait TxSamplingVariations[S] extends TxVariations[S] { def gen( reader: BlockchainReader, state: S, txTemplate: TxTemplate ): Gen[Future[Transaction]] def sampleSize: Int = 20 // samples for enumerate } ``` Example implementation: ```scala {5-7} val valueFuzz: TxSamplingVariations[AuctionState] = new TxSamplingVariations[AuctionState] { override def sampleSize = 30 def gen(reader, state, txTemplate) = for adaAmount <- Gen.oneOf(Coin(0), minUtxo, state.currentBid - 1, state.currentBid + 1) extraTokens <- Gen.someOf(knownPolicies) tokenAmount <- Gen.choose(0L, 1_000_000L) yield { given ExecutionContext = reader.executionContext TxBuilder(reader.cardanoInfo) .spend(state.auctionUtxo, BidRedeemer(adaAmount), auctionScript) .payToScript(auctionScriptAddress, updatedDatum, Value(adaAmount, ...)) .complete(reader, txTemplate.sponsor) .map(_.sign(txTemplate.signer).transaction) } } ``` `enumerate` samples N values from `gen` for bounded exploration in Scenario mode. ### Using Boundary Generators `StandardTxVariations` provides helper generators for boundary testing: ```scala // Generate values around a threshold (below, equal, above) TxVariations.standard.valuesAround(threshold: Coin): Gen[Coin] // Generate slots around a deadline (before, at, after) TxVariations.standard.slotsAround(deadline: Long): Gen[Long] ``` ### Composing Multiple Dimensions Use for-comprehension to compose boundary values across multiple dimensions: ```scala val variations: TxVariations[MyState] = new TxVariations[MyState] { override def enumerate(reader, state, txTemplate)(using ExecutionContext) = { val amounts = TxVariations.standard.valuesAround(state.threshold).sample.toSeq val timings = TxVariations.standard.slotsAround(state.deadline).sample.toSeq val txBuilders = for amount <- amounts timing <- timings yield TxBuilder(reader.cardanoInfo) .spend(state.scriptUtxo, MyRedeemer(amount).toData, myScript) .payToScript(scriptAddress, updatedDatum(amount), outputValue) .validFrom(timing) Future.sequence(txBuilders.map(_.complete(reader, txTemplate.sponsor) .map(_.sign(txTemplate.signer).transaction))) } } ``` Each dimension has ~3 values (below/at/above), so 3 dimensions = 27 combinations. ## Standard Variations Use `TxVariations.standard` to access pre-built attack patterns. The `default` method combines common attack vectors with minimal configuration: ```scala {6,15,20-21} import scalus.testing.TxVariations case class ContractState(utxo: Utxo) // Minimal setup - covers steal, duplicate output, partial theft val defaultVariations = TxVariations.standard.default[ContractState]( extractUtxo = _.utxo, extractDatum = s => s.utxo.output.requireInlineDatum, redeemer = _ => MyRedeemer.toData, script = myScript ) // Extended - adds corrupted datum and wrong address testing val extendedVariations = TxVariations.standard.defaultExtended[ContractState]( extractUtxo = _.utxo, extractDatum = s => s.utxo.output.requireInlineDatum, redeemer = _ => MyRedeemer.toData, script = myScript, corruptedDatums = _ => Gen.const(Data.I(BigInt(-1))), // invalid datum alternativeAddresses = _ => Gen.const(attackerAddress) ) ``` ### Available Attack Patterns Individual variations available via `TxVariations.standard`: | Variation | Description | |-----------|-------------| | `removeContractOutput` | Steal attack — no output back to script | | `stealPartialValue` | Return less value than expected | | `corruptDatum` | Wrong datum in output | | `wrongOutputAddress` | Send to wrong recipient | | `duplicateOutput` | Split into two outputs | | `unauthorizedMint` | Mint without authorization | | `mintExtra` | Mint more than allowed | | `aroundDeadline` | Test timing boundaries | | `wrongRedeemer` | Use invalid redeemer | | `doubleSatisfaction` | Satisfy one validator, steal from another | These patterns correspond to [common Cardano vulnerabilities](/docs/security/common-vulnerabilities). Using `default` or `defaultExtended` is the easiest way to get broad coverage. ## Custom Step Functions `ContractStepVariations` is a convenience helper, but the underlying interface consumed by `ScenarioExplorer` and `ContractScalaCheckCommands` is a plain function: ```scala step: BlockchainReader => Scenario[Unit] ``` You can implement this directly when you need full control over how actions are generated — for example, when the base-transaction-plus-variations pattern doesn't fit your use case. ## Complexity Control With `C` categories per dimension, `D` dimensions, and depth `N`: - Per step: C^D combinations (e.g., 3x3x3 = 27) - Multi-step: up to (C^D)^N total paths Keep categories at 2-4 per dimension. Use `guard` to prune impossible branches and `once` to stop at first violation in Scenario mode. ## Testing Patterns ### Single Action Boundaries Test one contract action with all boundary combinations: ```scala test("bid boundaries") { val provider = emulator val state = AuctionStep.extractState(provider).await() given Arbitrary[TxBuilder] = Arbitrary(AuctionStep.allVariationsGen(provider, state)) forAll { (tx: TxBuilder) => ... } } ``` ### Multi-Step State Exploration Test sequences of actions where each step changes the state: ```scala val emulator = Emulator(...) // Setup contract... val scenario = ScenarioExplorer.explore(maxDepth = 3) { reader => async[Scenario] { // Future.await works inside async[Scenario] val state = MyStep.extractState(reader).await Scenario.check(invariant(state)).await val txs = MyStep.allVariations(reader, state).await val tx = Scenario.fromCollection(txs).await val result = Scenario.submit(tx).await result match case Right(_) => () case Left(_) => Scenario.fail[Unit].await } } // Pass Emulator directly val results = Await.result(Scenario.runAll(emulator)(scenario), Duration.Inf) val violations = results.flatMap(_._2) assert(violations.isEmpty) ``` ### Attack Simulation Add malicious variations alongside standard ones: ```scala // Use standard steal variation val standardAttacks = TxVariations.standard.default[MyState]( extractUtxo = _.scriptUtxo, extractDatum = s => s.scriptUtxo.output.requireInlineDatum, redeemer = _ => MyRedeemer.claim.toData, script = myScript ) // Or create custom attack val customAttack: TxVariations[MyState] = new TxVariations[MyState] { override def enumerate(reader, state, txTemplate)(using ExecutionContext) = { TxBuilder(reader.cardanoInfo) .spend(state.scriptUtxo, MyRedeemer.claim.toData, myScript) .payTo(attackerAddress, state.scriptUtxo.output.value) // steal to attacker .complete(reader, txTemplate.sponsor) .map(b => Seq(b.sign(txTemplate.signer).transaction)) } } // Combine and test val allAttacks = standardAttacks ++ customAttack ``` ### Multi-UTXO State (Double Satisfaction) As described in [Define Observable State](#1-define-observable-state), the state type `S` should capture everything you need from the blockchain. When a contract can have multiple UTXOs at the same address (which is common — any contract that processes multiple independent interactions), model state as a collection of all open UTXOs. This naturally enables testing double satisfaction attacks where one transaction spends multiple UTXOs while only satisfying one validator: ```scala {36,51-52,54-55} // State includes all open UTXOs at the contract address case class MultiUtxoState( openUtxos: Seq[Utxo], datums: Seq[MyDatum] ) object MultiUtxoStep extends ContractStepVariations[MultiUtxoState] { def extractState(reader: BlockchainReader)(using ExecutionContext): Future[MultiUtxoState] = reader.findUtxos(scriptAddress).map { result => val utxos = result.getOrThrow val datums = utxos.map(u => MyDatum.fromData(u.output.requireInlineDatum)) MultiUtxoState(utxos, datums) } def makeBaseTx(reader: BlockchainReader, state: MultiUtxoState)(using ExecutionContext) = { val utxo = state.openUtxos.head Future.successful( TxTemplate( builder = TxBuilder(reader.cardanoInfo) .spend(utxo, myRedeemer, script) .payToScript(scriptAddress, state.datums.head.toData, utxo.output.value), sponsor = Alice.address, signer = Alice.signer ) ) } def variations: TxVariations[MultiUtxoState] = TxVariations.standard.default[MultiUtxoState]( extractUtxo = _.openUtxos.head, extractDatum = s => s.datums.head.toData, redeemer = _ => MyRedeemer.toData, script = myScript ) ++ doubleSatisfactionVariation } // Test spending multiple UTXOs in one transaction val doubleSatisfactionVariation: TxVariations[MultiUtxoState] = new TxVariations[MultiUtxoState] { override def enumerate( reader: BlockchainReader, state: MultiUtxoState, txTemplate: TxTemplate )(using ExecutionContext): Future[Seq[Transaction]] = { if state.openUtxos.size < 2 then Future.successful(Seq.empty) else { val (utxo1, utxo2) = (state.openUtxos(0), state.openUtxos(1)) val tx = TxBuilder(reader.cardanoInfo) .spend(utxo1, myRedeemer, script) .spend(utxo2, myRedeemer, script) // Only one output - steals from second UTXO .payToScript(scriptAddress, state.datums.head.toData, utxo1.output.value) .payTo(attackerAddress, utxo2.output.value) tx.complete(reader, txTemplate.sponsor).map { completedTx => Seq(completedTx.sign(txTemplate.signer).transaction) } } } } ``` This pattern tests that contracts correctly enforce independent validation of each UTXO spend, even when multiple UTXOs are consumed in one transaction. ### Time-Dependent Behavior Test behavior before and after deadlines: ```scala val scenario = async[Scenario] { setupHtlc(...).await // Try claiming before timeout — should fail Scenario.sleep(1).await val earlyResult = Try { val reader = Scenario.snapshotReader.await val state = HtlcStep.extractState(reader).await val txTemplate = HtlcStep.makeBaseTx(reader, state).await val tx = txTemplate.complete(reader).await Scenario.submit(tx).await } assert(earlyResult.isFailure) // Try claiming after timeout — should succeed Scenario.sleep(100).await val lateResult = Try { ... } assert(lateResult.isSuccess) } ``` ## API Reference ### Scenario Runners ```scala // Simple entry point — pass Emulator directly val results = Scenario.runAll(emulator)(scenario) // all results (up to 1000) val first = Scenario.runFirst(emulator)(scenario) // first result // Advanced — continue from existing ScenarioState val state = ScenarioState(immutableEmulator, org.scalacheck.rng.Seed(42L)) val results = Scenario.continueAll(state)(scenario) // all results val first = Scenario.continueFirst(state)(scenario) // first result ``` ### ContractTestActor ```scala trait ContractTestActor[S] { def name: String def actions(reader: BlockchainReader, state: S)(using ExecutionContext): Future[Seq[StepAction]] } object ContractTestActor { // Actor with base tx + attack variations def withVariations[S](actorName: String, baseTx: ..., txVariations: TxVariations[S] = TxVariations.empty): ContractTestActor[S] // Actor from a single optional transaction def simple[S](actorName: String, buildTx: (BlockchainReader, S) => Future[Option[Transaction]]): ContractTestActor[S] // Actor from multiple transactions def multi[S](actorName: String, buildTxs: (BlockchainReader, S) => Future[Seq[Transaction]]): ContractTestActor[S] } ``` ### ContractStepVariations.fromActors ```scala object ContractStepVariations { def fromActors[S]( extract: BlockchainReader => ExecutionContext ?=> Future[S], actors: Seq[ContractTestActor[S]], delays: S => Seq[Long] = _ => Seq.empty ): ContractStepVariations[S] } ``` Combines all actors' actions with slot delays into a single `ContractStepVariations`. The resulting step overrides `allActions` directly — `makeBaseTx`/`variations` are not used. ### ContractScalaCheckCommands ```scala class ContractScalaCheckCommands[S]( initialEmulator: ImmutableEmulator, step: ContractStepVariations[S], timeout: FiniteDuration = Duration(30, "seconds") )( checkInvariants: (BlockchainReader, S) => Future[Prop] = (_, _) => Future.successful(Prop.passed) )(using ExecutionContext) extends Commands ``` Factory method (preferred — accepts mutable `Emulator` and converts internally): ```scala val commands = ContractScalaCheckCommands(emulator, step) { (reader, state) => Future.successful(Prop.passed) } ``` The `Commands` instance generates two types of commands from `step.allActions`: - `SubmitTxCommand(tx)` — submit a transaction; on success, update model state and check invariants - `AdvanceSlotCommand(slots)` — advance the slot by the given amount Running: ```scala // Quick check commands.property().check() // With custom parameters val result = org.scalacheck.Test.check( org.scalacheck.Test.Parameters.default .withMinSuccessfulTests(10) .withMaxDiscardRatio(20), commands.property() ) assert(result.passed, s"$result") ``` ### Future.await in Scenario Inside `async[Scenario]` blocks, you can `.await` on `Future` values directly: ```scala val scenario = ScenarioExplorer.explore(maxDepth = 3) { reader => async[Scenario] { // Future[SlotNo] -> Scenario[SlotNo] via futureToScenarioConversion val currentSlot = reader.currentSlot.await // Future[S] -> Scenario[S] val state = step.extractState(reader).await // Future[Seq[Transaction]] -> Scenario[Seq[Transaction]] val txs = step.allVariations(reader, state).await } } ``` This works via `CpsMonadConversion[Future, Scenario]` which wraps the `Future` in a `Scenario.WaitFuture` node, preserving state correctly. ## Examples - [CrowdfundingScalaCheckCommandTest](https://github.com/scalus3/scalus/blob/master/scalus-examples/jvm/src/test/scala/scalus/examples/crowdfunding/CrowdfundingScalaCheckCommandTest.scala) — Multi-step property testing with 200 participants and invariant checking - [CrowdfundingScenarioTest](https://github.com/scalus3/scalus/blob/master/scalus-examples/jvm/src/test/scala/scalus/examples/crowdfunding/CrowdfundingScenarioTest.scala) — Exhaustive scenario exploration with non-deterministic branching ## See Also - [Unit Testing](/docs/testing/unit-testing) — ScalusTest trait, assertScriptFail, budget testing - [Property-Based Testing](/docs/testing/property-based-testing) — ScalaCheck forAll with random Cardano types - [Emulator](/docs/testing/emulator) — In-memory testing with instant feedback - [Common Vulnerabilities](/docs/security/common-vulnerabilities) — Vulnerability patterns the testkit helps detect - [Security](/docs/security) — Security principles for smart contracts --- Source: https://scalus.org/docs/testing/debugging --- # Debugging Cardano Smart Contracts ## Scala-Level Debugging Before compiling to UPLC, you can debug your validator as regular Scala code: - Use your IDE's debugger - Set breakpoints in validator methods - Step through execution - Inspect variables and data structures **This is the fastest way to debug logic errors.** ## Using `log` for Script Logging The `log` function accepts **variadic arguments** of any type that has a `Show` or `ToData` instance. All arguments are converted to strings and concatenated into a single trace entry, separated by spaces. ```scala inline def log(inline args: Any*): Unit ``` ### Basic Usage ```scala import scalus.cardano.onchain.plutus.prelude.log // Simple message log("Starting validation") // Label with a value — uses Show[BigInt] log("count", BigInt(42)) // trace output: "count 42" // Multiple values log("values", BigInt(1), BigInt(2), BigInt(3)) // trace output: "values 1 2 3" // Mixed types — uses Show[Boolean] and Show[BigInt] log("mixed", true, BigInt(42)) // trace output: "mixed True 42" ``` ### How Arguments Are Converted The `log` macro resolves each argument at compile time: 1. **String literals** — passed through as-is (unquoted), useful as labels 2. **Types with a `Show` instance** — converted via `Show[T]`. Built-in instances exist for `BigInt`, `Boolean`, `String`, `Data`, and `Unit` 3. **Types with a `ToData` instance** (fallback) — converted to `Data` first, then displayed via `Show[Data]`. This works for any case class or enum that `derives ToData` 4. **No instance found** — compilation error ```scala // String literal vs string expression val s: String = "hello" log("label", s) // trace output: label "hello" // ↑ literal (unquoted) ↑ expression (quoted via Show[String]) ``` ### Logging Custom Types Any case class or enum with a `ToData` instance can be logged: ```scala import scalus.cardano.onchain.plutus.prelude.* case class Point(x: BigInt, y: BigInt) derives ToData enum Color derives ToData: case Red, Green, Blue // In your validator: log("point", Point(BigInt(1), BigInt(2))) // trace output: "point <0, [1, 2]>" (Data representation) log("color", Color.Red) // trace output: "color <0, []>" ``` ### Validator Example ```scala import scalus.cardano.onchain.plutus.prelude.log @Compile object MyValidator extends Validator: inline override def spend( datum: Option[Data], redeemer: Data, tx: TxInfo, ownRef: TxOutRef ): Unit = { log("Starting validation") val myDatum = datum.getOrFail("Datum required").to[MyDatum] log("owner", myDatum.owner) val isValid = tx.isSignedBy(myDatum.owner) log("signed", isValid) require(isValid, "Must be signed by owner") } ``` ### Notes - `log()` with no arguments is a no-op - Each `log(...)` call produces exactly **one** trace entry - `log` compiles to `Builtins.trace`, so it consumes execution units on-chain. Remove or minimize logging for production deployments ## Where Do Logs Appear? **Local evaluation** (via `PlutusScriptEvaluator` or `evaluateDebug`): logs are always collected and included in: - The `Result.Success` object (when evaluation succeeds) - The `PlutusScriptEvaluationException` (when evaluation fails) This is the most reliable way to see trace output. Use `evaluateDebug` or the Emulator to inspect logs during development. **Node-side** (when submitting to a Cardano node): trace logs from failed scripts appear in the node error response as `"Script debugging logs: ..."`, but **only when the node runs in Verbose mode**. Yaci DevKit defaults to Quiet mode where trace output is not included in error responses. Blockfrost-connected nodes (Preprod, Mainnet) typically run in Verbose mode. To see trace logs from a Yaci DevKit node, the node must be configured with `VerboseMode = Verbose`. Note that successful script evaluations never expose trace output on-chain — only failures include logs in the error response. ## Evaluating with Error Traces ```scala import scalus.uplc.eval.PlutusVM given PlutusVM = PlutusVM.makePlutusV3VM() val compiled = compile { log("Validator starting") // your validator code log("Validator completed") } // Evaluate with error traces enabled val result = compiled.toUplc(generateErrorTraces = true).evaluateDebug ``` **The `generateErrorTraces` flag:** - `true`: Adds error location information (useful for debugging, but increases script size) - `false`: Minimal script size (for production deployment) ## Diagnostic Replay for Release Scripts When deploying to production, you typically compile scripts with `Options.release` (which sets `removeTraces = true` and `generateErrorTraces = false`) to minimize script size and execution costs. This means both trace logs and detailed error traces (e.g., `require` messages) are omitted in release scripts, so if a release script fails, you will not see useful error information in the logs — making it hard to diagnose the issue. **Diagnostic replay** solves this: when you use `CompiledPlutus` (e.g., `PlutusV3.compile(...)`) with `TxBuilder`, the builder automatically registers the compiled script for replay. The `CompiledPlutus` object retains the SIR, so it can recompile a debug version on demand. If the release script fails with empty logs, the evaluator: 1. Recompiles the script from SIR with error traces enabled 2. Replays the failing evaluation with the same arguments 3. Collects the diagnostic logs from the replay 4. Includes them in the `PlutusScriptEvaluationException` > **Note:** Automatic replay requires `CompiledPlutus` (which keeps the SIR). If you use an external > tx builder (e.g., meshJS or Bloxbean CCL), you won't have a `CompiledPlutus` object — instead, > use the `DebugScript` API to provide a pre-compiled debug script. See [DebugScript API](#debugscript-api-for-external-builders) below. ### Using Diagnostic Replay with TxBuilder Pass `CompiledPlutus` (the result of `PlutusV3.compile(...)`) instead of `PlutusScript` to `spend` or `mint`: ```scala given Options = Options.release // no traces for production val validator = PlutusV3.compile { (sc: Data) => // your validator code require(someCondition, "Condition failed") } // Using CompiledPlutus enables automatic diagnostic replay val tx = TxBuilder(env) .spend(scriptUtxo, redeemer, validator) // not validator.script! .payTo(recipient, Value.ada(10)) .build(changeTo = changeAddress) ``` For reference scripts, use the `references` overload: ```scala val tx = TxBuilder(env) .references(scriptRefUtxo, validator) // registers for replay .spend(scriptUtxo, redeemer) .build(changeTo = changeAddress) ``` When the script fails, the exception will contain diagnostic logs even though the on-chain script has no traces: ```scala try { builder.build(changeTo = changeAddress) } catch { case e: TxBuilderException.BalancingException => e.scriptLogs.foreach(logs => println(logs.mkString("\n"))) // Prints: "Condition failed" (from diagnostic replay) } ``` ## Debugging with IDE One of Scalus's biggest advantages is the ability to debug validators as regular Scala code: ### Setting Up Debug Mode 1. **Run tests in debug mode** - Use your IDE's debug test runner 2. **Set breakpoints** - Click in the gutter next to line numbers 3. **Inspect variables** - Hover over variables or use the debug panel 4. **Step through code** - Use step over, step into, step out ### Example Debug Session ```scala @Compile object MyValidator extends Validator: inline override def spend( datum: Option[Data], redeemer: Data, tx: TxInfo, ownRef: TxOutRef ): Unit = { val owner = datum.getOrFail("No datum").to[PubKeyHash] // Set breakpoint here ⬅ val signed = tx.signatories.contains(owner) // Inspect 'signed' variable require(signed, "Not signed") } ``` **Debugging workflow:** 1. Set a breakpoint in your validator 2. Run test in debug mode 3. When breakpoint hits, inspect variables 4. Step through execution to understand behavior 5. Fix logic errors before compiling to UPLC ## See Also - **[Profiling](/docs/testing/profiling)** - Find expensive code paths with CEK machine profiling - **[Unit Testing](/docs/testing/unit-testing)** - Write comprehensive tests for your validators - **[Emulator](/docs/testing/emulator)** - Test with in-memory Cardano node - **[Local Devnet](/docs/testing/local-devnet)** - Integration testing with real Cardano node - **[Compiling](/docs/smart-contracts/compiling)** - Compile debugged validators to Plutus scripts --- Source: https://scalus.org/docs/testing/emulator --- # 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 ```scala 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`: ```scala 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](/docs/ledger/ledger-rules) — 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 ```scala class Emulator( initialUtxos: Utxos = Map.empty, initialContext: Context = Context.testMainnet(), val validators: Iterable[STS.Validator] = Emulator.defaultValidators, val mutators: Iterable[STS.Mutator] = Emulator.defaultMutators, initialCertState: CertState = CertState.empty, initialDatums: Map[DataHash, Data] = Map.empty, initialAppliedTxLog: Vector[AppliedTx] = Vector.empty ) extends EmulatorBase // which extends BlockchainProvider ``` ### Factory Methods ```scala // 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: ```scala emulator.submit(tx).await() match { case Right(txHash) => println(s"Transaction submitted: $txHash") case Left(error: NodeSubmitError) => println(s"Rejected by ledger rules: ${error.message}") case Left(error) => println(s"Error: ${error.message}") } ``` ## Working with Time Use `setSlot()` to test time-dependent validators: ```scala // Set slot to test validity intervals emulator.setSlot(1000L) // Transaction with validity range [500, 1500] will pass val tx = TxBuilder(testEnv) .validFrom(500L) .validTo(1500L) .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(2000L) .validTo(3000L) // ... ``` ## Thread Safety The `Emulator` is thread-safe using `AtomicReference` for state management. Concurrent transaction submissions use compare-and-swap for atomic updates: ```scala // 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: ```scala 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 ) ``` ## Using the Emulator from Java The Emulator has a Java-friendly surface: factories take `java.util.List`, state accessors return Java collections, lookups return `null` instead of `Option`, and submission reports a `SubmitResult` instead of `Either` — so expected failures need no try/catch. `CompletableFuture` variants (`submitAsync`, `findUtxosForAddressAsync`) mirror the asynchronous `BlockchainProvider` shape; for the Emulator they complete immediately. ```java import scalus.cardano.address.Address; import scalus.cardano.ledger.*; import scalus.cardano.node.*; import java.util.List; import java.util.Map; // Create an emulator with two funded addresses (10,000 ADA each) Emulator emulator = Emulator.withAddresses(List.of(alice, bob)); // Or build the initial state explicitly EmulatorInitialState state = EmulatorInitialState.builder() .putUtxo(input, output) .addStakeRegistration(EmulatorStakeRegistration.of(credential, Coin.zero())) .build(); Emulator emulator2 = Emulator.withState(state); // Submit and inspect the outcome — no Either, no exceptions for rejections SubmitResult result = emulator.trySubmit(signedTx); if (result.isSuccess()) { System.out.println("Applied: " + result.getTxHashOrNull().toHex()); } else { System.out.println("Rejected: " + result.getErrorMessageOrNull()); } // Query state with Java collections Map utxos = emulator.getUtxos(); List aliceUtxos = emulator.findUtxosForAddress(alice); Transaction applied = emulator.getTransactionOrNull(result.getTxHashOrNull()); // Control time emulator.setSlot(1000L); emulator.tick(5); long slot = emulator.getCurrentSlot(); // CompletableFuture variant — same shape as asynchronous providers emulator.submitAsync(anotherTx).thenAccept(r -> System.out.println("async result: " + r.isSuccess())); ``` ## 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](/docs/testing/local-devnet)** when you need the actual Haskell Cardano node for final validation before deployment. ## Example: Testing a Minting Policy ```scala 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[_, _]] } ``` ## Pre-initializing UTxOs from JSON You can initialize the emulator with UTxOs declared in a JSON file using `Preconfiguration`. Address keys can be test party names (`"alice"`, `"bob"`) or raw bech32 addresses: ```json { "utxo": { "alice": [ { "ada": 10000 }, { "ada": 5, "datum": { "int": 42 } } ], "bob": [{ "ada": 5000, "datum_hash": "abcd..." }], "addr_test1qz...": [{ "ada": 100, "tx_id": "abcd...", "idx": 0 }] } } ``` Each UTxO entry supports: - `ada` (required) — amount in ADA - `datum` — inline datum as a Plutus JSON Data value - `datum_cbor` — inline datum as a CBOR hex string - `datum_hash` — datum hash hex (produces a datum-hash reference, not inline) - `tx_id`, `idx` — explicit transaction input; if absent, a genesis hash and auto-index are used ```scala import scalus.testing.{ImmutableEmulator, Preconfiguration} // From a JSON string val emulator = ImmutableEmulator.fromJson(json) // Or parse and resolve separately val config = Preconfiguration.fromJson(json) val utxos = Preconfiguration.resolveUtxos(config) val emulator = Emulator(initialUtxos = utxos) ``` ## See Also - [JS/TS Emulator](/docs/testing/js-emulator) — Emulator for JavaScript and TypeScript - [Local Devnet](/docs/testing/local-devnet) — Docker-based devnet for integration testing - [Provider](/docs/ledger/provider) — The Provider interface - [Ledger Rules](/docs/ledger/ledger-rules) — Transaction validation rules - [Transaction Builder](/docs/transactions) — Building transactions --- Source: https://scalus.org/docs/testing/js-emulator --- # Cardano Emulator for JavaScript and TypeScript The [Scalus Emulator](/docs/testing/emulator) — an in-memory Cardano node with full transaction validation and Plutus script execution — is also available as an npm package for JavaScript and TypeScript. ## What It Validates Scalus implements a ledger framework that emulates an in-memory node without consensus. Transactions are validated as if submitted to a real node: | Validation | Description | |------------|-------------| | **UTxO Rules** | Input existence, double-spend prevention, value conservation | | **Plutus Scripts** | V1, V2, V3 script execution with cost model evaluation | | **Staking** | Stake registration, delegation, reward withdrawals | | **Native Scripts** | Multisig and timelock validation | | **Fees & Collateral** | Fee calculation, collateral handling on script failure | | **Signatures** | Witness verification for required signers | The emulator runs the same [ledger rules](/docs/ledger/ledger-rules) as the JVM version — Phase 1 (transaction structure) and Phase 2 (script execution) validation. ## Installation ```bash npm install scalus ``` ## Quick Start ```typescript import { Emulator, SlotConfig } from "scalus"; /* `withAddresses` creates an emulator where every specified address has a UTxO with the specified lovelace amount. */ const emulator = Emulator.withAddresses( ["addr_test1qr...", "addr_test1qp..."], SlotConfig.preview, 10_000_000 ); // Encode your transaction to CBOR with your favorite encoder and submit the result to the Scalus emulator. const result = emulator.submitTx(txCborBytes); if (result.isSuccess) { console.log(`Transaction submitted: ${result.txHash}`); } else { console.log(`Failed: ${result.error}`); if (result.logs) { console.log(`Script logs: ${result.logs.join("\n")}`); } } ``` ## Creating an Emulator ### With Funded Addresses ```typescript const emulator = Emulator.withAddresses( [aliceAddress, bobAddress], SlotConfig.mainnet, BigInt(50_000_000_000) // 50 000 ADA ); ``` ### With Custom UTxOs For more control, provide CBOR-encoded UTxOs directly: ```typescript // UTxOs as CBOR-encoded Map const emulator = new Emulator(initialUtxosCbor, SlotConfig.preview); ``` ## Slot Configuration Use the built-in configurations for time conversion: ```typescript SlotConfig.mainnet // Mainnet (Shelley era start) SlotConfig.preview // Preview testnet SlotConfig.preprod // Preprod testnet // Or custom configuration const custom = new SlotConfig(zeroTime, zeroSlot, slotLength); ``` ## API Reference ### `submitTx(txCborBytes: Uint8Array): SubmitResult` Submit a CBOR-encoded transaction. Returns: ```typescript interface SubmitResult { isSuccess: boolean; txHash?: string; // On success error?: string; // On failure logs?: string[]; // Script trace logs on failure } ``` ### `getUtxosForAddress(addressBech32: string): Uint8Array[]` Get UTxOs for an address. Each entry is a CBOR-encoded `Map`: ```typescript const utxos = emulator.getUtxosForAddress(aliceAddress); // Decode with cbor-x or similar ``` ### `getAllUtxos(): Uint8Array[]` Get all UTxOs in the emulator state. ### `getUtxosCbor(): Uint8Array` Get the entire UTxO set as a single CBOR-encoded map. ### `setSlot(slot: number): void` Advance the current slot for time-based validation: ```typescript emulator.setSlot(1000); ``` ### `snapshot(): Emulator` Create a point-in-time copy: ```typescript const checkpoint = emulator.snapshot(); // ... submit transactions ... // checkpoint still has original state ``` ## Example: Simple Payment ```typescript import { Emulator, SlotConfig } from "scalus"; import { Decoder } from "cbor-x"; const decoder = new Decoder({ mapsAsObjects: false }); function hexToBytes(hex: string): Uint8Array { const bytes = new Uint8Array(hex.length / 2); for (let i = 0; i < hex.length; i += 2) { bytes[i / 2] = parseInt(hex.substring(i, i + 2), 16); } return bytes; } // Alice has 100 ADA, sends 25 ADA to Bob const initialUtxosCborHex = "a282582000000000000000000000000000000000000000000000000000000000000000000082581d60c8c47610a36034aac6fc58848bdae5c278d994ff502c05455e3b3ee81a05f5e10082582000000000000000000000000000000000000000000000000000000000000000000182581d60c8c47610a36034aac6fc58848bdae5c278d994ff502c05455e3b3ee81a00989680"; const emulator = new Emulator( hexToBytes(initialUtxosCborHex), SlotConfig.preview ); // Build transaction with your preferred library, get CBOR bytes const txCborBytes = buildTransaction(); // Your transaction builder const result = emulator.submitTx(txCborBytes); console.log(result.isSuccess ? `Success: ${result.txHash}` : `Error: ${result.error}`); ``` ## Working with UTxOs The emulator returns UTxOs as CBOR-encoded data. Decode with `cbor-x`: ```typescript import { Decoder } from "cbor-x"; const decoder = new Decoder({ mapsAsObjects: false }); const utxos = emulator.getUtxosForAddress(address); for (const utxoCbor of utxos) { const decoded = decoder.decode(utxoCbor); // decoded is Map for (const [input, output] of decoded) { console.log("Input:", input); console.log("Output:", output); } } ``` ## Integration with Lucid Use the emulator's UTxO state with Lucid Evolution for transaction building: ```typescript // Get UTxOs from emulator const utxosCbor = emulator.getUtxosCbor(); // Build transaction with Lucid const tx = await lucid.newTx() .pay.ToAddress(bobAddress, { lovelace: 25_000_000n }) .complete(); const signedTx = await tx.sign.withWallet().complete(); const txCbor = signedTx.toCBOR(); // Submit to emulator instead of network const result = emulator.submitTx(hexToBytes(txCbor)); ``` ## Script Evaluation For evaluating Plutus scripts without submitting transactions, use `evalPlutusScripts`: ```typescript import { Scalus, SlotConfig } from "scalus"; const redeemers = Scalus.evalPlutusScripts( txCborBytes, utxoCborBytes, SlotConfig.preview, costModels // [v1CostModel, v2CostModel, v3CostModel] ); for (const redeemer of redeemers) { console.log(`${redeemer.tag}[${redeemer.index}]: ${redeemer.budget.memory} mem, ${redeemer.budget.steps} steps`); } ``` ### Profiling a script `Scalus.evaluateScript(doubleCborHex)` runs a single fully-applied script (for example the output of `Scalus.applyDataArgToScript`) and returns a `Result` with `isSuccess`, `budget`, and `logs`. `Scalus.evaluateScriptProfile(doubleCborHex)` does the same but also collects [CEK machine profiling data](/docs/testing/profiling), exposed on the result as `profileJson` — per-source-location and per-builtin cost plus the transition edges. It is `undefined` for the plain `evaluateScript` (which has zero profiling overhead). ```typescript import { Scalus } from "scalus"; // `applied` is a fully-applied script, e.g. from Scalus.applyDataArgToScript(...) const result = Scalus.evaluateScriptProfile(applied); console.log(`success=${result.isSuccess}, cpu=${result.budget.steps}, mem=${result.budget.memory}`); if (result.profileJson) { const profile = JSON.parse(result.profileJson); console.log(`total cpu: ${profile.totalBudget.cpu}`); // profile.bySourceLocation / profile.byFunction / profile.transitions } ``` Only the profiling **data** is available from JavaScript. The interactive HTML report (sortable tables, hot paths/edges, annotated source) is rendered by the Scala/JVM [`ProfileFormatter`](/docs/testing/profiling#html-output) — it is deliberately kept out of `scalus.js` so the transaction-builder bundle stays small. Feed `profileJson` into your own tooling, or run the profiler on the JVM to get the HTML. ## See Also - [Emulator (JVM)](/docs/testing/emulator) — Full Scala API documentation - [Multiplatform](/docs/multiplatform) — Platform support overview - [`scalus` npm package](https://www.npmjs.com/package/scalus) — Full JavaScript/TypeScript API --- Source: https://scalus.org/docs/testing/local-devnet --- # Local Cardano Devnet with Yaci DevKit [Yaci DevKit](https://github.com/bloxbean/yaci-devkit) is a configurable local Cardano devnet running in Docker. It provides full ledger validation and Plutus script execution without external dependencies — no testnet ADA, no network latency, no third-party API limits. ## Prerequisites - **Docker** — Running locally ([Install Docker](https://docs.docker.com/get-docker/)) - **scalus-testkit** — Add to your test dependencies: ```scala libraryDependencies += "org.scalus" %% "scalus-testkit" % scalusVersion % Test ``` First run pulls the Yaci DevKit image (~1GB). Subsequent runs start in seconds. ## Quick Start ```scala import org.scalatest.funsuite.AnyFunSuite import scalus.testing.yaci.YaciDevKit class MyTest extends AnyFunSuite with YaciDevKit { test("submit transaction") { val ctx = createTestContext() // ctx.cardanoInfo, ctx.provider, ctx.alice.address, ctx.alice.signer ready to use } } ``` Run with `sbt test` — the container starts automatically. ## Why Yaci DevKit Running integration tests against preview/preprod testnets introduces: - Network latency (seconds per transaction) - Testnet ADA requirements - External infrastructure dependencies The Scalus [Emulator](/docs/testing/emulator) is excellent for fast development cycles — it runs Plutus scripts, validates transactions using Cardano ledger rules, and operates entirely in-memory with instant feedback. For most unit tests and rapid iteration, the Emulator is the right choice. Yaci DevKit complements the Emulator for scenarios requiring **complete Cardano node semantics**: the real Haskell node implementation, actual block production, consensus timing, and the full ledger rule set. Use it when you need confidence that your code works exactly as it will on mainnet. ## Provider The `IntegrationTestContext` includes a `BlockchainProvider` connected to the container's Yaci Store API. This provides a Blockfrost-compatible interface locally — your [transaction building](/docs/transactions) code works the same against local devnet and real networks: ```scala val ctx = createTestContext() // Same API as production Blockfrost val utxos = ctx.provider.findUtxos(ctx.alice.address).await() val params = ctx.provider.fetchLatestParams.await() ``` No API key needed — Yaci Store runs locally without authentication. ## YaciDevKit Trait The `YaciDevKit` trait handles container lifecycle automatically via ScalaTest's `BeforeAndAfterAll` hooks. Extend it in your test suite to get a running devnet: ```scala import org.scalatest.funsuite.AnyFunSuite import scalus.testing.yaci.YaciDevKit import scalus.cardano.txbuilder.TxBuilder import scalus.cardano.ledger.Value import scalus.utils.await import scala.concurrent.duration.* class MyIntegrationTest extends AnyFunSuite with YaciDevKit { test("submit transaction to local devnet") { val ctx = createTestContext() val tx = TxBuilder(ctx.cardanoInfo) .payTo(recipientAddress, Value.ada(10)) .complete(ctx.provider, ctx.alice.address) .await(30.seconds) .sign(ctx.alice.signer) .transaction ctx.submitTx(tx) match { case Right(txHash) => println(s"Transaction submitted: $txHash") ctx.waitForBlock() case Left(error) => fail(s"Submission failed: $error") } } } ``` ## IntegrationTestContext `createTestContext()` returns an `IntegrationTestContext` containing everything needed for transaction building and submission: ```scala trait IntegrationTestContext { def cardanoInfo: CardanoInfo // Protocol params + network + slot config def provider: BlockchainProvider // BlockfrostProvider to Yaci Store def parties: IndexedSeq[TestParty] // Pre-funded test parties def alice: TestParty // First test party def bob: TestParty // Second test party def eve: TestParty // Third test party } case class TestParty( party: Party, address: ShelleyAddress, addrKeyHash: AddrKeyHash, signer: TransactionSigner ) ``` The helper methods `submitTx()` and `waitForBlock()` handle common operations: ```scala // Submit and wait for confirmation ctx.submitTx(tx) match { case Right(txHash) => ctx.waitForBlock() case Left(error) => // handle error } ``` Yaci DevKit produces blocks approximately every 2 seconds, so `waitForBlock()` is a simple sleep-based wait suitable for test scenarios. ## Container Lifecycle Container management uses reference counting to allow multiple test suites to share a single container instance. The first suite to start acquires the container (starting it if needed), and the last suite to finish releases it: ```scala override def beforeAll(): Unit = { super.beforeAll() _container = YaciContainer.acquire(yaciConfig) } override def afterAll(): Unit = { YaciContainer.release() super.afterAll() } ``` Container cleanup is automatic — [Testcontainers](https://www.testcontainers.org/) stops containers when the JVM exits. Enable `reuseContainer` to keep containers running between test executions. ## Configuration Override `yaciConfig` to customize container behavior: ```scala import scalus.testing.yaci.{YaciDevKit, YaciConfig} class MyIntegrationTest extends AnyFunSuite with YaciDevKit { override def yaciConfig = YaciConfig( enableLogs = true, // Print container logs to console reuseContainer = true, // Reuse container across test runs containerName = "my-devkit" // Custom container name (for reuse) ) } ``` Setting `reuseContainer = true` significantly speeds up development iteration by keeping the container running between test executions. The container continues running with the same state, eliminating startup time. ## Pre-funded Wallet Yaci DevKit provides a pre-funded test wallet using a fixed mnemonic: ``` test test test test test test test test test test test test test test test test test test test test test test test sauce ``` This 24-word mnemonic produces deterministic addresses that Yaci DevKit pre-funds with test ADA. The `TestContext` automatically creates a wallet using HD derivation path `m/1852'/1815'/0'/0/0`. This is a **test-only** mnemonic. Never use it for real funds. ## Additional Signing Some operations require multiple signers. The default `ctx.alice.signer` only includes the payment key. For stake operations or governance actions, construct a signer with additional keys from the `Party` object: ```scala // Stake delegation requires both payment and stake keys val stakeSigner = new TransactionSigner( Set(Party.Alice.account.paymentKeyPair, Party.Alice.account.stakeKeyPair) ) TxBuilder(ctx.cardanoInfo) .delegateTo(stakeAddress, poolId) .complete(ctx.provider, ctx.alice.address) .await(30.seconds) .sign(stakeSigner) .transaction ``` The `Party` account provides key pairs for different purposes: - `paymentKeyPair` - For transaction fees and payments - `stakeKeyPair` - For stake registration and delegation ## Example: Minting Tokens Here's a complete example demonstrating minting tokens with a Plutus script: ```scala import scalus.compiler.compile import scalus.uplc.builtin.Data import scalus.cardano.ledger.{Script, AssetName, Coin} import scalus.{toUplc, plutusV2} test("mint tokens with PlutusV2 script") { val ctx = createTestContext() // Always-succeeds minting policy val mintingPolicy = compile { (_: Data, _: Data) => () } val script = Script.PlutusV2( mintingPolicy.toUplc().plutusV2.cborByteString ) val policyId = script.scriptHash val assetName = AssetName.fromString("TestToken") val mintAmount = 1000L val tx = TxBuilder(ctx.cardanoInfo) .mint(script, Map(assetName -> mintAmount), ()) .payTo(ctx.alice.address, Value.asset(policyId, assetName, mintAmount, Coin.ada(2))) .complete(ctx.provider, ctx.alice.address) .await(30.seconds) .sign(ctx.alice.signer) .transaction ctx.submitTx(tx) match { case Right(txHash) => println(s"Minted $mintAmount tokens: $txHash") ctx.waitForBlock() // Verify minted tokens appear in wallet val utxos = ctx.provider.findUtxos(ctx.alice.address).await() val hasMintedTokens = utxos.exists { utxo => utxo.value.value.multiAsset.exists { case (pid, assets) => pid == policyId && assets.get(assetName).contains(mintAmount) } } assert(hasMintedTokens, "Minted tokens should appear in wallet") case Left(error) => fail(s"Minting failed: $error") } } ``` ## When to Use Which | Scenario | Emulator | Yaci DevKit | |----------|:--------:|:-----------:| | Unit tests for transaction logic | ✓ | | | Plutus script validation | ✓ | ✓ | | Fast development iteration | ✓ | | | Full Cardano node semantics | | ✓ | | Real block production timing | | ✓ | | Pre-deployment confidence testing | | ✓ | | CI/CD pipelines (speed matters) | ✓ | | | CI/CD pipelines (accuracy matters) | | ✓ | **Use Emulator** for rapid development cycles. It runs Plutus scripts and validates transactions using Scalus's ledger rule implementations — instant feedback, no Docker required. **Use Yaci DevKit** when you need the real Cardano node: complete ledger rules, actual block production, and mainnet-identical behavior. Ideal for final validation before deployment. ## Multiplatform Support `scalus-testkit` is structured as a cross-platform module with shared abstractions in `shared/` and JVM-specific implementation in `jvm/`: ``` scalus-testkit/ ├── shared/src/main/scala/scalus/testing/ │ ├── yaci/YaciConfig.scala # Configuration (cross-platform) │ └── kit/Party.scala # Test party definitions │ └── jvm/src/main/scala/scalus/testing/ ├── integration/ │ ├── IntegrationTestContext.scala # Test context trait │ └── YaciTestContext.scala # Yaci implementation └── yaci/ ├── YaciContainer.scala # Docker container management (JVM-only) └── YaciDevKit.scala # ScalaTest integration (JVM-only) ``` This split exists because Yaci DevKit requires Docker/testcontainers (JVM-only) and Bloxbean Cardano Client libraries (JVM-only). The cross-platform abstractions like `TestContext` allow future alternative implementations for other platforms if suitable devnet solutions emerge for JavaScript or Native. ## Troubleshooting ### Container fails to start **Docker not running**: Ensure Docker Desktop (or daemon) is running: ```bash docker info ``` **Port conflicts**: Yaci DevKit uses ports 3001, 8080, 10000. Check for conflicts: ```bash docker ps ``` **Image pull fails**: Manually pull the image to see detailed errors: ```bash docker pull bloxbean/yaci-devkit:latest ``` ### Tests hang or timeout **Slow first run**: First execution downloads the ~1GB image. Set longer timeout or pre-pull the image. **Container reuse issues**: If using `reuseContainer = true` and tests fail unexpectedly, stop and remove the container: ```bash docker stop my-devkit && docker rm my-devkit ``` ### Transaction submission fails **Insufficient funds**: The pre-funded wallet has limited test ADA. For tests that consume many UTxOs, consider splitting operations across multiple test runs. **Script validation errors**: Enable container logs to see detailed Plutus execution traces: ```scala override def yaciConfig = YaciConfig(enableLogs = true) ``` ## See Also - [Emulator](/docs/testing/emulator) — In-memory Cardano node with Plutus execution and ledger validation - [Transaction Builder](/docs/transactions) — Building and submitting transactions - [Unit Testing](/docs/testing/unit-testing) — Property-based testing with ScalaCheck --- Source: https://scalus.org/docs/testing/protocol-version-builtins --- # Plutus Builtins and Protocol Versions The Scalus Emulator supports all Plutus builtins across all protocol versions. By default, `Emulator.withAddresses` and `Context.testMainnet()` use the latest mainnet protocol parameters -- protocol version 11 (van Rossem hard fork) -- so all builtins, including the batch6 additions (`expModInteger`, `dropList`, the array operations, `bls12_381_G1/G2_multiScalarMul`, and the `Value` builtins), are available without any extra configuration. ## How Builtin Availability Is Enforced Before a script executes, the emulator checks that every builtin it uses was available at the current protocol version, which mimics the ledger behavior. In particular, Scalus places this check in `ScriptsWellFormedValidator`, which calls `PlutusScript.isWellFormed`: ```scala val collectedBuiltins = term.collectBuiltins val allowedBuiltins = Builtins.findBuiltinsIntroducedIn(language, majorProtocolVersion) collectedBuiltins.subsetOf(allowedBuiltins) ``` A script using a builtin not yet available at the configured protocol version is rejected with a respective `ValidationError`. ## Verifying Rejection at an Older Protocol Version Suppose you want to confirm that a script using `Ripemd_160` (introduced at Plomin, PV10) is correctly rejected by a node running at Chang (PV9). Configure the emulator with earlier protocol parameters: ```scala import scalus.cardano.ledger.{AssetName, ProtocolVersion, Value} import scalus.cardano.ledger.rules.Context import scalus.cardano.node.Emulator import scalus.cardano.txbuilder.TxBuilder import scalus.compiler.Options import scalus.uplc.PlutusV3 import scalus.uplc.builtin.Builtins.{equalsInteger, lengthOfByteString, ripemd_160} import scalus.uplc.builtin.{ByteString, Data} import scalus.cardano.onchain.plutus.prelude.require // Compile a script that uses ripemd_160. val script = { given Options = Options.debug PlutusV3.compile { (_: Data) => val hash = ripemd_160(ByteString.fromHex("deadbeef")) require(equalsInteger(lengthOfByteString(hash), BigInt(20))) }.script } // Build a transaction that includes the script. val policyId = script.scriptHash val tx = TxBuilder(testEnv) .mint(script, Map(AssetName.fromString("token") -> 1L), Data.unit) .payTo(Alice.address, Value.asset(policyId, AssetName.fromString("token"), 1L)) .complete(initialUtxos, Alice.address) .sign(Alice.signer) .transaction val previousVersionContext = Context.testMainnet().copy( env = Context.testMainnet().env.copy( params = Context.testMainnet().env.params.copy( protocolVersion = ProtocolVersion(9, 0) ) ) ) val emulator = Emulator(initialUtxos = initialUtxos, initialContext = previousVersionContext) val result = emulator.submit(tx).await() assert(result.isLeft) assert(result.swap.toOption.get.message.contains("Ill-formed scripts")) ``` ## Accepting the Same Script With Latest Parameters The default emulator, which is configured with the latest mainnet parameters, accepts the same transaction without issue: ```scala val emulator = Emulator.withAddresses(Seq(Alice.address)) val utxos = emulator.findUtxos(Alice.address).await().toOption.get val tx = TxBuilder(testEnv) .mint(script, Map(AssetName.fromString("token") -> 1L), Data.unit) .payTo(Alice.address, Value.asset(policyId, AssetName.fromString("token"), 1L)) .complete(utxos, Alice.address) .sign(Alice.signer) .transaction val result = emulator.submit(tx).await() assert(result.isRight) ``` This is the default. No configuration is needed to use the latest builtins. ## See Also - [Emulator](/docs/testing/emulator) -- Emulator setup and usage - [Ledger Rules](/docs/ledger/ledger-rules) -- Transaction validation rules --- Source: https://scalus.org/docs/testing --- # Cardano Smart Contract Testing Scalus provides comprehensive testing tools for Cardano smart contracts — from unit tests through property-based testing to automated attack simulation. ## Testing Tools Overview | Tool | Purpose | Best For | |------|---------|----------| | **[TDD & ATDD Workflow](/docs/testing/tdd-atdd-workflow)** | Test-first development methodology | Defining behavior before implementation | | **[Unit Testing](/docs/testing/unit-testing)** | ScalusTest trait, assertScriptFail, budget testing | Validator logic, script context, execution costs | | **[Property-Based Testing](/docs/testing/property-based-testing)** | ScalaCheck forAll with random Cardano types | Edge cases, invariant verification | | **[Boundary Testing](/docs/testing/boundary-testing)** | Transaction variations & attack simulation | Vulnerabilities, multi-step state exploration | | **[Debugging](/docs/testing/debugging)** | IDE debugging, logging, error traces | Finding and fixing bugs | | **[Profiling](/docs/testing/profiling)** | CEK machine budget profiling | Finding expensive code paths | | **[Emulator](/docs/testing/emulator)** | In-memory Cardano node | Fast iteration, CI/CD | | **[JS/TS Emulator](/docs/testing/js-emulator)** | Emulator for JavaScript/TypeScript | Browser DApps, Node.js tooling | | **[Local Devnet](/docs/testing/local-devnet)** | Docker-based real Cardano node | Integration tests, pre-deployment | ## Unit Testing Mix `ScalusTest` into your test suite for validator testing with script context builders, result checking, and budget assertions: ```scala class MyValidatorTest extends AnyFunSuite, ScalusTest { test("validator accepts valid input") { val result = contract(scriptCtx.toData).program.evaluateDebug assert(result.isSuccess) assert(result.budget == ExUnits(memory = 42970, steps = 16_307848)) } test("reject invalid input") { assertScriptFail("Expected error message") { txCreator.buildTx(invalidInput) } } } ``` See [Unit Testing](/docs/testing/unit-testing) for the full guide including Party helpers, Emulator setup, and budget tracking. ## Property-Based Testing Use ScalaCheck `forAll` to verify invariants across hundreds of random inputs: ```scala class MyValidatorTest extends AnyFunSuite, ScalusTest, ScalaCheckPropertyChecks { test("bids at or below current highest are rejected") { forAll(Gen.choose(0L, state.currentBid)) { lowBid => val result = Try(evaluateValidator(lowBid)) assert(result.isFailure) } } } ``` Scalus provides `Arbitrary` instances for all Cardano types — `TxInfo`, `Value`, `Address`, `PubKeyHash`, and more. See [Property-Based Testing](/docs/testing/property-based-testing) for custom generators and validator property examples. ## Boundary & Attack Testing Automatically explore transaction variations around boundary values and run pre-built attack patterns: ```scala object AuctionStep extends ContractStepVariations[AuctionState] { def extractState(reader: BlockchainReader)(using ExecutionContext) = ... def makeBaseTx(reader: BlockchainReader, state: AuctionState)(using ExecutionContext) = ... def variations = TxVariations.standard.default[AuctionState]( extractUtxo = _.auctionUtxo, extractDatum = s => updatedDatum(s.currentBid + 1, Alice), redeemer = _ => BidRedeemer.toData, script = auctionScript ) } ``` Includes steal, partial theft, corrupted datum, double satisfaction, and more. Run with ScalaCheck Commands (random multi-step sequences) or Scenario (exhaustive exploration). See [Boundary Testing](/docs/testing/boundary-testing) for the full guide. ## Debugging Debug validators as regular Scala code — use IDE breakpoints, step through execution, and inspect variables: ```scala @Compile object MyValidator extends Validator: inline override def spend(...): Unit = { log("Starting validation") val owner = datum.getOrFail("No datum").to[PubKeyHash] // Set breakpoint here, inspect variables require(tx.signatories.contains(owner), "Not signed") } ``` See [Debugging](/docs/testing/debugging) for IDE setup and logging. ## Local Execution Environments ### Emulator — Fast In-Memory Testing The [Emulator](/docs/testing/emulator) validates transactions and executes Plutus scripts instantly — no Docker required: ```scala val emulator = Emulator.withAddresses(Seq(Alice.address, Bob.address)) val tx = TxBuilder(testEnv) .payTo(Bob.address, Value.ada(10)) .complete(emulator, Alice.address) .await() .sign(Alice.signer) .transaction emulator.submit(tx).await() // Instant validation + script execution ``` ### Local Devnet — Real Cardano Node [Local Devnet](/docs/testing/local-devnet) runs a real Cardano node (Yaci DevKit) in Docker: ```scala class MyTest extends AnyFunSuite with YaciDevKit { test("submit transaction to local devnet") { val ctx = createTestContext() val tx = TxBuilder(ctx.cardanoInfo) .payTo(recipient, Value.ada(10)) .complete(ctx.provider, ctx.address) .await(30.seconds) .sign(ctx.signer) .transaction ctx.submitTx(tx) // Real Cardano node validation } } ``` ## Recommended Workflow **Test pyramid:** Tests → Emulator → Local Devnet → Testnet → Mainnet ``` 1. Write acceptance tests (ATDD) — full transaction scenarios 2. Write unit tests (TDD) — validator behavior per action 3. Implement validator — iterate until tests pass 4. Add boundary & attack testing — catch what you didn't think of 5. Deploy — Emulator → Devnet → Testnet → Mainnet ``` See [TDD & ATDD Workflow](/docs/testing/tdd-atdd-workflow) for the full methodology. ## See Also - [TDD & ATDD Workflow](/docs/testing/tdd-atdd-workflow) — Test-first development for smart contracts - [Unit Testing](/docs/testing/unit-testing) — ScalusTest, assertScriptFail, budget testing - [Property-Based Testing](/docs/testing/property-based-testing) — ScalaCheck forAll with random Cardano types - [Boundary Testing](/docs/testing/boundary-testing) — Transaction variations and attack simulation - [Debugging](/docs/testing/debugging) — IDE debugging and logging - [Profiling](/docs/testing/profiling) — CEK machine budget profiling - [Emulator](/docs/testing/emulator) — In-memory testing with instant feedback - [JS/TS Emulator](/docs/testing/js-emulator) — Emulator for JavaScript and TypeScript - [Local Devnet](/docs/testing/local-devnet) — Integration testing with real Cardano node ## Related - [Smart Contracts](/docs/smart-contracts) — Write validators to test - [Security](/docs/security) — Security testing considerations - [DApp Starter Tutorial](/docs/dapp-development/dapp-starter-tutorial) — Complete testing example --- Source: https://scalus.org/docs/testing/profiling --- # Profiling Cardano Smart Contracts Scalus includes a built-in CEK machine profiler that shows where execution budget is spent — by source location and by builtin function. ## Quick Start Replace `evaluateDebug` with `evaluateProfile` to collect profiling data: ```scala import scalus.uplc.eval.{ProfileFormatter, PlutusVM} given PlutusVM = PlutusVM.makePlutusV3VM() val result = compiled.toUplc().evaluateProfile // result.isSuccess, result.budget, etc. work as usual assert(result.isSuccess) // Access profiling data result.profile.foreach { profile => println(ProfileFormatter.toText(profile)) } ``` ## Output `ProfileFormatter.toText` produces two tables: ``` === Profile by Source Location === location count mem cpu MyValidator.scala:42 153 153000 24480000 Prelude/List.scala:903 87 87000 13920000 === Profile by Function === function count mem cpu HeadList 74755 2392160 6215878250 UnIData 45025 1440800 933998600 MkCons 42126 1348032 3048321612 Total: mem=185927967 cpu=49131853260 ``` **By Source Location** shows which lines of your Scala source code are most expensive. **By Function** shows which UPLC builtin functions consume the most budget. Both tables are sorted by memory descending. ## HTML Output For a richer view with heat-bar visualization: ```scala import java.nio.file.{Files, Paths} result.profile.foreach { profile => Files.writeString(Paths.get("profile.html"), ProfileFormatter.toHtml(profile)) } ``` ## Machine-Readable Output (profile.json + manifest) `PlutusScriptEvaluator` can render profiles to files on every evaluation — that covers everything that runs through the ledger: `Emulator`, `TxBuilder`, transaction validation. (For tests that evaluate a compiled program directly, see [Profiling Direct UPLC Runs](#profiling-direct-uplc-runs) below.) The easiest switch is the environment variable (see [Project Commands](/docs/get-started/project-commands)): ```sh copy SCALUS_PROFILE=full sbt test ``` For each profiled script run this writes, into the report output directory (`SCALUS_DUMP_DIR`, default: `target/scalus`): - `--.profile.html` – the interactive report - `--.profile.json` – machine-readable data (`"schemaVersion": 1`) - `--.profile.csv` – flat table for spreadsheets - `profile-manifest.json` – the discovery entry point listing every run File names are stable per `(scriptHash, redeemer tag, index)`, so repeated evaluations (e.g. during fee balancing) overwrite rather than accumulate. Manifest entries are merged on that same key: a re-evaluated script updates its entry in place, while every other run already in the directory is preserved. The manifest maps each run to its files: ```json { "schemaVersion": 1, "runs": [ { "scriptHash": "ab12…", "language": "PlutusV3", "redeemer": { "tag": "Spend", "index": 0 }, "budget": { "mem": 185927967, "cpu": 49131853260 }, "files": [ { "format": "html", "file": "ab12…-Spend-0.profile.html" }, { "format": "json", "file": "ab12…-Spend-0.profile.json" }, { "format": "csv", "file": "ab12…-Spend-0.profile.csv" } ] } ] } ``` Tools should start from `profile-manifest.json` and check `schemaVersion` (currently `1` for both the manifest and `profile.json`). The Scalus VS Code extension consumes `profile.json` to annotate source lines with per-line cost. The report directory lives under `target/`, which is git-ignored. Editor tooling that discovers files through the workspace search index will not see it — VS Code's `findFiles` honours ignore files. The Scalus Profiler extension handles this from 0.2.2 onwards; older versions find nothing unless you point `SCALUS_DUMP_DIR` at a directory that is not ignored. ## Profiling Direct UPLC Runs Suites that evaluate a compiled program themselves — `runScript`, `evaluateDebug`, `runWithDebug` — never reach `PlutusScriptEvaluator`, so `SCALUS_PROFILE=full` writes no files for them. `ScalusTest` provides `runWithProfileReport` for that case: it evaluates with profiling on and emits the same renderings, into the same directory, indexed in the same manifest. ```scala class VestingValidatorTest extends AnyFunSuite, ScalusTest { test("Successful multiple partial withdrawals") { val result = compiled.scriptV3().runWithProfileReport(scriptContext) assert(result.isSuccess) } } ``` The redeemer tag in the file name follows the script purpose in the `ScriptContext`, so a spending validator lands in `-Spend-0.profile.*` just as it would from the ledger. Calling the method is itself the request for a report, so it writes the full set by default. `SCALUS_PROFILE`, `SCALUS_PROFILE_OUT` and `SCALUS_DUMP_DIR` override that exactly as they do for the ledger — `SCALUS_PROFILE=off` silences the output without touching the test. Because manifest entries merge, ledger- and test-produced runs accumulate side by side and the extension lists them together. ## Toggle Profiling in Tests Add a flag to switch between `evaluateDebug` and `evaluateProfile` without changing test assertions: ```scala class MyValidatorTest extends AnyFunSuite, ScalusTest { val profilingEnabled = false // flip to true when needed extension (term: scalus.uplc.Term) private def eval(using PlutusVM): Result = if profilingEnabled then val result = term.evaluateProfile result.profile.foreach(p => info(ProfileFormatter.toText(p))) result else term.evaluateDebug test("validator budget") { val result = compiled.toUplc().eval // uses evaluateDebug or evaluateProfile assert(result.isSuccess) } } ``` The `result.profile.foreach` line is safe to leave permanently — when profiling is off, `profile` is `None`. This prints to the test log. When you want files an editor can read instead, use [`runWithProfileReport`](#profiling-direct-uplc-runs). ## PlutusVM Methods For script-level profiling (with CIP-117 validation): ```scala val result = vm.evaluateScriptProfile(program.deBruijnedProgram) // result.profile contains profiling data ``` ## Performance Profiling adds ~40% overhead to CEK evaluation (measured on the Knights benchmark: 6s vs 8.4s). When `profiling = false` (the default), there is zero overhead. ## See Also - **[JS/TS Profiling](/docs/testing/js-emulator#profiling-a-script)** — Collect profiling data (JSON) from JavaScript/TypeScript with `Scalus.evaluateScriptProfile` - **[Debugging](/docs/testing/debugging)** — IDE debugging, logging, error traces - **[Unit Testing](/docs/testing/unit-testing)** — Test validators with budget assertions --- Source: https://scalus.org/docs/design-patterns/withdraw-zero --- # Stake Validator Pattern Delegate computation to a staking script using the "withdraw zero trick" to reduce execution costs from O(N²) to O(N). ## The Challenge When a Cardano validator processes multiple UTxOs in a single transaction, the spending script runs once per input. If each execution performs expensive operations (iterating all inputs, checking all outputs), costs grow quadratically: - **10 inputs** = 10 script executions × 10 iterations = 100 operations - **20 inputs** = 20 script executions × 20 iterations = 400 operations This makes batch operations prohibitively expensive. ## How It Works 1. **Spending validator** (runs per UTxO): Minimal logic - just checks stake validator ran 2. **Stake validator** (runs once): Heavy computation in the reward endpoint The spending validator essentially says: "As long as there is a reward withdrawal of the given script in this transaction, this UTxO can be spent." ## When to Use This Pattern **Best for:** - Consolidating complex business logic into a single validation point - Batch operations processing multiple UTxOs together - Separating payment validation from protocol logic (better composability) - Reducing script size and execution costs **Not ideal for:** - Single input transactions (no benefit) - Cases where each input needs unique, complex validation - Protocols where staking credentials aren't suitable ## API Reference | Function | Description | |----------|-------------| | `spend` | Check stake validator ran + validate its redeemer and withdrawal amount | | `spendMinimal` | Just check stake validator ran (most common) | | `withdraw` | Helper for reward endpoint - extracts script hash from credential | ## Implementation Guide ### Spending Endpoint ```scala import scalus.patterns.StakeValidator @Compile object MyValidator extends Validator { inline override def spend( datum: Option[Data], redeemer: Redeemer, tx: TxInfo, ownRef: TxOutRef ): Unit = { val ownScriptHash = tx.findOwnInputOrFail(ownRef).resolved.address.credential .scriptOption.getOrFail("Own address must be Script") // Option 1: Just check stake validator ran (withdraw zero trick) StakeValidator.spendMinimal(ownScriptHash, tx) // Option 2: Also validate redeemer and withdrawal amount StakeValidator.spend( withdrawalScriptHash = ownScriptHash, withdrawalRedeemerValidator = (redeemer, lovelace) => lovelace === BigInt(0), txInfo = tx ) } } ``` ### Reward Endpoint (Stake Validator) ```scala inline override def reward( redeemer: Redeemer, stakingKey: Credential, tx: TxInfo ): Unit = { StakeValidator.withdraw( withdrawalValidator = (redeemer, validatorHash, txInfo) => { // Your heavy validation logic here // This runs ONCE for all inputs val totalInputValue = txInfo.inputs.foldLeft(BigInt(0)) { (acc, input) => acc + input.resolved.value.getLovelace } // Verify outputs match expected distribution true }, redeemer = redeemer, credential = stakingKey, txInfo = tx ) } ``` **Script Configuration:** The spending script address must have a staking credential that points to your withdrawal validator. This is configured when deploying scripts. **Double Satisfaction:** When using this pattern with multiple inputs, ensure proper input-to-output mapping to prevent double satisfaction vulnerabilities. See [Common Vulnerabilities](/docs/security/common-vulnerabilities#3-double-satisfaction). ### Example: Optimized Payment Splitter The `scalus-examples` module includes a side-by-side comparison: | Validator | Description | |-----------|-------------| | `NaivePaymentSplitterValidator` | Runs full validation per UTxO (O(N²)) | | `OptimizedPaymentSplitterValidator` | Uses stake validator pattern (O(N)) | **Real-world savings:** Tests show **~71% reduction** in memory and CPU costs when spending multiple UTxOs with the optimized version. The optimized version also uses `SpendRedeemer.ownInputIndex` to avoid iterating through all inputs to find the own input: ```scala case class SpendRedeemer(ownInputIndex: BigInt) derives ToData, FromData // Spending endpoint - O(1) input lookup inline override def spend(...): Unit = { val ownInput = tx.inputs.at(spendRedeemer.ownInputIndex) require(ownInput.outRef === ownRef, "Own input index mismatch") val ownScriptHash = ownInput.resolved.address.credential .scriptOption.getOrFail("Own address must be Script") // Just check stake validator ran (withdraw zero trick) StakeValidator.spendMinimal(ownScriptHash, tx) } ``` The stake validator's reward endpoint receives pre-computed values and verifies them: ```scala case class SplitVerificationRedeemer( payeeWithChange: PubKeyHash, sumContractInputs: BigInt, splitPerPayee: BigInt, nPayed: BigInt ) derives ToData, FromData // Reward endpoint - runs ONCE, verifies claimed values match transaction inline override def reward(...): Unit = { val verification = redeemer.to[SplitVerificationRedeemer] // Verify sumContractInputs matches actual inputs // Verify outputs match claimed split amounts // All heavy iteration happens here, once } ``` ## Related Patterns - **[Merkelized Validator](/docs/design-patterns/merkelized-validator)** - When spending validators need to read verified data from stake validator - **[Transaction Level Minter](/docs/design-patterns/transaction-level-minting)** - Similar pattern using minting instead of staking ## Resources - [Anastasia Labs: Stake Validator Pattern](https://github.com/Anastasia-Labs/design-patterns/tree/main/stake-validator) - Original pattern documentation - [Scalus Design Patterns](https://github.com/scalus3/scalus/tree/master/scalus-design-patterns) - Implementation and tests - [OptimizedPaymentSplitterValidator](https://github.com/scalus3/scalus/blob/master/scalus-examples/jvm/src/main/scala/scalus/examples/paymentsplitter/OptimizedPaymentSplitterValidator.scala) - Optimized example with 71% savings - [NaivePaymentSplitterValidator](https://github.com/scalus3/scalus/blob/master/scalus-examples/jvm/src/main/scala/scalus/examples/paymentsplitter/PaymentSplitterValidator.scala) - Naive version for comparison - [PaymentSplitterTxBuilderTest](https://github.com/scalus3/scalus/blob/master/scalus-examples/jvm/src/test/scala/scalus/examples/paymentsplitter/PaymentSplitterTxBuilderTest.scala) - Cost comparison tests --- Source: https://scalus.org/docs/design-patterns/transaction-level-minting --- # Transaction Level Minter Pattern Couple spending and minting endpoints of the same validator to delegate heavy computation to a single minting execution. ## The Challenge Like the [Stake Validator Pattern](/docs/design-patterns/withdraw-zero), spending validators that run per-UTxO create quadratic costs for batch operations. However, not all protocols use staking, and some naturally involve minting or burning tokens as part of their logic. ## How It Works 1. **Spending validator** (runs per UTxO): Minimal - just checks minting endpoint executes 2. **Minting validator** (runs once): Heavy computation when minting/burning tokens The spending validator only ensures the minting endpoint executes by verifying a non-zero amount of its asset is being minted or burnt. ## When to Use This Pattern **Best for:** - Protocols that already use beacon tokens or minting/burning - State machines where minting signals state transitions - Cases where token existence proves validation occurred **Not ideal for:** - Protocols without natural minting requirements (use [Stake Validator](/docs/design-patterns/withdraw-zero) instead - lower execution costs) - Cases where spend validation must be independent of minting - When minting logic should not control spending authorization **Prefer Stake Validator when possible.** If minting tokens isn't a natural part of your protocol, use the Stake Validator pattern instead—it has lower execution unit costs. ## API Reference | Function | Description | |----------|-------------| | `spend` | Check minting policy ran + validate its redeemer and minted tokens | | `spendMinimal` | Just check at least one token is minted/burnt with the policy | ## Implementation Guide ### Spending Endpoint ```scala import scalus.patterns.TransactionLevelMinterValidator @Compile object MyValidator extends Validator { inline override def spend( datum: Option[Data], redeemer: Redeemer, tx: TxInfo, ownRef: TxOutRef ): Unit = { val ownScriptHash = tx.findOwnInputOrFail(ownRef).resolved.address.credential .scriptOption.getOrFail("Own address must be Script") // Option 1: Just check minting policy ran TransactionLevelMinterValidator.spendMinimal(ownScriptHash, tx) // Option 2: Also validate redeemer and minted tokens TransactionLevelMinterValidator.spend( minterScriptHash = ownScriptHash, minterRedeemerValidator = _.to[MintRedeemer].isValid, minterTokensValidator = tokens => { val (tokenName, qty) = tokens.toList.head tokenName === utf8"BEACON" && (qty === BigInt(1) || qty === BigInt(-1)) }, txInfo = tx ) } } ``` ### Minting Endpoint ```scala inline override def mint( redeemer: Redeemer, policyId: PolicyId, tx: TxInfo ): Unit = { val mintRedeemer = redeemer.to[MintRedeemer] // Your heavy validation logic here - runs ONCE val scriptInputsCount = tx.inputs.foldRight(BigInt(0)) { (input, acc) => input.resolved.address.credential match case Credential.ScriptCredential(hash) if hash === policyId => acc + 1 case _ => acc } require(scriptInputsCount === mintRedeemer.expectedInputCount, "Input count mismatch") // Verify all outputs meet requirements // ... additional validation logic } ``` ### Example: Beacon Token Protocol A common use case is minting/burning a "beacon" token to signal state changes: ```scala case class SpendRedeemer(ownIndex: BigInt, burn: Boolean) derives FromData case class MintRedeemer(maxUtxosToSpend: BigInt) derives FromData // Spending endpoint - checks beacon is minted/burnt inline override def spend(datum: Option[Data], redeemer: Redeemer, tx: TxInfo, ownRef: TxOutRef): Unit = { val spendRedeemer = redeemer.to[SpendRedeemer] val ownHash = tx.inputs.get(spendRedeemer.ownIndex) .getOrFail("Invalid index").resolved.address.credential .scriptOption.getOrFail("Must be script") TransactionLevelMinterValidator.spend( minterScriptHash = ownHash, minterRedeemerValidator = _.to[MintRedeemer].maxUtxosToSpend > 0, minterTokensValidator = tokens => { val (tokenName, qty) = tokens.toList.head require(tokenName === utf8"BEACON") if spendRedeemer.burn then qty === BigInt(-1) else qty === BigInt(1) }, txInfo = tx ) } // Minting endpoint - heavy logic runs once inline override def mint(redeemer: Redeemer, policyId: PolicyId, tx: TxInfo): Unit = { val mintRedeemer = redeemer.to[MintRedeemer] val scriptInputsCount = countScriptInputs(tx, policyId) require(scriptInputsCount === mintRedeemer.maxUtxosToSpend) } ``` **Stake Validator vs Transaction Level Minter:** Both patterns delegate computation to a single execution. Choose based on your protocol: - **Stake Validator**: When you don't need minting/burning, or staking is already part of your design - **Transaction Level Minter**: When beacon tokens or minting/burning naturally fits your protocol ## Related Patterns - **[Stake Validator](/docs/design-patterns/withdraw-zero)** - Similar pattern using staking instead of minting - **[Merkelized Validator](/docs/design-patterns/merkelized-validator)** - When spending validators need to read verified data ## Resources - [Anastasia Labs: Transaction Level Validator](https://github.com/Anastasia-Labs/design-patterns/tree/main/transaction-level-validator-minting-policy) - Original pattern documentation - [Scalus Design Patterns](https://github.com/scalus3/scalus/tree/master/scalus-design-patterns) - Implementation and tests - [TransactionLevelMinterValidatorExample](https://github.com/scalus3/scalus/blob/master/scalus-design-patterns/src/main/scala/scalus/examples/TransactionLevelMinterValidatorExample.scala) - Complete example --- Source: https://scalus.org/docs/design-patterns/merkelized-validator --- # Merkelized Validator Pattern Extend the [Stake Validator Pattern](/docs/design-patterns/withdraw-zero) to **delegate computation** to a withdrawal script and let spending validators **read verified results** from its redeemer. ## The Challenge Cardano validators face two constraints that compound in batch operations: **Script size limits.** Validators are bounded by the ~16KB reference script limit per script. Optimization techniques like loop unrolling and function inlining reduce ExUnits but increase script size — often pushing past the limit. There's no way to split a single validator's logic across multiple scripts. **Redundant computation.** When processing multiple UTxOs, each spending validator runs independently. Expensive computations (e.g., calculating a clearing price, verifying a Merkle proof) execute N times — once per input. Even with the Stake Validator pattern, spending validators can only verify that the stake validator ran; they cannot access its computation results. The Merkelized Validator pattern solves both: move expensive logic into a separate withdrawal script (bypassing size limits) and let spending validators read its verified output (avoiding redundant computation). ## How It Works 1. Off-chain code computes expensive values (e.g., clearing price, settlement amounts) 2. Values are included in the stake validator's redeemer 3. Stake validator verifies the values are correct (runs **once**) 4. Spending validators read the verified values via `MerkelizedValidator` (runs per UTxO) This reduces complexity from O(N²) to O(N) for batch operations while giving each spending validator access to shared, verified data. For script size optimization, the same mechanism applies: the heavy computation logic lives in the withdrawal script (which can use its own ~16KB budget for aggressive optimizations), while the spending validator stays small — it just reads the result. **Reference script limits.** The total size of reference scripts in a transaction is capped at 200KiB, and fees increase exponentially with size. Plan your script splitting accordingly. ## When to Use This Pattern **Best for:** - **Script size optimization** — move heavy logic (loop-unrolled, inlined) into a separate withdrawal script, keeping the spending validator small - **Batch operations** — auctions, settlements, or order matching where all inputs need the same computed result (clearing price, exchange rate) - **Shared verified state** — any scenario where spending validators need access to a value that's expensive to compute but only needs verification once **Not ideal for:** - Simple validation that doesn't need shared data (use [Stake Validator](/docs/design-patterns/withdraw-zero) instead) - Single-input transactions - When each input needs completely independent validation | Pattern | Use Case | |---------|----------| | **StakeValidator.spendMinimal** | Only need to check stake validator ran | | **MerkelizedValidator.verifyAndGetRedeemer** | Need to **read** verified data | ## API Reference | Function | Description | |----------|-------------| | `getStakeRedeemer(hash, txInfo)` | Retrieves the stake validator's redeemer | | `verifyAndGetRedeemer(hash, txInfo)` | Verifies withdrawal exists AND returns redeemer | ## Implementation Guide ### Stake Validator Redeemer Define a redeemer type that carries the verified computation results: ```scala case class AuctionSettlementRedeemer( clearingPrice: BigInt, totalUnitsAvailable: BigInt ) derives ToData, FromData ``` ### Spending Validator Read the verified data using `MerkelizedValidator`: ```scala import scalus.patterns.MerkelizedValidator @Compile object BatchAuctionValidator extends Validator { inline override def spend( datum: Option[Data], redeemer: Data, tx: TxInfo, ownRef: TxOutRef ): Unit = { val ownScriptHash = tx.findOwnInputOrFail(ownRef).resolved.address.credential .scriptOption.getOrFail("Own address must be Script") // Read verified settlement data from stake validator val stakeRedeemer = MerkelizedValidator.verifyAndGetRedeemer(ownScriptHash, tx) val settlement = stakeRedeemer.to[AuctionSettlementRedeemer] // Use the verified clearing price val bid = datum.getOrFail("Missing datum").to[BidDatum] if bid.bidPrice >= settlement.clearingPrice then // Fill the bid - verify bidder receives tokens else // Refund the bid - verify bidder receives ADA back } } ``` ### Stake Validator (Reward Endpoint) Verify the computation results are correct: ```scala inline override def reward(redeemer: Redeemer, stakingKey: Credential, tx: TxInfo): Unit = { val settlement = redeemer.to[AuctionSettlementRedeemer] // Verify clearing price calculation require(settlement.clearingPrice > BigInt(0), "Clearing price must be positive") // Verify supply/demand balance val totalDemand = calculateTotalDemand(tx, settlement.clearingPrice) require(totalDemand <= settlement.totalUnitsAvailable, "Demand exceeds supply") // ... additional verification logic } ``` **Performance Benefit:** When spending N UTxOs with iteration-heavy logic: - **Without pattern**: O(N²) - each spending validator iterates all inputs/outputs - **With pattern**: O(N) - stake validator iterates once, spending validators just read Run `BatchAuctionTest` to see actual memory/CPU savings. **Script Configuration:** The spending script address must have a staking credential that points to your withdrawal validator. Configure this when deploying scripts. ### Example: Batch Auction See `scalus.examples.BatchAuctionValidator` for a complete implementation where: - **Stake validator**: Verifies the clearing price calculation once - **Spending validator**: Reads the verified clearing price to determine if each bid is filled or refunded ## Related Patterns - **[Stake Validator](/docs/design-patterns/withdraw-zero)** - Base pattern when you don't need to read data - **[Transaction Level Minter](/docs/design-patterns/transaction-level-minting)** - Alternative using minting instead of staking ## Resources - [Anastasia Labs: Merkelized Validator](https://github.com/Anastasia-Labs/design-patterns/tree/main/merkelized-validators) - Original pattern documentation - [Scalus Design Patterns](https://github.com/scalus3/scalus/tree/master/scalus-design-patterns) - Implementation and tests - [BatchAuctionTest](https://github.com/scalus3/scalus/blob/master/scalus-design-patterns/src/test/scala/scalus/patterns/BatchAuctionTest.scala) - Test suite with budget comparison --- Source: https://scalus.org/docs/design-patterns/parameter-validation --- # Parameter Validation Pattern Verify that script instances are legitimate instantiations of parameterized scripts with specific parameter values. ## The Challenge When a minting policy needs to ensure tokens go only to a spending script parameterized with a specific value (e.g., a royalty address), it cannot directly inspect the target script's parameters. Each different parameter creates a different script hash, making it impossible to distinguish between legitimate and arbitrary script destinations. **Example problem:** An NFT creator wants to ensure their NFTs are always minted directly to a marketplace that enforces their royalty settings. Without parameter validation, a minting policy cannot verify the destination script has the correct creator address baked in. ## How It Works Script hashing in Cardano follows this formula: ``` script_hash = blake2b_224(language_tag ++ cbor_encoded_program) Language tags: - PlutusV1: 0x01 - PlutusV2: 0x02 - PlutusV3: 0x03 When parameters are applied: parameterized_program = base_program $ param1 $ param2 ... parameterized_hash = blake2b_224(language_tag ++ cbor(parameterized_program)) ``` The pattern: 1. **Off-chain**: Compile base script, apply parameters, compute expected hash 2. **Off-chain**: Pass expected hash to the dependent script (e.g., minting policy) 3. **On-chain**: Dependent script verifies outputs go to addresses matching expected hash ## When to Use This Pattern **Best for:** - Minting policies that need to verify tokens go to specific parameterized scripts - Multi-script coordination where scripts must verify each other's parameters - Factory patterns where minted tokens reference scripts with verified parameters - Royalty enforcement across marketplaces **Not ideal for:** - Simple single-script validators - Cases where parameter verification isn't security-critical ## API Reference ### Off-chain Functions (`ParameterValidation`) | Function | Description | |----------|-------------| | `computeScriptHashV3` | Compute hash for PlutusV3 script with applied parameters | | `computeScriptHashV2` | Compute hash for PlutusV2 script with applied parameters | | `computeScriptHashV1` | Compute hash for PlutusV1 script with applied parameters | ### On-chain Functions (`ParameterValidationOnChain`) | Function | Description | |----------|-------------| | `verifyScriptCredential` | Verify credential matches expected script hash (fails if not) | | `verifyAddressScript` | Verify address has expected script credential (fails if not) | | `findOutputsToScript` | Find outputs sent to a specific script hash | | `isExpectedScript` | Check if credential matches expected hash (returns Boolean) | ## Implementation Guide ### Off-chain: Computing Expected Hash ```scala import scalus.patterns.ParameterValidation import scalus.uplc.builtin.Data.toData // Compute the expected script hash for a parameterized script def computeMarketplaceHash(creatorPkh: PubKeyHash): ValidatorHash = { ParameterValidation.computeScriptHashV3( MarketplaceBaseProgram.program.deBruijnedProgram, creatorPkh.toData ) } // Create parameterized scripts val marketplaceHash = computeMarketplaceHash(creatorPkh) val marketplace = MarketplaceBaseProgram.program.deBruijnedProgram $ creatorPkh.toData val mintingPolicy = NFTMintingBaseProgram.program.deBruijnedProgram $ NFTMintParams(marketplaceHash, tokenName).toData ``` ### On-chain: Verifying Script Destination ```scala import scalus.patterns.ParameterValidationOnChain @Compile object NFTMintingPolicy { inline def validate(params: NFTMintParams)(scData: Data): Unit = { val sc = scData.to[ScriptContext] sc.scriptInfo match case ScriptInfo.MintingScript(policyId) => val mintedAmount = sc.txInfo.mint.quantityOf(policyId, params.tokenName) require(mintedAmount === BigInt(1), "Must mint exactly 1 NFT") // Find output containing our NFT val nftOutput = sc.txInfo.outputs.find { output => output.value.quantityOf(policyId, params.tokenName) > 0 }.getOrFail("NFT output not found") // Verify output goes to the expected marketplace script ParameterValidationOnChain.verifyAddressScript( nftOutput.address, params.expectedMarketplaceHash ) case _ => fail("Unsupported script purpose") } } ``` **Key insight:** The expected hash is computed off-chain and passed as a parameter to the minting policy. The on-chain code only needs to compare hashes, not recompute them. ### Example: NFT with Verified Marketplace The complete example shows an NFT minting policy that ensures NFTs can only be sent to a marketplace parameterized with the correct creator royalty address. **Marketplace validator (parameterized by creator):** ```scala @Compile object MarketplaceValidator { inline def validate(creatorPkh: PubKeyHash)(scData: Data): Unit = { val sc = scData.to[ScriptContext] sc.scriptInfo match case ScriptInfo.SpendingScript(_, datum) => val listing = datum.getOrFail("Datum required").to[Listing] val action = sc.redeemer.to[MarketplaceRedeemer] action match case MarketplaceRedeemer.Buy => // Verify royalty payment to creator (10%) val royaltyAmount = listing.price * BigInt(10) / BigInt(100) val creatorCred = Credential.PubKeyCredential(creatorPkh) // ... verify payments to creator and seller case MarketplaceRedeemer.Cancel => require(sc.txInfo.signatories.contains(listing.seller)) case _ => fail("Unsupported script purpose") } } ``` **Complete workflow:** ```scala object ParameterValidationUsage { // 1. Compute marketplace hash for a given creator def computeMarketplaceHash(creatorPkh: PubKeyHash): ValidatorHash = { ParameterValidation.computeScriptHashV3( MarketplaceBaseProgram.program.deBruijnedProgram, creatorPkh.toData ) } // 2. Create parameterized marketplace script def createMarketplaceScript(creatorPkh: PubKeyHash) = { MarketplaceBaseProgram.program.deBruijnedProgram $ creatorPkh.toData } // 3. Create parameterized NFT minting policy def createNFTMintingPolicy(expectedMarketplaceHash: ValidatorHash, tokenName: TokenName) = { val params = NFTMintParams(expectedMarketplaceHash, tokenName) NFTMintingBaseProgram.program.deBruijnedProgram $ params.toData } // 4. Complete setup def setupNFTWithMarketplace(creatorPkh: PubKeyHash, tokenName: TokenName) = { val marketplaceHash = computeMarketplaceHash(creatorPkh) val marketplace = createMarketplaceScript(creatorPkh) val mintingPolicy = createNFTMintingPolicy(marketplaceHash, tokenName) (marketplace, mintingPolicy, marketplaceHash) } } ``` **Security consideration:** The expected hash must be computed correctly off-chain. If an attacker can influence the hash computation, they could redirect tokens to malicious scripts. ## Alternative Approaches | Approach | Pros | Cons | |----------|------|------| | **Parameter Validation** | Direct verification, no runtime overhead | Requires off-chain hash computation | | **Beacon tokens** | Flexible, can verify at runtime | Additional minting policy needed | | **Oracle/reference inputs** | Dynamic parameter updates | Centralization risk | ## Related Patterns - **[Parameterized Validators](/docs/smart-contracts/parameterized-validators)** — How to create validators with compile-time parameters - **[Stake Validator](/docs/design-patterns/withdraw-zero)** — Optimize multi-input transactions - **[Transaction Level Minter](/docs/design-patterns/transaction-level-minting)** — Couple spending and minting endpoints ## Resources - [Anastasia Labs: Parameter Validation](https://github.com/Anastasia-Labs/aiken-design-patterns/blob/develop/README.md#parameter-validation) - Original pattern documentation - [Scalus Design Patterns](https://github.com/scalus3/scalus/tree/master/scalus-design-patterns) - Implementation and tests - [ParameterValidationExample](https://github.com/scalus3/scalus/blob/master/scalus-design-patterns/src/main/scala/scalus/examples/ParameterValidationExample.scala) - Complete example - [ParameterValidationTest](https://github.com/scalus3/scalus/blob/master/scalus-design-patterns/src/test/scala/scalus/patterns/ParameterValidationTest.scala) - Tests --- Source: https://scalus.org/docs/design-patterns/utxo-indexer --- # UTxO Indexer Pattern Map input UTxOs to output UTxOs using pre-computed indices, reducing on-chain validation from O(n) search to O(1) lookup. ## The Challenge When smart contracts handle multiple inputs and outputs in a single transaction, validation challenges arise: - **Multiple satisfaction** - A single output can be paired with multiple inputs, leading to value loss - **Unaccounted outputs** - Additional outputs can be added without validator checks - **Pairing complexity** - Determining how inputs and outputs correspond becomes expensive on-chain Searching through all inputs/outputs on-chain costs O(n) per lookup, making batch operations expensive. ## How It Works Instead of searching on-chain, the validator receives UTxO indices via the redeemer: 1. Transaction builder constructs the transaction 2. Builder calculates input/output indices after construction 3. Indices are embedded in the redeemer 4. Validator verifies correctness at known indices (O(1)) This leverages Cardano's deterministic script evaluation property, which guarantees that script interpreter arguments are fixed and outcomes depend solely on transaction contents. ## When to Use This Pattern **Best for:** - Validators handling multiple inputs and outputs - Protocols requiring explicit input-to-output mapping - Preventing double satisfaction attacks - Reducing on-chain search costs **Not ideal for:** - Simple single input/output scenarios - Cases where input-output pairing is implicit - Validators with minimal state transitions ## API Reference | Function | Use Case | |----------|----------| | `validateInput` | Validate a single input at a known index | | `oneToOne` | Map one input to one output | | `oneToMany` | Map one input to multiple outputs | | `multiOneToOneNoRedeemer` | Map multiple script inputs to outputs (same redeemer) | | `multiOneToOneWithRedeemer` | Map multiple script inputs with different redeemers | ## Implementation Guide ### One-to-One Indexer The most common case - map one input to one output: ```scala import scalus.patterns.UtxoIndexer case class IndexerRedeemer(inputIdx: BigInt, outputIdx: BigInt) derives FromData, ToData @Compile object MyValidator extends Validator { inline override def spend( datum: Option[Data], redeemer: Data, tx: TxInfo, ownRef: TxOutRef ): Unit = { val IndexerRedeemer(inputIdx, outputIdx) = redeemer.to[IndexerRedeemer] UtxoIndexer.oneToOne( ownRef, inputIdx, outputIdx, tx, validator = (input, output) => { // Your validation logic: check values, datums, addresses, etc. input.resolved.value.getLovelace === output.value.getLovelace } ) } } ``` ### One-to-Many Indexer Map one input to multiple outputs: ```scala UtxoIndexer.oneToMany( ownRef, inputIdx, outputIndices = List(0, 2, 4), // Non-contiguous indices supported tx, perOutputValidator = (input, idx, output) => { // Validate each output individually output.value.getLovelace >= minAmount }, collectiveValidator = (input, outputs) => { // Validate all outputs together outputs.foldLeft(BigInt(0))(_ + _.value.getLovelace) === input.resolved.value.getLovelace } ) ``` ### Multiple One-to-One (No Redeemer) Process multiple script UTxOs with the same validation logic: ```scala UtxoIndexer.multiOneToOneNoRedeemer( indexPairs = List((0, 0), (2, 1), (3, 2)), // (inputIdx, outputIdx) pairs scriptHash = ownScriptHash, tx = txInfo, validator = (inIdx, input, outIdx, output) => { // Validate each input-output pair input.resolved.value.getLovelace === output.value.getLovelace } ) ``` ### Multiple One-to-One (With Redeemer) When each input needs different redeemer data. Requires a staking script as coupling mechanism: ```scala UtxoIndexer.multiOneToOneWithRedeemer[MyRedeemer]( indexPairs = List((0, 0), (1, 1)), spendingScriptHash = spendScriptHash, stakeScriptHash = stakeScriptHash, tx = txInfo, redeemerCoercerAndStakeExtractor = (data: Data) => { val r = data.to[MySpendRedeemer] (r.payload, r.stakeCredential) }, validator = (inIdx, input, redeemer, outIdx, output) => { // Validate with per-input redeemer data true } ) ``` ### Off-Chain Index Computation Use `TxBuilder` with a redeemer builder function to compute indices after the transaction is assembled: ```scala import scalus.cardano.txbuilder.TxBuilder TxBuilder(env) .spend( scriptUtxo, redeemerBuilder = (tx: Transaction) => { val inputIdx = tx.body.value.inputs.toSeq.indexOf(scriptUtxo.input) val outputIdx = tx.body.value.outputs.indexWhere(_.address == recipientAddress) IndexerRedeemer(BigInt(inputIdx), BigInt(outputIdx)).toData }, script ) .payTo(recipientAddress, value) ``` **Input Ordering:** Inputs are reordered deterministically (first by transaction hash, then by output index). Transaction builders must account for this before generating redeemers. **Double Satisfaction:** The singular UTxO indexer patterns (`oneToOne`, `oneToMany`) do not provide built-in protection against double satisfaction. Implement your own protection based on your contract's needs. See [Common Vulnerabilities](/docs/security/common-vulnerabilities#3-double-satisfaction). ## Related Patterns - **[Stake Validator](/docs/design-patterns/withdraw-zero)** - For reducing per-input execution costs - **[Linked List](/docs/design-patterns/linked-list)** - For ordered on-chain data structures ## Resources - [Anastasia Labs: UTxO Indexers](https://github.com/Anastasia-Labs/design-patterns/tree/main/utxo-indexers) - Original pattern documentation with all variations - [Scalus Design Patterns](https://github.com/scalus3/scalus/tree/master/scalus-design-patterns) - Implementation and tests - [UtxoIndexerExample](https://github.com/scalus3/scalus/blob/master/scalus-design-patterns/src/main/scala/scalus/examples/UtxoIndexerExample.scala) - Complete example with off-chain code --- Source: https://scalus.org/docs/design-patterns/linked-list --- # On-Chain Linked List Distribute data across multiple UTxOs to overcome datum size limits and enable uniqueness proofs on-chain. ``` ╭──────╮ ╭───────╮ ╭────────╮ ╭────────╮ ╭───────╮ │ Root ├─>│ Apple ├─>│ Banana ├─>│ Orange ├─>│ Peach │ ╰──────╯ ╰───────╯ ╰────────╯ ╰────────╯ ╰───────╯ ``` ## The Challenge Two fundamental limitations in Cardano's UTxO model: 1. **Datum size limits** - Storing lists in datums is impractical. As a list grows, it can lead to unspendable UTxOs due to limited on-chain resources and transaction size limits. 2. **Non-existence proofs** - Validating that something *doesn't* exist on-chain is impossible with standard UTxOs. You only have access to transaction inputs, so detecting duplicates (e.g., preventing double votes) requires a data structure that guarantees uniqueness on insertion. ## How It Works Each element in the list is a UTxO containing: 1. A **unique NFT** identifying the element (the asset name encodes the node key) 2. An **inline datum** of type `Element` with the element's data and a `link` to the next node The list starts with a **root element** — a special UTxO whose NFT asset name is the `rootKey`. Node elements have asset names prefixed with a configurable `prefix` followed by the node key. Every mutating operation consumes an **anchor** — the element immediately before the affected position — and reproduces it with an updated `link`. The anchor's NFT and payload are preserved. For **ordered insertion** (`insert`), proving uniqueness is simple: inspect two adjacent keys and verify the new key fits between them according to byte-string ordering. This guarantees no duplicate exists. ## When to Use This Pattern **Best for:** - Preventing duplicates (e.g., one vote per user, unique registrations) - Growing collections that would exceed datum size limits - Registries requiring uniqueness guarantees - On-chain state that needs membership proofs **Not ideal for:** - High-frequency updates (each operation is a transaction) - Random access lookups (requires sequential traversal) - Small, fixed-size collections (simpler to use datum lists) ## Data Structures ```scala import scalus.patterns.* // Type aliases type RootKey = TokenName // Asset name of the root element's NFT type NodeKey = ByteString // Raw key for a node (asset name minus prefix) type NodeKeyPrefix = ByteString // Prefix prepended to every node's asset name type Link = Option[NodeKey] // Next-element pointer (None = end of list) // Datum variant for linked-list UTxOs enum ElementData derives FromData, ToData: case Root(data: Data) // Marks the start of the list case Node(data: Data) // Regular data node // Full datum stored in every linked-list UTxO case class Element(data: ElementData, link: Link) derives FromData, ToData ``` ## API Reference The `LinkedList` object provides validation functions for all list operations. Each function validates invariants but does not build transactions — it is called from your minting or spending validator. | Operation | Ordering | Description | |-----------|:--------:|-------------| | `init` | — | Create an empty list (mint root NFT) | | `deinit` | — | Destroy an empty list (burn root NFT) | | `insert` | Ordered | Insert node at sorted position (ascending by key) | | `prependUnordered` | Unordered | Insert immediately after the root | | `appendUnordered` | Unordered | Insert at the tail of the list | | `remove` | — | Remove a node (burn its NFT) | | `removeHead` | — | Remove the first node after root (root data may change) | | `validateElementUpdate` | — | Update a node's data without structural changes | | `requireListTokensMintedOrBurned` | — | Spending guard: require minting activity | ## Implementation Guide ### How Insert Works (Ordered) ``` ╭────────╮ ╭────────╮ │ Banana ├─>│ Orange │ INPUTS (anchor + successor) ╰───┬────╯ ╰────┬───╯ │ │ ┏━━━V━━━━━━━━━━━━V━━━━━━━━━━━━━━┓ ┃ Insert "Kiwi" Transaction ┃ ┗━━━┯━━━━━━━━━━┯━━━━━━━━━━┯━━━━━┛ │ │ │ ╭───V────╮ ╭──V───╮ ╭───V────╮ │ Banana ├─>│ Kiwi ├─>│ Orange │ OUTPUTS ╰────────╯ ╰──────╯ ╰────────╯ (Banana < Kiwi < Orange maintained) ``` **Validation ensures:** - Anchor node is reproduced at the same address with the same NFT and data - Anchor's `link` is updated to point to the new node - New node's `link` points to what the anchor previously referenced - Strict ordering: `anchorAssetName < newAssetName` and `newKey < linkKey` - Exactly one NFT is minted for the new node - New node's asset name has the correct prefix ### Using LinkedList in a Minting Validator The `LinkedList` functions are designed to be called from your minting policy. You pass the relevant UTxO inputs and outputs extracted from `TxInfo`: ```scala import scalus.* import scalus.uplc.builtin.{Data, FromData, ToData} import scalus.cardano.onchain.plutus.v3.* import scalus.cardano.onchain.plutus.prelude.* import scalus.patterns.* // Define redeemer actions for your list enum ListAction derives FromData, ToData: case Init case Deinit case Insert( anchorInputIndex: BigInt, contAnchorOutputIndex: BigInt, newElementOutputIndex: BigInt ) case Remove( anchorInputIndex: BigInt, removingInputIndex: BigInt, contAnchorOutputIndex: BigInt ) @Compile object ListAction @Compile object MyListValidator extends DataParameterizedValidator { inline override def mint(cfgData: Data, redeemer: Data, tx: TxInfo): Unit = { // Extract configuration (rootKey, prefix, etc.) val rootKey: RootKey = ??? // from cfgData val prefix: NodeKeyPrefix = ??? val prefixLen: BigInt = ??? val policyId = tx.findOwnMintingPolicyHash redeemer.to[ListAction] match case ListAction.Init => // Find the root output in tx.outputs val rootOut = tx.outputs.at(BigInt(0)) LinkedList.init(rootOut, tx.mint, policyId, rootKey) case ListAction.Deinit => // Find the root input val rootInput = tx.inputs.at(BigInt(0)) LinkedList.deinit(rootInput, tx.mint, policyId, rootKey) case ListAction.Insert(anchorIdx, contAnchorIdx, newElemIdx) => val anchorInput = tx.inputs.at(anchorIdx) val contAnchorOutput = tx.outputs.at(contAnchorIdx) val newElementOutput = tx.outputs.at(newElemIdx) LinkedList.insert( anchorInput, contAnchorOutput, newElementOutput, tx.mint, policyId, rootKey, prefix, prefixLen ) case ListAction.Remove(anchorIdx, removingIdx, contAnchorIdx) => val anchorInput = tx.inputs.at(anchorIdx) val removingInput = tx.inputs.at(removingIdx) val contAnchorOutput = tx.outputs.at(contAnchorIdx) LinkedList.remove( anchorInput, removingInput, contAnchorOutput, tx.mint, policyId, rootKey, prefix, prefixLen ) } } ``` ### Spending Validator (Coupling Pattern) The linked list uses a **coupling pattern** where the spending validator delegates structural checks to the minting policy. The spending validator only needs to verify that list tokens are being minted or burned: ```scala @Compile object MyListSpendingValidator extends Validator { inline override def spend( datum: Option[Data], redeemer: Data, tx: TxInfo, ownRef: TxOutRef ): Unit = { val policyId: PolicyId = ??? // your list's minting policy ID // Delegate structural validation to the minting policy LinkedList.requireListTokensMintedOrBurned(policyId, tx.mint) } } ``` ### Updating Element Data To update a node's payload without changing the list structure, use `validateElementUpdate`. This ensures the element preserves its NFT, address, and link — only the `data` field may change: ```scala LinkedList.validateElementUpdate( elementInputIndex = BigInt(0), contElementOutputIndex = BigInt(0), elementInputOutref = myOutRef, txInputs = tx.inputs, txOutputs = tx.outputs, txMint = tx.mint, policyId = policyId, rootKey = rootKey, prefix = prefix, prefixLen = prefixLen ) ``` **Key Uniqueness:** For ordered insertion (`insert`), keys are guaranteed unique by the strict ordering invariant (`anchorKey < newKey < linkKey`). Unordered operations (`appendUnordered`, `prependUnordered`) do not enforce ordering but still require unique NFT asset names via minting. **NFT Management:** Each element is identified by a unique NFT under the list's `policyId`. Structural operations (`init`/`insert`/`appendUnordered`/`prependUnordered`) mint exactly one NFT, while `deinit`/`remove`/`removeHead` burn exactly one. The `validateElementUpdate` operation requires no minting or burning. ## Related Patterns - **[UTxO Indexer](/docs/design-patterns/utxo-indexer)** - For efficient input-output mapping - **[Stake Validator](/docs/design-patterns/withdraw-zero)** - For reducing validation costs ## Resources - [Anastasia Labs: Aiken Linked List](https://github.com/Anastasia-Labs/aiken-linked-list) - Original Aiken implementation - [Plutarch Linked List Guide](https://github.com/Anastasia-Labs/data-structures/blob/main/pages/linked_list.mdx) - Detailed pattern explanation - [Scalus Design Patterns](https://github.com/scalus3/scalus/tree/master/scalus-design-patterns) - Implementation and tests --- Source: https://scalus.org/docs/design-patterns/validity-range --- # Validity Range Normalization Normalize Plutus validity ranges into four standardized forms to simplify time validation and protect against protocol changes. ## The Challenge Plutus validators receive transaction validity ranges with lower and upper bounds that can be: - Finite or infinite (-∞ or +∞) - Open or closed at each end This creates multiple valid representations of the same range: - `(a, b)` equals `[a+1, b-1]` for finite integer values - Infinite bounds are sometimes marked as closed despite being mathematically open - The "always" range is inconsistently denoted as `[-∞, +∞]` This ambiguity risks unintended validator failures. Since Cardano may change how ranges are communicated during hard forks, long-lived contracts must handle various representations to prevent funds from becoming permanently locked. ## How It Works Normalize all validity ranges into four standardized forms where finite bounds are always **inclusive**: ```scala import scalus.patterns.NormalizedInterval enum NormalizedInterval: case ClosedRange(lower: PosixTime, upper: PosixTime) // [lower, upper] case FromNegInf(upper: PosixTime) // (-∞, upper] case ToPosInf(lower: PosixTime) // [lower, +∞) case Always // (-∞, +∞) ``` This reduces pattern-matching complexity from dozens of cases to four clear categories. ## When to Use This Pattern **Best for:** - Any validator that checks transaction validity time - Long-lived contracts that must survive protocol upgrades - Simplifying time-based validation logic **Not ideal for:** - Contracts that don't use time constraints - Off-chain code (use standard library time functions) ## API Reference | Method | Description | |--------|-------------| | `interval.tryNormalize` | Safe normalization - returns `Option[NormalizedInterval]` | | `interval.normalize` | Unsafe normalization - fails on improper intervals | Extension methods are provided on the `Interval` type. ## Implementation Guide ### Safe Normalization ```scala import scalus.patterns.NormalizedInterval import scalus.cardano.onchain.plutus.v1.* val interval: Interval = txInfo.validRange interval.tryNormalize match case Option.Some(NormalizedInterval.ClosedRange(start, end)) => // Valid time window [start, end] require(currentTime >= start && currentTime <= end) case Option.Some(NormalizedInterval.ToPosInf(start)) => // Open-ended: [start, +∞) require(currentTime >= start) case Option.Some(NormalizedInterval.FromNegInf(end)) => // Before deadline: (-∞, end] require(currentTime <= end) case Option.Some(NormalizedInterval.Always) => // No time constraints () case Option.None => // Improper interval (e.g., Interval.never) fail("Invalid time range") ``` ### Unsafe Normalization ```scala // Throws error on improper intervals val normalized: NormalizedInterval = interval.normalize normalized match case NormalizedInterval.ClosedRange(start, end) => ... case NormalizedInterval.ToPosInf(start) => ... case NormalizedInterval.FromNegInf(end) => ... case NormalizedInterval.Always => ... ``` ### Examples **Exclusive bounds are converted to inclusive:** ```scala val interval = Interval( from = IntervalBound(IntervalBoundType.Finite(10), false), // exclusive 10 to = IntervalBound(IntervalBoundType.Finite(20), false) // exclusive 20 ) interval.normalize // ClosedRange(11, 19) ``` **Infinite bounds:** ```scala val openEnded = Interval( from = IntervalBound(IntervalBoundType.Finite(100), true), to = IntervalBound(IntervalBoundType.PosInf, false) ) openEnded.normalize // ToPosInf(100) val beforeDeadline = Interval( from = IntervalBound(IntervalBoundType.NegInf, false), to = IntervalBound(IntervalBoundType.Finite(500), true) ) beforeDeadline.normalize // FromNegInf(500) ``` **Improper intervals:** ```scala val never = Interval( from = IntervalBound(IntervalBoundType.Finite(200), true), to = IntervalBound(IntervalBoundType.Finite(100), true) // lower > upper! ) never.tryNormalize // None never.normalize // throws error ``` ### Type Class Instances `NormalizedInterval` provides `Eq`, `Ord`, and `Show` instances for use in on-chain code: ```scala val range1 = NormalizedInterval.ClosedRange(100, 200) val range2 = NormalizedInterval.ClosedRange(100, 300) range1 === range2 // false range1 < range2 // true (compares lower, then upper) range1.show // "NormalizedInterval.ClosedRange(100, 200)" ``` **On-Chain Safety:** Use `tryNormalize` in validators to handle all cases gracefully. The `normalize` method is useful when you're certain the interval is valid. ## Related Patterns - **[Stake Validator](/docs/design-patterns/withdraw-zero)** - Often combined with time checks in reward endpoints - **[UTxO Indexer](/docs/design-patterns/utxo-indexer)** - Time validation in indexed transactions ## Resources - [Anastasia Labs: Validity Range Normalization](https://github.com/Anastasia-Labs/design-patterns/blob/main/validity-range-normalization/VALIDITY-RANGE-NORMALIZATION.md) - Original pattern documentation - [Scalus Design Patterns](https://github.com/scalus3/scalus/tree/master/scalus-design-patterns) - Implementation and tests --- Source: https://scalus.org/docs/design-patterns --- # Cardano Smart Contract Design Patterns Proven patterns for building efficient and secure Cardano smart contracts, implemented in Scalus. ## Optimization Patterns Reduce execution costs by delegating computation to single-execution endpoints: - **[Withdraw Zero](/docs/design-patterns/withdraw-zero)** — Delegate computation to staking scripts using the "withdraw zero trick", reducing costs from O(N²) to O(N) - **[Transaction Level Minting](/docs/design-patterns/transaction-level-minting)** — Couple spend and mint endpoints so spending validators stay minimal - **[Merkelized Validator](/docs/design-patterns/merkelized-validator)** — Read verified data from stake validator redeemers for efficient batch operations ## Validation Patterns - **[Parameter Validation](/docs/design-patterns/parameter-validation)** — Verify that script instances across a multi-script protocol are parameterized with expected values ## Indexing Patterns - **[UTxO Indexer](/docs/design-patterns/utxo-indexer)** — Map inputs to outputs using pre-computed indices for O(1) on-chain verification ## Data Structures - **[Linked List](/docs/design-patterns/linked-list)** — Ordered and unordered linked lists using NFTs and datums for on-chain state management ## Utilities - **[Validity Range](/docs/design-patterns/validity-range)** — Normalize time ranges to simplify interval validation in your contracts ## Related - [Security Guide](/docs/security) — Common vulnerabilities these patterns help prevent - [Smart Contract Optimisations](/docs/smart-contract-optimisations) — Compiler-level optimisations - [Testing](/docs/testing/unit-testing) — Test your pattern implementations ## Resources - [Scalus Design Patterns](https://github.com/scalus3/scalus/tree/master/scalus-design-patterns) — Implementation and tests - [Anastasia Labs Design Patterns](https://github.com/Anastasia-Labs/design-patterns) — Original pattern documentation - [Plutonomicon](https://github.com/Plutonomicon/plutonomicon) — Community-driven Plutus knowledge base --- Source: https://scalus.org/docs/advanced-data-structures/merkle-tree --- # Merkle Tree A binary tree where each leaf is a hash of data and each internal node is a hash of its children. Proofs are log₂(N) hashes. **Best for:** Static sets known at deploy time — airdrop allowlists, governance voter registries, configuration snapshots. ## How It Works ``` ┌─────────┐ │ root │ │ H(A|B) │ └────┬────┘ ┌────────────┴──────────┐ ┌────┴────┐ ┌────┴────┐ │ A │ │ B │ │ H(C|D) │ │ H(E|F) │ └────┬────┘ └────┬────┘ ┌──────┴──────┐ ┌─────┴──────┐ ┌────┴────┐ ┌────┴────┐┌────┴────┐ ┌────┴────┐ │ C │ │ D ││ E │ │ F │ │ H(d₁|d₂)│ │ H(d₃|d₄)││ H(d₅|d₆)│ │ H(d₇|d₈)│ └────┬────┘ └────┬────┘└────┬────┘ └────┬────┘ ┌───┴───┐ ┌───┴───┐ ┌───┴───┐ ┌───┴───┐ d₁ d₂ d₃ d₄ d₅ d₆ d₇ d₈ Proof that d₃ is in the tree: [d₄, C, B] Verify: D = H(d₃|d₄), A = H(C|D), root = H(A|B) ✓ ``` To verify membership, the on-chain validator receives the element and its proof (the sibling hashes along the path to the root). It recomputes the hashes from leaf to root and checks that the result matches the known root hash. ## Cost - **Verification:** log₂(N) + 1 blake2b calls - **Proof size:** 33 × ceil(log₂(N)) bytes ## Limitations - **No mutations** — the tree is built once and cannot be modified on-chain - **Membership only** — cannot prove that an element is *not* in the set ## Background Ralph Merkle described the idea in 1979 in "A Certified Digital Signature". The motivation was signing many messages at once: instead of signing each message individually, hash them into a root and sign only the root. ## Example: Membership Token Validator The [MembershipTokenValidator](https://github.com/scalus3/scalus/tree/master/scalus-examples/jvm/src/main/scala/scalus/examples/MembershipToken.scala) uses a Merkle Tree to gate token minting — only members in a pre-built allowlist can mint. The Merkle root is baked into the script at deployment time via `ParameterizedValidator[ByteString]`. **On-chain** — verify the signer is in the allowlist: ```scala import scalus.cardano.onchain.plutus.crypto.tree.MerkleTree // merkleRoot is the script parameter (baked in at deploy time) // signer.hash is the pubkeyhash of the transaction signer // proof is the sibling hashes along the path to the root MerkleTree.verifyMembership(merkleRoot, signer.hash, proof) ``` **Off-chain** — build the tree and generate a proof: ```scala import scalus.crypto.tree.MerkleTree // Build the tree from a list of member pubkeyhashes val members: Seq[ByteString] = Seq(alice.hash, bob.hash, charlie.hash) val tree = MerkleTree.fromHashes(members) val root = tree.rootHash // bake this into the script parameter // Generate a membership proof for alice val proof = tree.proveMembership(alice.hash) ``` The [AnonymousDataValidator](https://github.com/scalus3/scalus/tree/master/scalus-examples/jvm/src/main/scala/scalus/examples/anonymousdata/AnonymousDataValidator.scala) demonstrates another pattern: Merkle Tree for participant authorization combined with `AssocMap` for encrypted key-value storage. ## Related - [Incremental Merkle Tree](/docs/advanced-data-structures/incremental-merkle-tree) — append-only variant for dynamic sets - [Merkle Patricia Forestry](/docs/advanced-data-structures/merkle-patricia-forestry) — general key-value with insert/delete support --- Source: https://scalus.org/docs/advanced-data-structures/incremental-merkle-tree --- # Incremental Merkle Tree A fixed-depth binary tree filled left-to-right, with empty slots initialized to a null hash. Supports append (sequential insertion) and membership verification. **Best for:** Append-only sets managed by an oracle — price feeds, event logs, audit trails. ## How It Works The idea of append-only hash tree commitments was explored by Peter Todd in his [merkle-mountain-range](https://github.com/opentimestamps/opentimestamps-server/blob/master/doc/merkle-mountain-range.md) work for OpenTimestamps (2012–2016). A different, fixed-depth variant became widely known through **Zcash** (2016, [Zerocash paper by Ben-Sasson et al., 2014](https://eprint.iacr.org/2014/349)): the Sapling and Orchard shielded pools use incremental Merkle trees of depth 32 to commit to note hashes. Our implementation follows the Zcash approach. Imagine a tree of N = 2^D leaves. At the leaf level, each cell contains either `hash(0)` or `hash(data)`. When you append a new leaf, you replace the leftmost `hash(0)` with `hash(data)`, then recompute all hashes on the path to the root. The proof is the sibling hashes along that path. ``` Before append (3 elements, depth=3): root ┌──┴──┐ A B=H(E|0₂) ┌─┴─┐ ┌─┴──┐ C D E 0₂ ┌┴┐ ┌┴┐ ┌┴┐ ┌┴┐ d₁ d₂ d₃ 0 0 0 0 0 Append d₄ → replace leftmost 0: root' ┌──┴──┐ A B'=H(E'|0₂) ┌─┴─┐ ┌──┴──┐ C D E' 0₂ ┌┴┐ ┌┴┐ ┌─┴─┐ ┌┴┐ d₁ d₂ d₃ 0 d₄ 0 0 0 Recompute: E'=H(d₄|0), B'=H(E'|0₂), root'=H(A|B') Proof for d₄: [0, 0₂, A] ``` ## Cost - **Membership verification:** log₂(N) + 1 blake2b calls - **Append:** 2 × log₂(N) + 2 blake2b calls - **Proof size:** 32–33 × ceil(log₂(N)) bytes On-chain costs scale linearly with tree depth D. Each extra depth level adds ~2M CPU steps for verification and ~3.4M steps for append. ## Limitations - **Append only** — elements cannot be deleted or updated - **Membership only** — cannot prove non-membership - **Fixed depth** — maximum capacity is 2^D elements (chosen at construction time) ## Frontier Merkle Tree An off-chain facade for the Incremental Merkle Tree that stores only the "frontier" — the rightmost path of hashes — instead of all N leaves. This allows it to support up to 2²⁵⁶ leaves without storing the full history. **Best for:** High-volume append-only use cases where storing all elements off-chain is impractical — oracle feeds with millions of data points. ### How It Works ``` Full IMT stores all nodes (O(N) memory): root ┌──┴──┐ A B ┌─┴─┐ ┌─┴──┐ C D E 0₂ ┌┴┐ ┌┴┐ ┌┴┐ ┌┴┐ d₁ d₂ d₃ d₄ 0 0 0 0 Frontier stores only the rightmost hashes (O(depth) memory): root ┌──┴──┐ A ● ← frontier[2] = B ┌─┴──┐ ● 0₂ ← frontier[1] = E ┌┴┐ ● 0 ← frontier[0] = d₄ ↑ next append goes here Stored: frontier = [d₄, E, B] (3 hashes, not 8 leaves) left_siblings = [A] (frozen left subtree roots) To append d₅: 1. E' = H(d₄ | d₅) ← combine frontier[0] with new leaf 2. B' = H(E' | 0₂) ← combine up using pre-computed null hashes 3. root' = H(A | B') ← A was stored as frozen left sibling 4. Update frontier: [d₅, E', B'] ``` The `FrontierMerkleTree` stores only D sibling hashes, requiring O(D) memory instead of O(N). For D=20 that's just 20 × 32 = 640 bytes regardless of how many millions of elements have been appended. ### On-Chain Cost The on-chain verifier is **identical** to the Incremental Merkle Tree — the validator doesn't care how the proof was generated. All cost savings are off-chain. The off-chain build time is higher (12s vs 2s for 1M elements) because the frontier tree recomputes intermediate hashes rather than looking them up in a stored structure, but for an oracle that appends one data point per block this is negligible. ## Example: Oracle-Managed Set Validator The [SetBenchImtValidator](https://github.com/scalus3/scalus/tree/master/scalus-examples/jvm/src/main/scala/scalus/examples/setbench/SetBenchImtValidator.scala) demonstrates an oracle-managed append-only set. The oracle appends new elements, and users prove membership to deposit or withdraw. **On-chain** — append a new element (oracle action): ```scala import scalus.cardano.onchain.plutus.crypto.tree.IncrementalMerkleTree val state = datum.getOrFail("No datum").to[ImtDatum] // Oracle appends a new key to the tree val newRoot = IncrementalMerkleTree.append( state.root, // current root hash state.size, // number of elements already in the tree state.depth, // fixed tree depth (e.g. 15 for up to 32K elements) key, // new element to append siblings // proof: sibling hashes along the path ) ``` **On-chain** — verify membership (user action): ```scala // User proves their key is in the tree to deposit or withdraw IncrementalMerkleTree.verifyMembership( state.root, // current root hash key, // element to verify state.depth, // tree depth siblings // membership proof ) ``` **Off-chain** — build the tree and generate proofs: ```scala import scalus.crypto.tree.IncrementalMerkleTree // Build a tree of depth 15 (capacity: 2^15 = 32,768 elements) var tree = IncrementalMerkleTree.empty(depth = 15) // Append elements one by one for (key, _) <- elements do tree = tree.append(blake2b_256(key)) // Generate proofs val membershipProof = tree.proveMembership(elementHash) val appendProof = tree.proveAppend(newElementHash) ``` See also: [SetBenchEmulatorTest](https://github.com/scalus3/scalus/tree/master/scalus-examples/jvm/src/test/scala/scalus/examples/setbench/SetBenchEmulatorTest.scala) for full emulator-based benchmarks measuring real transaction fees and execution units. ## Related - [Merkle Tree](/docs/advanced-data-structures/merkle-tree) — static variant for immutable sets - [Merkle Patricia Forestry](/docs/advanced-data-structures/merkle-patricia-forestry) — general key-value with insert/delete/non-membership proofs - [Bilinear Accumulators](/docs/advanced-data-structures/bilinear-accumulators) — constant proof size but ~35× more CPU - [Benchmarking Authenticated Collections](https://lantr.io/blog/scalus-authenticated-collections-benchmarks/) — IMT vs FusedMPF-16 fee comparisons --- Source: https://scalus.org/docs/advanced-data-structures/merkle-patricia-forestry --- # Merkle Patricia Forestry A radix-16 Patricia trie with path compression, where branch nodes use a binary Merkle tree of depth 4 to reduce proof size. Supports insert, delete, update, membership, and non-membership proofs. Scalus provides two variants: the standard **MPF** (Aiken-compatible) and the **Fused MPF** (8–12% cheaper on-chain, Scalus-only). **Best for:** General key-value storage on Cardano — DEX order books, token registries, on-chain key-value stores. ## Merkle Patricia Forestry Patricia is an acronym for "Practical Algorithm To Retrieve Information Coded In Alphanumeric", created by Donald R. Morrison in 1968. He invented prefix tries with path compression. ### Prefix Trie — What It Is Every key is a 256-bit number, represented as a row of 64 hex nibbles (each nibble is 0–F): ``` key_A: 3 A 7 0 F 2 ... (64 nibbles = 256 bits) ``` This is the "address" of the value in a conceptual 16⁶⁴ = 2²⁵⁶ address space. Stack two keys and read columns left to right: ``` key_A: 3 A 7 0 F 2 ... key_B: 3 A 7 0 F 8 ... ``` The first 5 nibbles (`3 A 7 0 F`) are identical — that's the **common prefix**. At column 5 they diverge: A goes to slot 2, B goes to slot 8. Instead of storing all 2²⁵⁶ slots, you store only the divergence points: ``` root └─ skip "3A70F" (common prefix) ├─ [2] → leaf(key_A, value_A) └─ [8] → leaf(key_B, value_B) ``` Add a third key that diverges earlier: ``` key_A: 3 A 7 0 F 2 ... key_B: 3 A 7 0 F 8 ... key_C: 3 A 1 ... ``` Now the common prefix is only `3 A`. At column 2, C diverges (nibble 1) from A and B (nibble 7): ``` root └─ skip "3A" ← skip node (encodes 2 nibbles) ├─ [1] → leaf(key_C, value_C) ← branch: nibble 1 selects child └─ [7] → skip "0F" ← branch: nibble 7 selects child, then skip node ├─ [2] → leaf(key_A) ← branch: nibble 2 selects child └─ [8] → leaf(key_B) ← branch: nibble 8 selects child ``` A **skip node** encodes a shared prefix — many nibbles at once (0, 1, 10, 30+, any length). This is the Patricia compression: you don't need 5 separate nodes for `3, A, 7, 0, F` — one skip node labeled `"3A70F"` does the job. A **branch node** has up to 16 children (one per nibble 0–F). It encodes exactly 4 bits of the key — the nibble that selects which child to follow. Not all 256 bits of the key go through branch nodes. With, say, 3 branch nodes on a path, only 12 bits are branched; the remaining 244 bits are encoded in skip prefixes. Every path from root to leaf consumes all 256 bits. Each node stores a hash: - **Leaf hash** = `blake2b(suffix_nibbles ++ blake2b(value))` - **Branch hash** = `blake2b(skip_nibbles ++ merkle_root_of_16_children)` The root hash is a single 32-byte commitment to the entire 2²⁵⁶ address space. Most of the space is empty (hash = 0x00...00), and the sparse Merkle tree of 16 children efficiently hashes away all the empty slots. ### The Forestry Optimization In Ethereum's original MPT (radix-16, formalized in the Yellow Paper by Gavin Wood, 2014), branch nodes store 16 child hashes as a flat list. To prove membership, you provide all 15 sibling hashes at each branch step — that's 15 × 32 = 480 bytes per step. The [Merkle Patricia Forestry](https://github.com/aiken-lang/merkle-patricia-forestry) (Aiken team) applies a sparse Merkle tree technique: arrange the 16 children as a **binary tree of depth 4** (since 2⁴ = 16). Now a proof needs only **log₂(16) = 4 sibling hashes** per step — that's 4 × 32 = 128 bytes instead of 480. A ~73% reduction in proof size, at the cost of ~30% more CPU for verification. ### Proofs To prove key_A is in the trie, you walk root → leaf and at each branch collect the sibling hashes needed to reconstruct the root. The verifier replays the hash computation and checks it matches the known root. - **Membership proof:** provide the path from root to the existing leaf. The verifier recomputes hashes and confirms they match the root. - **Non-membership proof:** provide the path to the point where the key *would* be, showing that the slot is empty or the skip prefix doesn't match. This is why MPT supports non-membership proofs while plain Merkle trees do not. ### Cost - **Verification:** ~5–15 blake2b calls per proof step - **Proof size:** ~200–800 bytes (Data-encoded) - **Proof size does not grow** with collection size (max 2²⁵⁶ entries) ## Fused Merkle Patricia Forestry The `FusedMerklePatriciaForestry` is a Scalus-specific optimization. The trie structure is the same — radix-16 with skip compression and the Forestry binary tree of depth 4. The difference is in **how proofs and hashes are encoded**. ### Proof Encoding: Flat ByteString Instead of Plutus Data In the standard (Aiken-compatible) variant, proofs are `List[ProofStep]` encoded as Plutus Data with CBOR serialization overhead per step. In the fused variant, proofs are packed into a single flat `ByteString` with fixed-size fields: - **Branch step**: 130 bytes — `0x00 | skip[1] | neighbors[128]` - **Fork step**: 68 bytes — `0x01 | skip[1] | nibble[1] | prefixLen[1] | halfLeft[32] | halfRight[32]` - **Leaf step**: 66 bytes — `0x02 | skip[1] | key[32] | value[32]` No CBOR, no Data deserialization — just flat byte parsing on-chain. ### Hashing: combine3 The standard variant hashes a branch as `blake2b(nibbles_as_bytes ++ merkle_root_of_16_children)` where `merkle_root_of_16_children` is `blake2b(halfLeft ++ halfRight)` — two nested blake2b calls (the 16 children are first reduced to two halves via a binary Merkle tree of depth 3). The fused variant hashes a branch as `blake2b(skip_count ++ halfLeft ++ halfRight)` — a single blake2b call instead of two, saving one hash per branch step. ### Trade-offs: Standard vs Fused | | **Standard MPF** | **Fused MPF** | |---|---|---| | Proof encoding | Plutus Data (CBOR) | Flat ByteString | | Proof size | ~200–800 bytes | ~100–500 bytes | | CPU cost | Baseline | ~8–12% less | | Aiken-compatible | Yes | No | Use the standard variant when you need to share Merkle roots with Aiken contracts. Use the fused variant for maximum on-chain efficiency in Scalus-only deployments. ## Example: Key-Value Set Validator (Standard MPF) The [SetBenchMpf16oValidator](https://github.com/scalus3/scalus/tree/master/scalus-examples/jvm/src/main/scala/scalus/examples/setbench/SetBenchMpf16oValidator.scala) demonstrates insert and delete operations with the Aiken-compatible MPF variant. **On-chain** — insert or delete a key-value pair: ```scala import scalus.cardano.onchain.plutus.crypto.trie.MerklePatriciaForestry import scalus.cardano.onchain.plutus.crypto.trie.MerklePatriciaForestry.* val state = datum.getOrFail("No datum").to[SetBenchDatum] val trie = MerklePatriciaForestry(state.root) // Insert returns a new trie with updated root val newTrie = trie.insert(key, value, proofData.to[Proof]) // Delete returns a new trie with the key removed val newTrie = trie.delete(key, value, proofData.to[Proof]) // Verify the continuing output has the new root require(outDatum.root === newTrie.root, "Wrong root") ``` **Off-chain** — build the trie and generate proofs: ```scala import scalus.crypto.trie.MerklePatriciaForestry // Build the trie from key-value pairs var trie = MerklePatriciaForestry.empty for (key, value) <- elements do trie = trie.insert(blake2b_256(key), blake2b_256(value)) // Generate proofs val membershipProof = trie.proveMembership(keyHash) val nonMembershipProof = trie.proveNonMembership(keyHash) ``` ## Example: Key-Value Set Validator (Fused MPF) The [SetBenchMpf16bValidator](https://github.com/scalus3/scalus/tree/master/scalus-examples/jvm/src/main/scala/scalus/examples/setbench/SetBenchMpf16bValidator.scala) uses the Fused MPF variant for ~8–12% lower on-chain cost. The API is the same — only the proof encoding differs. **On-chain** — the fused variant uses `ByteString` proofs instead of `Proof`: ```scala import scalus.cardano.onchain.plutus.crypto.trie.FusedMerklePatriciaForestry import scalus.cardano.onchain.plutus.crypto.trie.FusedMerklePatriciaForestry.* val trie = FusedMerklePatriciaForestry(state.root) // Same API as standard MPF, but proofs are flat ByteStrings val newTrie = trie.insert(key, value, unBData(proofData)) val newTrie = trie.delete(key, value, unBData(proofData)) ``` **Off-chain** — the fused variant generates binary proofs: ```scala import scalus.crypto.trie.FusedMerklePatriciaForestry var trie = FusedMerklePatriciaForestry.empty for (key, value) <- elements do trie = trie.insert(blake2b_256(key), blake2b_256(value)) val proof = trie.proveMembership(keyHash) // returns ByteString, not List[ProofStep] ``` See also: - [SetBenchMpf16bLightValidator](https://github.com/scalus3/scalus/tree/master/scalus-examples/jvm/src/main/scala/scalus/examples/setbench/SetBenchMpf16bLightValidator.scala) — minimal validator for fair Scalus-vs-Aiken cost comparison - [CollectionMembershipBudgetTest](https://github.com/scalus3/scalus/tree/master/scalus-examples/jvm/src/test/scala/scalus/examples/setbench/CollectionMembershipBudgetTest.scala) — budget analysis comparing MPF variants at different collection sizes - [AikenMpfCompatibilityTest](https://github.com/scalus3/scalus/tree/master/scalus-examples/jvm/src/test/scala/scalus/examples/setbench/AikenMpfCompatibilityTest.scala) — verifies Scalus MPF proofs are compatible with Aiken's implementation ## Related - [Merkle Trees](/docs/advanced-data-structures/merkle-tree) — simpler static and append-only variants - [Bilinear Accumulators](/docs/advanced-data-structures/bilinear-accumulators) — constant proof size but ~35× more CPU - [Benchmarking Authenticated Collections](https://lantr.io/blog/scalus-authenticated-collections-benchmarks/) — detailed CPU, memory, and fee comparisons between MPF variants --- Source: https://scalus.org/docs/advanced-data-structures/bilinear-accumulators --- # Bilinear Accumulators Bilinear accumulators represent a set as a single elliptic curve point. Unlike Merkle trees and tries (which use hash chains), accumulators use **polynomial commitments** and **pairings** on the BLS12-381 curve. **Best for:** Large sets where constant proof size matters — many proofs per transaction, tight tx size limits approaching the 16KB maximum. ## How It Works The high-level intuition: - A set {a₁, a₂, …, aₙ} is encoded as a polynomial P(x) = (x + a₁)(x + a₂)…(x + aₙ). - This polynomial is **committed** to a single elliptic curve point using a polynomial commitment scheme (specifically, [KZG commitments](https://dankradfeist.de/ethereum/2020/06/16/kate-polynomial-commitments.html)). - A commitment is computed from the polynomial coefficients and a trusted setup: given P(x) = c₀ + c₁x + … + cₙxⁿ and CRS points [g, τ·g, τ²·g, …], the commitment is `c₀·g + c₁·(τ·g) + … + cₙ·(τⁿ·g) = P(τ)·g` — a single group element (48 or 96 bytes) that uniquely represents the polynomial (and thus the set). - Properties of the polynomial (like whether it has certain roots, i.e., whether elements are in the set) can then be proven with small proofs verified using **pairings**. The accumulator value is a single group element — constant size regardless of set size. ## Trusted Setup (CRS) The accumulator requires a **Common Reference String** (CRS): a list of curve points [g, τ·g, τ²·g, …, τᵈ·g] where τ is a secret that must be destroyed after setup. Anyone who knows τ can forge proofs, so the setup must be performed by a trusted party or via a multi-party ceremony where at least one participant deletes their share. The maximum set size is bounded by the degree d. ## Proofs - **Membership:** to prove S ⊆ U, compute quotient Q = PU / PS and commit it. The verifier checks a pairing equation confirming that PS divides PU. - **Non-membership:** to prove D ∩ U = ∅, use extended GCD to find S, T such that S·PU + T·PD = 1 (possible only when the polynomials share no roots). The proof is (S, T) committed to curve points. ## When to Use Accumulators shine when proof size is critical — each proof is ~48–96 bytes (1–2 compressed curve points), regardless of set size. The trade-off is ~35× more CPU for on-chain verification compared to tries. Use them when you need many proofs per transaction or are hitting transaction size limits. ## Scalus API Any trusted setup ceremony can produce a CRS. Scalus provides: - `BilinearAccumulatorProver.trustedSetup(tau, maxDegree)` to run your own ceremony — generate τ, create the CRS, then delete τ - `Setup.fromPoints(g1Powers, g2Powers)` to load CRS points from an external ceremony - The `scalus-ethereum-kzg-ceremony` module bundles the [Ethereum KZG ceremony](https://ceremony.ethereum.org/) as a ready-made production CRS via `EthereumKzgCeremony.loadSetup()` ### G1 vs G2 Variants Scalus provides two variants depending on which curve group holds the accumulator point. BLS12-381 has two groups: G1 (48-byte points, cheaper arithmetic) and G2 (96-byte points). | | **G2 Accumulator** | **G1 Accumulator** | |---|---|---| | Accumulator on | G2 | G1 | | Off-chain prover needs | G2 powers | G1 powers | | On-chain verifier needs | G1 powers | G2 powers | | Proof on | G2 | G1 | | On-chain cost | Cheaper (G1 MSM) | Slightly more expensive (G2 MSM) | Both use 2 pairings for verification. The G2 variant would be cheaper on-chain (G1 MSM instead of G2 MSM) but the off-chain prover requires many G2 powers — the Ethereum KZG ceremony has 32,768 G1 powers but only 65 G2 powers, so only the G1 variant is practical with it. ## Background Reading - **KZG polynomial commitments** — Dankrad Feist's [KZG Polynomial Commitments](https://dankradfeist.de/ethereum/2020/06/16/kate-polynomial-commitments.html) explains the construction from scratch. - **Elliptic curve pairings** — Vitalik Buterin's [Exploring Elliptic Curve Pairings](https://medium.com/@VitalikButerin/exploring-elliptic-curve-pairings-c73c1f6b6b0) gives an accessible introduction. - **Bilinear accumulators** — The original construction is from [Nguyen (2005)](https://link.springer.com/chapter/10.1007/978-3-540-30580-4_19). For a more accessible treatment, see [Boneh, Bünz, Fisch: Batching Techniques for Accumulators](https://eprint.iacr.org/2018/1188), Section 3. ## Example: Accumulator-Based Set Validator The [SetBenchAccValidator](https://github.com/scalus3/scalus/tree/master/scalus-examples/jvm/src/main/scala/scalus/examples/setbench/SetBenchAccValidator.scala) demonstrates membership verification using the G1 accumulator with the Ethereum KZG ceremony as the trusted setup. **On-chain** — verify membership and update the accumulator: ```scala import scalus.cardano.onchain.plutus.crypto.accumulator.G1Accumulator import scalus.cardano.onchain.plutus.prelude.bls12_381.G2 // Decompress the accumulator and proof from datum/redeemer val acc = bls12_381_G1_uncompress(state.root) val proof = bls12_381_G1_uncompress(action.compressedProof) // Load CRS (G2 powers from the trusted setup) val g2_0 = G2.uncompress(SetBenchCRS.g2_0) val g2_1 = G2.uncompress(SetBenchCRS.g2_1) val crs = List(g2_0, g2_1) // Verify the element is in the set require( G1Accumulator.verifyMembership(crs, acc, List(action.element), proof), "Membership proof failed" ) // After deletion, the membership proof IS the new accumulator val newRoot = bls12_381_G1_compress(proof) ``` **Off-chain** — build the accumulator and generate proofs: ```scala import scalus.crypto.accumulator.EthereumKzgCeremony import scalus.crypto.accumulator.BilinearAccumulatorProver.* // Load the Ethereum KZG ceremony as trusted setup val ceremony = EthereumKzgCeremony.loadCeremony() val setup = Setup.fromPoints(ceremony.g1Monomial.toVector, ceremony.g2Monomial.toVector) // Create accumulator from a set of elements (as BigInt field elements) val elements: Vector[BigInt] = keys.map(k => byteStringToInteger(true, blake2b_256(k))) val acc = setup.accumulate(elements) // Generate a membership proof for a single element val proof = setup.proveMembership(elements, targetElement) ``` See also: [CollectionMembershipBudgetTest](https://github.com/scalus3/scalus/tree/master/scalus-examples/jvm/src/test/scala/scalus/examples/setbench/CollectionMembershipBudgetTest.scala) — budget analysis comparing accumulator vs MPF variants at different collection sizes. ## Related - [Merkle Patricia Forestry](/docs/advanced-data-structures/merkle-patricia-forestry) — hash-based alternative with ~35× less CPU but larger proofs - [Benchmarking Authenticated Collections](https://lantr.io/blog/scalus-authenticated-collections-benchmarks/) — fee breakdown showing how the ~35× CPU difference translates to actual transaction costs --- Source: https://scalus.org/docs/advanced-data-structures --- # Advanced Data Structures Starting from 0.16.0, Scalus includes a library of **authenticated collections**: data structures where you store data off-chain and keep only a small hash commitment on-chain. Cryptographic proofs verify that operations (membership, insert, delete) are valid — without the on-chain validator ever seeing the full dataset. This matters on Cardano because of eUTxO constraints: limited transaction size, expensive on-chain storage, and tight script execution budgets. Authenticated collections let you work with arbitrarily large datasets while keeping transactions small and cheap. ## When to Use Each Collection | Collection | Verification Cost | Proof Size | Grows with N? | Use Case | |---|---|---|---|---| | **[Merkle Tree](/docs/advanced-data-structures/merkle-tree)** | log₂(N)+1 blake2b calls | 33 × ceil(log₂(N)) bytes | Yes (logarithmic) | Static set known at deploy time. Airdrop allowlists, governance voter registries. | | **[Incremental Merkle Tree](/docs/advanced-data-structures/incremental-merkle-tree)** | Verify: log₂(N)+1 blake2b. Append: 2×log₂(N)+2 blake2b | 32–33 × ceil(log₂(N)) bytes | Yes (logarithmic) | Append-only sets. Oracle price feeds, event logs, audit trails. | | **[Frontier Merkle Tree](/docs/advanced-data-structures/incremental-merkle-tree#frontier-merkle-tree)** | Same as Incremental MT | Same as Incremental MT | Yes (logarithmic) | Same as Incremental MT but with O(depth) off-chain memory instead of O(N). | | **[Merkle Patricia Forestry](/docs/advanced-data-structures/merkle-patricia-forestry)** | ~5–15 blake2b calls per step | ~200–800 bytes (Data-encoded) | **No** (max 2²⁵⁶ entries) | General key-value with insert/delete/update + non-membership proofs. Aiken-compatible. DEX order books, token registries. | | **[Fused MPF](/docs/advanced-data-structures/merkle-patricia-forestry#fused-merkle-patricia-forestry)** | ~8–12% less CPU than MPF | ~100–500 bytes (flat ByteString) | **No** (max 2²⁵⁶ entries) | Same as MPF but cheaper on-chain. Not Aiken-compatible. | | **[Bilinear Accumulators](/docs/advanced-data-structures/bilinear-accumulators)** | 2 pairings + MSM (~35× more CPU) | ~48–96 bytes | **No** (constant, 1 group element) | Large sets where constant proof size matters. Many proofs per tx, tight size limits. | ## Decision Flowchart 1. **Static set?** → **[Merkle Tree](/docs/advanced-data-structures/merkle-tree)** (cheapest possible) 2. **Append-only?** → **[Incremental Merkle Tree](/docs/advanced-data-structures/incremental-merkle-tree)** / **[Frontier Merkle Tree](/docs/advanced-data-structures/incremental-merkle-tree#frontier-merkle-tree)** 3. **Need insert + delete?** → **[Fused MPF](/docs/advanced-data-structures/merkle-patricia-forestry#fused-merkle-patricia-forestry)** (cheapest) or **[MPF](/docs/advanced-data-structures/merkle-patricia-forestry)** (Aiken-compatible) 4. **Proof size critical?** → **[Bilinear Accumulator](/docs/advanced-data-structures/bilinear-accumulators)** (smallest constant proof, ~35× CPU) 5. **Need non-membership proofs?** → MPF, Fused MPF, or Bilinear Accumulator ``` What collection do you need? │ ┌──────▼──────┐ │ Static set? │── Yes ──→ Merkle Tree (MT) @ cheapest └──────┬──────┘ │ No ┌──────▼───────┐ │ Append-only? │── Yes ──→ Incremental MT / Frontier MT └──────┬───────┘ │ No │ ┌────────────▼────────────┐ │ Proof size critical? │── Yes ──→ Bilinear Accumulator │ (many proofs per tx, │ (smallest constant proof size, ~35x CPU) │ tight tx size limits) │ └────────────┬────────────┘ │ No ┌────────────▼────────────┐ │ Need Aiken │── Yes ──→ Merkle Patricia Forestry (MPF) │ compatibility? │ └────────────┬────────────┘ │ No ▼ Fused MPF (10% cheaper than MPF) ``` ## Supported Operations | Operation | Merkle Tree | Incremental MT / Frontier MT | Merkle Patricia Forestry | Fused MPF | Bilinear Accumulator | |---|---|---|---|---|---| | Verify membership | yes | yes | yes | yes | yes | | Verify non-membership | — | — | yes | yes | yes | | Insert (with proof) | — | append only | yes | yes | — | | Delete (with proof) | — | — | yes | yes | — | | Update (with proof) | — | — | yes | yes | — | Frontier Merkle Tree uses the same on-chain verifier as Incremental Merkle Tree — it is an off-chain memory optimization. ## Benchmarks For detailed performance benchmarks — including radix comparisons, fusing optimizations, and full Cardano transaction fee breakdowns — see [Benchmarking Authenticated Collections on Cardano](https://lantr.io/blog/scalus-authenticated-collections-benchmarks/). ## Examples Each data structure page includes on-chain and off-chain code snippets from real validators in the [scalus-examples](https://github.com/scalus3/scalus/tree/master/scalus-examples) module: - [**MembershipTokenValidator**](https://github.com/scalus3/scalus/tree/master/scalus-examples/jvm/src/main/scala/scalus/examples/MembershipToken.scala) — Merkle Tree for allowlist-gated token minting - [**SetBenchImtValidator**](https://github.com/scalus3/scalus/tree/master/scalus-examples/jvm/src/main/scala/scalus/examples/setbench/SetBenchImtValidator.scala) — Incremental Merkle Tree for oracle-managed sets - [**SetBenchMpf16oValidator**](https://github.com/scalus3/scalus/tree/master/scalus-examples/jvm/src/main/scala/scalus/examples/setbench/SetBenchMpf16oValidator.scala) — Standard MPF (Aiken-compatible) - [**SetBenchMpf16bValidator**](https://github.com/scalus3/scalus/tree/master/scalus-examples/jvm/src/main/scala/scalus/examples/setbench/SetBenchMpf16bValidator.scala) — Fused MPF (8–12% cheaper) - [**SetBenchAccValidator**](https://github.com/scalus3/scalus/tree/master/scalus-examples/jvm/src/main/scala/scalus/examples/setbench/SetBenchAccValidator.scala) — Bilinear Accumulator with Ethereum KZG ceremony - [**AnonymousDataValidator**](https://github.com/scalus3/scalus/tree/master/scalus-examples/jvm/src/main/scala/scalus/examples/anonymousdata/AnonymousDataValidator.scala) — Merkle Tree + AssocMap for privacy-preserving storage ## Related - [Design Patterns](/docs/design-patterns) — Optimization and validation patterns for Cardano contracts - [Smart Contract Optimisations](/docs/smart-contract-optimisations) — Compiler-level optimisations - [Testing](/docs/testing/unit-testing) — Test your authenticated collection implementations --- Source: https://scalus.org/docs/security/common-vulnerabilities --- # Common Plutus Vulnerabilities This guide summarizes known Plutus vulnerabilities and mitigation strategies. Understanding these patterns helps you write more secure validators. --- ## 1. UTxO Value Size Spam (Token Dust Attack) **Problem:** An attacker creates a UTxO containing thousands of different token types, approaching the 16KB maximum size limit. When sent to a script address, this oversized UTxO becomes expensive or impossible to spend, effectively locking the protocol. **Mitigation:** - Don't allow arbitrary values in trusted places - Implement minimum ADA requirements that scale with UTxO size - Whitelist acceptable tokens - Validate value composition before accepting UTxOs --- ## 2. Large Datum Size **Problem:** Oversized datums on UTxOs that must be consumed for critical operations create performance bottlenecks and can exceed transaction size limits. **Mitigation:** - Avoid infinitely-sized data types - Implement size limits on all data structures - Use bounded collections (lists with maximum length) - Consider using Merkle trees or cryptographic hashes for large datasets - Store large data off-chain with on-chain hash references --- ## 3. Double Satisfaction **Problem:** A spending validator that only checks transaction outputs can allow multiple UTxOs to be spent in one transaction while satisfying conditions only once. Example: Multiple NFTs sold while the seller receives payment for just one. **Mitigation:** - Validate single UTxO consumption by filtering inputs by script address and counting them - Tag outputs with corresponding input transaction IDs in datums - Explicitly require the redeemer to reference the specific input being spent - Verify exactly which UTxO is being consumed --- ## 4. Lack of Staking Control **Problem:** Protocols must control staking for protocol-held funds. Without proper checks, users could arbitrarily change staking pool addresses, redirecting rewards or manipulating protocol-controlled stake. **Mitigation:** - Use script-controlled staking credentials - Verify staking credentials remain unchanged in continuation outputs - Implement explicit staking withdrawal validators - Ensure all outputs to the script maintain the same staking credential --- ## 5. EUTxO Concurrency Denial of Service **Problem:** An attacker repeatedly spends and recreates the same UTxO with trivial transactions, blocking legitimate users from interacting with the protocol. In the EUTxO model, only one transaction can spend a specific UTxO per block. **Mitigation:** - Implement cancellation fees or economic disincentives - Add freezing periods allowing only keeper functions - Use time-range validators to create protocol "cold periods" - Require minimum time locks before allowing certain operations - Design protocols to minimize shared state --- ## 6. Unauthorized State Transitions **Problem:** Missing signature checks or insufficient validation of transaction signatories allows unauthorized parties to perform state transitions. **Mitigation:** - Always verify required signatures in `tx.signatories` - Implement multi-signature requirements where appropriate - Maintain comprehensive test suites where each unauthorized actor fails validation - Never assume a transaction is valid without explicit authorization checks --- ## 7. Oracle Attacks ### Price Manipulation **Problem:** Attackers manipulate oracle price feeds to cause incorrect liquidations or unfavorable trades. Example: Using flash loans to temporarily manipulate DEX spot prices. **Mitigation:** - Use Time-Weighted Average Price (TWAP) instead of spot prices - Implement maximum/minimum reportable price changes - Aggregate multiple oracle sources (Chainlink, DEXes, Charli3) - Verify price consistency across sources ### Oracle Key Compromise **Problem:** Oracle cryptographic keys are valuable attack targets. If compromised, attackers can submit fraudulent data. **Mitigation:** - Implement on-chain key revoking, expiry, and updating systems - Require multi-signature oracles - Build robust oracle ecosystems with redundancy - Verify oracle signatures on-chain ### Oracle Data Freshness **Problem:** Stale oracle data can lead to incorrect protocol decisions. **Mitigation:** - Verify timestamp freshness on oracle data - Reject oracle data older than acceptable threshold - Include timestamp validation in validators --- ## 8. Infinite Mint **Problem:** Unintended token minting via unexpected authorization pathways. Example: A forwarding minting policy that lacks proper mint-action checks in witness redeemers. **Mitigation:** - Always explicitly validate the mint amount in minting policies - Check the exact quantity being minted matches expectations - Don't rely solely on spending validators to constrain minting - For NFTs, verify exactly one token is minted - Implement time windows or other constraints for token creation --- ## 9. Parameterized Validator Verification **Problem:** Difficulty verifying on-chain that scripts are proper parameterized instantiations. Users risk interacting with malicious contracts that appear legitimate but have attacker-controlled parameters. **Mitigation:** - Store parameters in authenticated, unique UTxOs instead of baking into scripts - Use protocol NFTs to authenticate configuration UTxOs - Manually construct terms using UPLC Apply constructor for off-chain verification - Provide tools for users to verify script parameterization - Consider avoiding parameterization for critical protocols --- ## 10. Missing Transaction Validation **Problem:** Validators fail to check critical transaction properties such as time ranges, required reference inputs, proper continuation outputs, or expected mints/burns. **Mitigation:** - Always validate transaction time ranges when time-dependent - Verify required reference inputs are present and authentic - Check continuation outputs maintain correct state and value - Validate all state transitions are consistent - Ensure staking credentials remain unchanged where required - Verify expected mints/burns occur as intended --- Source: https://scalus.org/docs/security --- # Smart Contract Security Smart contracts on Cardano are immutable once deployed. Understanding security principles and common vulnerabilities is essential for writing secure validators. **Security First:** Always audit your contracts, test thoroughly, and consider professional security reviews before deploying to mainnet with significant value. ## Authorization - Always verify signatures for state-changing operations - Use `tx.signatories.contains(expectedSigner)` checks - Implement multi-sig where appropriate ## Input Validation - Validate all datum and redeemer inputs - Check value composition and amounts - Verify data structure sizes are bounded ## Fail Secure - Design validators to reject by default - Use explicit `require()` checks for all conditions - Provide clear error messages ## Comprehensive Testing - Test success paths - Test all failure cases - Test boundary conditions - Test with unauthorized actors - Use property-based testing ## Audit and Review - Peer review all validator code - Professional audits for high-value contracts - Run bug bounties on testnet - Consider formal verification for critical logic ## Summary Writing secure Plutus validators requires careful attention to: 1. **Value validation** - Control what tokens and amounts your script accepts 2. **Authorization** - Always verify signatures and permissions 3. **Concurrency** - Design for EUTxO model constraints 4. **Oracle security** - Use TWAP, multiple sources, and freshness checks 5. **Mint validation** - Explicitly check minting amounts and conditions 6. **Transaction validation** - Verify all transaction components 7. **Testing** - Comprehensive test coverage including attack scenarios Security is not optional in smart contract development. Study [common vulnerabilities](/docs/security/common-vulnerabilities), apply mitigations, and test exhaustively before mainnet deployment. ## Related - [Design Patterns](/docs/design-patterns) — Patterns that help prevent common vulnerabilities - [Smart Contracts](/docs/smart-contracts) — Writing validators with Scalus - [Testing](/docs/testing) — Comprehensive testing strategies ## Resources - [Common Plutus Vulnerabilities](/docs/security/common-vulnerabilities) - Known vulnerability patterns and mitigations - [Boundary Testing](/docs/testing/boundary-testing) - Boundary testing and attack simulation for smart contracts - [Scalus Testing Guide](/docs/testing/unit-testing) - Learn to test your validators thoroughly - [Scalus Examples](https://github.com/scalus3/scalus/tree/master/scalus-examples) - Study production validator patterns - [Plutonomicon Vulnerabilities](https://github.com/Plutonomicon/plutonomicon/blob/main/vulnerabilities.md) - Original vulnerability documentation --- Source: https://scalus.org/docs/dapp-development/dapp-starter-tutorial --- # Building a Complete DApp with Scalus Starter [Scalus Starter](https://github.com/scalus3/scalus-starter) is a ready-to-use template for building Cardano DApps with Scalus. It demonstrates a complete token minting service — from smart contract to REST API. In this guide, you'll: 1. **Run** the project and see it mint tokens on a local devnet 2. **Understand** how the pieces fit together (contract → transactions → API → tests) 3. **Modify** the smart contract and add new functionality 4. **Deploy** to a public testnet Let's start by getting it running. ## Run the Project The fastest way to see everything working is to run the integration tests. They spin up a local Cardano node, mint tokens, and burn them — all automatically. ### Prerequisites - Scala 3 development environment — see [Getting Started](/docs/get-started) for installation - Docker (for running Yaci DevKit via Testcontainers) **Nix users**: Run `nix develop` to get a complete environment with all dependencies. ### Clone and Run Unit Tests ```bash copy git clone https://github.com/scalus3/scalus-starter.git cd scalus-starter sbt test ``` Unit tests verify the smart contract logic without a blockchain — they're fast and don't require Docker. ### Run Integration Tests Integration tests run against a real local blockchain: ```bash copy sbt integration/test ``` This will: 1. Start a local Cardano node (Yaci DevKit) in Docker 2. Deploy the minting policy 3. Mint 100 tokens 4. Wait for block confirmation 5. Burn the tokens 6. Verify the final state You should see the tests pass — you just ran a complete DApp locally. Now that you've seen it work, let's understand how the pieces fit together. ## How It Works The project has four main components that work together: ``` ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ MintingPolicy │─────│ Transactions │─────│ Server │ │ (on-chain) │ │ (off-chain) │ │ (REST API) │ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ │ │ └──────────────────────┼───────────────────────┘ ▼ ┌─────────────────┐ │ Tests │ │ (unit + e2e) │ └─────────────────┘ ``` Let's examine each one, starting from the core. ### The Smart Contract The minting policy in `MintingPolicy.scala` is the on-chain validator that controls who can mint or burn tokens. **Configuration** — The contract is parameterized with values "baked in" at deployment: ```scala copy case class MintingConfig( adminPubKeyHash: PubKeyHash, // Only this key can authorize minting/burning tokenName: TokenName // The only token name this policy allows ) ``` Different configurations produce different policy IDs, so each deployment is unique. **Validator logic** — The `@Compile` annotation tells Scalus to generate on-chain code: ```scala copy {1, 17, 18, 22} @Compile object MintingPolicy extends DataParameterizedValidator { def mintingPolicy( adminPubKeyHash: PubKeyHash, tokenName: TokenName, ownPolicyId: PolicyId, tx: TxInfo ): Unit = { // Find tokens being minted under our policy ID val mintedTokens = tx.mint.toSortedMap.getOrFail(ownPolicyId, "Tokens not found") // Ensure exactly one token type with the correct name mintedTokens.toList match case List.Cons((tokName, _), tail) => tail match case List.Nil => require(tokName == tokenName, "Token name not found") case _ => fail("Multiple tokens found") case _ => fail("Impossible: no tokens found") // Only admin can mint or burn require(tx.signatories.contains(adminPubKeyHash), "Not signed by admin") } } ``` This validator enforces three rules: 1. Only the configured token name can be minted/burned 2. Only one token type per transaction (prevents accidental multi-minting) 3. The admin must sign the transaction The `List` type here is `scalus.cardano.onchain.plutus.prelude.List`, not `scala.List`. On-chain code uses Scalus's own collection types. **Compilation pipeline** — The `MintingPolicyGenerator` compiles Scala to Plutus Core: ```scala copy {3,6,7} object MintingPolicyGenerator { // Compile to CompiledPlutus (wraps SIR + UPLC + PlutusScript) val compiled: PlutusV3[Data => Data => Unit] = PlutusV3.compile(MintingPolicy.validate) // Apply configuration to get a concrete minting policy def makeMintingPolicy(adminPubKeyHash: PubKeyHash, tokenName: TokenName): PlutusV3[Data => Unit] = { val config = MintingConfig(adminPubKeyHash, tokenName) compiled.apply(config.toData) } } ``` The `.apply()` method applies the configuration to the compiled template, producing a fully instantiated `PlutusV3` that can be used directly in transaction builders. ### Transaction Building With the smart contract defined, we need off-chain code to build transactions that use it. The `Transactions` class in `Transactions.scala` handles this. **Minting tokens:** ```scala copy def makeMintingTx(amount: Long): Either[String, Transaction] = { Try { // Fetch UTxOs at our address val utxos = ctx.provider .findUtxos(ctx.address, None, None, None, None) .await(10.seconds) .getOrElse(throw new RuntimeException("Failed to fetch UTXOs")) val assetName = AssetName(ctx.tokenNameByteString) val assets = Map(assetName -> amount) val mintedValue = Value.asset(ctx.mintingScript.policyId, assetName, amount) // Use first UTxO as collateral val (input, output) = utxos.head val firstUtxo = Utxo(input, output) // Build the transaction TxBuilder(ctx.cardanoInfo) .spend(utxos) // Include UTxOs as inputs .collaterals(firstUtxo) // Collateral for script execution .mint( ctx.mintingPolicy, // The compiled minting policy assets, // What to mint Data.unit, // Redeemer (unused here) Set(ctx.addrKeyHash) // Admin must sign ) .payTo(ctx.address, mintedValue) // Send tokens to ourselves .complete(ctx.provider, ctx.address) // Balance and calculate fees .await(30.seconds) .sign(ctx.signer) .transaction }.toEither.left.map(_.getMessage) } ``` Key concepts: - **`CompiledPlutus`**: Passing the compiled contract (not a raw `PlutusScript`) enables diagnostic replay — if the script fails, TxBuilder recompiles with error traces and replays the execution - **Collateral**: Required for Plutus script execution; seized if the script fails - **Required signers**: Ensures the admin key hash appears in `tx.signatories` - **`complete()`**: Automatically selects inputs and balances the transaction **Burning tokens** works the same way, but with a negative amount: ```scala copy // Negative amount signals burning val assets = Map(assetName -> amount) // amount should be negative TxBuilder(ctx.cardanoInfo) .mint(ctx.mintingPolicy, assets, Data.unit, Set(ctx.addrKeyHash)) .complete(...) // No payTo needed — burned tokens simply disappear ``` ### The REST API The `Server` class exposes transaction building via HTTP, making the DApp accessible to external clients. **Endpoint definition** using [Tapir](https://tapir.softwaremill.com/): ```scala copy class Server(ctx: AppCtx): private val mint = endpoint.put .in("mint") .in(query[Long]("amount")) .out(stringBody) .errorOut(stringBody) .handle(mintTokens) ``` This creates `PUT /mint?amount=100` which returns a transaction hash or error. **Application context** wires everything together: ```scala copy case class AppCtx( cardanoInfo: CardanoInfo, // Protocol params, network, slot config provider: Provider, // Blockchain data provider account: Account, // HD wallet account signer: TransactionSigner, // Transaction signing tokenName: String // Token to mint/burn ) ``` Factory methods create contexts for different environments: ```scala copy // For testnet/mainnet with Blockfrost val prodCtx = AppCtx(Networks.preprod(), mnemonic, blockfrostApiKey, "MyToken") // For local development with Yaci DevKit val devCtx = AppCtx.yaciDevKit("MyToken") ``` ### Testing The project includes two types of tests that verify different aspects. **Unit tests** (`MintingPolicyTest.scala`) evaluate the validator logic directly — no blockchain needed: ```scala copy class MintingPolicyTest extends ScalusTest { test("reject invalid token name") { val wrongName = ByteString.fromString("WrongToken") // ... build mock TxInfo with wrong token name ... evaluateTx(txInfoData) shouldBe a[Left[_, _]] } test("reject missing admin signature") { // ... build TxInfo without admin in signatories ... evaluateTx(txInfoData) shouldBe a[Left[_, _]] } } ``` Unit tests are fast because they evaluate the UPLC directly without network calls. **Integration tests** (`MintingIT.scala`) run against a real blockchain: ```scala copy class MintingIT extends YaciDevKitTest { test("mint and burn tokens") { val mintResult = transactions.submitMintingTx(100) mintResult shouldBe a[Right[_, _]] Thread.sleep(5000) // Wait for confirmation val burnResult = transactions.submitBurningTx(-100) burnResult shouldBe a[Right[_, _]] } } ``` The `YaciDevKitTest` trait uses [Testcontainers](https://www.testcontainers.org/) to automatically start a Yaci DevKit node in Docker — no manual setup required. ## Make It Your Own Now that you understand the architecture, let's modify the code. We'll add a maximum mint amount check to the validator. ### Add a Maximum Amount to Config Edit `MintingPolicy.scala`: ```scala copy {4} case class MintingConfig( adminPubKeyHash: PubKeyHash, tokenName: TokenName, maxMintAmount: BigInt // NEW: Maximum tokens per mint ) ``` ### Update the Validator Logic ```scala copy {4,15-16} def mintingPolicy( adminPubKeyHash: PubKeyHash, tokenName: TokenName, maxMintAmount: BigInt, // NEW ownPolicyId: PolicyId, tx: TxInfo ): Unit = { val mintedTokens = tx.mint.toSortedMap.getOrFail(ownPolicyId, "Tokens not found") mintedTokens.toList match case List.Cons((tokName, amount), tail) => tail match case List.Nil => require(tokName == tokenName, "Token name not found") // NEW: Check amount doesn't exceed maximum require(amount <= maxMintAmount, "Exceeds max mint amount") case _ => fail("Multiple tokens found") case _ => fail("Impossible: no tokens found") require(tx.signatories.contains(adminPubKeyHash), "Not signed by admin") } ``` ### Write a Test Add to `MintingPolicyTest.scala`: ```scala copy test("reject mint amount exceeding maximum") { val maxAmount = BigInt(1000) val attemptedAmount = BigInt(2000) // ... build TxInfo with amount > maxAmount ... evaluateTx(txInfoData) shouldBe a[Left[_, _]] } ``` ### Verify ```bash copy sbt test ``` ## Add a Burn Endpoint Let's extend the REST API with an endpoint for burning tokens. ### Define the Endpoint In `Server.scala`: ```scala copy class Server(ctx: AppCtx): private val mint = endpoint.put .in("mint") .in(query[Long]("amount")) .out(stringBody) .errorOut(stringBody) .handle(mintTokens) // NEW private val burn = endpoint.put .in("burn") .in(query[Long]("amount")) .out(stringBody) .errorOut(stringBody) .handle(burnTokens) private val apiEndpoints = List(mint, burn) // Add burn ``` ### Add the Handler ```scala copy private def burnTokens(amount: Long): Either[String, String] = val result = txBuilder.submitBurningTx(-amount.abs) result match case Left(value) => println(s"Error burning tokens: $value") case Right(value) => println(s"Tokens burned successfully: $value") result ``` ### Add submitBurningTx In `Transactions.scala`: ```scala copy def submitBurningTx(amount: Long): Either[String, String] = { for tx <- makeBurningTx(amount) result <- ctx.provider.submit(tx).await(30.seconds).left.map(_.toString) yield result.toHex } ``` ### Test It Start the server and try your new endpoint: ```bash copy sbt "run yaciDevKit" # In another terminal: curl -X PUT "http://localhost:8088/burn?amount=50" ``` ## Deploy to Testnet Your modified DApp works locally. Let's deploy it to a public testnet. ### Get a Blockfrost API Key 1. Sign up at [blockfrost.io](https://blockfrost.io) 2. Create a project for **Preprod** testnet 3. Copy your API key ### Get Test ADA 1. Get your wallet address 2. Use the [Cardano Testnet Faucet](https://docs.cardano.org/cardano-testnets/tools/faucet/) to request test ADA ### Configure and Run ```bash copy export BLOCKFROST_API_KEY="your-api-key-here" export MNEMONIC="your 24-word mnemonic phrase here" sbt "run start" ``` Your API is now live: - `PUT http://localhost:8088/mint?amount=100` - `PUT http://localhost:8088/burn?amount=50` - Swagger UI: `http://localhost:8088/docs` **Security**: Never commit your mnemonic or API keys to version control. Use environment variables or a secrets manager. ## Project Structure ``` scalus-starter/ ├── src/main/scala/starter/ │ ├── MintingPolicy.scala # Plutus V3 smart contract │ ├── Transactions.scala # Transaction building │ ├── Server.scala # REST API + AppCtx │ └── Main.scala # CLI entry point ├── src/test/scala/starter/ │ └── MintingPolicyTest.scala # Unit tests └── integration/src/test/scala/ └── MintingIT.scala # Integration tests ``` ## Next Steps - **[Language Guide](/docs/language-guide)** — Scalus syntax and supported Scala features - **[Smart Contracts](/docs/smart-contracts/developing-smart-contracts)** — Deep dive into validators - **[Transaction Builder](/docs/transactions)** — Advanced transaction patterns - **[Testing](/docs/testing/unit-testing)** — Property-based testing with ScalaCheck - **[Design Patterns](/docs/design-patterns)** — Optimization patterns for efficient contracts --- Source: https://scalus.org/docs/dapp-development/working-with-contract --- # 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 Script ``` The 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 ```scala 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 Action ``` ### Step 2: Implement the Validator ```scala 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 HtlcValidator ``` ### Step 3: Compile the Validator Use `PlutusV3.compile` to compile your validator into a `PlutusV3` object: ```scala 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:** ```scala 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: ```scala // 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.plomin ``` ## Using Compiled Contracts ### Access Script and Address ```scala 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.doubleCborHex ``` ### Enable Error Traces for Debugging Use `.withErrorTraces` to get a version with detailed error messages: ```scala // 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:** ```scala 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:** ```scala 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`: ```scala @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: ```scala // 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.script ``` --- ## Blueprints (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: ```scala 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: ```scala 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: ```json { "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: ```scala 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 --- Source: https://scalus.org/docs/dapp-development/blockchain-providers --- # Blockchain Providers The `Provider` trait is Scalus's abstraction for interacting with the Cardano blockchain. Providers allow you to send transactions and query blockchain state in a consistent way, regardless of whether you're working with a real blockchain, test node, or local emulator. ## Overview The `Provider` interface provides methods for: - Querying UTxOs by address, datum, or tokens - Submitting CBOR-encoded transactions - Retrieving transaction information - Accessing blockchain state ## Available Providers ### Emulator The `Emulator` implements the `Provider` trait but operates entirely locally, without any network connection. It provides: - In-memory UTxO set management - Local transaction validation using [ledger rules](/ledger/ledger-rules) - Instant feedback without network delays - Perfect isolation for testing The Emulator is ideal for: - Unit test suites - Rapid development iterations - Scenarios where launching a local node would be inconvenient Learn more about the Emulator in the [Emulator documentation](/docs/testing/emulator). ### BlockfrostProvider `BlockfrostProvider` connects to the Cardano blockchain via any Blockfrost-compatible API. The Blockfrost API has become a de-facto standard implemented by multiple providers including [Blockfrost](https://blockfrost.io), [Yaci DevKit](https://github.com/bloxbean/yaci-devkit), and others. This provider is useful for: - Verifying transactions and scripts against a real blockchain - Testing against testnets (preview, preprod) or local devnets - Production deployments **Creating a BlockfrostProvider:** ```scala import scalus.cardano.node.BlockfrostProvider import scalus.utils.await import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.duration.* // Connect to preprod testnet val provider = BlockfrostProvider.preprod("your-api-key").await(30.seconds) // Other networks: // BlockfrostProvider.mainnet("your-api-key").await(30.seconds) // BlockfrostProvider.preview("your-api-key").await(30.seconds) // BlockfrostProvider.localYaci().await(30.seconds) ``` The same API works on both JVM and JavaScript platforms. Interacting with remote APIs incurs network delays, making them less suitable for rapid development cycles or unit tests. For fast local testing, use the Emulator or Yaci DevKit. ## Example Usage Here's how you might use different providers interchangeably: ```scala import scalus.cardano.node.{BlockfrostProvider, BlockchainProvider, Emulator} import scalus.cardano.txbuilder.TxBuilder import scalus.cardano.ledger.Value import scalus.utils.await import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.duration.* // Using Blockfrost-compatible API for production/testnet val blockfrostProvider = BlockfrostProvider.preprod("your-api-key").await(30.seconds) // Using Emulator for fast local testing val emulatorProvider = Emulator( Map( input(0) -> adaOutput(Alice.address, 100) ) ) // Same code works with both providers def buildAndSubmit(provider: BlockchainProvider) = { val tx = TxBuilder(provider.cardanoInfo) .payTo(Bob.address, Value.ada(10)) .complete(provider, Alice.address) .await(30.seconds) .transaction provider.submit(tx).await(30.seconds) } // Works with Blockfrost buildAndSubmit(blockfrostProvider) // Works with Emulator buildAndSubmit(emulatorProvider) ``` ## Querying UTXOs Providers offer several ways to query UTXOs from the blockchain. ### Basic Queries ```scala import scalus.cardano.address.Address.addr val myAddress = addr"addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer..." // Query all UTXOs at an address val utxosResult = provider.findUtxos(myAddress).await(30.seconds) utxosResult match { case Right(utxos) => println(s"Found ${utxos.size} UTXOs") utxos.foreach { case (input, output) => println(s" ${input.transactionId.toHex}#${input.index}: ${output.value.coin} lovelace") } case Left(error) => println(s"Query failed: $error") } // Find a specific UTXO by transaction input val specificUtxo = provider.findUtxo(Input(txHash, 0)).await(30.seconds) ``` ### Query DSL For more complex queries, use the `queryUtxos` DSL which provides a type-safe way to filter and paginate results: ```scala import scalus.cardano.ledger.{Coin, PolicyId, AssetName} // Query UTXOs with a specific native token val policyId = PolicyId.fromHex("abc123...") val assetName = AssetName.fromString("MyToken") val tokenUtxos = provider.queryUtxos { u => u.output.address == myAddress && u.output.value.hasAsset(policyId, assetName) }.execute().await(30.seconds) // Query with minimum lovelace amount val largeUtxos = provider.queryUtxos { u => u.output.address == myAddress && u.output.value.coin >= Coin.ada(10) }.execute().await(30.seconds) // Query with early termination once you have enough funds val enoughFunds = provider.queryUtxos { u => u.output.address == myAddress }.minTotal(Coin.ada(50)).execute().await(30.seconds) // Combine multiple conditions val complexQuery = provider.queryUtxos { u => u.output.address == myAddress && u.output.value.coin >= Coin.ada(5) && u.output.value.hasAsset(policyId, assetName) }.limit(10).execute().await(30.seconds) ``` ### Supported Query Expressions The DSL supports these expressions: - `u.output.address == addr` - filter by address - `u.input.transactionId == txId` - filter by transaction - `u.output.value.hasAsset(policyId, assetName)` - filter by native token - `u.output.value.coin >= amount` - filter by minimum lovelace - `u.output.hasDatumHash(hash)` - filter by datum hash - `&&` - AND combination - `||` - OR combination Query modifiers: - `.limit(n)` - limit number of results - `.skip(n)` - skip first n results - `.minTotal(amount)` - stop early once total lovelace reaches amount (optimization) ## See Also - [Emulator](/ledger/emulator) - Local development and testing - [Transaction Builder](/transaction-builder) - Building transactions with providers - [Ledger Rules](/ledger/ledger-rules) - Understanding transaction validation --- Source: https://scalus.org/docs/dapp-development/protocol-parameters --- # Protocol Parameters Protocol parameters (pps, params) represent general, static information about the Cardano network that your app needs. Protocol parameters are necessary for most chain-related operations, such as transaction building, fee calculation, and ledger rule validation. Scalus models all relevant ledger values, and protocol parameters are one of them. Chances are, your application already computes, consumes, or otherwise handles protocol parameters. This article shows you how to fetch them, parse them from JSON strings, or map from other libraries' representations. ## Fetching Protocol Parameters `Provider` is an interface for interacting with the blockchain. Its `fetchLatestParams` method allows you to get the parameters from a node, or any other source that a given `Provider` abstracts over. This method returns a `Future` of Scalus's representation of `ProtocolParams`, ready to use in all our APIs. ```scala import scalus.cardano.node.Provider import scala.concurrent.ExecutionContext.Implicits.global val provider: Provider = ??? // BlockfrostProvider, Emulator, etc. val paramsFuture = provider.fetchLatestParams // On JVM, use the await extension import scalus.utils.await val params = paramsFuture.await() ``` ## Parsing from JSON If you have a JSON string with protocol parameters, Scalus provides several methods for deserializing them into `ProtocolParams`. All of them are located in the companion object of `scalus.cardano.ledger.ProtocolParams`. ### fromBlockfrostJson If you have a string of JSON that you've obtained from Blockfrost yourself, e.g., from using bloxbean, or from querying the endpoint directly, you can call `ProtocolParams.fromBlockfrostJson` with your string to get an instance of `ProtocolParams`. ```scala import scalus.cardano.ledger.ProtocolParams // From string val params = ProtocolParams.fromBlockfrostJson(jsonString) // From InputStream import java.io.FileInputStream val stream = new FileInputStream("protocol-params.json") val params2 = ProtocolParams.fromBlockfrostJson(stream) ``` ### fromCardanoCliJson If you have a string of JSON that you've obtained from the Cardano CLI tool, you can call `ProtocolParams.fromCardanoCliJson` with your string to get an instance of `ProtocolParams`. ```scala import scalus.cardano.ledger.ProtocolParams // cardano-cli query protocol-parameters --mainnet > params.json val json = scala.io.Source.fromFile("params.json").mkString val params = ProtocolParams.fromCardanoCliJson(json) ``` ## See Also - [Ledger Rules](/ledger/ledger-rules) - [Transaction Builder](/transaction-builder) --- Source: https://scalus.org/docs/dapp-development/sbt-plugin --- # Scalus SBT Plugin The `ScalusSbtPlugin` adds sbt tasks to your project: `blueprint` for embedding [CIP-57](https://cips.cardano.org/cip/CIP-57) blueprint JSON files into JARs, `blueprintPin`/`blueprintCheck` for committing pinned blueprints to git, and `deploy` for publishing a contract as a reference script UTxO via Blockfrost. Projects scaffolded from the [`hello.g8` or `validator.g8` templates](/docs/get-started/project-commands) come with this plugin already wired in, so the `blueprint` and `deploy` tasks below work out of the box. (Those templates also select the test backend and profiling through the `SCALUS_TEST_ENV` and `SCALUS_PROFILE` environment variables — see [Project Commands](/docs/get-started/project-commands).) The setup below is for adding the plugin to an existing project. ## Setup `project/plugins.sbt`: ```scala copy addSbtPlugin("org.scalus" % "scalus-sbt-plugin" % "") ``` `build.sbt`: ```scala copy lazy val myProject = (project in file(".")) .enablePlugins(ScalusSbtPlugin) .settings( scalaVersion := "3.3.7", addCompilerPlugin("org.scalus" % "scalus-plugin" % "" cross CrossVersion.full), libraryDependencies ++= Seq( "org.scalus" %% "scalus" % "", "org.scalus" %% "scalus-cardano-ledger" % "", ), ) ``` The plugin was previously named `ScalusBlueprintPlugin`. That name still works as a deprecated alias, so existing builds keep compiling; switch to `ScalusSbtPlugin` to clear the warning. The compiler plugin discovers objects extending `Contract` automatically. Each one needs a compiled validator and a blueprint: ```scala copy @Compile object MyValidator extends Validator { inline override def spend( datum: Option[Data], redeemer: Data, tx: TxInfo, ownRef: TxOutRef ): Unit = { val owner = datum.getOrFail("No datum").to[PubKeyHash] require(tx.signatories.contains(owner), "Not signed by owner") } } object MyContract extends Contract { private given Options = Options.release lazy val compiled = PlutusV3.compile(MyValidator.validate) lazy val blueprint = Blueprint.plutusV3[PubKeyHash, Unit]( title = "My Validator", description = "Owner-signed spending validator", version = "1.0.0", license = None, compiled = compiled ) } ``` ## Blueprint Generation When a Cardano smart contract is deployed on-chain, only its script hash is visible. How do you verify that a given source code produced that hash? The verifiable artifact is the **JAR**. The `blueprint` task generates a CIP-57 blueprint JSON for every `Contract` in the project, nested under its package at `META-INF/scalus/blueprints//.json`, plus a single aggregate document at `/plutus.json` (the file name Aiken tooling expects) that merges all validators. Each blueprint contains the `compiledCode` (CBOR hex) and `hash` (script hash), so anyone with the JAR, or anyone who can build it from source, can independently confirm the on-chain script hash. Version provenance lives inside the JSON, never in file names: the Scalus version in `preamble.compiler.version` and the Scala version in the top-level `scalus.scalaVersion` extension key (different Scala versions can produce different UPLC, so both matter). ```sh copy sbt package ``` `blueprint` is a resource generator, so `package` (and `publish`) embed the JSON automatically: ``` [info] Wrote META-INF/scalus/blueprints/com/example/MyContract.json [info] Wrote plutus.json ``` Generation is cached: when the compiled classes have not changed, the task skips all work, so repeated `package`/`test` runs cost nothing. Run `sbt blueprint` to generate without packaging. To skip generation during `package`, set `blueprint / skip := true` in `build.sbt`, or pass `SCALUS_SKIP_BLUEPRINT=1`. `sbt package` is the standard sbt command for producing a JAR. A JAR is just a ZIP archive, so standard tools work to inspect it: ```sh copy unzip -p my-project.jar META-INF/scalus/blueprints/com/example/MyContract.json unzip -p my-project.jar plutus.json ``` The `hash` field is the standard Cardano script hash: `blake2b_224(0x03 || compiledCode)`. If you have the JAR, you can verify that this hash matches what's deployed on-chain. No Scalus installation or JVM required for verification, just the JAR and a blake2b implementation. ## Disabling blueprint generation Because `blueprint` is a resource generator, it runs whenever the resources are needed — `package`, `publish`, `run`, and `test`. There are two ways to turn it off. Disable it for a project in `build.sbt`: ```scala copy blueprint / skip := true ``` Or skip it for a single command, without editing the build, via an environment variable: ```sh copy SCALUS_SKIP_BLUEPRINT=1 sbt package ``` Both leave the explicit task untouched: `sbt blueprint` still generates on demand, regardless of `skip`. Use `skip` when you want fast `test`/`package` cycles and only generate blueprints deliberately (for example, in a release step or CI job). ## Pinning Blueprints in Git The automatic `blueprint` output lives under `target/` and is rebuilt as your code changes. When you reach a usable version whose on-chain scripts you want to lock down, pin it: ```sh copy sbt blueprintPin ``` This copies the current set to committed locations – `plutus.json` at the project root (the stable path off-chain tooling expects) and `blueprints//.json` per contract. Commit them; git history is your pin history, and a diff shows exactly how a code or toolchain change affected each `compiledCode` and `hash`. `sbt blueprintCheck` fails when the pinned files no longer match freshly generated ones – use it in release pipelines to catch forgotten pins. If your project cross-builds several Scala versions, set `blueprintScalaVersion := Some("3.3.7")` so pins are only written (and checked) under your primary Scala baseline; other versions still embed their own blueprints in their JARs. ## Deploying Contracts The `deploy` task creates a reference script UTxO from a `Contract` object. It extracts the compiled script, attaches it to a new UTxO, builds and signs the transaction, and submits it via Blockfrost. ```sh copy sbt "deploy MyContract --network preprod --blockfrost-key --mnemonic '<24 words>' --address " ``` The first argument is the simple name of your `Contract` object. Named flags: | Flag | Env Variable | Default | Description | |------|-------------|---------|-------------| | `--network` | `CARDANO_NETWORK` | `preview` | `preview`, `preprod`, or `mainnet` | | `--blockfrost-key` | `BLOCKFROST_API_KEY` | — | Blockfrost API key | | `--mnemonic` | `CARDANO_MNEMONIC` | — | BIP-39 mnemonic phrase | | `--address` | — | — | Bech32 address for the reference script UTXO | CLI flags take precedence over environment variables. Using env vars avoids secrets in shell history: ```sh copy export BLOCKFROST_API_KEY=preprodXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX export CARDANO_MNEMONIC="word1 word2 ... word24" export CARDANO_NETWORK=preprod sbt "deploy MyContract --address addr_test1q..." ``` Example output: ``` [info] Deploying contract 'MyContract' to preprod... [info] Tx e484fdac... submitted successfully [info] Deployed successfully! Transaction hash: e484fdac... ``` ## What's Next? - **[Working with Contract](/docs/dapp-development/working-with-contract)** — Reference the deployed script from off-chain dapp code - **[Building Transactions](/docs/transactions/building-first-transaction)** — Spend from your reference script with TxBuilder --- Source: https://scalus.org/docs/dapp-development --- # Cardano DApp Development Build complete decentralized applications on Cardano — from smart contracts to REST APIs. ## Tutorials - **[DApp Starter Tutorial](/docs/dapp-development/dapp-starter-tutorial)** — Build a complete token minting service step-by-step - **[Working with Contracts](/docs/dapp-development/working-with-contract)** — Compile validators and integrate with off-chain code ## Infrastructure - **[Blockchain Providers](/docs/dapp-development/blockchain-providers)** — Connect to Cardano via Blockfrost, Koios, or custom providers - **[Protocol Parameters](/docs/dapp-development/protocol-parameters)** — Access network parameters for transaction building ## Related - [Smart Contracts](/docs/smart-contracts) — Write validators for your DApp - [Transactions](/docs/transactions) — Build and submit transactions - [Testing](/docs/testing) — Test your DApp locally --- Source: https://scalus.org/docs/ledger/ledger-rules --- # Ledger Rules Reference This page catalogs all built-in validators and mutators in the Scalus [Ledger Rules Framework](/docs/ledger). For architecture, composition, and customization patterns, see the [Framework overview](/docs/ledger). ## Validating and updating UTxO state (EraRule UTXOW) The following diagram shows how Scalus validators and mutators map to the Conway-era UTXOW state transition in the Cardano ledger: ```mermaid --- config: layout: elk --- flowchart LR EUC[EraRule UTXOW Conway] EUC --> babbageUtxowTransition["babbageUtxowTransition ??Scalus:CardanoMutator"]:::done babbageUtxowTransition --> validateFailedBabbageScripts["validateFailedBabbageScripts Scalus:NativeScriptsValidator"]:::done babbageUtxowTransition --> babbageMissingScripts["babbageMissingScripts Scalus:MissingOrExtraScriptHashesValidator"]:::done babbageUtxowTransition --> missingRequiredDatums["missingRequiredDatums Scalus:MissingRequiredDatumsValidator"]:::done babbageUtxowTransition --> hasExactSetOfRedeemers["hasExactSetOfRedeemers Scalus:ExactSetOfRedeemersValidator"]:::done babbageUtxowTransition --> validateVerifiedWits["Shelley.validateVerifiedWits Scalus:VerifiedSignaturesInWitnessesValidator"]:::done babbageUtxowTransition --> validateNeededWitnesses["validateNeededWitnesses Scalus:MissingKeyHashesValidator"]:::done babbageUtxowTransition --> validateMetadata["Shelley.validateMetadata Scalus:MetadataValidator"]:::done babbageUtxowTransition --> validateScriptsWellFormed["validateScriptsWellFormed Scalus:ScriptsWellFormedValidator"]:::done babbageUtxowTransition --> ppViewHashesMatch["ppViewHashesMatch Scalus:ProtocolParamsViewHashesMatchValidator"]:::done babbageUtxowTransition --> EUTXOC[EraRule UTXO Conway]:::done EUTXOC --> utxoTransition:::done utxoTransition --> disjointRefInputs["disjointRefInputs Scalus:InputsAndReferenceInputsDisjointValidator"]:::done utxoTransition --> validateOutsideValidityIntervalUtxo["Allegra.validateOutsideValidityIntervalUtxo Scalus:OutsideValidityIntervalValidator"]:::done utxoTransition --> validateOutsideForecast["Alonzo.validateOutsideForecast Scalus:OutsideForecastValidator"]:::done utxoTransition --> validateInputSetEmptyUTxO["Shelley.validateInputSetEmptyUTxO Scalus:EmptyInputsValidator"]:::done utxoTransition --> feesOk["feesOk Scalus:FeesOkValidator"]:::done utxoTransition --> validateBadInputsUTxO["Shelley(?).validateBadInputsUTxO Scalus:AllInputsMustBeInUtxoValidator"]:::done utxoTransition --> validateValueNotConservedUTxO["Shelley.validateValueNotConservedUTxO Scalus:ValueNotConservedUTxOValidator"]:::done utxoTransition --> validateOutputTooSmallUTxO["validateOutputTooSmallUTxO Scalus:OutputsHaveNotEnoughCoinsValidator"]:::done utxoTransition --> validateOutputTooBigUTxO["Alonzo.validateOutputTooBigUTxO Scalus:OutputsHaveTooBigValueStorageSizeValidator"]:::done utxoTransition --> validateOutputBootAddrAttrsTooBig["Shelley.validateOutputBootAddrAttrsTooBig Scalus:OutputBootAddrAttrsTooBigValidator"]:::done utxoTransition --> validateWrongNetwork["Shelley.validateWrongNetwork Scalus:WrongNetworkValidator"]:::done utxoTransition --> validateWrongNetworkWithdrawal["Shelley.validateWrongNetworkWithdrawal Scalus:WrongNetworkWithdrawalValidator"]:::done utxoTransition --> validateWrongNetworkInTxBody["Alonzo.validateWrongNetworkInTxBody Scalus:WrongNetworkInTxBodyValidator"]:::done utxoTransition --> validateMaxTxSizeUTxO["Shelley.validateMaxTxSizeUTxO Scalus:TransactionSizeValidator"]:::done utxoTransition --> validateExUnitsTooBigUTxO["Alonzo.validateExUnitsTooBigUTxO Scalus:ExUnitsTooBigValidator"]:::done utxoTransition --> validateTooManyCollateralInputs["Alonzo.validateTooManyCollateralInputs Scalus:TooManyCollateralInputsValidator"]:::done utxoTransition --> EUTXOSC[EraRule UTXOS Conway]:::done EUTXOSC --> utxosTransition:::done utxosTransition --> isValidTxL{isValidTxL}:::done isValidTxL --> |True| conwayEvalScriptsTxValid:::done conwayEvalScriptsTxValid --> expectScriptsToPass(expactScriptsToPass):::done conwayEvalScriptsTxValid --> conwayEvalScriptsTxValidUtxosPrime[(utxos')]:::done isValidTxL --> |False| babbageEvalScriptsTxInvalid:::done babbageEvalScriptsTxInvalid --> evalPlutusScripts(evalPlutusScripts FAIL):::done babbageEvalScriptsTxInvalid --> babbageEvalScriptsTxInvalidUtxosPrime([utxos']):::done EUC --> LedgerState[(LedgerState utxoState'' certStateAfterCERTS)] classDef todo fill:#FFCDD2 classDef wip fill:#FFE0B2 classDef question fill:#E1BEE7 classDef done fill:#C8E6C9 ``` ## Validators ### Input Validation - **AllInputsMustBeInUtxoValidator** — Ensures all transaction inputs (spending, collateral, and reference inputs) exist in the UTxO set - **EmptyInputsValidator** — Verifies that the transaction has at least one input - **InputsAndReferenceInputsDisjointValidator** — Ensures regular inputs and reference inputs don't overlap ### Value and Balance Validation - **ValueNotConservedUTxOValidator** — Validates that the total value consumed equals the total value produced (conservation of value) - **OutputsHaveNotEnoughCoinsValidator** — Checks that all outputs meet the minimum ADA requirement - **OutputsHaveTooBigValueStorageSizeValidator** — Validates that output values don't exceed maximum storage size limits - **OutputBootAddrAttrsSizeValidator** — Ensures bootstrap address attributes don't exceed size limits ### Fee and Collateral Validation - **FeesOkValidator** — Comprehensive fee validation including minimum fee check, collateral requirements, and collateral input validation - **ExUnitsTooBigValidator** — Validates that execution units (memory and CPU steps) don't exceed protocol limits - **TooManyCollateralInputsValidator** — Ensures the number of collateral inputs doesn't exceed the maximum allowed ### Script Validation - **NativeScriptsValidator** — Validates native scripts in the transaction - **ScriptsWellFormedValidator** — Ensures all scripts are properly formed and valid - **MissingOrExtraScriptHashesValidator** — Verifies that all required script hashes are present and no extra ones exist - **MissingRequiredDatumsValidator** — Checks that all required datums are provided in the transaction - **ExactSetOfRedeemersValidator** — Validates that the set of redeemers matches exactly what's needed ### Witness Validation - **VerifiedSignaturesInWitnessesValidator** — Verifies that all required cryptographic signatures are present and valid - **MissingKeyHashesValidator** — Ensures all required key hashes for signing are provided ### Network and Metadata Validation - **WrongNetworkValidator** — Validates that transaction addresses match the expected network - **WrongNetworkWithdrawalValidator** — Checks withdrawal addresses match the network - **WrongNetworkInTxBodyValidator** — Validates network consistency in transaction body - **MetadataValidator** — Validates transaction metadata format and size ### Stake and Certificate Validation - **CertsValidator** — Conway CERTS rule: validates certificate sequences in the transaction - **StakeCertificatesValidator** — Conway DELEG rule: validates stake registration, deregistration, and delegation certificates - **StakePoolCertificatesValidator** — Conway POOL rule: validates stake pool registration and retirement certificates ### Protocol and Transaction Validation - **ProtocolParamsViewHashesMatchValidator** — Ensures protocol parameter view hashes match expected values - **TransactionSizeValidator** — Validates that transaction size doesn't exceed maximum limits - **OutsideValidityIntervalValidator** — Checks that the transaction is within its validity interval (time-to-live) - **OutsideForecastValidator** — Validates forecast-related constraints for transaction validity ## Mutators ### Script Execution - **PlutusScriptsTransactionMutator** — Evaluates Plutus scripts (V1/V2/V3), processes collateral on script failure, and updates the UTxO set (adds outputs, removes consumed inputs) ### Stake Delegation - **StakeCertificatesMutator** — Processes stake delegation certificates (registration, deregistration, delegation) and updates the delegation state ### Composition - **CardanoMutator** — Top-level orchestrator that runs all `DefaultValidators.all` then `DefaultMutators.all` in sequence ## Conway Rule Mapping The following table maps Cardano ledger rules (from [cardano-ledger](https://github.com/IntersectMBO/cardano-ledger)) to their Scalus implementations: | Cardano Ledger Rule | Scalus Implementation | |---|---| | Conway UTXOW | `CardanoMutator` (orchestrator) | | Conway CERTS | `CertsValidator` | | Conway DELEG | `StakeCertificatesValidator` + `StakeCertificatesMutator` | | Conway POOL | `StakePoolCertificatesValidator` | | Conway UTXOS (isValid=true) | `PlutusScriptsTransactionMutator` | | Conway UTXOS (isValid=false) | `PlutusScriptsTransactionMutator` | | Shelley.validateValueNotConservedUTxO | `ValueNotConservedUTxOValidator` | | Shelley.validateBadInputsUTxO | `AllInputsMustBeInUtxoValidator` | | Shelley.validateInputSetEmptyUTxO | `EmptyInputsValidator` | | Shelley.validateVerifiedWits | `VerifiedSignaturesInWitnessesValidator` | | Shelley.validateNeededWitnesses | `MissingKeyHashesValidator` | | Shelley.validateMetadata | `MetadataValidator` | | Shelley.validateMaxTxSizeUTxO | `TransactionSizeValidator` | | Shelley.validateWrongNetwork | `WrongNetworkValidator` | | Shelley.validateWrongNetworkWithdrawal | `WrongNetworkWithdrawalValidator` | | Shelley.validateOutputBootAddrAttrsTooBig | `OutputBootAddrAttrsSizeValidator` | | Allegra.validateOutsideValidityIntervalUtxo | `OutsideValidityIntervalValidator` | | Alonzo.validateOutsideForecast | `OutsideForecastValidator` | | Alonzo.validateWrongNetworkInTxBody | `WrongNetworkInTxBodyValidator` | | Alonzo.validateExUnitsTooBigUTxO | `ExUnitsTooBigValidator` | | Alonzo.validateOutputTooBigUTxO | `OutputsHaveTooBigValueStorageSizeValidator` | | Alonzo.validateTooManyCollateralInputs | `TooManyCollateralInputsValidator` | | Babbage.FeesOK | `FeesOkValidator` | | babbageMissingScripts | `MissingOrExtraScriptHashesValidator` | | missingRequiredDatums | `MissingRequiredDatumsValidator` | | hasExactSetOfRedeemers | `ExactSetOfRedeemersValidator` | | validateFailedBabbageScripts | `NativeScriptsValidator` | | validateScriptsWellFormed | `ScriptsWellFormedValidator` | | ppViewHashesMatch | `ProtocolParamsViewHashesMatchValidator` | | disjointRefInputs | `InputsAndReferenceInputsDisjointValidator` | | validateOutputTooSmallUTxO | `OutputsHaveNotEnoughCoinsValidator` | ## See Also - [Ledger Rules Framework](/docs/ledger) — Architecture, composition, and rule customization - [Emulator](/docs/testing/emulator) — In-memory node using these rules for fast testing - [Yaci DevKit](/docs/testing/local-devnet) — Docker-based devnet with real Cardano node - [Protocol Parameters](/docs/ledger/protocol-parameters) — Network configuration used by validators --- Source: https://scalus.org/docs/ledger --- # Ledger Rules Framework Scalus implements Cardano's ledger validation rules as a composable **State Transition System (STS)** in pure Scala 3, running on **JVM**, **JavaScript**, and **Native** platforms. These rules power the [Emulator](/docs/testing/emulator) for fast local testing, and can be used standalone for transaction validation. The Ledger Rules Framework is in active development and may change. ## Overview The framework implements two validation phases matching Cardano node behavior: - **Phase 1** — Structural validation (inputs exist, fees correct, signatures valid, size limits) - **Phase 2** — Script execution (Plutus V1/V2/V3 and native scripts) Rules are organized into **Validators** (read-only checks) and **Mutators** (state transitions). See the [Ledger Rules Reference](/docs/ledger/ledger-rules) for the full catalog of built-in rules. Source: `scalus-cardano-ledger/shared/src/main/scala/scalus/cardano/ledger/rules/` ## Architecture The framework is built on a **State Transition System (STS)** pattern with two rule types: ```scala import scalus.cardano.ledger.rules.* // Validators — read-only checks that return Right(()) or Left(error) trait STS.Validator { def validate(context: Context, state: State, event: Transaction): Either[Error, Unit] } // Mutators — state transitions that return Right(newState) or Left(error) trait STS.Mutator { def transit(context: Context, state: State, event: Transaction): Either[Error, State] } ``` ### Composition When processing a transaction, the framework runs **all validators first** (short-circuiting on the first error), then **all mutators** sequentially, threading state through each one: ```scala // Run validators, then mutators STS.Mutator.transit(validators, mutators, context, state, transaction) // Internally: // 1. validators.foldLeft(Right(())) { (acc, v) => acc.flatMap(_ => v.validate(...)) } // 2. mutators.foldLeft(Right(state)) { (acc, m) => acc.flatMap(s => m.transit(..., s, ...)) } ``` ### Default Rule Registries All built-in rules are collected in two registries: - **`DefaultValidators.all`** — 26 validators in a `SortedSet` (ordered by name) - **`DefaultMutators.all`** — 2 mutators: `PlutusScriptsTransactionMutator`, `StakeCertificatesMutator` **`CardanoMutator`** is the top-level entry point that runs all default rules: ```scala object CardanoMutator extends STS.Mutator { override def transit(context: Context, state: State, event: Event): Result = STS.Mutator.transit(DefaultValidators.all, DefaultMutators.all, context, state, event) } ``` ## Rule Customization Both `Emulator` and `ImmutableEmulator` accept `validators` and `mutators` as constructor parameters, making it straightforward to enable, disable, or extend rules. ```scala class Emulator( initialUtxos: Utxos = Map.empty, initialContext: Context = Context.testMainnet(), val validators: Iterable[STS.Validator] = Emulator.defaultValidators, val mutators: Iterable[STS.Mutator] = Emulator.defaultMutators ) ``` ### Disable all validators Skip all Phase-1 checks for fast iteration during development: ```scala val emulator = Emulator( initialUtxos = utxos, validators = Set.empty, mutators = Emulator.defaultMutators ) ``` ### Enable only specific validators Run a focused subset for targeted testing: ```scala import scalus.cardano.ledger.rules.* // Only check that inputs exist in the UTxO set val emulator = Emulator( initialUtxos = utxos, validators = Set(AllInputsMustBeInUtxoValidator), mutators = Emulator.defaultMutators ) ``` ### Script execution only Keep only the Plutus script mutator — useful when you want to test script logic without other ledger checks: ```scala val emulator = Emulator( initialUtxos = utxos, mutators = Set(PlutusScriptsTransactionMutator) ) ``` ### Remove a specific rule Filter out a rule while keeping everything else: ```scala val emulator = Emulator( initialUtxos = utxos, validators = DefaultValidators.all.filterNot(_ == FeesOkValidator), mutators = DefaultMutators.all ) ``` ### Add a custom rule Extend the defaults with your own validator: ```scala val emulator = Emulator( initialUtxos = utxos, validators = DefaultValidators.all ++ Set(MyCustomValidator), mutators = DefaultMutators.all ) ``` ### ImmutableEmulator The same mechanism works with `ImmutableEmulator` for functional state-threading patterns: ```scala val emulator = ImmutableEmulator( state = State(utxos = utxos), env = UtxoEnv.testMainnet(), validators = Set.empty, mutators = Set(PlutusScriptsTransactionMutator) ) ``` ## Writing Custom Rules Implement `STS.Validator` for read-only checks: ```scala import scalus.cardano.ledger.rules.* object MyCustomValidator extends STS.Validator { override type Error = TransactionException override def validate(context: Context, state: State, event: Event): Result = { if someCondition(event) then success else failure(new TransactionException("Validation failed: ...")) } } ``` Implement `STS.Mutator` for state transitions: ```scala object MyCustomMutator extends STS.Mutator { override type Error = TransactionException override def transit(context: Context, state: State, event: Event): Result = { // Check preconditions, then return updated state val newState = state.copy(fees = state.fees + event.body.fee) success(newState) } } ``` You can also create validators from functions without defining an object: ```scala val myValidator = STS.Validator[TransactionException]( (context, state, event) => { if event.body.inputs.nonEmpty then Right(()) else Left(new TransactionException("No inputs")) }, validatorName = "MyInlineValidator" ) ``` ## See Also - [Ledger Rules Reference](/docs/ledger/ledger-rules) — Complete catalog of built-in validators and mutators - [Emulator](/docs/testing/emulator) — In-memory node using these rules for fast testing - [Yaci DevKit](/docs/testing/local-devnet) — Docker-based devnet with real Cardano node - [Protocol Parameters](/docs/ledger/protocol-parameters) — Network configuration used by validators --- Source: https://scalus.org/docs/catalyst/scalus-1100252 --- # Scalus - multiplatform Scala implementation of Cardano Plutus - Catalyst Project Link: https://projectcatalyst.io/funds/11/cardano-open-developers/scalus-multiplatform-scala-implementation-of-cardano-plutus - Project Number: 1100252 - Project Manager: Alexander Nemish - Date Project Started: 2024-03-11 - Date Project Completed: 2025-10-14 ## Project Description [Scalus](https://scalus.org) is a Scala implementation of Cardano Plutus, PlutusTx, including UPLC evaluation machine and built‑ins for V1 and V2. It allows developers to write Cardano smart contracts using [Scala 3](https://scala-lang.org/) programming language. Scalus leverages Scala compiler plugin system and macros to convert Scala code to Cardano Untyped Plutus Core (UPLC), similarly to how Plutus converts Haskell to UPLC. It also provides a full‑featured UPLC parser, pretty‑printer, and an evaluation engine that can run on multiple platforms: JVM (Java, Kotlin, etc.), JavaScript (Node.js, browsers), and even Native ( C/C++/Rust, etc.) via LLVM backend. ## Close‑out Video ## Project KPIs • How We Addressed Them | Project KPI | Outcome | |-----------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------| | **Scala to UPLC compiler** | Huge amount of unit tests, Scalus Standard library code and real smart contracts like Hydrozoa, Binocular, Cosmex and AdaStream | | **Full Plutus V1/V2 CEK Coverage** | 100 % of V1 & V2 built‑ins + cost models implemented; V3 core delivered ahead of schedule | | **Cross‑Platform Reach**
Make tooling available beyond Haskell ecosystem | One codebase ships as JVM JAR, JavaScript ESM/NPM package, and Native C‑ABI library (Key Achievements #2) | | **Script Budget Accuracy** | Verified against 100k mainnet scripts with exact budget match | | **Packaging & Documentation** | 12+ tagged releases to Maven Central & NPM, Scalus Starter Project, full API docs, site with documentation and tutorials | | **Community Uptake** | 100 ★ on GitHub, >50 Discord members, 18 contributors; integrations with leading JS SDKs (Key Achievements #4 & #5) | | **Ecosystem Collaboration**
Demonstrate real‑world adoption | Library already powers **Binocular** oracle & **Hydrozoa** mini‑Hydra; Lucid Evolution & Mesh.JS integrations underway (Key Achievements #4) | --- ## Key Achievements 1. **Scala to UPLC Compiler** – Developed Scala 3 compiler backend to translate Scala code to UPLC, enabling writing Plutus contracts in Scala.
[UPLC CAPE benchmarks](https://intersectmbo.github.io/UPLC-CAPE/benchmarks/fibonacci_naive_recursion.html) show Scalus‑generated UPLC is on par with and even better than Plinth/Aiken/Plutarch. 2. **Complete CEK & Built‑ins** – Implemented Plutus V1 & V2 virtual machines, built‑ins, cost models, UPLC parser & pretty‑printer entirely in Scala 3. 3. **Triple‑Target Build** – Single codebase compiles to JVM (JAR), JavaScript (NPM/ESM via Scala.js) and Native shared library (Scala Native + LLVM). 4. **Deterministic Budget Validation** – Replayed ≈100,000 mainnet scripts; Scalus results matched the reference node *byte‑for‑byte* for both outcome and budget. 5. **Ecosystem Integration** – Scalus already underpins the **Binocular** optimistic Bitcoin oracle and the [**Hydrozoa**](https://github.com/cardano-hydrozoa/hydrozoa) improved Hydra‑like state channels protocol prototype; active work to embed the JS library into **Lucid Evolution** and **Mesh.JS** SDKs. 6. **On‑boarding & Community** – Published Scalus Starter Project, full API site, Discord support, CI‑driven release flow to Maven Central & NPM; project has ~100 GitHub stars and several external contributors. 7. **V3 Support** – Delivered core UPLC V3 support ahead of schedule, including CEK updates and built‑ins. These milestones collectively make Scalus the only end‑to‑end, full-stack, multiplatform Cardano DApp development platform. --- ## Key Learnings * A rigorously limited, platform‑agnostic subset of Scala 3 can faithfully mirror Plutus semantics while maintaining readable, idiomatic code. * Cross‑compiling to three targets from one codebase is achievable with careful boundary abstractions and continuous integration testing. * Early usage of Scalus for full-fledged DApps (Binocular, Cosmex, AdaStream, Hydrozoa) surfaced edge‑cases sooner and materially improved library robustness. --- ## Next Steps * Plutus V4 support, including new built‑ins and cost models. * Improved developer experience and documentation, including more tutorials and examples. * Explore funding for formal verification of the CEK implementation using [Stainless](https://epfl-lara.github.io/stainless/intro.html). --- ## Final Thoughts Catalyst funding transformed an experimental prototype into production‑ready infrastructure adopted by multiple projects in less than 15 months. We will continue to drive Scalus forward as the de‑facto full‑stack platform for Cardano DApps. --- ## Resources * GitHub • [https://github.com/scalus3/scalus](https://github.com/scalus3/scalus) * Docs • [https://scalus.org/api/index.html](https://scalus.org/api/index.html) * Starter • [https://github.com/lantr-io/scalus-starter](https://github.com/lantr-io/scalus-starter) * Discord • [https://discord.gg/ygwtuBybsy](https://discord.gg/ygwtuBybsy) * Multiplatform evaluation & budgeting demo: [https://youtu.be/A8MQsGn5XFo](https://youtu.be/A8MQsGn5XFo) --- Source: https://scalus.org/docs/catalyst/multiplatform-1100198 --- # Multiplatform Plutus Script Cost & Evaluation Library (JS/JVM/LLVM) Catalyst Project Link: https://projectcatalyst.io/funds/11/cardano-open-developers/multiplatform-plutus-script-cost-and-evaluation-library-jsjvmllvm - Project Number: 1100198 - Project Manager: Alexander Nemish - Date Project Started: 2024-03-11 - Date Project Completed: 2025-06-23 ## Project Description At the moment of project initiation, there was no library to evaluate Plutus scripts and its execution budget except Haskell implementation. The goal of this project was to create a multiplatform library for evaluating Plutus scripts and calculating their execution budget on JVM, JavaScript, and Native platforms. The library is written in Scala 3 and compiles to all three platforms, providing a unified codebase that significantly reduces code duplication and enhances developer productivity. ## Close-out Video There I demonstrate the Plutus V2 script evaluation and execution cost calculation on 3 platforms: JVM, JavaScript, and Native. ## List of Challenge/Project KPIs and How the Project Addressed Them The key performance indicators (KPIs) for this project were: 1. **Plutus Script Evaluation:** The library must accurately evaluate Plutus V1 and V2 scripts, including built-in functions and data models. 2. **Execution Budget Calculation:** The library must calculate the execution budget for Plutus scripts, ensuring compatibility with Cardano's cost model. 3. **Testing and Validation:** The library must pass all Plutus V1 and V2 conformance tests and be tested against a substantial number of Mainnet scripts to ensure reliability and accuracy. 4. **Cross-platform Compatibility:** The library must compile and run on JVM, JavaScript, and Native platforms 5. **Publish Platform Artifacts:** The library must be published as platform artifacts for JVM and JavaScript platforms, ensuring easy integration into existing projects. We successfully addressed these KPIs through the following achievements: 1. Robust Scala 3 implementation of Plutus V1/V2 CEK virtual machine. 2. Complete implementation of Plutus V1/V2 built-in functions and data model. 3. Comprehensive Plutus V1/V2 cost models and execution budget calculation logic. 4. UPLC parser and pretty-printer implementation. 5. Successful compilation to JavaScript using [Scala.js](https://www.scala-js.org/). 6. Successful compilation to native binaries via [Scala Native](https://scala-native.org/en/stable/) and [LLVM](https://llvm.org/). 7. Full compatibility and availability for JVM languages, including Java and Kotlin. 8. Extensive testing using property-based testing with [ScalaCheck](https://www.scalacheck.org/) and [ScalaTest](https://www.scalatest.org/), and comparing results with the Haskell `uplc` CLI tool to ensure we are getting the same results as the reference implementation. 9. Passing all Plutus V1 and V2 [conformance tests](https://github.com/scalus3/scalus/blob/a87f25a91e58311cfac341ef893dc5ce1af0e571/scalus-core/shared/src/test/scala/scalus/uplc/eval/PlutusConformanceTest.scala#L17) 10. Extensive testing on \~100k mainnet Plutus scripts, ensuring high accuracy and reliability. 11. Publishing platform artifacts on [Maven Central](https://central.sonatype.com/) for JVM, and [NPM](https://www.npmjs.com/) for JavaScript platforms, making the library easily accessible for developers. 12. Integration into the BloxBean [Cardano Client Library (CCL)](https://cardano-client.dev/docs/integrations/scalus-integration-api) - a JVM transaction building library as a **TransactionEvaluator** implementation. This integration allows developers to calculate the exact execution budget and fees during Transaction construction, eliminating the need for querying an external service and speed up transaction building. ## Key Achievements ### Cross-platform Plutus Execution Provided robust implementations of Plutus V1/V2 Virtual Machine across JVM, JavaScript, and Native platforms from the same codebase. ### Developer Efficiency Enabled significant code reuse through multiplatform Scala 3 compilation, enhancing developer productivity. ### Accuracy and Reliability Passed all Plutus V1 and V2 [Plutus Conformance](https://github.com/scalus3/scalus/blob/a87f25a91e58311cfac341ef893dc5ce1af0e571/scalus-core/shared/src/test/scala/scalus/uplc/eval/PlutusConformanceTest.scala#L17), demonstrating compatibility with the reference Haskell implementation. Implemented thorough testing against \~100k Cardano mainnet scripts to ensure correctness and reliability. ### Adoption Scalus ability to calculate Plutus script budgets and evaluate scripts has been integrated into BloxBean [Cardano Client Library (CCL)](https://cardano-client.dev/docs/integrations/scalus-integration-api) and is used in production by Cardano Foundation. Current applications and protocols using Scalus include: * **[Hydrozoa Protocol](https://github.com/cardano-hydrozoa/hydrozoa/)** A simplified and modern Hydra implementation. * **[Binocular](https://github.com/lantr-io/binocular/)** A decentralized optimistic Bitcoin oracle on Cardano. * **[Cosmex](https://cosmex.io)** A specialized L2 off-chain order book exchange protocol. ## Key Learnings Compiling Scala 3 to JVM, JavaScript, and Native from a unified codebase is achievable and highly beneficial, significantly improving maintainability and reducing code duplication. Ensuring platform agnosticism requires deliberate planning but is feasible with careful architectural considerations. Overall, Scala 3's multiplatform capabilities have proven to be a powerful tool for building cross-platform libraries, enabling us to deliver a robust and efficient solution for Plutus script evaluation and budget calculation across multiple platforms. ## Next Steps for the Product/Service Developed Ongoing development of Scalus includes the recent implementation of Plutus V3 CEK machine and Conway era built-in functions on JVM and JavaScript platforms. Future integration plans involve Scalus JS library integration into [Lucid Evolution](https://github.com/no-witness-labs/lucid-evolution) and [Mesh.JS](https://meshjs.dev/) for enhanced script budget evaluation capabilities. ## Final Thoughts/Comments Catalyst funding significantly accelerated our project's development, providing essential resources that led to substantial adoption and validation. Completion of this project makes Scalus the *only* end-to-end multiplatform and well tested Plutus execution library available today. We remain committed to enhancing Scalus further as the leading platform for full-stack Cardano DApp development. ## Links to Other Relevant Project Sources or Documents * [Scalus GitHub Repository](https://github.com/scalus3/scalus) * [Scalus Website](https://scalus.org) * [Scalus API Reference](https://scalus.org/api/index.html) * [Discord Community](https://discord.gg/ygwtuBybsy) * [Scalus Starter Project](https://github.com/lantr-io/scalus-starter) * [Lantr](https://lantr.io) * [BloxBean Cardano Client Library](https://cardano-client.dev) --- Source: https://scalus.org/docs/catalyst/txbuilder-1300009 --- # Scalus Multiplatform Tx Builder API - Catalyst Project Link: https://projectcatalyst.io/funds/13/cardano-open-developers/lantr-scalus-multiplatform-tx-builder-same-code-for-frontandbackend - Project Number: 1300009 - Project Manager: Alexander Nemish - Date Project Started: 2025-01-20 - Date Project Completed: 2025-12-23 ## Project Description Scalus Multiplatform Tx Builder API allows constructing Cardano transactions on both JVM and JavaScript platforms from a single Scala 3 codebase. This frees developers from implementing the same transaction building logic twice — once for server-side (JVM) and once for client-side (browser/Node.js) applications. The project delivers a TxBuilder API following best practices from MeshJS, Lucid Evolution, and Cardano Client Lib, compiled via Scala.js to JavaScript and published as an NPM package with full TypeScript definitions. ## Close-out Video [Video demonstration of TxBuilder API on JavaScript platform](https://www.youtube.com/watch?v=yoLFbIcs3oU) The video demonstrates that mainnet transactions can be deserialized and serialized back on the JavaScript platform, and all types of transactions can be constructed and successfully submitted to a real Cardano node using Yaci DevKit. ## Project KPIs | Project KPI | Outcome | |------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------| | **Cross-Platform Code Reuse** — Single codebase for JVM and JS | One Scala 3 codebase compiles to JVM JAR (Maven Central) and JavaScript ESM/NPM package | | **Transaction Type Coverage** — Support all Conway era transactions | 100% of Conway CDDL transaction types implemented with CBOR serialization | | **Library Integration** — Integrate with existing JS/JVM libraries | Full integration with Lucid Evolution for wallet management, key derivation, and transaction signing | | **Transaction Validation** — Verify against real mainnet transactions | 100,000+ mainnet transactions successfully deserialized and re-serialized with byte-by-byte match | | **Script Budget Accuracy** — Match cardano-node execution metrics | Plutus script evaluation with exact budget match verified against cardano-node; 130+ conformance tests passed | | **NPM Package Publication** — Publish usable JavaScript package | Published scalus package v0.14.0 on NPM with TypeScript definitions | | **Documentation** — Comprehensive API documentation | ScalaDoc for all public APIs, TypeScript type definitions, README with usage examples | ## Key Achievements 1. **Single-Codebase Multi-Platform Build** — One Scala 3 codebase compiles to both JVM (JAR for Maven Central) and JavaScript (ESM bundle for NPM) via Scala.js, enabling true code sharing between server and client applications. 2. **100% Conway Transaction Coverage** — Full implementation of all Conway era transaction types defined in the CDDL specification, with comprehensive CBOR serialization support and 63+ property-based roundtrip tests. 3. **Lucid Evolution Integration** — Complete integration with Anastasia Labs' Lucid Evolution TypeScript library for wallet management, mnemonic-based key derivation, and transaction signing on the JavaScript platform. 4. **Plutus Script Evaluation on JavaScript** — Fee and script budget calculation working natively in JavaScript, validated against mainnet execution metrics and passing 130+ Plutus conformance tests. 5. **Transaction Balancing** — Full transaction balancing implementation including UTXO selection ( UtxoPool), change output calculation, and automatic fee balancing — all working cross-platform. 6. **Published NPM Package** — Released scalus v0.14.0 on NPM with optimized JavaScript bundle, TypeScript type definitions, and comprehensive README documentation. 7. **Comprehensive Integration Testing** — 9 transaction types (payment, minting, stake registration, stake delegation, DRep registration, vote delegation, proposal submission, voting, native script minting) successfully tested against a real Cardano node using Yaci DevKit. ## Key Learnings - Scala.js enables true code sharing between server (JVM) and client (browser/Node.js) platforms with minimal platform-specific code, making cross-platform DApp development practical. - Integration with existing JavaScript libraries (Lucid Evolution) provides a familiar developer experience while leveraging Scalus's type-safe transaction building. - Property-based testing with ScalaCheck catches edge cases in CBOR serialization that would be missed by example-based tests alone. - Early integration testing against real Cardano nodes (via Yaci DevKit) surfaces protocol-level issues before production deployment. ## Next Steps - **Plutus V4 Support** — Implement new V4 built-ins and cost models as they become available. - **Mesh.JS Integration** — Extend JavaScript ecosystem reach with Mesh.JS SDK integration. - **Enhanced Developer Experience** — More tutorials, examples, and documentation for transaction building patterns. ## Final Thoughts This project successfully delivered a truly cross-platform transaction building API for Cardano. By leveraging Scala 3 and Scala.js, developers can now write transaction building logic once and deploy it to both JVM servers and JavaScript clients without duplication. The integration with Lucid Evolution provides a familiar developer experience, while Scalus's type-safe approach catches errors at compile time rather than runtime. We look forward to continued adoption and feedback from the Cardano developer community. ## Resources - GitHub: [https://github.com/scalus3/scalus](https://github.com/scalus3/scalus) - NPM Package: [https://www.npmjs.com/package/scalus](https://www.npmjs.com/package/scalus) - Documentation: [https://scalus.org](https://scalus.org) - API Docs: [https://scalus.org/api/index.html](https://scalus.org/api/index.html) - Starter Project: [https://github.com/lantr-io/scalus-starter](https://github.com/lantr-io/scalus-starter) - Discord: [https://discord.gg/ygwtuBybsy](https://discord.gg/ygwtuBybsy) --- Source: https://scalus.org/docs/catalyst --- ## Catalyst This section covers Catalyst projects that were funded by the Cardano community. ### Fund 11 - Close-out report: [1100252 - Scalus - multiplatform Scala implementation of Cardano Plutus](/docs/catalyst/scalus-1100252) - Close-out report: [1100198 - Multiplatform Plutus Script Cost & Evaluation Library](/docs/catalyst/multiplatform-1100198) ### Fund 13 - Close-out report: [1300009 - Scalus: Multiplatform Tx Builder API](/docs/catalyst/txbuilder-1300009)